栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

第十四章 注解(Annotation)

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

第十四章 注解(Annotation)

本章要点

  • 注解的概念和作用
  • @Override注解的功能和用法
  • @Deprecated注解的功能和用法
  • @SuppressWarnings注解的功能和用法
  • @Retention注解的功能和用法
  • @Target注解的功能和用法
  • @Doucumented注解的功能和用法
  • @Inherited注解的功能和用法
  • 自定义注解
  • 提取注解信息
  • 重复注解
  • 类型注解
  • 使用APT工具

从JDK5开始,Java增加了对元数据(MetaData)的支持,也就是Annotaion(即注解,偶尔也被翻译为注释),这种注解与第三章所介绍的注释有一定的区别。本章所介绍的注解,其实就是代码里特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。通过使用注解,程序开发人员可以在不改变逻辑的情况下,在源文件中嵌入一些补充的信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证或者部署。
注解提供了一种为程序元素设置元数据的方法,从1某些方面来看,注解就像修饰符一样,可以用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明,这些信息被存储在注解的“name=value”对中。
注释是一个接口,程序可以通过反射来获取指定程序元素的java.lang.annotation.Annotation对象,然后通过java.lang.annotation对象来取得注解里的元数据。
注解能被用来为程序元素(类、方法、成员变量等)设置元数据。值得指出的是,注解不影响程序代码的执行,无论增加、删除注解,代码都始终如一的执行。如果希望让程序中的注解在运行时起到一定的作用,只有通过某种配套的工具对注解中信息进行访问和处理,访问和处理注解的工具统称 APT(Annotation Processing Tool)。

基本注解

注解必须使用工具来处理,工具负责提取注解里包含的元数据,工具还会根据这些元数据增加额外的功能。在系统学习新的注解语法之前,先看一下Java提供的5个基本注解的用法——使用注解时要在其前面增加@符号,并把该注解当成一个修饰符使用,用于修饰它支持的程序元素。
5个基本的注释如下:

  • @Override
  • @Deprecated
  • @SuppressWarnings
  • @SafeVarargs
  • @FunctionalInterface

第四个是Java7新增的,第五个是Java8新增的,它们都在java.lang下。

限定重写父类方法:@Override
package com.tianyi.javase.test;

public class Fruit {
    public void info(){
        System.out.println("水果的info方法...");
    }
}
class Apple extends Fruit{
    //使用@Override指定下面方法必须重写父类方法
    @Override
    public void info(){
        System.out.println("苹果重写水果的info方法");
    }
}

这里用到包了,可以自己定义自己的包
编译上面程序可能看不出程序中的@Override有任何作用,因为@Override的作用是告诉编译器检查这个方法,保证父类要包含一个被该方法重写的方法,否则就会编译出错。@Override主要是辅助程序员避免一些低级错误。例如把上面Apple类中的info方法不小心写成了inf0,
这样的低级错误可能会称为后期排错时的巨大障碍。
疯狂软件教育中心在讲解Struts2.x框架过程中会告诉学员定义Action的方法:需要继承系统的Action基类,并重写execute()方法,但由于Struts Action基类里包含的execute()方法比较复杂,经常有学员出现重写execute()方法时方法签名写错的错误——这种错误在编译、运行时没有任何提示,只是运行时不出现所期望的结果,这种没有任何错误提示的错误才是最难调试的错误。如果在重写execute()方法时使用了@Override修饰,就可以轻松避免这种错误。
如果把Apple类中的info方法错误写成inf(),编译程序时将出现如下错误提示:

@Override只能修饰方法,不能修饰其他程序元素。

Java9增强的@Deprecated

@Deprecated用于标识某个程序元素(类、方法等)已过时,当其他程序使用已过时的类、方法时,编译器将会给出警告。如下程序指定Apple类中的info()方法已经过时,其他程序中使用Apple类的info()方法时编译器将会给出警告。
Java9为@Deprecated注解增加了如下两个属性

  • forRemoval:该boolean类型的属性指定该API在将来是否会被删除。
  • since:该String类型属性指定该API从哪个版本被表级为过时。
package com.tianyi.javase.test;

class Apple{
    //定义info方法已过时
    //since属性指定从哪个版本开始,forRemoval指定该API将来会被删除
    @Deprecated(since = "9",forRemoval = true)
    public void info(){
        System.out.println("Apple的info方法");
    }
}
public class DeprecatedTest{
    public static void main(String[]args){
        //下面使用info()方法时将会被编译器警告
        new Apple().info();
    }
}


