# 《Java8新特性》第16章:重复注解与类型注解

# 写在前面

在Java8之前,在某个类或者方法,字段或者参数上标注注解时,同一个注解只能标注一次。但是在Java8中,新增了重复注解和类型注解,也就是说,从Java8开始,支持在某个类或者方法,字段或者参数上标注多个相同的注解。那么,有读者就会问了:如何实现呢?别急,往下看!文中不只是Java8中的注解。

# JDK5中的注解

# 1.注解(@)

注解就相当于一种标记,在程序中加了注解就等于为程序加了某种标记。(JDK1.5新特性)。

# 2.作用

告诉javac编译器或者java开发工具……向其传递某种信息,作为一个标记。

# 3.如何理解注解?

一个注解就是一个类。

标记可以加在包、类、字段、方法,方法参数以及局部变量上。可以同时存在多个注解。

每一个注解结尾都没有“;”或者其他特别符号。

定义注解需要的基础注解信息如下所示。

@SuppressWarnings("deprecation")  //编译器警告过时(source阶段)
@Deprecated						//过时(Runtime阶段)
@Override						//重写(source阶段)
@Retention(RetentionPolicy.RUNTIME)	
//保留注解到程序运行时。(Runtime阶段)
@Target({ElementType.METHOD,ElementType.TYPE})
//标记既能定义在方法上,又能定义在类、接口、枚举上等。
1
2
3
4
5
6
7

注意:

1)添加注解需要有注解类。RetentionPolicy是一个枚举类(有三个成员)。

2)Target中可以存放数组。它的默认值为任何元素。

  • ElementType.METHOD:表示只能标记在方法上。

  • ElementType.TYPE:表示只能标记定义在类上、接口上、枚举上等

    3)ElementType也是枚举类。成员包括:ANNOTATION_TYPE(注解)、CONSTRUCTOR(构造方法)、FIEID(成员变量)、LOCAL_VARIABLE(变量)、METHOD(方法)、PACKAGE(包)、PARAMETER(参数)、TYPE。

# 4.关于注解

  • 元注解:注解的注解(理解:给一个注解类再加注解)

  • 元数据:数据的数据

  • 元信息:信息的信息

# 5.注解分为三个阶段

java源文件--> class文件 --> 内存中的字节码。

Retention的注解有三种取值:(分别对应注解的三个阶段)

  • RetentionPolicy.SOURCE

  • RetentionPolicy.CLASS

  • RetentionPolicy.RUNTIME

注意:注解的默认阶段是Class。

# 6.注解的属性类型

原始类型(就是八个基本数据类型)、String类型、Class类型、数组类型、枚举类型、注解类型。

# 7.为注解增加属性

value:是一个特殊的属性,若在设置值时只有一个value属性需要设置或者其他属性都采用默认值时 ,那么value=可以省略,直接写所设置的值即可。

例如:@SuppressWarnings("deprecation")

为属性指定缺省值(默认值):
例如:String value() default "blue"; //定义在注解类中

数组类型的属性:
例如:int[] arrayArr() default {3,4,5,5};//定义在注解类中
SunAnnotation(arrayArr={3,9,8}) //设置数组值
注意:如果数组属性中只有一个元素时,属性值部分可以省略大括号。
例如:SunAnnotation(arrayArr=9)

枚举类型的属性:
例如:EnumDemo.TrafficLamp lamp()
////枚举类型属性, 定义在注解类中,这里使用了自定义的枚举类EnumDemo.java并没有给出相关代码,这里只是举个例子
default EnumDemo.TrafficLamp.RED;

注解类型的属性:
例如:MetaAnnotation annotationAttr()
//定义在一个注解类中,并指定缺省值,
//此属性关联到注解类:MetaAnnotation.java, 
default @MetaAnnotation("lhm");
//设置注解属性值
@SunAnnotation(annotationAttr=@MetaAnnotation("flx"))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# Java8中的注解

对于注解(也被称做元数据),Java 8 主要有两点改进:类型注解和重复注解。

# 1.类型注解

1)Java 8 的类型注解扩展了注解使用的范围。

在java 8之前,注解只能是在声明的地方所使用,java8开始,注解可以应用在任何地方。

例如:

创建类实例

new @Interned MyObject();
1

类型映射

myString = (@NonNull String) str;
1

implements 语句中

class UnmodifiableList<T> implements@Readonly List<@Readonly T> { ... }
1

throw exception声明

void monitorTemperature() throws@Critical TemperatureException { ... }
1

注意:

在Java 8里面,当类型转化甚至分配新对象的时候,都可以在声明变量或者参数的时候使用注解。 Java注解可以支持任意类型。

类型注解只是语法而不是语义,并不会影响java的编译时间,加载时间,以及运行时间,也就是说,编译成class文件的时候并不包含类型注解。

