Java 反射机制

不论是 Java 开发 还是 Android 开发,反射、泛型、注解 都是架构设计中很重要的一个知识点。

为了更好的理解反射,需要先简单了解一些类加载器相关的知识。

类加载器

一、类的初始化

当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载,连接,初始化三步来实现对这个类进行初始化。

  1. 加载
    就是指将 class 文件读入内存,并为之创建一个 Class 对象,任何类被使用时系统都会建立一个 Class 对象。
  2. 连接
  • 验证:是否有正确的内部结构,并和其他类协调一致。
  • 准备:负责为类的静态成员分配内存,并设置默认初始化值,静态随着类的加载而加载。
  • 解析:将类的二进制数据中的符号引用替换为直接引用。
  1. 初始化
    为堆栈开辟内存,默认初始化,构造初始化,等等。

二、类初始化时机

  • 创建类的实例。
  • 访问类的静态变量,或者为静态变量赋值。
  • 调用类的静态方法。
  • 使用反射方式来强制创建某个类或接口对应的 java.lang.Class 对象。
  • 初始化某个类的子类。
  • 直接使用 java.exe 命令来运行某个主类。

三、类加载器

负责将 .class 文件加载到内在中,并为之生成对应的 Class 对象。

  • Bootstrap ClassLoader
    根类加载器,也被称为引导类加载器,负责 Java 核心类的加载,比如 System、String 等。在 JDK 中 JRE 的 lib 目录下 rt.jar 文件中。

  • Extension ClassLoader
    扩展类加载器,负责 JRE 的扩展目录中 jar 包的加载,在 JDK 中 JRE 的 lib 目录下 ext 目录。

  • System ClassLoader
    系统类加载器,负责在 JVM 启动时加载来自 java 命令的 class 文件,以及 classpath 环境变量所指定的 jar 包和类路径。也就是说,平时我们写的 java 文件,编译后生成的 class 文件,都是通过该加载器进行加载的。

通过这些描述我们就可以知道我们常用的东西的加载都是由谁来完成的。

那么,我们如何使用这些class文件中的内容呢?这就是反射要研究的内容。

反射

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

一、获取 Class 类对象

要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是 Class 类中的方法。所以先要获取到每一个字节码文件对应的 Class 类型的对象。

有三种方式获取,下面用这个 Book.java 举例:

public class Book {

    private String name;
    public int price;

    public Book() {
    }

    Book(String name) {
        this.name = name;
    }

    public Book(String name, int price) {
        this.name = name;
        this.price = price;
    }

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

    public void function(String s) {
        System.out.println("function: " + s);
    }

