Java基础-反射

590 阅读8分钟

Java基础-反射

1.概述

Java反射机制指的是在Java程序运行状态中,对于任何一个类,都可以获得这个类的所有属性和方法;对于给定的一个对象,都能够调用它的任意一个属性和方法。这种动态获取类的内容以及动态调用对象的方法称为反射机制

通过Java中的反射机制可以实现:

(1)在运行时判定任意一个对象所属的类 ,如:逆向代码、反编译

(2) 在运行时构造任意一个类的对象 ,如:与注解相关的框架

(3) 在运行时判定任意一个类所具有的成员变量和方法 ,如:单纯反射机制应用框架

(4)在运行时调用任意一个对象的方法,如:动态生成类框架(Gson)

(5)动态代理

感觉反射就是框架设计的灵魂,优点在于提高了Java程序的灵活性和扩展性,有效降低了类之间的耦合。

缺点是反射相当于一系列解释操作,性能较差,而且使用反射会破坏抽象,模糊了内部逻辑。

2.相关概念

要理解反射需要先了解Class、Constructor 、Method、Field 四个类,以及静态加载和动态加载

2.1 Class类

认识Class对象之前,先来了解一个概念,RTTI(Run-Time Type Identification)运行时类型识别,其作用是在运行时识别一个对象的类型和类的信息。Class对象就是用于描述一个类运行时的类型信息,如有哪些构造方法,都有哪些成员方法,都有哪些成员变量等。

创建Class对象

我们知道当需要使用一个类时需要经过加载->链接->初始化三个阶段。Class对象是由JVM在加载过程创建的:

(1)通过类全限定名获取.class的二进制字节流。

  • 从zip文件中读取,如:从jar、war、ear等格式的文件中读取class文件内容
  • 从网络中读取,如:Applet
  • 动态生成,如:动态代理、ASM框架等

(2)将Class的二进制内容加载到虚拟机方法区。

(3)在内存中生成一个java.lang.Class 对象来描述刚加载的类

获取Class对象

获取Class对象主要有三种方式:

(1)Class.forName(类的全限定名) 获得指定类的Class对象。

Class clazz = Class.forName("com.www.Test");

(2)对象实例.getClass() 获得对象实例的Class对象。

Class clazz = person.getClass();

(3)类字面常量。

Class clazz = Person.class;
int.class

Class对象的作用

Class对象可以获取类中属性的类型和名称,获取类中的方法,获取类的基类等等 。

(1)获取类加载器

​ ClassLoader getClassLoader()

(2)获取构造器

  • Constructor getConstructor(Class<?>... parameterTypes)

    获取该类中与参数类型匹配的公有构造器方法。

  • Constructor<?>[] getConstructors()

    获取该类中所有公有构造器方法。

  • Constructor getDeclaredConstructor(Class<?>... parameterTypes) 获取与参数类型匹配的构造方法,包括私有构造方法。

  • Constructor<?>[] getDeclaredConstructors() 获取该类中所有构造器方法。

(3)获取类中方法

  • Method getMethod(String name, Class<?>... parameterTypes)

    根据方法名称和参数类型匹配公有方法。

  • Method[] getMethods()

    返回类中所有公有方法。

  • Method getDeclaredMethod(String name, Class<?>... parameterTypes)

    获取某个指定方法。

  • Method[] getDeclaredMethods()

    获取类中所有方法。

(4)获取类中属性

  • Field getField(String name) 根据名称获取类中公有属性。
  • Field[] getFields() 获取类中公有属性的列表。
  • Field getDeclaredField(String name) 根据名称获取类属性。
  • Field[] getDeclaredFields() 获取类中属性列表。

(5)其他重要方法

  • getInterfaces() 获取类实现的接口列表。
  • getSuperclass() 获取类的父类Class对象。
  • T newInstance() 创建一个实例对象,调用无参构造函数。
  • A getAnnotation(Class<A> annotationClass) 根据传入类型获取Class中的注解
  • Class<?> forName(String className) 根据传入类型全路径名创建Class对象

方法很多这里暂不展开。

示例

public class Person {
    public String name;
    public int age;
    // 无参构造器
    public Person() {
        this.name = "";
        this.age = 0;
    }
    // 有参构造器
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // 私有构造器
    private Person(String name){
        this.name = name;
        this.age = 99999;
    }

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

使用Class对象创建类实例对象。

Class<Person> personClass = Person.class;
System.out.println(personClass.getName());
System.out.println(personClass.getClassLoader().getClass().getName());
Person person = personClass.newInstance();
System.out.println(person);

2.2 Constructor类

Constructor代表了一个构造器该有的相关信息,当然构造器最重要的作用就是创建实例对象。

创建实例对象

调用Constructor的newInstance方法即可创建实例对象。

