作为Java开发者,你认为反射这个知识点重要程度,在你心里是什么样的呢? 以前我也只觉得反射非常重要,但总归是听这个文章说,听那个朋友说,学是学了,但却没怎么应用。 当我正式进入到社会当 cv 仔的时候,需要考虑的问题多了,慢慢思考问题了,就觉得反射是个力大无穷的东西,更会感觉反射是个无所不能的东西,如各种各样的框架的底层,各种各样的拦截器的实现,反射都是其中少不了的一部分~ 如果平时着重于开发业务的话,那么确实会较少使用到反射机制,但并非是说反射它不重要,反射它可是搭建脚手架的基础的基础勒~
文章大致思路:
全文共 7500 字左右,案例均可运行,阅读时间大约需要20分钟左右,如有问题,请留言或发送邮件(nzc_wyh@163.com)。
编写的过程中,即使书写完已阅读过,但难免可能会出现遗漏,如有发现问题请及时联系修正,非常感谢你的阅读,希望我们都能成为技术道路上的朋友。
一、反射是什么?
JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
不过要想解剖一个类,就要先获取到该类的字节码文件对应的Class类型的对象.
稍后就会讲到~
“反射之所以被称为框架的灵魂”,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力,这种能力也就是我们常说的动态性,利用这种性质可以使编写的程序更灵活更通用。
反射机制可以用来:
- 在运行时分析类的能力,如可以构造任意一个类,可以获取任意一个类的全部信息,
- 在运行时检查对象,如在运行时判断任意一个对象所属的类
- 实现泛型数组操作代码,因为在运行时可以获取泛型信息
- 利用Method对象,如我们经常使用的动态代理,就是使用
Method.invoke()
来实现方法的调用。
反射是一种功能强大且复杂的机制,在开发Java工具或框架方面,反射更是不可缺少的一部分。
二、Class 对象详解
之前说到了,如果要分析一个类,就必须要获取到该类的字节码文件对应 Class 类型对象。
另外如果有听到类模板对象
,这个说的其实就是Class对象
,大家不要误会了。
2.1、如何获取到Class对象呢?
得到Class的方式总共有四种:
- 通过对象调用
getClass()
方法来获取 - 直接通过
类名.class
的方式得到 - 通过
Class
对象的forName()
静态方法来获取 Classloader
,通过类加载器进行获取
/**
* @description:
* @author: Ning Zaichun
* @date: 2022年09月15日 22:49
*/
public class ClassDemo01 {
@Test
public void test1() throws Exception {
//1、通过对象调用 getClass() 方法来获取
// 类型的对象,而我不知道你具体是什么类,用这种方法
Student student = new Student();
Class studentClass1 = student.getClass();
System.out.println(studentClass1);
// out: class com.nzc.Student
//2、直接通过`类名.class` 的方式得到
// 任何一个类都有一个隐含的静态成员变量 class
// Class studentClass2 = Student.class;
Class<?> studentClass2 = Student.class;
System.out.println(studentClass2);
// out: class com.nzc.Student
System.out.println("studentClass1和studentClass2 是否相等==>" (studentClass1 == studentClass2));
// studentClass1和studentClass2 是否相等==>true
//3、通过 Class 对象的 forName() 静态方法来获取,使用的最多
// 但需要抛出或捕获 ClassNotFoundException 异常
Class<?> studentClass3 = Class.forName("com.nzc.Student");
System.out.println(studentClass3);
// out: class com.nzc.Student
System.out.println("studentClass1和studentClass3 是否相等==>" (studentClass1 == studentClass3));
//studentClass1和studentClass3 是否相等==>true
//4、 使用类的加载器:ClassLoader 来获取Class对象
ClassLoader classLoader = ClassDemo01.class.getClassLoader();
Class studentClass4 = classLoader.loadClass("com.nzc.Student");
System.out.println(studentClass4);
System.out.println("studentClass1和studentClass4 是否相等==>" (studentClass1 == studentClass4));
//studentClass1和studentClass4 是否相等==>true
}
}
在这四种方式中,最常使用的是第三种方式,第一种都直接new对象啦,完全没有必要再使用反射了;第二种方式也已经明确了类的名称,相当于已经固定下来,失去了一种动态选择加载类的效果;而第三种方式,只要传入一个字符串,这个字符串可以是自己传入的,也可以是写在配置文件中的。
像在学 JDBC 连接的时候,大家肯定都使用过 Class
对象的 forName()
静态方法来获取Class对象,再加载数据库连接对象,但可能那时候只是匆匆而过罢了。
注意:不知道大家有没有观察,我把各种方式所获取到的class
对象,都进行了一番比较,并且结果都为true
,这是因为一个类在 JVM 中只会有一个 Class 实例,为了解释此点,我把类加载过程也简单的做了一个陈述。
2.2、类的加载过程
当我们需要使用某个类时,如果该类还未被加载到内存中,则会经历下面的过程对类进行初始化。
即类的加载 —> 链接 —> 初始化三个阶段。
在这里我只着眼于类的加载过程了,想要了解更为详细的,就需要大家去找找资料看看啦~
加载过程:
1、在我们进行编译后(javac.exe命令),会生成一个或多个字节码文件(就是项目中编译完会出现的 target 目录下的以.class结尾的文件)
2、接着当我们使用 java.exe 命令对某个字节码文件进行解释运行时。
3、加载过程
- 就相当于将 class 文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构;
- 并生成一个代表这个类的
java.lang.Class
对象,这个加载到内存中的类,我们称为运行时类,此运行时类,就是Class
的一个实例,所有需要访问和使用类数据只能通过这个Class
对象。 - 所谓
Class
对象,也称为类模板对象,其实就是 Java 类在 JVM 内存中的一个快照,JVM 将从字节码文件中解析出的常量池、 类字段、类方法等信息存储到模板中,这样 JVM 在运行期便能通过类模板而获 取 Java 类中的任意信息,能够对 Java 类的成员变量进行遍历,也能进行 Java 方法的调用。 - 反射的机制即基于这一基础。如果 JVM 没有将 Java 类的声明信息存储起来,则 JVM 在运行期也无法进行反射。
4、这个加载的过程还需要类加载器参与,关于类加载器的类型大家可以去了解了解,还有双亲委派机制等,此处我便不再多言
2.3、为了更便于记忆的图
(图片说明:为更好的描述JVM中只有一个Class对象,而画下此图,希望通过这张简图,让你记忆更为深刻)
2.4、Class 常用的API
通过 Class 类获取成员变量、成员方法、接口、超类、构造方法等
getName()
:获得类的完整名字。getFields()
:获得类的public
类型的属性。getDeclaredFields()
:获得类的所有属性。包括private
声明的和继承类getMethods()
:获得类的public
类型的方法。getDeclaredMethods()
:获得类的所有方法。包括private
声明的和继承类getMethod(String name, Class[] parameterTypes)
:获得类的特定方法,name
参数指定方法的名字,parameterTypes
参数指定方法的参数类型。getConstructors()
:获得类的public
类型的构造方法。getConstructor(Class[] parameterTypes)
:获得类的特定构造方法,parameterTypes
参数指定构造方法的参数类型。newInstance()
:通过类的不带参数的构造方法创建这个类的一个对象。
另外就还有反射包下的几个常用的对象Constructor、Filed、Method
等,分别表示类的构造器、字段属性、方法等等
这些都会在下文慢慢陈述出来~
三、获取运行时类完整信息并使用
所谓运行时类,就是程序运行时所创建出来的类,你直接理解为通过反射获取到的类也可。大体意思是如此。
在讲述这一小节时,先要理解Java中一切皆对象这句话。
我们平常编写一个类,我们会将它称为一个Java对象,但是在反射这里将此概念再次向上抽象了。
类的基本信息:构造方法、成员变量,方法,类上的注解,方法注解,成员变量注解等等,这些都是Java对象,也是证明了Java中一切皆对象
这句话。
其中Constructor
就表示构造方法的对象,他包含了构造方法的一切信息,
Field、Method
等等都是如此。
不要太过于麻烦和重复书写,我将案例中操作的所有相关代码,都放在此处了,案例中的依赖全部基于此。
代码语言:javascript复制 public interface TestService {
}
代码语言:javascript复制 @Target({ElementType.TYPE,ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MarkAnnotation {
String value() default "";
}
代码语言:javascript复制 public class Generic<T> {
}
代码语言:javascript复制 public interface GenericInterface<T> {
}
代码语言:javascript复制 // TYPE 表示可以标记在类上
// PARAMETER 表示可以标记在方法形式参数
// METHOD 方法
// FIELD 成员属性上
@Target({ElementType.TYPE,ElementType.METHOD, ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME) // 这里表明是运行时注解
@Documented
public @interface LikeAnnotation {
String value() default "";
String[] params() default {};
}
代码语言:javascript复制 @Data
@MarkAnnotation
public class Person implements TestService{
private String sex;
public Integer age;
private void mySex(String sex){
System.out.println("我的性别" sex);
}
public void myAge(Integer age){
System.out.println("我的年龄" age);
}
}
代码语言:javascript复制 @ToString(callSuper = true) // 增加这行是为了打印时将父类属性也打印出来,方便查看~
@Data
@LikeAnnotation(value = "123")
public class Student extends Person implements Serializable {
@LikeAnnotation
private String username;
@LikeAnnotation
public String school;
private Double score;
public Student() {
}
private Student(String username) {
this.username = username;
}
public Student(String username, String school) {
this.username = username;
this.school = school;
}
@LikeAnnotation
public void hello() {
System.out.println("世界,你好");
}
public void say( String username) {
System.out.println("你好,我叫" username);
}
private void myScore(Double score) {
System.out.println("我的分数是一个私密东西," score);
}
public void annotationTest(@LikeAnnotation String username,@MarkAnnotation String str){
System.out.println( "测试获取方法参数中的注解信息");
}
}
3.1、反射获取运行时类构造方法并使用
class获取构造方法的相关API
// 获取所有的构造函数
Constructor<?>[] getConstructors()
// 获取 public或 private 修饰的狗赞函数,只要参数匹配即可
Constructor<?>[] getDeclaredConstructors()
// 获取所有 public 修饰的 构造函数
Constructor<T> getConstructor(Class<?>... parameterTypes)
//调用此方法,创建对应的运行时类的对象。
public T newInstance(Object ... initargs)
newInstance()
:调用此方法,创建对应的运行时类的对象。内部调用了运行时类的空参的构造器。
要想此方法正常的创建运行时类的对象,要求:
- 运行时类必须提供空参的构造器;
- 空参的构造器的访问权限得够。通常,设置为 public。
为什么要 javabean 中要求提供一个 public 的空参构造器?
原因: 1、便于通过反射,创建运行时类的对象;2、便于子类继承此运行时类时,默认调用 super() 时,保证父类有此构 造器。
想要更详细的了解,建议去看生成的字节码文件,在那里能够给出你答案。
测试:
代码语言:javascript复制 /**
* @description:
* @author: Ning Zaichun
* @date: 2022年09月17日 1:17
*/
public class ConstructorTest {
/**
* 获取公有、私有的构造方法 并调用创建对象
*/
@Test
public void test1() throws Exception {
Class<?> aClass = Class.forName("com.nzc.Student");
System.out.println("======获取全部public 修饰的构造方法=========");
Constructor<?>[] constructors = aClass.getConstructors();
for (Constructor<?> constructor : constructors) {
System.out.println(constructor);
}
System.out.println("======获取 public、private 修饰的构造方法,只要参数匹配即可=========");
/**
* 这里的参数 Class<?>... parameterTypes 填写的是参数的类型,而并非某个准确的值信息
*/
Constructor<?> constructor = aClass.getDeclaredConstructor(String.class);
System.out.println(constructor);
// 因为此构造函数是 private 修饰,如果不设置暴力反射,则没有权限访问。
// 这里setAccessible(true) 是设置暴力反射,如果不设置,则会报错,
constructor.setAccessible(true);
// 这里调用的有参构造,所以在调用 newInstance 方法时,也需要填写参数
Object o1 = constructor.newInstance("宁在春");
constructor.setAccessible(false);
System.out.println("o1===>" o1);
/**
* 如果需要获取有参构造,只需要填写对应的参数类型即可,
* 获取无参构造,填null或不填都可。
*/
Constructor<?> constructor1 = aClass.getConstructor();
Constructor<?> constructor2 = aClass.getConstructor(String.class,String.class);
System.out.println("无参构造==>" constructor1);
System.out.println("有参构造==>" constructor2);
Object o2 = constructor1.newInstance();
Object o3 = constructor2.newInstance("宁在春2","xxxx社会");
System.out.println("o2===>" o2);
System.out.println("o3===>" o3);
}
}
既然能够获取到构造方法,那么也就是可以使用的,用Constructor.newInstance()
方法来调用构造方法即可,在下列的打印信息中,也可以看出来确实如此,如果明确要获取为Student
对象的话,进行强转即可。
======获取全部public 修饰的构造方法=========
public com.nzc.Student(java.lang.String,java.lang.String)
public com.nzc.Student()
======获取 public、private 修饰的构造方法,只要参数匹配即可=========
private com.nzc.Student(java.lang.String)
o1===>Student(username=宁在春, school=null, age=null)
无参构造==>public com.nzc.Student()
有参构造==>public com.nzc.Student(java.lang.String,java.lang.String)
o2===>Student(username=null, school=null, age=null)
o3===>Student(username=宁在春2, school=xxxx社会, age=null)
3.2、反射获取运行时类成员变量信息
class
对象中获取类成员信息使用到的API
Field[] getFields();
Field[] getDeclaredFields();
Field getDeclaredField(String name);
public native Class<? super T> getSuperclass();
这里的 Field
类对象,其实就是表示类对象中的成员属性,不过还有多了很多其他在反射时需要用到的属性和API罢了~
3.2.1、获取私有公有类成员信息
为了有更好的对比,我先编写了一段不使用反射时的正常操作。
代码语言:javascript复制 /**
* 不使用反射的进行操作
*/
@Test
public void test1() {
Student student = new Student();
student.setUsername("username");
student.setSchool("xxxx社会");
student.setScore(100.0);
// 永远三岁的小伙子 哈哈