1,概述
JAVA 反射机制是在运行状态中,对于任意一个类,都能够找到这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。
JDK中关于反射的相关类型都在 java.lang.reflect
包下,并不需要额外的第三⽅包来完成反射的⼯
作.
1.1 什么是反射
反射就是可以在运行时加载类信息,并可以创建其对象访问其属性和方法。这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。
1.2 反射的功能
Java 反射机制主要提供以下功能:
在运行时判断任意一个对象所属的类;
在运行时构造任意一个类的对象;
在运行时判断任意一个类所具有的成员变量和方法;
在运行时调用任意一个对象的方法;
生成动态代理。
2,Class对象
Class对象就是⼀个类被加载到jvm中后的运⾏时表现.⽽反射的⾸要任务就是得到Class对象.得到Class对象的⽅法如下
- Class.forName(“类的全称”)
- 类加载器获取
- 类的class字段
- 对象的getClass⽅法
2.1 Class.forName()
这种⽅法适合只知道类的字符串表示,也就是全称的情况.如果类还没有加载它还会加载此类,如果已经加载了就会返回已加载的class对象给你,⽐如:
Class personCls = Class.forName("com.Person");
2.2 类的class字段
这种⽅式是此类已经预先知道的情况下就⾮常合适,⽐如下⾯的代码
Class personCls = Person.class;
Class integerCls = Integer.class;
2.2.1 基本类型的class对象
对于8个基本类型想获取其class对象信息除了可以⽤class字段的⽅式以外还可以利⽤其对应包装类型的TYPE字段来获取,⽐如:
Class intCls = int.class;
Class intCls = Integer.TYPE;
2.3 对象的getClass方法
这种⽅式适合于已经有此类对象的情况下来获取类的class对象信息,⽐如下⾯的代码:
Person p = new Person();
Class personCls = p.getClass();
2.4 Void类对象
java⽅法的返回类型中有⼀个特殊的值就是void,其⽤java.lang.Void
类来代表.所以获取此特殊的Class对象就⽤Void的class
字段或TYPE
字段来实现
Class<Void> clazz = Void.class;
Class<Void> clazz = Void.TYPE;
3,Class对象的基本操作
有了Class对象之后,我们就可以利⽤它来获取各种各样的信息,主要可以获取的信息有如下⼀些:
- 获取类的名字
- 类的⽗类
- 类的修饰符
- 类实现的所有接⼝
- 类的构造函数
- 类的所有字段
- 类的所有⽅法
3.1 获取类的名字
getSimpleName()
getName()
getCanonicalName()
getTypeName()
类的名字分为简称和全称,比如下面的累其简称为Person,全称为com.Person
package com;
public class Person{}
通过反射获取类的名称方法如下:
Class<T> clazz = ...;
String simpleName = clazz.getSimpleName();
String fullQualifiedName = clazz.getName();
3.2 获取类的修饰符
getModifiers()
通过Class对象的 getModifiers
⽅法获取类的修饰符,此⽅法返回的是⼀个整数,然后依赖 Modifier 类的⼀系
列⽅法来分析 getModifiers ⽅法返回整数的含义.
int modifier = clazz.getModifiers();
boolean isPublic = Modifier.isPublic(modifier);
boolean isAbs = Modifier.isAbstract(modifier);
3.3 获取包的信息
getPackage()
可以通过Class对象对的 getPackage()
⽅法获取类的包信息,此⽅法返回的是Package类型,通过此Package类型就可以得到包的名字,⽐如:
Package pkg = clazz.getPackage();
String pkgName = pkg.getName();
3.4 获取父类信息
getSuperclass()
getGenericSuperclass()
getAnnotatedSuperclass()
获取⽗类信息主要是靠 getSuperClass()
⽅法实现,如果当前的Class对象代表的是Object类型,接⼝类型,void类型,基本类型,那么此⽅法返回null值
Class<?> superClazz = clazz.getSuperclass();
String superClassName = superClazz.getSimpleName();
3.5 获取实现的接口
getInterfaces()
getGenericInterfaces()
getAnnotatedInterfaces()
可以通过 getInterfaces
⽅法获取实现或继承的接⼝信息.如果当前的Class对象代表的是⼀个类,那么此⽅法得到是此类声明实现的所有接⼝信息,不包含其⽗类实现的接⼝信息.返回的数组中按声明的顺序排序.如果没有实现接⼝就返回⻓度为0的数组.
如果当前的Class对象代表的是⼀个接⼝,那么此⽅法返回的是此接⼝extends的所有接⼝信息,返回的数组中按照声明的接⼝顺序排序.如果没有继承任何接⼝,返回的数组是⼀个⻓度为0的数组.
如果当前的Class对象代表的是void或者基本类型,此⽅法返回⻓度为0的数组.
如果当前的Class对象代表的是数组类型,那么返回的是 Cloneable
和 Serializable
Class[] personInterfaces = clazz.getInterfaces();
3.6 获取构造函数
getConstructors()
getConstructor()
getDeclaredConstructor()
getEnclosingConstructor()
getDeclaredConstructors()
通过 getConstructors
⽅法可以获取类的所有构造函数.⽐如下⾯的⽅法:
Constructor<?>[] constructors = clazz.getConstructors();
3.7 获取字段
getField()
getFields()
getDeclaredFields()
getDeclaredField()
字段分为类⾃⼰声明(Declare)的和从⽗类型继承过来的字段,想得到所有的字段(不区分是继承的还是⾃⼰声明的)就通过 getFields
⽅法,通过 getDeclaredFields()
⽅法可以获取此Class对象代表的类声明的所有字段,不包括继承过来的字段.⽐如下⾯的代码
Field[] fields = clazz.getDeclaredFields();
上⾯的⽅法可以得到任意修饰符的字段,静态与实例字段可以得到,public,private等访问修饰符的字段也可以得到.如果想得到某⼀个具体名字的字段可以通过 getDeclaredFields(String name)
⽅法获取
Field f = clazz.getDeclaredFields("字段名");
如果Class对象代表的类型没有字段或者代表的是数组类型,基本数据类型或者void类型会返回⼀个⻓度为0的数组.
返回数组中的元素并没有进⾏排序,并且并没有特定的顺序,⽐如按照声明的顺序,所以你的代码不能依赖
反射中得到的字段的顺序来编写逻辑.
⽽ getFields ⽅法会返回⾃身和继承过来的所有 public 字段.并不包含其它修饰符的字段
3.8 获取方法
getMethod()
getMethods()
getEnclosingMethod()
getDeclaredMethods()
getDeclaredMethod()
⽅法也分为类⾃⼰声明(Declare)的和从⽗类型继承过来的.可以通过 getDeclaredMethods()
⽅法获取此Class对象代表的类声明的所有⽅法,通 过 getMethods()
⽅法获取所有的⽅法.⽐如下⾯的代码
Method[] methods = clazz.getDeclaredMethods();
如果想获得特定的⽅法,就需要传递⽅法名与参数类型,因为⽅法有重载.⽐如下⾯的代码表示取得只有⼀个String类型参数的⽅法doSth.
Method method = clazz.getDeclaredMethods("doSth",String.class)
getDeclaredMethods的第⼆个参数是可变⻓度的,因为⽅法的参数可以有多个.
如果⼀个类只有静态代码块,那么getDeclaredMethods返回的数组中不包括这个静态代码块.
如果Class对象代表的类型没有⽅法或者代表⼀个数组,基本类型,void类型,那么返回的是⻓度为0的数组.
返回数组中的元素并没有进⾏排序,并且并没有特定的顺序,⽐如按照声明的顺序,所以你的代码不能依赖反射中得到的⽅法的顺序来编写逻辑.
⽽getMethods⽅法会返回⾃身和继承过来的所有 public ⽅法.并不包含其它修饰符的⽅法.
3.9 实例化对象
newInstance()
可以通过Class对象直接实例化⼀个类的对象出来.⽐如下⾯的代码
Class<Person> clazz = ...;
Person p = clazz.newInstance();
这个⽅法必须要求类有⼀个 nullary constructor ,也就是⽆参的构造函数.默认构造就属于 nullary
constructor .如果没有这样的构造函数,那么调⽤ newInstance()
⽅法时会抛出异常.
如果没有⽆参的构造函数,就不能这样实例化对象了,只能靠反射先获取Constructor对象,然后通过
Constructor来创建对象.
默认构造函数与⽆参构造函数这2个术语有⼀点点的区别.默认构造函数⽐较强调由编译器⽣成
(c++领域不强调编译器⽣成,⾃⼰写的也算默认构造函数)以及是public修饰符的,⽆参构造函数不强
调是public的,也不强调是编译器⽣成https://en.wikipedia.org/wiki/Default_constructor
https://en.wikipedia.org/wiki/Nullary_constructor
3.10 isInstance()
isInstance()
Class对象的 isInstance
⽅法的作⽤等价于 instanceof
操作符.⽐如下⾯的代码会返回true
Person person = new Person();
Class<Person> clazz = Person.class;
System.out.println(clazz.isInstance(person));
4,构造函数
由于⼀个类可以有多个构造函数,所以反射API提供了 getConstructors()
⽅法得到所有的构造函数,⽤
getConstructor(Class… parameterTypes)
来得到某⼀个具体的构造函数.⽐如下⾯的代码
Constructor<?>[] constructors = clazz.getConstructors();
Constructor<?> cons1 = clazz.getConstructor();
Constructor<?> cons2 = birdClass.getConstructor(String.class);
如果没有对应参数类型的构造函数会抛出 NoSuchMethodException 异常.
有了Constructor对象之后,我们就可以调⽤调⽤其 newInstance
⽅法来实例化对象,其作⽤等价于new⼀个
类的对象时指定调⽤这个构造函数.
cons1.newInstance();
cons2.newInsance("hello");
5,方法
获得了Method对象后,我们可以获取⽅法的名字,可访问性以及调⽤⽅法等操作.⽐如下⾯的代码获取了⽅
法的名字,可访问性
Method m = ...;
boolean access = m.isAccessible();//获取可访问性
String name = m.getName();//得到⽅法名字
int parameterCount = m.getParameterCount();//得到⽅法参数个数
Parameter[] ps = m.getParameters();//获取所有参数信息
5.1 调用方法
反射的⽅式调⽤⽅法主要是靠Method对象的 invoke(Object obj,Object…args)
⽅法来完成.其第⼀个参数是此⽅法所属类的对象,如果这个⽅法是个静态⽅法,这个参数可以传递null值,第⼆个参数就是⽅法需要的参数数据,是⼀个可变⻓度的参数.因为⽅法的参数个数可以有任意数量.
invoke⽅法的返回值就是反射⽅法调⽤后的返回值.
假设⼀个类中有下⾯的⽅法:
public void doSth(String name) {
System.out.println(name + " do sth");
}
通过反射调⽤的⽅法的实现代码如下:
Class<Person> clazz = Person.class;
Person p = clazz.newInstance();
Method m = clazz.getDeclaredMethod("doSth", String.class) ;
m.invoke(p,"cj");//p.doSth("cj")
如果⽅法不能访问,仍然需要通过 setAccessible
来调整访问性.⽅法调⽤完毕之后再调回其访问性
Method m = clazz.getDeclaredMethod("doSth", String.class) ;
m.setAccessible(true);
m.invoke(p,"cj");
m.setAccessible(false);
5.2 方法参数
⽅法的参数可以通过Method对象来获取,主要有以下⼏个⽅法来取得参数相关的信息
getParameterCount()
: 获取⽅法参数个数getParameterTypes()
:获取⽅法的参数类型数组getParameters()
:获取所有的参数信息
int paramCount = method.getParameterCount();
Class[] paramClz = method.getParameterTypes();
Parameter[] parameters = method.getParameters();
5.2.1 Parameter
反射时通过Parameter类型来代表⽅法的参数信息,通过此类型可以获取参数的名字,类型,在参数上修饰的注解,参数的修饰符等信息.
//获取⽅法的第⼀个参数
Parameter parameter = method.getParameters()[0];
String name = parameter.getName();
//得到参数的类型信息
Class[] paramClz = parameter.getParameterTypes();
int modifier = parameter.getModifiers();
//得到参数上声明的注解信息
Annotation[] annos = parameter.getDeclaredAnnotations();
其中参数名默认是得不到的,得到是arg0,arg1这样的名字,需要给编译器提供额外的参数 -parameters ,以便编译的时候保留参数相关的调试信息,这样反射的时候就可以获取参数的名字信息
6,字段
我们反射得到了某个Field对象之后,就可以得到此Field的名字,类型以及给字段赋值等操作.⽐如下⾯的代码就得到了字段的名字与类型
Field field = ...;
Class<?> fieldClass = field,getType();
String name = field.getName();
int modifier = field.getMofiers();//得到修饰符
6.1 获取字段值
获取字段主要是靠 get()
方法来完成,如果你确定字段的类型,可以调用对应类型的方法,比如调用 getInt 方法获取整数字段的值,这种以 get 开头获取字段值得方法主要针对基本类型,所以有8个这样的方法。
获取字段值分为获取静态与实例字段的值。如果是静态字段,调用上述方法获取字段是传递 null ,如果是实例字段的话,必须传递此字段所属类的对象给方法。
取静态字段的值:
Class<Person> clazz = Person.class;
Person p = clazz.newInstance();
//sa 是类的一个public static字段
Field sa = clazz.getDeclaredField("sa");
System.out.print(sa.get(null));
System.out.print(sa.getInf(null));
获取实例字段的值:
Class<Person> clazz = Person.class;
Person p = clazz.newInstance();
Field a = clazz.getDeclaredField("a");
System.out.print(a.get(p));
System.out.print(a.getInt(p));
上面获取字段的值得方式,必须去报字段的修饰符是可以访问的,否则会抛出 IllegalAccessException
如果字段的修饰符限制了访问,可以通过修改其访问性来获取字段值获取调用对应的getter方法来获取字段的值,比如下面的代码就是修改修饰符的方式来获取字段的值
Field a = clazz.getDeclareField("a");
a.setAccessible(true);
System.out.print(a.get(p));
a.setAccessible(false);
修改可访问性只是设定是否跳过java语言的访问控制检查,并不是修改了字段的修饰符
如果一个不能访问的字段,比如私有字段有对应的getter方法,那么可以通过调用getter方法来获取字段的值,不过遗憾的是Field类型并没有提供获取此字段对应的getter的API
6.2 设置字段值
设置值与获取值是类似的,主要是通过 set()
方法以及setDouble,setInt等方法来完成,比如下面的代码
Field a = clazz.getDeclaredField("a");
a.setAccessible(true);
a.set(p,600);
System.out.print(a.get(p));
a.setAccessible(false);
7,数组与反射
数组是某个类型对象的一个合集,基于这个认识,数组的反射与普通的反射有一点点不同,比如下面的代码创建了一个长度为10的字符串数组
Class cls = Class.forName("java.lang.String");
Object arr = Array.newInstance(cls, 10);
Array.set(arr, 5, "this is a test");//设置第六个位置的字符串值
String s = (String)Array.get(arr, 5);
System.out.println(s);
String[] arr2 = (String[])arr;
System.out.println(arr2[5]);
其中Array类是在 java.lang.reflection
包下面的一个类型
8,泛型的反射
经常会说在编译的时候会擦除所有的泛型信息,这样在运⾏时你就不能得到 任何 的泛型信息,这并不完全
正确.在某些情况下,在运⾏时是可以得到⼀些泛型相关的信息的.
当你通过反射检查 java.util.List
这样的泛型接⼝时,你是不能得到任何的泛型信息的,因为这个类型不知道
会参数化为哪个具体的类型.
但⼀个对参数化的泛型实例的引⽤是有办法得到⼀些泛型信息的,⽐如下⾯的代码中,如果mylist是⼀个类
的字段,此字段引⽤的是⼀个实例化的泛型类型,通过mylist字段是可以得到类型实参String的.
List<String> mylist = new ArrayList<String>();
总⽽⾔之,只有通过实参化的泛型类型的引⽤来得到泛型信息,⽽不能通过泛型本身得到泛型信息,这些引
⽤主要是如下⼏种情况:
字段
⽅法的返回值
⽅法的参数
下⾯通过具体的代码来演示这三种情况,所有的代码都是在下⾯类的基础上进⾏的
public class MyClass {
protected List<String> stringList ;
public List<String> getStringList(){
return this.stringList;
}
public void setStringList(List<String> list){
this.stringList = list;
}
}
8.1 方法的泛型返回类型
getStringList
⽅法的返回类型是⼀个以String作为List泛型的实参的类型,想得到实参String就可以通过下
⾯的⽅式来获得
Method method = MyClass.class.getMethod("getStringList", null);
Type returnType = method.getGenericReturnType();
if(returnType instanceof ParameterizedType){
ParameterizedType type = (ParameterizedType) returnType;
Type[] typeArguments = type.getActualTypeArguments();
for(Type typeArgument : typeArguments){
Class typeArgClass = (Class) typeArgument;
//输出java.lang.String
System.out.println("typeArgClass = " + typeArgClass);
}
}
8.2 方法的泛型参数类型
setStringList
⽅法的参数是⼀个实参为String的List泛型类型,想通过反射得到实参String的信息的代码如
下:
method = Myclass.class.getMethod("setStringList", List.class);
Type[] genericParameterTypes = method.getGenericParameterTypes();
for(Type genericParameterType : genericParameterTypes){
if(genericParameterType instanceof ParameterizedType){
ParameterizedType aType = (ParameterizedType) genericParameterType;
Type[] parameterArgTypes = aType.getActualTypeArguments();
for(Type parameterArgType : parameterArgTypes){
Class parameterArgClass = (Class) parameterArgType;
System.out.println("parameterArgClass = " + parameterArgClass);
}
}
}
8.3 字段的泛型类型
字段 stringList 声明的是⼀个实参为String的List类型,通过反射得到实参String的⽅法如下:
Field field = MyClass.class.getField("stringList");
Type genericFieldType = field.getGenericType();
if(genericFieldType instanceof ParameterizedType){
ParameterizedType aType = (ParameterizedType) genericFieldType;
Type[] fieldArgTypes = aType.getActualTypeArguments();
for(Type fieldArgType : fieldArgTypes){
Class fieldArgClass = (Class) fieldArgType;
System.out.println("fieldArgClass = " + fieldArgClass);
}
}
6,综合案例
反射通常⽤在bean之间的拷⻉,从Map中拷⻉数据到bean中,以及把数据库中读取的记录转换为⼀个bean
等等场景中.
从 Map 生成 Bean
public <T> T getBean(Map<String,Object> source,Class<T> clazz) throws IllegalAccessException, InstantiationException, NoSuchFieldException {
T t = clazz.newInstance();
for(Map.Entry<String,Object> entry : source.entrySet()){
String key = entry.getKey();
Object value = entry.getValue();
Field field = clazz.getField(key);
field.setAccessible(true);
field.set(t,value);
field.setAccessible(false);
}
return t;
}
调用
Map<String,Object> map = new HashMap<>();
map.put("stuId",100);
map.put("stuName","syl");
map.put("stuAge","2001-10-21");
map.put("stuSex","男");
StudentEntity bean = new Demo1().getBean(map, StudentEntity.class);
System.out.println(bean);
参考资料
https://www.oracle.com/technetwork/articles/java/javareflection-1536171.html
https://docs.oracle.com/javase/tutorial/reflect/member/methodparameterreflection.html