何为反射?

反射(Reflection),是指Java程序具有在运转期分析类以及修正其本身状况或行为的才能。 浅显点说 便是 经过反射咱们能够动态地获取一个类的一切特点和办法,还能够操作这些办法和特点。

实例的创立

一般咱们创立一个方针实例Person zhang = new Person(); 虽然是简简单单一句,但JVM内部的完成进程是杂乱的:

  1. 将硬盘上指定方位的Person.class文件加载进内存
  2. 履行main办法时,在栈内存中拓荒了main办法的空间(压栈-进栈),然后在main办法的栈区分配了一个变量zhang。
  3. 履行new,在堆内存中拓荒一个 实体类的 空间,分配了一个内存首地址值
  4. 调用该实体类对应的结构函数,进行初始化(假如没有结构函数,Java会补上一个默许结构函数)。
  5. 将实体类的 首地址赋值给zhang,变量zhang就引用了该实体。(指向了该方针)

JAVA反射机制详解

其中上图过程1 Classloader(类加载器) 将class文件加载到内存中详细分为3个阶段:加载、衔接、初始化

JAVA反射机制详解

而又在 加载阶段,类加载器将类对应的.class文件中的二进制字节流读入到内存中,将这个字节流转化为办法区的运转时数据结构,然后在堆区创立一个 java.lang.Class方针(类相关的信息),作为对办法区中这些数据的拜访进口

拓宽:

mp.weixin.qq.com/s/v91bqRiKD… mp.weixin.qq.com/s/tsbDfyYLq…

然后再经过类的实例来执操作类的办法和特点,比方zhang.eat(), zhang.getHeight()等等

假如咱们运用反射的话,咱们需求拿到该类Person的Class方针,再经过Class方针来操作类的办法和特点或许创立类的实例

Class personClass = Person.class;//这边仅仅举一个比方,获取class方针的多种办法,本文后面再渐渐道来
Object person = personClass.newInstance();

咱们能够发现 经过new创立类的实例和反射创立类的实例,都绕不开.class文件 和 Class类的。

.class文件

首先咱们得先了解一下 什么是.class文件 举个简单的比方,创立一个Person类:

public class Person {
    /**
     * 状况 or 特点
     */
    String name;//姓名
    String sex;//性别
    int height;//身高
    int weight;//体重
    /**
     * 行为
     */
    public void sleep(){
    	System.out.println(this.name+"--"+ "睡觉");
	}
    public void eat(){
        System.out.println("吃饭");
    }
    public void Dance(){
        System.out.println("跳舞");
    }
}

咱们履行javac指令,编译生成Person.class文件 然后咱们经过vim 16进制 打开它

#打开file文件
vim Person.class 
#在指令形式下输入.. 以16进制显示
 :%!xxd
#在指令形式下输入.. 切换回默许显示
:%!xxd -r

JAVA反射机制详解

不同的操作系统,不同的 CPU 具有不同的指令集,JAVA能做到渠道无关性,依靠的便是 Java 虚拟机。 .java源码是给人类读的,而class字节码是给JVM虚拟机读的,计算机只能识别 0 和 1组成的二进制文件,所以虚拟机便是咱们编写的代码和计算机之间的桥梁。 虚拟机将咱们编写的 .java 源程序文件编译为 字节码 格局的 .class 文件,字节码是各种虚拟机与一切渠道统一运用的程序存储格局,class文件首要用于解决渠道无关性的中间文件

JAVA反射机制详解

Person.class文件 包含Person类的一切信息

Class类

咱们来看下jdk的官方api文档对其的界说:

Class类的类表明正在运转的Java应用程序中的类和接口。 枚举是一种类,一个注释是一种界面。 每个数组也归于一个反映为类方针的类,该方针由具有相同元素类型和维数的一切数组同享。 原始Java类型( boolean , byte , char , short , int , long , float和double ),和关键字void也表明为类方针。 类没有公共结构函数。 相反, 类方针由Java虚拟机自动构建,由于加载了类,而且经过调用类加载器中的defineClass办法。。

java万物皆是Class类

