初步理解Java中的反射的原理与实现

154 阅读14分钟

文章目录


1. 静态语言和动态语言

计算机编程语言有很多很多,目前来说最为常用的有C、C++、Java、Python、Go…如果按照不同的分类规则进行分类,我们可以将所有的编程语言分为不同的类别,比如:

  • 编译型语言:C、C++……,它的特点是速度快,但较复杂,开发效率低
  • 解释性语言:Java、Python……,它的特点是跨平台性好,在不同的操作系统上都可以运行相同的代码,但缺点就是执行速度慢,需要依赖于解释器来执行

当然分类的结果还有很多。之所以会对编程语言进行分类,一方面是为了使用户对不同的语言有个感性的区分认识,便于理解不同语言之间的差别;另一方面使得用户可以根据不同的应用场景进行合适的选择,这取决于业务需求对于移植性要求高,还是对于程序的执行速度要求更高。

为了便于理解后续的内容,我们从另一个角度对语言做一个区分:

  • 动态语言:指一类在运行时可以改变其结构的语言,如C#、JavaScript、PHP、Python等
  • 静态语言:指运行时结构不可改变的语言,如Java、C、C++等

而Java又可称为准动态语言,其中的动态特性就依赖于Java中的反射机制。


2. Java程序的三个阶段

Java作为一门面向对象的编程语言,一个重要的思想就是万物皆对象。当我们需要处理某一个任务时,需要从任务所在的场景中抽象出所有可能的类,并定义类的成员属性和成员方法。当程序中需要使用到某个类时,通常使用new关键字创建类的对象,并通过对象调用类的成员属性和成员方法,或者使用类名直接调用静态方法。

在之前的三篇文章中,我们对于程序执行过程中JVM内存空间的变化有了一个初步的、感性的认识。比如知道了JVM内存空间的划分,程序的不同部分应该在哪一类内存空间中,以及程序的执行是如何改变内存空间中所存储的内容等。

图形化理解Java中的形参和实参

理解Java类对象使用过程中内存的变化过程

Java中对对象采用的什么传递方式呢?

除了知道JVM内存空间的划分外,我们还需要了解Java程序从编写到运行在计算机中所经历的三个阶段,这三个阶段为:

  • Source源代码阶段

  • Class类对象加载阶段

  • RunTime运行时阶段


    在这里插入图片描述

2.1 Source源代码阶段

如上所示,假设当前创建的Person类为:

public class Person {
    private String name;
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
	
	// 其他的构造方法、getter和setter方法即其他重写的方法
    public void say(){
        System.out.println("say hello...");
    }
}

当类创建完毕后,编译器就会将Person.java文件编译为Person.class文件。Person.class文件的底层是一系列的二进制代码,它表示类中所包含的所有信息。

2.2 Class类对象阶段

当我们定义好Person类并通过编译得到了对应的.class文件后,如果想要使用这个类,程序就可以通过new关键字创建类对象,并通过对象使用类的成员属性和成员方法。但是程序是如何得到这个类对象的呢?或者说通过new Person()为什么就可以得到一个Person类对象呢?这依赖于Class类加载阶段所做的工作。

类加载阶段使用类加载器(ClassLoader)将Person.class文件加载到内存的方法区中,并使用Class类对象来描述Person.class中的字节码信息。Class类对象使用三部分内容对字节码文件进行描述,它们和类定义的内容是一一对应的:

  • 成员变量:Field[] fields
  • 构造方法:Constructor[] cons
  • 成员方法:Method[] methods

好怕怕的类加载器

2.3 RunTime运行时阶段

前一个阶段已经通过ClassLoader将字节码文件的内容加载到了内存中的Class对象中,Class对象中包含了之前类定义时所有的内容。最后在运行时阶段,通过反射机制就可以在堆内存中创建Person类对象,并获取到其中的成员变量、构造方法和成员方法进行使用。

