言成言成啊 | Kit Chen's Blog

java中的反射与注解

发布于2020-04-22 23:30:09,更新于2021-11-16 19:39:48,标签:java  文章会持续修订,转载请注明来源地址:https://meethigher.top/blog

先附上java14的官方最新文档,不懂就查,方便快捷,一步到位。

一、反射

反射:框架设计的灵魂。

框架:半成品软件。可以在框架的基础上进行软件开发,简化编码。

在使用框架的时候,不会反射,没有问题;但如果要开发一个框架,就需要用到反射。

1.1 反射的概念

java代码在计算机中经历三个阶段:源代码阶段、类对象阶段、运行时阶段

上图中,在类对象阶段,将成员方法,封装成Constructor[]数组这种过程,这就是反射。

举个例子,就比如写代码时,定义的一个String变量,可以通过快捷键将其所有方法显示出来,这就是通过反射完成的。

概念:将类的各个组成部分封装为其他对象,这个过程叫做反射,也就是反射机制。

好处:

  1. 可以在程序运行过程中,操作这些对象。
  2. 可以解耦,提高程序的可扩展性。

很抽象,一会上案例。

1.2 获取Class对象

通过Class.forName(“全类名”)

如果此时处于源代码阶段,那我们可以通过Class.forName("全类名")这种方式,是将字节码文件加载进内存,返回class对象。

全类名:包名.类名

1
2
3
4
5
6
7
public class Demo01Reflect {
public static void main(String[] args) throws ClassNotFoundException {
//Class.forName("全类名"),全类名指包名.类名,即使是同一个包的,也不能省略包名
Class cls=Class.forName("demo44.Person");
System.out.println(cls);
}
}

适用场景:

多用于配置文件,将类名定义在配置文件中。读取文件,加载类

通过类名.class

如果此时处于类对象阶段,那我们可以通过类名的属性类名.class获取class对象

1
2
3
4
5
6
7
public class Demo01Reflect {
public static void main(String[] args) throws ClassNotFoundException {
//类名.class
Class cls2=Person.class;
System.out.println(cls2);
}
}

适用场景:

多用于参数的传递

通过对象.getClass()

如果此时处于运行时阶段,那我们可以通过对象名.getClass来获取class对象。

getClass()是在Object类中定义的,所以所有的类,都有这个方法

1
2
3
4
5
6
7
8
public class Demo01Reflect {
public static void main(String[] args) throws ClassNotFoundException {
//对象名.getClass
Person p=new Person();
Class cls3=p.getClass();
System.out.println(cls3);
}
}

适用场景:

多用于对象获取字节码的方式

一次运行只会加载一次字节码文件

将三个不同过程中,获取到的对象,进行地址值的比较,会发现都是同一个地址。

由此,我们可以得出结论:同一字节码文件(.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的class对象,都是同一个

1.3 使用Class对象的获取功能

以下面这个Person类为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class Person {
private String name;
private int age;
public int sex;
protected String intro;
String education;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}

public Person() {
super();
// TODO Auto-generated constructor stub
}

@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", sex=" + sex + ", intro=" + intro + ", education="
+ education + "]";
}

public void say() {
System.out.println(name + "最美!我爱" + name);
}

}

获取成员变量

常用方法

  • Field getField(String name) 返回一个Field对象,它反映此 Class对象所表示的类或接口的指定公共成员字段
  • Field[] getFields() 返回一个包含某些 Field 对象的数组,这些对象反映此 Class 对象所表示的类或接口的所有可访问公共字段
  • Field getDeclaredField(String name) 返回一个 Field 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明字段
  • Field[] getDeclaredFields() 返回 Field 对象的一个数组,这些对象反映此 Class 对象所表示的类或接口所声明的所有字段

