Java基础-注解

637 阅读10分钟

Java基础-注解

1.概述

Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。Java 注解是从 Java5 开始添加到 Java 的,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。

官方的解释总是让人一脸懵逼,但有两个点需要关注,一是元数据,二是不直接影响你的代码执行。

元数据:是用来描述数据的数据,这里就是指描述代码的数据。纳尼这不就是注释么?从一定意义上说确实和注释一样。

不直接影响你的代码执行:意思是有可能可以影响代码执行。注释如何影响代码执行?注解和注释最大的不同是我们可以通过代码运行或编译时拿到注解,进而影响代码执行。

Java注解不仅描述了源代码还能间接影响代码运行。

2.相关概念

Java中的注解主要分为三类:

2.1 Java内置注解

Java 内置常用注解共有5个,在java.lang包中。

  • @Override - 检查该方法是否是重载方法。如果发现其父类或引用的接口中并没有该方法时,会报编译错误。
  • @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
  • @SuppressWarnings - 指示编译器去忽略注解中声明的警告。
  • @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
  • @FunctionalInterface - Java 8 开始支持,编译器javac检查一个接口是否符合函数接口的标准。

2.2 元注解

元注解是用于定义注解的注解。Java中的元注解都在java.lang.annotation包中,前4个是元注解,Java1.8添加了后面2个注解。

  • @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
  • @Documented - 标记这些注解是否包含在用户文档中。
  • @Target - 标记这个注解应该是哪种 Java 成员。
  • @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
  • @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。
  • @Native - 标记属性为native属性,用在代码中,给IDE工具做提示使用。

2.3 自定义注解

根据自己需求使用元注解定义我们的注解。

3.使用注解

先看一个注解的定义,使用@interface关键字定义一个注解,还需要使用@Retention标记该注解保留到什么时期,@Target标记该注解可用在什么地方。

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface Override {}

要理解注解的使用,需要从元注解入手,其中@Retention 和 @Target 是定义一个注解所必须的。

3.1 理解元注解

@Retention

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    // 返回该注解保留到什么阶段
    RetentionPolicy value();
}

@Retention 的定义多了一个value()方法,返回的是一个RetentionPolicy类型的值,这是一个枚举类型的值说明只能有一个保留策略。这个返回值是在使用注解@Retention(RetentionPolicy.RUNTIME) 括号内的值RetentionPolicy.RUNTIME 。

public enum RetentionPolicy {
    SOURCE,/*仅在源码中保留,编译时会删除该注解*/
    CLASS,/*保留到编译期,在Class字节码文件中保留,但JVM不会加载该注解;这是默认值*/
    RUNTIME/*保留到运行期,JVM会加载该注解,所以可以被反射读取*/
}

RetentionPolicy有三个值,分别标记该注解是保留在源码、编译期还是运行期,只有运行期的注解才会被反射读取。

@Target

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    // 返回该注解可以在何处使用,可以有多个位置
    ElementType[] value();
}

@Target标记该注解可以用在什么位置,返回的是一个ElementTypep类型数组,说明一个注解可以被用在多种位置。

public enum ElementType {
    /** 可用在类、接口、枚举声明前 */
    TYPE,
    /** 字段声明(包括枚举常量) */
    FIELD,
    /** 方法声明 */
    METHOD,
    /** 参数声明 */
    PARAMETER,
    /** 构造方法声明 */
    CONSTRUCTOR,
    /** 局部变量声明 */
    LOCAL_VARIABLE,
    /** 注解声明*/
    ANNOTATION_TYPE,
    /** 包声明 */
    PACKAGE,
    /**
     * 该注解可用于类型变量的声明语句中,如泛型声明
     * @since 1.8
     */
    TYPE_PARAMETER,
    /**
     * 该注解能写在使用类型的任何语句中
     * @since 1.8
     */
    TYPE_USE
}

TYPE_PARAMETER 和 TYPE_USE 我们会重点做示例讲解,这里先不展开说。

@Documented

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {}

@Documented是一个纯粹的语义元注解,生成JavaDoc文档时,被@Documented标记的注解在所使用的地方会被文档收录进去,否则Java文档不会有该注解信息。

@Inherited

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {}

@Inherited标记的注解是可以被集成的,当父类被可继承的注解标记,子类会自动拥有该注解。在运行时将不断向上寻找对应注解是否存在。注意在方法和接口上的注解不具有继承特性。

@Repeatable

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
    Class<? extends Annotation> value();
}

@Repeatable 标记的注解,可以在同一个位置存在多个

3.2 定义注解

了解了元注解以及使用方式后,自定义注解就很简单了。