通过三个阶段的执行,程序就可以使用定义的类中的所有内容。那么在运行时阶段,通过反射机制就可以在堆内存中创建Person类对象一句的反射体现在哪里呢?如果将源代码中创建的类看作是源事物,那么Class类对象就相当于一面镜子,程序通过镜子中看到的内容来使用类,这个过程就形象的类比于反射一词的本意。


在这里插入图片描述


3. 概念

Java 反射机制在程序运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。这种 动态的获取信息 以及 动态调用对象的方法 的功能称为 java 的反射机制

反射机制很重要的一点就是运行时,这使得可以在程序运行时加载、探索以及使用编译期间完全未知的 .class 文件。换句话说,Java 程序可以加载一个运行时才得知名称的 .class 文件,然后获悉其完整构造,并生成其对象实体、或对其 fields(变量)设值、或调用其 methods(方法)。

经过前面Java程序的三个阶段的理解,我们基本上可以理解上面的这两段话,但感觉还是有些云里雾里。句中的每个字眼到底是如何体现在反射机制的使用中的呢?后面将会通过代码的方法逐层的剖析,逐步的进行理解。


4. 功能

反射机制最重要的特点就是可以在运行时获取任意一个类的所有属性和方法,如果将其细看,反射机制提供的功能有:

  • 判断任意一个对象所属的类
  • 构造任意一个类的对象
  • 判断任意一个类所具有的成员变量和方法
  • 获取泛型信息
  • 调用任意一个对象的成员变量和方法
  • 处理注解

当然上述的功能都是体现在程序的RunTime运行时阶段,这个要始终牢记。


5. 获取Class类对象

ClassLoader会将编译后的.class文件中的内容加载到内存中的Class对象中,程序如果想通过反射机制来创建某个类的对象,就需要首先获取到内存中的Class类对象。通常来说,获取Class类对象有三种方式:

  • Class.forName("全类名"):将字节码文件加载进内存,返回Class对象,多用于配置文件,将类名定义在配置文件中,读取文件,加载类
  • 类名.class:通过类名的属性class获取,多用于参数传递
  • 对象.getClass():getClass()方法在Object类中定义,多用于对象的获取字节码的方式

假设此时定义了Person类,类定义如下所示:

package ReflectDemo;

public class Person {
    private String name;
    private int age;
    public String school = "STU";

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getSchool() {
        return school;
    }

    public void setSchool(String school) {
        this.school = school;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", school='" + school + '\'' +
                '}';
    }

    public void say(){
        System.out.println("say hello...");
    }

    public void show(String str){
        System.out.println(str);
    }
}

编译器通过编译会生成Person.class文件,然后ClassLoader会将其加载到内存的Class类对象中。

5.1 Class.forName("全类名")方式

其中全类名指的是类所在的包名和类名的组合,如xxx.xxx.xx.Person。通过Class类中的forName()就可以获取到指定类名的类对象。

 Class<?> cls = Class.forName("ReflectDemo.Person");
 System.out.println(cls);  // class ReflectDemo.Person

5.2类名.class方式

Class<Person> cls2 = Person.class;
System.out.println(cls2);  // // class ReflectDemo.Person

5.3 对象.getClass()方式

首先通过new来实例化类对象,然后通过类对象的getClass()来获取Class类对象。

Person p = new Person();
Class<? extends Person> cls3 = p.getClass();
System.out.println(cls3);  // // class ReflectDemo.Person

那么三种方法得到的Class类对象是否是相同的呢?我们通过==来比较一下,发现它们都是相同的。这是因为同一个.class文件在一次程序的运行过程中,只会被加载一次。因此,无论通过哪一种方法获取到的Class类对象都是同一个。

System.out.println(cls == cls2);  // true
System.out.println(cls == cls3);  // true

6. 使用Class类对象

通过上述三种方式获取到Class类对象后,如何使用它其中的内容呢?Class类对象顾名思义它就是java.lang.Class<T>的一个类对象,因此通过查看Class类中的方法就可以知道如何使用它。其中最常用的有四类,它们和Class类对象中的内容也是对应的,如下所示:

  • 获取成员变量们
  • 获取构造方法们
  • 获取成员方法们
  • 获取类名

