Skip to content

Java中的注解

  1. 什么是注解Java基础 :反射、注解、代理、线程池、依赖的学习和理解
  2. 注解的基本使用Java面试---Day5_Liknananana的博客-CSDN博客
  3. 为什么需要注解,注解的意义何在?Java注解Annotation基础 - Java开发 - 开发语言与工具 - 深度开源
  4. 反射+注解 依赖注入Java基础 :反射、注解、代理、线程池、依赖的学习和理解
    1. 什么是依赖注入反射、注解与依赖注入总结
    2. 依赖注入的使用场景用Dagger2在Android中实现依赖注入

参考资料:关于注解更详细的资料https://zhuanlan.zhihu.com/p/202586806

这是一篇入门级的文章,通过它你可以学到以下知识:

  1. 什么是注解
  2. 注解的本质
  3. 注解的分类
  4. 元注解是什么
  5. 注解的生命周期
  6. 注解的意义
  7. 怎么自定义注解

什么是注解

注解注释很像,注释是给程式员看的,而注解是给计算机看的。 注解是一种对程序的说明,告诉计算机这段程序或者这个变量、方法的作用。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。 被注解标记的这些类,方法,属性等,在虚拟机运行时可以获取到(某些注解只在编译时或者编写代码时起作用,运行起来的时候不会加载到字节码文件中),并做出相应的处理。

注解的本质

注解本质上就是一个接口,该接口默认继承java.lang.annotation.Annotation接口。

注解的分类

按照一般的分类方式,可以分为三类:

  1. 标准注解
  2. 元注解
  3. 自定义的注解

如果按照注解的生命周期分类,同样可以分为三类,主要是通过描述注解的元注解的**@Retention属性的不同取值来分类:**

  1. SOURCE(源码注解):当前被描述的注解只在源文件中有效,不会保留到class字节码文件中,也不会被JVM读取到。
  2. CLASS(编译时注解):当前被描述的注解,会保留到class字节码文件中,不会被JVM读取到。
  3. RUNTIME(运行时注解):当前被描述的注解,会保留到class字节码文件中,并被JVM读取到。

标准注解

@Override:用于检测被该注解标注的方法是否时继承自父类(接口)的。以下就是源码中Override注解的定义,可以看到它是一个源码注解

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

@Deprecated:用于标注已经过时的方法; @SuppressWarnnings:用于通知Java编译器禁止特定的编译警告;

元注解

元注解就是用于注释注解的注解,用来标识一个注解的作用和使用范围等。 **@Target:**描述注解能够作用的位置,以下是源码中元注解Target的定义

java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

之前我们提到注解可以声明在包、类、字段、方法、局部变量、方法参数等的前面,但并不是每一个注解都能声明在这些地方。可以使用**@Target来限制注解的作用位置** @Target的属性的取值可以是:

  • CONSTRUCTOR:用于描述构造器
  • FIELD:用于描述域
  • LOCAL_VARIABLE:用于描述局部变量
  • METHOD:用于描述方法
  • PACKAGE:用于描述包
  • PARAMETER:用于描述参数
  • TYPE:用于描述类、接口(包括注解类型) 或enum声明

@Retention:描述该注解的生命周期,表示在什么编译级别上保存该注解的信息。 Annotation被保留的时间有长短:某些Annotation仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,而另一些在class被装载时将被读取。因此有些注解仅在编译阶段生效,在运行阶段失效就是通过这个元注解@Retention来描述的 按照注解的生命周期分类可以分为:源码注解,编译时注解,运行时注解 @Retention属性的取值可以是(RetentionPoicy):

  1. SOURCE(源码注解):当前被描述的注解只在源文件中有效,不会保留到class字节码文件中,也不会被JVM读取到。
  2. CLASS(编译时注解):当前被描述的注解,会保留到class字节码文件中,不会被JVM读取到。
  3. RUNTIME(运行时注解):当前被描述的注解,会保留到class字节码文件中,并被JVM读取到。

注解的意义

根据注解的生命周期其实可以总结出在不同场景下的意义:

  1. 为编译器提供辅助信息,以便于检测错误,抑制警告
  2. 编译源代码时进行额外处理,生成源代码或者xml文件
  3. 在运行时进行额外的处理

简而言之,注解起到**“描述、配置”**的作用。

怎么定义注解

语法格式

定义注解的语法格式,其实在上面在标准注解Override以及元注解Target处,我们已经见过注解的定义了

java
public @interface 注解名称{
    ... 属性列表
}

如何解析注解