2)新增ElementType.TYPE_USE 和ElementType.TYPE_PARAMETER(在Target上)

新增的两个注释的程序元素类型 ElementType.TYPE_USE 和 ElementType.TYPE_PARAMETER用来描述注解的新场合。

  • ElementType.TYPE_PARAMETER 表示该注解能写在类型变量的声明语句中。

  • ElementType.TYPE_USE 表示该注解能写在使用类型的任何语句中(例如:声明语句、泛型和强制转换语句中的类型)。

例如,下面的示例。

@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}
1
2

3)类型注解的作用

类型注解被用来支持在Java的程序中做强类型检查。配合第三方插件工具Checker Framework(注:此插件so easy,这里不介绍了),可以在编译的时候检测出runtime error(例如:UnsupportedOperationException; NumberFormatException;NullPointerException异常等都是runtime error),以提高代码质量。这就是类型注解的作用。

注意:使用Checker Framework可以找到类型注解出现的地方并检查。

例如下面的代码。

import checkers.nullness.quals.*;
public class TestDemo{
    void sample() {
        @NonNull Object my = new Object();
    }
}
1
2
3
4
5
6

使用javac编译上面的类:(当然若下载了Checker Framework插件就不需要这么麻烦了)

javac -processor checkers.nullness.NullnessChecker TestDemo.java
1

上面编译是通过的,但若修改代码:

@NonNull Object my = null;
1

但若不想使用类型注解检测出来错误,则不需要processor,正常javac TestDemo.java是可以通过编译的,但是运行时会报 NullPointerException 异常。

为了能在编译期间就自动检查出这类异常,可以通过类型注解结合 Checker Framework 提前排查出来错误异常。

注意java 5,6,7版本是不支持注解@NonNull,但checker framework 有个向下兼容的解决方案,就是将类型注解@NonNull 用/**/注释起来。

import checkers.nullness.quals.*;
public class TestDemo{
    void sample() {
        /*@NonNull*/ Object my = null;
    }
}
1
2
3
4
5
6

这样javac编译器就会忽略掉注释块,但用checker framework里面的javac编译器同样能够检测出@NonNull错误。 通过 类型注解 + checker framework 可以在编译时就找到runtime error。

# 2.重复注解

允许在同一声明类型(类,属性,或方法)上多次使用同一个注解。

Java8以前的版本使用注解有一个限制是相同的注解在同一位置只能使用一次,不能使用多次。

Java 8 引入了重复注解机制,这样相同的注解可以在同一地方使用多次。重复注解机制本身必须用 @Repeatable 注解。

实际上,重复注解不是一个语言上的改变,只是编译器层面的改动,技术层面仍然是一样的。

例如,我们可以使用如下示例来具体对比Java8之前的版本和Java8中的注解。

**1)**自定义一个包装类Hints注解用来放置一组具体的Hint注解

@interface MyHints {
    Hint[] value();
}
 
@Repeatable(MyHints.class)
@interface Hint {
    String value();
}
1
2
3
4
5
6
7
8

使用包装类当容器来存多个注解(旧版本方法)

@MyHints({@Hint("hint1"), @Hint("hint2")})
class Person {}
1
2

使用多重注解(新方法)

@Hint("hint1")
@Hint("hint2")
class Person {}
1
2
3

**2)**完整类测试如下所示。