前三个之所以加,是因为类中这三类可能有多个,例如构造方法就有无参构造、有参构造。

6.1 获取成员变量们

通过Class类对象获取成员变量们主要有四种方式:

  • Field getField(String name)
  • Field[] getFields()
  • Field getDeclaredField(String name)
  • Field[] getDeclaredFields()

下面依然使用前面定义的Person类进行说明:

public class ReflectTest {
    public static void main(String[] args) throws Exception {
        getFieldsMethods();
    }

    private static void getFieldsMethods() throws Exception {
        Class<Person> cls = Person.class;
        // 获取被public修饰的成员变量
        Field[] fields = cls.getFields();
        for(Field f:fields){
            System.out.println(f); // public java.lang.String ReflectDemo.Person.school
        }

        // 获取指定名称的被public修饰的成员变量
        Field fs = cls.getField("school");
        System.out.println(fs); // public java.lang.String ReflectDemo.Person.school
		
        // 获取成员变量的默认值
        Person p = new Person();
        Object value = fs.get(p);
        System.out.println(value);  // STU
		
        // 修改成员变量的值
        fs.set(p, "CUG");
        System.out.println(p);  // Person{name='null', age=0, school='CUG'}

        // 获取所有的成员变量,不考虑修饰符
        Field[] declaredFields = cls.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            System.out.println(declaredField);
        }
        /*
        private java.lang.String ReflectDemo.Person.name
        private int ReflectDemo.Person.age
        public java.lang.String ReflectDemo.Person.school
         */

        Field value2 = cls.getDeclaredField("age");
        // 忽略访问权限修饰符的安全检查
        value2.setAccessible(true);  // 强制获取,暴力反射
        System.out.println(value2.get(p));  // 0
    }
}

从输出结果中可以看出,使用getFields()只能获取到被public修饰的成员变量,如Person类中的school。另外,这些成员变量既包含该类的,也包含其父类中的成员变量。使用getField(String name)需要传递想要获取的成员变量名,同样只能获取被pulic修饰的变量,否则会抛出异常。

如果想要获取所有的成员变量,可以使用getDeclaredFields(String name)。它在获取成员变量的过程中,不会考虑使用的是什么访问控制修饰符。类似可以使用getDeclaredField()获取指定名称的成员变量。

不管是使用上述的哪一种方式获取成员变量,下一步需要进行使用。Class类中提供了get(Object obj)用于获取成员变量的值,set(Object obj, Object value)用于修改成员变量的值。但如果使用get()获取被private修饰的变量时,JVM会抛出异常。

Exception in thread "main" java.lang.IllegalAccessException

如果非要获取,可以使用setAccessible()并传入true强行忽略访问写权限修饰符的安全检查,进行暴力反射。

6.2 获取构造方法们

通过Class类对象获取构造方法同样有四个方法:

  • Constructor<T> getConstructor(Class<?>... parameterTypes)
  • Constructor<?>[] getConstructors()
  • Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
  • Constructor<?>[] getDeclaredConstructors()
public class ReflectTest {
    public static void main(String[] args) throws Exception {
        getConstructorsMethods();
    }
    
    private static void getConstructorsMethods() throws Exception {
        Class<Person> cls = Person.class;
        
        Constructor<?>[] constructors = cls.getConstructors();
        for (Constructor<?> constructor : constructors) {
            System.out.println(constructor);
        }
        /*
        public ReflectDemo.Person()
        public ReflectDemo.Person(java.lang.String,int)
         */

        Constructor<Person> c1 = cls.getConstructor(String.class, int.class);
        System.out.println(c1);  // public ReflectDemo.Person(java.lang.String,int)
        
        // 创建对象
        Object p1 = c1.newInstance("Forlogen", 10);
        System.out.println(p1); // Person{name='Forlogen', age=10, school='STU'}

        Constructor<Person> c2 = cls.getConstructor();
        System.out.println(c2);  // public ReflectDemo.Person()
        // 创建对象
        Object p2 = c2.newInstance();
        System.out.println(p2); // Person{name='null', age=0, school='STU'}
    }
}

