JavaSE - 反射-注解
本节学习目标:
- 了解注解的概念;
- 了解并掌握注解的使用方式;
- 了解并掌握Annotation接口及JDK元注解;
- 了解并掌握自定义注解的编写方式;
- 了解并掌握如何使用反射处理注解;
- 了解JDK提供的一些注解。
1. 注解概述
1.1 注解简介
从JDK5开始,Java增加对元数据的支持,也就是注解,注解与注释是有一定区别的,可以把注解理解为代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。
注解 - 百度百科
Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。Java 语言中的类、方法、变量、参数和包等都可以被标注。和 Javadoc 不同,Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容 。 当然它也支持自定义 Java 标注。
Java 注解(Annotation)- 菜鸟教程
注解(Annotation)又称标注,是JDK1.5引入的一种新的注释机制。
注解其实就是代码里的前缀为@
的特殊标记(如@Override
),注解是一种接口。注解可以在编译,类加载,运行时被读取并执行相应的处理。通过使用注解,用户可以在不改变原有逻辑的情况下,
在源码中嵌入一些补充信息,代码分析工具,开发工具或者部署工具可以通过这些信息进行验证或者进行部署。
1.2 注解的作用
注解可以像修饰符一样被使用,可以用于修饰包、类、方法、变量、参数、甚至返回值,这些信息被保存到注解内部的键值对中。
在进行JavaSE开发中,注解的使用目的较为简单,例如标记过时的功能、忽略一些警告等。在进行JavaEE开发或者Android开发中,注解扮演了更重要的角色,例如用来配置应用程序的某些功能,以代替JavaEE传统的基于XML的配置和冗余代码。
未来的Java开发模式都是基于注解的,比如JPA规范,Spring框架,Hibernate框架,Struts2框架等都有注解功能。注解是一种趋势,一定程度上可以说:
框架 = 注解 + 反射 + 设计模式
2. 注解的使用方式
以JDK提供的三个基本注解@Override
、@Deprecated
和@SuppressWarnings
演示注解的使用方式:
2.1 @Override
当一个方法上标注了@Override
注解时,表示这个方法重写了父类或者接口中的方法:
class Person {
public void eat() {
System.out.println("人正在吃东西");
}
}
interface Studdable {
void study();
}
public class Student extends Person implements Studdable {
@Override // 重写了父类Person的方法
public void eat() {
System.out.println("学生正在吃东西");
}
@Override // 实现了接口Studdable的方法
public void study() {
System.out.println("学生正在学习");
}
@Override // 重写了父类Object的方法
public String toString() {
return "我是一个学生";
}
}
@Override
注解不会改变任何功能,它的作用是告诉编译器检查被标注的方法是否重写自父类或者接口中的方法。如果给一个非重写的方法强制标注上@Override
注解,此时编译器会报错:
public class Test {
@Override
public void test() {
}
}
尝试编译,编译器报错:
2.2 @Deprecated
被注解@Deprecated
标注的结构(类,接口,方法,变量等)都为过时的结构,表示不再推荐使用:
class Human {
@Deprecated
public void crawl() {
System.out.println("人爬行行走");
}
public void run() {
System.out.println("人站立行走");
}
}
public class Test {
public static void main(String[] args) {
Human human = new Human();
human.crawl();
human.run();
}
}
尝试编译,编译成功,运行结果:
实际操作可以发现被注解@Deprecated
标注的方法仍可使用,但编译时会发出警告,提示使用了过时的方法。@Deprecated
注解也不会改变任何功能,只是提示用户此方法因为某些原因(如不安全,或者有更好的替代方法)不推荐继续使用。但是为了兼容性,保证使用此方法的代码不受影响,因此选择标注@Deprecated
注解而不移除。
2.3 @SuppressWarnings
注解@SuppressWarnings
用来抑制编译器的一些警告,它需要提供一个参数:
参数 | 类型 | 说明 |
---|---|---|
value | String[] | 抑制的警告类型,可以多选 |
可以抑制的一些警告类型(完整列表参见:Excluding warnings using @SuppressWarnings - eclipse ):
警告类型 | 说明 |
---|---|
all | 所有警告 |
deprecation | 结构过时警告 |
rawtypes | 使用带有泛型的原始类型警告 |
serial | 序列化类未提供serialVersionUID 警告 |
unchecked | 调用原始类型结构警告 |
unused | 结构未使用警告 |
可以使用注解@SuppressWarnings
抑制2.2章节中@Deprecated
注解产生的编译时警告:
class Human {
@Deprecated
public void crawl() {
System.out.println("人爬行行走");
}
public void run() {
System.out.println("人站立行走");
}
}
public class Test {
@SuppressWarnings("deprecation")
public static void main(String[] args) {
Human human = new Human();
human.crawl();
human.run();
}
}
尝试编译,编译成功,运行结果:
使用了注解@SuppressWarnings
后,编译器就不会输出警告信息。
3. Annotation 接口
Annotation
接口位于java.lang.annotation
包下,它是所有注解的父接口,定义了注解接口中的基本方法:
方法 | 返回值类型 | 功能 |
---|---|---|
equals(Object obj) | boolean | 继承自Object 类,比较自身与对象obj 是否相同 |
hashCode() | int | 继承自Object 类,返回当前对象的哈希值 |
toString() | String | 继承自Object 类,返回当前对象的字符串形式 |
annotationType() | Class<? extends Annotation> | 返回当前注解的类型 |
所有注解都直接或间接继承了Annotation
接口。
4. 元注解
Java为注解接口提供了五种元注解(Meta annotation),元注解被称为注解中的注解,它们定义了注解的特性与使用方式。
4.1 @Retention
元注解@Retention
用来约束注解的保留规则,指定注解可以保留多长时间。它需要提供的参数:
参数 | 类型 | 说明 |
---|---|---|
value | RetentionPolicy | 注解的保留规则 |
RetentionPolicy
是一个枚举类,位于java.lang.annotation
包下,它定义的常量规定了注解的保留规则:
RetentionPolicy.SOURCE
:
注解信息只保留在源码中,编译时编译器会去除;RetentionPolicy.CLASS
:
注解信息会保留在字节码文件中,但在运行期间Java虚拟机无法访问,如果一个注解未使用@Retention
注解,默认使用此保留规则;RetentionPolicy.RUNTIME
:
注解信息会保留在字节码文件中,在运行期间Java虚拟机可以访问,可以利用反射获取注解信息。
4.2 @Target
元注解@Target
用来约束注解的标注位置,指定注解只能标注在哪些位置,它需要提供的参数:
参数 | 类型 | 说明 |
---|---|---|
value | ElementType[] | 注解的标注位置 |
ElementType
是一个枚举类,位于java.lang.annotation
包下,它的定义的常量规定了注解的标注位置:
常量 | 说明 |
---|---|
ElementType.TYPE | 注解可标注在类、接口(注解)、枚举类上 |
ElementType.FIELD | 注解可标注在成员变量(包括枚举类的常量)上 |
ElementType.METHOD | 注解可标注在方法上 |
ElementType.PARAMETER | 注解可标注在方法的参数上 |
ElementType.CONSTRUCTOR | 注解可标注在构造方法上 |
ElementType.LOCAL_VARIABLE | 注解可标注在局部变量上 |
ElementType.ANNOTATION_TYPE | 注解可标注在其他注解上(使被标注的注解成为元注解,事实上元注解的标注位置都为此值) |
ElementType.PACKAGE | 注解可标注在包上(在package-info.java中使用,以运行javadoc 命令时对包输出注释说明) |
ElementType.TYPE_PARAMETER | JDK1.8提供,注解可标注在泛型的声明上 |
ElementType.TYPE_USE | JDK1.8提供,注解可标注在任何使用类型的地方 |
编写代码进行测试:
import static java.lang.annotation.ElementType.*;
import java.lang.annotation.Target;
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE, TYPE_PARAMETER, TYPE_USE})
@interface Type {
String value();
}
@Type("ANNOTATION_TYPE,注解")
@interface Anno {
}
@Type("TYPE,类")
public class Test {
@Type("FIELD,成员变量")
private String field;
@Type("FIELD,静态变量")
public static String staticField = "static";
@Type("FIELD,静态常量")
public static final String constField = "const";
@Type("CONSTRUCTOR,构造方法")
public Test(@Type("PARAMETER,参数") String field) {
this.field = field;
}
@Type("METHOD,成员方法")
public void foo() {
@Type("LOCAL_VARIABLE,局部变量")
String local = "local";
}
@Type("METHOD,静态方法")
public static void staticFoo() throws @Type("TYPE_USE,使用类型") Exception {
Object o = 15;
Integer i = (@Type("TYPE_USE,使用类型") Integer) o;
}
public <@Type("TYPE_PARAMETER,泛型声明") T> void parameterTypeFoo() {
}
}
4.3 @Documented
被元注解@Documented
标注的注解,在使用javadoc
工具时会被提取成文档。
比如注解@Deprecated
,它内部被元注解@Documented
标注:
// java.lang.Deprecated
// Deprecated.java jdk1.8.0_202 Line:41~45
@Documented // 被@Documented元注解标注
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
使用注解@Deprecated
标注的结构,经过javadoc
工具生成文档后,@Documented
注解也会被记录在文档里面。
编写代码进行测试:
public class Test {
@Deprecated
public void foo() {
System.out.println("测试方法");
}
}
运行命令javadoc Test.java
,生成javadoc文档,在文档中可看到此方法被@Deprecated
注解标注,同时指明已过时:
注意:被元注解@Documented
标注的注解,它的保留规则必须为RetentionPolicy.RUNTIME
。
4.4 @Inherited
被元注解@Inherited
标注的注解会具有继承性,如果一个类被标注了@Inherited
元注解的注解标注,那么它的子类将自动被它所标注的注解标注。
编写代码进行测试:
import java.lang.annotation.*;
import java.util.Arrays;
@Inherited // 自定义注解@MyAnno被元注解@Inherited标注
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface MyAnno { // 自定义注解@MyAnno
}
@MyAnno
class A { // 父类A被@MyAnno注解标注
}
public class B extends A { // 子类B继承与A,同时B没有被任何注解标注
public static void main(String[] args) {
// 获取B标注的所有注解
System.out.println(Arrays.toString(B.class.getAnnotations()));
}
}
/* 运行结果:
[@MyAnno()]
*/
分析结果,类B
虽然没有被任何注解标注,但它继承于类A
,A
被被标注了@Inherited
元注解的注解@MyAnno
标注,所以类B
也自动被@MyAnno
注解标注。
4.5 @Repeatable
在JDK1.8中,Java提供了第五个元注解@Repeatable
。被元注解@Repeatable
标注的注解称为可重复注解,可以在同一个地方重复标注。它需要提供的参数:
参数 | 类型 | 说明 |
---|---|---|
value | Class<? extends Annotation> | 指定存放重复标注的容器注解 |
举个例子,一个人在生活中可能会担任很多角色,比如学生,儿子,同学等等,编写一个@Role
注解以标注Person
类:
@interface Role {
String value();
}
@Role("学生")
@Role("儿子")
@Role("同学")
public class Person {
}
但是@Role
注解不能在同一个位置使用两次以上,尝试进行编译,编译器报错:
在JDK1.8之前,一般是再编写一个容器注解,用来存放需要重复出现的注解:
@interface Role {
String value();
}
@interface Roles { // 容器注解
Role[] value(); // @Role注解数组,存放重复出现的@Role注解
}
@Roles({@Role("学生"), @Role("儿子"), @Role("同学")})
public class Person {
}
JDK1.8中Java提供了元注解@Repeatable
,只需要在@Role
注解上标注@Repeatable
元注解并指定容器注解为@Roles
,就可以重复使用了:
@Repeatable(Roles.class) // 指定容器注解
@interface Role {
String value();
}
@interface Roles { // 容器注解
Role[] value(); // @Role注解数组,存放重复出现的@Role注解
}
@Role("学生") // 此处就可以重复使用@Role注解了
@Role("儿子")
@Role("同学")
public class Person {
}
注意:
- 容器注解的保留规则必须比可重复注解的保留规则更宽松(比如可重复注解
@Role
的保留规则为RetentionPolicy.CLASS
,容器注解@Roles
的保留规则只能为RetentionPolicy.CLASS
或RetentionPolicy.RUNTIME
,不能为RetentionPolicy.SOURCE
); - 可重复注解的标注位置必须包含容器注解的标注位置(比如可重复注解
@Role
的标注位置为类或方法上,则容器注解@Roles
的标注位置只能在类或方法上)。 - 容器注解和可重复注解要么都被
@Inherited
元注解标注,要么都不被标注。
5. 自定义注解的编写
以上章节已经涉及到了自定义注解的编写,在此详细说明。
注解实质上是一种接口,使用字符@
加关键字interface
进行定义:
public @interface MyAnno {
}
在注解中定义成员变量以在接口中编写方法的写法进行声明:
public @interface MyAnno {
String str(); // String类型成员str
int[] arr(); // int[]类型成员arr
}
// 使用此注解时要对其内部成员进行赋值:
// @MyAnno(str = "HelloWorld", arr = {1, 2, 3})
注意:注解中成员变量的数据类型只能为以下其一:
- 基本数据类型(
int
、double
等); - 字符串类型(
String
); Class
类型(Class<?>
);- 枚举类型(
<T extends Enum<T>> T
,即enum
类型); - 注解类型(
<T extends Annotation> T
,即@interface
类型); - 以上类型的数组类型。
注解中的成员变量可以使用default
关键字设置缺省值:
public @interface MyAnno {
String str() default "HelloWorld";
int[] arr() default {1, 2, 3};
}
// 使用此注解时可以对其内部成员进行赋值,不赋值的话其值会使用缺省值
之后可以使用Java提供的五个元注解对自定义注解进行约束。
6. 反射处理注解
用来描述结构的Java反射类(如Class
类描述类或接口,Constructor
类描述构造方法,Field
类描述变量等)绝大多数都实现了AnnotatedElement
接口。
它位于java.lang.reflect
包下,用来定义使用反射处理注解的基本方法:
方法 | 返回值类型 | 功能 |
---|---|---|
getAnnotation(Class<T> annotationClass) | <T extends Annotation> T | 返回标注此结构的指定注解(包括继承的注解), 若不存在返回 null |
getAnnotations() | Annotation[] | 返回标注此结构的全部注解(包括继承的注解) |
getDeclaredAnnotation(Class<T> annotationClass) | <T extends Annotation> T | 返回标注此结构的指定注解,若不存在返回null |
getDeclaredAnnotations() | Annotation[] | 返回标注此结构的全部注解 |
isAnnotationPresent(Class<? extends Annotation> annotationClass) | boolean | 返回此结构是否被指定注解标注,是返回true ,否返回false |
举个例子:编写一个注解@Hello
,使用动态代理实现调用被@Hello
注解标注的方法时输出一条欢迎语句。
编写@Hello
注解:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Hello {
}
编写方法接口Helloable
:
public interface Helloable {
void foo();
void foo2();
}
编写两个接口实现类,每个类对不同的方法标注@Hello
注解:
class HelloClass implements Helloable {
@Hello
@Override
public void foo() {
System.out.println("执行了HelloClass中的foo()方法");
}
@Override
public void foo2() {
System.out.println("执行了HelloClass中的foo2()方法");
}
}
class HelloClass2 implements Helloable {
@Override
public void foo() {
System.out.println("执行了HelloClass2中的foo()方法");
}
@Hello
@Override
public void foo2() {
System.out.println("执行了HelloClass2中的foo2()方法");
}
}
编写代理工厂类ProxyFactory
,并进行测试:
public class ProxyFactory {
public static Object newProxyInstance(Object subject) {
ClassLoader loader = subject.getClass().getClassLoader();
Class<?>[] interfaces = subject.getClass().getInterfaces();
InvocationHandler handler = methodEnhance(subject);
return Proxy.newProxyInstance(loader, interfaces, handler);
}
private static InvocationHandler methodEnhance(Object subject) {
return (proxy, method, args) -> {
// 获取被代理对象的方法
Method method1 = subject.getClass().getMethod(method.getName(), method.getParameterTypes());
// 判断被代理对象的方法有没有被@Hello注解标注
if (method1.isAnnotationPresent(Hello.class)) {
// 如果有则输出欢迎语句
System.out.println("HelloWorld");
}
return method.invoke(subject, args);
};
}
public static void main(String[] args) {
Helloable hello1 = (Helloable) ProxyFactory.newProxyInstance(new HelloClass());
hello1.foo();
hello1.foo2();
Helloable hello2 = (Helloable) ProxyFactory.newProxyInstance(new HelloClass2());
hello2.foo();
hello2.foo2();
}
}
运行结果:
HelloWorld
执行了HelloClass中的foo()方法
执行了HelloClass中的foo2()方法
执行了HelloClass2中的foo()方法
HelloWorld
执行了HelloClass2中的foo2()方法
编写自定义注解后一定要编写处理注解的逻辑,不然标注的注解将毫无意义。
7. JDK提供的其他常用注解
在2章节已经介绍了@Override
、@Deprecated
和@SuppressWarnings
注解,本节介绍JDK提供的其他常用注解。
7.1 @SafeVarargs
在声明具有模糊类型(比如Object
类型或者泛型)的可变参数的构造方法或方法时,编译器会发出警告。
如果用户可以确定方法体不会对其可变参数造成不安全的操作的话,可以使用@SafeVarargs
注解标注,以抑制编译器警告。类似被@SuppressWarnings("unchecked")
标注。
编写代码进行测试:
public class TestSafeVarargs<T> {
@SafeVarargs // 构造方法使用泛型类型的可变参数
public TestSafeVarargs(T... args) {
for (T t : args) {
System.out.println(t.toString());
}
}
@SafeVarargs // 静态方法使用Object类型的可变参数
public static void printVarargs(Object... args) {
for (Object t : args) {
System.out.println(t.toString());
}
}
@SafeVarargs // final方法使用泛型类型的可变参数
public final void printVarargs1(T... args) {
for (T t : args) {
System.out.println(t.toString());
}
}
}
@SafeVarargs
注解只能用在有可变参数的方法或构造方法上,且方法必须声明为static
或final
,否则会出现编译错误。一个方法使用@SafeVarargs
注解的前提是,用户必须确保这个方法的实现中对泛型类型参数的处理不会引发类型安全问题。
7.2 @FunctionalInterface 与 Lambda 表达式
在JDK1.8中Java提供了@FunctionalInterface
注解,用于标识函数式接口(Functional Interface)。
函数式接口指有且仅有一个抽象方法的接口,但是可以有多个非抽象方法,也可以有带有默认实现的抽象方法。
注解@FunctionalInterface
可以标注在函数式接口上,以便编译器检查是否为函数式接口:
@FunctionalInterface
interface MyFunctionalInterface1 {
void foo();
}
@FunctionalInterface
interface MyFunctionalInterface2 {
void foo(String param);
}
在JDK1.8之前,实现函数式接口主要是使用匿名内部类的形式:
public class Test {
public static void main(String[] args) {
MyFunctionalInterface1 interface1 = new MyFunctionalInterface1() {
@Override
public void foo() {
System.out.println("无参函数式接口");
}
};
interface1.foo();
MyFunctionalInterface2 interface2 = new MyFunctionalInterface2() {
@Override
public void foo(String param) {
System.out.println(param);
}
};
interface2.foo("有参函数式接口");
}
}
JDK1.8引入了注解@FunctionalInterface
注解和Lambda表达式的概念,使得函数式接口可以使用更简洁的实现方式——Lambda表达式:
public class Test {
public static void main(String[] args) {
MyFunctionalInterface1 interface1 = () -> System.out.println("无参函数式接口");
interface1.foo();
MyFunctionalInterface2 interface2 = param -> System.out.println(param);
interface2.foo("有参函数式接口");
}
}
对于无参抽象方法的函数式接口,使用Lambda表达式实现:
// 如果方法体只有一条语句
() -> 语句;
// 如果方法体有多条语句
() -> {
// 语句块
};
对于有参抽象方法的函数式接口,使用Lambda表达式实现:
// 如果只有一个参数
形参 -> 语句;
// 如果有多个参数
(形参1, 形参2, ...) -> 语句;
用户使用Lambda表达式来简化匿名方法,但有些情况下,我们使用Lambda表达式仅仅是调用一些已经存在的方法,除了调用这些方法之外不做其他操作。在这种情况下,Lambda表达式可以进一步简化为方法引用。使用双冒号(::
)。
方法引用有四大分类:
引用类型 | 语法 | 对应的Lambda表达式 |
---|---|---|
静态方法引用 | 类名::方法名 | (参数) -> 类名.方法名(参数) |
实例方法引用 | 对象名::方法名 | (参数) -> 对象名.方法名(参数) |
对象方法引用 | 类名::方法名 | (对象名, 参数) -> 类名.方法名(参数) |
构造方法引用 | 类名::new | (参数) -> new 类名(参数) |
- 静态方法引用:如果Lambda表达式的代码块中仅有一条语句,且语句调用了一个类的静态方法时,可以使用双冒号替换为静态方法引用:
@FunctionalInterface
interface I {
void foo();
}
public class Test {
public static void main(String[] args) {
// 匿名内部类写法
I i1 = new I() {
@Override
public void foo() {
System.gc();
}
};
// Lambda表达式写法
I i2 = () -> System.gc();
// 方法引用写法
I i3 = System::gc;
}
}
- 实例方法引用:如果Lambda表达式的代码块中仅有一条语句,且语句调用了一个对象的成员方法时,可以使用双冒号替换为实例方法引用:
@FunctionalInterface
interface I {
void foo();
}
public class Test {
public static void main(String[] args) {
String str = new String();
// 匿名内部类写法
I i1 = new I() {
@Override
public void foo() {
str.toString();
}
};
// Lambda表达式写法
I i2 = () -> str.toString();
// 方法引用写法
I i3 = str::toString;
}
}
- 对象方法引用:如果Lambda表达式的第一个参数是方法调用者,而第二个参数为方法的参数时,可以使用双冒号替换为对象方法引用:
@FunctionalInterface
interface I {
void foo(String a, String b);
}
public class Test {
public static void main(String[] args) {
// 匿名内部类写法
I i1 = new I() {
@Override
public void foo(String a, String b) {
a.compareTo(b);
}
};
// Lambda表达式写法
I i2 = (a, b) -> a.compareTo(b);
// 方法引用写法
I i3 = String::compareTo;
}
}
- 构造方法引用:如果Lambda表达式的代码块中仅有一条语句,且语句调用了一个类的构造方法时,可以使用双冒号替换为构造方法引用:
- 需要调用的构造方法的参数列表要与函数式接口中的抽象方法的参数列表一致。
@FunctionalInterface
interface I {
String foo(String a);
}
public class Test {
public static void main(String[] args) {
// 匿名内部类写法
I i1 = new I() {
@Override
public String foo(String a) {
return new String(a);
}
};
// Lambda表达式写法
I i2 = a -> new String(a);
// 方法引用写法
I i3 = String::new;
}
}