关键字:枚举、反射、Class类、反射、Constructor类、Field类、Method类、数组的反射、框架、类加载器、配置文件
枚举enum
定义一个类型,让该类的变量的取值只能是若干个固定值中的一个,否则编译报错,这个过程称为枚举。枚举可以让编译器在编译时就控制源程序中的非法值,普通变量无法实现。本质是限制了对象的创建。
普通类实现枚举:本类中,私有化构造函数,并限制了对象和引用变量的创建,将引用变量变成常量(对象类型)。外部类在使用本类时,引用变量只能赋值这些常量,不能指定规定以外的值,例如一星期的七天,3色的交通灯。
普通类实现枚举的思路:
1.
2.
3.
通过点击左边框里的错误图标,可以直接创建新的类、接口等,非常便捷。
1.
2.
3.
4.
访问修饰符:外部类:public和默认。内部类:public、private、protected和默认,和成员函数平级,访问修饰符也一样。
反射(Reflect)与Class类
作用在于,在不知道类的具体内容(API或源代码)的情况下(只有.class文件)操作类的内容,也就是在类被编译后,再对类进行操作,操作的内存里的字节码。例如,给出参数,选择一个构造函数创造对象,这就需要调用反射来获得类的构造函数;获得对象中指定的变量的值;有对象和参数,调用某个方法。正因为此,需要抛出异常,因为这些成员可能没有。
JDK1.2出现,不是新特性。
Class类《——》java类
Constructor类《——》构造函数
Field类《——》成员变量
Method类《——》成员函数
Array《——》处理数组
Class类(java.lang包)
java类用于描述事物的特性。java程序中的任何java类和接口也属于同一种事物,描述这类事物使用Class类。对象——》java类和接口——》Class类。枚举是类,注释是接口。
通过Class中的各种方法,可以获得java类的信息,例如,类名、包名、父类等。但是没有构造函数,Class的实例对象就是java类在内存里的字节码,具有唯一性。例如,Class ch = Data.class;,对象为Data类在内存的字节码。.class文件不是字节码,只有被类加载器加载入内存后,内存里的二进制数据才是字节码。
通过forName()获得java类的字节码,例如,Class.forName(“java.lang.String”);获得String在内存的字节码。forName()获得字节码的情况有两种:
1. 如果该类已经在虚拟机,无需加载。直接找到字节码返回即可。
2. 如果该类不在虚拟机,使用类加载器,将类加载入虚拟机,然后从虚拟机中获得字节码。
java类的对象调用getClass()方法,获得对应的java类。例如,stud.getClass();。
获得该字节码对应的Class类对象的3种方式:
1. 类名.class,例如,System.class
2. 对象.getClass(),例如,new Data.getClass()//获得该对象的类的Class类对象
3. Class.forName(“类名’),静态方法,例如,Class.forName(“java.lang.String”);反射中使用比较多,类名也可以是变量,不用提前知道java类的名字。需要处理异常,可能类没有加载,但没有使用类加载器ClassLoader
通过上面三种方法都可以获得字节码,一个类在内存中只有唯一的一份字节码。
8个基本类型也有对应的Class对象。void也有对应的Class对象,例如,Class ch = void.class;。所以共有9个预定义的Class对象。
isPrimitive(),是否是基本数据类型。基本类型获得字节码,例如,int.class()。注意,基本类型的字节码和包装类的字节码不一样,例如,int.class 和Integer.class对应的字节码不一样。但是,Integer.TYPE表示包装类内部基本数据类型的字节码,所以int.class 和Integer.TYPE是一致的。9个预定义类型都是如此。
基本数据类型组成的数组,也有对应的字节码,是Class的实例对象,但不是基本类型。是数组类型,可以用isArray()判断是否是数组类型。
将鼠标放在错误图标上,可以看到错误原因。
总之,只要是在源程序中出现的类型,都有各自对应的Class实例对象。例如,数组等。
反射
反射就是将java类中的各种成分都解析成对应的java类(解构主义),例如,函数用Method类描述、包用Package、成员变量用Field、构造函数用Constructor。java类,纵向形成对象,横向的各种成分形成对应的java类。
具体的一个java类中的成分,用反射类的对象表示,例如,System类中有许多函数,exits()、out.println(),用Method类下不同的对象obj1、obj2表示,一个Method对象对应一个System类的函数。
一个类中的所有成员都可以用相应的反射API类的一个实例对象表示,通过调用Class类的方法获得这些对象,并对其进行操作。
Constructor类
描述类中的构造方法。通过调用Class中的方法获得Constructor对象。与java类相关。
获得构造函数:Constructor[] getConstructors();返回的是构造方法的数组。
Constructor<T> getConstructor(Class… type),传入想要的构造函数中的参数类型的Class类对象,获得该参数对应的一个构造函数。使用了可变参数,满足不同的参数需求。使用泛型,可以指定构造函数对象构造的类型。
用getConstructor返回的构造函数对象可以创建对应类的实例对象,使用newInstance()方法,例如
//获得String类的构造函数,该StringBuffer表示选择哪个构造方法。
Constructor con = String.class.getConstructor(StringBuffer.class);
//必须传入指定的参数(StringBuffer对象),而且需要转换类型。
String str =(String)con.newInstance(new StringBuffer("kkk"));
注意:newInstance()返回的是泛型,需要在Constructor处指定类型或者创建号对象后类型转换,例如,Constructor<String> con = String.class.getConstructor(StringBuffer.class);
编译时和运行时对程序的处理不同:编译只负责检查语法错误,并不执行等号右边的语句,也不明确变量的类型,所以需要类型转换或者指定泛型。树立编译和运行的两阶段思想。
需要注意,获得构造函数和用构造函数创建对象,内部使用的参数需要统一,不能上面用StringBuffer,下面用String,一种构造函数只能使用一种参数。
通过反射创建实例对象的步骤:
1、class字节码—》Constructor构造函数对象—》newInstance()创建对象。
2、class字节码—》(Class类的)newInstance()创建对象。
Class类的newInstance()方法,用于创建空参数的实例对象。底层仍然是调用Constructor类的newInstance()方法,使用缓冲机制保存实例对象,等到使用时提供出去,非常占用资源。
String str1 = String.class.newInstance();//已经知道类型,无需转换。
String str2 = (String)Class.forName("java.lang.String").newInstance();//需要转换类型。
Field类
代表java类中的成员变量。
右键—》Source—》使用构造函数目标创建构造函数。
ReflectPoint rf = new ReflectPoint(3,6);
Field fy = ReflectPoint.class.getField("y");//fy是ReflectPoint类的共有的成员变量y
System.out.println(fy.getInt(rf));//想要获得fy对应值,需要指定具体的ReflectPoint对象。
获得变量:Class类中的getField()只能获得共有的变量,getFields()返回变量的数组,getDeclaredField(),返回所有的变量,包括私有。
Field类中的get()方法获得变量对应的值,返回Object类型,需要转换。对于私有变量,还需要设置“暴力反射”才能获得对应值,例如,fx.setAccessible(true);。也有一些可以获得基本类型的值的方法,但是必须预先知道变量的类型才能使用,例如,getInt()。总之,值是和对象有关的。
字节码之间的比较应该使用==,能够确定是否是同一份字节码,equals会比较内容,不够严谨。
set(Object
getType(),获得某个变量的类型,注意返回Class对象。
getName(),获得某个变量的变量名。
变量的组成:类型、变量名和值,对应的获得方法:getType()、getName()和get()。这些方法都需要抛出异常。
Method类
代表类中成员方法。与java类相关,与对象无关。先获得方法,再用对象调用。
需要导包:import java.lang.reflect.*;。
获得java类中方法:使用Class类中的方法,
1、getMethod(String
2、getMethods(),获得方法组成的数组,Method[]。
对象调用方法:invoke(Object
JDK1.4时没有可变参数,传入数组,例如,Character ch = (Character)meCharAt.invoke(s1, new Object[]{2});。将数组打开,获得里面的元素作为参数,这个数组可以放入各种对象,例如,Integer、String等,使用多态。JDK1.5兼容了1.4的方法,会自动拆包数组。
专家模式:谁拥有数据,谁就可以调用方法。
用反射执行main主函数
可以直接用类调用主函数,例如,TestArguments.main(new String[]{});。写程序时,并不知道需要调用哪个类的main函数,等程序运行后在告诉调用哪个类的main,相当于主函数传入参数。先调用类,即使这个类没有被写好,只要运行时写好就可以。
需要给本类传入参数,右键—》Run As—》Arguments—》Program arguments,将外面类放入,即主函数传入参数。
String classname = args[0];
Method mainMethod = Class.forName(classname).getMethod("main", String[].class);
mainMethod.invoke(null,(Object)new String[]{"kk","qq"});//
JDK1.5为了兼容JDK1.4,如果传入一个数组,会默认为Object数组,自动拆包,将数组中的元素作为参数。将上例的数组拆包成2个字符串对象。为了防止这种情况,处理方法有二种:
1、将数组变成一个Object数组对象,例如,new Object[]{(new String[]{"kk","qq"})}。因为数组也属于Object类的子类。
2、转换数组的类型,例如,(Object)new String[]{"kk","qq"}。
数组的反射
相同的维度和数据类型的数组,其Class对象一样。
String getName(),返回类名,即该Class对象对应的类、接口、数组类、基本类型的名称。如果是数组,会按照数组给出简写字母。例如,int[]数组的Class对象的名字是[I,[表示数组,I表示int类型。
int[] a1 = new int[]{2,5,7};
String[] a4 = new String[]{"d","j","f"};
Class getSuperclass(),获得该Class对象的父类的Class对象。
所有一维数组的父类是Object,二维数组可以看做Object[]。也就是说,整个一维数组、二维数组中的数组都可以看做Object类型。例如,a1、a3的元素、a4的元素都是Object。另外,引用数据的数组既可以当做Object,也可以当做Object[]。
Arrays的asList()方法在处理基本类型数组(int[])和引用类型数组(String[])时的差异:
可以将引用数据的数组变成集合,数组中的元素变成集合中的元素;会将整个基本数据的数组变成一个Object对象。因为JDK1.4中,asList()接受的是数组Object[];JDK1.5中,asList()接受的是可变参数T…a,T可以是基本型或引用型。例如,a4按照1.4处理,直接打印;a1按照1.5处理,打印出哈希值。想要打印基本数据的数组,可以逐个元素录入,或者
使用反射中的Array工具类对数组进行反射操作。Class中拥有Arrays没有的方法,例如判断对象所属的类是否是数组等,再用Array中的方法操作数组。这些方法基本是静态方法,需要将数组对象传入。例如,打印对象,类似拆包
public static void printObject(Object obj){
}
目前还无法通过数组中的元素获得数组的类型,因为基本数据无法使用getClass()方法,引用数据可以使用该方法。因为getClass()方法属于Object类。通过getClass().getName()获得该元素的类型。考虑到多态,无法确定数组的类型。
HashSet,存入的也是对象的引用。先判断该对象是否存在,如果已经存在就不放入;不存在的话,再通过哈希值排序后存入。要替换已有的相同对象,必须先删除原有的对象,再存入新对象。
按照hashCode()和equals()方法进行比较。将HashSet中的元素按照哈希值分成若干区域,先将对象按照哈希值放入对应的区域,再在区域内使用哈希值和equals()排序。这种算法只在HashSet集合中才有效。只有哈希值和equals()都相等,才能是同一个元素。在其他集合中,不需要按照哈希值排序。
当一个对象被存入HashSet集合后,就不能修改对象中参与计算哈希值的变量了。对于元素的操作,都是在区域中完成,如果元素的哈希值变化,会进入其他区域,也就无法操作,例如remove()、contains()。
内存泄露:对于某些不用的数据,虽然代码上已经被释放,但实际内存里并没有释放,一直在运行。或者调用了系统底层的资源,但是没有关闭资源。日积月累,内存会被消耗殆尽。例如,在HashSet集合中,由于修改了元素的哈希值,导致无法对元素进行操作,尽管代码上的确进行了操作。
反射的作用——实现框架功能
先建框架,然后再写类。运行时,框架调用这些类。
使用别人的类的方式有两种:
1、直接调用别人的类。
2、通过反射,让别人的类调用我的类。
自己的类是工具,别人的类是框架。通过工具类,使得框架合乎自己的需求。例如,框架是房子,我的类是门、窗户等配件。
框架解决的核心问题:写框架时,还不知道需要调用什么类,需要使用反射Class.forName(“类名”),等到类写好后,框架获得类名即可使用类、操作类的对象。
类的名字一般放在配置文件中。创建一个File文件config.properties作为配置文件,设置className=类名,需要时可以通过更改配置文件,将框架改造成自己需要的样子。配置文件放在工程目录下。举例,写个小框架
InputStream ips = new FileInputStream("config.properties");
Properties props = new Properties();
props.load(ips);
ips.close();
String classname = props.getProperty("className");
Collection collections = (Collection)Class.forName(classname).newInstance();
通过配置文件创建想要的对象,更改classname对应的类,即可获得其他想要的对象,例如,HashSet对象。
类加载器
得到配置文件的方式:
1、一定要使用完整的路径,但完整的路径不是固定的,是利用getRealPath()方法运算获得。项目的路径不固定,但配置文件在项目内的路径是固定的,例如,金山词霸\\配置文件,金山词霸安装位置不固定,但配置文件的目录是固定的。
这种方式还可以保存对配置文件的更改,使用更广泛。
2、类加载器(ClassLoader)会将.class文件和配置文件都加载进内存,所以可以使用类加载器加载指定的配置文件。编写代码时,将配置文件放在classpath目录下,Eclipse放在src目录或子目录下;编译后,Eclipse会自动将所有源代码编译后的.class文件存放到classpath目录或bin目录下,并将非.class文件原样复制到这些目录下。例如,
InputStream ips =
InputStream ips = ReflectTest2.class.getResourceAsStream("config.properties");