目录
二.获取Class对象的三种方法:(Three ways to get a Class object)
newInstance(Object... initargs):(重点)
invoke (Object obj, Object... args) :(invoke本身就是调用的意思)
set (Object obj, Object value):
前言
大家好,今天给大家带来一篇详解JAVA反射基础的博文(将来会出JAVA反射进阶的讲解,准备放在JAVA进阶专栏),我会从字节码文件对象,构造器对象,方法对象,属性对象一一进行代码演示,由于Constructor,Method,和Field三者的用法大同小异,于是up决定重点把Constructor构造器这块儿讲得通透点,注意:注释内容也是重点,(注释内容也是重点,注释内容也是重点,)有利于大家举一反三。不要眼高手低,跟着up一块儿练习,看完就会用反射。学习反射基础需要有一定的javaSE基础,尤其是file类和Exception类。我之后会陆续出一系列专门讲javaSE基础的文章,把javaSE基础篇全讲一遍。所有文章都会适时补充完善,良工不示人以朴。PS:使用IEDA讲解,点击目录可以跳转。(前言编辑于2022.10.12)
一.反射及其相关概念
1.什么是反射?
反射指的是在程序运行的过程中分析类的一种能力。你可以这么理解,在程序运行过程中,我可以查看并使用其他类的构造器,方法和属性,这就是反射的能力。反射可以在不修改源码的情况下,通过外部配置文件来控制程序,符合设计的ocp原则(开闭原则:不修改源码,扩充功能)。
光这么说还是过于抽象,我们直接上图片:
如图,我们可以得知反射的第一步是获取字节码文件对象 。
2.反射的用途:
①分析类:
加载并初始化一个类(反射创建类的字节码文件对象时会加载类)
查看类的所有属性和方法(即Constructor, Method, Field,etc)
②查看并使用对象:
查看一个对象的全部属性和方法
使用对象的任意属性和方法
3.反射的应用场景:
构建通用的工具
搭建具有高度灵活性和扩展性的系统框架(框架底层的核心就是——反射可以动态的构建和使用对象)。
4.类加载器:
类加载器,(ClassLoader)。.java的源文件经过javac.exe编译后,会生成.class的字节码文件,而类加载器负责将类的字节码文件(.class)加载到堆内存中,并生成对应的Class对象。
类加载器加载类的过程,就是反射的体现;当类的字节码文件被加载到堆内存后,类的成员便可以当作对象被处理,其中,成员变量对应Field[]、构造器对应Constructor[]、成员方法对应Method[]。
此外,通过new关键字创建出的堆空间中真正的对象,即类的实例,也可以通过这种映射关系找到它自身对应的Class对象(也叫字节码文件对象)。
PS : 类的加载还可以分为静态加载和动态加载。
静态加载指编译时加载相关的类,如果没有该类则报错,依赖性强;
动态加载指运行时加载相关的类,如果运行时没有用到该类,那么即使该类不存在也不会报错,降低了依赖性。
类的加载时机:
①创建类的实例时:(静态加载)
eg:Dog dog = new Dog();
注意:一个类的字节码文件,只会被加载一次
②访问类的静态成员时:(静态加载)
eg:Collections.shuffle();
③初始化类的子类时:(要先加载其父类)(静态加载)
eg : 假设Honor类继承自Phone类,
class Honor extends Phone {
Honor h = new Honor();
}
//那么,在加载子类Honor类的字节码文件之前,必须先加载它的父类Phone类的字节码文件。
④反射方式创建类的Class对象时:(重点)(动态加载)
eg:class c = Class.forName("类的正名"); //类的正名 = 包名 + 类名
forName() 方法我们后面马上讲。
类的加载阶段:
类的加载阶段可细分为加载(loading),连接(linking),和初始化(initialization);其中,连接(linking)又可细分为验证、准备、解析三个阶段。如下图所示 :
①加载(loading) : 类加载器负责——将类的字节码文件加载到内存中的方法区,并在堆空间中生成字节码文件对应的Class对象(字节码文件对象)。
②验证(verification) : jvm负责——确实字节码文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全;具体检验内容包括文件格式验证(是否以魔数oxcafebabe开头),元数据验证,字节码验证和符号引用验证。PS : 可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间。
③准备(preparation) : jvm负责——对静态变量分配内存并进行默认初始化,从JDK8.0开始,static修饰的成员变量位于堆空间中(详情可见up的static关键字的万字详解);非静态变量(实例变量)在此阶段不作处理。而静态常量则会直接被赋值为指定的值。
④解析(resolution) : jvm负责——将常量池内的符号引用替换为直接引用的过程。
⑤初始化(initialization) : jvm负责——真正意义上地开始执行类中定义的java源代码,初始化阶段其实就是执行<clinit>()方法的过程。<clinit>()方法是由编译器按照静态语句在源文件中出现时的顺序,依次自动收集类中所有的静态变量的赋值动作和静态代码块中的语句,并进行合并(eg : 若先定义的静态变量有赋值操作,而后定义的静态代码块中仍有对同一静态变量的赋值操作,合并后,以后定义的为准)。PS : 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁,同步;如果多个线程同时去初始化一个类,那么就会只有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕(此举保证了Class对象的唯一性)。
5.Class对象:
Class对象,即java.lang.Class类的对象,也叫字节码文件对象。每个字节码文件对应一个Class对象。
联系:
①如果.java的源文件中只有一个类,那么一个.java的源文件对应一个.class的字节码文件,对应一个Class对象。
②如果.java的源文件中不止一个类,那么编译后,每一个类都对应一个字节码文件。
二.获取Class对象的三种方法:(Three ways to get a Class object)
1.通过Object类的getClass() 方法:
Class c = 对象名.getClass();
该方法需要用Class类型(首字母大写) 的引用变量来作接收,即得到一个Class对象。
注意:getClass方法要用对象名. 的形式来调用,所以一定要先创建一个对象。(得到运行类型其实就是得到实例对应的Class对象)
2.通过类的静态属性:
Class c2 = 类名.class; //eg : Integer.class
该方式多用于参数的传递,例如获取构造器对象时传入“参数.class”。
3.通过Class类的静态方法:
Class c3 = Class.forName("类的正名"); //类的正名 = 包名 + 类名。中间用点'.'来连接
该方式多用于从配置文件读取类的全类名(正名),继而加载类。(多用于底层框架)
4.代码演示:(注意注释)
我们先在包下新建一个类,创建一个标准的javabean学生公共类。
package knowledge.reflect;
public class Student {
//私有属性
private String name;
private String sex;
private int age;
//空参构造
public Student() {
}
//带参构造
public Student(String name, String sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}
//setter,getter方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
好的,创建好学生类后,借机再说一下类的正名。注意看,这个学生类最顶部的那一串去掉头尾,即去掉package和分号就是包名,所以up创建的这个学生类的包名就是"knowledge.reflect",类的正名是包名+类名,所以这个学生类的正名就是"knowledge.reflect.Student"。
🆗,明白什么是正名以后,我们直接上代码,演示一下如何获取学生类的Class对象:
package knowledge.reflect;
public class GetClassObject {
//需求:使用上述提到的三种方法分别获取字节码文件对象
public static void main(String[] args) throws ClassNotFoundException {
//1.通过Object类的getClass() 方法:
Student st = new Student();
Class c1 = st.getClass(); /** 这里的Class类型一定要记得大写 */
//2.通过类的静态属性:
Class c2 = Student.class; //创建好的Student类可以现用
Class c2ex = Integer.class;
Class c2ex2 = Object.class; //这里多获取两个Class对象,作输出结果的对比。
//3.通过Class类的静态方法:
Class c3 = Class.forName("knowledge.reflect.Student");
/*
这里报错异常原因: 是因为这个正名有可能是你瞎写的,计算机不知道。
解决方案:抛出异常。
IDEA可以将光标放在报错处,快捷键Alt+Enter,选第一个即可
快速抛出异常,或者直接在本类的类名后加上throws Exception,
抛出它的父类,也可以解决。
*/
/**可以去复制所需类顶部的包名,加上类名即是类的正名*/
//思考: 如何判断获取到的三个Class对象是不是同一个对象
System.out.println("查看获取到的Class对象c1:" + c1);
System.out.println("查看获取到的Class对象c2:" + c2);
System.out.println("查看获取到的Class对象c2ex:" + c2ex);
System.out.println("查看获取到的Class对象c2ex2:" + c2ex2);
System.out.println("查看获取到的Class对象c3:" + c3);
System.out.println("-------------------------------");
System.out.println("你们说的c1和c2会不会是同一个Class对象?" + (c1 == c2));
System.out.println("你们说的c2和c3会不会是同一个Class对象?" + (c2 == c3));
System.out.println("你们说的c1和c3会不会是同一个Class对象?" + (c1 == c3));
System.out.println("你意思是三个Class对象其实是同一个?" + (c1==c2 && c2==c3 && c1==c3));
}
}
IDEA抛出异常快捷键Alt + Enter 演示:将光标浮在报错处,使用快捷键,效果如下:
继续直接回车,或者鼠标点击第一个选项即可抛出异常。
输出结果:
根据输出的结果,我们也能得到一些信息,比如说:
①Class对象的格式为:小写class 后面跟着该类的正名
②验证了我们上方说的:编译后,每一个类都对应一个字节码文件,且一个类只对应一个Class对象。c1,c2,c3这三个都是Student类的Class对象,所以它们的输出结果才相同。即,一个类只能有一个字节码文件对象。
三.获取构造器(Constructor)
1.前言(重要):
①不管是通过反射获取类的构造器(Constructor) ,方法(Method) ,还是属性(Field) , 第一步都需要先获取字节码文件对象,然后再通过字节码文件对象来分别获取构造器对象,方法对象,和属性对象。
②Constructor,Method,和FIeld都属于java.base模块下的 java.lang.reflect包,来张示意图就是这样:
③建议在用反射分析类之前,先导包!其目的是一劳永逸,以绝后患。 例如,要获取构造器对象Constructor时,就先导包:import java.lang.reflect.Constructor;
④遇到异常没必要见一个抛一个,建议直接抛出它们的父类Exception类,省时省力。 如图所示:
⑤前言看不懂没关系,之后代码演示会更直观。
2.三种方式获取构造器对象:
方式一:
getConstructor(Class<?>... parameterTypes) :
说明(重要):
①该方法返回一个Constructor类型的对象,因此需要用Constructor类型来做接收,该方法仅限于获取公共的构造函数。
②parameterTypes是参数类型的意思,
③该方法 需要传入你要获取的构造器参数的字节码文件对象,大白话就是说,假如我要获取的构造器是个无参构造,那我就不需要传入任何字节码文件对象了,假如我要获取的构造器是个带参构造,那带什么参数,我用这方法时就传入什么参数对应的字节码文件对象,说这么复杂,其实实操时很简单,直接参数.class就搞定了,构造器本身有几个参数就传入几个参数.class。
④ ?表示通配符,代表不确定的任意类型。
⑤假如本该传入对应参数的字节码文件对象,你没有传入,默认输出的是所分析类的公有空参构造。
方式二:
getDeclaredConstructor(Class<?>... parameterTypes) :
说明:
用法和方式一没什么区别,唯一一点不同就是方式二是用来获取私有构造器的。
方式三:
getConstructors() :
说明:
①该方法返回目标类所有非私有的构造函数的数组,因此需要用一个构造器类型的数组作接收。即,Constructor[] cons = ......
②可使用增强for循环来遍历获得的构造器数组(IDEA快捷:输入iter)
3.Constructor类的常用方法:
getName():
该方法可以查看该构造器位于哪个类,并返回该类的正名,用String类型做接收。
newInstance(Object... initargs):(重点)
说明:
①使用获得的构造器和对应的参数可以"创建"并初始化对象,本质上几乎可以说,获取Constructor对象的根本目的就是去使用newInstance() 方法。
②newInstance方法返回一个Object类型的对象,理应用Object类型作接收,但实际开发中,往往直接利用强制类型转换来向下转型,即直接用所分析类类型的对象来作接收,
举个栗子:Student1 stu = (Student1) constructor2.newInstance(.....);
③至于括号里那一大堆,没必要说是非去弄明白init是初始化,args是arguments的缩写,是参数的意思。现在去扣这些没什么用,搞清楚自己的目的,是先去掌握反射的用法。实际操作中简单的一塌糊涂,就是构造器本身需要什么参数,你就直接传入什么类型的参数就好了,比获取构造器省事得多,回顾一下:(当初获取构造器是传入对应参数的字节码文件对象)。
4.代码演示:(注释给我好好看)
老规矩,我们先在本包下创建一个演示类,模拟我们要分析的类,这里我们创建了一个学生类Student1,如图所示:(后面加1是为了与之前的Student类做区分,之后的代码演示也同理)
看一下学生类Student1的代码:
package knowledge.reflect.constructor;
public class Student1 {
//公共的无参构造
public Student1() { }
//公共的带参构造
public Student1(String name) {
System.out.println("What's 👴's name :"+ name);
}
//私有的带参构造
private Student1(int age) {
System.out.println("What's 👴's age:"+ age);
}
//公共的空参方法
public void show1() {
System.out.println("👴是公有的空参方法!");
}
//公共的带参方法
public void show2(int a) {
System.out.println("👴是公有的带参方法!您输入的值是:"+ a);
}
//私有的带参方法
private int show3(int c, int d) {
System.out.println("👴是私有的带参方法!");
return (2*c + 3*d);
}
}
正片开始,获取构造器对象的三种方式及Constructor类的常用方法,代码演示:
package knowledge.reflect.constructor;
import java.lang.reflect.Constructor;//别忘了导包
/**
* 需求: 通过反射的方式创建: Student1 类型的对象。
* 其最终目的是要调用newInstance方法
*/
public class GetConstructor {
public static void main(String[] args) throws Exception {
//1.首先获取字节码文件对象(注意为了见名知意,这里变量名用了clazz,别看错了)
Class clazz = Class.forName("knowledge.reflect.constructor.Student1");
/*
注意刚刚写到这里已经报了ClassNotFoundException异常,
原因我们在前面获取字节码文件对象那里就已经讲过了,这里不再赘述。
建议直接抛出异常的顶层父类Exception,一招毙命。
*/
//2.通过获取的字节码文件对象来获取构造器对象(三种方式)
//方式一: getConstructor(Class<?>... parameterTypes)
//①获取公有的空参构造(不需要传入参数)
Constructor c1 = clazz.getConstructor();
System.out.println("输出空参构造器c1,注意观察输出构造器的形式:" + c1);
/*
如果前面没有抛出父类Exception异常,
此处就会报NoSuchMethodException异常,
所以还是建议一劳永逸。
*/
//②获取公有的带参构造(传入对应参数的字节码文件对象)
/*
因为我们定义Student1类时,公共的带参构造器所需的参数是String类型,
所以此处需要传入String类型的字节码文件对象,即String.class.(class小写)
*/
Constructor c2 = clazz.getConstructor(String.class);
//又是NoSuchMethodException异常,写到这里你要是还没抛出Exception你可真头铁
System.out.println("输出带参构造器c2,注意观察输出构造器的形式:" + c2);
//方式二: getDeclaredConstructor(Class<?>... parameterTypes)
/*
因为我们定义Student1类时,私有的带参构造器所需的参数是int类型,
所以此处需要传入int类型的字节码文件对象,即int.class.(class小写)
*/
Constructor c3 = clazz.getDeclaredConstructor(int.class);
//NoSuchMethodException异常三杀
System.out.println("输出带参构造器c3,注意观察输出构造器的形式:" + c3);
System.out.println("------------------------------------------");
//方式三: getConstructors() ,获取所有非私有的构造器,用构造器类型的数组来作接收,注意仅限非私有的构造器
Constructor[] cons = clazz.getConstructors();
//使用增强for循环来遍历获得的构造器数组
for (Constructor con : cons) {
System.out.println("遍历构造器数组,注意要和之前的c1,c2作比较:" + con);
}
System.out.println("------------------------------------------");
//3.Constructor类的常用方法:
//①getName():
String c1name = c1.getName();
System.out.println("来看看公有的空参构造是属于哪个类的:" + c1name);
//②newInstance(): 实际开发中,这里往往直接利用强制类型转化向下转型
Student1 stu1 = (Student1) c1.newInstance();
Student1 stu2 = (Student1) c2.newInstance("王五");
/*
1)这里调用了带参构造,别忘了传入参数,此处不需要传入字节码文件对象,直接传入对应参数本身类型即可。
2)如果之前没抛出Exception异常,这里会报一堆异常错误,
怎么说,抛一个要比抛一堆省事儿的多
*/
System.out.println("👴看看用反射创建的第一个对象长什么样子:" + stu1);
System.out.println("👴看看用反射创建的第二个对象长什么样子:" + stu2);
/** 最后打印的地址值代表学生对象 */
}
}
5.输出结果及分析:
I.注意观察前三行输出,看它们开头的修饰符public,public,private,以及结尾的参数列表,发现和Student1类中定义的三个构造器保持一致,即一个公有空参构造,一个公有带参构造,还有一个私有带参构造,我们再来回顾一下之前的Student1类中对构造器的定义,我直接截张图,省着大家来回翻页,如图:
嗯,雀氏一致。接着看呗:
II.遍历获得的构造器数组时 ,也雀氏只输出了全部的公有构造,符合我们的预期!
III.当通过getName方法获取第一个空参构造所在类的名字时,也成功打印!(看结果其实就是类的正名给打印出来了。)
IV.当我们用公有的带参构造来newInstance时,成功输出了该构造器中的输出语句中的内容。
V.最后打印出通过反射创建的对象,由于没有重写toString() 方法,所以默认打印出对象的地址值,可以看到两个对象的地址值并不相同。
这时候,可能有同学要问了,***凭什么没有通过私有构造器来创建对象?
我只能说,你**牛逼,好问题,那我们现在就来试一试:
我们在第63行后面,也就是我们用newInstance创建对象的后面,悄悄加上两条代码:
Student1 stu3 = (Student1) c3.newInstance(19);
System.out.println("👴看看用反射创建的第三个对象长什么样子:" + stu3);
诶巧了,IDEA这时候也不报错,那我们就运行呗。
这时候,一运行你傻眼了,如下图:
你可能会想:靠,***这咋回事儿啊?
别急,注意看, 出现了IllegalAccessException,即非法访问异常。这是因为我们无权过问Student1类的私有构造方法。怎么办?这辈子就栽在这上头了?Of Course Not!
解决方法:开启暴力反射!
你不让我用?我偏要!我就任性!(bushi)
这时我们需要用到setAccessible (boolean flag) 方法(这个方法在讲到获取Field对象时会更详细地讲解),我们这里直接说怎么用:即谁的访问权限受限,谁就调用setAccessible() 方法,传入的boolean值为true。
上代码:
c3.setAccessible(true);
Student1 stu3 = (Student1) c3.newInstance(19);
System.out.println("👴看看用反射创建的第三个对象长什么样子:" + stu3);
没错,我们在创建stu3对象之前,先开启暴力反射,这样一来就能输出了,
输出结果:
Ohhhhhhhhhhhhhhh!成功!
四.获取成员方法(Method)
1.前言:
①其实Constructor那里已经讲过了,我再重申一遍是怕大家忘了,我只把最重要的一条复制过来:不管是通过反射获取类的构造器(Constructor) ,方法(Method) ,还是属性(Field) ,第一步都需要先获取字节码文件对象,然后再通过字节码文件对象来分别获取构造器对象,方法对象,和属性对象。 这叫——万变不离其宗!
②前言真的很重要,特别是对于新人们,前言是我把易犯的错误和需要注意的要点进行的总结,大家千万不要置若罔闻。
2.三种方式获取Method对象:
方式一:
getMethod (String name, Class <?>... parameterTypes):
说明(重要):
①仔细看这三种方式,其实你会发现和Constructor的获取方式真的大同小异,唯一一个需要注意的变化是, 使用方式一和方式二需要先传入一个String类型的变量, 这个String类型的变量指的是所调用方法的名字。没错,就是方法的名字,注意是以字符串的形式传入。然后就和Constructor一样,传入方法形参对应的字节码文件对象就可以了,如果方法的参数列表为空,就只传入方法名。
②该方法会返回一个Method对象,因此需要用Method类型来作接收,注意方式一仅限公共的成员方法。
方式二:
getDeclaredMethod (String name, Class <?>... ):
说明:
同上,但方式二可用于获取私有的成员方法。
方式三:
getMethods():
说明:
还是大同小异,注意要用Method类型的数组作接收,即Method[] methods = .......;
3.Method类的常用方法:
getName () :
该方法可以返回对应函数的方法名,返回值是String类型,需要用String类型的变量作接收。
invoke (Object obj, Object... args) :(invoke本身就是调用的意思)
invoke方法说明(重要):
①该方法较特别的一点在于: 可作接收可不作接收,其取决于所调用函数的返回值类型。若返回值类型是void类型,则不作接收,若返回值类型不是void,则需要作接收。
②该方法默认返回类型是Object,因此,在作接收时,同样可直接向下转型。
③invoke() 方法所需的形参有两类,首先, 需要传入一个你所分析类的对象,这个对象可以由构造器中的方法newlnstance() 来创建,其次, 再直接传入方法所需的形参。如果是无参方法,就只传入所分析类的对象。
eg1:Method method1 = clazz.getMethod("function1"); method1.invoke(student2);
eg2:Method method3 = clazz.getMethod("function3", int.class);
int i = (int) method3.invoke(student2, 11);
④invoke() 复杂的地方在于,它既需要考虑函数的返回值类型(决定是否接收),又需要考虑函数是否带参(决定是否传入形参)。把握好这两点就不易出错了。
getModifiers() :
该方法以int形式返回修饰符[private = 2;默认修饰符 = 0;protected = 4;public = 1;static = 8;final = 16。若出现多个修饰符,返回它们的和]。
getReturnType() :
该方法以Class形式返回方法的返回值类型。
getParameterTypes() :
该方法以Class[]返回参数类型数组。
4.代码演示(不理解多看注释):
老规矩,我们先在method包下创建一个学生类,用来模拟我们要分析的类。
Student2类的代码如下:
package knowledge.reflect.method;
public class Student2 {
//成员变量(private)
private int age;
//(公共的)空参构造和带参构造
public Student2() {}
public Student2(int age) {this.age = age;}
//getXXX和setXXX方法
public void setAge(int age) {this.age = age;}
public int getAge() {return age;}
//公共的空参方法
public void function1() {
System.out.println("👴是公有的空参方法!");
}
//公共的带参方法
public void function2(int a) {
System.out.println("👴是公有的带参方法!您输入的值是:"+ a);
}
//私有的带参方法
private int function3(int c, int d) {
System.out.println("👴是私有的带参方法!");
return c * d;
}
// 重新toString方法,用来打印对象的各个属性值
@Override
public String toString() {
return "Student2{"+
"age='"+ age + '\''
+ '}';
}
}
好的,接下来我们用代码来演示一下获取Method对象的三种方式以及Method类的常用方法。
package knowledge.reflect.method;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;//别忘了导包,虽然IDEA是自动导包的。
/**
*需求: 通过反射获取Student1类中的成员方法并调用
*
* 方法PS:
* public void setAccessible(boolean flag) //是否开启暴力反射(true:开启)
*/
public class GetMethod {
public static void main(String[] args) throws Exception { //老规矩,抛出异常的顶层父类Exception
//1.首先获取Student2类的字节码文件对象。
Class zz = Class.forName("knowledge.reflect.method.Student2");
//2.通过获取的字节码文件对象来获取构造器对象,并通过newInstance()方法创建Student2类的对象。
//注意看两个构造情况下输出结果的不同。
//空参构造
Constructor con = zz.getConstructor();
Student2 st = (Student2) con.newInstance();
System.out.println("看看重写了toString方法后打印出的Student2类对象st长什么样子:" + st);
//带参构造
Constructor con2 = zz.getConstructor(int.class);
Student2 st2 = (Student2) con2.newInstance(19);
System.out.println("看看重写了toString方法后打印出的Student2类对象st2长什么样子:" + st2);
System.out.println("``````````````````````");
/*
①注意我们输出利用空参构造创建的Student2对象st时,age = 0,
这是因为我们没有给age赋初值,而int类型的默认值就是0
②而当我们输出利用带参构造创建的Student2对象st2时,age = 19,
这是因为我们在带参构造中传入了参数19,给age赋了初值为19。
*/
//3.通过获取的字节码文件对象来获取成员方法对象(三种方式),并通过invoke()调用此方法
//方式一:getMethod (String name, Class <?>... parameterTypes):
//调用公共的空参方法
Method method1 = zz.getMethod("function1");
/*
因为调用的是公有的无参方法,所以此处只需传入字符串形式的方法名
*/
System.out.println("打印一下方法对象method1:"+ method1);
System.out.println("只打印 method1对象的方法名:"+ method1.getName());
System.out.println("``````````````````````");
/**调用invoke() 方法时,接不接收取决于函数的返回值类型!!!*/
method1.invoke(st);
/*
因为method1是公有的空参方法对象,而Student2类中,公有的空参方法
function1() 返回值类型是void,因此不需要作接收。
又因为是空参方法,所以也不需要传入参数,只需传入一个Student2类的对象就可以了
所以此处直接调用invoke() 方法,且只传入了Student2类的对象,
而没有作接收。
*/
System.out.println("``````````````````````");
//调用公共的带参方法
Method method2 = zz.getMethod("function2", int.class);
/*
因为调用的是公有的带参方法,所以此处除了要传入字符串形式的方法名,
还需要传入该带参方法的形参对应的字节码文件对象。
*/
method2.invoke(st, 5);
/*
因为method2是公有的带参方法对象,而Student2类中,公有的带参方法
function2() 返回值类型是void,因此也不需要作接收。
又因为是带参方法,所以不仅需要传入一个Student2类型的对象,还需传入对应的参数。
所以此处直接调用invoke() 方法,且同时传入了Student2类型的对象和该方法对应的参数,
而没有作接收。
*/
System.out.println("``````````````````````");
//方式二:getDeclaredMethod (String name, Class <?> ...):
//调用私有的带参方法
Method method3 = zz.getDeclaredMethod("function3", int.class, int.class);
/** 此处私有不让调用,开启暴力反射!! */
method3.setAccessible(true);
int i = (int) method3.invoke(st, 3,9);
System.out.println("您输入的两个数的乘积为:"+ i);
System.out.println("``````````````````````");
//方式三:获取Student1类的所有的成员方法(不含私有),用Method[] 作接收。
Method[] methods = zz.getMethods();
for (Method md : methods) {
System.out.println(md);
}
System.out.println("``````````````````````");
/*
因为Student2类有继承自Object类的方法,因此这里遍历的方法会非常多。巨多。
*/
//4.试着来获取Student2类中的setAge()方法,来给Student对象设置值。
Method method4 = zz.getMethod("setAge", int.class);
method4.invoke(st, 20);
System.out.println("通过setAge设置值后,打印出st对象,注意与之前的st对象作比较:" + st);
}
}
/*
* 总结:
* invoke() 复杂的地方在于,它既需要考虑函数的返回值类型(决定是否接收),
* 有需要考虑函数是否带参(决定是否传入形参)。把握好这两点就不易出错了。
* */
5.输出结果及分析:
输出结果:(太长了只能分两块)
分析:
I.首先我们看到,当我们用空参构造创建学生对象时,输出的学生对象的属性值age默认为0. 而当我们用带参构造来创建学生对象时,输出的学生对象的属性值age是我们传入的形参值19.
II.其次我们看到,打印出的方法对象,前缀修饰符+ 返回值类型,后缀方法名。不禁使我们想起打印出的构造器对象,当然构造器对象是肯定没有返回值类型的。
III. 然后分别是公有空参方法method1.invoke(),公有带参方法method2.invoke() 和私有带参方法method3.invoke()的实现。
IV. 由于Student2类中有继承自Object类的非私有方法,所以遍历Method数组时,你会看到好多好多的方法,巨多。
V.通过setAge修改了之前 通过空参构造创建的对象st的属性值, 成功!
五.获取成员变量(Field)
1.前言:
你能挺到这里,可见你的头铁(优秀),其实把Constructor解决后,后头这俩都是小菜。
2.Field对象:
Field对象又称域对象,指类的属性(成员变量)。
3.三种方式获取Field对象:
方式一:
getField (String name):
说明:
①该方法返回一个Field对象,用Field类型作接收。
②该方法仅需传入一个字符串类型的变量,即你要修改的属性的名字(属性名)
③仅限于公共属性
方式二:
getDeclaredField (String name):
说明:
①可用于获取私有属性。
方式三:
getDeclaredFields ()
说明:
①该方法可以返回此类所有(包含私有) 属性的数组。用Field类型的数组作接收,即Field[] fields = .........;
4.Field类的常用方法:
set (Object obj, Object value):
说明:
①该方法返回值为void类型,即不需要作接收。
②该方法可以设置obj对象的指定属性值为value。obj对象即是你用构造器中newInstance() 方法所创建的对象,指定属性即为创建field对象时传入属性名的那个属性。
setAccessible (boolean flag):
说明:
①是不是很眼熟? 没错,这就是访问私有构造器,私有方法,私有成员前必须进行的工作:开启暴力反射(设置为true)。
②该方法可以将Field对象对应的属性的可访问性设置为指定布尔值,即一个属性能不能访问你来说了算!
getModifiers() :
说明:
①该方法以int形式返回修饰符[private = 2;默认修饰符 = 0;protected = 4;public = 1;static = 8;final = 16。若出现多个修饰符,返回它们的和]。
getType() :
说明:
①该方法返回属性对应的类型的Class对象。
5.代码演示:
老规矩,我们先在field包下创建一个演示类Student3,用来模拟我们要分析的类。
Student3学生类代码如下:
package knowledge.reflect.Field;
public class Student3 {
//公共的属性
public String name;
//私有的属性
private int age;
//重写toString方法,用来打印学生对象属性值(可用快捷键Alt + Insert,选择toString())
@Override
public String toString() {
return "Student3{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
正片开始,GetField代码演示:
package knowledge.reflect.Field;
import java.lang.reflect.Field; //别忘了导包
import java.lang.reflect.Constructor;
/**
* 需求: 通过反射获取成员变量并使用
*/
public class GetField {
public static void main(String[] args) throws Exception{
//1.首先获取Student3类的字节码文件对象
Class cs = Class.forName("knowledge.reflect.Field.Student3");
//2.通过获取的字节码文件对象来获取构造器对象。
Constructor con = cs.getConstructor();
Student3 student1 = (Student3) con.newInstance();
/*
当你把前两步用习惯之后,可以直接把两步合二为一,达到代码优化的效果
这也叫做链式编程,如下代码第20行:
*/
Student3 student2 = (Student3) cs.getConstructor().newInstance();
//3.获取Field对象的三种方式:
//方式一: getField (String name):
/*
需要传入属性名,String类型
*/
Field field1 = cs.getField("name");
System.out.println("给👴看看name属性的样子:" + field1);
//方式二: getDeclaredField (String name):
Field field2 = cs.getDeclaredField("age");
System.out.println("给👴看看age属性的样子:" + field2);
System.out.println("----------------------------");
//方式三: getDeclaredFields ()
/*
以Field类型的数组作接收,可使用增强for或者迭代器来遍历。
*/
Field[] fields = cs.getDeclaredFields();
for (Field field : fields) {
System.out.println(field);
}
//4.Field类的常用方法:
//set (Object obj, Object value):
/*
在调用set() 方法 修改Student3类的属性之前
我们先输出一下学生对象student2,
来看看属性的初始值是多少。
*/
System.out.println("----------------------------");
System.out.println("看看默认的属性值是多少:" + student2);
/*
结果显示为name='null', age=0
然后我们来修改属性值
*/
field1.set(student2, "Cyan");
/*先修改一下公有属性name看看效果如何*/
System.out.println("修改name后学生类的属性值为:" + student2);
/*再来修改一下私有属性age看看效果如何*/
/*
假设修改私有属性前,没有开启暴力反射,又会报IllegalAccessException错误
*/
field2.setAccessible(true);//开启暴力反射。
field2.set(student2, 19);
System.out.println("修改age后学生类的属性值为:" + student2);
}
}
6.输出结果及分析:
输出结果:
分析:
I.来看属性的输出结果: 前面是修饰符,name属性是紧接着String类的正名,而int是基本数据类型,最后是它们各自的变量名。而且它们都是Student3这个类的属性。
II.通过结果可以看到我们成功用Field类的set方法修改了Student3类的属性值。
六.完结撒花❀❀❀❀❀
祝贺你成功学完了反射(Class-Constructor-Method-Field一条路),非常感谢你能看到这里,创作不易,所有代码基本都做了非常详细的注释,觉得不错就给up点个赞吧。
(-----------2023.5.2补充——“反射专题到此结束,下一专题是多线程专题,也是java基础的最后一个专题-----------”)。感谢阅读!
System.out.println("END---------------------------------------------------------------------------");