操作

  • 获取值:Object get(Object obj) 返回指定对象上此 Field 表示的字段的值。
  • 设置值:void set(Object obj, Object value) 将指定对象变量上此 Field 对象表示的字段设置为指定的新值。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Demo02Reflect {
public static void main(String[] args) throws Exception {
//获取Person的class对象
Class personClass=Person.class;

//获取成员变量
//getFields(),获取所有public修饰的成员变量
Field[] fields=personClass.getFields();
for(Field field:fields) {
System.out.println(field);//只能获取到public修饰的成员变量
}

//getField(String)同样是获取指定public修饰的成员变量
Field sex=personClass.getField("sex");
//获取成员变量sex的值
Person p=new Person();
Object value=sex.get(p);
System.out.println(value);//因为没有给他赋值,所以获取的是默认的值0
//设置sex的值
sex.set(p, 1);
System.out.println(p);//Person{name='null',age=0,sex=1,intro='null',education='null'}

System.out.println("===============");

//getDeclaredFields(),获取所以的成员变量,不考虑修饰符
//连私有的都可以获取,这就很牛逼,不用反射时,私有的属性是不能在类外面进行访问或设置的;
//但是在反射里面,不存在私有。我的就是我的,你的还是我的
Field[] declaredFields=personClass.getDeclaredFields();
for(Field declaredField:declaredFields) {
System.out.println(declaredField);
}
Field name=personClass.getDeclaredField("name");
// Object value2=name.get(p);
// System.out.println(value2);//java.lang.IllegalAccessException,会报错。虽然私有的可以访问,但不能直接访问。

//在访问非public的成员变量的时候,需要忽略访问权限修饰符的安全性检查
name.setAccessible(true);//暴力反射
Object value2=name.get(p);
System.out.println(value2);//null
name.set(p, "胡列娜");
System.out.println(p);//Person{name='胡列娜',age=0,sex=1,intro='null',education='null'}
}
}

注意点:

  1. 一般地,私有的成员变量不能在类外面进行访问或设置;但是在反射中,私有成员变量就可以获取和设置。这就是反射一个很牛逼的点。
  2. 在访问非public的成员变量的时候,会抛异常IllegalAccessException。这就需要忽略访问权限修饰符的安全性检查setAccessible(boolean flag)true表示忽略,这也叫做暴力反射

获取构造方法

常用方法

  • ConstructorgetConstructor(Class<?>… parameterTypes) 返回一个 Constructor 对象,它反映此 Class 对象所表示的类的指定公共构造方法。
  • Constructor<?>[] getConstructors() 返回一个包含某些 Constructor 对象的数组,这些对象反映此 Class 对象所表示的类的所有公共构造方法。
  • ConstructorgetDeclaredConstructor(Class<?>… parameterTypes) 返回一个 Constructor 对象,该对象反映此 Class 对象所表示的类或接口的指定构造方法。
  • Constructor<?>[] getDeclaredConstructors() 返回 Constructor 对象的一个数组,这些对象反映此 Class 对象表示的类声明的所有构造方法。

操作

  • 创建对象:T newInstance(Object… initargs) Uses the constructor represented by this Constructor object to create and initialize a new instance of the constructor’s declaring class, with the specified initialization parameters.

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Demo03Reflect {
public static void main(String[] args) throws Exception {
// 获取Person的class对象
Class personClass = Person.class;


//获取构造方法
//Constructor<T> getConstructor(Class<?>... parameterTypes) 参数是一个可变参数
//参数列表,表示名称和性别
Constructor c=personClass.getConstructor(String.class,int.class);//注意Integer.class!=int.class
System.out.println(c);
//T newInstance(Object... initargs)通过这个来创建对象
Object person=c.newInstance("胡列娜",22);
System.out.println(person);

System.out.println("======================");
//也可以通过空参来获取构造方法
Constructor c2=personClass.getConstructor();
System.out.println(c2);
Object person2=c2.newInstance();
System.out.println(person2);

//如果想使用空参构造方法创建对象,操作可以简化:Class对象的newInstance()
//但是,这个方法在java9以后,就已经过时了,不推荐使用。
System.out.println(personClass.newInstance());

}
}

注意:

私有的构造方法,也是需要通过暴力反射来解决。同理,获取成员方法时也一样。

获取成员方法

常用方法

  • Method getMethod(String name, Class<?>… parameterTypes) 返回一个 Method 对象,它反映此 Class 对象所表示的类或接口的指定公共成员方法。
  • Method[] getMethods() 返回一个包含某些 Method 对象的数组,这些对象反映此 Class 对象所表示的类或接口(包括那些由该类或接口声明的以及从超类和超接口继承的那些的类或接口)的公共 member 方法。
  • Method getDeclaredMethod(String name, Class<?>… parameterTypes) 返回一个 Method 对象,该对象反映此 Class 对象所表示的类或接口的指定已声明方法。
  • Method[] getDeclaredMethods() 返回 Method 对象的一个数组,这些对象反映此 Class 对象表示的类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。

