反射

1. 反射的引入

在介绍反射之前,这里先通过一个小案例,来引入反射的概念以及什么时候使用反射。

情景:

/*
  接口的制定方:美了么
 */
public interface Mlm {
    //在线支付功能:
    public void payOnline();
}

/*
  接口的实现方:支付宝
*/
public class AliPay implements Mlm {
    @Override
    public void payOnline() {
        System.out.println("正在使用支付宝支付!");
    }
}
/*
  接口的实现方:微信
*/
public class WeChat implements Mlm {
    @Override
    public void payOnline() {
        System.out.println("正在使用微信支付!");
    }
}

而对于具体功能演示,我们就有了许多方法:

  • 方式一:new对象,调用payOnline()

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String method = scanner.nextLine();
        if ("微信".equals(method)) {
            // 使用微信支付
            new WeChat().payOnline();
        } else if ("支付宝".e quals(method)) {
            // 使用支付宝支付
            new AliPay().payOnline();
        }
    }
    
  • 方式二:将同一功能进行抽取,使用方法重载来调用不同的支付方式

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String method = scanner.nextLine();
        if ("微信".equals(method)) {
            // 使用微信支付
            pay(new WeChat());
        } else if ("支付宝".equals(method)) {
            // 使用支付宝支付
            pay(new AliPay());
        }
    }
    
    public static void pay(WeChat weChat) {
        weChat.payOnline();
    }
    
    public static void pay(AliPay aliPay) {
        aliPay.payOnline();
    }
    
  • 方式三:由于都实现了Mlm接口,所以可以向上抽取,利用面向对象特性:多态

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String method = scanner.nextLine();
        if ("微信".equals(method)) {
            // 使用微信支付
            pay(new WeChat());
        } else if ("支付宝".equals(method)) {
            // 使用支付宝支付
            pay(new AliPay());
        }
    }
    
    public static void pay(Mlm mlm) {
        mlm.payOnline();
    }
    

多态,确实已经实现了代码的高扩展。但是此时扩展性并没有达到最好。

试想一下:如果有一天和支付宝的合作取消,我们需要手动删除支付宝相关的else if分支;或者有一天与某某银行达成合作,还需要自己添加else if分支。

这里就需要引入本次的主角:反射机制
首先,我们来看一下利用反射实现上述功能有多简单:

public static void main(String[] args) throws Exception {
    String way = "top.zjax.reflectTest.WeChat"; // WeChat的全包名
    Class<?> payClass = Class.forName(way);
    Object payInstance = payClass.newInstance();
    Method payOnline = payClass.getMethod("payOnline");
    payOnline.invoke(payInstance);
}

可能到这里也没发现反射有什么优势,但是仔细想想,以后只需要修改way变量的值(即类的包路径),无论以后有多少种支付方式实现了Mlm接口,都不需要再添加分支语句,而实际开发中way的值也绝不是我们手动修改的(可能是前台的请求携带或是其他方式),所以这里就体现了反射具有很高的动态性和扩展性

这里,先对反射有个初步印象。

2. 反射的概念

📌 Java反射机制是指在运行状态中(Runtime),对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制
注意:所有是包括正常使用时无法调用的private, protected等

在编译后产生字节码文件的时候,类加载器子系统通过二进制字节流,负责从文件系统加载class文件。在执行程序(java.exe)时候,将字节码文件读入JVM(Java虚拟机)中,这个过程叫做类的加载。也就是说当程序使用任何一个类时,系统都会为之生成一个java.lang.Class对象。JVM会在内存中对应创建一个java.lang.Class对象,此对象是用来获取加载进来的类的字节码信息,并且这个Class对象将被作为程序访问方法区中的这个类的各种数据的外部接口。
(注意:Class的C是大写,这是一个类,并不是以前编译java文件产生的后缀名)

比如说我们执行new WeChat(),JVM会读取WeChat.class的字节码信息,然后将它的属性和方法(如payOnline() )赋给内存中新建的一个Class类。