构造方法的获取和上面成员变量的获取类似,从方法名上也可以看出。值得注意的是,如果需要获取指定类型的有参构造,需要在方法的参数列表中传递参数类型的Class对象,如String.class

当得到了各种构造方法后,可以使用Class中的newInstance(Object... initargs)方法创建对应的类对象。当参数列表为空时,程序将使用空参构造创建对象,对象中成员变量的值为默认值。如果想要使用带参构造方法,就需要传入对应类型的参数,此时得到的对象中变量的值就初始化为传入的参数值。

6.3 获取成员方法们

通过Class类对象获取成员方法同样有四种方式:

  • Method getMethod(String name, Class<?>... parameterTypes)
  • Method[] getMethods()
  • Method getDeclaredMethod(String name, Class<?>... parameterTypes)
  • Method[] getDeclaredMethods()
public class ReflectTest {
    public static void main(String[] args) throws Exception {
        getMethods();
    }

    private static void getMethods() throws Exception{
        Class<Person> cls = Person.class;

        // 获取所有被public修饰的成员方法
        // 包括类本身的成员方法和从父类中继承的方法
        Method[] methods = cls.getMethods();
        for (Method method : methods) {
            System.out.println(method + " : " + method.getName());
        }

        // 获取指定名称的方法
        // 传入方法名
        Method m2 = cls.getMethod("say");
        // 执行方法
        Person p = new Person();
        m2.invoke(p); // say hello...

        // 获取指定名称和参数类型的方法
        Method m3 = cls.getMethod("show", String.class);
        System.out.println(m3); // public void ReflectDemo.Person.show(java.lang.String)
    }
}

使用getMethods()可以获取到所有被public修饰的成员方法,其中结果中既有类自身定义的成员方法,也包含它父类中的成员方法。由于所有类都默认继承了Object类,因此,结果中自然也包含Object类中的诸多方法。

如果使用getMethod()获取指定名称的方法时,无参方法只需要传入方法名,带参方法还需要传入参数类型的Class类对象。当得到了成员方法后,通过invoke()就可以对指定的类对象使用成员方法。

getmethods()获取到的是该类和从父类中继承得到的所有被public修饰的成员方法;getDeclaredMethods()可以获取这个类或接口中的全部方法,但不包含从它的父类中继承的方法。

6.4 获取类名

获取类名直接通过getName()就可以办到。

public class ReflectTest {
    public static void main(String[] args) throws Exception {
        getClassName();
    }

    private static void getClassName() {
        Class<Person> cls = Person.class;
        System.out.println(cls.getName()); // ReflectDemo.Person
    }   
}

7. 应用

最后我们看一个应用反射机制来运行配置文件内容的例子。同样我们使用前面定义的Person类,配置文件pro.properties中写入如下内容

className = ReflectDemo.Person
methodName = say

它们分别表示要加载的类和使用的方法名。然后执行如下操作:

  • 通过Properties类的load()加载读取配置文件的IO流
  • 使用getProperty()获取配置文件中的内容,即类名和方法名
  • 将类加载进内存
  • 获取方法对象并创建Person类对象
  • 执行say()
public class Demo {
    public static void main(String[] args) throws Exception {
        // 加载配置文件
        Properties pro = new Properties();

        ClassLoader classLoader = Demo.class.getClassLoader();
        InputStream in = classLoader.getResourceAsStream("pro.properties");
        pro.load(in);
        
        // 获取配置文件中定义的数据
        String className = pro.getProperty("className");
        String methodName = pro.getProperty("methodName");

        // 加载类进内存
        Class<?> aClass = Class.forName(className);
        // 获取方法对象
        Method method = aClass.getMethod(methodName);
        // 创建对象
        Object o = aClass.newInstance();
        // 执行方法
        method.invoke(o);
    }
}

通过使用配置文件的方式实现了,当所使用的类和方法改变的时候,只需要更改配置文件中的内容,而不必修改代码达到目的。


8. 更多阅读

Java 反射由浅入深 | 进阶必备

java 反射

学习java应该如何理解反射?