一、注解入门
注解:Annotation
- 从JDK 1.5 引入
- 位于源码中(代码/注释/注解),使用其他工具进行处理的标签
- 注解用来修饰程序的元素,但不会对被修饰的对象有直接的影响
- 只有通过某种配套的工具才会对注解信息进行访问和处理
主要用途
- 提供信息给编译器/IDE工具
- 可用于其他工具来产生额外的代码/配置文件等
- 有一些注解可在程序运行时访问,增加程序的动态性
1.1常见普通注解
1.2常见元注解
1.3自定义注解
二、Java预定义的普通注解
Java预定义的普通注解是一些用于修饰类、方法、字段等元素的注解,它们可以提供一些编译器或运行时的信息,例如检查重写方法是否正确、标记过时的元素、忽略警告信息等。Java预定义的普通注解有以下几种:
- @Override:用于修饰方法,表示该方法重写了父类或接口中的方法,如果没有匹配的父类或接口方法,编译器会报错。
- @Deprecated:用于修饰类、方法、字段等元素,表示该元素已经过时,不建议使用,如果使用该元素,编译器会发出警告。
- @SuppressWarnings:用于修饰类、方法、字段等元素,表示忽略指定的编译器警告,可以指定一个或多个警告类型。
- @SafeVarargs:用于修饰可变参数的方法或构造器,表示该方法或构造器不会对泛型参数进行不安全的操作,可以忽略堆污染警告。
- @FunctionalInterface:用于修饰函数式接口,表示该接口只有一个抽象方法,可以用lambda表达式实现。
2.1@Override
@Override注解是一种用于表示方法重写的注解,它可以让编译器检查子类的方法是否正确地覆盖了父类或接口中的方法,如果没有匹配的父类或接口方法,编译器会报错。@Override注解可以提高代码的可读性和可维护性,也可以避免一些潜在的错误。
在子类中重写父类的方法,使用@Override注解标明
class Animal {
public void eat() {
System.out.println("Animal eats");
}
}
class Dog extends Animal {
@Override // 表示重写父类的eat方法
public void eat() {
System.out.println("Dog eats");
}
}
在实现接口的类中重写接口的方法,使用@Override注解标明
interface Shape {
public void draw();
}
class Circle implements Shape {
@Override // 表示重写接口的draw方法
public void draw() {
System.out.println("Draw a circle");
}
}
2.2@Deprecated
@Deprecated注解是一种用于标记已经过时的类、方法、字段等元素的注解,它可以让编译器在使用该元素时发出警告,提醒开发者不要再使用该元素,而是使用新的替代方案
package org.example;
class Animal {
@Deprecated
public void makeSound() {
System.out.println("This method is deprecated.");
}
public void communicate() {
System.out.println("Animals communicate in various ways.");
}
}
public class Test {
public static void main(String[] args) {
Animal animal = new Animal();
// 使用已过时的方法,会生成警告
animal.makeSound();
// 使用新方法
animal.communicate();
}
}
2.3@SuppressWarnings
@SuppressWarnings注解是一种用于忽略指定的编译器警告的注解,它可以用于修饰类、方法、字段等元素,可以指定一个或多个警告类型。
- 压制各种不同类型的警告信息,使得编译器不显示警告
- 各种不同类型是叠加,如修饰类的警告类型,和修饰方法的警告类型,对于方法来说,是叠加的。
- 警告类型名称是编译器/IDE工具自己定的,Java规范没有强制要求哪些名称。编译器厂商需要自行协商,保证同名警告类型在各个编译器.上一样工作。
package org.example;
import java.util.ArrayList;
import java.util.List;
public class SuppressWarningsExample {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
// 创建一个泛型列表,不指定泛型类型
List myList = new ArrayList();
myList.add("Item 1");
myList.add("Item 2");
// 使用 @SuppressWarnings 抑制未经检查的警告
List<String> stringList = myList;
// 不会再生成未经检查的警告
System.out.println(stringList.get(0));
System.out.println(stringList.get(1));
}
}
2.4@SafeVarargs
@SafeVarargs是一个用于抑制编译器警告的注解,它可以用于修饰使用可变参数(varargs)的方法或构造器,表示该方法或构造器不会对其可变参数执行潜在的不安全的操作,例如将其转换为不同的类型或返回给外部。@SafeVarargs注解可以提高代码的可读性和可维护性,也可以避免一些潜在的错误。
@SafeVarargs注解在Java 7中引入,最初只能用于修饰final或static的方法或构造器,因为这些方法或构造器不能被重写,从而保证了安全性。但是在Java 9中,@SafeVarargs注解也可以用于修饰private的实例方法,因为这些方法也不能被重写。
2.5@FunctionalInterface
@FunctionalInterface是一个用于标记函数式接口的注解,它表示该接口只有一个抽象方法,可以用lambda表达式或方法引用来实现。函数式接口是Java 8引入的一种新特性,它可以让代码更简洁、清晰和优雅。函数式接口的一些例子有Runnable、ActionListener、Comparable等。@FunctionalInterface注解可以让编译器检查接口是否符合函数式接口的定义,如果不符合,编译器会报错。@FunctionalInterface注解也可以提高代码的可读性,明确表达接口的用途。
package org.example;
@FunctionalInterface // 表示这是一个函数式接口
interface Adder {
int add(int a, int b); // 定义一个抽象方法add
}
public class Test {
public static void main(String[] args) {
Adder adder = (a, b) -> a + b; // 使用lambda表达式创建Adder接口的实例
System.out.println(adder.add(10, 20)); // 调用add方法,输出30
}
}
三、自定义注解
3.1注解可以包括的类型
3.2使用步骤
自定义注解的基本流程是:第一步,定义注解,使用@interface语法,并指定元注解(Annotation的注解)来配置注解的作用域和策略;第二步,使用注解,把注解标记在需要用到的程序代码中;第三步,解析注解,在编译期或运行时检测到注解,并进行特殊操作
3.3特殊
- 注解可以使用元注解来指定其作用域和策略,例如@Target指定注解可以用在哪些元素上,@Retention指定注解在什么时候有效,@Documented指定注解是否包含在用户文档中,@Inherited指定注解是否可以被子类继承,@Repeatable指定注解是否可以在同一个元素上重复使用,例如:
@Target(ElementType.METHOD) // 表示该注解只能用于方法上 @Retention(RetentionPolicy.RUNTIME) // 表示该注解在运行时有效 @Documented // 表示该注解会被javadoc工具记录 @Inherited // 表示该注解可以被子类继承 @Repeatable(MyAnnotations.class) // 表示该注解可以重复使用,需要指定一个容器注解来存放重复的注解 public @interface MyAnnotation { // 注解内容 }
3.4相关代码
注解代码
package org.example;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
//表示该注解会保留在class文件中
@Target(ElementType.METHOD)
//表示该注解只能用于方法
public @interface MultipleTest {
int a() default 0;
int b() default 0;
}
测试代码
package org.example;
public class Foo {
@MultipleTest(a=1,b=1)
public static void m1(int a, int b) {
if (a + b < 0) {
throw new RuntimeException("Crash");
}
}
@MultipleTest
public static void m2(int a, int b) {
//全部采用默认值
if (a + b < 0) {
throw new RuntimeException("Broken");
}
}
@MultipleTest(b=-2,a=1)
public static void m3(int a, int b) {
//全部采用默认值
if (a + b < 0) {
throw new RuntimeException("Broken");
}
}
}
解析代码
package org.example;
import java.lang.reflect.Method;
public class tes {
public static void main(String[] args) throws Exception {
int passed = 0, failed = 0; // 给passed变量赋值为0
String className = "org.example.Foo";
for (Method m : Class.forName(className).getMethods()) {
if (m.isAnnotationPresent(MultipleTest.class)) {
System.out.println(m.getName());
MultipleTest st = m.getAnnotation(MultipleTest.class);
try {
m.invoke(null,st.a(),st.b());
passed++;
} catch (Throwable ex) {
System.out.printf("Test %s failed: %s %n", m, ex.getCause()); // 使用%符号代替$符号
failed++;
}
}
}
System.out.printf("passed:%d , failed:%d",passed,failed);
}
}
四、 Java预定义的元注解
元注解是一种用于给其他注解进行说明的注解,它可以用来指定注解的作用域、保留策略、文档化、继承性和重复性等特性
4.1@retention
@Retention是一个用于指定注解的保留策略的元注解,它表示注解在什么时候有效,例如源码、字节码或运行时。它需要一个RetentionPolicy类型的参数,只能是一个
@Retention的参数有以下三种取值:
- RetentionPolicy.SOURCE:表示注解只在源码中有效,编译器会丢弃它,不会写入字节码文件。
- RetentionPolicy.CLASS:表示注解在源码和字节码中都有效,编译器会把它写入字节码文件,但是虚拟机不会读取它,这是默认值。
- RetentionPolicy.RUNTIME:表示注解在源码、字节码和运行时都有效,编译器会把它写入字节码文件,并且虚拟机会读取它,可以通过反射机制获取它
package org.example;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME) // 指定注解在运行时保留
public @interface AnimalInfo {
String species() default "Unknown"; // 动物的种类属性,默认值为"Unknown"
int age() default 0; // 动物的年龄属性,默认值为0
}
package org.example;
@AnimalInfo(species = "Generic Animal", age = 1) // 使用注解为这个动物类提供信息
public class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
public class AnimalInfoExample {
public static void main(String[] args) {
// 获取Animal类的Class对象
Class<Animal> animalClass = Animal.class;
// 获取Animal类上的所有注解
Annotation[] annotations = animalClass.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation instanceof AnimalInfo) {
AnimalInfo animalInfo = (AnimalInfo) annotation;
String species = animalInfo.species();
int age = animalInfo.age();
System.out.println("Species: " + species);
System.out.println("Age: " + age);
}
}
}
}
4.2 @Target
@Target是一个用于指定注解可以用在哪些元素上的元注解,它表示注解的作用范围,例如类、方法、字段、参数等。它需要一个ElementType类型的参数,可以是一个或多个
@Target的参数有以下几种取值:
- ElementType.TYPE:表示注解可以用于类、接口、枚举或注解类型。
- ElementType.FIELD:表示注解可以用于字段或枚举常量。
- ElementType.METHOD:表示注解可以用于方法。
- ElementType.PARAMETER:表示注解可以用于方法参数。
- ElementType.CONSTRUCTOR:表示注解可以用于构造器。
- ElementType.LOCAL_VARIABLE:表示注解可以用于局部变量。
- ElementType.ANNOTATION_TYPE:表示注解可以用于注解类型。
- ElementType.PACKAGE:表示注解可以用于包声明。
- ElementType.TYPE_PARAMETER:表示注解可以用于类型参数,这是Java 8新增的特性。
- ElementType.TYPE_USE:表示注解可以用于任何类型的使用,这也是Java 8新增的特性
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD}) // 指定注解可以应用于类和方法
@Retention(RetentionPolicy.RUNTIME) // 指定注解在运行时保留
public @interface AnimalInfo {
String species() default "Unknown"; // 动物的种类属性,默认值为"Unknown"
int age() default 0; // 动物的年龄属性,默认值为0
}
4.3@Inherited
@Inherited是一个用于指明父类注解会被子类继承得到的元注解,它表示该注解可以被子类继承。它没有参数。
@Inherited的作用是让自定义的注解具有继承性,即如果一个类使用了某个注解,那么它的子类也会自动拥有该注解,而不需要再次标注。这样可以简化代码,避免重复的注解。@Inherited只对类注解有效,对于方法和属性注解无效
import java.lang.annotation.*;
@Inherited // 指示该注解可以被继承
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CustomAnnotation {
String value() default "Default Value";
}
@CustomAnnotation(value = "Annotated Parent Class")
public class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Cat extends Animal {
public Cat(String name) {
super(name);
}
}
import java.lang.annotation.Annotation;
public class InheritedAnnotationExample {
public static void main(String[] args) {
// 获取 Cat 类的 Class 对象
Class<Cat> catClass = Cat.class;
// 获取 Cat 类上的所有注解,包括继承的注解
Annotation[] annotations = catClass.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation instanceof CustomAnnotation) {
CustomAnnotation customAnnotation = (CustomAnnotation) annotation;
String value = customAnnotation.value();
System.out.println("Annotation Value: " + value);
}
}
}
}
4.4@Documented
@Documented是一个用于将注解包含在javadoc中的元注解,它表示该注解会被javadoc工具记录。它没有参数。
@Documented的作用是让自定义的注解在生成文档时能够显示出来,而不是被忽略。这样可以提高代码的可读性和可维护性,也可以让其他人更容易理解注解的含义和用法。@Documented只对类注解有效,对于方法和属性注解无效
import java.lang.annotation.*;
@Documented // 指示编译工具生成文档时包括此注解信息
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface AnimalInfo {
String species() default "Unknown"; // 动物的种类属性,默认值为"Unknown"
int age() default 0; // 动物的年龄属性,默认值为0
}
4.5@Repeatable
@Repeatable是一个用于表示注解可以在同一个元素上重复使用的元注解,它需要指定一个容器注解来存放重复的注解。@Repeatable注解可以让代码更简洁和清晰,也可以避免一些潜在的错误。@Repeatable注解在Java 8中引入,最初只能用于修饰final或static的方法或构造器,但是在Java 9中,@Repeatable注解也可以用于修饰private的实例方法
package org.example;
import java.lang.annotation.*;
// 自定义注解 @RepeatableAnnotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(RepeatableAnnotations.class) // 指示该注解可以多次应用于同一目标元素,RepeatableAnnotations.class 是容器注解
public @interface RepeatableAnnotation {
int a() default 0;
int b() default 0;
int c() default 0;
}
package org.example;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 容器注解 @RepeatableAnnotations
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RepeatableAnnotations {
RepeatableAnnotation[] value(); // 一个数组属性,用于存储多个 @RepeatableAnnotation 注解
}
package org.example;
import java.lang.annotation.*;
// 示例类 Student
public class Student {
// 使用 @RepeatableAnnotation 注解并检查条件
@RepeatableAnnotation(a = 2, b = 3, c = 5) // 第一个 @RepeatableAnnotation 注解
@RepeatableAnnotation(a = 4, b = 8, c = 11) // 第二个 @RepeatableAnnotation 注解
public static void add(int a, int b, int c) {
if (c != a + b) {
throw new ArithmeticException("Wrong");
}
System.out.println("Sum: " + c);
}
public static void main(String[] args) throws Exception {
String className = "org.example.Student";
for (java.lang.reflect.Method m : Class.forName(className).getMethods()) {
if (m.isAnnotationPresent(RepeatableAnnotations.class)) {
RepeatableAnnotation[] annos = m.getAnnotationsByType(RepeatableAnnotation.class);
for (RepeatableAnnotation anno : annos) {
System.out.println("a: " + anno.a() + ", b: " + anno.b() + ", c: " + anno.c());
try {
m.invoke(null, anno.a(), anno.b(), anno.c());
} catch (Throwable ex) {
System.out.printf("Test %s failed: %s%n", m, ex.getCause().getMessage());
}
}
}
}
}
}
五、注解的解析
解的解析是指通过反射机制获取程序元素上的注解信息,并根据注解的属性值进行相应的处理。
Java注解的解析
- RetentionPolicy.RUNTIME注解采用反射进行解析
- RetentionPolicy.CLASS注解采用专用的字节码工具进行解析
- RetentionPolicy.SOURCE注解采用注解处理器进行解析
注解处理器继承AbstractProcessor,重写process方法
javac -processor Processor1, Processor2, … sourceJavaFile
六、RUNTIME注解的实现本质
6.1步骤
为了获取和处理RUNTIME注解,我们需要以下几个步骤:
- 首先,我们需要使用Class类的forName方法或者对象的getClass方法来获取目标元素所属的Class对象。
- 然后,我们需要使用Class对象的getMethods、getFields、getConstructors等方法来获取目标元素所对应的Method、Field、Constructor等对象。
- 接着,我们需要使用Method、Field、Constructor等对象的isAnnotationPresent方法来判断目标元素上是否存在指定的RUNTIME注解。
- 最后,我们需要使用Method、Field、Constructor等对象的getAnnotation方法或者getAnnotationsByType方法来获取目标元素上的RUNTIME注解实例,并读取注解的属性值,然后进行相应的处理。
6.2增加代理类导出设置
RUNTIME注解代理类导出设置的具体方法有以下几种:
- 在程序中使用System.setProperty方法来设置系统属性,例如:
System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); // 设置保存代理类
- 在命令行中使用-D参数来设置系统属性,例如:
java -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true Main // 设置保存代理类
如果RUNTIME注解被JVM加载进来,Java可以用反射获取到注解内容
- 程序自动为注解产生一个代理类,来拦截注解的方法访问
- 代理类有一个AnnotationInvocationHandler成员变量,其内部存放所有的注解的赋值