目录
前言
一.反射及其相关概念
1.什么是反射?
2.反射的用途:
①分析类:
②查看并使用对象:
3.反射的应用场景:
4.类加载器:
类的加载时机:
类的加载阶段:
5.Class对象:
联系:
二.获取Class对象的三种方法:(Three ways to get a Class object)
1.通过Object类的getClass() 方法:
2.通过类的静态属性:
3.通过Class类的静态方法:
4.代码演示:(注意注释)
三.获取构造器(Constructor)
1.前言(重要):
2.三种方式获取构造器对象:
方式一:
方式二:
方式三:
3.Constructor类的常用方法:
getName():
newInstance(Object... initargs):(重点)
4.代码演示:(注释给我好好看)
5.输出结果及分析:
四.获取成员方法(Method)
1.前言:
2.三种方式获取Method对象:
方式一:
方式二:
方式三:
3.Method类的常用方法:
getName () :
invoke (Object obj, Object... args) :(invoke本身就是调用的意思)
getModifiers() :
getReturnType() :
getParameterTypes() :
4.代码演示(不理解多看注释):
5.输出结果及分析:
五.获取成员变量(Field)
1.前言:
2.Field对象:
3.三种方式获取Field对象:
方式一:
方式二:
方式三:
4.Field类的常用方法:
set (Object obj, Object value):
setAccessible (boolean flag):
getModifiers() :
getType() :
5.代码演示:
6.输出结果及分析:
六.完结撒花❀❀❀❀❀
大家好,今天给大家带来一篇详解JAVA反射基础的博文(将来会出JAVA反射进阶的讲解,准备放在JAVA进阶专栏),我会从字节码文件对象,构造器对象,方法对象,属性对象一一进行代码演示,由于Constructor,Method,和Field三者的用法大同小异,于是up决定重点把Constructor构造器这块儿讲得通透点,注意:注释内容也是重点,(注释内容也是重点,注释内容也是重点,)有利于大家举一反三。不要眼高手低,跟着up一块儿练习,看完就会用反射。学习反射基础需要有一定的javaSE基础,尤其是file类和Exception类。我之后会陆续出一系列专门讲javaSE基础的文章,把javaSE基础篇全讲一遍。所有文章都会适时补充完善,良工不示人以朴。PS:使用IEDA讲解,点击目录可以跳转。(前言编辑于2022.10.12)
反射指的是在程序运行的过程中分析类的一种能力。你可以这么理解,在程序运行过程中,我可以查看并使用其他类的构造器,方法和属性,这就是反射的能力。反射可以在不修改源码的情况下,通过外部配置文件来控制程序,符合设计的ocp原则(开闭原则:不修改源码,扩充功能)。
光这么说还是过于抽象,我们直接上图片:
如图,我们可以得知反射的第一步是获取字节码文件对象 。
加载并初始化一个类(反射创建类的字节码文件对象时会加载类)
查看类的所有属性和方法(即Constructor, Method, Field,etc)
查看一个对象的全部属性和方法
使用对象的任意属性和方法
构建通用的工具
搭建具有高度灵活性和扩展性的系统框架(框架底层的核心就是——反射可以动态的构建和使用对象)。
类加载器,(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源代码,初始化阶段其实就是执行
Class对象,即java.lang.Class类的对象,也叫字节码文件对象。每个字节码文件对应一个Class对象。
①如果.java的源文件中只有一个类,那么一个.java的源文件对应一个.class的字节码文件,对应一个Class对象。
②如果.java的源文件中不止一个类,那么编译后,每一个类都对应一个字节码文件。
Class c = 对象名.getClass();
该方法需要用Class类型(首字母大写) 的引用变量来作接收,即得到一个Class对象。
注意:getClass方法要用对象名. 的形式来调用,所以一定要先创建一个对象。(得到运行类型其实就是得到实例对应的Class对象)
Class c2 = 类名.class; //eg : Integer.class
该方式多用于参数的传递,例如获取构造器对象时传入“参数.class”。
Class c3 = Class.forName("类的正名"); //类的正名 = 包名 + 类名。中间用点'.'来连接
该方式多用于从配置文件读取类的全类名(正名),继而加载类。(多用于底层框架)
我们先在包下新建一个类,创建一个标准的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) ,方法(Method) ,还是属性(Field) , 第一步都需要先获取字节码文件对象,然后再通过字节码文件对象来分别获取构造器对象,方法对象,和属性对象。
②Constructor,Method,和FIeld都属于java.base模块下的 java.lang.reflect包,来张示意图就是这样:
③建议在用反射分析类之前,先导包!其目的是一劳永逸,以绝后患。 例如,要获取构造器对象Constructor时,就先导包:import java.lang.reflect.Constructor;
④遇到异常没必要见一个抛一个,建议直接抛出它们的父类Exception类,省时省力。 如图所示:
⑤前言看不懂没关系,之后代码演示会更直观。
getConstructor(Class>... parameterTypes) :
说明(重要):
①该方法返回一个Constructor类型的对象,因此需要用Constructor类型来做接收,该方法仅限于获取公共的构造函数。
②parameterTypes是参数类型的意思,
③该方法 需要传入你要获取的构造器参数的字节码文件对象,大白话就是说,假如我要获取的构造器是个无参构造,那我就不需要传入任何字节码文件对象了,假如我要获取的构造器是个带参构造,那带什么参数,我用这方法时就传入什么参数对应的字节码文件对象,说这么复杂,其实实操时很简单,直接参数.class就搞定了,构造器本身有几个参数就传入几个参数.class。
④ ?表示通配符,代表不确定的任意类型。
⑤假如本该传入对应参数的字节码文件对象,你没有传入,默认输出的是所分析类的公有空参构造。
getDeclaredConstructor(Class>... parameterTypes) :
说明:
用法和方式一没什么区别,唯一一点不同就是方式二是用来获取私有构造器的。
getConstructors() :
说明:
①该方法返回目标类所有非私有的构造函数的数组,因此需要用一个构造器类型的数组作接收。即,Constructor[] cons = ......
②可使用增强for循环来遍历获得的构造器数组(IDEA快捷:输入iter)
该方法可以查看该构造器位于哪个类,并返回该类的正名,用String类型做接收。
说明:
①使用获得的构造器和对应的参数可以"创建"并初始化对象,本质上几乎可以说,获取Constructor对象的根本目的就是去使用newInstance() 方法。
②newInstance方法返回一个Object类型的对象,理应用Object类型作接收,但实际开发中,往往直接利用强制类型转换来向下转型,即直接用所分析类类型的对象来作接收,
举个栗子:Student1 stu = (Student1) constructor2.newInstance(.....);
③至于括号里那一大堆,没必要说是非去弄明白init是初始化,args是arguments的缩写,是参数的意思。现在去扣这些没什么用,搞清楚自己的目的,是先去掌握反射的用法。实际操作中简单的一塌糊涂,就是构造器本身需要什么参数,你就直接传入什么类型的参数就好了,比获取构造器省事得多,回顾一下:(当初获取构造器是传入对应参数的字节码文件对象)。
老规矩,我们先在本包下创建一个演示类,模拟我们要分析的类,这里我们创建了一个学生类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);
/** 最后打印的地址值代表学生对象 */
}
}
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!成功!
①其实Constructor那里已经讲过了,我再重申一遍是怕大家忘了,我只把最重要的一条复制过来:不管是通过反射获取类的构造器(Constructor) ,方法(Method) ,还是属性(Field) ,第一步都需要先获取字节码文件对象,然后再通过字节码文件对象来分别获取构造器对象,方法对象,和属性对象。 这叫——万变不离其宗!
②前言真的很重要,特别是对于新人们,前言是我把易犯的错误和需要注意的要点进行的总结,大家千万不要置若罔闻。
getMethod (String name, Class >... parameterTypes):
说明(重要):
①仔细看这三种方式,其实你会发现和Constructor的获取方式真的大同小异,唯一一个需要注意的变化是, 使用方式一和方式二需要先传入一个String类型的变量, 这个String类型的变量指的是所调用方法的名字。没错,就是方法的名字,注意是以字符串的形式传入。然后就和Constructor一样,传入方法形参对应的字节码文件对象就可以了,如果方法的参数列表为空,就只传入方法名。
②该方法会返回一个Method对象,因此需要用Method类型来作接收,注意方式一仅限公共的成员方法。
getDeclaredMethod (String name, Class >... ):
说明:
同上,但方式二可用于获取私有的成员方法。
getMethods():
说明:
还是大同小异,注意要用Method类型的数组作接收,即Method[] methods = .......;
该方法可以返回对应函数的方法名,返回值是String类型,需要用String类型的变量作接收。
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() 复杂的地方在于,它既需要考虑函数的返回值类型(决定是否接收),又需要考虑函数是否带参(决定是否传入形参)。把握好这两点就不易出错了。
该方法以int形式返回修饰符[private = 2;默认修饰符 = 0;protected = 4;public = 1;static = 8;final = 16。若出现多个修饰符,返回它们的和]。
该方法以Class形式返回方法的返回值类型。
该方法以Class[]返回参数类型数组。
老规矩,我们先在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() 复杂的地方在于,它既需要考虑函数的返回值类型(决定是否接收),
* 有需要考虑函数是否带参(决定是否传入形参)。把握好这两点就不易出错了。
* */
输出结果:(太长了只能分两块)
分析:
I.首先我们看到,当我们用空参构造创建学生对象时,输出的学生对象的属性值age默认为0. 而当我们用带参构造来创建学生对象时,输出的学生对象的属性值age是我们传入的形参值19.
II.其次我们看到,打印出的方法对象,前缀修饰符+ 返回值类型,后缀方法名。不禁使我们想起打印出的构造器对象,当然构造器对象是肯定没有返回值类型的。
III. 然后分别是公有空参方法method1.invoke(),公有带参方法method2.invoke() 和私有带参方法method3.invoke()的实现。
IV. 由于Student2类中有继承自Object类的非私有方法,所以遍历Method数组时,你会看到好多好多的方法,巨多。
V.通过setAge修改了之前 通过空参构造创建的对象st的属性值, 成功!
你能挺到这里,可见你的头铁(优秀),其实把Constructor解决后,后头这俩都是小菜。
Field对象又称域对象,指类的属性(成员变量)。
getField (String name):
说明:
①该方法返回一个Field对象,用Field类型作接收。
②该方法仅需传入一个字符串类型的变量,即你要修改的属性的名字(属性名)
③仅限于公共属性
getDeclaredField (String name):
说明:
①可用于获取私有属性。
getDeclaredFields ()
说明:
①该方法可以返回此类所有(包含私有) 属性的数组。用Field类型的数组作接收,即Field[] fields = .........;
说明:
①该方法返回值为void类型,即不需要作接收。
②该方法可以设置obj对象的指定属性值为value。obj对象即是你用构造器中newInstance() 方法所创建的对象,指定属性即为创建field对象时传入属性名的那个属性。
说明:
①是不是很眼熟? 没错,这就是访问私有构造器,私有方法,私有成员前必须进行的工作:开启暴力反射(设置为true)。
②该方法可以将Field对象对应的属性的可访问性设置为指定布尔值,即一个属性能不能访问你来说了算!
说明:
①该方法以int形式返回修饰符[private = 2;默认修饰符 = 0;protected = 4;public = 1;static = 8;final = 16。若出现多个修饰符,返回它们的和]。
说明:
①该方法返回属性对应的类型的Class对象。
老规矩,我们先在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);
}
}
输出结果:
分析:
I.来看属性的输出结果: 前面是修饰符,name属性是紧接着String类的正名,而int是基本数据类型,最后是它们各自的变量名。而且它们都是Student3这个类的属性。
II.通过结果可以看到我们成功用Field类的set方法修改了Student3类的属性值。
祝贺你成功学完了反射(Class-Constructor-Method-Field一条路),非常感谢你能看到这里,创作不易,所有代码基本都做了非常详细的注释,觉得不错就给up点个赞吧。
(-----------2023.5.2补充——“反射专题到此结束,下一专题是多线程专题,也是java基础的最后一个专题-----------”)。感谢阅读!
System.out.println("END---------------------------------------------------------------------------");