  • public T newInstance(Object ... initargs)

代码实例:

Class<Person> clazz = Person.class;
// 1.调用有参构造函数并创建实例对象
Constructor<Person> c1 = clazz.getConstructor(new Class[]{String.class, int.class});
Person p1 = c1.newInstance("aaa", 18);

// 2.调用无参构造函数创建实例对象
Constructor c2 = clazz.getConstructor();
Person p2 = (Person) c2.newInstance();

// 3.调用私有构造函数并创建对象
Constructor<Person> c3 = clazz.getDeclaredConstructor(new Class[]{String.class});
c3.setAccessible(true);// 私有方法须开启权限
Person p3 = c3.newInstance("ccc");

2.3 Method类

Method封装了一个方法拥有的所有信息,包括方法名称、返回类型、参数类型注解等等,这些信息可通过一系列getXxx 方法得到。

调用方法

当然作为一个Method类最重要的方法是invoke

Object invoke(Object obj, Object... args) 第一个参数是要运行方法的宿主对象。

示例:

Class<Person> clazz = Person.class;
// 1. 先创建要运行方法的宿主对象实例
Person p = clazz.newInstance();

// 2. 调用公有方法
Method method1 = clazz.getMethod("sayHello", String.class);
method1.invoke(p,"Hi !");

// 3. 调用私有方法
Method method2 = clazz.getDeclaredMethod("spendMoney", float.class);
method2.setAccessible(true);
method2.invoke(p,78.88f);

2.4 Field类

Field类描述了一个类属性相关信息,包括属性名称和类型等,主要功能是修改和查看属性值。

主要方法:

  • String getName() 查询属性名称。
  • Object get(Object obj) 查询传入obj实例对象该Field中的值。
  • void set(Object obj, Object value) 设置obj对象的该Field属性的值为value。

当然还包括了一些基本类型属性的get/set方法。

示例:

Class<Person> clazz = Person.class;
Person p = clazz.newInstance();

// 修改并查看公有属性
Field name = clazz.getField("name");
System.out.println(name.getName());
name.set(p, "xiaomin");// 修改p实例对象的name属性
System.out.println(name.get(p));

// 修改并查看私有属性
Field age = clazz.getDeclaredField("age");
age.setAccessible(true);// 打开权限
age.set(p, 18);// 修改
System.out.println(age.get(p));

2.5 静态加载和动态加载

静态加载:在程序编译时就加载可能用到的所有类,任何一个类出错都无法编译通过,简单来说所有new出来的对象都是静态加载的。

动态加载:在程序运行到某处要用到一个类的时候再加载。

实例:

静态加载将可能用到的类都硬编码到代码中,编译时都会加载。

static void staticDemo(String cmd) {
    // 静态加载对象
    if ("ACar".equals(cmd)) {
        ACar aCar = new ACar();
        aCar.run();
    } else if ("BCar".equals(cmd)) {
        BCar bCar = new BCar();
        bCar.run();
    }
}

动态加载将相同的方法抽象为接口,动态加载要运行的类对象。

static void dynamicDemo(String cmd) {
    try {
        Class clazz = Class.forName(cmd);
        ICar car = (ICar) clazz.newInstance();
        car.run();
    } catch (Exception e) {
        e.printStackTrace();
    }
}


interface ICar {
    void run();
}

class ACar implements ICar {
    @Override
    public void run() {
        System.out.println("ACar run!!!");
    }
}

class BCar implements ICar {
    @Override
    public void run() {
        System.out.println("BCar run!!!");
    }
}

3.反射应用

Java反射机制是指java程序在运行过程中,可以获取任意一个类的相关信息,并且能够调用其方法和属性,这种动态获取信息和动态调用方法的功能叫做Java的反射机制

这里先总结下反射的流程:

(1)先拿到一个类的Class对象。

(2)使用Constructor对象的newInstance方法创建类实例对象。

(3)得到要调用方法的Method对象,利用invoke进行方法调用。

(4)使用Field对象获取或修改实例对象属性。

针对私有成员还需要先setAccessible设置权限。

一句话,反射机制的优点就是可以实现动态创建对象和编译,体现出很大的灵活性,特别是在J2EE的开发中它的灵活性就表现的十分明显。比如,一个大型的软件,不可能一次就把把它设计的很完美,当这个程序编译后,发布了,当发现需要更新某些功能时,我们不可能要用户把以前的卸载,再重新安装新的版本,假如这样的话,这个软件肯定是没有多少人用的。采用静态的话,需要把整个程序重新编译一次才可以实现功能的更新,而采用反射机制的话,它就可以不用卸载,只需要在运行时才动态的创建和编译,就可以实现该功能。 它的缺点是对性能有影响。使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且它满足我们的要求。这类操作总是慢于只直接执行相同的操作。

// 后续补充其它应用案例

参考: