Java-基础-02-注解

159 阅读12分钟

一、注解介绍

1)、什么是注解?

要解释注解我们首先要明白什么是元数据:元数据就是为其他数据提供信息的数据

那么还是引入官方一段对注解的解释:注解用于为代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。Java 注解是从 JDK 1.5 开始添加到 Java 的。

简单的理解:注解就是附加到代码上的一种额外补充信息

2)、注解有哪些作用?

源码阶段注解: 编译器可利用该阶段注解检测错误,提示警告信息,打印日志等

编译阶段注解:利用注解信息自动生成代码、文档或者做其它相应的自动处理

运行阶段注解: 可通过反射获取注解信息,做相应操作

3)、如何自定义定义一个注解?

使用 @interface + 注解名称这种语法结构就能定义一个注解,如下:

@interface TestAnnotation{

}
复制代码

通常我们会使用一些元注解来修饰自定义注解

二、元注解

了解了之前的元数据,元注解就是为注解提供注解的注解 😂,这句话可能有点绕,反正你清楚元注解是给注解用的就行了

JDK 给我们提供的元注解有如下几个:

1、@Target

2、@Retention

3、@Inherited

4、@Documented

5、@Repeatable

1)、@Target

@Target 表示这个注解能放在什么位置上,具体选择的位置列表如下:

ElementType.ANNOTATION_TYPE //能修饰注解
ElementType.CONSTRUCTOR //能修饰构造器
ElementType.FIELD //能修饰成员变量
ElementType.LOCAL_VARIABLE //能修饰局部变量
ElementType.METHOD //能修饰方法
ElementType.PACKAGE //能修饰包名 
ElementType.PARAMETER //能修饰参数
ElementType.TYPE //能修饰类、接口或枚举类型
ElementType.TYPE_PARAMETER //能修饰泛型,如泛型方法、泛型类、泛型接口 (jdk1.8加入)
ElementType.TYPE_USE //能修饰类型 可用于任意类型除了 class (jdk1.8加入)

@Target(ElementType.TYPE)
@interface TestAnnotation{

}
复制代码

注意:默认情况下无限制

2)、@Retention

@Retention 表示注解的的生命周期,可选的值有 3 个:

RetentionPolicy.SOURCE //表示注解只在源码中存在,编译成 class 之后,就没了

RetentionPolicy.CLASS //表示注解在 java 源文件编程成 .class 文件后,依然存在,但是运行起来后就没了

RetentionPolicy.RUNTIME //表示注解在运行起来后依然存在,程序可以通过反射获取这些信息

@Retention(RetentionPolicy.RUNTIME)
@interface TestAnnotation{

}
复制代码

注意:默认情况下为 RetentionPolicy.CLASS

3)、@Inherited

@Inherited 表示该注解可被继承,即当一个子类继承一个父类,该父类添加的注解有被 @Inherited 修饰,那么子类就可以获取到该注解,否则获取不到

@Inherited
@interface TestAnnotation{

}
复制代码

注意:默认情况下为不可继承

4)、@Documented

@Documented 表示该注解在通过 javadoc 命令生成 Api 文档后,会出现该注解的注释说明

@Documented
@interface TestAnnotation{

}
复制代码

注意:默认情况下为不出现

5)、@Repeatable

@Repeatable 是 JDK 1.8 新增的元注解,它表示注解在同一个位置能出现多次,这个注解有点抽象,我们通过一个实际例子理解一下

//游戏玩家注解
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface GamePlayer{
    Game[] value();
}

//游戏注解
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(GamePlayer.class)
@interface Game{
    String gameName();
}

@Game(gameName = "CF")
@Game(gameName = "LOL")
@Game(gameName = "DNF")
class GameTest{

}
复制代码

注意:默认情况下不可重复

经验:通常情况下,我们会使用多个元注解组合来修饰自定义注解

三、注解属性

1)、注解属性类型

注解属性类型可以为以下的一些类型:

1、基本数据类型

2、String

3、枚举类型

4、注解类型

5、Class 类型

6、以上类型的一维数组类型

2)、定义注解属性

首先我们定义一些注解属性,如下:

@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface TestAnnotation{
    //这就是注解属性的语法结构
    //定义一个属性并给了默认值
    String name() default "erdai";

    //定义一个属性未给默认值
    int age();
}
复制代码

可能你会有些疑问:这难道不是在定义方法吗?还可以给默认值?

这些疑问先留着,我们继续分析

自定义注解默认都会继承 Annotation ,Annotation 是一个接口,源码如下:

public interface Annotation {

    boolean equals(Object obj);

    int hashCode();

    String toString();

    Class<? extends Annotation> annotationType();
}
复制代码

我们知道,在接口中可以定义属性和方法,那么作为自定义注解,是否也可以定义呢?

可以,接口中的属性默认都是用public static final 修饰的,默认是一个常量,对于自定义注解来说,这点没有任何区别。而接口中的方法其实就相当于自定义注解的属性,只不过自定义注解还可以给默认值。因此我们在学习自定义注解属性时,我们应该把它当作一个新知识,加上我刚才对接口的分析对比,你上面的那些疑问便可以迎刃而解了

3)、注解属性使用

1、在使用注解的后面接上一对括号,括号里面使用 属性名 = value 的格式,多个属性之间中间用 ,隔开

2、未给默认值的属性必须进行赋值,否则编译器会报红

//单个属性
@TestAnnotation(age = 18)
class Test{

}

//多个属性
@TestAnnotation(age = 18,name = "erdai666")
class Test{

}
复制代码

4)、注解属性获取

注解属性的获取可以参考我的上一篇文章 传送门 ,上篇文章我们讲的是通过类对象获取注解,咱们补充点上篇文章没讲到的

1、我们在获取属性的时候,可以先判断一下是否存在该注解,增强代码的健壮性,如下:

@TestAnnotation(age = 18,name = "erdai666")
class Test{

}

Class<Test> testClass = Test.class;
//获取当前注解是否存在
boolean annotationPresent = testClass.isAnnotationPresent(TestAnnotation.class);
//如果存在则进入条件体
if(annotationPresent){
    TestAnnotation declaredAnnotation = testClass.getDeclaredAnnotation(TestAnnotation.class);
    System.out.println(declaredAnnotation.name());
    System.out.println(declaredAnnotation.age());
}
复制代码

2、获取类属性的注解属性

@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface TestField{
    String filed();
}

class Test{
    @TestField(filed = "我是属性")
    public String test;
}

//通过反射获取属性注解
Class<Test> testClass1 = Test.class;
try {
    Field field = testClass1.getDeclaredField("test");
    if(field.isAnnotationPresent(TestField.class)){
        TestField fieldAnnotation = field.getDeclaredAnnotation(TestField.class);
        System.out.println(fieldAnnotation.filed());
    }
} catch (NoSuchFieldException e) {
    e.printStackTrace();
}
//打印结果
我是属性
复制代码

3、获取类方法的注解属性

@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface TestMethod{
    String method();
}

class Test{
    @TestMethod(method = "我是方法")
    public void test(){

    }
}
//通过反射获取方法注解
Class<Test> testClass2 = Test.class;
try {
    Method method = testClass2.getDeclaredMethod("test");
    if(method.isAnnotationPresent(TestMethod.class)){
        TestMethod methodAnnotation = method.getDeclaredAnnotation(TestMethod.class);
        System.out.println(methodAnnotation.method());
    }
} catch (Exception e) {
    e.printStackTrace();
}
//打印结果
我是方法
复制代码

四、JDK 提供的内置注解

JDK 给我们提供了很多内置的注解,其中常用的有:

)1、@Override

2、@Deprecated

3、@SuppressWarnings

4、@FunctionalInterface

1)@Override

@Override 用在方法上,表示这个方法重写了父类的方法,例如 toString 方法

@Override
public String toString() {
    return super.toString();
}
复制代码

2)@Deprecated

@Deprecated 表示这个方法被弃用,不建议开发者使用

image-20210626113644915

可以看到用 @Deprecated 注解的方法调用的时候会被划掉

3)@SuppressWarnings

