[Java]反射

简介: 本文详细介绍了Java反射机制的基本概念、使用方法及其注意事项。首先解释了反射的定义和类加载过程,接着通过具体示例展示了如何使用反射获取和操作类的构造方法、方法和变量。文章还讨论了反射在类加载、内部类、父类成员访问等方面的特殊行为,并提供了通过反射跳过泛型检查的示例。最后,简要介绍了字面量和符号引用的概念。全文旨在帮助读者深入理解反射机制及其应用场景。

【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://developer.aliyun.com/article/1631542
出自【进步*于辰的博客

参考笔记二,P75.3;笔记三,P15.2、P43.2、P44.2、P64.3、P69.1。

1、什么是“反射”?

关于类加载,详述可查阅博文《[Java]知识点》中的【类加载】一栏。

1.1 概述

大家先看一个图,
在这里插入图片描述
过程说明:

  1. A → B。当JVM运行,将Java源文件编译成class字节码文件。
  2. B → D。当如下代码执行时,JVM通过类加载器 ClassLoader 将 class 字节码文件加载进JVM方法区、生成 class 信息、进而创建 Class 对象,这个过程就是类加载。(注:只有对类的主动使用才会触发类加载,例如:反射、实例化)。
    1、A.class;// A 是类名
    2new A().getClass();
    3、Class.forName();
    
  3. D → E。通过调用newInstance(),使用 Class 对象创建实例。

总结:反射是一种通过类加载加载磁盘中的class字节码文件、创建实例的机制。

1.2 反射的另一种情形

先说一个结论:

通过对实例进行反编译、进而创建 Class 对象的机制也属于反射,

PS:坦白说,我得出这个结论的依据是:“生成Class对象是反射的标志”,目前这个结论没有理论支持。当然,“生成Class对象是反射的标志”这个结论也没有理论支持,是我对反射的理解。可能未必准确,但在目前,这有助于我的学习和理解。

步入正题:

1、A.class;
2new A().getClass();
3、Class.forName();

由上文可知,生成 Class 对象是反射的标志,以上这3条代码都可创建 Class 对象。所以这三种情形都是反射,反射基于类加载,那是不是都触发了类加载?

实际上,只有第3种才会触发类加载。下面我一一证明。

大家先看个图,这是通过反编译,使用实例生成 Class 对象的过程。(PS:这就是开头结论所说的“反射”)
在这里插入图片描述
大家看出来了吧,这就是getClass()执行的过程,可这个过程不会触发类加载。为什么?因为类加载只会执行一次,既然存在实例,自然已完成了类加载。

结论一

getClass()是反射,但不会触发类加载。

反射的最终目的是实例,可有时候只是为了获取 Class 对象。若已存在实例,则通过调用getClass()获取会更简便。

我为何要特别说明“反编译不会触发类加载”这一细节?

平日看源码的时候,经常会看到这样的代码块:

static {
   }