操作

  • 执行方法:Object invoke(Object obj, Object… args) 在具有指定参数的指定对象上,调用此Method对象表示的基础方法
  • 获取方法名:String getName() 以String形式返回此Method对象表示的方法的名称。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Demo04Reflect {
public static void main(String[] args) throws Exception {
// 获取Person的class对象
Class personClass = Person.class;

//获取成员方法
//Method getMethod(String name, Class<?>... parameterTypes)
Method m=personClass.getMethod("say");
//Object invoke(Object obj, Object... args)
//执行方法
m.invoke(new Person("胡列娜",20));//胡列娜最美!我爱胡列娜

System.out.println("===========");

//获取所有public修饰的方法getMethods
Method[] ms=personClass.getMethods();
for(Method method:ms) {
System.out.println(method.getName());//包含父类里面的public方法
}
}
}

下面放出一个坑

1
2
3
4
5
6
7
8
9
10
11
public class TestCircle {
public double getArea(double r) {
return Math.PI*r*r;
}
public static void main(String[] args) throws NoSuchMethodException, SecurityException {
Class c=TestCircle.class;
Method m=c.getDeclaredMethod("getArea", Double.class);
System.out.println(m);//java.lang.NoSuchMethodException
}

}

仔细一看报错了吧。这是我老师上课写的,结果我下课研究的时候,整了两个小时才整明白,找到错误。

这个错,就是方法中变量的类型跟反射里面定义的类型不一样。

通俗点说,就是你的方法的变量类型是int,那么反射中获取方法时,变量的类型是int.class,同理,如果是Integer,那么反射中应是Integer。可以参照这篇文章

获取类名

常用方法

  • String getName() 以 String 的形式返回此 Class 对象所表示的实体(类、接口、数组类、基本类型或 void)名称。

代码

1
2
Class personClass = Person.class;
System.out.println(personClass.getName());//输出的是全类名,即包名+类名

1.4 反射案例

需求

使用反射,写一个框架类,在不改变该类的任何代码的情况下,可以创建任意类的对象,并且执行其中任意的方法

实现:

  1. 配置文件
  2. 反射

步骤:

  1. 将需要创建的对象的全类名和需要执行的方法定义在配置文件中
  2. 在程序中加载读取配置文件
  3. 使用反射技术来加载类文件进内存
  4. 创建对象
  5. 执行方法

通过这样,我们以后如果修改的话,就只需要改配置文件,而不需要改代码了

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 框架类
* @author kitchen
*
*/
public class ReflectTest {
public static void main(String[] args) throws Exception {
// 使用反射,在不改变任何代码的情况下,可以帮我们创建任意类的对象,并且执行其中任意方法

// 1.加载配置文件
// 加载文件方法由很多,比方说,可以通过文件io流来进行读取,但是java里面有现成的对象可以处理配置文件
// 1.1创建Properties对象
Properties pro = new Properties();
// 1.2加载配置文件,转换为一个集合
// 1.2.1获取class目录下的配置文件的方式
ClassLoader classLoader = ReflectTest.class.getClassLoader();
// pro.load(new FileInputStream("src/demo44/pro.properties"));//这个是读取demo44下的配置文件
InputStream is = classLoader.getResourceAsStream("pro.properties");// 这样读取的话,默认是放到src根目录下的
pro.load(is);

// 2.获取配置文件中定义的数据
String className = pro.getProperty("className");
String methodName = pro.getProperty("methodName");

// 3.加载该类进内存
Class cls = Class.forName(className);
// 4.创建对象
Object obj = cls.newInstance();
// 5.获取方法对象
Method met = cls.getMethod(methodName);
// 6.执行方法
System.out.println(cls.getName());
met.invoke(obj);

}
}

当类是GirlFriend时,执行marry方法

当类是Wife时,执行divorce方法

同样的代码,可以有不同的运行结果,这个过程只需要改配置文件。

1
2
3
4
5
# className=demo44.domain.GirlFriend
# methodName=marry

className=demo44.domain.Wife
methodName=divorce

好处

当项目比较庞大的时候,如果直接改代码,改完之后还需要进行测试,容易出问题。而通过反射,只需要改配置文件就可以了。

二、注解

2.1 注解的概念

注释:用文字描述程序。比方说描述程序的功能,程序变量的含义之类的。注释给开发者看的

注解:说明程序的注解给计算机看的

概念描述

  • jdk1.5之后的新特性
  • 说明程序的
  • 使用注解:@注解名称

作用分类

  1. 编写文档:通过代码里标识的注解生成文档(通过javadoc 文件全名生成doc文档)
  2. 代码分析:通过代码里标识的元数据对代码进行分析(使用反射)
  3. 编译检查:通过代码里标识的元数据让编译器能够实现基本的编译检查(Override)