@元注解1
@元注解2
修饰符 @interface 注解名 {   
    注解元素的声明1 
    注解元素的声明2   
}

(1)定义注解使用@interface 关键字

(2)使用@Retention@Target 元注解指定保留时期和使用位置。

(3)注解可以添加成员变量,定义类似方法,模式是:变量类型 变量名() ,还可以为成员变量添加默认值,模式是:变量类型 变量名() default 默认值

(4)成员变量可支持基本类型、String、Class、enum、Annotation、及其以上类型的数组。

(5)没有成员变量的注解称为标记,有成员变量的注解称为元数据。

下面举几个例子:

  • 可用在类、接口、枚举之前使用。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ClassAnnotaion {
    String calssId() default "xbox";
    int classCode();
}

  • 可用在Method上的注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(MethodAnnotationArray.class)
public @interface MethodAnnotation {
    String tag() default "方法默认tag";
}
// 可重复注解的容器
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodAnnotationArray {
    MethodAnnotation[] value();
}

  • 可用在Field和参数上的注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.PARAMETER})
public @interface FieldAnnotation {
    boolean isCheck() default false;
}

  • 可用在参数上的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface ParameterAnnotation {
    String value();
}

3.3 使用注解

@ClassAnnotaion(calssIds = "bacd",classCode = 34)
public class Demo1 {
    @FieldAnnotation(isCheck = true)
    String name;
    @MethodAnnotation(tag = "demo方法")
    @MethodAnnotation(tag = "可重复注解")
    public void deal(@ParameterAnnotation("kk") String input){
        System.out.println(input);
    }
}

要使注解只需要在对应位置添加定义的注解

@注解名(变量1 = 值1 ,变量2 = 值2, 变量3 = {数组元素1,数组元素2,数组元素3})

注意:当定义注解只有一个成员变量,且属性名称为value,此时使用注解时可以在括号内直接对value赋值,而不用显式指定value = 值。

此时在代码上已经加上了自己定义的注解,运行这些代码,此时的注解和普通的注释一样,不会影响目前的代码。

如果想要利用这些注解达到间接改变代码执行的目的,需要编写专门的注解处理器,来解析并处理注解。

3.4 处理注解

注解编译后同样会生成ClassAnnotaion.class 字节码,这里使用jad 工具反编译生成的字节码。

// jad ClassAnnotation.class 反编译后代码如下:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   ClassAnnotaion.java
package com.company.part1;
import java.lang.annotation.Annotation;
public interface ClassAnnotaion extends Annotation{
    public abstract String calssIds();
    public abstract int classCode();
}

从中可以看出,注解其实就是一个接口,并且继承了Annotation接口。该接口在java.lang.annotation包。

public interface Annotation {
    boolean equals(Object obj);
    int hashCode();
    String toString();
    /**
     * @return the annotation type of this annotation
     */
    Class<? extends java.lang.annotation.Annotation> annotationType();
}

通过以上源码可以看出,注解本质上就是一个接口,接口可以有成员变量和成员方法,但接口中的成员变量时static final 类型的,使用时无法重新赋值,这对注解来说是没意义的;所以注解使用成员方法来当做注解成员变量,这也解释了定义注解时,成员变量为什么带有括号。

要处理注解需要使用反射,注解可以在很多地方使用,java在java.lang.reflect包中定义了一个AnnotatedElement接口,这个接口定义了可以使用注解的类型,简而言之实现了该接口的类型都可以使用注解。

public interface AnnotatedElement {
    // 指定类型的注解是否在当前元素上
    default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
        return getAnnotation(annotationClass) != null;
    }
    // 在当前元素上获取指定类型注解的实例,有则返回,无则返回null
    <T extends Annotation> T getAnnotation(Class<T> annotationClass);
    // 返回当前元素上所有的注解,包括当前元素继承父类的注解。
    Annotation[] getAnnotations();
    // 返回当前元素上直接存在的注解。
    Annotation[] getDeclaredAnnotations();
    //------------------以下1.8新增-------------------
	// 获取当前元素上指定类型的重复注解实例,
    default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
        T[] result = getDeclaredAnnotationsByType(annotationClass);
        if (result.length == 0 && // Neither directly nor indirectly present
                this instanceof Class && // the element is a class
                AnnotationType.getInstance(annotationClass).isInherited()) { // Inheritable
            Class<?> superClass = ((Class<?>) this).getSuperclass();
            if (superClass != null) {
                // Determine if the annotation is associated with the
                // superclass
                result = superClass.getAnnotationsByType(annotationClass);
            }
        }
        return result;
    }
	// 获取当前元素上直接存在的指定类型注解
    default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) {
        Objects.requireNonNull(annotationClass);
        // Loop over all directly-present annotations looking for a matching one
        for (Annotation annotation : getDeclaredAnnotations()) {
            if (annotationClass.equals(annotation.annotationType())) {
                // More robust to do a dynamic cast at runtime instead
                // of compile-time only.
                return annotationClass.cast(annotation);
            }
        }
        return null;
    }
	// 获取当前元素上指定类型的重复注解
    default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) {
        Objects.requireNonNull(annotationClass);
        return AnnotationSupport.
                getDirectlyAndIndirectlyPresent(Arrays.stream(getDeclaredAnnotations()).
                                collect(Collectors.toMap(Annotation::annotationType,
                                        Function.identity(),
                                        ((first,second) -> first),
                                        LinkedHashMap::new)),
                        annotationClass);
    }
}

