Skip to content

Latest commit

 

History

History
213 lines (158 loc) · 11.5 KB

反射的原理,反射创建类实例的三种方式.md

File metadata and controls

213 lines (158 loc) · 11.5 KB
反射的原理,反射创建类实例的三种方式

​ 反射就是指程序在运行时能够动态的获取到一个类的类型信息的一种操作。它是现代框架的灵魂,几尽所有的框架能够提供的一些自动化机制都是靠反射实现的,这也是为什么各类框架都不允许你覆盖掉默认的无参构造器的原因,因为框架需要以反射机制利用无参构造器创建实例。

一、Class 类型信息

​ 虚拟机的类加载机制中,每一种类型都会在初次使用时被加载进虚拟机内存的方法区中,包含类中定义的属性字段,方法字节码等信息。

​ Java 中使用类 java.lang.Class 来指向一个类型信息,通过这个 Class 对象,我们就可以得到该类的所有内部信息。

​ 而获取一个 Class 对象的方法主要有三种,通过这些方法你可以得到任意类的类型信息,该类的所有字段属性方法表等信息都可以通过这个 Class 对象进行获取):

1、类名.class:

只要使用类名点 class 即可得到方法区该类型的类型信息。

2、getClass:
public final native Class<?> getClass();//Object 类 方法 
new ObjectOne().getClass();
3、forName(Class 类的静态方法):

​ 允许你传入一个全类名,该方法会返回方法区代表这个类型的 Class 对象,如果这个类还没有被加载进方法区,forName 会先进行类加载。

​ 由于方法区 Class 类型信息由类加载器和类全限定名唯一确定,所以想要去找这么一个 Class 就必须提供类加载器和类全限定名,这个 forName 方法默认使用调用者的类加载器。

public static Class<?> forName(String className) {
	// native 方法
    Class<?> caller = Reflection.getCallerClass();
    // native 方法
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

Class 类中还有一个 forName 重载,允许你传入类加载器和类全限定名来匹配方法区类型信息:

public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException {
    Class<?> caller = null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        caller = Reflection.getCallerClass();
        if (sun.misc.VM.isSystemDomainLoader(loader)) {
            ClassLoader ccl = ClassLoader.getClassLoader(caller);
            if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
                sm.checkPermission(
                    SecurityConstants.GET_CLASSLOADER_PERMISSION);
            }
        }
    }
    return forName0(name, initialize, loader, caller);
}

二、反射字段属性

​ 一个 Field 实例包含某个类的一个属性的所有信息,包括字段名称,访问修饰符,字段类型。除此之外,Field 还提供了大量的操作该属性值的方法。

Class 中有关获取字段属性的方法主要以下几个:

  • public Field[] getFields():返回该类型的所有 public 修饰的属性,包括父类的
  • public Field getField(String name):根据字段名称返回相应的字段
  • public Field[] getDeclaredFields():返回本类型中申明的所有字段,包含非 public 修饰的但不包含父类中的
  • public Field getDeclaredField(String name):同理

通过传入一个类实例,就可以直接使用 Field 实例操作该实例的当前字段属性的值:

public class ReflectClient {
    public static void main(String[] args) throws Exception {
        Class<People> cls = People.class;
        Field name = cls.getField("name");
        People people = new People();
        // 检索 People 对象是否具有一个 name 代表的字段,如果有将字符串 hello 赋值给该字段即可
        name.set(people,"hello");
        // console: hello
        System.out.println(people.name);
    }
    public static class People {
        public String name;
    }
}

三、反射方法

​ Method 抽象地代表了一个方法,同样有描述这个方法基本信息的字段和方法,例如方法名,方法的参数集合,方法的返回值类型,异常类型集合,方法的注解等。

Class 类也提供了四种方法来获取其中的方法属性:

  • public Method[] getMethods():返回所有的 public 方法,包括父类中的
  • public Method getMethod(String name, Class<?>... parameterTypes):返回指定的方法
  • public Method[] getDeclaredMethods():返回本类申明的所有方法,包括非 public 修饰的,但不包括父类中的
  • public Method getDeclaredMethod(String name, Class<?>... parameterTypes):同理invoke 方法用于间接调用其他实例的该方法
public class ReflectClient {
    public static void main(String[] args) throws Exception {
        Class<People> cls = People.class;
        Method sayHello = cls.getMethod("sayHello");
        People people = new People();
        // console:hello wrold
        sayHello.invoke(people);
    }
    public static class People {
        public void sayHello() {
            System.out.println("hello wrold ");
        }
    }
}

四、反射构造器

Class 类依然为它提供了四种获取实例的方法:

  • public Constructor<?>[] getConstructors():返回所有 public 修饰的构造器
  • public Constructor getConstructor(Class<?>... parameterTypes):带指定参数的
  • public Constructor<?>[] getDeclaredConstructors():返回所有的构造器,无视访问修饰符
  • public Constructor getDeclaredConstructor(Class<?>... parameterTypes) :同理Constructor 类中有一个 newInstance 方法用于创建一个该 Class 类型的实例对象出来:
public class ReflectClient {
    public static void main(String[] args) throws Exception {
        Class<People> cls = People.class;
        Constructor c = cls.getConstructor();
        People p = (People) c.newInstance();
        // console:hello wrold
        p.sayHello();
    }
    public static class People {
        public void sayHello() {
            System.out.println("hello wrold ");
        }
    }
}

五、反射与数组

数组是一种特殊的类型,它本质上由虚拟机在运行时动态生成,所以在反射这种类型的时候会稍有不同。

public native Class<?> getComponentType();
// Class 类中的非静态方法,返回数组 Class 实例元素的基本类型,只有当前的 Class 对象代表的是一个数组类型的时候,该方法才会返回数组的元素实际类型,其他的任何时候都会返回 null。

代表数组的这个由虚拟机动态创建的类型,它直接继承的 Object 类,并且所有有关数组类的操作,比如为某个元素赋值或是获取数组长度的操作都直接对应一个单独的虚拟机数组操作指令。同样也因为数组类直接由虚拟机运行时动态创建,所以你不可能从一个数组类型的 Class 实例中得到构造方法,编译器根本没机会为类生成默认的构造器。于是你也不能以常规的方法通过 Constructor 来创建一个该类的实例对象。

Java 中有一个类 java.lang.reflect.Array 提供了一些静态的方法用于动态的创建和获取一个数组类型:

// 创建一个一维数组,componentType 为数组元素类型,length 数组长度
public static Object newInstance(Class<?> componentType, int length)

// 可变参数 dimensions,指定多个维度的单维度长度
public static Object newInstance(Class<?> componentType, int... dimensions)

完全是因为数组这种类型并不是由常规的编译器编译生成,而是由虚拟机动态创建的,所以想要通过反射的方式实例化一个数组类型是得依赖 Array 这个类型的相关 newInstance 方法的。

六、反射与泛型

泛型是 Java 编译器范围内的概念,它能够在程序运行之前提供一定的安全检查,而反射是运行时发生的,也就是说如果你反射调用一个泛型方法,实际上就绕过了编译器的泛型检查了。我们看一段代码:

ArrayList<Integer> list = new ArrayList<>();
list.add(23);
//list.add("fads");编译不通过
Class<?> cls = list.getClass();
Method add = cls.getMethod("add",Object.class);
add.invoke(list,"hello");
System.out.println(list.get(1));

​ 最终你会发现我们从整型容器中取出一个字符串,因为虚拟机只管在运行时从方法区找到 ArrayList 这个类的类型信息并解析出它的 add 方法,接着执行这个方法。它不像一般的方法调用,调用之前编译器会检测这个方法存在不存在,参数类型是否匹配等,所以没了编译器的这层安全检查,反射地调用方法更容易遇到问题。

​ 除此之外,之前我们说过的泛型在经过编译期之后会被类型擦除,但实际上代表该类型的 Class 类型信息中是保存有一些基本的泛型信息的,这一点我们可以通过反射得到。

七、反射中,Class.forName 和 ClassLoader 区别

java 中 class.forName() 和 classLoader 都可用来对类进行加载。

class.forName() 前者除了将类的 .class 文件加载到 jvm 中之外,还会对类进行解释,执行类中的 static 块,且对静态变量赋值(如果可以的话,比如一个静态变量由一个静态方法直接赋值)。

而 classLoader 只干一件事情,就是将 .class 文件加载到 jvm 中,不会执行 static 中的内容,只有在 newInstance 才会去执行 static 块。

Class.forName(name, initialize, loader) 带参函数也可控制是否加载 static 块。并且只有调用了 newInstance() 方法采用调用构造函数,创建类的对象。

ClassLoader 就是遵循双亲委派模型最终调用启动类加载器的类加载器,实现的功能是“通过一个类的全限定名来获取描述此类的二进制字节流”,获取到二进制流后放到JVM中。Class.forName() 方法实际上也是调用的 CLassLoader 来实现的。

下面结合着类装载机制阐述一下原理:

Java 类装载过程:

—> 加载 —> 验证 —> 准备 —>  —> 初始化 —> 使用 —> 卸载   // 其中 验证+准备+解析 环节属于链接范畴。
  • 装载:通过类的全限定名(com.codetop.***.User)获取二进制字节流,将二进制字节流转换成方法区中的运行时数据结构,在内存中生成 Java.lang.Class 对象;
  • 链接:执行下面的验证、准备和解析步骤,其中解析步骤是可以选择的: 校验:检查导入类或接口的二进制数据的正确性;(文件格式验证,元数据验证,字节码验证,符号引用验证); 准备:给类的静态变量分配并初始化存储空间; 解析:将常量池中的符号引用转成直接引用;
  • 初始化:激活类的静态变量,初始化 Java 代码和静态 Java 代码块,并初始化程序员设置的变量值。

ClassLoader.loadClass(className) 方法,内部实际调用的方法是 ClassLoader.loadClass(className,false),第 2 个 boolean 参数,表示目标对象是否进行链接,false 表示不进行链接,由上面介绍可知,不进行链接意味着不进行包括初始化等一些列步骤,那么静态块和静态对象就不会得到执行。

扩展内容见:https://www.jianshu.com/p/52c38cf2e3d4