Java--反射技术,超详细讲解,一次性带你掌握反射机制

一. 简介

反射(Reflection) 是⼀种运⾏时检视(Inspect)类型信息并且修改属性,调⽤⽅法的机制.它常常⽤于如下⼀些场景中

  • 运⾏时实例化类的对象,⽐如实例化⼀个Servlet类,此Servlet类的名字容器预先是不知道的
  • 运⾏时调⽤⽅法,⽐如调⽤Servlet对象的service⽅法
  • 运⾏时修改属性值.⽐如Apache DbUtils⼯具库可以把数据库的⼀条记录默认依据名称相同的策略赋值⼀个对象的字段

JDK中关于反射的相关类型都在java.lang.reflect包下,并不需要额外的第三⽅包来完成反射的⼯作.2. Class对象

二.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;

三. Class对象的基本操作

有了Class对象之后,我们就可以利⽤它来获取各种各样的信息,主要可以获取的信息有如下⼀些
获取类的名字

  • 类的⽗类类的修饰符
  • 类实现的所有接
  • 类的构造函数
  • 类的所有字段
  • 类的所有⽅法

3.1. 获取类的名字

类的名字分为简称和全称,⽐如下⾯的类其简称为Person,全称为com.Person

package com;
public class Person{
     }

3.2. 获取类的修饰符

通过Class对象的getModifiers⽅法获取类的修饰符,此⽅法返回的是⼀个整数,然后依赖Modifier类的⼀系列⽅法来分析getModifiers⽅法返回整数的含义.

int modifier = clazz.getModifiers();
boolean isPublic = Modifier.isPublic(modifier);
boolean isAbs = Modifier.isAbstract(modifier);

3.3. 获取包的信息

可以通过Class对象对的getPackage()⽅法获取类的包信息,此⽅法返回的是Package类型,通过此Package类型就可以得到包的名字,⽐如:

 Package pkg = clazz.getPackage();
 String pkgName = pkg.getName();

3.4. 获取⽗类信息

获取⽗类信息主要是靠getSuperClass()⽅法实现,如果当前的Class对象代表的是Object类型,接⼝类型,void类型,基本类型,那么此⽅法返回null值

Class<?> superClazz = clazz.getSuperclass();
String superClassName = superClazz.getSimpleName();

3.5. 获取实现的接⼝

可以通过getInterfaces⽅法获取实现或继承的接⼝信息.如果当前的Class对象代表的是⼀个类,那么此⽅法得到是此类声明实现的所有接⼝信息,不包含其⽗类实现的接⼝信息.返回的数组中按声明的顺序排序.如果没有实现接⼝就返回⻓度为0的数组.

如果当前的Class对象代表的是⼀个接⼝,那么此⽅法返回的是此接⼝extends的所有接⼝信息,返回的数组中按照声明的接⼝顺序排序.如果没有继承任何接⼝,返回的数组是⼀个⻓度为0的数组.

如果当前的Class对象代表的是void或者基本类型,此⽅法返回⻓度为0的数组.

如果当前的Class对象代表的是数组类型,那么返回的是Cloneable和Serializable

Class<?>[] personInterfaces = clazz.getInterfaces();

3.6. 获取构造函数

通过getConstructors⽅法可以获取类的所有构造函数.⽐如下⾯的⽅法

Constructor<?>[] constructors = clazz.getConstructors();

3.7. 获取字段

字段分为类⾃⼰声明(Declare)的和从⽗类型继承过来的字段,想得到所有的字段(不区分是继承的还是⾃⼰声明的)就通过getFields⽅法,通过getDeclaredFields()⽅法可以获取此Class对象代表的类声明的所有字段,不包括继承过来的字段.⽐如下⾯的代码

Field[] fields = clazz.getDeclaredFields();

上⾯的⽅法可以得到任意修饰符的字段,静态与实例字段可以得到,public,private等访问修饰符的字段也可以得到.如果想得到某⼀个具体名字的字段可以通过getDeclaredFields(String name)⽅法获取

Field f = clazz.getDeclaredFields("字段名");

如果Class对象代表的类型没有字段或者代表的是数组类型,基本数据类型或者void类型会返回⼀个⻓度为0的数组.

返回数组中的元素并没有进⾏排序,并且并没有特定的顺序,⽐如按照声明的顺序,所以你的代码不能依赖反射中得到的字段的顺序来编写逻辑.

⽽getFields⽅法会返回⾃身和继承过来的所有public字段.并不包含其它修饰符的字段.

3.8. 获取⽅法

