编译软件:IntelliJ IDEA 2019.2.4 x64
运行环境:win10 家庭中文版
jdk版本:1.8.0_361
一. 反射是什么?
我们之前写代码的步骤:
先在编译期间,先确定要创建的对象的类型,然后用new关键字去创建对象,并且也是在编译期间确定它要调用的方法,或者要操作的属总结: 在编译时什么都已知并且确定了。
在之后写代码可能会遇到这样的问题?
在编译期间,或者写代码的时候,还不能确定你要new的对象的类型是什么? 所以就无法直接使用new表达式创建对象.
或者说,在编译时还不能确定要调用对象的哪个方法,或者操作哪个属性,就无法写 对象.方法,对象.属性表达式。
例如: JDBC,Java代码操作数据库
现在需要写一段程序,来读取数据库中的数据。
但是还不清楚,你要操作的数据库的数据是什么,可能是一个学生表,可能是一个员工表,可能是一个订单表…
就是无法确定在Java中需要new什么对象。
问题的根本原因,需要在运行时才能确定,你要创建什么类型的对象,调用什么方法,或者为什么属性赋值。
原来: 已知的Java类 --> 获取类的信息
现在: 在运行时去动态的获取某个类 CLass对象–>通过CLass对来获取类的信息–> 创建对象
二. java.lang.Class类
要想解剖一个类,必须先要获取到该类的Class对象。而剖析一个类或用反射解决具体的问题就是使用相关AP!
(1) java.lang.Class
(2) java.lang.reflect.*。
所以,🔺Class对象是反射的根源。
2.1 哪些类型可以获取class对象?
Java所有类型都有CLass对象,包括基本数据类型、void、引用数据类型(数组、类、接口、枚举、注解等)。
2.2 如何获取一个类的class对象?
1.最简单但是有局限性,要求编译时这个类型就是已知,
已存在的类型类型名.class
代码演示如下:
public class TestClass {
@Test
public void test01(){
//基本数据类型
Class c1 = int.class;
//void 空类型
Class c2 = void.class;
//数组
Class c3=int[].class;
//接口
Class c4=Comparable.class;
//String
Class c5="hss".getClass();
//自定义类型-->自己写的类
Class c6=TestClass.class;
//注解
Class c7=Test.class;
//类类型
Class c8=Math.class;
System.out.println("基本数据类型int:"+c1);
System.out.println("空类型void:"+c2);
System.out.println("数组int[]:"+c3);
System.out.println("接口Comparable:"+c4);
System.out.println("String:"+c5);
System.out.println("自定义类型TestClass:"+c6);
System.out.println("注解Test:"+c7);
System.out.println("类类型Math:"+c8);
}
}
2.只针对引用数据类型
对象名.getCLass()
代码演示如下:
@Test
public void test02(){
//String
Class c5="hss".getClass();
System.out.println(c5);
}
3.通用的方法,一般是针对普通引用数据类型,
一般数组、基本数据类型这种内置的类型或动态编译生成的类型不会用它获取
cLass.forName(“类型的全名称”)
@Test
public void test03() throws ClassNotFoundException {
Class c1 = Class.forName("java.lang.String");
System.out.println(c1);
}
4.一般很少用,一般用于自定义的类加载器类加载器对象.Loadclass("类型的全名称”)
ClassLoader类中有一个方法getSystemClassLoader获取当前系统的默认的类加载器对象
@Test
public void test04() throws ClassNotFoundException {
ClassLoader c1= ClassLoader.getSystemClassLoader();//得到系统默认的类加载器对象
Class c2=c1.loadClass("java.lang.Thread");
System.out.println(c2);
}
💡小tips:
针对同一个类型来说,以上四种方式获取到的CLass是同一个。
也就是说每一种类型在内存中只有唯一的一个CLass对象。
三. 类加载
任何一个类的class对象并不是由程序员手动创建,而是类在加载时自动产生
类在内存中完整的生命周期: 加载–使用–>卸载
一般不会考虑到卸载这一步
3.1 类的加载过程
当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、连接、初始化三个步骤来对该类进行初始化,如果没有意外,JMM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载。
类的加载又分为三个阶段:
(1) 加载: load
就是指将类型的class字节码数据读入内存
(2) 连接: link
①验证:校验合法性等
②准备: 准备对应的内存(方法区),创建Cass对象,为类变量赋默认值,为静态常量赋初始值
③解析: 把字节码中的符号引用替换为对应的直接地址引用
什么叫“符号引用"?
比如:String str=“Hello world”;
String 就是 符合引用
符号引用 | 类和接口的全限定名 |
---|---|
字段的名称和描述符 | |
方法的名称和描述符 |
举例:str是个字符串,就会在字符串常量池中找到’'Hello world”的地址值,将前面的符合引用(String)替换为
''Hello world”的地址值,这样程序运行时就不用线性寻址,加快程序运行速度。即使类加载时会慢一点,但在运
行时速度会很快。
(3)初始化: initialize (类初始化)即执行类初始化方法,大多数情况下,类的加载就完成了类的初始化,有些情况下,会延迟类的初始化。
四. 类初始化
4.1 哪些操作会导致类的初始化?
(1) 运行主方法所在的类,要先完成类初始化,再执行main方法
(2)第一次使用某个类型就是在new它的对象,此时这个类没有初始化的话,先完成类初始化再做实例初始化
(3) 调用某个类的静态成员(类变量和类方法) ,此时这个类没有初始化的话,先完成类初始化(4)子类初始化时,发
现它的父类还没有初始化的话,那么先初始化父类
(5) 通过反射操作某个类时,如果这个类没有初始化,也会导致该类先初始化
🔔类初始化执行的是0,该方法由 (1) 类变量的显式赋值代码(2) 静态代码块中的代码构成
4.2 哪些代码会让类初始化延迟进行?
(1)如果通过一个子类使用从父类继承的静态变量,静态方法时。延迟子类的初始化。
(2) 如果使用某个类声明数组,使用数组,不会导致这个类初始化。
(3) 如果使用某个类的静态的常量,也不会导致这个类初始化。
代码演示如下:
public class TestMethod {
public static void main(String[] args) {
System.out.println(son.a);
//定义长度为4的son类数组
son[] sons=new son[4];
System.out.println(sons.length);//只是得到的是son类数组的长度,son类数组sons里没有装任何对象
System.out.println(son.MIN);//该类的常量早在类的加载中在执行第二步连接阶段准备部分就已被赋默认值,不需要进行初始化
}
}
class fa{
static int a=1;
static {
System.out.println("fa.静态代码块");
}
}
class son extends fa{
public static final int MIN=10;
static{
System.out.println("son.静态代码块");
}
}
五. 类加载器
5.1 类加载器的类型 ClassLoader类
(1) 引导类加载器
: 负责加载最最核心的类库,例如: JRE的核心类库中的rt.jar等.
如何查看JRE的核心类库中的rt.jar包?
请看如下:
(2) 扩展类加载器
:负责加载JRE目录下lib 文件中的ext扩展库
如何查看JRE目录下lib 文件中的ext扩展库?
找到自己jdk安装目录–>jre–>lib–>ext
如下所示:
(3) 应用程序类加载器
: 负责程序员自己编写的类、接口等
例如: TestClassLoader是我们自己写的
(4) 自定义类加载器
:
通常情况下我们是不需要自定义类加载器,
但是如果你的系统有如下两种情况可以自定义类加载器:
A: 字节码文件需要加密,自定义类加载器在加载类的过程中先解密,然后再创建CLass对象等。
B:自定义的类或类路径是在特殊的,特定的路径,和平时其他项目的类路径不同。
例如: tomcat服务器,它的classes是放在 WEB-INF/classes 文件夹
有的系统是把字节码文件放到网络中的某个数据库中,或者是某台文件服务器,和程序运行不
个服务器。
5.2 这些类加载器是如何一起工作?
我们这里主要讨论 (1) (2) (3)
它们之间的工作模式被称为**“双亲委托模式”**。
🤮设计目的是为了安全。
双亲:
生活中:父母双亲
这里是: parent属性
每一个类加载器都有一个parent属性,记录父加载器。
🤮类加载器工作流程如下:
A:当系统需要加载某个类时,同时时应用程序类加载器先接到任务,例如: 要加载java.lang.String类型。
B:应用程序类加载器接到任务后,会先在方法区搜索这个类的CLss对象,如果找到了,说明这个类已经被加
载过了那么就直接返回它的CLass对象,不会重复加载。如果没有找到这个类的CLass对象,会把这个任务先
提交给“父”加载器应用程序加载器的父加载器就是扩展类加载器。
C:扩展类加载器接到任务之后,会在方法区搜索这个类的CLass对象,如果找到了,说明这个类已经被加载过
了那么就直接返回它的CLass对象,不会重复加载。
如果没有找到这个类的CLass对象,会把这个任务先提交给“父”加载器
扩展类加载器的父加载器就是引导类加载器,也称为根加载器
D:引导类加载器接到任务之后,会在方法区搜索这个类的CLass对象,如果找到了,说明这个类已经被加载过
了那么就直接返回它的CLass对象,不会重复加载。
如果没有找到这个类的CLass对象,会把在自己负责的区域加载这个类,例如它负责rt.jar等。
如果找到了,就返回它的CLass对象,
如果没找到,就把任务往回传,传给扩展类加载器
E: 扩展类加载器接到父加载器回传的任务,就会在他负责的目录下加载这个类,例如ire/ib/ext如果找到了,
就返回它的CLass对象,如果没找到,就把任务往回传,传给应用程序类加载器F;应用程序类加载器接到父加
载器回传的任务,就会在他负责的目录下加载这个类,例如:项目路径下(idea中就是out目录)如果找到了,就
返回它的CLass对象,如果没找到,就报错CLassNotFoundException
5.3 获取某个类的类加载器对象
通过这个类的CLass对象.getCLassLoader()
代码演示如下:
public class TestMain {
public static void main(String[] args) throws ClassNotFoundException {
//引导区的类加载器
Class c1= Class.forName("java.lang.String");
ClassLoader classLoader = c1.getClassLoader();
System.out.println("Str类的类加载器:"+classLoader);//Str类的类加载器:null
//why?引导区的类加载器不是由Java语言实现的,所以得不到它的对象,故而为null
//应用程序类加载器
Class c2= Class.forName("exercise.e10.TestMain");
ClassLoader loader = c2.getClassLoader();
System.out.println(loader);//sun.misc.Launcher$AppClassLoader@18b4aac2 应用程序类加载器
//扩展类加载器
Class c3=Class.forName("sun.security.pkcs11.Config");
System.out.println(c3.getClassLoader());//sun.misc.Launcher$ExtClassLoader@54bedef2
}
}
六. 反射的基本应用
🤮注意:
特别用cLass.forName(“类型的全名称”)获取class对象时
若被打jar包的模块中发生代码的增删修改,比如新建一个类或成员变量或方法。一定要及时build Artifacts–>build 自己打好的jar包,然后把新build的jar包及时替换到jdk–>jre–>lib-ext目录下
6.1 获取类型的详细信息
🚩只要可以获取一个类的CLass对象,就可以获取它里面的所有的信息。
代码演示如下:
@Test
public void test01() throws ClassNotFoundException {
Class c1=Class.forName("java.lang.String");
//1)获取String类的class对象的包名
System.out.println(c1.getPackage());
//2)获取class对象的类名
System.out.println(c1.getName());
//3)获取类的修饰符
int modifiers = c1.getModifiers();
System.out.println(modifiers);//17
System.out.println(Modifier.toString(modifiers));
//4)获取类的父类
System.out.println(c1.getSuperclass());
//5)获取类的实现的接口
System.out.println(c1.getName()+"实现的接口:");
Class[] interfaces = c1.getInterfaces();
for (Class anInterface : interfaces) {
System.out.println(anInterface);
}
//6)获取String类的成员变量
/*
获取string类所有成员变量
clazz.getField("属性名"); 获取某一个公共的成员变量
clazz.getFields(); 获取所有的公共的成员变量
clazz.getDeclaredField("属性名"); 获取某一个已声明的成员变量
clazz.getDeclaredFields(); 获取所有已声明的成员变量,包括非公共的,私有的等
*/
System.out.println("String类的所有成员变量:");
Field[] declaredFields = c1.getDeclaredFields();
for (Field declaredField : declaredFields) {
System.out.println(declaredField);
}
//7)获取String类的构造方法
//获取string类的构造器们
/* clazz.getConstructor(构造器的形参类型列表) 获取一个公共的构造器
clazz.getConstructors(); 获取所有公共的构造器
clazz.getDeclaredConstructor(构造器的形参类型列表) 获取一个已声明的构造器
clazz.getDeclaredConstructors(); 获取所有已声明的构造器
*/
System.out.println(c1.getName()+"所有的构造器:");
Constructor[] declaredConstructors = c1.getDeclaredConstructors();
for (Constructor declaredConstructor : declaredConstructors) {
System.out.println(declaredConstructor);
}
//8)获取String类的方法们
//获取string类的方法们
/*
cLazz.getMethod(方法名, 方法的形参类型列表): 获取某个公共的方法
clazz.getDeclaredMethod(方法名,方法的形参类型列表): 获取某个已声明的方法
clazz.getMethods() 获取所有公共的方法
clazz.getDeclaredMethods() 获取所有已声明的方法 */
System.out.println(c1.getName()+"所有的方法们:");
Method[] declaredMethods = c1.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
System.out.println(declaredMethod);
}
System.out.println("-------------------------------------------");
Class c2= ArrayList.class;
//9)获取ArrayList的子类们
System.out.println(c2.getName()+"所有的内部类们:");
Class[] declaredClasses = c2.getDeclaredClasses();
for (Class innerClass : declaredClasses) {
System.out.println(innerClass);
}
}
6.2 创建任意类型的对象
🚩当然不包括 基本数据类型和void,抽象类,接口等
6.2.1 方案一
步骤:
(1) 先获取这个类的CLass对象
(2) 调用CLass对象的newInstance()方法来创建实例对象
🔔注意:
这种方式创建对象有限制,必须要求这个类有无参的公共构造器
代码演示如下:
@Test
public void test02() throws Exception {
Class<?> c1 = Class.forName("testPackage.TestMain");
Object obj = c1.newInstance();
System.out.println(obj);//java.lang.ClassNotFoundException: testPackage.TestMain
//原因:另一个模块outerModule的testPackage.TestMain没有导入到本模块demo的train.t1 的扩展类加载器中
}
🚩如何导出为jar包?
请看如下演示:
1)单击工具栏的图标,如下所示:
然后打开项目设置
2)选择Artifacts(打包部署),选择添加jar。
3)给导出的jar命名,确认导出的路径
4)确认创建或选择已有的Manifest.MF文件。单词“manifest”的意思是“显示” 。 这个 manifest 文件定义了与扩展和包相关的数据。
🌏 说明:
打开Java的JAR文件,我们经常可以看到文件中包含着一个META-INF目录,这个目录下会有一些文件,其中必有一个MANIFEST.MF,这个文件描述了该Jar文件的很多信息 。
5)选择Manifest.MF文件存储目录。
6)选择jar要包含的.class文件的目录,即要打包的模块的out目录
7)编译生成jar
💡小tips:
编译好的jar包在哪里?
在第三步中创建jar包时指定的jar包存储路径里去找
8)把编译好的jar包复制,粘贴到jdk安装目录下 jre–>lib–>ext 下
💡小tips:
这样就可以通过扩展类加载器去找另一个模块下的类的clss对象
代码演示如下:
public class t1 {
public static void main(String[] args) throws Exception{
Class<?> c1 = Class.forName("testP1.testMain");
Object obj = c1.newInstance();//这个obj对象实际上是模块Testmodule下的testP1.testMain类的对象
System.out.println(obj);
}
}
代码演示如下:
Class c2=Class.forName("testP1.testDemo");
Object o = c2.newInstance();//这个obj对象实际上是模块Testmodule下的testP1.testDemo类的对象
System.out.println(o);
/*
Exception in thread "main" java.lang.InstantiationException: testP1.testDemo
Caused by: java.lang.NoSuchMethodException: testP1.testDemo.<init>()
模块Testmodule下的testP1.testDemo类没有无参构造
*/
6.2.2 方案二
步骤:
(1) 先获取这个类的CLass对象
(2) 先获取有参构造器对象
(3) 调用构造器对象的newInstance()方法来创建实例对象
代码演示如下:
public class testDemo {
private String name;
private int id;
public testDemo(String name, int id) {
this.name = name;
this.id = id;
}
@Override
public String toString() {
return "testDemo{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
}
Class c3=Class.forName("testP1.testDemo");
Constructor construct = c3.getDeclaredConstructor(String.class,int.class);
Object o1 = construct.newInstance("jack",12);
System.out.println(o1);
🔔如果构造器是非公共的,那么需要调用 构造器对象的setAccessible(true)
🚩结论:
为了后期很多的框架可以为你的类自动创建的对象更方便,请保留你这个类公共的无参构造。
6.3 动态的操作任意对象的任意属性
步骤:
(1) 获取类的CLass对象
(2) 通过CLass对象newInstance()
🔔前提条件是这个类有公共的无参构造
(3) 先获取id属性的Field对象
(4) 可选的,如果属性的权限修饰符允许,这一步可以不要
如果属性的权限修饰符不允许,可以加这步通过id属性对应的Field对象.setAccessible(true)
(5)操作id居性的值通过id属性对应的Field对象.get(实例对象)获取id属性的值通过id属性对应的Field对象.set(实例对象,值)设置id属性的值
代码演示如下:
package testP1;
public class Student {
private String name;
private int id;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
}
@Test
public void test02() throws Exception{
//通过反射创建Student类的对象,通过反射给student类的对象的id和name属性赋值,并通过反射获取它们的值
Class<?> aClass = Class.forName("testP1.Student");//获取模块Testmodule下的testP1.Student类的class对象
Object stud = aClass.newInstance();//获取Student类的实例对象
Field idField = aClass.getDeclaredField("id");//获取id属性的Field对象
idField.setAccessible(true);//将Student类id属性的权限修饰符设为允许访问,之前是private,不允许访问
System.out.println(idField.get(stud));//获取id属性的值 ==> stud.id
idField.set(stud,2);//给stud的id属性赋值 ==> stud.id=2;
System.out.println(idField.get(stud));
System.out.println("----------------------------------");
//给stud的name属性赋值
Field nameField = aClass.getDeclaredField("name");//获取name属性的Field对象
nameField.setAccessible(true);
System.out.println(nameField.get(stud));
nameField.set(stud,"张三");
System.out.println(nameField.get(stud));
System.out.println(stud);
}
6.4 在运行时动态的调用任意类的任意方法
步骤:
(1) 获取CLass对象
(2) 创建这个类的实例对象
🔔前提条件是这个类有公共的无参构造
(3) 先获取你要调用的方法的Method对象
(4) 调用方法
通过Method对象.invoke(实例对象,实参)
代码演示如下:
public class testMain {
public int doubleValue(int a){
return a*2;
}
public long doubleValue(long a){
return a*2;
}
}
@Test
public void test03() throws Exception{
Class<?> aClass = Class.forName("testP1.testMain");//获取class对象
Object testMain = aClass.newInstance();//获取testP1.testMain类的实例对象
Method doubleValueMethod = aClass.getDeclaredMethod("doubleValue", int.class);
/*
如何唯一的确定某个类的方法?
(1) 类: 通过cLazz对象确定是testMain类
(2) 方法名: doubleValue
(3) 形参列表:因为有可能doubleValue会有重载
*/
Object returnValue = doubleValueMethod.invoke(testMain, 10);//获取返回值
System.out.println(returnValue);
}
@Test
public void test04() throws Exception{
Class<?> aClass = Class.forName("testP1.testMain");//获取class对象
Object testMain = aClass.newInstance();//获取testP1.testMain类的实例对象
Method doubleValueMethod = aClass.getDeclaredMethod("doubleValue", long.class);
System.out.println(doubleValueMethod.invoke(testMain,20));
}
6.5 通过反射操作某个类的静态变量和静态方法
🤮反射操作某个类的静态方法
步骤:
(1) 获取CLass对象
(2) 先获取你要调用的方法的Method对象
(3) 调用方法
通过Method对象.invoke(实例对象,实参)
🤮反射操作静态变量的步骥
(1) 获取CLass对象
(2)获取要换作/访问的静态变量的Field对象(3)某个静态变量对应的FieLd对象.setAccessible(true);
(4)可以访问静态变量的值,或者给静态变量赋值某个静态变量对应的Field对象.get(null)就是获取静态变量的值某个静态变量对应的Field对象.set(null,值)就是设置静态变量的值
代码演示如下:
public class testMain {
static int b=1;
public static void getInfo(String info){
System.out.println("testMain.method");
System.out.println("info:"+info);
}
public int doubleValue(int a){
return a*2;
}
public long doubleValue(long a){
return a*2;
}
}
@Test
public void test05() throws Exception{
//通过反射调用testP1.testMain类的静态方法
Class<?> aClass = Class.forName("testP1.testMain");//获取class对象
Method getInfo = aClass.getDeclaredMethod("getInfo", String.class);
getInfo.invoke(null,"java");//null 表示这里不需要testP1.testMain类的对象,它是静态方法;Java相当于getInfo方法的实参
System.out.println("---------------------------------");
//通过反射调用testP1.testMain类的静态变量
Field bField = aClass.getDeclaredField("b");
bField.setAccessible(true);
System.out.println(bField.get(null));//null 表示这里不需要testP1.testMain类的对象,它是静态变量
bField.set(null,10);
System.out.println(bField.get(null));//null 表示这里不需要testP1.testMain类的对象,它是静态变量
}
七. 自定义注解与反射
7.1 注解
@Override: 标记某个重写的方法
@Deprecated: 标记某个类、方法已过时
@SuppressWarnings: 抑制警告
@Test: JUnit测试的标记
7.2 自定义注解
语法格式:
[修饰符] @interface 注解名 {
}
7.3 如何使用自定义注解
可以在类、方法、成员变量等上面加注解
7.4 自定义注解包含三个部分
7.4.1 声明
public @interface MyAnnotation {
}
7.4.2 使用
@MyAnnotation
public class MyClass {
}
7.4.3 读取
如果没有 读取部分,前面的代码就完全没用。
就好比说,
@Override
声明: public @interface Override {
}
使用: class Son extends Father{
@Override //注解的使用
public void method() {
System.out.println(“son .method”) ;
}
读取:
编译器会读取这个注解,会对方法进行格式检查
7.5 元注解
给注解加注释的注解
@Target : 用来解释/注释某个注解可以用在哪里可以用
注解的位置一共有10个。
ELementType是一个枚举类型,每一个常量对象,代表一个注解可以使用的位置
ELementType.TYPE: 表示是类型上面
ELementType.FIELD: 表示是属性/字段/成员变量上面
@Retention: 用来解释/注释某个注解的生命周期
每一个注解的生命周期有3个阶段:
SOURCE: 源代码
CLASS: 字节码
RUNTIME: 运行时,内存中
@Documented: 用来解释/注释某个注解是不是可以被javadoc工具读取到API
@Inherited: 用来解释/注释某个注解是不是可以被子类继承
代码演示如下:
import java.lang.annotation.*;
@Inherited //该元注解表明子定义注解可被子类继承
@Target(ElementType.TYPE)//a该自定过注解只能放在类类型上
@Retention(RetentionPolicy.RUNTIME)//代表它的生命周期是在运行时,只有这样,才能被反射读到
public @interface MyAnnotation {
String getValue();
//自定义的注解中只能写无参的抽象方法
String info();
//可以使用 default 关键字为抽象方法指定默认返回值
/*
抽象方法的返回值类型有限制:
只能是八种基本数据类型、String类型、CLass类型、enum类型、Annotation类型、以上所有类型的数组
*/
}
@YourAnnotation
@MyAnnotation(getValue ="jack",info = "你好")
/*
@Override
public String getInfo(String str){
return str
}
*/
public class MyClass {
}
public class MySub extends MyClass {
}
import java.lang.annotation.Annotation;
public class TestAnnotation {
public static void main(String[] args) {
Class aClass=MyClass.class;
//获取Annotation对象
Annotation annotation = aClass.getAnnotation(MyAnnotation.class);
System.out.println(annotation);
MyAnnotation my=(MyAnnotation)annotation; //需要向下转型,才能调用MyAnnotation对象的抽象方法
System.out.println(my.getValue());
System.out.println(my.info());
System.out.println("---------------------");
Class c1=MySub.class;
//获取Annotation对象
Annotation annotation1 = c1.getAnnotation(MyAnnotation.class);
System.out.println(annotation1);
}
}
public @interface YourAnnotation {
}
🔔注意:
1.抽象方法的返回值类型有限制:
只能是八种基本数据类型、String类型、CLass类型、enum类型、Annotation类型、以上所有类型的数组2.自定义的注解中只能写无参的抽象方法,在无参的抽象方法中可以使用 default 关键字为抽象方法指定默认返回值