反射

Smile_slime_47

Class对象


在JVM底层原理中我们已经知道了:

当我们实例化一个新的对象时,JVM会先通过classLoader搜寻对应的class文件并创建一个Class对象,一个Class对象包含了关于这个类的详细信息,然后JVM会根据这个Class对象去初始化实例对象。

一个Class对象包含了这个类的 全限定名(name)、包名(package)、父类(super)、实现接口(interface)、成员(fields)、方法(methods) 等信息,这些信息是用来描述这个类本身的元信息,反过来说,我们也可以通过得到一个类的Class对象来获取关于这个类的元信息

通过Class获取类的元信息的操作我们叫做反射(Reflection)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
public static void main(String[] args){
Integer n = Integer.valueOf(123);

System.out.println(n instanceof Integer);
System.out.println(n instanceof Number);
System.out.println(n.getClass() == Integer.class);
System.out.println(n.getClass() == Number.class);
System.out.println(n.getClass().getSuperclass() == Number.class);
}
}

stdout:
true //n是Integer,Integer自然是Integer
true //n是Integer,自然也是Number
true //n是Integer,Integer.class自然是Integer.class
false //n是Integer,Integer.class!=Number.class,实际上有的编译器会直接在这一步报错
true //n是Integer,Integer的父类是Number,Number.class自然是Number.class

通过上述代码可以知道,每个类的Class对象都是独立的,即Integer虽然在继承关系上属于Number,但是Integer.class和Number.class是不同的两个Class对象,但我们仍可以通过Integer.class.getSuperclass()来获取Integer父类的Class对象

获取Class

我们可以通过多种方法得到一个类的Class对象,由于Class对象在JVM中具有唯一性,对于同一个类获取的Class对象永远是同一个

声明Class引用

1
Class<String> cls=String.class;

要注意的是,这里我们只是创建了一个class的引用,并没有显式地创建一个新的Class对象

调用实例对象的getClass方法

1
2
String s="123";
Class<? extends String> cls=s.getClass();

调用Class的forName静态方法

1
Class<?> cls = Class.forName("java.lang.String");

如你所见,这种方法需要知道类的全限定名才能获取

Class提供的方法

在我们获取到一个Class对象后,可以用Class对象的许多get方法获取关于类的信息

有趣的是,当我们试图打印一些数据类型的ClassName时,如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {
public static void main(String[] args) {
int[] array=new int[5];
Integer i=Integer.valueOf(5);
String[] sArr={"123"};
System.out.println("Class name: " + array.getClass().getName());
System.out.println("Class name: " + i.getClass().getName());
System.out.println("Class name: " + sArr.getClass().getName());
}
}