这个叫做“静态代码块”,它执行于类初始化时(类加载的第三过程)。在这里会编写一些为类变量赋初始值或初始操作的代码,而往往这些代码并不容易看懂,那就需要debug。(PS:进行debug前当然需要先知道什么情况下才会执行static {}

总结

只有Class.forName()实例化 才会触发类加载,而getClass()不会。并且,A.class是反射,测试得出,A.class也同样不会触发类加载,故可判断A.class也是通过反编译进行反射,自然也不会执行static {}

PS:的确,这个结论不是很严谨。不过,我们学习,很多时候不都是“从结论看过程”嘛。

1.3 扩展

1:静态内部类的类加载。

大家看一个栗子。

class A {
   
    static class B {
   
        static {
   
            sout "csdn";
        }
    }
}

什么情况下才会打印"csdn"?据上文可知,只要进行类加载,就会执行static {}

虽然内部类属“懒加载”,但其类加载在本质上与外部类的类加载相同,即当执行Class.forName()或实例化时就会触发类加载。也就是这样:

1、Class bClass = Class.forName("A$B");
2、B b1 = new A.B();

2:为什么不能在类方法中实例化非静态内部类,而静态内部类可以?

因为类方法加载于类加载时,而非静态内部类属“懒加载”,在外部类调用时才加载。换言之,外部类类加载时不会加载非静态内部类(可视为不存在),自然无法实例化。

而静态内部类同外部类一起加载(可视为“积极加载”),自然可以实例化。

PS
可能大家会疑惑,为什么我不对其他几种内部类的类加载进行说明?因为:

  1. 对于其他几种内部类的类加载我暂未研究;
  2. 只有静态内部类内才能定义static {}。(具体说明可查阅博文《[Java]知识点》中的【static关键字】一栏)

3:在外部类已加载(如:已实例化)的情况下,静态内部类与非静态内部类的状态如何(两者都还未使用)?

先说静态内部类。静态内部类属于“积极加载”,会跟随外部类一同加载。因此,此时静态内部类已分配内存(存在引用),各个成员都为默认值。

再说非静态内部类。非静态内部类属于“懒加载”,只有当外部类使用时才开始加载。因此,此时JVM中还不存在非静态内部类的Class信息。又因为非静态内部类也是外部类的成员,外部类已加载,所有成员为默认值,故非静态内部类为null

注意:虽然内部类是外部类的成员,但与成员变量不同,以上说“非静态内部类为null”是根据理论推导得出,实际无法在以上条件下测试。因此,大家可理解为“可视为非静态内部类为null”。

2、如何使用反射?

2.1 概述

我们使用反射是为了什么?自然是获取类成员。在反射的使用中,直接涉及的类是Class。也就是说,我们是通过Class类的成员方法来获取各种类成员。

以下3个方法可分别用于获取构造方法、方法(包括成员方法、类方法)和变量(包括成员变量、类变量)。

// 获取构造方法,xx 是构造方法形参的数据类型的 Class对象
getConstructor(xx);
// 获取方法,包括成员方法和类方法,a 是方法名,b 是方法形参的数据类型的Class对象,故 b 的位置是可变参数
getMethod(a, b);
// 获取变量,包括成员变量和类变量,xx 是变量名
getField(xx);

这3个方法,为何通过这些参数,就可以定位到具体的成员?大家想想这3种成员的特点就明白了。

大家看到这里,肯定产生了两个问题:

  1. 有没有可以获取对应所有成员的方法?
  2. 以上这3个方法,可以在所有范围内(private、protected、default、public),获取到对应的指定成员吗?

大家点开Class类的源码,就可以看到,有一些以s结尾的成员方法,那就是第一个问题的答案。

同时,也有一些以getDeclared开头的成员方法,如:getConstructor()getDeclaredConstructor(),两者有什么区别?前者返回的是公共(public)构造方法,后者返回的是所有构造方法。

同理,获取另外两种成员的成员方法也是这样,大家自行测试一下就都明白了。

2.2 综合示例

为了便于大家阅读,我以大家最熟悉的String类为例。
(PS:我会尽量在这个示例内,简洁明了地将三种成员的获取与使用展示出来,关键的是方法的使用,大家注意形参和返回值,功能不重要)

Class z1 = Class.forName("java.lang.String");
// 获取构造方法 String(char[] value, boolean share)
Constructor c1 = z1.getDeclaredConstructor(char[].class, boolean.class);
c1.setAccessible(true);
sout c1;// java.lang.String(char[],boolean)

char[] arr1 = {
   'c', 's', 'd', 'n'};
String s1 = (String) c1.newInstance(arr1, true);// 获取示例
sout s1;// csdn

/**
 * 大家看过String类源码的都知道,String类的底层存储结构是 private final char value[],里面存储着字符串的字符序列。
 */
// 获取变量 value
Field f1 = z1.getDeclaredField("value");
f1.setAccessible(true);
// 获取值
sout Arrays.toString((char[]) f1.get(s1));// [c, s, d, n]

// 修改 value[]
char[] arr2 = {
   '博', '客', '园'};
f1.set(s1, arr2);
// 再次获取值
sout Arrays.toString((char[]) f1.get(s1));// [博, 客, 园]

/**
 * 获取大家比较熟悉的方法 public String substring(int beginIndex, int endIndex),
 * 截取,返回 [beginIndex, endIndex) 的子字符串。
 */
Method m1 = z1.getMethod("substring", int.class, int.class);
String subS1 = (String) m1.invoke(s1, 0, 2);// 调用 substring()
sout subS1;// 博客

PS:简单举例,相信大家已经有了初步掌握,其它的就需要大家自行测试了。

下面我补充一点使用细节

1:构造方法。

获取无参构造方法时,参数列表可以是()(null),调用newInstance()获取实例时同样。

2:变量。

无论成员变量、类变量,都具有唯一性,故如getField()都可以获取。获取变量值是f1.get(obj)obj是实例,表示获取哪个实例的变量值。因此,如果f1是类变量时,类变量属于类,故objnull

3:方法。

与变量同理。调用invoke()时,第 2 个参数后采用了“可变参数”。

2.3 一个特例:通过反射调用 main()

PS:相信大家看到这里,对反射已经比较熟悉了,下面这个示例我写快点。

class E {
   
    static class EE {
   
        public static void main(String[] args) {
   
            System.out.println("haha");
        }
    }

    public static void main(String[] args) throws Exception {
   
        Method m1 = EE.class.getMethod("main", String[].class);
        m1.invoke(null, (Object) args);// 打印:haha
    }
}

在调用invoke()时,实参必须强转为Object,且强转前类型必须是String[]。(注:这是反射main()时需要注意的,若是其他方法,则不需要)

3、注意

1:若获取的成员由非public修饰,则存在访问限制,在执行功能前,必须先调用setAccessible(true),目的是设置为允许强制访问

2:无法通过使用子类的 Class 对象进行反射获取任何父类成员,反之亦然。因为:

  1. 子类可访问父类所有成员,而==并非拥有==,
  2. 在JVM内存空间的中,父类初始化数据存储于子类内存空间。而==反射执行的位置是在方法区==,自然无法获取到父类成员。(详述可查阅博文《[Java]知识点》中的【类加载】一栏)

一种特殊情况:当父类的成员变量或成员方法由public修饰时(没有其他修饰符),通过getField()/getMethod()可获取。

难道真的没办法获取父类成员?

当然不是。无论 Class 对象还是实例,有一点是确定的:子类可访问父类成员。那么,就可以从此处着手。

具体办法:(目前仅限于获取父类变量。至于其他成员,由于实用性不大,故暂不探讨)

  • 办法一:将父类变量作为子类方法的返回值;
  • 办法二:先获取父类的 Field 对象,调用get()时,传入子类实例。

3:通过反射无法获取抽象类或接口的方法。

4一个误区:定义方法void get(Object obj) {},调用时,实参类型可以任意,但当通过class.getMethod("get", xx)获取此方法时,xx只能是Object.class,因为每个类的 Class 对象==唯一且不存在继承关系==。

5:获取内部类的 Class 对象,需使用特殊符号$

示例:(获取ArrayList类的嵌套类-迭代器类Itr的 Class 对象)

1、Class.forName("java.util.ArrayList$Itr");2、Class.forName("java.util.ArrayList.Itr");    ×

6一个结论

反射的本质其实就是加载 class 字节码文件、生成 Class 对象的过程。类与类之间可能存在关联,如:组合、继承或依赖等,但类的 Class 信息一定是唯一且独立的。因此,==无法通过一个类的 Class 对象获取另一个类的成员。==

对于在第2点中提到:“子类可以通过getField()/getMethod()获取父类成员变量和成员方法”,那是因为这2个方法的底层存在“父类递归机制”(从源码中获知,具体暂不明)。注意:构造方法没有此性质。

4、反射运用:跳过泛型检查

一个大家看过无数次的栗子:

ArrayList<Integer> list = new ArrayList<>();
list.add(2021);
list.add("csdn");// 编译错误

在编译时,JVM会进行泛型检查,目的是判断所赋的值或加入的值的类型是否与泛型的类型实参相同。

反射的底层机制是类加载,不经过编译,故可以跳过泛型检查。

示例:使用反射向List<Integer>集合内添加字符串。

ArrayList<Integer> list = new ArrayList<>();
list.add(2021);

// 反射获取方法,与泛型的类型实参无关,所以是Object
Method addMethod = ArrayList.class.getMethod("add", Object.class);
addMethod.invoke(list, "csdn");// 成功

sout list;// [2021, csdn]

为什么List<Integer>可以存放字符串?

关于泛型,推荐一位前辈的博文《java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一》(转发) 。
如果大家对那篇博文中的一些概念晦涩不清,可以浏览一下我写的这篇文章《[Java]泛型》。

无论是泛型接口、泛型类,亦或者泛型方法,泛型的限制作用都在于泛型检查,==作用于编译阶段==,例如示例中的addMethod.invoke(list, "csdn"),是通过反射获取的 Method 对象,直接将字符串"csdn"加入到list中,不经过编译,故跳过了泛型检查。

最后

class字节码文件中包含 字面量符号引用

1:什么是字面量?

字面量也称为“字面常量”,顾名思义,就是表面上看到的,它的表示(名称)就是它的值,

“字面量”是解释型语言中的常用的概念,如:a = 1,a 是变量,1 是字面量;编译型语言中少用,对应的是“常量”,如:int a = 1,a 是变量,1 是常量。

2:什么是符号引用?

符号引用指变量在编译时的一个地址标识,==不是确切的地址==,因为只有在运行时,才会为变量分配内存地址。

如:String s = "csdn",这个 s 写在代码中,在编译之前,就叫做“符号引用”;在编译后,称为“引用”,对应JVM内存地址。

PS:诸如这些,都是一些概念,是为各种数据所赋予的一种“称谓”。

本文中的例子是为了方便大家理解和阐述知识点而简单举出的,旨在阐明知识点,并不一定有实用性,仅是抛砖引玉。

如果大家想要更深入地了解和掌握类加载与反射,可查阅博文《Java基础之—反射(非常重要)》(转发)。

本文完结。

相关文章
|
3月前
|
安全 Java 索引
Java——反射&枚举
本文介绍了Java反射机制及其应用,包括获取Class对象、构造方法、成员变量和成员方法。反射允许在运行时动态操作类和对象,例如创建对象、调用方法和访问字段。文章详细解释了不同方法的使用方式及其注意事项,并展示了如何通过反射获取类的各种信息。此外,还介绍了枚举类型的特点和使用方法,包括枚举的构造方法及其在反射中的特殊处理。
67 9
Java——反射&枚举
|
2月前
|
安全 Java 测试技术
🌟Java零基础-反射:从入门到精通
【10月更文挑战第4天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
26 2
|
3月前
|
安全 Java API
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
String常量池、String、StringBuffer、Stringbuilder有什么区别、List与Set的区别、ArrayList和LinkedList的区别、HashMap底层原理、ConcurrentHashMap、HashMap和Hashtable的区别、泛型擦除、ABA问题、IO多路复用、BIO、NIO、O、异常处理机制、反射
【Java面试题汇总】Java基础篇——String+集合+泛型+IO+异常+反射(2023版)
|
2月前
|
IDE Java 编译器
java的反射与注解
java的反射与注解
17 0
|
3月前
|
Java 程序员 编译器
Java的反射技术reflect
Java的反射技术允许程序在运行时动态加载和操作类,基于字节码文件构建中间语言代码,进而生成机器码在JVM上执行,实现了“一次编译,到处运行”。此技术虽需更多运行时间,但广泛应用于Spring框架的持续集成、动态配置及三大特性(IOC、DI、AOP)中,支持企业级应用的迭代升级和灵活配置管理,适用于集群部署与数据同步场景。
|
3月前
|
存储 安全 Java
扫盲java基础-反射(一)
扫盲java基础-反射(一)
|
3月前
|
Java
扫盲java基础-反射(二)
扫盲java基础-反射(二)
|
5月前
|
安全 Java 测试技术
day26:Java零基础 - 反射
【7月更文挑战第26天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
35 5
|
4月前
|
缓存 安全 Java
【Java 第十篇章】反射
Java 反射技术让程序能在运行时动态获取类信息并操作对象,极大提升了灵活性与扩展性。本文将介绍反射的基本概念、原理及应用,包括如何使用 `Class`、`Field`、`Method` 和 `Constructor` 类进行动态操作。此外,还将探讨反射在动态加载、框架开发与代码测试中的应用场景,并提醒开发者注意性能与安全方面的问题,帮助你更合理地运用这一强大工具。
33 0
|
5月前
|
IDE Java 测试技术
Java进阶之反射
【7月更文挑战第14天】Java反射机制允许在运行时动态获取类信息、创建对象及调用其方法。它基于`Class`类,让我们能访问类的属性、方法、构造器。例如,通过`Class.forName()`加载类,`Class.newInstance()`创建对象,`Method.invoke()`执行方法。反射广泛应用于动态代理、单元测试、序列化及框架中,提供灵活性但牺牲了性能,且可破坏封装性。IDE的代码补全也是反射的应用之一。在使用时需谨慎,避免对私有成员的不当访问。
41 1