上面程序中代码使用了Apple的info()方法,而Apple类中定义info()方法时使用了@Deprecated修饰,这表明该方法已过时,所以将会引起编译器警告。
@Deprecated的作用与文档注释中的@deprecated标记的作用基本相同,但它们的用法不同,前者是JDK5才支持的注解,无须放在文档注释语法(/*/部分)中,而是直接用于修饰程序中的程序单元,如方法、类、接口等。

抑制编译器警告:@SuppressWarnings

@SuppressWarnings指示被该注解修饰的程序元素(以及该程序元素中的所有子元素)取消显示指定的编译器错误。@SuppressWarnings会一直作用于该程序元素的所有子元素,例如,使用@SuppressWarnings修饰某个类取消显示某个编译器警告,同时又修饰该类里的某个方法取消显示另个编译器警告,那么该方法将会同时取消显示这两个编译器警告。

package com.tianyi.javase.test;

import java.util.ArrayList;
import java.util.List;

//关闭整个类里的编译器警告
@SuppressWarnings(value = "unchecked")
public class SuppressWarningsTest{
    public static void main(String[] args) {
        List myList = new ArrayList();
    }
}
“堆污染”警告与Java9增强的@SafeVarargs

前面介绍泛型查出时,介绍了如下代码可能导致运行异常。

List list = new ArrayList();
        list.add(20);//添加元素时引发unchecked异常
        //下面代码会引起“未经检查的转换”的警告,编译、运行时完全正常
        Listls = list;//①
        //但只要访问ls里的元素,如下代码就会引起运行时异常
        System.out.println(ls.get(0));

Java把这种错误的原因称为“堆污染(Heap pollution),当把一个不带泛型的对象赋给一个带泛型的变量时,往往就会发生这种堆污染”,如上①号代码所示。
对于形参个数可变的方法,该形参的类型又是泛型,这将更容易导致“堆污染”。例如如下工具类。

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class ErrorUtils {
    public static void faultyMethod(List...listStrArray){
        //Java语言不允许创建泛型数组,因此listArray只能被当成List[]处理
        //此时相当于把List赋给了List,已经发生了“堆污染”
        List[]listArray=listStrArray;
        ListmyList = new ArrayList<>();
        myList.add(new Random().nextInt(100));
        //把listArray的第一个元素赋为myArray
        listArray[0]=myList;
        String s = listStrArray[0].get(0);
    }
}

上面程序中已经发生了“堆污染”。由于该方法有个形参是List String…类型,个数可变的形参相当于是数据,但Java又不支持泛型数组,因此程序只能把List String…当成List[]处理,这里就发生了“堆污染”。
在Java6以及更早的版本中,Java编译器认为faultyMethod()方法完全没有问题,既不会提示错误也没有警告。
等到使用该方法时,例如如下程序

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

public class ErrorUtils {
    public static void faultyMethod(List...listStrArray){
        //Java语言不允许创建泛型数组,因此listArray只能被当成List[]处理
        //此时相当于把List赋给了List,已经发生了“堆污染”
        List[]listArray=listStrArray;
        ListmyList = new ArrayList<>();
        myList.add(new Random().nextInt(100));
        //把listArray的第一个元素赋为myArray
        listArray[0]=myList;
        String s = listStrArray[0].get(0);
    }

    public static void main(String[] args) {
        ErrorUtils.faultyMethod(Arrays.asList("Hello!"),Arrays.asList("World!"));//①
    }
}

"C:Program FilesJavajdk-11.0.11binjava.exe" "-javaagent:D:IntelliJ IDEA 2020.1.1libidea_rt.jar=50486:D:IntelliJ IDEA 2020.1.1bin" -Dfile.encoding=UTF-8 -classpath D:DemoProjectoutproductionDemoProject;D:DemoProjectlibmysql-connector-java-8.0.13.jar ErrorUtils
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
	at ErrorUtils.faultyMethod(ErrorUtils.java:15)
	at ErrorUtils.main(ErrorUtils.java:19)

Process finished with exit code 1


这个unchecked警告出现得比较“突兀”:定义faultyMethod()方法时没有任何警告,调用该方法时却引发了一个“警告”。
上面程序故意利用堆污染,因此程序将会引发ClassCastException异常。
从Java7开始,Java编译器将会进行更严格的检查,Java编译器在编译ErrorUtils时就会发出一个如下的警告。

ErrorUtils.java:15:警告:[unchecked]参数化 vararg类型
List的堆可能已受污染
public static void faultyMethod(List...listStrArray)
					^
一个警告

但如果开发者不希望看到这些警告,则可以使用如下三种方式来抑制这个警告

  • 使用@SafeVarargs修饰引发该警告的方法或构造器。Java9增强了该注释,允许使用该注释修饰私有实例方法。
  • 使用@SuppressWarnings(“unchecked”)修饰。
  • 编译时使用-Xlint:varargs选项

很明显,第三中方式一般比较少用,通常可以选择第一种或第二种方式,尤其是使用@SafeVaragrs修饰引发该警告的构造器和方法。它是Java7专门为抑制“堆污染”警告提供的。
如果程序使用@SafeVarargs修饰ErrorUtils类中的faultyMethod()方法,则编译上面两个程序时都不会发生任何警告。

函数式接口与@Functionnallnterface

前面已经提到,从Java8开始:如果接口中只有一个抽象方法(可以包含多个默认方法或多个static方法),该接口就是函数式接口。@FunctionalInterface就是用来指定某个接口必须时函数式接口。例如,如下程序使用@FunctionalInterface修饰了函数式接口。
函数式接口就是为Java8的Lambda表达式准备的,Java8允许使用Lambda表达式创建函数式接口的实例,因此Java8专门增加了@FunctionalInterface。

@FunctionalInterface
public interface FunInterface {
    static void foo(){
        System.out.println("foo类方法");
    }
    default void bar(){
        System.out.println("bar默认方法");
    }
    void test();//只定义一个抽象方法
}

该注解保证接口只有一个抽象方法。
@FunInterface只能修饰接口,不能修饰其他程序元素。

JDK的元注解

JDK除在java.lang下提供了5个基本的注解之外,还在java.lang.annotation包下提供了6个Meta注解(元注解),其中5个元注解都用于修饰其他的注解定义。其中Repeatable专门用于定义Java8新增的重复注解,本章后面会重点介绍相关内容。此处先介绍常用的4个元注解。

使用@Retention

@Retention只能用于修饰注解定义,用于指定被修饰的注解可以保留多次时间,@Retention包含一个RetentionPolicy类型的value成员变量,所以使用@Retention时必须为该value成员变量指定值。
value成员变量的值只能是如下三个

  • RetentionPolicy.CLASS:编译器将注解记录在class文件中。当运行Java程序时,JVM不可获取注解信息。这是默认值。
  • RetentionPolicy.RUNTIME:编译器将把注解记录在class文件中。当运行Java程序时,JVM也可获取注解信息,程序可以通过反射获取该注解信息。
  • RetentionPolicy.SOURCE:注解只保留在源代码中,编译器直接丢弃这种注解。
    如果需要通过反射注解信息,就需要使用value属性值为RetentionPolicy.RUNTIME的@Retention。使用@Retention元注解可采用如下代码为value指定值。
//定义下面的@Testable注解保留到运行时
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Testable{}

也可采用如下代码来为value指定值。

//定义下面的@Testable注解将被编译器直接丢弃
@Retention(RetentionPolicy.SOURCE)
public @interface Testable{}

上面代码中使用@Retention元注解时,并未通过value = RetentionPolicy.SOURCE的方式来为该成员变量指定值,这是因为当注解的成员名为value时,程序中可以直接在注解后的括号里指定该成员变量的值,无须使用name=value形式。
如果使用注解时只需要为value成员变量指定值,则使用该注解时可以直接在该注解后的括号里指定value成员变量的值,无须使用"value = 变量值"的形式

使用@Target

@Target也只能修饰注解定义,它用于指定被修饰的注解能用于修饰哪些程序单元。@Target元注解也包含一个名为value的成员变量,该成员变量的值只能是如下几个。

  • ElementType.ANNOTATION_TYPE:指定该策略的注解只能修饰注解。
  • ElementType.CONSTRUCTOR:指定该策略的注解只能修饰构造器。
  • ElementType.FIELD:指定该策略的注解只能修饰成员变量。
  • ElementType.LOCAL_VARIABLE:指定该策略的注解只能修饰局部变量。
  • ElementType.METHOD:指定该策略的注解只能修饰方法定义。
  • ElementType.PACKAGE:指定该策略的注解只能修饰包定义。
  • ElementType.PARAMETER:指定该策略的注解可以修饰参数。
  • ElementType.TYPE:该策略的注解可以修饰类、接口(包括注解类型)或枚举定义。
    与使用@Retention类似的是,使用@Target也可以直接在括号里指定value值,而无须使用name = value 形式。如下代码指定@ActionListenerFor注解只能修饰成员变量。
@Target(ElementType.FIELD)
public @interface ActionListenerFor{}

如下代码片段指定@Testable注解只能修饰方法。

@Target(ElementType.METHOD)
public @interface Testable{}
使用@Documented

@Documented用于指定被该院注解修饰的注解类被javadoc工具类提取成文档,如果定义注解类时使用了@Doocumented修饰,则所有使用该注解修饰的程序元素的API文档中将会包含该注解说明。
下面代码定义了一个Testable注解。程序使用@Documented来修饰@Testable注解定义,所以该注解将被javadoc工具所提取。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
//定义Testable注解将被javadoc工具提取
@Documented
public @interface Testable{}

上面代码中指定了javadoc工具生成的API文档将提取@Testable的使用信息。
下面代码定义了一个MyTest类,该类中的info()方法使用了@Testable修饰

public class MyTest
{
	//使用@Testable修饰info()方法
	@Testable修饰info()方法
	@Testable
	public void info(){
		System.out.print("info方法...");
	}
}
使用Inherited

@Inherited元注解指定被它修饰的注解将具有继承性——如果某个类使用了@Xxx注解(定义该注解时使用了@Inherited修饰)修饰,则其子类将自动被@Xxx修饰。
下面使用@Inherited元注解修饰@Inheritable定义,则该注解将具有继承性。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inheritable {
}

上面程序表明@Inheritable具有继承性,如果某个类使用了@Inheritable修饰,则该类的子类将自动使用@Inheriatble修饰
下面程序中定义了一个Base基类,该基类使用了@Inheritable修饰,则Base类的子类将会默认使用@Inheritable修饰。

//使用Inheritable修饰的Base类
@Inheritable
class Base{

}
//InheritableTest类只是继承了Base类
//并未直接使用@Inheritable注解修饰
public class InheritableTest extends Base{
    public static void main(String[] args) {
        //打印InheritableTest类是否有@Inheritable修饰
        System.out.println(InheritableTest.class.isAnnotationPresent(Inheritable.class));
    }

}
自定义注解 定义注解

定义新注解类型使用@interface关键字(在原有的interface关键字前增加@符号)定义一个新的注解类型与定义一个接口非常像,如下代码可定义一个简单的注解类型。

//定义一个简单的注解类型
public @interface Test
{
}

定义了该注解之后,就可以在程序的任意地方使用该注解,使用注解的语法非常类似于public、final这样的修饰符,通常可用于修饰程序中的类、方法、变量、接口等等一。通常会把注解放在所有修饰符之前,而且由于使用注解时可能还需要为成员变量指定值,因而注解的长度可能较长,所以通常把注解另放一行,如下程序所示。

//使用@Test修饰类定义
@Test
public class MyClass{
	...
}

在默认情况下,注解可用于修饰任何程序元素,包括类、接口、方法等,如下程序使用@Test来修饰方法。

public class MyClass{
	//使用@Test注解修饰方法
	@Test
	public void info(){
		...
	}
}

注解不仅可以是这种简单的注解,还可以带成员变量,成员变量在注解定义中以无形参的方法形式来声明,其方法名和返回值定义了该成员变量的名字和类型。如下代码可以定义一个有成员变量的注解。

public @interface MyTag{
	//定义带两个成员变量的注解
	//注解中的成员变量以方法的形式来定义
	String name();
	int age();
}

可能有读者会看出,上面定义注解的代码与定义接口的语法非常像,只是MyTag使用@interface关键字来定义,而接口使用Interface来定义。
使用@interface定义的注解的确非常像定义了一个注解接口,这个注解接口继承了java.lang.annotation.Annotation接口,这一点可以通过反射看到MyTag接口里包含了java.lang.annotation.Annotation接口里的方法。
一旦在注解里定义了成员变量之后,使用注解时就应该为它的成员变量指定值,如下面代码所示。

public class Test{
	//使用带成员变量的注解时,需要为成成员变量赋值
	@MyTag(name ='xx',age = 6)
	public void info(){
		...
	}
	...
}

也可以第你故意注解的成员变量时为其指定初始值(默认值),指定成员变量的初始值可使用default关键字。如下代码定义了@MyTag注解,该注解里包含了两个成员变量:name和age,这两个成员变量使用default指定了初始值。

public @interface MyTag{
	//定义了两个成员变量的注解
	//使用default为两个成员变量指定初始值
	String name() default "yeeku";
	int age() default 32;

如果为注解的成员变量指定了默认值,使用该注解时则可以不为这些成员变量指定值,而是直接使用默认值。

public class Test{
	//使用带成员变量的注解
	//因为它的成员变量有默认值,所以可以不为它的成员变量指定值
	@MyTag
	public void info(){
		...
	}
	...
}

当然也可以在使用MyTag注解时为成员变量指定值,如果为MyTag的成员变量指定了默认值,则默认值不会起作用。
根据注解是否可以包含成员变量,可以把注解分为如下两类。

  • 标记注解:没有定义成员变量的注解类型被称为标记。这种注解仅利用自身的存在于否来提供信息,如前面介绍的@Override、@Test等注解。
  • 元数据注解:包含成员变量的注解,因为它们可以接受更多的元数据,所以也被称为元数据注解。
提取注解信息

使用注解修饰了类、方法、成员方法等成员之后,这些注解不会自己生效,必须由开发者提供相应的工具来提取并处理注解信息。
Java使用java.lang.annotation.Annotation接口来代替程序元素前面的注解,该接口是所有注解的父接口。Java5在java.lang.reflect包下新增了AnnotatedElement接口,该接口代表程序中可以接受注解的程序元素。该接口主要有如下几个实现类。

  • Class:类定义。
  • Constructor:构造器定义。
  • Field:类的成员变量定义
  • Method:类的方法定义。
  • Package:类的包定义。

java.lang.reflect包下主要包含一些实现反射功能的工具类,从Java5开始,java.lang.reflect包所提供的反射API增加了读取运行时注解的功能。只有当定义注解时使用了@Retention(RetentionPolicy.RUNTIME)修饰,该注解才会在运行时课件,JVM才会装在*.class文件时读取保存在class文件中的注解信息。
AnnotateElement接口是所有程序元素(如Class、Method、Constructor等)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象(如Class、Method、Constructor等)之后,程序就可以调用该对象的如下几个方法来访问注解信息。

  • <,A extends Annotation > A get Annotation(Class
  • <,A extends Annotaition>A getDeclaredAnnotation(Class
  • Annotation[]getAnnotations():返回该程序元素上存在的所有注解。
  • Annotation[]getDeclaredAnnotations():返回直接修饰该程序元素的所有注解。
  • boolean isAnnotationPresent(ClassannotationClass):判断该程序元素上是否存在指定类型的注解,如果存在则返回true,否则返回false。
  • <,A extends Annotation>A[]getAnnotationsByType(ClassannotationClass):该方法的功能与前面介绍的getAnnotation()方法基本相似。但由于Java8增家了重复注解功能,因此需要使用该方法获取修饰该程序元素、指定类型的多个注解。
  • <,A extends Annotation>:A[]getDeclaredAnnotationsByType(Class<,A>annotationClass):该方法的功能与前面介绍的getDeclaredAnnotations()方法基本相似。但由于Java8增家了重复注解功能,因此需要使用该方法获取直接修饰该程序元素、指定类型的多个注解。

为了获得程序中程序元素(如Class、Method等),必须使用反射知识
下面程序片段用于获取Test类的info方法里的所有注解,并将这些注解打印出来。

//获取Test类的info方法的所有注解
Annotation[]aArray = Class.forName("Test").getMethod("info").getAnnotations();
//遍历所有的注解
for(var an: aArry){
	System.out.printnl(an);
}

如果需要获取某个注解里的元数据,则可以将注解强制类型转换成需要的注解类型,然后通过注解对象的抽象方法来访问这些元数据将:

import java.lang.annotation.Annotation;

//使用Inheritable修饰的Base类
@Inheritable
class Base{

}
//InheritableTest类只是继承了Base类
//并未直接使用@Inheritable注解修饰
public class InheritableTest extends Base{
    public static void main(String[] args) {
        //获取tt对象额info方法所包含的所有注解
        Annotation[]annotations=tt.getClass().getMethod("info").getAnnotations();
        for(var tag:annorarion){
            if(tag instanceof MyTag1){
                System.out.println("Tag is:"+tag);
                System.out.println("tag.name():"+((MyTag1)tag).method1());
                System.out.println("tag.age():"+((MyTag1)(tag)).method2());
            }
            if(tag instanceof MyTql2){
                System.out.println("Tag is:"+tag);
                System.out.println("tag.name():"+((MyTag2)tag).method2());
                System.out.println("tag.age():"+((MyYag2)(tag)).method2());
            }
        }
    }

}
使用注解的示例
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/877509.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号