stdout:
Class name: [I
Class name: java.lang.Integer
Class name: [Ljava.lang.String;

JVM给不同的数据类型(基本类型、对象、数组)设置了一套描述符,在字节码中用一系列大写字母来表示不同数据类型

描述符 数据类型 描述符 数据类型
B byte J long
C char S short
D double V void
F float Z boolean
I int
L* Object [* 数组类型

于是从上面的输出我们会发现:

  1. [I说明了这是一个整数类型的数组
    • 第一行输出中大写字母I指的则是int类型数据,**[** 则指的是这是一个int类型的数组
  2. 而Integer的ClassName则是java.lang.Integer,即Integer对象的全限定名
  3. 当我们试图输出String[]的ClassName时得到的是[Ljava.lang.String,结合第一点和第二点,实际上这个name分为两部分
    • [L说明了这是一个对象类型的数组
    • java.lang.String则是这个对象的全限定名的符号引用

字段


Field(字段)对象是一个用于描述类的成员的对象,如String源码下的private final byte[] value就是String的一个字段

获取字段

Class提供了如下几个方式来获得某个class对象的指定field

1
2
3
4
public Field getField(String name)             //根据字段名获取某个public的field(包括父类)
public Field getDeclaredField(String name) //根据字段名获取某个field(不包括父类)
public Field[] getFields() //获取所有public的field(包括父类)
public Field[] getDeclaredFields() //获取所有field(不包括父类)

在获取到一个字段后,我们可以通过一系列get获取到关于该字段的信息

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 class Main {
public static void main(String[] args) throws NoSuchFieldException {
String sArr="123";
Field f=sArr.getClass().getDeclaredField("value");

System.out.println("getName:"+f.getName());
System.out.println("getType:"+f.getType());
System.out.println("getModifiers:"+f.getModifiers());
System.out.println("isPublic:"+Modifier.isPublic(f.getModifiers()));
System.out.println("isPrivate:"+Modifier.isPrivate(f.getModifiers()));
}
}

stdout:
getName:value //字段名称为value
getType:class [B //字段类型为byte类型数组
getModifiers:18 //获取修饰符
isPublic:false //通过修饰符判断是否为public
isPrivate:true //通过修饰符判断是否为private
```
### 操作字段
在获取到一个字段后,我们就可以对一个对象的该指定字段进行操作
```java
public Object get(Object obj) //从obj中寻找该字段,并返回该字段的值
public void set(Object obj, Object value) //从obj中寻找该字段,并修改该字段的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class test {
private int a;
public test(){
a=10;
}
}

public class Main {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
test t=new test();
Field f=t.getClass().getDeclaredField("a");

f.setAccessible(true); //要注意,这里的a是private类型字段,所以要修改字段的访问权限
System.out.println(f.get(t)); //f.get(t)等同于t.a,即getter
f.set(t,20); //f.set(t,20)等同于t.a=20,即setter
System.out.println(f.get(t));
}
}

stdout:
10
20

反射是获取类内成员信息的一种非常规用法,可以让我们在不知道一个对象内任何信息(即黑盒状态)的情况下,获取/修改一个特定字段的值,常用于底层框架中。

此外,有一些Java核心类的SecurityManager会阻止setAccessible,如String的private final byte[] value字段,在使用setAccessible(true)时则会抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String sArr="123";
Field f=sArr.getClass().getDeclaredField("value");

f.setAccessible(true);
System.out.println(f.get(sArr));
}
}

stderr:
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module @16b98e56
at java.base/java.lang.reflect.AccessibleObject.throwInaccessibleObjectException(AccessibleObject.java:387)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:363)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:311)
at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:180)
at java.base/java.lang.reflect.Field.setAccessible(Field.java:174)
at Main.main(Main.java:9)

方法


要注意,通过反射获取的方法永远是遵循多态规则的,即总是优先选择被子类覆写的方法

获取方法

和字段同理,我们也可以通过如下几个方式来获得某个class对象的指定field

1
2
3
4
public Method getMethod(String name, Class...)             //获取某个public的Method(包括父类)
public Method getDeclaredMethod(String name, Class...) //获取某个Method(不包括父类)
public Method[] getMethods() //获取所有public的Method(包括父类)
public Method[] getDeclaredMethods() //获取所有Method(不包括父类)

在获取到一个方法后,我们可以通过一系列get获取到关于该方法的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Main {
public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {
String sArr="123";
Method f=sArr.getClass().getMethod("charAt", int.class);

System.out.println("getName:"+f.getName());
System.out.println("getParameterTypes:"+f.getParameterTypes());
System.out.println("getParameterTypes:"+ Arrays.toString(f.getParameterTypes()));
System.out.println("getReturnType:"+f.getReturnType());
System.out.println("getModifiers:"+f.getModifiers());
System.out.println("isPublic:"+Modifier.isPublic(f.getModifiers()));
System.out.println("isPrivate:"+Modifier.isPrivate(f.getModifiers()));
}
}

stdout:
getName:charAt //方法名称
getParameterTypes:[Ljava.lang.Class;@27d6c5e0 //参数类型
getParameterTypes:[int]
getType:char //返回类型
getModifiers:1 //修饰符
isPublic:true
isPrivate:false

操作方法

我们可以通过invoke调用一个对象的指定方法,同理的,我们也可以通过setAccessible来操作private方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {
public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
String sArr="123";
Method f=sArr.getClass().getMethod("charAt", int.class);
Method m=Integer.class.getMethod("valueOf", int.class);

System.out.println(f.invoke(sArr,0));
System.out.println(f.invoke(sArr,1));
System.out.println(f.invoke(sArr,2));

System.out.println(m.invoke(null,10)); //要注意,当调用静态方法时,第一个参数为null
}
}

stdout:
1
2
3
10

实际上,invoke非常接近JVM实际调用非静态方法的情形,当调用一个非静态方法时,实际上隐藏了一个参数,即当前对象的引用,当我们执行s.charAt(0)时,JVM看到的实际上是String.charAt(s,0);但是在class文件中,static方法并非传入了一个null,而是压根就没有这个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class test {
static public int get5(){
return 5;
}
}

> javap -c -v test.class
···
public static int get5();
descriptor: ()I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_5
1: ireturn
LineNumberTable:
line 8: 0
}
···

构造器


获取构造器

1
2
3
4
public getConstructor(Class...)             //获取某个public的Constructor;
public getDeclaredConstructor(Class...) //获取某个Constructor;
public getConstructors() //获取所有public的Constructor;
public getDeclaredConstructors() //获取所有Constructor。

public newInstance())这个获取零参构造器的方法在Java9中已经被弃用了

1
2
3
4
@Deprecated(since = "9") 
@NotNull
public T newInstance()
throws InstantiationException, IllegalAccessException

调用构造器

我们可以通过newInstance来调用构造器并实例化一个新的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {
public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
String sArr="123";
Constructor c=sArr.getClass().getConstructor(String.class);
//这里会自动根据参数类型返回相应的构造器
System.out.println(sArr);

String sAnother= (String) c.newInstance("456");
System.out.println(sAnother);
}
}

stdout:
123
456
Comments