⽅法也分为类⾃⼰声明(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. 实例化对象

可以通过Class对象直接实例化⼀个类的对象出来.⽐如下⾯的代码

 Class<Person> clazz = ...;
 Person p = clazz.newInstance();

这个⽅法必须要求类有⼀个nullary constructor,也就是⽆参的构造函数.默认构造就属于nullaryconstructor.如果没有这样的构造函数,那么调⽤newInstance()⽅法时会抛出异常.

如果没有⽆参的构造函数,就不能这样实例化对象了,只能靠反射先获取Constructor对象,然后通过Constructor来创建对象.

默认构造函数与⽆参构造函数这2个术语有⼀点点的区别.默认构造函数⽐较强调由编译器⽣成
(c++领域不强调编译器⽣成,⾃⼰写的也算默认构造函数)以及是public修饰符的,⽆参构造函数不强调是public的,也不强调是编译器⽣成
/https://en.wikipedia.org/wiki/Default_constructor
https://en.wikipedia.org/wiki/Nullary_constructor

3.10. isInstance()

Class对象的isInstance⽅法的作⽤等价于instanceof操作符.⽐如下⾯的代码会返回true

Person person = new Person();
Class<Person> clazz = Person.class;
System.out.println(clazz.isInstance(person));

四. 构造函数

由于⼀个类可以有多个构造函数,所以反射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");

五. ⽅法

获得了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,以便编译的时候保留参数相关的调试信息,这样反射的时候就可以获取参数的名字信息.

在IDEA中给编译器设置编译参数如下图所示,设置完成之后需要重新编译代码,这样反射就可以取得参数名信息.
Java--反射技术,超详细讲解,一次性带你掌握反射机制_第1张图片

六. 字段

我们反射得到了某个Field对象之后,就可以得到此Field的名字,类型以及给字段赋值等操作.⽐如下⾯的代码就得到了字段的名字与类型

Field field = ...;
Class<?> fieldClass = field.getType();
String name = field.getName();
int modifier = field.getModfiers();//得到修饰符

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.println(sa.get(null));
System.out.println(sa.getInt(null));

取得实例字段的值:

Class<Person> clazz = Person.class;
Person p = clazz.newInstance();
Field a = clazz.getDeclaredField("a");
System.out.println(a.get(p));
System.out.println(a.getInt(p));

上⾯获取字段的值的⽅式,必须确保字段的修饰符是可以访问的,否则会抛出IllegalAccessException.

如果字段的修饰符限制了访问,可以通过修改其访问性来获取字段值或者调⽤其对应的getter⽅法来获取字段的值.⽐如下⾯的代码就是修改修饰符的⽅式来获取字段的值

Field a = clazz.getDeclaredField("a");
a.setAccessible(true);
System.out.println(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.println(a.get(p));
a.setAccessible(false);

七. 数组与反射

数组是某个类型对象的⼀个合集,基于这个认识,数组的反射与普通的反射有⼀点点不同.⽐如下⾯的代码创建了⼀个⻓度为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包下⾯的⼀个类型.

八.泛型的反射

经常会说在编译的时候会擦除所有的泛型信息,这样在运⾏时你就不能得到任何的泛型信息,这并不完全正确.在某些情况下,在运⾏时是可以得到⼀些泛型相关的信息的.

当你通过反射检查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);
      }
 }

九. 综合案例

反射通常⽤在bean之间的拷⻉,从Map中拷⻉数据到bean中,以及把数据库中读取的记录转换为⼀个bean等等场景中.

9.1. 从Map⽣成bean

⽤过servlet的都知道,我们获取请求数据时需要把请求的数据封装到⼀个bean中,⽽Servlet中所有的请求的数据是放在⼀个map中的,基于这种情况,下⾯的案例是把⼀个map中的数据赋值给⼀个bean对象,map中键的名字作为bean的字段的名字,map中键对应的值作为对应字段的值.map的数据如下:

Map<String,Object> map = new HashMap<>();
map.put("id",100);
map.put("name","cj");
map.put("age",18);

bean类的代码如下:

public class UserInfoEntity {
      
    private Integer id; 
    private String name; 
    //省略getter,setter,toString()
}

实现把map中的数据拷⻉到bean的代码如下,其中map中的age键在map中没有对应的字段,所以getDeclaredField时会抛出异常,但我们捕获了.所以并不影响最终bean实例的⽣成.

public static void main(String[] args) throws Exception {
      
      Map<String,Object> map = new HashMap<>(); 
      map.put("id",100); map.put("name","cj"); 
      map.put("age",18); 
     UserInfoEntity userinfo = copyData(map,UserInfoEntity.class);  
     System.out.println(userinfo); 
  }
    public static <T> T copyData(Map<String,Object> source,Class<T> targetClass) throws 
  Exception {
      
         T instance = targetClass.newInstance();
         for(Map.Entry<String,Object> entry: source.entrySet()) {
      
            try {
      
              String key = entry.getKey();
              Field f = targetClass.getDeclaredField(key);
              f.setAccessible(true);
              f.set(instance, entry.getValue()); 
              f.setAccessible(false);
           }catch(NoSuchFieldException e) {
     
            e.printStackTrace();
           }
        } 
        return instance;
   }

最终输出的结果是:

UserInfoEntity [id=100, name=cj]

你可能感兴趣的:(java,反射)