@SuppressWarnings 用于忽略警告信息,常见的取值如下:

  • deprecation:使用了不赞成使用的类或方法时的警告(使用 @Deprecated 使得编译器产生的警告)
  • unchecked:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型; 关闭编译器警告
  • fallthrough:当 Switch 程序块直接通往下一种情况而没有 Break 时的警告
  • path:在类路径、源文件路径等中有不存在的路径时的警告
  • serial:当在可序列化的类上缺少 serialVersionUID 定义时的警告
  • finally:任何 finally 子句不能正常完成时的警告
  • rawtypes 泛型类型未指明
  • unused 引用定义了,但是没有被使用
  • all:关于以上所有情况的警告

以泛型举个例子:

image-20210626114048630

当我们创建 List 未指定泛型时,编译器就会报黄提示我们未指明泛型,这个时候就可以使用这个注解了:

image.png

4)@FunctionalInterface

@FunctionalInterface 是 JDK 1.8 新增的注解,用于约定函数式接口,函数式接口就是接口中只有一个抽象方法

@FunctionalInterface
interface testInterface{
    void testMethod();
}
复制代码

而当你有两个抽象方法时,注解会报红提示你:

image-20210626114855416

五、注解实际应用场景

SOURCE<CLASS<RUNTIME,即CLASS包含了SOURCE,RUNTIME包含SOURCE、CLASS

1)SOURCE(源码)

RetentionPolicy.SOURCE作用于源码级别的注解,可提供给IDE语法检查、APT等场景使用,注解信息只保留在源代码中,编译后被丢弃

image.png

在类中使用SOURCE 级别的注解,其编译之后的class中会被丢弃。

IDE语法检查

在Android开发中, support-annotations 与androidx.annotation) 中均有提供@IntDef 注解,此注解的定义如

下:

@Retention(SOURCE) //源码级别注解
@Target({ANNOTATION_TYPE})
public @interface IntDef {
    int[] value() default {};
    boolean flag() default false;
    boolean open() default false;
}

 

Java中Enum(枚举)的实质是特殊单例的静态成员变量,在运行期所有枚举类作为单例,全部加载到内存中。

比常量多5到10倍的内存占用。

此注解的意义在于能够取代枚举,实现如方法入参限制。

如:我们定义方法test ,此方法接收参数teacher 需要在:Lance、Alvin中选择一个。如果使用枚举能够实现

为:

public enum Teacher{
    LANCE,ALVIN
}
public void test(Teacher teacher) {

}

而现在为了进行内存优化,我们现在不再使用枚举,则方法定义为:

public static final int LANCE = 1;
public static final int ALVIN = 2;
public void test(int teacher) {

}

然而此时,调用test 方法由于采用基本数据类型int,将无法进行类型限定。此时使用@IntDef增加自定义注解:

public static final int LANCE = 1;
public static final int ALVIN = 2;
@IntDef(value = {MAN, WOMEN}) //限定为LANCE,ALVIN
@Target(ElementType.PARAMETER) //作用于参数的注解
@Retention(RetentionPolicy.SOURCE) //源码级别注解
public @interface Teacher {

}
public void test(@Teacher int teacher) {

}

此时,我们再去调用test 方法,如果传递的参数不是LANCE 或者ALVIN 则会显示 Inspection 警告(编译不会报错)。

image.png

可以修改此类语法检查级别:

image.png

以上注解均为SOURCE 级别,本身IDEA/AS 就是由Java开发的,工具实现了对Java语法的检查,借助注解能对被注

解的特定语法进行额外检查。

Android中我们需要设计接口以供使用者调用时,如出现需要对入参进行类型限定,如限定为资源ID、布局ID 等类型参数,将参数类型直接给定int即可。然而,我们可以利用Android为我们提供的语法检查注解,来辅助进 行更为直接的参数类型检查与提示。

参数限制为:图片资源ID。

Resource.java:  
public Drawable getDrawable(@DrawableRes int id) throws NotFoundException

同时,我们也可以通过利用@Intdef来定义自己的入参类型检查。

2)CLASS(编译器字节码)

定义为编译期注解,注解信息会保留在.class文件中,但运行时JVM会丢弃该注解信息。此时完全符合

此种注解的应用场景为字节码操作。如:AspectJ、热修复Roubust中应用此场景。

