Java 反射

参考:

《尚硅谷Java入门视频教程》https://www.bilibili.com/video/BV1Kb411W75N

《【狂神说Java】Java零基础学习视频通俗易懂》https://www.bilibili.com/video/BV12J41137hu

推荐看尚硅谷的视频,更加详细

动态/静态语言

动态语言是在运行时能动态改变结构的语言,例如PHP,所以可以用一句话木马执行很多命令。

静态语言是在运行时不能动态改变结构的语言,如Java、C等。

反射机制能让静态语言Java获得一些动态特性,变为准动态语言。

反射是什么

Reflection(反射)是Java被视为动态语言的关键,反射机制允许程序在执行期间借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。

加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,形象地称之为:反射。

反射,要注意这个“反”字,例如下图:

Java 反射

优缺点

优点:可以实现动态创建对象和编译,体现出很大的灵活性

缺点:对性能有影响。使用反射基本上是一种解释操作,我们告诉JVM,我们希望做什么并且它满足我们的要求。这类操作总是慢于直接执行相同的操作。

下面是一个测试性能的例子:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

// 分析性能问题
public class Test10 {
    // 普通方式调用
    public static void test01() {
        User user = new User();

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 1000000000; i++) {
            user.getName();
        }

        long endTime = System.currentTimeMillis();

        System.out.println("普通方式执行10亿次:" + (endTime - startTime) + "ms");
    }

    // 反射方式调用
    public static void test02() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        User user = new User();
        Class c1 = user.getClass();

        Method getName = c1.getDeclaredMethod("getName", null);

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 1000000000; i++) {
            getName.invoke(user, null);
        }

        long endTime = System.currentTimeMillis();

        System.out.println("反射方式执行10亿次:" + (endTime - startTime) + "ms");
    }

    // 反射方式调用,关闭检测
    public static void test03() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        User user = new User();
        Class c1 = user.getClass();

        Method getName = c1.getDeclaredMethod("getName", null);
        getName.setAccessible(true);

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 1000000000; i++) {
            getName.invoke(user, null);
        }

        long endTime = System.currentTimeMillis();

        System.out.println("关闭检测执行10亿次:" + (endTime - startTime) + "ms");
    }

    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
        test01();
        test02();
        test03();
    }
}

执行结果:

Java 反射

Class对象

要学会反射,首先要学习一下Class对象,要学习Class对象,首先要简单复习一下类的加载过程。

一个Class对象就是java.lang.Class类的一个实例。

.java文件(源代码)经过javac.exe编译后生成一个或多个.class文件(字节码文件),使用java.exe对某个字节码文件进行解释运行,相当于将某个字节码文件加载到内存中,这就是类的加载。这个加载到内存中的类,就是java.lang.Class的一个实例,也称为运行时类。

上面说过,这个java.lang.Class对象包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。通过这个对象,可以获得其对应类的属性、方法、构造器、实现的接口等。那我们怎么获取这个对象呢?有三种方式可以获取一个类的Class对象:

  1. 调用运行时类的class属性:Class clazz1 = User.class;

  2. 通过运行时类的对象,调用getClass():

    User u1 = new User();
    Class clazz2 = u1.getClass();
    
  3. 调用Class的静态方法:forName(String classPath) Class clazz3 = Class.forName("com.a.reflection.User");。forName方法既可以获取我们自己写的类的Class对象,也可以获取系统类的Class对象。

上面三种方式如果获取的是同一个类的Class对象,则它们获取的是同一个对象,也就是 clazz1 == clazz2 == clazz3,因为一个类同一时间只会在内存中生成一个Class对象。加载到内存中的运行时类,会缓存一定的时间,在此时间之内,我们可以通过不同的方式来获取此运行时类。

除了类,还可以有Class对象的类型有:interface(接口)、数组(对于数组来说,只要数组的元素类型与维度一样,就是同一个Class)、enum(枚举)、annotation(注解)、基本数据类型、void(void也可以看作一种类型)。

下面是Class对象的一些注意点:

  • Class对象只能由系统建立
  • 一个加载的类在JVM中只会有一个Class实例
  • 一个Class对象对应的是一个加载到JVM中的.class文件
  • 每个类的实例都会记得自己是由哪个Class实例所生成
  • 通过Class对象可以完整地得到一个类中的所有被加载的结构
  • Class类是Reflection的根源,针对任何想动态加载、运行的类,唯有先获得相应的Class对象。

Java 反射

类的加载