2.2 jdk的内置注解

常用内置注解

  • @Override:检测被该注解标注的方法是否是继承自父类(接口)的

  • @Deprecated:将该注解标注的内容,已过时

  • @SuppressWarnings: 指示编译器去忽略注解中声明的警告。

了解更多的java注解,移步到菜鸟教程。

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@SuppressWarnings("all")
public class Demo01Annotation {

@Override
public String toString() {
return "Demo01Annotation []";
}

@Deprecated
public void play() {
// 这个方法后来发现有缺陷,又新增了一个play2方法
}

public void play2() {
// 替代play方法,推荐开发者使用play2,而不使用play。
// 同时为了保证能够兼容低版本jdk,play方法选择保留,但是要提示开发者不建议使用
ArrayList list = new ArrayList();
}

public void demo() {
play();
}

}

描述一下@SuppressWarning注解的使用。如果代码段报警告,可以通过@SuppressWarnings(“all”)标注在其方法上来忽略警告,不过一般地,会直接将其标注在类上。

2.3 自定义注解

格式

如何自定义注解?先来看一下内置注解的格式:

通过对两个内置注解的观察,我们可以发现,注解是由下面这两个部分组成的。

  • 元注解
  • public @interface 注解名称{}

举个例子

1
2
public @interface Demo02Annotation {
}

以上面这个为例,我们将其进行javac编译,然后再通过javap反编译,得到以下这个代码

1
2
public interface Demo02Annotation extends java.lang.annotation.Annotation {
}

注解本质上就是一个接口,该接口默认继承了Annotation。接口里面可以定义什么,注解里面同样可以定义什么

属性

注解里的属性,可以认为是接口中的抽象方法,因为它可以在使用时,跟属性(成员变量)一样用

要求:

  1. 属性的返回值类型:基本数据类型、String、枚举、注解、以上类型的数组
  2. 定义了属性,在使用时,需要给属性赋值。
    • 如果定义属性时,使用default关键字给属性默认初始化值,则使用注解时,可以不进行属性的赋值
    • 如果只有一个属性需要赋值,并且属性的名称是value,则value可以省略(可以参考@SuppressWarnings的源码)
    • 数组赋值时,用{}包裹。如果其中只有一个值的时候,{}可以省略不写

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//定义一个枚举
enum Person {
Male, Female
}

//定义一个注解
@interface Anno {

}

public @interface Demo02Annotation {
int value();

String name() default "胡列娜";
Person p();//这是枚举
Anno a();//这是注解
String[] pers();//这是String类型数组
}

//尝试使用注解
//@Demo02Annotation(age = 22, name = "江厌离")
//如果我们不想给他赋值,我们可以在定义的时候,默认给他值
//如 String name() default "初值"
@Demo02Annotation(value=22,a = @Anno, p = Person.Male, pers = { "胡列娜","江厌离","邱若水" })
class Worker {
}

元注解

元注解:用于描述注解的注解

常用的元注解:

  • @Target:描述注解能够作用的位置
    • ElementType取值
      • TYPE:可以作用在类上
      • METHOD:可以作用在方法上
      • FIELD:可以作用在成员变量上
  • @Retention:描述注解被保留的阶段
    • @Retention(RetentionPolicy.RUNTIME):当前被描述的注解,会保留到class字节码文件中,并被jvm读取到。一般也只会用到这个
  • @Documented:描述注解会被抽取到api文档中
  • @Inherited:描述注解会被子类继承

对于@Retention,把源码截图放到这里,其中刚好对应java代码经过的三个阶段

像Documented,还有@Inherited就不多赘述了,演示过程也就不放上来了。通过命令javadoc生成文档时,会发现区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//表示Demo03Annotation注解只能做作用于类、方法和变量上
//注解被保留到Runtime阶段
//同时因为只有一个变量,所以value可以省略
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE,ElementType.METHOD,ElementType.FIELD })
public @interface Demo03Annotation {

}
@Demo03Annotation
class Worker01 {
@Demo03Annotation
public void show() {

}
}

2.4 使用(解析)注解

在程序中使用(解析)注解:获取注解中定义的属性值

由此,在大多数时候,注解是用来替换配置文件

步骤

  1. 获取注解定义的位置的对象
  2. 获取指定的注解
    • getAnnotation(Class)
  3. 调用注解中的抽象方法,获取配置的属性值

代码

反射案例一样,实现同样的功能。用注解替代配置文件

Pro.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Pro {
String className();

String methodName();
}