所以:我们通过这个Class对象可以看到任意其他类的结构,这个对象就好像是一面镜子,透过镜子看到类的各种信息,我们形象的称之为反射。这种“看透”class的能力(the ability of the program to examine itself)被称为introspection(内省、内观、反省)。Reflection和introspection是常被并提的两个术语。

说明:在运行期间,如果我们要产生某个类的对象,Java虚拟机(JVM)会检查该类型的Class对象是否已被加载。如果没有被加载,JVM会根据类的名称找到.class文件并加载它。一旦某个类型的Class对象已被加载到内存,就可以用它来产生该类型的所有对象。

补充: 动态语言 与 静态语言

  1. 动态语言 是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构。主要动态语言: Object-C、 C#、JavaScript、 PHP、 Python、 Erlang 。

  2. 静态语言 与动态语言相对应的,运行时结构不可变的语言就是静态语言。如Java、C、C++。

所以Java不是动态语言,但Java可以称之为“准动态语言”。即Java有一定的动态性,我们可以利用反射机制、字节码操作获得类似动态语言的特性。Java的动态性让编程的时候更加灵活!

3. 反射涉及的相关类以及对Class类的理解

先介绍一下与反射有关的类,没必要去记,这里只是为了便于后续的学习:

类名用途
Class类代表类的实体,在运行的Java应用程序中表示类和接口
Field类代表类的成员变量(成员变量也称为类的属性)
Method类代表类的方法
Constructor类代表类的构造器

根据上面第反射概念的理解,我们可以知道:所有的类都是在对其第一次使用时,动态加载到JVM中的(懒加载)。当程序创建第一个对类的静态成员的引用时,就会加载这个类。使用new创建类对象的时候也会被当作对类的静态成员的引用。因此Java程序程序在它开始运行之前并非被完全加载,其各个类都是在必需时才加载的。

通过一个类比我们来体会一下这其中的意思。

假如我们有对象1和对象2,属性如下图,那么我们很轻易就能想到可以将它们两个的共同点(品牌,颜色等)抽取出来,抽出一个电脑类。

对象能抽取,那类呢?对于对象的抽取,我们是发现了他们属性、方法等运行逻辑上的共同点。而对于类的抽取我们更容易发现它们的共同点:都有属性,都有方法,都有构造器等等。在这里我们不用管这些类在属性类型,属性名称,方法返回值等上的差异,我们只在乎它是否有或者说是否可以有“有”或者“可以有”就是它们的共同点

那么我们就可以抽取。到这里,我们对上面表格的内容就很好理解了。Class是我们抽取出来的类。而通过这个类,我们可以获取到其他抽取出来的类如:Field 属性类、Method 方法类、Constructor构造器类等等 。

对于这些如何理解,我觉得最重要的就是一个思想:抽取。它也体现了Java的核心思想——万物皆对象。即便是类,向上抽取之后,也可以看作是一个对象。

4. 获取字节码信息的四种方式

新建一个Person类:

/*
    Person类
 */
public class Person {
    private int age;
    public String name;

    private void eat() {
        System.out.println("Person --> Eat");
    }

    public void sleep() {
        System.out.println("Person --> Sleep");
    }
}

获取字节码信息的四种方式:

// 以Person的字节码信息为例
public class Test02 {
    public static void main(String[] args) throws Exception {
        // 1.已有对象,通过 getClass() 方法获取
        Person person = new Person();
        Class<? extends Person> personClass1 = person.getClass();
        System.out.println(personClass1);

        // 2.通过内置class属性获取
        Class<Person> personClass2 = Person.class;
        System.out.println(personClass2);
        
        /* **************************
         * 注意:方式1和方式2不常用 *
         ****************************/

        // 3.(最常用)调用 Class 类提供的静态方法 forName()
        Class<?> personClass3 = Class.forName("top.zjax.reflectTest.Person");
        System.out.println(personClass3);

        // 4.利用类加载器 ClassLoader 获取
        ClassLoader classLoader = Test02.class.getClassLoader();
        Class<?> personClass4 = classLoader.loadClass("top.zjax.reflectTest.Person");
        System.out.println(personClass4);

        System.out.println(personClass1 == personClass2);
        System.out.println(personClass1 == personClass3);
        System.out.println(personClass3 == personClass4);
    }
}

