0%

Java反射

概述

反射机制就是允许编程人员在程序运行时来改变程序的结构或者变量的类型。通过这个特性,我们可以在运行时得知某个类的所有成员,包括其属性和方法,同时也能够调用这些方法。请注意反射机制的特殊之处就在于可以使用编译期间完全未知的类,也就是通过反射机制可以加载一个在运行时才得知名字的类,从而取得其内部的成员函数并调用。

  1. 动态语言:

    一般认为在程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言。从这个观点看,Perl,Python,Ruby是动态语言,C++,Java,C#不是动态语言。尽管这样,JAVA有着一个非常突出的动态相关机制:反射(Reflection)。运用反射我们可以于运行时加载、探知、使用编译期间完全未知的classes。换句话说,Java程序可以加载在运行时才得知名称的class,获悉其完整构造方法,并生成其对象实体、或对其属性设值、或唤起其成员方法。

  2. 反射:

    要让Java程序能够运行,就得让Java类被Java虚拟机加载。Java类如果不被Java虚拟机加载就不能正常运行。正常情况下,我们运行的所有的程序在编译期时候就已经把那个类被加载了。Java的反射机制是在编译时并不确定是哪个类被加载了,而是在程序运行的时候才加载。使用的是在编译期并不知道的类。这样的编译特点就是java反射。

  3. 反射的作用:

    如果有AB两个程序员合作,A在写程序的时需要使用B所写的类,但B并没完成他所写的类。那么A的代码是不能通过编译的。此时,利用Java反射的机制,就可以让A在没有得到B所写的类的时候,来使自身的代码通过编译。

  4. 反射的实质:

    反射就是把Java类中的各种存在给解析成相应的Java类。要正确使用Java反射机制就得使用Class(C大写) 这个类。它是Java反射机制的起源。当一个类被加载以后,Java虚拟机就会自动产生一个Class对象。通过这个Class对象我们就能获得加载到虚拟机当中这个Class对象对应的方法、成员以及构造方法的声明和定义等信息。

  5. 反射机制的优点与缺点:

    • 静态编译:在编译时确定类型,绑定对象,即通过。

    • 动态编译:运行时确定类型,绑定对象。动态编译最大限度发挥了java的灵活性,体现了多态的应用,降低类之间的藕合性。

      一句话,反射机制的优点就是可以实现动态创建对象和编译,体现出很大的灵活性,特别是在J2EE的开发中它的灵活性就表现的十分明显。比如,一个大型的软件,不可能一次就把把它设计的很完美,当这个程序编译后,发布了,当发现需要更新某些功能时,我们不可能要用户把以前的卸载,再重新安装新的版本,假如这样的话,这个软件肯定是没有多少人用的。采用静态的话,需要把整个程序重新编译一次才可以实现功能的更新,而采用反射机制的话,它就可以不用安装,只需要在运行时才动态的创建和编译,就可以实现该功能。它的缺点是对性能有影响。使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且它满足我们的要求。这类操作总是慢于只直接执行相同的操作。

Class

在面向对象的世界里,万事万物皆对象。在java语言中,静态的成员,普通数据类型不是对象。

类是对象,类是java.lang.Class类的实例对象。这个对象的表示方式有三种:

  • 第一种表示方式:

    1
    Class c1 = Foo.class;//任何一个类都有一个隐含的静态成员变量class
  • 第二种表示方式:

    1
    Class c2 = foo1.getClass();//已知该类的对象,通过getClass方法得到这个实例类的class(类类型)
  • 第三种表达方式

    1
    Class c3 = Class.forName("imooc.reflect.Foo");

    可以通过类类型创建该类的类对象

    1
    2
    Class c1 = Foo.class;//c1就是类类型
    Foo foo=(Foo)c1.newInstance();//需要有无参数的构造方法。

静态动态加载类

编译时刻加载类是静态加载类,运行时刻是动态加载类。

new创建对象是静态加载类,在编译时刻就需要加载所有可能用到的类。如果创建了一个可以使用的C1对象和不可使用的C2对象,即使C1可用,由于C2不可以而编译不过使得C1不可用。用动态加载类可以解决该问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public interface OfficeAble {
void start();
}

public class Word implements OfficeAble {
public void start() {
System.out.println("Word...start...");
}
}

