[Java]反射

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

参考笔记二,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 对象,这个过程就是 类加载 \color{red}{类加载} 类加载。(注:只有对类的主动使用才会触发类加载,例如:反射、实例化)。
1A.class;// A 是类名
2new A().getClass();
3Class.forName();
  1. D → E。通过调用newInstance(),使用 Class 对象创建实例。

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

1.2 反射的另一种情形

先说一个结论:

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

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

步入正题:

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

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

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

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

结论一 \color{red}{结论一} 结论一

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

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

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

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

static {}

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

总结 \color{red}{总结} 总结

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

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

1.3 扩展

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

大家看一个栗子。

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

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

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

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

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

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

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

P S \color{brown}{PS} 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]知识点》中的【类加载】一栏)

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

难道真的没办法获取父类成员? \color{grey}{难道真的没办法获取父类成员?} 难道真的没办法获取父类成员?

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

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

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

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

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

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

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

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

6 一个结论 \color{red}{一个结论} 一个结论

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

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

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

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

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

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

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

示例:使用反射向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]

为什么 \color{grey}{为什么} 为什么List<Integer> 可以存放字符串? \color{grey}{可以存放字符串?} 可以存放字符串?

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

无论是泛型接口、泛型类,亦或者泛型方法,泛型的限制作用都在于 泛型检查 \color{red}{泛型检查} 泛型检查作用于编译阶段,例如示例中的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基础之—反射(非常重要)》(转发)。

本文完结。

  • 18
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

进步·于辰

谢谢打赏!!很高兴可以帮到你!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值