所谓字节码操作即为,直接修改字节码Class文件以达到修改代码执行逻辑的目的。在程序中有多处需要进行是否

登录的判断。

image.png

如果我们使用普通的编程方式,需要在代码中进行if-else 的判断,也许存在十个判断点,则需要在每个判断点加

入此项判断。此时,我们可以借助AOP(面向切面)编程思想,将程序中所有功能点划分为: 需要登录与无需登录

两种类型,即两个切面。对于切面的区分即可采用注解。

//Java源码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Login {

}
@Login
public void jumpA(){
    startActivity(new Intent(this,AActivity.class));
}
public void jumpB(){
    startActivity(new Intent(this,BActivity.class));
}

在上诉代码中, jumpA 方法需要具备登录身份。而Login 注解的定义被设置为CLASS 。因此我们能够在该类所编

译的字节码中获得到方法注解Login 。在操作字节码时,就能够根据方法是否具备该注解来修改class中该方法的

内容加入if-else 的代码段:

//Class字节码
@Login
public void jumpA() {
if (this.isLogin) {
    this.startActivity(new Intent(this, LoginActivity.class));
} else {
    this.startActivity(new Intent(this, AActivity.class));
    }
}

public void jumpB() {
    startActivity(new Intent(this,BActivity.class));
}

注解能够设置类型元素(参数),结合参数能实现更为丰富的场景,如:运行期权限判定等。

3)RUNTIME(运行时)

主要针对运行阶段注解,注解信息在运行时仍然保留,可以通过反射等方式获取。

举个实际的例子:例如我们开车去自助加油机加油,设定的 Money 是 200,如果少于 200 则提示 加油中...,否则提示 油已加满,如果出现异常情况,提示 加油失败

现在我们通过注解来实现一下它,如下:

@Inherited//表示这个注解可以被子类继承
@Documented//表示这个注解将被包含在Javadoc文档中
@Retention(RetentionPolicy.RUNTIME)//表示这个注解将在运行时保留,可以在运行时通过反射访问。
@Target(ElementType.METHOD)//表示这个注解只能用于方法上。
@interface OilAnnotation{
    //注解参数
    double maxOilMoney() default 0;
}

class GasStation{

    @OilAnnotation(maxOilMoney = 200)
    public void addOil(double money){
        String tips = processOilAnnotation(money);
        System.out.println(tips);
    }

    @SuppressWarnings("all")
    private String processOilAnnotation(double money){
        try {
            //获取类对象
            Class<GasStation> aClass = GasStation.class;
            //运行时注解需要通过反射获取
            Method addOilMethod = aClass.getDeclaredMethod("addOil", double.class);
            //获取方法注解是否存在
            boolean annotationPresent = addOilMethod.isAnnotationPresent(OilAnnotation.class);
            if(annotationPresent){
            	//获得注解实例
                OilAnnotation oilAnnotation = addOilMethod.getDeclaredAnnotation(OilAnnotation.class);
                if(money >= oilAnnotation.maxOilMoney()){
                    return "油已加满";
                }else {
                    return "加油中...";
                }
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return "加油失败";
    }
}

 public static void main(String[] args) {
        GasStation gasStation = new GasStation();
        gasStation.addOil(100); // 输出: 加油中...
        gasStation.addOil(200); // 输出: 油已加满
    }

 

六、APT注解处理器

APT全称为:"Anotation Processor Tools",意为编译期注解处理器。顾名思义,其用于处理注解。编写好的Java源文

件,需要经过javac 的编译,翻译为虚拟机能够加载解析的字节码Class文件。注解处理器是 javac 自带的一个工

具,用来在编译时期扫描处理注解信息,你可以为某些注解注册自己的注解处理器。 注册的注解处理器由javac

调起,并将注解信息传递给注解处理器进行处理。

注解处理器是对注解应用最为广泛的场景。在Glide、EventBus3、Butterknifer、Tinker、ARouter等等常用

  1. ButterKnife主要使用RetentionPolicy.CLASSRetentionPolicy.RUNTIME的注解,以便在编译时生成额外的代码,并在运行时使用这些注解信息来简化Android开发过程‌