Java,反射机制与反射的应用
关于反射:
有时对象的编译时类型和运行时类型是不一致的。比如在使用多态的场景下,有一个Object类型的数组,其中的元素有着各种不同的类型,而调用相应的元素的方法时,比如调用toString方法时,希望调用的是各个元素相对应的类型的toString方法。如果用instanceof来创建if判断再进行相应的强转再进行调用,当类型太多时,就太繁琐,也易出错。这时便要用到反射机制。
反射机制:
反射(Reflection)是被视为动态语言的关键,反射机制使得程序在运行期间借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性和方法。
加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象,一个类只有一个Class对象,这个Class对象包含了完整的类的结构信息。可以通过这个Class对象看到类的结构。
关于Class类的理解:
针对于编写好的.java源文件进行编译(使用javac.exe指令),会生成一个或多个.class字节码文件。接着,我们使用java.exe指令对指定的.class文件进行解释运行。这个解释运行的过程中,我们需要将.class字节码文件加载(使用类的加载器加载到内存中的方法区)到内存中。加载到内存中的结构即为Class的一个实例。也称为运行时类。只要类型和维度一致,就是同一个Class的的实例。
Class看作是反射的源头。
获取Class实例的方式:(若A、B、C、D分别是某个类)
方式①:调用运行时类的静态属性 class
Class classA = A.class;
方式②:调用运行时类的对象的getClass( )方法
(若bb是B类的实例)
Class classB = bb.getClass( );
方式③:调用Class的静态方法forName(String className)
Class classC = Class.forName(C的全类名); (全类名是指定了位于哪个包下的信息的类名)
方式④:使用类的加载器的方式
Class classD = ClassLoader.getSystemClassLoader( ).loadClass(D的全类名);
类的加载过程:
过程①:类的装载(Loading)
将类的class文件读入内存,并为其创建一个java.lang.Class对象,此过程具体有由类的加载器完成。
过程②:链接(Linking)
验证(Verify):确保加载的类的信息符合jvm规范。比如,以cafebabe开头,无安全方面问题。
准备(Prepare):正式为类变量(静态变量)分配内存并设置类变量默认初始化值的阶段,这些内存都将在方法区中进行分配。
解析(Resolve):虚拟机中常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
过程③:初始化(Initialization)
-
执行类构造器<clinit>( )方法的过程。
类构造器<clinit>( )方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(即完成静态变量的显示赋值和静态代码块中赋值的操作)
-
当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
类的加载器(以jdk8)为例:
作用:负责类的加载,并对应于一个Class的实例。
分类(分为两种):
①BootstrapClassLoader:引导类加载器、启动类加载器。
使用c/c++的代码写的,不能通过Java代码获取其实例。
负责加载Java的核心库。
②继承于ClassLoader的类加载器:
ExtensionClassLoader:扩展类加载器。
SystemClassLoader/ApplicationClassLoader:系统类加载器、应用程序类加载器。(自定义的类默认使用的类的加载器)
用户自定义类的加载器:实现应用的隔离(使得同一个类在一个应用程序中可以加载多份),数据的加密。
关于反射的应用:
应用①:创建运行时类的对象
创建运行时类的对象的操作:
通过Class的实例调用newInstance( )方法即可
创建运行时类的要求:
①运行时类中必须提供一个空参构造器。
②要求符合提供的空参的构造器的权限
JavaBean中要求类中要有一个空参构造器。一是因为子类对象在实例化时,子类的构造器默认调用父类的空参构造器,防止调用出错(也可以指明调用父类的有参构造器)。二是在反射中,创建运行时类的对象时,各个运行时类都提供一个空参构造器,便于编写通用的创建运行时类的代码。
newInstance( )在jdk9中标识为过时的,替换为通过Constructor类调用newInstance(可变形参)。
应用②:获取运行时类的内部结构
获取的结构:所有的属性、方法、构造器。父类、接口,包、带泛型的父类、父类的泛型 等。
注:获取父类的泛型的方式的代码为:
Class c1 = Class.forName("反射.类的加载器.Person");
//获取带泛型的父类(Type是一个接口,Class类实现了此接口)
Type superclass = c1.getGenericSuperclass();
//如果父类是带泛型的,则可以强转为ParameterizedType类型的
ParameterizedType parameterizedType = (ParameterizedType) superclass;
//调用getActualTypeArguments()获取泛型参数,结果是一个数组,因为可能有多个泛型参数。
Type[] arguments = parameterizedType.getActualTypeArguments();
//获取泛型参数的名称
for (int i = 0; i < arguments.length; i++)
{
System.out.println(((Class)arguments[i]).getName());
}
应用③:调用指定的属性、方法、构造器
一、调用属性
调用指定属性的步骤:
-
调用public的属性(非静态):
① 通过类的Class的实例(即运行时类)调用getField(String name)方法,传入要调用的属性名。返回值即为运行时类的属性的Field实例。
② 获取属性:再用Field实例调用其公有的属性的get(Object obj)方法,参数传入相应类的实例对象,返回值即为指定的属性。/操作属性:调用Field实例的set(Object obj,Object value),参数一传入要修改的此类的对象,参数二传入要对此属性修改的内容value,即可将对象的相应属性修改为value的值。
-
调用其他的不符合其访问权限的属性(非静态):
① 通过类的Class的实例(即运行时类)调用getDeclaredField(String name)方法,传入要调用的属性名,返回值即为其运行时类的属性的Field实例(只要在相应类中声明过的属性都可以获取)。
② 用Field实例调用其setAccessiable(boolean flag)方法,传入true,即可以表示在权限外访问此属性。如果不调用其setAccessiable(boolean flag)方法,则默认为传入false,即不允许在权限外调用此属性。
③ 获取属性:再用Field实例调用其公有的属性的get(Object obj)方法,参数传入相应类的实例对象,返回值即为指定的属性。/操作属性:调用Field实例的set(Object obj,Object value),参数一传入要修改的相应类的实例对象,参数二传入要对此属性修改的内容value,即可将对象的相应属性修改为value的值。
操作代码的例子如下(此处异常已抛出到方法声明处,需处理异常):
注:Person类的age属性是用public权限修饰,String属性是用private权限修饰。
//创建类Person的Class实例
Class cc = Person.class;
//通过运行时类调用newInstance()方法创建对象
Person pp = (Person) cc.newInstance();
//获取运行时类的名为age的属性的Field实例
Field ageField = cc.getField("age");
System.out.println(ageField.get(pp));
//将pp的age属性赋值为2
ageField.set(pp,2);
System.out.println(ageField.get(pp));
System.out.println();
//获取运行时类的名为name的属性的Field实例
Field nameField = cc.getDeclaredField("name");
//表明可以在权限外访问name属性
nameField.setAccessible(true);
//将pp对象的name属性赋值为Tom
nameField.set(pp,"Tom");
System.out.println(nameField.get(pp));
-
调用静态的属性
(以private权限修饰的静态属性为例)
① 通过类的Class的实例(即运行时类)调用getDeclaredField(String name)方法,传入要调用的属性名,返回值即为其运行时类的属性的Field实例(只要在相应类中声明过的属性都可以获取)。
② 用Field实例调用其setAccessiable(boolean flag)方法,传入true,即可以表示在权限外访问此属性。如果不调用其setAccessiable(boolean flag)方法,则默认为传入false,即不允许在权限外调用此属性。(①和②步骤与前面的调用其他的不符合其访问权限的属性(非静态)的步骤一致)
③ 获取属性:再调用Field实例的get(Object obj)方法,参数传入相应类的Class的实例,返回值即为指定的属性。/操作属性:调用Field实例的set(Object obj,Object value),参数一传入要修改的相应类的Class实例,参数二传入要对此属性修改的内容value,即可将此类的相应属性修改为value的值。
注:与调用其他的不符合其访问权限的属性(非静态)的步骤不同的是,由于静态属性(类变量)与类有关,并非与类的对象有关,所以调用时的获取和操作时的get和set方法中传入的参数应该为类的Class实例(运行时类),并非类的对象。
操作代码的例子如下(此处异常已抛出到方法声明处,需处理异常):
注:Person的静态属性info的声明为:private static String info;
//创建类Person的Class实例
Class cc = Person.class;
//获取info的属性的Field的实例
Field infoField = cc.getDeclaredField("info");
//表明可以在权限外访问info属性
infoField.setAccessible(true);
//在set和get方法中的参数是Person.class或cc
System.out.println(infoField.get(cc));
infoField.set(cc,"world");
System.out.println(infoField.get(cc));
二、调用方法
调用指定方法的步骤:
-
调用public的方法(非静态):
① 通过类的Class的实例(即运行时类)调用getMethod(String name,Class<?> ... parameterType)方法,先传入要调用的类的方法名,再传入该方法的参数的参数类型的Class实例(如:int.class,String.class)。获取运行时类的公有(public)的方法的Method实例。
② 调用方法:调用Method实例的invoke(Object obj,Object ... args)方法,先传入要调用方法的对象,再传入要调用的方法的实参参数值。invoke方法的返回值即为目标方法的返回值。
-
调用其他的不符合其访问权限的方法(非静态):
① 通过类的Class的实例(即运行时类)调用getDeclaredMethod(String name,Class<?> ... parameterType)方法,先传入要调用的类的方法名,再传入该方法的参数的参数类型的Class实例(如:int.class,String.class)。获取运行时类的方法的Method实例。
② 用Method实例调用其setAccessiable(boolean flag)方法,传入true,即可以表示在权限外访问此方法。如果不调用其setAccessiable(boolean flag)方法,则默认为传入false,即不允许在权限外调用此方法。
③ 调用方法:调用Method实例的的invoke(Object obj,Object ... args)方法,先传入要调用方法的对象,再传入要调用的方法的实参参数值。invoke方法的返回值即为Method实例对应的方法的返回值。如果Method实例对应的方法的返回值为void,则invoke方法的返回值为null。
-
调用静态的方法:
(以private权限修饰的静态方法为例)
与调用其他的不符合其访问权限的方法(非静态)不同的是,在③步骤中,调用Method实例的的invoke(Object obj,Object ... args)方法,先传入的是要调用方法的类的Class实例,再传入要调用的方法的实参参数值。
反射调用方法的操作代码的例子如下(此处异常已抛出到方法声明处,需处理异常):
//创建类Person的Class实例
Class cc = Person.class;
//通过运行时类调用newInstance()方法创建对象
Person pp = (Person) cc.newInstance();
//获取运行时类的名为age的属性的Field实例
Field ageField = cc.getField("age");
System.out.println(ageField.get(pp));
//将pp的age属性赋值为2
ageField.set(pp,2);
System.out.println(ageField.get(pp));
System.out.println();
//获取运行时类的名为name的属性的Field实例
Field nameField = cc.getDeclaredField("name");
//表明可以在权限外访问name属性
nameField.setAccessible(true);
//将pp对象的name属性赋值为Tom
nameField.set(pp,"Tom");
System.out.println(nameField.get(pp));
三、调用构造器
(以private权限修饰的构造器为例)
① 通过类的Class的实例(即运行时类)调用getDeclaredConstructor(Class<?> ... parameterType)方法,传入该构造器的参数的参数类型的Class实例(如:int.class,String.class)。获取运行时类的构造器的Constructor实例。
② 用Constructor实例调用其setAccessiable(boolean flag)方法,传入true,即可以表示在权限外访问此构造器。如果不调用其setAccessiable(boolean flag)方法,则默认为传入false,即不允许在权限外调用此构造器。
③调用Constructor实例的newInstance(Object ... initargs)方法,参数传入构造器中的参数的实参参数值,返回值即为此类的对象(Object类型)。
注:可以用Constructor实例调用newInstance的方式创建对象来替换原来的用Class实例调用newInstance的方式。
调用构造器操作代码如下:
//创建Person的Class实例
Class cc = Person.class;
//调用getDeclaredConstructor的方法获取相应构造器的Constructor实例
Constructor ccDeclaredConstructor = cc.getDeclaredConstructor(int.class, String.class);
//表明可以在权限外使用此构造器
ccDeclaredConstructor.setAccessible(true);
//调用Constructor实例的newInstance方法创建对象。
Person pp = (Person) ccDeclaredConstructor.newInstance(19, "liergou");
System.out.println(pp);
应用④:获取指定的注解
获取类声明上的注解:
用类的Class实例调用getDeclaredAnnotation(Class annotation)方法,参数传入注解的Class实例,返回值即为相应的注解(Annotation类型的,要进行具体操作,需要强转为具体的注解类型)。
获取属性声明上的注解:
先调用getDeclaredField方法获取类的属性的Field实例,调用getDeclaredAnnotation(Class annotation)方法,参数传入注解的Class实例,返回值即为相应的注解(Annotation类型的,要进行具体操作,需要强转为具体的注解类型)。
获取方法和构造器上的注解同理。
反射的优缺点:
反射的优点是提高了程序的灵活性和拓展性,降低了耦合性,提高自适应能力,允许程序创建和控制任何类的对象,无需提前硬编码目标类。
反射的缺点是放射的性能较低,会模糊程序内部逻辑结构,可读性较差。反射机制主要应用在对灵活性和拓展性要求很高的系统框架上。