咱们来看下Class类的源码,源码太多了,挑了几个要点:

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement {
    private static final int ANNOTATION= 0x00002000;
    private static final int ENUM      = 0x00004000;
    private static final int SYNTHETIC = 0x00001000;
    private static native void registerNatives();
    static {
        registerNatives();
    }
    /*
     * Private constructor. Only the Java Virtual Machine creates Class objects.
     * This constructor is not used and prevents the default constructor being
     * generated.
     */
    private Class(ClassLoader loader) { //私有化的 结构器
        // Initialize final field for classLoader.  The initialization value of non-null
        // prevents future JIT optimizations from assuming this final field is null.
        classLoader = loader;
    }
    ...
    // reflection data that might get invalidated when JVM TI RedefineClasses() is called
    private static class ReflectionData<T> {
        volatile Field[] declaredFields;//字段
        volatile Field[] publicFields;
        volatile Method[] declaredMethods;//办法
        volatile Method[] publicMethods;
        volatile Constructor<T>[] declaredConstructors;//结构器
        volatile Constructor<T>[] publicConstructors;
        // Intermediate results for getFields and getMethods
        volatile Field[] declaredPublicFields;
        volatile Method[] declaredPublicMethods;
        volatile Class<?>[] interfaces;//接口
        // Value of classRedefinedCount when we created this ReflectionData instance
        final int redefinedCount;
        ReflectionData(int redefinedCount) {
            this.redefinedCount = redefinedCount;
        }
    }
      ...
     //注释数据
     private volatile transient AnnotationData annotationData;
    private AnnotationData annotationData() {
        while (true) { // retry loop
            AnnotationData annotationData = this.annotationData;
            int classRedefinedCount = this.classRedefinedCount;
            if (annotationData != null &&
                annotationData.redefinedCount == classRedefinedCount) {
                return annotationData;
            }
            // null or stale annotationData -> optimistically create new instance
            AnnotationData newAnnotationData = createAnnotationData(classRedefinedCount);
            // try to install it
            if (Atomic.casAnnotationData(this, annotationData, newAnnotationData)) {
                // successfully installed new AnnotationData
                return newAnnotationData;
            }
        }
    } 
    ...

咱们能够发现Class也是类,是一种特殊的类,将咱们界说一般类的共同的部分进行笼统,保存类的特点,办法,结构办法,类名、包名、父类,注解等和类相关的信息。 Class类的结构办法是private,只要JVM能创立Class实例,咱们开发人员 是无法创立Class实例的,JVM在结构Class方针时,需求传入一个类加载器类也是能够用来存储数据的,Class类就像 一般类的模板 一样,用来保存“类一切相关信息”的类

JAVA反射机制详解

咱们来持续看这个利用反射的比方:Class personClass = Person.class; 由于JVM为加载的 Person.class创立了对应的Class实例,并在该实例中保存了该 Person.class的一切信息,因而,假如获取了Class实例(personClass ),咱们就能够经过这个Class实例获取到该实例对应的Person类的一切信息。

反射的运用

获取Class实例4种办法

  1. 经过方针调用 getClass() 办法来获取
Person p1 = new Person();
Class c1 = p1.getClass();

像这种已经创立了方针的,再去进行反射的话,有点多此一举。 一般是用于传过来的是Object类型的方针,不知道详细是什么类,再用这种办法比较靠谱

  1. 类名.class
Class c2 = Person.class;

这种需求提前知道导入类的包,程序功能更高,比较常用,经过此办法获取 Class 方针**,Person类不会进行初始化**

  1. 经过 Class 方针的 forName() 静态办法来获取,最常用的一种办法
Class c3 = Class.forName("com.zj.demotest.domain.Person");

这种只需传入类的全途径**,Class.forName会进行初始化initialization过程,即静态初始化(会初始化类变量,静态代码块)。**

  1. 经过类加载器方针的loadClass()办法
public class TestReflection {
    public static void main(String[] args) throws ClassNotFoundException {
        Person p1 = new Person();
        Class c1 = p1.getClass();
        Class c2 = Person.class;
        Class c3 = Class.forName("com.zj.demotest.domain.Person");
        //第4中办法,类加载器
        ClassLoader classLoader = TestReflection.class.getClassLoader();
        Class c4 = classLoader.loadClass("com.zj.demotest.domain.Person");
        System.out.println(c1.equals(c2));
        System.out.println(c2.equals(c3));
        System.out.println(c3.equals(c4));
        System.out.println(c1.equals(c4));
    }
}

loadClass的源码:

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

loadClass 传入的第二个参数是”false”,因而它不会对类进行衔接这一过程,依据类的生命周期咱们知道,假如一个类没有进行验证和预备的话,是无法进行初始化进程的,即不会进行类初始化,静态代码块和静态方针也不会得到履行

咱们将c1,c2,c3,c4进行 equals 比较

System.out.println(c1.equals(c2));
System.out.println(c2.equals(c3));
System.out.println(c3.equals(c4));
System.out.println(c1.equals(c4));

成果:

true

true

true

true

由于Class实例在JVM中是仅有的,所以,上述办法获取的Class实例是同一个实例,一个类在 JVM 中只会有一个 Class 实例

Class类常用的API

日常开发的时分,咱们一般运用反射是为了 创立类实例(方针)、反射获取类的特点和调用类的办法

getName() 取得类的完好名字
getFields() 取得类的public类型的特点
getDeclaredFields() 取得类的一切特点。包含private 声明的和继承类
getMethods() 取得类的public类型的办法
getDeclaredMethods() 取得类的一切办法。包含private 声明的和继承类
getMethod(String name, Class[] parameterTypes) 取得类的特定办法,name参数指定办法的名字,parameterTypes 参数指定办法的参数类型。
getConstructors() 取得类的public类型的结构办法
getConstructor(Class[] parameterTypes) 取得类的特定结构办法,parameterTypes 参数指定结构办法的参数类型
newInstance() 经过类的不带参数的结构办法创立这个类的一个方针
getSuperClass() 用于返回表明该 Class 表明的任何类、接口、原始类型或任何 void 类型的超类的Class(即父类)

咱们这边就不全部打开讲了,挑几个要点解说一下

创立方针

  1. 调用class方针的newInstance()办法
Class c1 = Class.forName("com.zj.demotest.domain.Person");
Person p1 = (Person) c1.newInstance();
p1.eat();

成果:

吃饭

留意:Person类必须有一个无参的结构器类的结构器的拜访权限不能是private

  1. 运用指定结构办法Constructor来创立方针

假如咱们非得让Person类的无参结构器设为private呢,咱们能够获取对应的Constructor来创立方针

Class c1 = Class.forName("com.zj.demotest.domain.Person");
Constructor<Person> con =  c1.getDeclaredConstructor();
con.setAccessible(true);//答应拜访
Person p1 = con.newInstance();
p1.eat();

成果:

吃饭

留意:setAccessible()办法能在运转时压制Java言语拜访控制查看(Java language access control checks),从而能任意调用被私有化保护的办法、域和结构办法。 由此咱们能够发现单例形式不再安全,反射可破之

拜访特点

Field getField(name) 依据字段名获取某个public的field(包含父类)
Field getDeclaredField(name) 依据字段名获取当时类的某个field(不包含父类)
Field[] getFields() 获取一切public的field(包含父类)
Field[] getDeclaredFields() 获取当时类的一切field(不包含父类)

咱们来看一个比方:

public class TestReflection3 {
    public static void main(String[] args) throws Exception {
        Object p = new Student("li hua");
        Class c = p.getClass();
        Field f = c.getDeclaredField("name");//获取特点
        f.setAccessible(true);//答应拜访
        Object val= f.get(p);
        System.out.println(val);
    }
    static class Student {
        private String name;
        public Student(String name) {
            this.name = name;
        }
    }
}

成果:

li hua

咱们能够发现反射能够破坏类的封装

调用办法

Method getMethod(name, Class…) 获取某个public的Method(包含父类)
Method getDeclaredMethod(name, Class…) 获取当时类的某个Method(不包含父类)
Method[] getMethods() 获取一切public的Method(包含父类)
Method[] getDeclaredMethods() 获取当时类的一切Method(不包含父类)

咱们来看一个比方:

public class TestReflection4 {
    public static void main(String[] args) throws Exception {
        //获取私有办法,需求传参:办法名和参数
        Method h = Student.class.getDeclaredMethod("setName",String.class);
        h.setAccessible(true);
        Student s1 =new Student();
        System.out.println(s1.name);
        //传入方针方针,调用对应的办法
        h.invoke(s1,"xiao ming");
        System.out.println(s1.name);
    }
    static class Student {
        private String name;
        private void setName(String name) {
            this.name = name;
        }
    }
}

成果:

null

xiao ming

咱们发现获取办法getMethod()时,需求传参 办法名和参数 这是由于.class文件中一般有不止一个办法,获取办法getMethod()时,会去调用searchMethods办法循环遍历一切Method,然后依据 办法名和参数类型 找到仅有契合的Method返回。

JAVA反射机制详解
咱们知道类的办法是在JVM的办法区中 ,当咱们new 多个方针时,特点会别的拓荒堆空间存放,而办法只要一份,不会额定消耗内存,办法就像一套指令模板,谁都能够传入数据交给它履行,然后得到对应履行成果。 method.invoke(obj, args)时传入方针方针,即可调用对应方针的办法

假如获取到的Method表明一个静态办法,调用静态办法时,无需指定实例方针,所以invoke办法传入的第一个参数永远为null, method.invoke(null, args)

那假如 办法重写了呢,反射依旧遵从 多态 的原则

反射的应用场景

假如平常咱们仅仅写业务代码,很少会接触到直接运用反射机制的场景,究竟咱们能够直接new一个方针,功能比还反射要高。 但假如咱们是工具结构的开发者,那一定十分熟悉,像 Spring/Spring Boot、MyBatis 等等结构中都大量运用反射机制,反射被称为结构的魂灵 比方:

  1. Mybatis Plus能够让咱们只写接口,不写完成类,就能够履行SQL
  2. 开发项目时,切换不同的数据库只需更改装备文件即可
  3. 类上加上@Component注解,Spring就帮咱们创立方针
  4. 在Spring咱们只需 @Value注解就读取到装备文件中的值
  5. 等等

扩展:反射装备文件

咱们来模仿一个装备高于编码的比方

新建my.properties,将其放在resources的目录下

#Person类的包途径
className=com.zj.demotest.domain.Person 
methodName=eat

Person类 还是本文 一向用的,在文章的开头有

最后咱们来编写一个测验类

public class TestProp {
    public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        Properties properties = new Properties();
        ClassLoader classLoader = TestProp.class.getClassLoader();
        InputStream inputStream = classLoader.getResourceAsStream("my.properties");// 加载装备文件
        properties.load(inputStream);
        String className = properties.getProperty("className");
        System.out.println("装备文件中的内容:className="+className);
        String methodName = properties.getProperty("methodName");
        System.out.println("装备文件中的内容:methodName="+methodName);
        Class name = Class.forName(className);
        Object object = name.newInstance();
        Method method = name.getMethod(methodName);
        method.invoke(object);
    }
}

成果:

装备文件中的内容:className=com.zj.demotest.domain.Person

装备文件中的内容:methodName=eat

吃饭

紧接着,咱们修正装备文件:

className=com.zj.demotest.domain.Person
methodName=Dance

成果变为:

装备文件中的内容:className=com.zj.demotest.domain.Person

装备文件中的内容:methodName=Dance

跳舞

是不是很方便?

JAVA反射机制详解

尾语

反射机制是一种功能强大的机制,让Java程序具有在运转期分析类以及修正其本身状况或行为的才能。 关于特定的杂乱系统编程使命,它是十分必要的,为各种结构供给开箱即用的功能供给了便当,为解耦合供给了保障机制。 可是世事无绝对,反射相当于一系列解释操作,通知 JVM 要做的工作,功能比直接拜访方针要差点(JIT优化后,关于结构来说实践是影响不大的),还会增加程序的杂乱性等(明明直接new一下就能解决的工作,非要写一大段代码)。


本篇文章到这儿就结束啦,很感谢你能看到最后,假如觉得文章对你有帮助,别忘记重视我!更多精彩文章在大众号「小牛呼噜噜 」