    public String returnValue(String name, int price) {
        return name + " - " + price;
    }

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

    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}
  1. Object 类的 getClass() 方法
    在可以获取到该实例对象的情况下,采用该方法

    // 方式一
    Book book = new Book();
    Class c1 = book.getClass();
    
  2. 数据类型的静态 class 属性
    在可以导入该类的情况下,采用该方法

    // 方式二
    Class c2 = Book.class;
    
  3. 通过Class类的静态方法 forName(String className)
    在得知完整类名的情况下,采用该方法

    // 方式三
    try {
        Class c3 = Class.forName("com.ff.reflect.Book");
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    

开发中经常会使用方式三,首先,方式三可以结合配置文件使用,从配置文件中获取完整类名;其次,很多情况下不能获取实例对象和导入类。

二、获取构造方法

前提条件就是先要获取到 Class 文件对象

Class clazz = null;
try {
    clazz = Class.forName("com.ff.reflect.Book");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}
if (clazz == null) {
    return;
}
获取全部构造方法
  1. 获取所有公共构造方法 getConstructors()
    可以获取到 public 修饰的构造方法。

    Constructor[] constructors = clazz.getConstructors();
    for (Constructor constructor : constructors) {
        System.out.println(constructor);
    }
    

    打印结果:

    public com.ff.reflect.Book(java.lang.String,int)
    public com.ff.reflect.Book()
    
  2. 获取所有构造方法 getDeclaredConstructors()
    可以获取到全部构造方法,包括 public、protected、private 以及默认修饰的构造方法。

    Constructor[] declaredConstructors = clazz.getDeclaredConstructors();
    for (Constructor declaredConstructor : declaredConstructors) {
        System.out.println(declaredConstructor);
    }
    

    打印结果:

    private com.ff.reflect.Book(int)
    com.ff.reflect.Book(java.lang.String)
    public com.ff.reflect.Book(java.lang.String,int)
    public com.ff.reflect.Book()
    
获取单个构造方法

开发中我们一般需要使用一种构造方法,所以下面方式更为常用。

  1. 获取单个公共构造方法 getConstructor()

    • 无参构造
    try {
        Constructor constructor = clazz.getConstructor();
        Object object = constructor.newInstance();// 无参构造
        System.out.println(object);
    } catch (NoSuchMethodException | IllegalAccessException
            | InstantiationException | InvocationTargetException e) {
        e.printStackTrace();
    }
    
    • 带参构造
    try {
        Constructor constructor = clazz.getConstructor(String.class, int.class);
        Object object = constructor.newInstance("Java", 18);// 两个参数的构造
        System.out.println(object);
    } catch (NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    
  2. 获取单个非公共构造方法 getDeclaredConstructor()
    和上面获取公共构造是类似的,只不过将 getConstructor() 替换为 getDeclaredConstructor() 就可以获取非 public 的构造方法了。

    try {
        Constructor declaredConstructor = clazz.getDeclaredConstructor(String.class);
        Object object = declaredConstructor.newInstance("Java");
        System.out.println(object);
    } catch (NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    

    需要注意的是,如果反射得到的是私有构造方法,那么直接调用会报: IllegalAccessException 非法访问异常,需要使用暴力访问,即设置 setAccessible(true)

    try {
        Constructor constructor = clazz.getDeclaredConstructor(int.class);
        constructor.setAccessible(true);// 暴力访问
        Object object = constructor.newInstance(18);// 私有构造
        System.out.println(object);
    } catch (NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    

三、获取成员变量

与上面获取构造方法大同小异

获取全部成员变量
  1. 获取所有公共成员变量 getFields()
    Field[] fields = clazz.getFields();
    for (Field field : fields) {
        System.out.println(field);
    }
    
  2. 获取所有成员变量 getDeclaredFields()
    Field[] declaredFields = clazz.getDeclaredFields();
    for (Field declaredField : declaredFields) {
        System.out.println(declaredField);
    }
    
获取单个成员变量
  1. 获取单个公共成员变量 getField()

    try {
        Constructor constructor = clazz.getConstructor();
        Object object = constructor.newInstance();
        Field price = clazz.getField("price");// 获取 price 成员变量
        price.set(object, 18);// 修改成员变量的值
        System.out.println(object);
    } catch (NoSuchFieldException | NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    
  2. 获取单个非公共成员变量 getDeclaredField()

    try {
        Constructor constructor = clazz.getConstructor();
        Object object = constructor.newInstance();
        Field name = clazz.getDeclaredField("name");// 获取 name 成员变量
        name.set(object, "Java");// 修改成员变量的值
        System.out.println(object);
    } catch (NoSuchFieldException | NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    

    需要注意的是,如果反射得到的是私有成员变量,那么直接调用会报: IllegalAccessException 非法访问异常,需要使用暴力访问,即设置 setAccessible(true)

    try {
        Constructor constructor = clazz.getConstructor();
        Object object = constructor.newInstance();
        Field name = clazz.getDeclaredField("name");// 获取 name 成员变量
        name.setAccessible(true);// 暴力访问,可访问私有成员变量
        name.set(object, "Java");// 修改成员变量的值
        System.out.println(object);
    } catch (NoSuchFieldException | NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    

四、获取成员方法

获取全部成员方法
  1. 获取所有公共成员方法,包括父类 getMethods()

    Method[] methods = clazz.getMethods();
    for (Method method : methods) {
        System.out.println(method);
    }
    
  2. 获取所有成员方法,不包含父类 getDeclaredMethods()

    Method[] declaredMethods = clazz.getDeclaredMethods();
    for (Method declaredMethod : declaredMethods) {
        System.out.println(declaredMethod);
    }
    
获取单个成员方法
  1. 获取单个公共成员方法 getMethod()

    • 无参数、无返回值
    try {
        Constructor constructor = clazz.getConstructor();
        Object object = constructor.newInstance();
        Method show = clazz.getMethod("show");// show 方法
        show.invoke(object);// 调用 show 方法
    } catch (NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    
    • 带参数、无返回值
    try {
        Constructor constructor = clazz.getConstructor();
        Object object = constructor.newInstance();
        Method function = clazz.getMethod("function", String.class);// function 方法
        function.invoke(object, "hello");// 调用 function 方法,传参 hello
    } catch (NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    
  • 带多个参数,有返回值
    try {
        Constructor constructor = clazz.getConstructor();
        Object object = constructor.newInstance();
        Method returnValue = clazz.getMethod("returnValue", String.class, int.class);// returnValue 方法
        Object string = returnValue.invoke(object, "Java", 18);// 调用 returnValue 方法,传参,得到方法返回值
        System.out.println(string);// 打印 returnValue 方法返回值
    } catch (NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    
  1. 获取单个非公共成员方法 getDeclaredMethod()
    需要注意的是,如果反射得到的是私有成员方法,那么直接调用会报: IllegalAccessException 非法访问异常,需要使用暴力访问,即设置 setAccessible(true)
    try {
        Constructor constructor = clazz.getConstructor();
        Object object = constructor.newInstance();
        Method hello = clazz.getDeclaredMethod("hello");// 私有成员方法
        hello.setAccessible(true);// 暴力访问
        hello.invoke(object);// 调用 hello 方法
    } catch (NoSuchMethodException | InstantiationException
            | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    

反射的应用

一、跳过泛型检查

向 ArrayList<Integer> 中添加字符串数据。
由于泛型只在编译期间生效,而反射是在运行期间调用,所以可以利用这两点进行实现:

/**
 * 向 ArrayList<Integer> 中添加字符串数据
 */
private static void test() {
    ArrayList<Integer> array = new ArrayList<>();

    Class<? extends ArrayList> aClass = array.getClass();
    try {
        Method add = aClass.getMethod("add", Object.class);
        add.invoke(array, "hello");
        add.invoke(array, "world");
    } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
        e.printStackTrace();
    }
    System.out.println(array);
}

二、通用工具类

设置某个对象的某个属性为指定值:
public void setProperty(Object obj, String propertyName, Object value){},
此方法可将obj对象中名为propertyName的属性的值设置为value。

public class Utils {

    /**
     * 设置某个对象的某个属性为指定值
     *
     * @param obj          对象
     * @param propertyName 属性
     * @param value        值
     */
    public static void setProperty(Object obj, String propertyName, Object value) {
        Class<?> aClass = obj.getClass();
        try {
            Field declaredField = aClass.getDeclaredField(propertyName);
            declaredField.setAccessible(true);
            declaredField.set(obj, value);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

使用工具类:

private static void test() {
    Book book = new Book();
    Utils.setProperty(book, "name", "Java");
    Utils.setProperty(book, "price", 18);
    System.out.println(book);
}

在架构设计中的应用也很常见,比如动态代理等等,就不在这里展开了。

至此,基本的 Java 反射机制都已经介绍完了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容