Java反射机制学习
1. 前言
Java的反射功能平时已使用了多次,但从来没有仔细的梳理过,趁着最近在梳理Java基础,再来系统的了解下Java反射机制,本文所使用的JDK版本是JDK 8。
2. Java中为什么会有反射?
本次,我们通过How-What-Why
的方式来进行学习,首先,我们来了解下为什么会有反射。
- 首先,Java反射功能是在JDK1.1时引入的,所围绕的点就是网络。大概在互联网刚出现的时候,Java所倡导的跨平台让它迅速占领了一席之地,而跨平台的实现,很大程度上是通过Java的Applet来实现的。
- 那么applet是如何运行的呢?Applet的运行是通过把远程的类文件下载到本地来运行,而网络在当时给我们的感觉就是一个字:慢,如果Java采用传统可执行文件的方式,即执行一个完整的可执行文件,把整个Applet下载下来然后再运行,只怕等到花儿也谢了。所以Java采用的方式是把文件拆开,以类为单位进行组织,这就是我们今天见到的class文件。这样,执行的过程就变成第一个类下载之后就可以运行,大大节省了最初的等待时间。而好的设计会把程序分成若干的模块,所以,绝大多数程序不可能写在一个类中。因此,类文件中必须包含它所用到的类,这样就会加载对应的用到的类,进一步字段等信息也会加载进来,这样几乎一个完整类的信息就出来了,而这样的实现恰好是反射的雏形。
- 但JDK1.1的反射其实还不能完全的叫反射,在维基百科中,对JDK1.1的reflection解释为:reflection which supported Introspection only, no modification at runtime was possible,JDK1.1的反射其实就是内省。维基百科地址:https://en.wikipedia.org/wiki/Java_version_history
2.1 什么是内省机制
在计算机科学中,内省(Introspection)是指计算机程序在运行的时候检查对象类型的一种能力,通常也可以称为运行时类型检查。而反射是在内省基础之上增加了可以修改的能力,内省和反射还是有些区别的,因为有些语言支持内省,不支持反射,比如 C++语言。反射与内省只是概念上的区分,其实内省还是通过反射来实现的。
内省是Java 语言对 Bean 类属性、事件的一种缺省处理方法。例如类 A 中有属性 name, 那我们可以通过 getName,setName 来得到其值或者设置新的值,而通过 getName/setName 来访问 name 属性,这就是默认的规则。Java种提供了一套API用来内省相关实现,位于java.beans包中,比如 BeanInfo,Introspector等相关接口与类。由于本文的重点是反射,所以有关内省就不多说了,如果有兴趣,可以参考底部链接。
3. 什么是反射?
现在,我们可以大概来说说什么是反射了。所谓反射,其实就是动态的内省,是指在程序运行期间,能够动态获取对象信息并且能够动态调用对象方法的一种功能。由于Java是一种编译型语言,一般情况下,对象的类型在编译期间就已经确定下来了,而使用反射我们可以动态的创建对象,而这样的对象的类型从在编译期间可以是未知的。
一般情况下,一个类中有成员变量,方法,构造方法,包等,通过反射可以将类中的各个属性映射为一个个对象,然后通过对这些对象来进行操作。
Java中的反射机制大概有如下几种使用情况:
- 在运行时构造一个类的对象;
- 在运行时获取一个类拥有的属性和方法;
- 在运行时调用一个对象的方法;
- 在运行时判断一个对象所属的类;
4. 反射的使用
接下来我们就来了解下反射中相关API的使用,反射相关的类一般都在java.lang.relfect
包目录下。
4.1 Class对象
首先,Class是一个类,一个描述类本身的类,封装了描述方法的Method,描述字段的Filed,描述构造器的Constructor等属性。而CLASS类的实例表示正在运行的JAVA应用程序中的类和程序接口,也就是在JVM中每个实例都会有且仅有一个对应的CLASS对象。
而获取Class对象的方式一般有三种:
- 第一种方式,直接通过类名的方式获取;
Class<Person> clazz = Person.class;
Class<Integer> integerClass = Integer.TYPE;
- 第二种方式,通过Class类的forName静态方法进行加载,传递类名的时候需要是类的全路径,由于是根据类名加载,该类会抛出一个ClassNotFoundException异常,注意处理下;
// 我们以前通过JDBC加载数据库驱动的时候经常使用这种方式
Class<?> cls = Class.forName("jdk8.stream.Person");
- 第三种方式,通过某个对象的getClass方法来获取;
String str = "reflection";
Class<?> cls = str.getClass();
针对基础类型和包装类型,这里再多说一点,通过 int/Integer来举例,在Integer类中有一个TYPE变量,用于返回基础类型的Class对象,也就是说:Integer.class
返回的是Integer类所对应的Class对象,Integer.TYPE
返回的是基础类型int的Class对象。
4.2 判断对象是否是某个类的实例
在Java中,我们一般是通过 instanceof
关键字来判断是否为某个类的实例,同时我们也可以借助反射中Class对象的 isInstance()
方法来判断是否是某个类的实例,该方法是一个native方法:
public native boolean isInstance(Object obj);
使用方式比较简单:
Class<Person> cls = Person.class;
Person person = new Person();
System.out.println(cls.isInstance(person));
4.3 通过反射来创建对象,生成实例
通过反射来创建对象一般有两种方式:
- 第一种是通过Class对象的newInstance()方法,不过记得需要类提供无参构造方法,另外注意下反射的相关异常:
Class<Person> cls = Person.class;
Person person = cls.newInstance();
- 第二种方式是通过Constructor对象的newInstance方法来创建,这种方法可以用指定的构造器来构造类的实例,不过我们需要先通过Class对象获取指定的Constructor对象:
Class<Person> cls = Person.class;
// 只有一个参数的构造方法
Constructor constructor = cls.getConstructor(String.class);
//根据构造器创建实例
Person person =(Person)constructor.newInstance("beijing");
System.out.println(person);
4.4 通过反射创建数组
数组在Java中可以算比较特殊的一种类型了,通过反射创建数组和上述创建对象的方式有些不同,我们可以借助java.lang.reflect.Array
类的newInstance
方法来创建:
Class<String> cls = String.class;
Object array = Array.newInstance(cls, 25);
//往数组里添加内容
Array.set(array, 0, "hello");
Array.set(array, 1, "wold");
Array.set(array, 2, "java");
//获取某一项的内容
System.out.println(Array.get(array, 2));
有兴趣的可以看下Array类的一些get开头的方法,比如getInt
,getDouble
等方法。
4.5 获取方法
我们通过Class类提供的一系列方法,可以获取到该Class对象对应类或接口的方法,属性,构造方法等,我们先来看看如何获取方法:
-
getDeclaredMethods
方法,获取类或接口声明的所有方法,包括public,protected,默认访问和private方法,但不包括继承的方法,返回的是Method[]
数组;
-
-
getMethods
方法,返回类的公用方法,包括继承的公用方法,默认情况下类继承自Object,所以会返回Object的一些方法,比如notify,equals,hashCode方法等;
-
-
getMethod(String name, Class<?>... parameterTypes)
,返回类的特定公用方法,第一个参数是方法名,第二个参数是一个可变参数,表示该方法的参数;该方法不能是继承类的方法,必须是目标类的方法;
-
-
getDeclaredMethod(String name, Class<?>... parameterTypes)
,功能和getMethod类似,但返回的方法类型不但但是public方法,还有protected,默认访问方法和private方法,同样不包括继承的方法;
-
-
getEnclosingMethod
,该方法表示当前类的一个内部类是在哪一个方法中被定义的,比如:
-
public void newInnerClass() {
class InnerClass {}
}
而针对其他方法,可以简单测试下:
// 先在Person类中创建一个公用方法,用于测试
public void getSumByCity(String city) {
System.out.println("test");
}
Class<Person> cls = Person.class;
Method[] methods = cls.getMethods();
// 第一个是方法名, 后面是方法对应的参数
Method method = cls.getMethod("getSumByCity", String.class);
4.6 获取构造方法
获取构造方法和上述获取方法一样,通过如下方法:
public Constructor<T> getConstructor(Class<?>... parameterTypes)
public Constructor<?>[] getConstructors()
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
public Constructor<?>[] getDeclaredConstructors()
public Constructor<?> getEnclosingConstructor()
至于方法的访问权限,都是和上述接口一致的,比如说getDeclaredConstructors
能获取所有构造方法,不论是私有的还是公用的。
4.7 获取属性
同样获取属性也是类似的,通过如下方法:
public Field getField(String name)
public Field[] getFields()
public Field getDeclaredField(String name)
public Field[] getDeclaredFields()
4.8 获取注解
获取注解的方式和上述是一致的,有如下方法:
public Annotation[] getAnnotations()
public <A extends Annotation> A getAnnotation(Class<A> annotationClass)
public <A extends Annotation> A getDeclaredAnnotation(Class<A> annotationClass)
public Annotation[] getDeclaredAnnotations()
除了这几个常用的方法之外,JDK 8还引入了两个方法:
public <A extends Annotation> A[] getDeclaredAnnotationsByType(Class<A> annotationClass)
public <A extends Annotation> A[] getAnnotationsByType(Class<A> annotationClass)
由于JDK 8引入了重复注解的支持,也就是使用@Repeatable
,这两个方法就是用于返回重复注解的类型。还有一个方法,用于判断某一个类是否有某注解:
// 如果存在指定注解返回true,否则返回false
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
4.9 获取枚举类型
Class对象提供了一个方法用于获取枚举对象并转换为数组:
public T[] getEnumConstants()
我们通过一个例子来看一下。首先,定义枚举类:
public enum RESULT_STATE {
PASS("已通过", 1),
REJECT("未通过", -1),
PROCESSING("进行中", 2);
private final String name;
private final Integer value;
//get,set,构造方法省略
}
然后使用反射获取枚举对象,并执行对应的getName方法:
public static void main(String[] args) throws Exception {
// 枚举类定义在了类MainTest内部
Class<?> class1 = Class.forName("reflection.MainTest$RESULT_STATE");
Method method = class1.getDeclaredMethod("getName");
if (class1.isEnum()) {
List<?> list = Arrays.asList(class1.getEnumConstants());
for (Object enu : list) {
System.out.println(method.invoke(enu));
}
}
}
这里再多说一点,Class提供了多个以is
开头的方法,用来判断该Class对象的实际类型,比如:
public boolean isEnum() // 是否是枚举类型
public native boolean isInterface() // 是否是接口
public native boolean isArray() // 是否是数组
public boolean isAnnotation() // 是否是注解
剩余的有兴趣的童鞋可以自行查看源代码。
对于枚举的获取,我们可以借助反射写一个通用的方法:根据枚举类型获取枚举的所有对象,或者根据枚举的名称来获取所有对象:
public static Map<String, Integer> convertEnumToMap(Enum enu) {
Map<String, Integer> map = new LinkedHashMap<>();
try {
Class<?> cls = enu.getDeclaringClass();
Method nameMethod = cls.getMethod("getName");
Method valueMethod = cls.getMethod("getValue");
// 针对方法,我们也可以通过cls.cls.getDeclaredMethods()来获取
Object[] objects = cls.getEnumConstants();
for (Object object : objects) {
map.put(String.valueOf(nameMethod.invoke(object)), (Integer)valueMethod.invoke(object));
}
return map;
} catch (ReflectiveOperationException e) {
e.printStackTrace();
}
return map;
}
调用的时候:
Map<String, Integer> map = convertEnumToMap(RESULT_STATE.PASS);
// output : {已通过=1, 未通过=-1, 进行中=2}
4.10 invoke方法
当我们通过反射从类中获取了一个方法后,我们就可以用invoke()方法来调用这个方法。invoke方法的功能就是用来在运行时动态地调用某个实例的方法,来先看个简单的例子:
Class<Person> cls = Person.class;
//先实例化对象
Object obj = cls.newInstance();
// 然后获取对应类的方法
Method method = cls.getMethod("getCity");
// 执行方法
Object result = method.invoke(obj);
System.out.println(result);
该方法就相当于我们执行了Person类的 getCity 方法,然后打印出返回的值。其中invoke方法的第一个参数是实例化之后的对象,第二个参数是个可变参数,是要调用方法的参数;而如果要访问静态方法的话,只需要将invoke方法的对象参数设置为null即可:
// 然后获取对应类的静态方法
Method method = cls.getDeclaredMethod("test");
// 执行方法
Object result = method.invoke(null);
在使用中,我们经常能看到无论是Field,Method还是Constructor都有一个setAccessible
方法,该方法是用来控制Java的访问控制检查,比如执行私有的方法或属性等。我们来简单了解下:
invoke方法在执行的时候,会先进行权限检查,也就是检查AccessibleObject类中的override值,而AccessibleObject 类是 Field、Method 和 Constructor 对象的基类,它提供了将反射的对象标记为在使用时取消默认 Java 语言访问控制检查的能力。override的值默认是false,表示调用方法的时候需要检查访问控制的权限;我们可以用setAccessible方法设置为true,若override的值为true,表示忽略权限规则,调用方法时无需检查权限(也就是说可以调用任意的private方法,违反了封装)。
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
而有关invoke源码解读,强烈推荐深入解析Java反射(2) - invoke方法
5. 如何让一个方法不被反射调用?
正常情况下,无论是私有的,共有的方法,还是静态的,非静态的方法,都可以通过反射来进行调用,不过知乎上的老哥提供了一种相关的解决方案:
对于私有方法,在私有方法的内部,来判断方法的堆栈来将其限定到当前类所属方法。虽然不知道行不行得通,但可以试试。
知乎地址:https://www.zhihu.com/question/47896687?sort=created
6. 反射很慢么?
正常情况下,如果我们测试过反射的性能的话,会发现反射会比直接调用慢一些。而反射的慢是因为由于Java本身是一种静态语言,一般情况下类型在编译期间已经确定了,而反射涉及到类型的动态处理,Java的编译器如JIT 没办法对反射相关的代码进行优化,并且反射执行的时候还有安全检查,访问控制等操作,所以说反射可能会比常规的调用慢一些,不过基本每次JDK 版本更新,反射的性能都会被优化一些,所以目前的反射其实不会慢太多。
这里参考:Java 反射到底慢在哪里?,感谢R神的回复。
7. JDK 7处理反射方法的异常
在JDK 7之前,当调用一个反射方法时,不得不捕获多个不相关的检查期异常,比如:
Class.forName("jdk8.stream.GoodsInfo").getMethod("getName").invoke(null,new String[]{});
上面这行代码通过Java反射动态加载一个类,并调用它的某个方法,为了保证编译器能通过,我们能需要捕获如下异常:
try {
Class.forName("jdk8.stream.GoodsInfo").getMethod("getName").invoke(null,new String[]{});
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
当然,借助于JDK7新的异常处理的特性,我们可以通过一个catch分支来捕获:
try {
Class.forName("jdk8.stream.GoodsInfo").getMethod("getName").invoke(null,new String[]{});
} catch (IllegalAccessException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
不过,JDK 7引入了一个新的反射操作相关异常的父类 ReflectiveOperationException
,这样的话就可以通过这一异常来捕获所有其他反射操作相关的子类异常,从而使我们的代码更加简洁:
try {
Class.forName("jdk8.stream.GoodsInfo").getMethod("getName").invoke(null,new String[]{});
} catch (ReflectiveOperationException e) {
e.printStackTrace();
}
8. 使用反射越过泛型检查
拿一个例子来说,比如定义一个字符串类型的List,List<String> list
,如果我们想往里面添加一个Integer
类型的元素,能添加进去么?正常情况下,是不行的,因为编译的时候就直接提示错误了,不过通过反射我们可以实现:
public static void main(String[] args) throws Exception {
List<String> list = new ArrayList<>();
list.add("hello");
list.add("world");
Class<?> cls = list.getClass();
Method method = cls.getMethod("add", Object.class);
method.invoke(list, 100);
System.out.println(list);
// output: [hello, world, 100]
}
因为泛型是用于编译期间的,编译过后泛型擦除,所以我们才可以借助反射来实现。
9. 总结
到这里,有关反射的内容基本就学习完了,现在来简单总结一下。反射增加了程序的灵活性,所以在一般的框架中使用比较多,如Spring等。反射的功能很强大,在一定程度上可以说是破坏了Java语言封装的特性,另外反射调用的时候可以忽略权限的检查,从而可能会导致对象的安全性问题。不过,我们不妨换一个角度来思考,思考下什么是封装?什么是安全?
- 所谓封装,就是将具体的实现细节隐藏,将实现后的结果通过共有方法返回给外部调用,而针对私有方法,即使别人能通过反射的方式调用,但即使调用但却得不到一个完整的结果,因为一般情况下只有共有方法的返回才是完整的。从这一点来说,封装性其实没有被破坏。
- 而所谓安全,如果是为了保护源码的话,那其实没必要,即使不通过反射,也有其他方式获取源码。
- 因为Java语言毕竟是一种静态语言,为了让语言拥有动态的特性,必须要有反射机制,而反射机制本身就是底层的处理,不可能按常规的封装特性来处理。也就是说不给调用私有方法的能力,很多程序受到局限,那么实现起来就麻烦了。
- 所以说我们可以认为,反射机制只是提供了一种强大的功能,使得开发者能在封装之外,按照特定的需要实现一些功能。没有太多的必要纠结于反射是否安全等问题。
备注:TODO Java获取泛型,有时间了解下。
本文参考自:
Java内省机制
Java为什么支持反射机制?
深入解析Java反射(1) - 基础
java反射机制是不是破坏了JAVA的卦装性呢