方式1和方式2是基本不使用的,因为既然都有对象了,就没必要使用反射创建了,除非是想修改原来类的结构。方式3是最常见的使用方式,方式4利用ClassLoader也比较常见。

补充说明:

  1. ClassLoader 是全局唯一的,这里使用当前运行类Test02.class.getClassLoader()获取,或使用其他运行中的类获取都是能获取到同一个ClassLoader的。
  2. 最后的三个 ==是恒成立的,因为同一个类(这里是Person类)的字节码信息只加载一次,所以无论哪种方式,获取到的字节码信息都是一样的。

5. 可以作为Class类的实例的种类

Class类的具体的实例:
(1)类:外部类,内部类
(2)接口
(3)注解
(4)数组
(5)基本数据类型
(6)void

public class Test03 {
    public static void main(String[] args) {
        /*
        Class类的具体的实例:
        (1)类:外部类,内部类
        (2)接口
        (3)注解
        (4)数组
        (5)基本数据类型
        (6)void
        */
        Class<Person> class1 = Person.class; // 类
        Class<Comparable> class2 = Comparable.class; // 接口
        Class<Override> class3 = Override.class; // 注解

        // 数组
        int[] arr1 = {1, 2, 3};
        Class<? extends int[]> class4 = arr1.getClass();
        int[] arr2 = {5, 6, 7};
        Class<? extends int[]> class5 = arr2.getClass();
        // 同一个维度,同一个元素类型,得到的字节码是同一个。这里结果为true
        System.out.println(class4 == class5); 
        
        // 基本数据类型
        Class<Integer> class6 = int.class;
        // void
        Class<Void> class7 = void.class;
    }
}

说明:同一个维度(维度指一维数组,二维数组等),同一个元素类型(int, long, String等),得到的字节码是同一个。

6. 获取运行时类的结构

修改Person类,新建接口MyInterface,新建注解MyAnnotation,新建Student类

  • Person

    /*
        Person类
     */
    public class Person implements Serializable {
        private int age;
        public String name;
    
        private void eat() {
            System.out.println("Person --> Eat");
        }
    
        public void sleep() {
            System.out.println("Person --> Sleep");
        }
    }
    
  • MyInterface

    /*
        自定义接口
    */
    public interface MyInterface {
        public void myMethod(String string);
    }
    
  • MyAnnotation

    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    import static java.lang.annotation.ElementType.*;
    
    /*
        自定义注解
        @Target: 定义当前注解能够修饰程序中的哪些元素
        @Retention: 定义注解的声明周期
     */
    @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyAnnotation {
        String value();
    }
    
  • Student

    /*
        Student类
     */
    @MyAnnotation(value = "ZJax")
    public class Student extends Person implements MyInterface {
        public int sno;
        protected int level;
        double height;
        private double weight;
    
        // 构造器
        public Student() {
            System.out.println("空参构造器。");
        }
    
        public Student(int sno) {
            this.sno = sno;
        }
    
        protected Student(int sno, double weight) {
            this.sno = sno;
            this.weight = weight;
        }
    
        Student(int sno, int level) {
            this.sno = sno;
            this.level = level;
        }
    
        private Student(int sno, int level, double height, double weight) {
            this.sno = sno;
            this.level = level;
            this.height = height;
            this.weight = weight;
        }
    
        public void study() {
            System.out.println("好好学习,天天向上!!!");
        }
    
        private void study(String subject) {
            System.out.println("我要学" + subject);
        }
    
        void relax() {
            System.out.println("放松一下吧~");
        }
    
        protected void play() {
            System.out.println("玩一会儿吧~");
        }
    
        @Override
        @MyAnnotation("Haha")
        public void myMethod(String string) throws RuntimeException {
            System.out.println("我学会了重写myMethod()方法!!!" + string);
        }
    
        @Override
        public String toString() {
            return "Student{" +
                    "sno=" + sno +
                    ", level=" + level +
                    ", height=" + height +
                    ", weight=" + weight +
                    '}';
        }
    }
    
    