在1.8之前,一个元素上只能有一个相同类型的注解,1.8之后被Repeatable标记的注解在使用时可在一个元素上同时标注多个。

上面讲元注解**@Target**时提到了ElementType枚举,可以用于定义注解可以使用在哪些地方,下面列举部分。

ElementType 实现类型 含义
TYPE Class 代表了类、接口、枚举
FIELD Field 代表了成员变量
METHOD Method 代表了成员方法
CONSTRUCTOR Constructor 代表了构造函数
PARAMETER Parameter 代表了参数
......

Class 定义如下,其实现了AnnotatedElement接口。

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement {}

Field 定义如下,同样实现该接口:

class Field extends AccessibleObject implements Member {}

Constructor和Method定义如下:

public class AccessibleObject implements AnnotatedElement {}
public abstract class Executable extends AccessibleObject implements Member, GenericDeclaration {}

public final class Constructor<T> extends Executable {}
public final class Method extends Executable {]

Parameter定义如下:

public final class Parameter implements AnnotatedElement {}

所以通过反射来处理注解可以简单分为两步:

第一步:通过反射拿到想要处理的元素上的注解。

第二步:拿到注解中属性值,做进一步处理。

(1)处理类上的注解

Demo1 demo1 = new Demo1();
clazz = demo1.getClass();
if (clazz.isAnnotationPresent(ClassAnnotaion.class)) {
    ClassAnnotaion classAnnotaion = (ClassAnnotaion)
                                    clazz.getDeclaredAnnotation(ClassAnnotaion.class);
    System.out.println("类型注解值:" + 
                      classAnnotaion.calssIds() + "   " + 
                      classAnnotaion.classCode()
                      );
}

(2)处理属性上的注解

Field field = clazz.getField("name");
if (field.isAnnotationPresent(FieldAnnotation.class)) {
    FieldAnnotation fieldAnnotation = field.getDeclaredAnnotation(FieldAnnotation.class);
    System.out.println("属性注解值:" + fieldAnnotation.isCheck());
}

(3)处理方法上的注解

Method method = clazz.getMethod("deal", String.class, String.class);
if (method.isAnnotationPresent(MethodAnnotationArray.class)) {
    MethodAnnotationArray methodAnnotationArray = 
        					method.getDeclaredAnnotation(MethodAnnotationArray.class);
    MethodAnnotation[] methodAnnotations = methodAnnotationArray.value();
    for (MethodAnnotation temp : methodAnnotations) {
        System.out.println("方法注解值:" + temp.tag());
    }
}

可重复注解需要指定一个容器,虽然写的时候使用的是MethodAnnotation类型,但最终编译后会被装进MethodAnnotationArray,最终在方法上合并成一个MethodAnnotationArray类型注解。

(4)处理参数注解

Annotation[][] annotations1 = method.getParameterAnnotations();
for (int i = 0; i < annotations1.length; i++) {
    System.out.println("第" + (i + 1) + "个参数的注解");
    Annotation[] tmp = annotations1[i];
    for (Annotation a : tmp) {
        System.out.println(tmp);
    }
}

一个方法有多个参数,一个方法又有多个参数,所以这里使用一个二维数组来存储参数列表中的注解。

4.自定义注解应用

自定义注解在工作中的应用还是很多的,这块准备自己再积累积累再做补充,使用+原理。

1.生成文档.例如:@see,@param,@return 等

2.代替配置文件功能.例如spring基于注解的配置

3.在编译时进行格式检查。

4.JUnit 、ButterKnife、Dagger2、Retrofit

参考:

Java 注解完全解析,by 若丨寒

深入理解java注解的实现原理, by 知了123

java元注解 @Target注解用法, by 就这个名字好

Java反射API研究(1)——注解Annotation, by 光闪

Java枚举和注解梳理, by itzhouq的博客

Java之注解的定义及使用, by 炼金术师cck