注解与反射
后面要学习的SSM、SpringBoot等框架的底层实现机制都是注解与反射。注解与反射是是以后学好框架的基础,十分重要。
1、注解(Annotation)
1.1、什么是注解
- 注解是从JDK5.0开始引入的新技术(2022年JDK已经更新到18了)。
- 与注释(Comment)类似,注解不是程序本身,但注解(Annotation)可以对程序作出解释,可以被其它程序(如:编译器等)通过反射的方式读取。
- 注解必须按照它定义时规定的格式使用,否则会报错。
(1)注解的格式:
格式一:
1 | @注解名 |
格式二:
1 | @注解名(参数值) |
注解以@注解名
的形式在代码中存在,有时也需要添加一些参数值。如:@SuppressWarnings(value="unchecked")
(2)注解的用处
注解可以附加在package(包)、class(类)、method(方法)、field(属性)等前面,相当于给它们添加了额外的辅助信息,我们可以通过反射机制编程实现对这些元数据的访问。
1.2、内置注解
Java语言内部定义了许多内置注解。这里仅介绍3个常用的内置注解:
- @Override【重写】:定义在
java.lang.Override
中,只适用于修饰方法,表示一个方法将重写父类中的另一个方法。 - @Deprecated【废弃】:定义在
java.lang.Deprecated
中,可以用于修饰方法、属性和类,表示不鼓励程序员使用这样的元素(但仍然是可以使用的),通常是因为它很危险或者存在更好的选择。 - @SuppressWarings【镇压警告】:定义在
java.lang.SuppressWarings
中,用来抑制编译时的警告信息。与前面两个注解有所不同,我们需要添加一个参数才能正确使用该注解,这些参数在源码中已经被定义好了,我们只需要选择性的使用即可。如:@SuppressWarings("all")
@SuppressWarings("unchecked")
@SuppressWarings(value={"unchecked","deprecation"})
- ……
1.3、元注解(meta-annotation)
元注解,即:解释其它注解的注解。
- Java语言在
java.lang.annotation
中定义了4个标准的元注解(meta-annotation)类型。
(1)@Target
- 用于描述注解的使用范围,即:被描述的注解可以用在什么地方。注解的使用范围取决于
@Target
的属性ElementType
的取值。
取值 | 使用范围 |
---|---|
@Target(ElementType.TYPE) | 接口、类、枚举、注解 |
@Target(ElementType.FIELD) | 字段、枚举的常量 |
@Target(ElementType.METHOD) | 方法 |
@Target(ElementType.PARAMETER) | 参数 |
@Target(ElementType.CONSTRUCTOR) | 构造函数 |
@Target(ElementType.LOCAL_VARIABLE) | 局部变量 |
@Target(ElementType.ANNOTATION_TYPE) | 注解 |
@Target(ElementType.PACKAGE) | 包 |
@Target(ElementType.TYPE_PARAMETER) | 类型参数(@since 1.8) |
@Target(ElementType.TYPE_USE) | 用户类型(@since 1.8) |
(2)@Retention
- 用于描述注解的生命周期,生命周期的长短取决于
@Retention
的属性RetentionPolicy
的取值。
取值 | 描述 | 作用范围 | 使用场景 |
---|---|---|---|
RetentionPolicy.SOURCE | 表示注解只保留在源文件,当java文件编译成class文件,就会消失。 | 源文件 | 只是做一些检查性的操作,如 :@Override 和 @SuppressWarnings。 |
RetentionPolicy.CLASS | 表示注解被保留到class文件,当jvm加载class文件时候被遗弃,这是默认的生命周期。 | class文件(默认) | 要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife) |
RetentionPolicy.RUNTIME | 表示注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在。 | 运行时也存在 | 需要在运行时去动态获取注解信息 |
注:
- 上述三种类型生命周期:SOURCE < CLASS < RUNTIME。
- 我们一般都需要在程序运行时去动态获取注解信息,所以我们一般将
@Retention
的值设置为:RUNTIME。
(3)@Documented
表示该注解将被生成在javadoc
(文档注释)中。
(4)@Inherited
表示子类可以继承父类中的该注解。
1.4、自定义注解
格式:
1 |
|
注:
- 注解定义中的每一个方法实际上是声明了一个配置参数。
- 方法的名称就是参数的名称;
- 返回值类型就是参数的类型;返回值类型只能是基本数据类型、Class、String、Enum。
- 可以通过default来声明参数的默认值,我们经常在定义注解参数时使用空字符串、0作为默认值。
- 如果只有一个参数成员,一般参数名称为
value
,那么在使用该注解时可以省略参数名称value
直接给该参数赋值。 - 定义了参数的注解在使用时,我们必须要给没有默认值的参数赋值,否则会报错。
- 如果一个注解有多个参数,那么再使用该注解时,给各参数的赋值是无序的。
例:
1 | import java.lang.annotation.ElementType; |
1.5、注解的运行原理:反射+动态代理(重点)
注解的本质是:一个继承了Annotation
的特殊接口。其具体实现类是java
运行时,通过动态代理机制生成的动态代理类。
-
编译器可以通过反射的方式读取注解。
-
当编译器通过反射获取注解时,返回的是Java运行时生成的动态代理对象
$Proxy1
,进而可以通过代理对象$Proxy1
调用注解(接口)中的方法。 -
通过代理对象
$Proxy1
调用注解(接口)中的方法,会最终会调用AnnotationInvocationHandler
的invoke
方法。该方法会从memberValues
这个Map
中索引出对应的值。而memberValues
的来源是Java
常量池。
2、反射(Reflection)
2.1、Java反射机制概述
2.1.1、静态语言 VS 动态语言
(1)动态语言(弱类型语言)
- 动态语言是在运行时确定数据类型与结构的语言。变量使用之前不需要类型声明,通常变量的类型是被赋值的那个值的类型(如:JavaScript中的var变量)。
1 | var a = 1; |
-
常见的动态语言:Object-C、C#、JavaScript、PHP、Python等。
-
优点: 给实际的编码带来了很大的灵活性,我们只关注对象的行为,而不关注对象本身。
-
缺点: 代码运行期间有可能会发生与类型相关的错误。
(2)静态语言
-
静态语言是在编译时确定变量的数据类型,运行期间不可以改变其结构的语言。即运行前可确定的语言。
多数静态类型语言要求在使用变量之前必须声明数据类型。
-
常见的静态语言:Java、C、C++等。
-
优点:
-
1.避免程序运行时发生变量类型相关的错误;
-
2.先前明确了变量的类型,编译器可以针对这些信息对程序做出一些优化,从而提高程序执行的速度。
-
-
缺点:
-
1.写代码的时候,需要格外注意变量的类型;
-
2.过多的类型声明会增加更多的代码。
-
注: Java 是静态语言,但是它具有一定的动态性。虽然Java不是动态语言,但是可以称之为“准动态语言”。即Java具有一定的动态性,我们可以通过Java的反射机制获得类似动态语言的特性。Java的动态性让编程更加灵活。
2.1.2、什么是反射
**反射(Reflection)**主要是指程序可以访问、检测和修改它本身状态或行为的一种能力。(就像照镜子反射一样)
Java反射机制是指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个属性和方法。这种动态获取类的信息以及动态调用对象的属性和方法的功能称为Java语言的反射机制。
2.1.3、反射的基本原理
反射(Reflection)是Java被视为动态语言的关键。反射机制运行程序在执行过程中借助Reflection API取得任何类的内部信息(包括:类名、类的属性、类的成员变量、类的方法等),并能直接操作任意对象的内部属性和方法。
**注:**即使是private
修饰的成员变量和方法,我们也可以通过反射机制直接获取到这些私有成员。
**类加载完成后,会在堆内存的方法区中产生一个Class
类型的对象(一个类只有一个Class对象),这个对象就包含了类的完整的结构信息。**我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子可以看到类的结构。所以,我们形象地称之为“反射”。
通过正常方式和反射反射获取实例的过程如下图所示。
2.1.4、反射的作用
Java反射机制提供的功能:
-
在程序运行时判断任意一个对象所属的类;
-
在程序运行时构造任意一个类的对象;
-
在程序运行时判断任意一个类所具有的成员变量和方法;
-
在程序运行时获取泛型信息;
-
在程序运行时调用任意一个对象的成员变量和方法;
-
在运行时处理注解;
-
生成动态代理
**注:**动态代理是一种机制,在以后学习AOP(面向切面编程)时会用到,框架中大量运用了动态代理。
-
……
2.1.5、Java反射机制的优缺点
**(1)优点:**可以实现动态创建对象和编译,提高了代码的灵活性。
**(2)缺点:**会降低程序的性能。通过反射获取对象的速度比通过new的方式获取对象的速度慢几十倍。
2.2、理解Class类并获取Class实例
2.2.1、Class类
Java在Object类中定义了public final Class getClass()
方法,该方法将被所有子类继承。该方法的返回值类型是一个Class类类型,此类是Java反射的源头。
实际上反射从程序的运行结果来看也很好理解,即:可以通过对象反射求出类的名称。
对象照镜子后可以得到的信息包括:某个类的属性、方法、构造器、实现的接口、父类等。对于每个类而言,JRE都为其保留了一个不变的Class类型的对象。无论这个类创建了多少个对象,但这个类对应Class对象是唯一的。
注:
- Class本身也是一个类;
- Class对象只能由系统创建对象;
- 在加载一个类时JVM中只会有一个Class实例;
- 一个Class对象对应的是一个加载到JVM中的一个
.class
文件; - 每个类的实例都会记得自己是由哪个Class实例所生成的;
- 通过Class对象可以完整地得到一个类中的所有被加载的结构;
- Class类是反射(Reflection)的根源,针对任何你想动态加载、运行的类,必须先获得相应的Class对象。
2.2.2、Class类的常用方法
方法名称 | 功能说明 |
---|---|
static ClassforName(String name) | 获取已知类名的Class对象 |
Object newInstance() | 调用默认构造函数,创建Class对象的一个实例 |
getName() | 获取该Class对象所表示的实体(类、接口、数组类或void)的名称 |
Class getSuperClass() | 获取当前Class对象父类的Class对象 |
Class[] getInterfaces() | 获取当前Class对象的接口 |
ClassLoader getClassLoaders() | 获取该类的类加载器 |
Constructor[] getConstructors() | 获取当前Class对象的构造器数组 |
Method getMethod(String name, Class… T) | 获取该类的一个方法(Method)对象 |
Field[] getDeclaredFields() | 获取该类的所有字段(包括私有成员) |
2.2.3、如何获取Class类的实例
**(1)已知具体的类,通过类的class属性获取。**该方法最为安全可靠,程序性能最高。如:
1 | Class 类名 = Person.class; |
**(2)已知某个类的实例,调用该实例的getClass()
方法获取Class对象。**如:
1 | Class 类名 = person.getClass(); |
**(3)已知一个类的全类名,且该类在类路径下,则可通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException异常。**如:
1 | Class 类名 = Class.forName("com.atang.demo01.Student"); |
**(4)内置基本数据类型的包装类可以直接用类名.Type
获取。**如:
1 | Class 类名 = Integer.TYPE; //int类型的包装类 |
(5)利用ClassLoader(类加载器)获取。
2.2.4、哪些类型可以有Class对象
具有Class对象的数据类型包括:
- class:类类型。包括:外部类、成员(成员内部类、静态内部类)、局部内部类、匿名内部类。
- interface:接口类型
- []:数组类型
- enum:枚举类型
- annotation:注解类型(@interface)
- primitive type:基本数据类型的包装类
- void:空类型
例:
1 | import java.lang.annotation.ElementType; |
运行结果如下图所示:
2.3、类的加载与ClassLoader
2.3.1、类加载内存分析
(1)Java内存分析
Java的内存分为堆、栈和方法区。其中,方法区是一种特殊的堆。
(2)类的加载过程
当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过如下三个步骤来对该类进行初始化。
-
加载:将类的class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象。
-
**链接:**将Java类的二进制代码合并到
JVM
运行状态之中的过程。- 验证:确保加载的类信息符合JVM规范,没有安全方面的问题。
- 准备:正式为类变量(static)分配内存,并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
- 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
-
初始化:
- 执行类构造器
<clinit>()
方法的过程。类构造器<clinit>()
方法执行时,编译器会自动收集类中所有类变量的赋值动作,并将其与静态代码块中的语句合并。(类构造器是构造类信息的,不是构造该类对象的构造器)。 - 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发器父类的初始化。
- 虚拟机会保证一个类的
<clinit>()
方法在多线程环境中被正确加锁和同步。
- 执行类构造器
2.3.2、什么时候会发生类的初始化
-
类的主动引用(一定会发生类的初始化)
- 当虚拟机启动时,先初始化
main
方法所在的类; new
一个类的对象时,该类会被初始化;- 调用类的静态成员(除final常量)和静态方法时,该类会被初始化;
- 使用
java.lang.reflect
包的方法对类进行反射调用时,该类会被初始化; - 当初始化一个类时,如果其父类没有被初始化,则先会初始化其父类。
- 当虚拟机启动时,先初始化
-
类的被动引用(不会发生类的初始化)
-
当访问一个静态域时,只有真正声明这个域的类才会被初始化。如:当通过子类调用父类的静态变量,不会导致子类被初始化;
-
通过数组定义类引用,不会触发此类的初始化。如:
1
Student[] array = new Student[5]; //此时Student类并不会被初始化
-
引用常量不会触发此类的初始化。(常量在链接阶段就存入调用类的常量池中了)
-
2.3.3、类加载器(ClassLoader)
**(1)类加载器的作用:**将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class
对象,作为方法区中类数据的访问入口。
**(2)类缓存:**标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM的垃圾回收机制可以回收这些Class对象。
(3)类加载器的类型
JVM规范定义了如下类型的类加载器:
- **引导类加载器:**用C++编写的,是JVM自带的类加载器,负责Java平台核心库(即:
jre/lib
目录下的rt.jar
包(root)),用来装载核心类库。该加载器无法直接获取。 - **扩展类加载器:**负责
jre/lib/ext
目录下的jar
包或-D java.ext.dirs
所指目录下的jar
包装入工作库。 - **系统类加载器:**负责
java -classpath
或-D java.class.path
所指目录下的类与jar
包装入工作。System ClassLoader又称APPClassLoader,是最常用的加载器。
(4)类加载器的加载顺序
2.4、通过反射获取运行时类的完整结构
我们可以通过反射获取运行时类的完整结构,主要包括:**字段(Field)、方法(Method)、构造器(Constructor)、父类(Superclass)、接口(Interface)、注解(Annotation)**等。
如:
1 | import java.lang.annotation.ElementType; |
运行结果如下图所示(部分):
2.5、通过反射动态创建与使用对象
(1)如何通过反射动态创建对象
-
当某个类含有无参构造函数时,我们可以直接通过该类的Class对象调用
newInstance()
方法来动态创建该类的对象。 -
当某个类只有有参构造函数时,我们可以:
①先通过该类的Class对象调用
getConstructor(Class<?>... parameterTypes)
方法得到该类指定形参类型的构造器;②然后再通过该有参构造器调用
newInstance(形参列表)
方法来动态创建该类的对象。
(2)如何通过反射动态调用类的方法(含私有)
主要步骤:
①先通过该类的Class对象动态创建该类的对象;
②然后再通过该类的Class对象调用getMethod(String name, Class<?>... parameterTypes)
方法取得一个Method对象,并设置此方法操作时所需要的参数类型;
③最后,使用(激活函数)invoke(Object obj, Object... args)
方法进行调用,并向方法中传递要设置的obj
对象的参数信息。
附1:激活函数invoke(Object obj, Object... args)
方法
- 返回值
Object
对应原方法的返回值。若原方法无返回值,则返回null
。 - 若原方法为静态方法,则形参
Object obj
可为null
。 - 若原方法形参列表为空,则
Object[] args
为null
。 - 若原方法声明为
private
,则需要在调用此invoke()
方法前,显式调用方法对象的setAccessible(true)
方法,将可访问private
的方法。
附2:setAccessible(boolean flag)
方法
Method
和Field、Constructor
对象都有setAccessible(boolean flag)
方法。setAccessible
的作用是启动和禁止访问安全检查的开关。- 若参数值为
true
,则指示反射的对象在使用时应该取消Java语言访问检查。- 可以提高反射的效率。
- 使得原本无法访问的私有成员也可以访问。
- 若参数值为
false
,则指示反射的对象在使用时应该实施Java语言访问检查(默认设置)。
(3)如何通过反射动态调用类的属性(含私有)
主要步骤:
①先通过该类的Class对象动态创建该类的对象;
②然后再通过该类的Class对象调用getField(String name)
方法取得一个Field对象;
③最后,使用set(Object obj, Object value)
方法进行调用。
**例:**先在src目录下自定义一个用户类,然后通过反射创建用户对象,并调用其属性和方法。
User.java文件:
1 | public class User { |
在主函数中通过反射动态创建对象,并通过该对象调用其方法和属性。
test.java文件:
1 | import java.lang.reflect.Constructor; |
程序运行结果如下:
2.6、性能对比分析
下面通过代码,对比分析通过普通方式调用方法、通过反射方式调用方法(不关闭安全监测)和通过反射方式调用方法(关闭安全监测)三者间所需要的时间。
1 | import java.lang.reflect.InvocationTargetException; |
程序运行结果如下:
结论:
通过反射方式(不关闭安全监测)用时
>通过反射方式(关闭安全监测)用时
> >普通方式用时
。- 若平时经常使用反射,则建议关闭安全监测,这样可以提升程序的运行效率。
2.7、通过反射操作注解
下面我们将通过练习ORM,学习如何通过反射操作注解。
2.7.1、什么是ORM
ORM,即:Object-Relational Mapping(对象关系映射)。它的作用是在关系型数据库和业务实体对象之间作一个映射。
其中:
- 类和表结构对应;
- 属性和字段对应;
- 对象和记录对应。
常用的 Java ORM 框架有 Hibernate 和 Mybatis。
2.7.2、利用注解与反射完成类和表结构的映射关系
例:利用注解与反射完成Student
类和db_student
表结构(字段分别为:db_id、db_age、db_name
)的映射关系。
1 | import java.lang.annotation.*; |
(本讲完,系列博文持续更新中…… )
关注**“阿汤笔迹”** 微信公众号,获取更多学习笔记。
原文地址:http://www.atangbiji.com/2022/09/08/annotationAndReflection
博主最新文章在个人博客 http://www.atangbiji.com/ 发布。