6.1 获取构造器和创建对象

方法用途
getConstructor(Class...<?> parameterTypes)获得该类中与参数类型匹配的公有构造方法
getConstructors()获得该类的所有公有构造方法
getDeclaredConstructor(Class...<?> parameterTypes)获得该类中与参数类型匹配的构造方法
getDeclaredConstructors()获得该类所有构造方法
import java.lang.reflect.Constructor;

public class Test04 {
    public static void main(String[] args) throws Exception {
        // 1.获取字节码信息
        Class<Student> studentClass = Student.class;

        // 2.通过字节码信息获取构造器
        // 以下分开演示各个方法
        
        // 3.创建对象
    }
} 
    1. 通过字节码信息获取构造器
    • getConstructors()

      // getConstructors()只能获取当前运行时类中被public修饰的构造器
      System.out.println("========== Student的公有构造方法 ==========");
      Constructor<?>[] constructors = studentClass.getConstructors();
      for (Constructor<?> constructor : constructors) {
          System.out.println(constructor);
      }
      
      ========== Student的公有构造方法 ==========
      public top.zjax.reflectTest.Student(int)
      public top.zjax.reflectTest.Student()
      
    • getDeclaredConstructors()

      // getDeclaredConstructors()能获取所有构造器
      System.out.println("========== Student的所有构造方法 ==========");
      Constructor<?>[] declaredConstructors = studentClass.getDeclaredConstructors();
      for (Constructor<?> declaredConstructor : declaredConstructors) {
          System.out.println(declaredConstructor);
      }
      
      ========== Student的所有构造方法 ==========
      private top.zjax.reflectTest.Student(int,int,double,double)
      top.zjax.reflectTest.Student(int,int)
      protected top.zjax.reflectTest.Student(int,double)
      public top.zjax.reflectTest.Student(int)
      public top.zjax.reflectTest.Student()
      
    • getConstructor(Class...<?> parameterTypes)

      // 获取 public 修饰的指定构造器
      System.out.println("========== Student类中被public修饰的指定构造方法 ==========");
      Constructor<Student> con1 = studentClass.getConstructor(); // 无参
      Constructor<Student> con2 = studentClass.getConstructor(int.class); // 有参
      System.out.println(con1);
      System.out.println(con2);
      
      ========== Student下被public修饰的指定构造方法 ==========
      public top.zjax.reflectTest.Student()
      public top.zjax.reflectTest.Student(int)
      
    • getDeclaredConstructor(Class...<?> parameterTypes)

      // 获取指定构造器
      System.out.println("========== Student指定构造方法 ==========");
      Constructor<Student> deCon1 = studentClass.getDeclaredConstructor(int.class, int.class);
      System.out.println(deCon1);
      
      ========== Student指定构造方法 ==========
      top.zjax.reflectTest.Student(int,int)
      
    1. 创建对象
    System.out.println("========== Student创建对象 ==========");
    Student student1 = con1.newInstance();
    System.out.println("student1:\n" + student1);
    Student student2 = con2.newInstance(123);
    System.out.println("student2:\n" + student2);
    Student student3 = studentClass.newInstance(); // 空参构造
    System.out.println(student3);
    
    ========== Student创建对象 ==========
    空参构造器。
    student1:
    Student{sno=0, level=0, height=0.0, weight=0.0}
    student2:
    Student{sno=123, level=0, height=0.0, weight=0.0}
    空参构造器。
    Student{sno=0, level=0, height=0.0, weight=0.0}
    