注解作为供计算机“阅读”的注释,那么对于运行时注解(关于源码注解和编译时注解暂不做讨论)计算机或者说Java虚拟机是如何从注解中获取到信息的呢? 首先之前有提到注解的本质是接口,它们都直接或者间接的继承自**Annotation** 首先我们要明白通过反射可以动态获取对象信息和调用对象方法。而一个定义为运行时可见的注解,在运行时,也就是当编译的产物class文件,被转载到虚拟机中时,class文件中的Annotation就会被虚拟机读取。解析,运行时注解的原理正是利用了反射机制(解析编译时注解用到的是APT机制,之后再讲)。 在Java的**java.lang.reflect** 包下新增了**AnnotatedElement**接口,这个接口是所有程序元素(Field、Method、Package、Class和Constructor)的父类。程序通过反射获取了某个类的**AnnotatedElement**对象之后,程序就可以调用该对象的如下七个方法来访问Annotation信息:

  1. **T getAnnotation(Class annotationClass) **

返回该程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null;

  1. **Annotation[] getDeclaredAnnotation(Class)**

返回该程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null;与此接口中的其他方法不同,该方法将忽略继承的注解;

  1. **Annotation[] getAnnotations()**

返回该程序元素上存在的所有注解;

  1. **Annotation[] getDeclaredAnnotations()**

返回直接存在于此元素上的所有注释。与此接口中的其他方法不同,该方法将忽略继承的注解;

  1. **Annotation[] getAnnotationsByType(Class)**

返回直接存在于此元素上指定注解类型的所有注解;

  1. **Annotation[] getDeclaredAnnotationsByType(Class)**

返回直接存在于此元素上指定注解类型的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注解;

  1. **boolean isAnnotationPresent(Class<?extends Annotation> annotationClass)**

判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false;

解析注解代码实现

理论先行,下面我们不妨动手实践,自定义一个注解,并通过反射去解析注解

自定义注解

这里简单的声明一个水果名称的注解,可以看到我使用**@Target**,并指定它的属性是**ElementType.FIELD**,说明这是一个作用在成员属性上的注解;并且使用了**@Retention**,指定它的属性是**RetentionPolicy.RUNTIME**,说明这是一个运行时注解

java

/**
 * 水果名称注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FruitName {
    String value() default " ";
}

使用注解

使用注解这没什么好说的,就是在对应的程序元素上声明即可

java
public class Apple {
    @FruitName("Apple")
    private String appleName;
    
    public String getAppleName() {
        return appleName;
    }

    public void setAppleName(String appleName) {
        this.appleName = appleName;
    }

    public void displayName(){
        System.out.println(getAppleName());
    }
}

解析注解

最后实现利用反射来获取到注解的信息,拿到注解的信息,我们就可以将这些信息添加到我们的实例对象中。

java
public class AnnotationParser {
    public static void main(String[] args) {
        //获取到声明的属性
        Field[] fields = Apple.class.getDeclaredFields();
        for (Field field : fields) {
            //通过getAnnotation拿到程序元素上指定类型的注解
            if (field.isAnnotationPresent(FruitName.class)){
                FruitName fruitName = field.getAnnotation(FruitName.class);
                System.out.println("水果的名称:" + fruitName.value());
            }
        }
    }
}

不出意外的化输出的结果就是:

java
水果的名称:Apple

总结:所以注解就是一个接口,它用来说明程序元素的作用,让计算机在运行时根据注解的内容去动态的处理被注解的程序元素,而解析注解的内容是通过Java的**java.lang.reflect** 包下新增了**AnnotatedElement**接口实现的。

注解的用途

在面试中也遇到过这个问题,一般什么时候我们会去使用注解呢?当时是比较懵的,自己开发中没有去自定义过注解,但是很多框架中都用到了注解,明白为什么这些框架中要去使用它,这个问题就也明晰了。

实现反射动态加载类

在java编程中,我们往往是面向抽象编程,利用依赖倒转原则,通常在参数传递时定义的类型是抽象类型,而具体哪一个子类通过反射动态去加载,这样能降低系统的耦合性。首先定义一个用于声明类名和方法的注解

java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Pro {
    String className();
    String methodName();
}

下面就是具体的使用了,我们可以通过反射获取注解上声明的具体类名和方法名,然后再利用反射去找到这个类,并执行其中的方法

java
@Pro(className = "annomation.Apple",methodName = "displayName")
public class ReflectPro {
    public static void main(String[] args) {
        //1.1获取该类的字节码文件对象
        Class<ReflectPro> reflectProClass= ReflectPro.class;
        //1.2获取上边的注解对象
        Pro an=reflectProClass.getAnnotation(Pro.class);
        //1.3调用注解对象中定义的抽象方法,获取返回值;
        String className=an.className();
        String methodName=an.methodName();
        //验证是否真的获取到了
        System.out.println(className);
        System.out.println(methodName);

        //3.加载该类进入内存
        Class cls= null;
        try {
            cls = Class.forName(className);
            //4.创建对象
            Object obj=cls.newInstance();
            //5.获取方法对象
            Method method=cls.getMethod(methodName);
            //6.执行方法
            method.invoke(obj);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

测试框架(编译时注解APT)

反射完成View的注入