反射2
Class类
首先,Class类,是一个类,一个名叫“Class”的class,定义于java.lang包。为了防止混淆,后文中采用首字母大写的英文单词Class代表这个Class类;而面向对象概念中的class,后文统一采用中文汉字类表示。
Java虚拟机(JVM)在运行代码的时候,不会在一开始就把所有的类信息都加载到内存中,而是充分发扬“勤拿少取”的精神:当JVM第一次需要使用一个类时(比如第一次运行到new该类的语句),才会在内存中创建一个属于该类的Class对象。该对象中包含这个类的全部信息,且在整个程序的运行期间,该Class对象是唯一的。
举个例子,之前编写了一个Person类,当该类在程序中第一次被使用时,JVM在内存中创建一个独属于Person类的Class对象(这里假设这个对象叫persClass)。这个对象中,包含了Person类的全部信息:有哪些字段、有哪些方法、实现了什么接口、继承自哪个父类等等。在整个程序运行的过程中,只有这个persClass会记录Person类的信息,不会再出现诸如persClass2等这样的Class类副本。
Class类的获取
在程序中,如何获取到这个某个类的Class对象呢?直接new一个?不可能的。
代码语言:java复制/*
* Private constructor. Only the Java Virtual Machine creates Class objects.
* This constructor is not used and prevents the default constructor being
* generated.
*/
private Class(ClassLoader loader, Class<?> arrayComponentType) {
// Initialize final field for classLoader. The initialization value of non-null
// prevents future JIT optimizations from assuming this final field is null.
classLoader = loader;
componentType = arrayComponentType;
}
Class的构造函数为私有的,这意味着Class对象只能由JVM创建。不过我们可以通过如下三种渠道获取到类对应的Class对象,这里以最常用的String类为例:
代码语言:java复制// 方法1:直接通过类的静态变量获取到它的Class对象。
Class cls = String.class;
代码语言:java复制// 方法2:通过该类的对象获取到类的Class对象。
String str = new String("HAHAHAHAHAHA");
Class cls = str.getClass();
代码语言:java复制// 方法3:通过Class类的静态方法forName,参数为类名字符串。
Class cls = Class.forName("java.lang.String");
这里可以主要关注下方法3,还记得我们一开始提的小需求吗?之后的小需求的实现主要靠它。
通过反射创建类对象
把“不走寻常路”,或者说“吃饱了撑的”的精神贯彻到底。在之前的main函数中,我们通过正常手段创建了Person对象(new语句),这次,我们通过反射手段创建Student对象。Class提供了两种方法让我们通过反射创建类对象:
代码语言:java复制// 方法一:newInstance方法
public T newInstance()
throws InstantiationException, IllegalAccessException
// 方法二:getConstructor方法
public Constructor<T> getConstructor(Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException
newInstance的特点是简单,可以直接通过Class对象生成对应的类对象;缺点是由于该函数没有参数,只能调用无参数的构造寒函数。
代码语言:java复制package com.sunhw.main;
import com.sunhw.user.Student;
public class Main {
public static void main(String[] args) {
try {
Class cls = Class.forName("com.sunhw.user.Student");
Student stu = (Student) cls.newInstance();
stu.hello();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
代码语言:txt复制输出:Student Hello
getConstructor方法则是首先获取到该类的构造函数对象(对,Java万物皆对象,构造函数也可以看成是一个对象),然后调用这个构造函数创建对象。使用该方法可以传入构造函数的参数类型,所以相比于newInstance(),其优点为可以调用类的有参构造函数。这里我们通过反射调用Student类的有参构造方法创建Student对象:
代码语言:java复制package com.sunhw.main;
import com.sunhw.user.Student;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class Main {
public static void main(String[] args) {
try {
Class cls = Class.forName("com.sunhw.user.Student");
Constructor constructor = cls.getConstructor(String.class);
Student student = (Student) constructor.newInstance("ST999");
System.out.println(student.getStuID());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
代码语言:txt复制输出:ST999
获取构造器方法总共如下:
代码语言:java复制// 获取某个public的Constructor
public Constructor<T> getConstructor(Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException
// 获取某个Constructor(无视访问权限)
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException
// 获取所有public的Constructor
public Constructor<?>[] getConstructors() throws SecurityException
// 获取所有Constructor(无视访问权限)
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException
有人会说了:这么一大坨,看起来不好记啊!没事没事,后面获取变量和方法还有两坨......其实是有规律的,而且非常直观,这个放到后面和获取变量及方法一起说。
获取类中的字段和方法
由于这两者的获取实在是太像了,所以这里直接放在一起讲:
代码语言:java复制// 获取变量字段Field的方法
// 根据字段名获取某个public的field(包括父类)
public Field getField(String name)
throws NoSuchFieldException, SecurityException
// 根据字段名获取当前类的某个field(无视访问权限且不包括父类)
public Field getDeclaredField(String name)
throws NoSuchFieldException, SecurityException
// 获取所有public的field(包括父类)
public Field[] getFields() throws SecurityException
// 获取当前类的所有field(无视访问权限且不包括父类)
public Field[] getDeclaredFields() throws SecurityException
代码语言:java复制// 获取类中方法Method的方法
// 获取某个public的Method(包括父类)
public Method getMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException
// 获取当前类的某个Method(无视访问权限且不包括父类)
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException
// 获取所有public的Method(包括父类)
public Method[] getMethods() throws SecurityException
// 获取当前类的所有Method(无视访问权限且不包括父类)
public Method[] getDeclaredMethods() throws SecurityException
获取构造器、字段、方法,看起来内容很多,其实一点也不少......开个玩笑,规律非常明显。
- 单数复数:这个没什么好说的,学过小学英语的都能看懂。
- 这些函数主要可以分为如下两类:getXXX 和 getDeclaredXXX。
getXXX,是正常方法,和点操作符“.”几乎没有任何区别。你通过点操作符能访问到的字段和方法,用getXXX函数也能访问到;通过点操作符能访问不到的,getXXX也访问不到。
getDeclaredXXX,是特殊手段,它可以无视访问权限访问类的全部内容。而作为限制,它无法访问该类的父类内容。
我们还是以Student为例,比较两种方法之间的区别。
代码语言:java复制package com.sunhw.main;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) {
try {
Class cls = Class.forName("com.sunhw.user.Student");
System.out.println("--------------------getFields---------------------");
for (Field f : cls.getFields()) {
System.out.println(f);
}
System.out.println("--------------------getDeclaredFields---------------------");
for (Field f : cls.getDeclaredFields()) {
System.out.println(f);
}
System.out.println("---------------------getMethods--------------------");
for (Method m : cls.getMethods()) {
System.out.println(m);
}
System.out.println("---------------------getDeclaredMethods--------------------");
for (Method m : cls.getDeclaredMethods()) {
System.out.println(m);
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
代码语言:txt复制--------------------getFields---------------------
public java.lang.String com.sunhw.user.Person.pubField
public static java.lang.String com.sunhw.user.Person.sex
--------------------getDeclaredFields---------------------
private java.lang.String com.sunhw.user.Student.stuID
---------------------getMethods--------------------
public java.lang.String com.sunhw.user.Student.getStuID()
public void com.sunhw.user.Student.eatQuickly()
public void com.sunhw.user.Student.hello()
public void com.sunhw.user.Person.eat()
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 java.lang.String java.lang.Object.toString()
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()
---------------------getDeclaredMethods--------------------
public java.lang.String com.sunhw.user.Student.getStuID()
public void com.sunhw.user.Student.eatQuickly()
public void com.sunhw.user.Student.hello()
private void com.sunhw.user.Student.playGames()
通过反射使用字段
反射获取字段的方法会返回Field对象,该对象包含这个字段的全部信息。它提供了很多接口,具体可以看Field类的定义。我个人感觉常用的主要有如下几个:
代码语言:java复制public String getName() // 获取字段名
public Class<?> getType() // 获取字段类型
public int getModifiers() // 获取字段修饰(如访问权限、静态等)
public Object get(Object obj) // 获取字段值
public void set(Object obj, Object value) // 设置字段值
举个简单的例子如下:
代码语言:java复制package com.sunhw.main;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) {
try {
Class cls = Class.forName("com.sunhw.user.Student");
Object stu = cls.newInstance();
for (Field f : cls.getFields()) {
if (f.getName().equals("pubField")) {
System.out.println(f.get(stu));
f.set(stu, "hahahah");
System.out.println(f.get(stu));
}
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
代码语言:txt复制输出:
null
hahahah
要注意的是,除非静态字段,其他字段都是跟对象绑定,所以get和set时,需要传入对应的对象,static字段则要传入null。
PS:可以注意一个有意思的小细节:这次的Main代码虽然到处都在用Student类,但是已经不用import它了。从代码上来说,Main和Student两个类已经在一定程度上解耦了。
通过反射使用函数
和使用字段几乎一样,我觉得常用的如下(这次我连注释都不想写,我觉得对着函数名猜也能猜出来):
代码语言:java复制public String getName()
public int getModifiers()
public Class<?>[] getParameterTypes()
public Class<?> getReturnType()
public Class<?>[] getExceptionTypes()
public Object invoke(Object obj, Object... args) // 执行该函数
同样举一个简单的小例子:
代码语言:java复制package com.sunhw.main;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) {
try {
Class cls = Class.forName("com.sunhw.user.Student");
Object stu = cls.newInstance();
Class clsp = Class.forName("com.sunhw.user.Person");
Object ps = clsp.newInstance();
// 通过Student类反射的方法
for (Method m : cls.getMethods()) {
if (m.getName().equals("hello")) {
m.invoke(stu, null);
}
if (m.getName().equals("eat")) {
m.invoke(stu, null);
}
}
// 通过Person类反射的方法
Method mp = clsp.getMethod("hello", null);
mp.invoke(ps, null); // 调用方法的实例为Person对象
mp.invoke(stu, null); // 调用方法的实例为Student对象
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
代码语言:txt复制输出:
Student Hello
Person eat now
Person Hello
Student Hello
这个例子同时也解答了可能出现的反射关于多态的问题:反射调用的hello函数,Person类本身有实现,Student继承自Person并覆写,当采用反射调用这个函数时,实际上调用的是哪个函数呢?
PS:不用想那么复杂,还是那句话:这玩意和点操作符没什么区别。
访问非public字段和方法
反射提供了getDeclaredXXX方法来获取到类中非public字段和方法,我们获取到之后,能直接使用吗?以Student类中定义的private字段stuID为例:
代码语言:java复制package com.sunhw.main;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class Main {
public static void main(String[] args) {
try {
Class cls = Class.forName("com.sunhw.user.Student");
Object stu = cls.getConstructor(String.class).newInstance("HS6666");
Field f = cls.getDeclaredField("stuID");
f.get(stu);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
代码语言:txt复制输出:
Exception in thread "main" java.lang.RuntimeException: java.lang.IllegalAccessException: class com.sunhw.main.Main cannot access a member of class com.sunhw.user.Student with modifiers "private"
at com.sunhw.main.Main.main(Main.java:19)
Caused by: java.lang.IllegalAccessException: class com.sunhw.main.Main cannot access a member of class com.sunhw.user.Student with modifiers "private"
at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:392)
at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:674)
at java.base/java.lang.reflect.Field.checkAccess(Field.java:1102)
at java.base/java.lang.reflect.Field.get(Field.java:423)
at com.sunhw.main.Main.main(Main.java:13)
如果是直接访问私有字段,程序会抛出IllegalAccessException,表示这次访问是不合法的。如果强行要使用,则需要调用setAccessible方法将其无视访问权限,设置成可以访问。
代码语言:java复制package com.sunhw.main;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class Main {
public static void main(String[] args) {
try {
Class cls = Class.forName("com.sunhw.user.Student");
Object stu = cls.getConstructor(String.class).newInstance("HS6666");
Field f = cls.getDeclaredField("stuID");
f.setAccessible(true); // 这里强行将Student的私有字段设置为允许访问。
System.out.println(f.get(stu));;
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
代码语言:txt复制输出:
HS6666
对于类中的非public方法,方法也是一样的,甚至setAccessible的使用都完全一样。因为无论是Field还是Method,他们都继承自一个共同的类:AccessibleObject。这个类中存在这样一个方法:
代码语言:java复制public void setAccessible(boolean flag)
Method和Field类在定义中都对这个方法进行了覆写。
PS:既然通过反射可以直接无视类的字段访问权限,那Java还设置public、protected和private有什么意义?其实正常状态下,Java的权限控制都有点“防君子不防小人”的意思,而且这么操作付出和回报不太成正比:花费老鼻子劲,写这么多行代码,就拿到了Student的私有变量stuID,图啥?这个字段真的就非拿不可?至于真的涉及到安全敏感的场景,用户可以在JVM的启动参数-Djava.security.manager设置安全策略禁止setAccessible(true)的执行。
获取类的父类和接口
代码语言:java复制public native Class<? super T> getSuperclass()
public Class<?>[] getInterfaces()
举个例子:
代码语言:java复制package com.sunhw.main;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class Main {
public static void main(String[] args) {
try {
Class cls = Class.forName("com.sunhw.user.Student");
Class pcls = cls.getSuperclass();
System.out.println(pcls);
Class[] interfaces = cls.getInterfaces();
for (Class c : interfaces) {
System.out.println(c);
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
代码语言:txt复制class com.sunhw.user.Person
interface com.sunhw.user.FasterEater
从例子中可以看出来:getSuperclass方法只返回当前类的直接父类,Student的直接父类为Person,而Person的父类Object它不返回(毕竟返回值不是数组);getInterfaces方法只返回当前类实现的全部接口,但也仅限当前类,不包括父类实现的接口,也不包括实现接口的父类。