这里先做个小小总结:方法中带Declared就是获取本类中的各种数据,而不带Declared能获取本类及父类带public的各种数据。这在下面的学习中同样适用。

6.2 获取属性以及对属性进行赋值

方法用途
getField(String name)获得某个公有的属性对象
getFields()获得所有公有的属性对象
getDeclaredField(String name)获得某个属性对象
getDeclaredFields()获得所有属性对象
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public class Test05 {
    public static void main(String[] args) throws Exception {
        // 1.获取字节码信息
        Class<Student> studentClass = Student.class;

        // 2.通过字节码信息获取属性


        /*
        3. 获取属性的具体结构
            修饰符 数据类型 名字
        例:public   int      sno
         */


        // 4.赋值
    }
}
    1. 通过字节码信息获取属性
    • getFields()

      // getFields():获取当前运行时类和父类中被public修饰的属性
      System.out.println("========== Student以其父类的公有属性 ==========");
      Field[] fields = studentClass.getFields();
      for (Field field : fields) {
          System.out.println(field);
      }
      
    • getDeclaredFields

      // getDeclaredFields:获取当前运行时类的所有属性(不包括父类)
      System.out.println("========== Student的所有属性 ==========");
      Field[] declaredFields = studentClass.getDeclaredFields();
      for (Field declaredField : declaredFields) {
          System.out.println(declaredField);
      }
      
    • getField()/getDeclaredField()

      // 获取指定属性getField()/getDeclaredField()
      System.out.println("========== 获取Student的指定属性 ==========");
      Field sno = studentClass.getField("sno");
      Field level = studentClass.getDeclaredField("level");
      System.out.println(sno);
      System.out.println(level);
      
    1. 获取属性的具体结构
    // 修饰符
    System.out.println("========== 属性sno的修饰符 ==========");
    int modifiers = sno.getModifiers();
    String s = Modifier.toString(modifiers);
    System.out.println(modifiers);
    System.out.println(s);
    
    // 数据类型
    System.out.println("========== 属性sno的数据类型 ==========");
    Class<?> snoType = sno.getType();
    System.out.println(snoType);
    
    // 名字
    System.out.println("========== 属性sno的名字 ==========");
    String name = sno.getName();
    System.out.println(name);
    
    1. 赋值
    System.out.println("========== 属性sno赋值 ==========");
    Object obj = studentClass.newInstance();
    sno.set(obj, 666); // 给属性赋值,必须要有对象
    System.out.println(obj);
    
    

注意点:给属性赋值,必须要有对象。就如平时想要赋值,就必须要创建对象一个道理。

6.3 获取方法和调用方法

getMethod(String name, Class...<?> parameterTypes)获得该类某个公有的方法
getMethods()获得该类所有公有的方法
getDeclaredMethod(String name, Class...<?> parameterTypes)获得该类某个方法
getDeclaredMethods()获得该类所有方法
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

