Java·反射

Class类

在程序运行期间,Java运行时系统始终为所有对象维护一个运行时的类型标识。保存这些信息的类名为Class。
反射是一个功能强大复杂的机制,用来分析类的能力,运行时检查对象,实现泛型数组操作代码等。

如何得到Class类的实例对象

Class对象会描述一个特定类的属性。
第一种,最常用的Class方法是getName
我们先定义一个Person类:

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
package com.xzc;

public class Person {
public Person(){

}
public String name;
private int age;

@Override
public String toString(){
return "Name: "+this.name+" Age: "+this.age;
}

public void setAge(int age) {
this.age = age;
}
public void setName(String name){
this.name = name;
}
public String getName(){
return this.name;
}
public int getAge(){
return this.age;
}
}

再来一个测试类Test

1
2
3
4
5
6
7
8
9
package com.xzc;

public class Test {
public static void main(String[] args) {
Person p = new Person();
Class c = p.getClass();
System.out.println(c.getName());
}
}

可以看到输出结果:

com.xzc.Person

第二种,静态方法forName获得类名对应的Class对象。

1
2
String className = "java.util.Random";
Class c2 = Class.forName(className);

这个方法将抛出一个检查型异常ClassNotFoundException,要提供一个异常处理器。
第三种,如果T是任意Java类型,T.class将代表匹配的类对象,例如:

1
2
3
Class c3 = int.class;
Class c4 = Random.class;//注意import java.util.Random
Class c5 = double[].class;

可以看到T不仅可以是类型,还可以是基本数据类型与数组类型。
现在我们看一下Test.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.xzc;

import java.util.Random;

public class Test {
//使用forName方法会抛出Class不存在异常
public static void main(String[] args) throws ClassNotFoundException {
Person p = new Person();
Class c = p.getClass();
System.out.println(c.getName());
String className = "java.util.Random";
Class c2 = Class.forName(className);
System.out.println(c2.getName());
Class c3 = int.class;
Class c4 = Random.class;
Class c5 = double[].class;
System.out.println(c3.getName());
System.out.println(c4.getName());
System.out.println(c5.getName());
}
}

输出结果:

com.xzc.Person
java.util.Random
int
java.util.Random
[D

综上所述,我们有三种方法可以得到Class类的对象:
1.通过某一类的实例对象的getClass方法得到
2.通过Class类的静态方法forName得到(注意抛出异常)
3.通过T.class得到,T可以是类也可以不是类

比较两个类对象

因为虚拟机为每一个类型管理一个唯一的Class对象,因此可以用==运算符实现两个类的对象比较。

我们新建一个Student类继承Person类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.xzc;

public class Student extends Person{
int score;
public void setScore(int score){
this.score = score;
}
@Override
public String toString(){
return "Name: "+getName()+" Age: "+getAge()+" Score: "+getScore();
}
public int getScore(){
return this.score;
}
}

在Test.java中测试对象比较,注意与关键字instanceof的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.xzc;

public class Test {
public static void main(String[] args) {
Person p = new Person();
Student s = new Student();
if(s instanceof Person){
System.out.println("s instanceof Person");
}else{
System.out.println("s does not instanceof Person");
}
if(p.getClass() == s.getClass()){
System.out.println("Student == Person");
}else {
System.out.println("Student != Person");
}
}
}

输出结果

s instanceof Person
Student != Person

可以清晰看到Class即使是子类也不能判断相等,必须两个类是相同的类才能相等,而instanceof关键字可以判断父子类关系。

通过得到无参构造器来实例化

调用getConstructor方法可以得到一个Constructor类型对象,然后使用newInstance方法来构造一个实例。如果这个类没有无参数构造器,getConstructor会抛出一个异常。
看一下Test.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.xzc;

public class Test {
public static void main(String[] args) {
//getConstructor与newInstance方法均会抛出异常
Person p = new Person();
Class c = p.getClass();
try {
Object obj = c.getConstructor().newInstance();
//向下强制类型转换
((Person)obj).setAge(10);
((Person)obj).setName("A person created by Class");
System.out.println(obj.toString());
}catch (Exception e){
e.printStackTrace();
}
}
}

输出结果(注意我们重写了Person类的toString方法)

Name: A person created by Class Age: 10

利用反射机制分析类的能力

在java.lang.reflect包中有三个类Field,Method,Constructor分别用于描述类的字段,方法和构造器。

这三个类都有一个叫做getName的方法,用来返回字段,方法或构造器的名称。Field类还有一个getType方法,用来返回描述字段类型的一个对象,这个对象的类型同样是Class。
Class类中的getFields,getMethods和getConstors方法分别返回这个类支持的公共字段,方法和构造器的数组,其中包括超类的公共成员,而Class类中的getDeclaredFields,getDeclaredMethods和getDeclaredConstors方法将分别返回类中声明的全部字段,方法和构造器的数组,其中包括私有成员,包成员,和受保护的成员,但不包括超类的成员
看一下Test.java

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
package com.xzc;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;

public class Test {
public static void main(String[] args) throws NoSuchFieldException {
Person p = new Person();
Class c = p.getClass();
Field f = c.getField("name");

//name的类型是String类 应该输出String
System.out.println(f.getType());
//返回公共方法,包括超类的公共方法
Method[] a = c.getMethods();
System.out.println(Arrays.toString(a));
//返回公共字段,包括超类的公共字段
Field[] b = c.getFields();
//age是private name是public可以看到name被get了而age无法被get
System.out.println(Arrays.toString(b));
//返回所有字段 但不返回超类字段
Field[] b1 = c.getDeclaredFields();
System.out.println(Arrays.toString(b1));
Constructor[] con = c.getConstructors();
System.out.println(Arrays.toString(con));
}
}

输出结果:

class java.lang.String

[public java.lang.String com.xzc.Person.getName(), public
java.lang.String com.xzc.Person.toString(), public void
com.xzc.Person.setName(java.lang.String), public int
com.xzc.Person.getAge(), public void com.xzc.Person.setAge(int),
public final void java.lang.Object.wait(long,int) throws
java.lang.InterruptedException, public final void
java.lang.Object.wait() throws java.lang.InterruptedException, public
final native void java.lang.Object.wait(long) throws
java.lang.InterruptedException, public boolean
java.lang.Object.equals(java.lang.Object), public native int
java.lang.Object.hashCode(), public final native java.lang.Class
java.lang.Object.getClass(), public final native void
java.lang.Object.notify(), public final native void
java.lang.Object.notifyAll()]

[public java.lang.String com.xzc.Person.name]

[public java.lang.String com.xzc.Person.name, private int com.xzc.Person.age]

[public com.xzc.Person()]

利用反射在运行时分析对象

我们已经知道如何查看任意对象数据字段得到名字和类型
1.获得对应的Class对象
2.在这个Class对象上调用getDeclaredField等方法。

而利用反射机制,还可以查看编译时不知道的对象字段。关键方法中是Field类中的get方法,如果f是一个Field类型的对象,obj是包含f字段的类的对象,f.get(obj)将返回一个对象,其值为obj的当前字段值。除了get方法还有set方法(f.set(obj,value))把f表示的字段设置为新值
直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.xzc;

import java.lang.reflect.Field;


public class Test {
//get方法和getField方法会抛出异常
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Person p = new Person();
Class c = p.getClass();
p.setName("Hawkeye");
Field f = c.getField("name");
Object v = f.get(p);
System.out.println(v);
f.set(p,"HawkeyeNew");
v = f.get(p);
System.out.println(v);
}
}

输出结果:

Hawkeye
HawkeyeNew

可以看到我们通过Person的实例对象p得到一个相应的Class对象c,通过c的getField方法得到其中名为name的字段f(此时name是public,关于修饰符后续再说),而通过f的get方法f.get(p)可以得到一个值为p对象的name字段的值(Hawkeye)的对象v,所以才输出了Hawkeye。这样我们就查到了Person的实例对象p的name字段的值,而不只是知道他是String类型。
例子中也可以看到通过set方法修改了值,但是要注意之前v已经被赋值了,所以set后要再次赋值才能更改v。

这里由于name是public字段,所以get和set方法没有抛出异常,如果是private该怎么办?
这里我们用私有的age字段测试,上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.xzc;

import java.lang.reflect.Field;


public class Test {
//get方法和getField方法会抛出异常
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Person p = new Person();
Class c = p.getClass();
p.setName("Hawkeye");
p.setAge(10);
//注意这里age是private字段所以要用getDeclaredField
Field f = c.getDeclaredField("age");
f.setAccessible(true);
Object v = f.get(p);
System.out.println(v);
}
}

输出结果:

10

反射的默认行为受限于Java的访问控制。不过可以调用Field,Method或者Constructor的setAccessible方法覆盖Java得到访问控制。setAccessible方法是AccessibleObject类中的一个方法,它是Field、Method和Constructor类的公共超类。

利用反射编写泛型数组代码

如果我们已经有了个Person数组

1
var p = new Person[100];

数组满了我们可以利用copyOf方法来扩充:

1
p = Arrays.copyOf(p,2*p.length);

但是这不通用,如何编写一个通用的方法呢?考虑到Person数组可以转换为Object数组,尝试一下

1
2
3
4
5
public Object[] badCopyOf(Object[] a,int newLen){
var newArray = new Object[newLen];
System.arraycopy(a,0,newArray,0,Math.min(a.length,newLen));
return newArray;
}

事实上,这种方法有一个问题,返回的是Object数组这是因为我们new了一个Object数组进行返回,而对象数组不能强制转换成Person数组。将一个Person数组转换成Object再转换回来是可以的,但是一开始就是Object数组却永远不能转换成Person数组。
为此我们需要java.lang.reflect的Array类中的方法,其中最关键的是静态方法newInstance。它需要两个参数Class类的component和int类型的newLength。
为了能够具体实现,需要获得新数组的长度和元素类型。
可以调用Array.getLength方法获得数组长度。(注意是Array类中的静态方法getLength)
要获得新数组的元素类型,就需要完成以下工作:
1.首先获得a数组的类对象
2.确认它确实是一个数组
3.使用Class类的getComponentType方法(注意是Class类非静态方法,返回类型也是Class类)确定数组的正确类型
下面给出例子:

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
package com.xzc;

import java.lang.reflect.Array;
import java.util.Arrays;


public class Test {
public static void main(String[] args) {
int[] a = {1,2,3,4};
Person[] p = {new Person(),new Person()};
a = (int[])goodCopyOf(a,10);
System.out.println(Arrays.toString(a));
p = (Person[])goodCopyOf(p,4);
System.out.println(Arrays.toString(p));
}
//这里要在main方法里不声明Test实例对象调用 所以goodCopyOf方法声明为static
public static Object goodCopyOf(Object a, int newLength){
Class c= a.getClass();
if(!c.isArray()){
return null;
}
Class componentType = c.getComponentType();
int length = Array.getLength(a);
Object newArray = Array.newInstance(componentType,newLength);
System.arraycopy(a,0,newArray,0, Math.min(length,newLength));
return newArray;
}

}

输出结果:

[1, 2, 3, 4, 0, 0, 0, 0, 0, 0]
[Name: null Age: 0, Name: null Age: 0, null, null]

请注意,这个CopyOf方法可以用来扩展任意类型的数组,而不只是对象数组(比如上述扩展了int数组)
为了能够实现扩展int数组,应该将goodCopyOf声明为Object类型,参数也声明为Object类型,这是因为int[]类型可以转换为Object但是不能转换为对象数组。
如果我们声明为Object[]类型,就只能对对象数组进行扩展,而不能对int[]类型扩展。
在这里插入图片描述

调用任意方法和构造器

Field类中有get方法可以查看对象的字段值,与之类似,Method类中有一个invoke方法,允许你调用包装在当前Method对象中的方法,其签名是

1
Object invoke(Object obj,Object... args)

第一个参数是隐式参数,其余的对象提供了显式参数。对于静态方法,对第一个参数可以忽略,即可以把它设为null。直接看Test.java

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
package com.xzc;


import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Test {
//getMethon方法和invoke方法会抛出异常
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Person p = new Person();
p.setName("Hawkeye");
p.setAge(10);
Class c = p.getClass();
Method m = c.getMethod("getName");
Method m2 = c.getMethod("getAge");
//返回Object类要转型
String n = (String) m.invoke(p);
System.out.println(n);
int a = (int) m2.invoke(p);
int b = (Integer) m2.invoke(p);
System.out.println(a);
System.out.println(b);
}
}

输出结果

Hawkeye
10
10

可以看到我们先用getMethod方法获得了p的getName方法与getAge方法的Method对象,然后用他们的invoke方法可以运行p的getName与getAge方法。注意返回类型是Object需要转型,如果是基本类型,invoke应该返回相应包装器类型,测试发现强制类型转换为Integer或int都可以。
如何得到Method对象呢?
其实我们可以调用getDeclareMethods方法得到Method对象数组,搜索我们想要的,也可以直接用例子中的getMethod方法得到想要的方法。
考虑到方法会有重载,可以在getMethod方法中提供想要方法的参数类型,getMethod签名是:

1
Method getMethod(String name,Class... parameterTypes)

同样适用于构造器创建实例
我们将Person类中的getAge重载一下,返回year年后的年龄,同时创建一个两个参数的构造器。

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
37
package com.xzc;

public class Person {
public Person(){

}
//除了无参构造器外创建一个两个参数的构造器
public Person(String name,int age){
this.name = name;
this.age = age;
}
public String name;
private int age;

@Override
public String toString(){
return "Name: "+this.name+" Age: "+this.age;
}

public void setAge(int age) {
this.age = age;
}
public void setName(String name){
this.name = name;
}
public String getName(){
return this.name;
}
public int getAge(){
return this.age;
}
//重载getAge方法
public int getAge(int year){
return this.age + year;
}
}

然后我们来测试Test.java

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
package com.xzc;


import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Test {
//getMethon方法和invoke方法和getConstr方法会抛出异常
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
Person p = new Person();
p.setName("Hawkeye");
p.setAge(10);
Class c = p.getClass();
Method m = c.getMethod("getName");
Method m2 = c.getMethod("getAge");
//返回Object类要转型
String n = (String) m.invoke(p);
System.out.println(n);
int a = (int) m2.invoke(p);
int b = (Integer) m2.invoke(p);
System.out.println(a);
System.out.println(b);
Method m3 = c.getMethod("getAge",int.class);//重点
int d = (int)m3.invoke(p,10);//方法有一个int参数要赋予一个int值
System.out.println("Age after 10 years:"+d);
//无参构造器前面已经说了我们直接上有参,无非就是传几个参数的class。
Constructor con = c.getConstructor(String.class,int.class);//重点
Object o = con.newInstance("HawkeyeNew",15);//构造器有参数了要赋予参数
System.out.println(o.toString());
}
}

输出结果:

Hawkeye
10
10
Age after 10 years:20
Name: HawkeyeNew Age: 15

通过这个样例我们可以清晰地看到如何获得重载的方法和构造器,指定参数类型即可。


Java·反射
http://example.com/2021/04/28/My-First-Blog/
作者
AHawkeye
发布于
2021年4月28日
许可协议