首页 > 编程语言 >[Java]反射

[Java]反射

时间:2024-05-23 20:07:01浏览次数:20  
标签:反射 Java 成员 Class 获取 class 加载

【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://www.cnblogs.com/cnb-yuchen/p/17960654
出自【进步*于辰的博客

参考笔记二,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 是类名
2、new A().getClass();
3、Class.forName();
  1. D → E。通过调用newInstance(),使用 Class 对象创建实例。

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

1.2 反射的另一种情形

先说一个结论:

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

(PS:坦白说,我是通过“生成Class对象是反射的标志”来得出这个结论的,目前这个结论没有理论支持。当然,““生成Class对象是反射的标志””这个结论也没有理论支持,是我本人对反射的理解。不过,大家暂且可以相信。有不对之处,还请大家指正)

步入正题:

1、A.class;
2、new 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基础之—反射(非常重要)》(转发)。

本文完结。

标签:反射,Java,成员,Class,获取,class,加载
From: https://www.cnblogs.com/cnb-yuchen/p/17960654

相关文章

  • Java.年月日正则表达式
    表达式:yyyy-MM:^([1-9]{1}[0-9]{3}[\\-]{1}){1}((1[0-2]{1}){1}|(0[1-9]{1})|([1-9]{1})){1}$yyyy-MM-dd:^((((19|20)\\d{2})(-)(0?[13578]|1[02])-(0?[1-9]|[12]\\d|3[01]))|(((19|20)\\d{2})(-)(0?[469]|11)-(0?[1-9]|[12]\\d|30))|(((19|20)\\d{2})(-)(0......
  • JavaScript中reduce()详解及使用方法。
    一、定义和用法reduce()方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。reduce()可以作为一个高阶函数,用于函数的compose。reduce()方法为归并类方法,最常用的场景就是,计算数组中的每一项的总和。注意:reduce()对于空数组是不会执行回调函数的......
  • Spring AI 抢先体验,5 分钟玩转 Java AI 应用开发
    SpringAI是Spring官方社区项目,旨在简化JavaAI应用程序开发,让Java开发者像使用Spring开发普通应用一样开发AI应用。SpringCloudAlibabaAI以SpringAI为基础,并在此基础上提供阿里云通义系列大模型全面适配,让用户在5分钟内开发基于通义大模型的JavaAI应用。......
  • java 通过 microsoft graph 调用outlook(三)
    这次会添加一个Reply接口,并且使用6.10.0版本 直接上代码一,POM<!--office365--><dependency><groupId>com.microsoft.graph</groupId><artifactId>microsoft-graph</artifactId><......
  • Pairwise实现(Java篇)
    importjava.util.HashMap;/***PairWise(成对)测试方法*author:likeqc*date:2021-4-411:06:59*/classPairWise{/***@paramstrString[][],二维数组,一维数组str[i]中存放第i个因素的因子*/privatestaticvoidsolution(String[][]s......
  • 位运算符在 Javascript 中的运用
    零、资料JavaScript中的位运算和权限设计javascript位运算技巧巧用JS位运算JavaScript位运算及其妙用聊聊JavaScript中的二进制数一、权限在权限设计时,每一个基础权限单元都是二进制数形式,有且只有一位值是1,其余全部是0,即权限码是 2^n 。所以,在这套设......
  • 关于如何使用JNI将C语言接口打包成可供java环境调用的so库文件
    一、环境检查在linux下打包.so文件,首先需要确认是否有安装java环境,可通过在终端中输入指令java的方式来进行查看。如下图所示,则为已安装java环境。  若当前未安装java环境,则可通过在终端中输入如下指令进行安装,我这里使用的java环境为1.8.0版本。sudoapt-getinstallo......
  • 使用-HTML5-和-JavaScript-开发-Windows-商店应用-全-
    使用HTML5和JavaScript开发Windows商店应用(全)原文:zh.annas-archive.org/md5/8F13EC8AC7BDB8535E7218C5DDB48475译者:飞龙协议:CCBY-NC-SA4.0序言使用HTML5和JavaScript开发WindowsStore应用是一本实践性强的指南,涵盖了WindowsStore应用的基本重要特性以及......
  • 精通-JavaScript-高性能-全-
    精通JavaScript高性能(全)原文:zh.annas-archive.org/md5/582AFDEF15013377BB79AB8CEA3B2B47译者:飞龙协议:CCBY-NC-SA4.0序言欢迎来到精通JavaScript高性能。在这本书中,我们已经以帮助任何JavaScript开发者,无论他们是新手上路还是经验丰富的老手的方式,覆盖了JavaScrip......
  • Java的深浅拷贝认识
    目录浅拷贝深拷贝分辨代码里的深浅拷贝在Java中,深拷贝和浅拷贝是对象复制的两种方式,主要区别在于对对象内部的引用类型的处理上。浅拷贝定义:浅拷贝是指创建一个新的对象,但这个新对象的属性(包括引用类型的属性)仍然指向原来对象的属性。换言之,如果原对象中的属性是一个引用类型......