下面简单讲一下类的加载过程,类的加载分为3个过程(加载、链接、初始化):

  1. 加载:使用java.exe将字节码文件内容加载到内存中,并生成一个代表这个类的java.lang.Class对象。
  2. 链接:为类变量(static)分配内存并设置默认初始值。
  3. 初始化:使用类构造器<clinit>()方法对类进行初始化,顺序是:静态变量显式赋值/静态代码块 => 匿名代码块 => 构造方法。初始化一个类时,如果发现其父类还没有被初始化,则会先初始化它的父类。

反射的实际应用

创建运行时类的对象

newInstance(): 创建对应的运行时类的对象

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

只有通过构造器才能构造对象,newInstance()内部也是调用了对应类的无参构造器。

newInstance()能正常地创建运行时类的对象的条件:

  1. 运行时类必须提供空参的构造器。
  2. 运行时类的空参构造器可被访问,通常设置为public

获取运行时类的完整结构

包括获取运行时类的所有方法、构造器、父类、接口、所在包、注解等,都是用形如getXxx()的方法来实现的。

Class clazz = Person.class;

// getFields(): 获取当前类及其父类的所有public属性
Field[] fields = clazz.getFields();
// getDeclaredFields(): 获取当前运行时类声明的所有属性,包括所有权限(不包含父类中声明的属性)。
Field[] declaredFields = clazz.getDeclaredFields();

// getMethods(): 获取当前运行时类及其父类声明的所有public方法。
Method[] methods = clazz.getMethods();
// getDeclaredMethods(): 获取当前运行时类声明的所有方法,包括所有权限(不包含父类中声明的方法)。
Method[] declaredMethods = clazz.getDeclaredMethods();

// getConstructors(): 获取当前运行时类中声明为public的构造器
Method[] methods = clazz.getConstructors();
// getDeclaredConstructors(): 获取当前运行时类中声明的所有构造器
Method[] declaredMethods = clazz.getDeclaredConstructors();

// getSuperclass(): 获取运行时类的父类
Class superclass = clazz.getSuperclass();

Class[] interfaces = clazz.getInterfaces();
// 要获取父类的接口,可以先获取父类,再获取接口
Class[] interfaces1 = clazz.getSuperclass().getInterfaces();

// 获取当前运行时类所在的包
Package pack = clazz.getPackage();

// 获取运行时类声明的注解
Annotation[] annotations = clazz.getAnnotations();

调用运行时类的指定结构

操作运行时类中的指定属性

Class clazz = Person.class;

// 创建运行时类的对象
Person p = (Person) clazz.newInstance();

// 获取指定的public属性
Field id = clazz.getField("id");
// getDeclaredField(String fieldName): 获取运行时类中指定的属性
Field name = clazz.getDeclaredField("name");
// 使当前属性可访问
name.setAccessible(true);
/*
设置当前属性的值
set(): 参数1:指明设置哪个对象的属性  参数2:将此属性值设置为多少
*/
id.set(p, 1001);
// 设置静态属性的值
id.set(null, 1001);
/*
获取当前属性的值
get(): 参数1:获取哪个对象的当前属性值
*/
int pId = (int) id.get(p);
// 获取静态属性的值
int pId2 = (int) id.get(null);

操作运行时类中的指定方法

Class clazz = Person.class;

// 创建运行时类的对象
Person p = (Person) clazz.newInstance();

/*
1. 获取指定的某个方法
getDeclaredMethod(): 参数1:指明获取的方法的名称  参数2:指明获取的方法的形参列表
*/
Method show = clazz.getDeclaredMethod("show", String.class);
// 2. 保证当前方法是可访问的
show.setAccessible(true);
/*
3. invoke(): 参数1:方法的调用者  参数2:给方法形参赋值的实参
invoke()的返回值即为对应类中调用的方法的返回值。
*/
Object returnValue = show.invoke(p, "CHN");

// 调用静态方法
// private static void showDesc()
Method showDesc = clazz.getDeclaredMethod("showDesc");
showDesc.setAccessible(true);
// 如果调用的运行时类中的方法没有返回值,则此invoke()返回null
Object returnVal = showDesc.invoke(null);

调用运行时类中的指定构造器

Class clazz = Person.class;

// private Person(String name)
/*
1. 获取指定的构造器
getDeclaredConstructor(): 参数:指明构造器的参数列表
*/
Constructor constructor = clazz.getDeclaredConstructor(String.class);

// 2. 保证此构造器是可访问的
constructor.setAccessible(true);

// 3. 调用此构造器创建运行时类的对象
Person per = (Person) constructor.newInstance("Tom");
上一篇:通俗易懂讲反射


下一篇:防抖 与 节流