深入理解Java反射机制原理、使用方法

目录

一、反射基础

1. 反射的用途

2. 了解反射的底层运作

直接使用类 

使用反射

总结

3. 反射的缺点

二、在Java中使用反射

1. 获取类型信息

1.1. Object.getClass()

1.2. XXX.class

1.3. Class.forName()

1.4. Integer.TYPE

1.5. 通过反射类ClassAPI获取类

2. 获取类的成员变量

2.1. 获取字段:

2.2. 获取方法:

2.3. 获取构造器:

3. 操作java.lang.reflect.Field类

3.1. 获取字段类型:

3.2. 获取字段修饰符:

3.3. 获取和设置字段值:

4. 反射修改final修饰的属性值

5. 操作java.lang.reflect.Method类

5.1. 获取方法类型的信息:

6. 操作java.lang.reflect.Constructor类

结语


参考:The Reflection API

      深入理解Java虚拟机第三版

​​


一、反射基础

1. 反射的用途

反射功能通常用于检查或修改Java虚拟机运行中(runtime)的应用程序的行为,这一句话就精准的描述了反射的全部功能,更详细来说可以分为以下几点:

        1. 在运行中分析类的能力,可以通过完全限定类名创建类的对象实例。

        2. 在运行中查看和操作对象,可以遍历类的成员变量。

        3. 反射允许代码执行非反射代码中非法的操作,可以检索和访问类的私有成员变量,包括私有属性、方法等。

注意:要有选择的使用反射功能,如果可以直接执行操作,那么最好不要使用反射。

2. 了解反射的底层运作

为了彻底理解反射的原理,可以先理解一下虚拟机的工作机制。

通常,java在编译之后,会将Java代码生成为class源文件,JVM启动时,将会载入所有的源文件,并将类型信息存放到方法区中;将所有对象实例存放在Java堆中,同时也会保存指向类型信息的指针。

深入理解Java反射机制原理、使用方法_第1张图片

以下分两种情况来分析,直接使用类和使用反射的区别,以此理解反射的实现原理。

直接使用类 

正常流程下,我们要创建一个类的实例,是一定确定这个类的类型信息的,我们知道这个类的名字、方法、属性等等。我们可以很容易的创建实例,也可以通过实例很容易的获取属性、调用方法。

        ArrayList list = new ArrayList<>();
        list.add("A");
        int size = list.size();

使用反射

在一个方法中,如果我们不知道在实际运行(runtime)时,它将要处理的对象是谁,它的类型信息是怎么样的,那我们如何访问这个对象或为这个对象创建一个新的实例呢?

与直接使用类相反,我们需要先获取到对象在方法区类型信息,获取到类型信息后,我们就知道这个类的构造器、属性、方法、注解、子类、父类等等信息了,这个时候,我们就可以通过这些类型信息来回调处理对象,来完成自己想要的操作了。

没错,这就是反射的原理了。反射在运行时,通过读取方法区中的字节码,来动态的找到其反射的类以及类的方法和属性等(实际上就是在运行时,根据全类型名在方法区找对应的类),用这些类型信息完成对该类实例的操作,其实就是直接使用类的一个逆向使用。

void reflectMethod(Object obj) {
        // 处理这个无法明确类型的实例对象

        // 获取类型信息
        Class aClass = obj.getClass();
        Field[] fields = aClass.getFields();
        Method[] methods = aClass.getMethods();
        Annotation[] annotations = aClass.getAnnotations();
        Constructor[] constructors = aClass.getConstructors();
        Class[] interfaces = aClass.getInterfaces();
        // ...
        // 操作属性或方法
        Field field = fields[0];
        Object o = field.get(obj); // 获取obj的属性值
}