public static void main(String[] args) {
try {
//动态加载类,在运行时加载
Class c = Class.forName(args[0]);
//通过类类型,创建该对象
OfficeAble oa = (OfficeAble) c.newInstance();
oa.start();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}

获取类的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//首先获取类的类类型
Class cl = object.getClass();
//获取类的名称/简称
System.out.println("名称:" + cl.getName() + " 简称:" + cl.getSimpleName());
/**
* Method类,方法对象,一个成员方法就是一个Method对象
* getMethods获取的是所有public方法,包括从父类继承而来的
* getDeclaredMethods获取的是所有该类自己声明的方法,不问访问权限
*
*/
Method[] methods = cl.getMethods();//cl.getDeclaredMethods()
for (int i = 0; i < methods.length; i++){
//得到方法的返回值类型的类类型
Class returnType = methods[i].getReturnType();
System.out.print(returnType.getName() + " ");
//得到方法的名称
System.out.print(methods[i].getName() + "(");
//获取参数列表的类型的类类型
Class[] params = methods[i].getParameterTypes();
for (Class c: params){
System.out.print(c.getName() + ",");
}
System.out.println(")");
}

获取类的属性

在使用反射获取或者修改一个变量的值时,编译器不会进行自动装/拆箱。因此我们无法给一个 Integer 类型的变量赋整型值,必须给它赋一个 Integer 对象才可以。

1
2
3
4
5
6
7
8
9
10
11
/**
* getFields获取的是所有public属性,包括从父类继承而来的
* getDeclaredFields获取的是所有该类自己定义的属性,不问访问权限
*/
Field[] fields = cl.getDeclaredFields();
for (int i = 0; i < fields.length; i++){
//得到成员变量的类型的类类型
Class fieldType = fields[i].getType();
System.out.println(fieldType.getName());
System.out.println(fields[i].getName());
}

对于 final 修饰的字段:

构造方法赋值:

  • 构造方法赋值在JVM编译期不会优化,运行时决定字段的值,修改后通过反射和其他方式访问到的都是新值。

直接赋值:

  • 基本类型、String类型:JVM编译期会优化成常量,导致修改后的值通过反射可以访问到新值,其他方式访问到的仍是旧值。
  • 对象类型:JVM编译期不会优化,运行时决定字段的值,修改后通过反射和其他方式访问到的都是新值。

间接赋值:

  • 间接赋值在JVM编译期不会优化,运行时决定字段的值,修改后通过反射和其他方式访问到的都是新值。

小结

如果final字段值是运行时赋值的,则修改后无论通过何种方式访问获得的都是新值;基本类型、String类型直接赋值时由于JVM编译优化,编译时期用到字段的地方会直接被字段值替换,导致通过反射修改字段值后用到字段的地方仍是原值,但通过反射访问获取到的是新值(给人的错觉是没修改成功)。

获取类的构造方法

1
2
3
4
5
/**
* getConstructors获取的是所有public的构造函数,包括从父类继承而来的
* getDeclaredConstructors获取的是所有该类自己定义的构造函数,不问访问权限
*/
Constructor[] constructor = cl.getDeclaredConstructors();

方法反射

使用method.invok();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
A a = new A();
Class c = a.getClass();
try {
Method method = c.getMethod("print", int.class, int.class);
//方法的反射操作是指用method对象进行操作
//方法如果没有返回值则返回null, 有返回值则返回返回值
method.invoke(a, new Object[]{20, 30});
//method.invoke(a, 20, 30);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}

属性反射

1
2
3
4
5
6
7
8
9
10
Field[] fields = c.getDeclaredFields();

for (Field f : fields) {
f.setAccessible(true);
try {
f.set(object, args...);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}

泛型的本质

编译之后集合的泛型是去泛型化的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ArrayList list = new ArrayList();
ArrayList<String> list1 = new ArrayList<String>();

Class c1 = list.getClass();
Class c2 = list1.getClass();
System.out.println(c1 == c2);//true

/**反射都是编译之后的操作(字节码)
* 返回true说明编译之后集合的泛型是去泛型化的
* Java中集合的泛型,是防止错误输入的,只在编译阶段有效
* 验证:通过方法的反射来给list1加入int类型的数据,绕过编译
*/
try {
Method method = c2.getMethod("add", Object.class);
method.invoke(list1, 20);
method.invoke(list1, "string");
System.out.println(list1);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}

MethodHandle

概述

java7增加了对动态类型语言的支持,使java也可以像C语言那样将方法作为参数传递,其实现在lava.lang.invoke包中。MethodHandle作用类似于反射中的Method类,但它比Method类要更加灵活和轻量级。通过MethodHandle进行方法调用一般需要以下几步:

  1. 创建MethodType对象,指定方法的签名;
  2. 在MethodHandles.Lookup中查找类型为MethodType的MethodHandle;
  3. 传入方法参数并调用MethodHandle.invoke或者MethodHandle.invokeExact方法。

MethodType

创建方法:

1.通过MethodType.methodType及其重载方法,需要指定返回值类型以及0到多个参数:

1
2
public static MethodType methodType(Class<?> rtype, Class<?>[] ptypes);
public static MethodType methodType(Class<?> rtype, List<Class<?>> ptypes);

2.MethodType.genericMethodType,需要指定参数的个数,类型都为Object:

1
2
public static MethodType genericMethodType(int objectArgCount, boolean finalArray);
public static MethodType genericMethodType(int objectArgCount)

3.fromMethodDescriptorString,通过方法描述来创建:

1
public static MethodType fromMethodDescriptorString(String descriptor, ClassLoader loader);

MethodHandles.Lookup

MethodHandles.Lookup相当于MethodHandle工厂类,通过findxxx方法可以得到相应的MethodHandle,还可以配合反射API创建MethodHandle,对应的方法有unreflect、unreflectSpecial等。

invoke

在得到MethodHandle后就可以进行方法调用了,有三种调用形式:

  1. invokeExact: 调用此方法与直接调用底层方法一样,需要做到参数类型精确匹配;
  2. invoke: 参数类型松散匹配,通过asType自动适配;
  3. invokeWithArguments: 直接通过方法参数来调用。其实现是先通过genericMethodType方法得到MethodType,再通过MethodHandle的asType转换后得到一个新的MethodHandle,最后通过新MethodHandle的invokeExact方法来完成调用。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Main {
private static String hello(String s) {
return "s = " + s;
}

public static void main(String[] args) {
MethodHandle methodHandle = getMethodType();

try {
String result = (String) methodHandle.invokeExact("hearing");
System.out.println(result);
} catch (Throwable throwable) {
throwable.printStackTrace();
}

try {
// String s = (String) methodHandle.bindTo(...).invokeWithArguments("hearing-2");
String s = (String) methodHandle.invokeWithArguments("hearing-2");
System.out.println(s);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}

private static MethodHandle getMethodType() {
MethodType methodType = MethodType.methodType(String.class, String.class);
MethodHandle methodHandle = null;
try {
methodHandle = MethodHandles.lookup().findStatic(Main.class, "hello", methodType);
} catch (NoSuchMethodException | IllegalAccessException e) {
e.printStackTrace();
}

return methodHandle;
}
}