public class Test06 {
    public static void main(String[] args) throws Exception {
        // 1.获取字节码信息
        Class<Student> studentClass = Student.class;

        // 2.通过字节码信息获取方法


        /*
        3.获取方法的具体结构:
            @注解
            修饰符 返回值类型 方法名(参数列表) throws XXX{}
         */


        // 4.调用方法
    }
}

    1. 通过字节码信息获取方法
    // getMethods():获取public修饰的所有方法
    System.out.println("========== Student以其父类的所有公有方法 ==========");
    Method[] methods = studentClass.getMethods();
    for (Method method : methods) {
        System.out.println(method);
    }
    
    // getDeclaredMethods():获取Student类(运行时类)中的所有方法
    System.out.println("========== Student类中的所有方法 ==========");
    Method[] declaredMethods = studentClass.getDeclaredMethods();
    for (Method declaredMethod : declaredMethods) {
        System.out.println(declaredMethod);
    }
    
    // 获取指定方法
    System.out.println("========== 获取Student类中指定的方法 ==========");
    Method study1 = studentClass.getMethod("study");
    Method study2 = studentClass.getDeclaredMethod("study", String.class);
    Method relax = studentClass.getDeclaredMethod("relax");
    System.out.println(study1);
    System.out.println(study2);
    System.out.println(relax);
    
    1. 获取方法的具体结构
    System.out.println("========== 获取myMethod()的具体结构 ==========");
    Method myMethod = studentClass.getDeclaredMethod("myMethod", String.class);
    // 注解,只有Runtime的注解才能获取到@Retention(RetentionPolicy.RUNTIME)
    System.out.println("myMethod()的注解:");
    Annotation[] annotations = myMethod.getAnnotations();
    for (Annotation annotation : annotations) {
        System.out.println(annotation);
    }
    // 修饰符
    System.out.println("myMethod()的修饰符:");
    System.out.println(Modifier.toString(myMethod.getModifiers()));
    // 返回值
    System.out.println("myMethod()的返回值:");
    System.out.println(myMethod.getReturnType());
    // 方法名
    System.out.println("myMethod()的方法名:");
    System.out.println(myMethod.getName());
    // 参数列表
    System.out.println("myMethod()的参数列表:");
    Class<?>[] parameterTypes = myMethod.getParameterTypes();
    for (Class<?> parameterType : parameterTypes) {
        System.out.println(parameterType);
    }
    // 异常
    System.out.println("myMethod()的异常:");
    Class<?>[] exceptionTypes = myMethod.getExceptionTypes();
    for (Class<?> exceptionType : exceptionTypes) {
        System.out.println(exceptionType);
    }
    
    1. 调用方法
    System.out.println("========== 调用myMethod() ==========");
    Object o = studentClass.newInstance();
    myMethod.invoke(o, "Haha");
    

6.4 获取类的接口,所在包,注解

import java.lang.annotation.Annotation;

public class Test07 {
    public static void main(String[] args) {
        // 获取字节码信息
        Class<Student> studentClass = Student.class;

        // 获取运行时类(Student)的接口
        System.out.println("========== Student的接口 ==========");
        Class<?>[] interfaces = studentClass.getInterfaces();
        for (Class<?> anInterface : interfaces) {
            System.out.println(anInterface);
        }

        // 得到父类的接口
        System.out.println("========== Student父类(Person)的接口 ==========");
        Class<? super Student> superclass = studentClass.getSuperclass();
        Class<?>[] superclassInterfaces = superclass.getInterfaces();
        for (Class<?> superclassInterface : superclassInterfaces) {
            System.out.println(superclassInterface);
        }

        // 获取运行时类所在的包
        System.out.println("========== Student的包路径 ==========");
        Package aPackage = studentClass.getPackage();
        System.out.println(aPackage);
        System.out.println(aPackage.getName());

        // 获取运行时类的注解
        System.out.println("========== Student的注解 ==========");
        Annotation[] annotations = studentClass.getAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println(annotation);
        }
    }
}

7. 关于反射的面试题

【1】问题1:创建Person的对象,以后用new Person() 创建,还是用反射创建?
【分析】隐含意思是,什么时候用反射创建对象?
【答】平常创建对象当然是new。所以应该回答的是,什么时候使用反射?当要体现程序的拓展性时才会使用反射。例如之前的支付宝/微信案例,需要进行动态处理的时候,才会使用反射来创建对象。反射体现的是拓展性和动态性

【2】问题2:反射是否破坏了面向对象的封装性?
【分析】确实是破坏了,因为可以对private修饰的属性方法进行操作。但是反过来,如果真的破坏了,为什么还有那么多人,那么多框架使用?这里要知道封装性的目的是为了提高代码安全性,这里应该绕开封装这个概念去讲反射真正的目的。
【答】确实破坏了。但是反射并不是为了破坏封装性而出现的,它是为了提高代码的动态性而出现的。private是一种警示作用,是建议不使用。简单举例:男女厕所的标志,女厕所标志是为了警告男士不能进入,但是不乏有男扮女装之流进入,那也没办法。