public class RepeatingAnnotations {
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Filters {
        Filter[] value();
    }
    
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Repeatable(Filters.class)
    public @interface Filter {
        String value();
    }
    @Filter("filter1")
    @Filter("filter2")
    public interface Filterable {
    }
    public static void main(String[] args) {
        for (Filter filter : Filterable.class.getAnnotationsByType(Filter.class)) {
            System.out.println(filter.value());
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

输出结果:

filter1
filter2
1
2

分析:

注释Filter被@Repeatable( Filters.class )注释。Filters 只是一个容器,它持有Filter, 编译器尽力向程序员隐藏它的存在。通过这样的方式,Filterable接口可以被Filter注释两次。

另外,反射的API提供一个新方法getAnnotationsByType() 来返回重复注释的类型(注意Filterable.class.getAnnotation( Filters.class )将会返回编译器注入的Filters实例。

**3)**java 8之前也有重复使用注解的解决方案,但可读性不好。

public @interface MyAnnotation {  
     String role();  
}  
 
public @interface Annotations {  
    MyAnnotation[] value();  
}  
 
public class RepeatAnnotationUseOldVersion {  
    @Annotations({@MyAnnotation(role="Admin"),@MyAnnotation(role="Manager")})  
    public void doSomeThing(){  
    }  
}
1
2
3
4
5
6
7
8
9
10
11
12
13

Java8的实现方式(由另一个注解来存储重复注解,在使用时候,用存储注解Authorities来扩展重复注解),可读性更强。

@Repeatable(Annotations.class) 
public @interface MyAnnotation {  
     String role();  
}  
 
public @interface Annotations {  
    MyAnnotation[] value();  
}  
 
public class RepeatAnnotationUseOldVersion {  
	@MyAnnotation(role="Admin")  
    @MyAnnotation(role="Manager")
    public void doSomeThing(){  
    }  
} 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

什么?没看懂?那就再来一波!!!

# Java8对注解的增强

Java 8对注解处理提供了两点改进:可重复的注解及可用于类型的注解。总体来说,比较简单,下面,我们就以实例的形式来说明Java8中的重复注解和类型注解。

首先,我们来定义一个注解类BingheAnnotation,如下所示。

package io.mykit.binghe.java8.annotition;

import java.lang.annotation.*;

/**
 * @author binghe
 * @version 1.0.0
 * @description 定义注解
 */
@Repeatable(BingheAnnotations.class)
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.LOCAL_VARIABLE,ElementType.TYPE_PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface BingheAnnotation {
    String value();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

注意:在BingheAnnotation注解类上比普通的注解多了一个@Repeatable(BingheAnnotations.class)注解,有小伙伴会问:这个是啥啊?这个就是Java8中定义可重复注解的关键,至于BingheAnnotations.class,大家别急,继续往下看就明白了。

接下来,咱们定义一个BingheAnnotations注解类,如下所示。

package io.mykit.binghe.java8.annotation;

import java.lang.annotation.*;

/**
 * @author binghe
 * @version 1.0.0
 * @description 定义注解
 */
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.LOCAL_VARIABLE,ElementType.TYPE_PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface BingheAnnotations {
    BingheAnnotation[] value();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

看到这里,大家明白了吧!!没错,BingheAnnotations也是一个注解类,它相比于BingheAnnotation注解类来说,少了一个@Repeatable(BingheAnnotations.class)注解,也就是说,BingheAnnotations注解类的定义与普通的注解几乎没啥区别。值得注意的是,我们在BingheAnnotations注解类中,定义了一个BingheAnnotation注解类的数组,也就是说,在BingheAnnotations注解类中,包含有多个BingheAnnotation注解。所以,在BingheAnnotation注解类上指定@Repeatable(BingheAnnotations.class)来说明可以在类、字段、方法、参数、构造方法、参数上重复使用BingheAnnotation注解。

接下来,我们创建一个Binghe类,在Binghe类中定义一个init()方法,在init方法上,重复使用@BingheAnnotation注解指定相应的数据,如下所示。

package io.mykit.binghe.java8.annotation;

/**
 * @author binghe
 * @version 1.0.0
 * @description 测试注解
 */
@BingheAnnotation("binghe")
@BingheAnnotation("class")
public class Binghe {

    @BingheAnnotation("init")
    @BingheAnnotation("method")
    public void init(){

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

到此,我们就可以测试重复注解了,创建类BingheAnnotationTest,对重复注解进行测试,如下所示。

package io.mykit.binghe.java8.annotation;

import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * @author binghe
 * @version 1.0.0
 * @description 测试注解
 */
public class BingheAnnotationTest {

    public static void main(String[] args) throws NoSuchMethodException {
        Class<Binghe> clazz = Binghe.class;
        BingheAnnotation[] annotations = clazz.getAnnotationsByType(BingheAnnotation.class);
        System.out.println("类上的重复注解如下:");
        Arrays.stream(annotations).forEach((a) -> System.out.print(a.value() + " "));

        System.out.println();
        System.out.println("=============================");

        Method method = clazz.getMethod("init");
        annotations = method.getAnnotationsByType(BingheAnnotation.class);
        System.out.println("方法上的重复注解如下:");
        Arrays.stream(annotations).forEach((a) -> System.out.print(a.value() + " "));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

运行main()方法,输出如下的结果信息。

类上的重复注解如下:
binghe class 
=============================
方法上的重复注解如下:
init method 
1
2
3
4
5

# 星球服务

加入星球,你将获得:

1.项目学习:微服务入门必备的SpringCloud Alibaba实战项目、手写RPC项目—所有大厂都需要的项目【含上百个经典面试题】、深度解析Spring6核心技术—只要学习Java就必须深度掌握的框架【含数十个经典思考题】、Seckill秒杀系统项目—进大厂必备高并发、高性能和高可用技能。

2.框架源码:手写RPC项目—所有大厂都需要的项目【含上百个经典面试题】、深度解析Spring6核心技术—只要学习Java就必须深度掌握的框架【含数十个经典思考题】。

3.硬核技术:深入理解高并发系列(全册)、深入理解JVM系列(全册)、深入浅出Java设计模式(全册)、MySQL核心知识(全册)。

4.技术小册:深入理解高并发编程(第1版)、深入理解高并发编程(第2版)、从零开始手写RPC框架、SpringCloud Alibaba实战、冰河的渗透实战笔记、MySQL核心知识手册、Spring IOC核心技术、Nginx核心技术、面经手册等。

5.技术与就业指导:提供相关就业辅导和未来发展指引,冰河从初级程序员不断沉淀,成长,突破,一路成长为互联网资深技术专家,相信我的经历和经验对你有所帮助。

冰河的知识星球是一个简单、干净、纯粹交流技术的星球,不吹水,目前加入享5折优惠,价值远超门票。加入星球的用户,记得添加冰河微信:hacker_binghe,冰河拉你进星球专属VIP交流群。

# 星球重磅福利

跟冰河一起从根本上提升自己的技术能力,架构思维和设计思路,以及突破自身职场瓶颈,冰河特推出重大优惠活动,扫码领券进行星球,直接立减149元,相当于5折, 这已经是星球最大优惠力度!


领券加入星球,跟冰河一起学习《SpringCloud Alibaba实战》、《手撸RPC专栏》和《Spring6核心技术》,更有已经上新的《大规模分布式Seckill秒杀系统》,从零开始介绍原理、设计架构、手撸代码。后续更有硬核中间件项目和业务项目,而这些都是你升职加薪必备的基础技能。

100多元就能学这么多硬核技术、中间件项目和大厂秒杀系统,如果是我,我会买他个终身会员!

# 其他方式加入星球

特别提醒: 苹果用户进圈或续费,请加微信 hacker_binghe 扫二维码,或者去公众号 冰河技术 回复 星球 扫二维码加入星球。

# 星球规划

后续冰河还会在星球更新大规模中间件项目和深度剖析核心技术的专栏,目前已经规划的专栏如下所示。

# 中间件项目

  • 《大规模分布式定时调度中间件项目实战(非Demo)》:全程手撸代码。
  • 《大规模分布式IM(即时通讯)项目实战(非Demo)》:全程手撸代码。
  • 《大规模分布式网关项目实战(非Demo)》:全程手撸代码。
  • 《手写Redis》:全程手撸代码。
  • 《手写JVM》全程手撸代码。

# 超硬核项目

  • 《从零落地秒杀系统项目》:全程手撸代码,在阿里云实现压测(已上新)。
  • 《大规模电商系统商品详情页项目》:全程手撸代码,在阿里云实现压测。
  • 其他待规划的实战项目,小伙伴们也可以提一些自己想学的,想一起手撸的实战项目。。。

既然星球规划了这么多内容,那么肯定就会有小伙伴们提出疑问:这么多内容,能更新完吗?我的回答就是:一个个攻破呗,咱这星球干就干真实中间件项目,剖析硬核技术和项目,不做Demo。初衷就是能够让小伙伴们学到真正的核心技术,不再只是简单的做CRUD开发。所以,每个专栏都会是硬核内容,像《SpringCloud Alibaba实战》、《手撸RPC专栏》和《Spring6核心技术》就是很好的示例。后续的专栏只会比这些更加硬核,杜绝Demo开发。

小伙伴们跟着冰河认真学习,多动手,多思考,多分析,多总结,有问题及时在星球提问,相信在技术层面,都会有所提高。将学到的知识和技术及时运用到实际的工作当中,学以致用。星球中不少小伙伴都成为了公司的核心技术骨干,实现了升职加薪的目标。

# 联系冰河

# 加群交流

本群的宗旨是给大家提供一个良好的技术学习交流平台,所以杜绝一切广告!由于微信群人满 100 之后无法加入,请扫描下方二维码先添加作者 “冰河” 微信(hacker_binghe),备注:星球编号

冰河微信

# 公众号

分享各种编程语言、开发技术、分布式与微服务架构、分布式数据库、分布式事务、云原生、大数据与云计算技术和渗透技术。另外,还会分享各种面试题和面试技巧。内容在 冰河技术 微信公众号首发,强烈建议大家关注。

公众号:冰河技术

# 视频号

定期分享各种编程语言、开发技术、分布式与微服务架构、分布式数据库、分布式事务、云原生、大数据与云计算技术和渗透技术。另外,还会分享各种面试题和面试技巧。

视频号:冰河技术

# 星球

加入星球 冰河技术 (opens new window),可以获得本站点所有学习内容的指导与帮助。如果你遇到不能独立解决的问题,也可以添加冰河的微信:hacker_binghe, 我们一起沟通交流。另外,在星球中不只能学到实用的硬核技术,还能学习实战项目

关注 冰河技术 (opens new window)公众号,回复 星球 可以获取入场优惠券。

知识星球:冰河技术