在实际开发过程会遇到很多这种情况,譬如常用到的Bean属性工具类org.springframework.beans.BeanUtils.copyProperties(Object source, Object target),在复制对象属性前,它是并不知道source、target这两个对象有什么属性的,那么这个工具类是如何完成属性复制呢?这里其实就用到了反射功能。可以简单了解下流程:

  • 获取target的类型
  • 获取target类中属性、getter和setter方法
  • 遍历target中的属性,查询source中是否有属性名相同且支持getter和setter的属性
  • 通过source.getter.invoke方法读取值
  • 最后通过target.setter.invoke(source.getter.invoke) 设置刚刚从source读取的值
  • 循环遍历target所有属性后,就完成了整个属性的复制

这里只是一个简单的反射运用,感兴趣的可以看看源码

总结

直接使用是在运行前就明确类型信息,然后在运行时根据这个类来操作对象;

而反射是运行时先拿到对象,根据对象得到方法区中的类型信息后,再根据属性、方法来操作该对象。

3. 反射的缺点

1. 额外的性能开销(Performance Overhead):由于反射涉及动态类型的解析,它无法执行某些Java虚拟机优化,因此反射操作的性能通常要比非反射操作慢。

2. 安全限制(Security Restrictions):反射需要运行时操作权限,此操作可能在一些安全管理器下不被允许。

3. 内部泄露(Exposure of Internals):由于反射允许代码执行非反射代码中非法的操作(例如访问私有字段和方法),因此使用反射可能会导致意外的副作用,这可能会使代码无法正常工作并可能破坏可移植性。反射性代码破坏了抽象,因此可能会随着平台的升级而改变行为。


二、在Java中使用反射

1. 获取类型信息

1.1. Object.getClass()

从一个实例对象中获取它的类。这仅适用于继承自Object的引用类型(当然Java的类默认继承于Object)。

Map hashMap = new HashMap<>();
Class aClass = hashMap.getClass();
String text = "text";
Class aClass1 = text.getClass();


// Object类源码
public final native Class getClass();

1.2. XXX.class

直接从未实例化的类获取类。

Class integerClass = int.class;
Class hashMapClass = HashMap.class;

1.3. Class.forName()

通过完全限定类名获取类。即包名加类名(java.util.HashMap)。否则会报找不到类错误。

Class hashMapClass = Class.forName("java.util.HashMap");


