Java注解与反射
1.反射
1.1 相关概念
反射就是Reflection,Java的反射是指程序在运行期可以拿到一个对象的所有信息。
正常情况下,如果我们要调用一个对象的方法,或者访问一个对象的字段,通常会传入对象实例。
有一个实例:
// Main.java
import com.itranswarp.learnjava.Person;
public class Main {
String getFullName(Person p) {
return p.getFirstName() + " " + p.getLastName();
}
}
但是,如果不能获得Person
类,只有一个Object
实例,比如这样:
String getFullName(Object obj) {
return ???
}
有很多人肯定以为强制转型。
强制转型的时候,你会发现一个问题:编译上面的代码,仍然需要引用Person
类。不然,去掉import
语句,你看能不能编译通过?
所以,反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。
1.2 Class类
实际上,除了int
等基本类型外,Java的其他类型全部都是class
(包括interface
)。例如:
String
Object
Runnable
Exception
- …
仔细思考,我们可以得出结论:class
(包括interface
)的本质是数据类型(Type
)。无继承关系的数据类型无法赋值。
而class
是由JVM在执行过程中动态加载的。JVM在第一次读取到一种class
类型时,将其加载进内存。
每加载一种class
,JVM就为其创建一个Class
类型的实例,并关联起来。注意:这里的Class
类型是一个名叫Class
的class
。它长这样:
public final class Class {
private Class() {}
}
以String
类为例,当JVM加载String
类时,它首先读取String.class
文件到内存,然后,为String
类创建一个Class
实例并关联起来:
Class cls = new Class(String);
这个Class
实例是JVM内部创建的,如果我们查看JDK源码,可以发现Class
类的构造方法是private
,只有JVM能创建Class
实例,我们自己的Java程序是无法创建Class
实例的。
所以,JVM持有的每个Class
实例都指向一个数据类型(class
或interface
)
如果获取了某个Class
实例,我们就可以通过这个Class
实例获取到该实例对应的class
的所有信息。因为JVM为每个加载的class
创建了对应的Class
实例,并在实例中保存了该class
的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等。
这种通过Class
实例获取class
信息的方法称为反射(Reflection)。
如何获取一个class
的Class
实例?有三个方法:
方法一:直接通过一个class
的静态变量class
获取:
Class cls = String.class;
方法二:如果我们有一个实例变量,可以通过该实例变量提供的getClass()
方法获取:
String s = "Hello";
Class cls = s.getClass();
方法三:如果知道一个class
的完整类名,可以通过静态方法Class.forName()
获取:
Class cls = Class.forName("java.lang.String");
因为Class
实例在JVM中是唯一的,所以,上述方法获取的Class
实例是同一个实例。可以用==
比较两个Class
实例:
Class cls1 = String.class;
String s = "Hello";
Class cls2 = s.getClass();
boolean sameClass = cls1 == cls2; // true
用instanceof
不但匹配指定类型,还匹配指定类型的子类。而用==
判断class
实例可以精确地判断数据类型,但不能作子类型比较。
注意到数组(例如String[]
)也是一种Class
,而且不同于String.class
,它的类名是[Ljava.lang.String
。此外,JVM为每一种基本类型如int也创建了Class
,通过int.class
访问。
1.3 访问字段
对任意的一个Object
实例,只要我们获取了它的Class
,就可以获取它的一切信息。
我们先看看如何通过Class
实例获取字段信息。Class
类提供了以下几个方法来获取字段:
- Field getField(name):根据字段名获取某个public的field(包括父类)
- Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)
- Field[] getFields():获取所有public的field(包括父类)
- Field[] getDeclaredFields():获取当前类的所有field(不包括父类)
我们看一个示例代码:
public class Main {
public static void main(String[] args) throws Exception {
Class stdClass = Student.class;
// 获取public字段"score":
System.out.println(stdClass.getField("score"));
// 获取继承的public字段"name":
System.out.println(stdClass.getField("name"));
// 获取private字段"grade":
System.out.println(stdClass.getDeclaredField("grade"));
}
}
class Student extends Person {
public int score;
private int grade;
}
class Person {
public String name;
}
上述代码首先获取Student
的Class
实例,然后,分别获取public
字段、继承的public
字段以及private
字段,打印出的Field
类似:
public int Student.score
public java.lang.String Person.name
private int Student.grade
一个Field
对象包含了一个字段的所有信息:
getName()
:返回字段名称,例如,"name"
;getType()
:返回字段类型,也是一个Class
实例,例如,String.class
;getModifiers()
:返回字段的修饰符,它是一个int
,不同的bit表示不同的含义。
以String
类的value
字段为例,它的定义是:
public final class String {
private final byte[] value;
}
如果我们想要获得字段值:
利用反射拿到字段的一个Field
实例只是第一步,我们还可以拿到一个实例对应的该字段的值。
例如,对于一个Person
实例,我们可以先拿到name
字段对应的Field
,再获取这个实例的name
字段的值
public class Main {
public static void main(String[] args) throws Exception {
Object p = new Person("Xiao Ming");
Class c = p.getClass();
Field f = c.getDeclaredField("name");
Object value = f.get(p);
System.out.println(value); // "Xiao Ming"
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
上述代码先获取Class
实例,再获取Field
实例,然后,用Field.get(Object)
获取指定实例的指定字段的值。
运行代码,如果不出意外,会得到一个IllegalAccessException
,这是因为name
被定义为一个private
字段,正常情况下,Main
类无法访问Person
类的private
字段。要修复错误,可以将private
改为public
,或者,在调用Object value = f.get(p);
前,先写一句:
f.setAccessible(true);
调用Field.setAccessible(true)
的意思是,别管这个字段是不是public
,一律允许访问。
而反射是一种非常规的用法,使用反射,首先代码非常繁琐,其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。
通过Field实例既然可以获取到指定实例的字段值,自然也可以设置字段的值。
设置字段值是通过Field.set(Object, Object)
实现的,其中第一个Object
参数是指定的实例,第二个Object
参数是待修改的值。我们还需要上述方法来获得相关的值。
1.4 方法调用
我们已经能通过Class
实例获取所有Field
对象,同样的,可以通过Class
实例获取所有Method
信息。Class
类提供了以下几个方法来获取Method
:
Method getMethod(name, Class...)
:获取某个public
的Method
(包括父类)Method getDeclaredMethod(name, Class...)
:获取当前类的某个Method
(不包括父类)Method[] getMethods()
:获取所有public
的Method
(包括父类)Method[] getDeclaredMethods()
:获取当前类的所有Method
(不包括父类)
我们看一个代码:
public class Main {
public static void main(String[] args) throws Exception {
Class stdClass = Student.class;
// 获取public方法getScore,参数为String:
System.out.println(stdClass.getMethod("getScore", String.class));
// 获取继承的public方法getName,无参数:
System.out.println(stdClass.getMethod("getName"));
// 获取private方法getGrade,参数为int:
System.out.println(stdClass.getDeclaredMethod("getGrade", int.class));
}
}
class Student extends Person {
public int getScore(String type) {
return 99;
}
private int getGrade(int year) {
return 1;
}
}
class Person {
public String getName() {
return "Person";
}
}
结果是:
Note: Main.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
public int Student.getScore(java.lang.String)
public java.lang.String Person.getName()
private int Student.getGrade(int)
一个Method
对象包含一个方法的所有信息:
getName()
:返回方法名称,例如:"getScore"
;getReturnType()
:返回方法返回值类型,也是一个Class实例,例如:String.class
;getParameterTypes()
:返回方法的参数类型,是一个Class数组,例如:{String.class, int.class}
;getModifiers()
:返回方法的修饰符,它是一个int
,不同的bit表示不同的含义。
调用方法:
其他详情就看这里:https://www.liaoxuefeng.com/wiki/1252599548343744/1264803678201760
1.5 构造方法的调用
我们通常使用new
操作符创建新的实例:
Person p = new Person();
如果通过反射来创建新的实例,可以调用Class提供的newInstance()方法:
Person p = Person.class.newInstance();
调用Class.newInstance()的局限是,它只能调用该类的public无参数构造方法。如果构造方法带有参数,或者不是public,就无法直接通过Class.newInstance()来调用。
为了调用任意的构造方法,Java的反射API提供了Constructor对象,它包含一个构造方法的所有信息,可以创建一个实例。
强大之处在于不仅领域返回一个方法,调用结果总是返回实例。
public class Main {
public static void main(String[] args) throws Exception {
// 获取构造方法Integer(int):
Constructor cons1 = Integer.class.getConstructor(int.class);
// 调用构造方法:
Integer n1 = (Integer) cons1.newInstance(123);
System.out.println(n1);
// 获取构造方法Integer(String)
Constructor cons2 = Integer.class.getConstructor(String.class);
Integer n2 = (Integer) cons2.newInstance("456");
System.out.println(n2);
}
}
结果是:123 456
通过Class实例获取Constructor的方法如下:
getConstructor(Class...)
:获取某个public
的Constructor
;getDeclaredConstructor(Class...)
:获取某个Constructor
;getConstructors()
:获取所有public
的Constructor
;getDeclaredConstructors()
:获取所有Constructor
。
注意Constructor
总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。
1.6 继承关系获取
有了继承关系,我们在getClass方法里面还可以获得父类信息:
public class Main {
public static void main(String[] args) throws Exception {
Class i = Integer.class;
Class n = i.getSuperclass();
System.out.println(n);
Class o = n.getSuperclass();
System.out.println(o);
System.out.println(o.getSuperclass());
}
}
我们采用getSuperClass方法进行获取。
class java.lang.Number
class java.lang.Object
null
运行上述代码,可以看到,Integer
的父类类型是Number
,Number
的父类是Object
,Object
的父类是null
。除Object
外,其他任何非interface
的Class
都必定存在一个父类类型。
接口的获取:
由于一个类可能实现一个或多个接口,通过Class
我们就可以查询到实现的接口类型。例如,查询Integer
实现的接口:
public class Main {
public static void main(String[] args) throws Exception {
Class s = Integer.class;
Class[] is = s.getInterfaces();
for (Class i : is) {
System.out.println(i);
}
}
}
运行上述代码可知,Integer
实现的接口有:
- java.lang.Comparable
- java.lang.constant.Constable
- java.lang.constant.ConstantDesc
要特别注意:getInterfaces()
只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型
继承关系:
我们在判断是否继承的情况下,我们看看:
Object n = Integer.valueOf(123);
boolean isDouble = n instanceof Double; // false
boolean isInteger = n instanceof Integer; // true
boolean isNumber = n instanceof Number; // true
boolean isSerializable = n instanceof java.io.Serializable; // true
如果是两个Class
实例,要判断一个向上转型是否成立,可以调用isAssignableFrom()
:
// Integer i = ?
Integer.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Integer
// Number n = ?
Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number
// Object o = ?
Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object
// Integer i = ?
Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer
2.注解
2.1 基本概念
本质上来说 Annotation(注解) 是 Java 推出的一种注释机制,后面我们统一叫 Annotation,和普通的注释有个显著的特别,那就是 Annotation 是可以在编译、运行阶段读取的,注释很明显不可以。
能够在编译、运行阶段读取信息,这就给了我们很多的扩展空间,而且不会污染源代码。
我们看一个实例:
// this is a component:
@Resource("hello")
public class Hello {
@Inject
int n;
@PostConstruct
public void hello(@Param String name) {
System.out.println(name);
}
@Override
public String toString() {
return "Hello";
}
}
Java的注解可以分为三类:
第一类是由编译器使用的注解,例如:
@Override
:让编译器检查该方法是否正确地实现了覆写;@SuppressWarnings
:告诉编译器忽略此处代码产生的警告。
这类注解不会被编译进入.class
文件,它们在编译后就被编译器扔掉了。
第二类是由工具处理.class
文件使用的注解,比如有些工具会在加载class的时候,对class做动态修改,实现一些特殊的功能。这类注解会被编译进入.class
文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。
第三类是在程序运行期能够读取的注解,它们在加载后一直存在于JVM中,这也是最常用的注解。例如,一个配置了@PostConstruct
的方法会在调用构造方法后自动被调用(这是Java代码读取该注解实现的功能,JVM并不会识别该注解)。
定义一个注解时,还可以定义配置参数。配置参数可以包括:
- 所有基本类型;
- String;
- 枚举类型;
- 基本类型、String、Class以及枚举的数组。
因为配置参数必须是常量,所以,上述限制保证了注解在定义时就已经确定了每个参数的值。
如果只写了注解,都采用默认值。
注解(Annotation)是Java语言用于工具处理的标注:
注解可以配置参数,没有指定配置的参数使用默认值;
2.2 注解定义
Java语言使用@interface
语法来定义注解(Annotation
),它的格式如下:
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
注解的参数类似无参数方法,可以用default
设定一个默认值(强烈推荐)。最常用的参数应当命名为value
。
元注解:
@Target
最常用的元注解是@Target
。使用@Target
可以定义Annotation
能够被应用于源码的哪些位置:
- 类或接口:
ElementType.TYPE
; - 字段:
ElementType.FIELD
; - 方法:
ElementType.METHOD
; - 构造方法:
ElementType.CONSTRUCTOR
; - 方法参数:
ElementType.PARAMETER
。
例如,定义注解@Report
可用在方法上,我们必须添加一个@Target(ElementType.METHOD)
:
如果想要同时作用于类和方法上,那么你就可以
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention
另一个重要的元注解@Retention
定义了Annotation
的生命周期:
- 仅编译期:
RetentionPolicy.SOURCE
; - 仅class文件:
RetentionPolicy.CLASS
; - 运行期:
RetentionPolicy.RUNTIME
。
如果@Retention
不存在,则该Annotation
默认为CLASS
。因为通常我们自定义的Annotation
都是RUNTIME
,所以,务必要加上@Retention(RetentionPolicy.RUNTIME)
这个元注解
@Repeatable
使用@Repeatable
这个元注解可以定义Annotation
是否可重复。这个注解应用不是特别广泛。
@Inherited
使用@Inherited
定义子类是否可继承父类定义的Annotation
。
如何定义Annotation
我们总结一下定义Annotation
的步骤:
第一步,用@interface
定义注解:
public @interface Report {
}
第二步,添加参数、默认值:
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
把最常用的参数定义为value()
,推荐所有参数都尽量设置默认值。
第三步,用元注解配置注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
其中,必须设置@Target
和@Retention
,@Retention
一般设置为RUNTIME
,因为我们自定义的注解通常要求在运行期读取。一般情况下,不必写@Inherited
和@Repeatable
。
2.3 注解处理
Java的注解本身对代码逻辑没有任何影响。根据@Retention
的配置:
SOURCE
类型的注解在编译期就被丢掉了;CLASS
类型的注解仅保存在class文件中,它们不会被加载进JVM;RUNTIME
类型的注解会被加载进JVM,并且在运行期可以被程序读取。
如何使用注解完全由工具决定。SOURCE
类型的注解主要由编译器使用,因此我们一般只使用,不编写。CLASS
类型的注解主要由底层工具库使用,涉及到class的加载,一般我们很少用到。只有RUNTIME
类型的注解不但要使用,还经常需要编写。
相关文档:https://www.liaoxuefeng.com/wiki/1252599548343744/1265102026065728