//下面这个代码,请忽略,只是为了帮助ReflectAnnotationTest.java理解第2步
//相当于实现了注解接口的实现类,具体代码参照ReflectAnnotationTest.java
class ProImpl implements Pro {

@Override
public Class<? extends Annotation> annotationType() {
// TODO Auto-generated method stub
return null;
}

@Override
public String className() {
// TODO Auto-generated method stub
return null;
}

@Override
public String methodName() {
// TODO Auto-generated method stub
return null;
}

}

ReflectAnnotation.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Pro(className = "demo44.domain.GirlFriend", methodName = "marry")
public class ReflectAnnotationTest {
public static void main(String[] args) throws Exception {
// 1.解析注解
// 1.1获取该类的字节码文件对象
Class<ReflectAnnotationTest> c = ReflectAnnotationTest.class;
// 2.获取上边的注解对象
Pro an = c.getAnnotation(Pro.class);// 其实就是在内存中生成了一个该注解接口的子类实现对象,这个过程我另外写一个代码ProImpl.java
// 3.调用注解对象中定义的抽象方法,获取返回值。因为该注解接口,已经在上一步中被实现了
String className = an.className();
String methodName = an.methodName();

// 4.加载该类进内存
Class cls = Class.forName(className);
// 5.创建对象
Object obj = cls.newInstance();
// 6.获取方法对象
Method met = cls.getMethod(methodName);
// 7.执行方法
System.out.println(cls.getName());
met.invoke(obj);
}
}

2.5 实现简单的测试框架

Check.java

1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Check {
}

Calculator.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Calculator {

// 加法
@Check
public void add() {
String str = null;
str.toString();
System.out.println("1 + 0 =" + (1 + 0));
}

// 减法
@Check
public void sub() {
System.out.println(Integer.parseInt("hahah123"));
System.out.println("1 - 0 =" + (1 - 0));
}

// 乘法
@Check
public void mul() {
System.out.println("1 * 0 =" + (1 * 0));
}

// 除法
@Check
public void div() {
System.out.println("1 / 0 =" + (1 / 0));
}

public void show() {
System.out.println("永无bug...");
}

}

TestCheck.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 简单的测试框架
* 当主方法执行后,会自动去执行被检测的所有方法(加了check注解的方法)
* 判断是否有异常,然后记录到文件中
* @author kit chen
*/
public class TestCheck {
public static void main(String[] args) throws IOException {
int count=0;//记录出错的次数
BufferedWriter bw=new BufferedWriter(new FileWriter("bug.txt"));
//1.创建计算器对象
Calculator c=new Calculator();
//2.获取字节码文件
Class cls=c.getClass();
//3.获取所有方法
Method[] methods=cls.getMethods();
//4.判断方法上是否有check注解,有就执行,无就跳过
for(Method m:methods) {
if(m.isAnnotationPresent(Check.class)) {
try {
m.invoke(c);
} catch (Exception e) {
//5.捕获异常,记录到文件
count++;
bw.write("异常的方法:"+m.getName());
bw.newLine();
bw.write("异常的名称:"+e.getCause().getClass().getSimpleName());
bw.newLine();
bw.write("异常的原因:"+e.getCause().getMessage());
bw.newLine();
bw.write("======================");
bw.newLine();
}
}
}
bw.write("本次测试一共出现 "+count+" 次异常");
bw.flush();
bw.close();
}
}

运行结果

这就实现了一个简单的测试框架。

同样的道理,像原来学过的Junit就算是一个测试框架了,里面的@Test就是注解。

总结:

  • 大多数时候,会使用注解,而不是自定义注解

  • 注解为谁服务?

    • 编译器
    • 解析程序。像刚才的TestCheck.java就算是一个解析程序了。如果没有程序的存在,注解的存在将毫无意义
  • 注解不是程序的一部分,可以将注解理解成一种标签。以刚才例子来说,给方法加上@Check,就表示,这个方法要被检测的意思。

PS:今天形式政策考试考了65,老师在群里让同学发成绩单,我的分数是最低的。这也算是我一段时间来学习态度的反映吧。像数学跟线代还有英语,这学期,上网课期间,我不是上课睡觉,就是在玩电脑,一点都没听,我也该收收心好好学学数学了。加油!奥利给!好好学习!

发布:2020-04-22 23:30:09
修改:2021-11-16 19:39:48
链接:https://meethigher.top/blog/2020/reflection-and-annotation/
标签:java 
付款码 打赏 分享
Shift+Ctrl+1 可控制工具栏