// class类源码
public static Class forName(String className)
            throws ClassNotFoundException {
    Class caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

1.4. Integer.TYPE

基本类型的包装类通过TYPE获取类。都是Java早期版本的产物,已过时。

// Integer
@SuppressWarnings("unchecked")
public static final Class  TYPE = (Class) Class.getPrimitiveClass("int");


// Double
@SuppressWarnings("unchecked")
public static final Class   TYPE = (Class) Class.getPrimitiveClass("double");

1.5. 通过反射类ClassAPI获取类

注意,只有在已经直接或间接获得一个类的情况下,才可以访问这些API。

try {
  Class className = Class.forName("java.lang.String");
  // 获取父类
  Class superclass = className.getSuperclass();
  // 返回调用类的成员变量,包括所有公共的类、接口和枚举
  Class[] classes = className.getClasses();
  // 返回调用类的依赖,包括所有类、接口和显式声明的枚举
  Class[] declaredClasses = className.getDeclaredClasses();
} catch (ClassNotFoundException e) {
  e.printStackTrace();
}

2. 获取类的成员变量

2.1. 获取字段:

Class API

是否是列表

是否获取父类属性

能否获取私有成员

getDeclaredField()

no

no

yes

getField()

no

yes

no

getDeclaredFields()

yes

no

yes

getFields()

yes

yes

no

2.2. 获取方法:

Class API

是否是列表 

是否获取父类属性

能否获取私有成员

getDeclaredMethod()

no

no

yes

getMethod()

no

yes

no

getDeclaredMethods()

yes

no

yes

getMethods()

yes

yes

no

2.3. 获取构造器:

Class API

是否是列表

是否获取父类属性

能否获取私有成员

getDeclaredConstructor()

no

N/A1

yes

getConstructor()

no

N/A1

no

getDeclaredConstructors()

yes

N/A1

yes

getConstructors()

yes

N/A1

no

3. 操作java.lang.reflect.Field类

说明:Field字段具有类型和值。Field提供访问属性对象类型信息的方法;以及获取和设置字段值的方法。

3.1. 获取字段类型:

字段可以是原始类型或引用类型。

有八种基本类型:boolean,byte,short,int,long,char,float,和double。

引用类型是java.lang.Object类的直接或间接子类,包含接口,数组和枚举类型等 。

Class className = Class.forName("java.util.HashMap");
Field table = className.getDeclaredField("table");
Class type = table.getType();

3.2. 获取字段修饰符:

  • 访问修饰符:public,protected,和private
  • 仅用于字段的控制运行时行为的修饰符:transient和volatile
  • 限制单实例的修饰符: static
  • 禁止值修改的修饰符: final
  • 注解
Class className = Class.forName("java.util.HashMap");
Field table = className.getDeclaredField("table");
// 获取属性的名字
String name = table.getName();
// 获取属性的类型
Class type = table.getType();
// 获取修饰符
int modifiers = table.getModifiers();
System.out.println(Modifier.toString(modifiers));
// 获取注解
Override annotation = table.getDeclaredAnnotation(Override.class);
Annotation[] declaredAnnotations = table.getDeclaredAnnotations();

3.3. 获取和设置字段值:

给定一个类的实例,可以使用反射来设置该类中字段的值。通常仅在特殊情况下无法以常规方式设置值时才执行此操作。因为这样的访问通常会违反该类的设计意图,所以应绝对谨慎地使用它。

HashMap map = new HashMap<>();
map.put("1", 2);
Class mapClass = map.getClass();
Field capacity = mapClass.getDeclaredField("MAXIMUM_CAPACITY");
capacity.setAccessible(true); // 访问私有成员
Object o1 = capacity.get(map); // 获取属性值
capacity.set(map, 20); // 设置属性值

上面的设置属性值将会报错,因为hashmap中的MAXIMUM_CAPACITY参数是一个被static修饰的成员。

Exception in thread "main" java.lang.IllegalAccessException: Can not set static final int field java.util.HashMap.MAXIMUM_CAPACITY to java.lang.Integer

注意:通过反射设置字段的值会有一定的性能开销,因为必须进行各种操作,例如验证访问权限。从运行时的角度来看,效果是相同的,并且操作是原子的,就好像直接在类代码中更改了值一样。除此之外,反射会破坏Java原本的设定,列如可以重新设置final属性的值等。

4. 反射修改final修饰的属性值

反射功能强大,能修改private以及final修饰的变量。如下代码中,展示了JVM的优化以及反射的一些劣势。

@Data
public class FieldReflectDemo {
  // 引用直接指向常量池中的常量值
  private final String constantStr = "FinalConstantStringField";
  // JVM优化了getter方法,直接将对constantStr引用全部替换成了常量
//  public String getConstantStr() {return "FinalConstantStringField";}


  // 在堆中新建了一个对象
  private final String newStr = new String("FinalNewStringField");
  
  public FieldReflectDemo(){}
    
    public static void main(String[] args) {
    FieldReflectDemo fieldReflectDemo = new FieldReflectDemo();
    try {
      Class className = fieldReflectDemo.getClass();
      Field constantStr = className.getDeclaredField("constantStr");
      Field newStr = className.getDeclaredField("newStr");
      // 获取实例对象的字段值
      System.out.println("constantStr原:" + constantStr.get(fieldReflectDemo));
      System.out.println("newStr原:" + newStr.get(fieldReflectDemo));
      constantStr.setAccessible(true);
      newStr.setAccessible(true);
      constantStr.set(fieldReflectDemo, "New Filed Name");
      newStr.set(fieldReflectDemo, "New Filed Name");
      System.out.println("constantStr反射修改:" + constantStr.get(fieldReflectDemo));
      System.out.println("newStr反射修改:" + newStr.get(fieldReflectDemo));
    } catch (NoSuchFieldException | IllegalAccessException e) {
      e.printStackTrace();
    }
    System.out.println("constantStr实例对象值:" + fieldReflectDemo.getConstantStr());
    System.out.println("newStr实例对象值:" + fieldReflectDemo.getNewStr());
  }
  
  /**
   * 输出
   * constantStr原:FinalConstantStringField
   * newStr原:FinalNewStringField
   * constantStr反射修改:New Filed Name
   * newStr反射修改:New Filed Name
   * constantStr实例对象值:FinalConstantStringField
   * newStr实例对象值:New Filed Name
   */
}

因为JVM在编译时期, 就把final类型的直接赋值的String进行了优化, 在编译时期就会把String处理成常量。反射成功将其值修改成功了,但是在它的get方法中,返回的不是当前变量,而是返回JVM优化好的一个常量值。

5. 操作java.lang.reflect.Method类

说明:

Method方法具有参数和返回值,并且方法可能抛出异常;

Method提供获取参数信息、返回值的方法;

它也可以调用(invoke)给定对象的方法。

5.1. 获取方法类型的信息:

方法声明包含了方法名、修饰符、参数、返回类型以及抛出的多个异常。

以及通过反射调用实例对象的方法。

public class MethodReflectDemo {


public MethodReflectDemo() {
  
private void getNothing(String name) {
  
public int getNumByName(String name) throws NullPointerException {
  if (StringUtils.isEmpty(name))
    throw new NullPointerException("名字为空");
  return name.length();
}


  public static void main(String[] args) {
    MethodReflectDemo methodReflectDemo = new MethodReflectDemo();
    try {
      Class demoClass = methodReflectDemo.getClass();
      Method method = demoClass.getDeclaredMethod("getNumByName", String.class);
      String name = method.getName();
      System.out.println("方法名:" + name);
      // 修饰符
      int modifiers = method.getModifiers();
      System.out.println("所有修饰符:" + Modifier.toString(modifiers));
      // 参数
      Parameter[] parameters = method.getParameters();
      // 返回类型
      Class returnType = method.getReturnType();
      System.out.println("返回类型:" + returnType.getTypeName());
      // 异常
      Class[] exceptionTypes = method.getExceptionTypes();
      System.out.println("");
      // 实例对象调用方法
      Object invoke = method.invoke(methodReflectDemo, "名称");
      System.out.println(invoke);
    } catch (NoSuchMethodException e) {
      e.printStackTrace();
    }
  }
}

6. 操作java.lang.reflect.Constructor类

Constructor与Method相似,但有几点不同:

  • 构造函数没有返回值
  • 构造函数无法被实例对象执行,它的调用只能为给定的类创建对象的新实例。
public class ConstructorReflectDemo {


  public ConstructorReflectDemo() {}


  private void getNothing(String name) { }


  public int getNumByName(String name) throws NullPointerException {
    if (StringUtils.isEmpty(name))
      throw new NullPointerException("名字为空");
    return name.length();
  }


  public static void main(String[] args) {
    ConstructorReflectDemo methodReflectDemo = new ConstructorReflectDemo();
    try {
      Class demoClass = methodReflectDemo.getClass();
      Constructor constructor = demoClass.getConstructor();
      String name = constructor.getName();
      System.out.println("构造方法名:" + name);
      // 修饰符
      int modifiers = constructor.getModifiers();
      System.out.println("所有修饰符:" + Modifier.toString(modifiers));
      // 参数
      Parameter[] parameters = constructor.getParameters();
      // 异常
      Class[] exceptionTypes = constructor.getExceptionTypes();
      System.out.println("");
      // 构造方法无法被调用,只可以创建新实例
      ConstructorReflectDemo constructorReflectDemo = constructor.newInstance();
    } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
      e.printStackTrace();
    }
  }


}

结语

自己也是玩心太大,很多时候都是抽空闲时间写的,所以写这一篇文章前前后后花了快一周吧。

此外,由于自己对内存模型那块还不是特别熟悉,所以错误在所难免。希望和各位大佬交流交流、

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