第九章 Annotation(注解)
在JDK5开始,Java增加了对元数据(MetaData)的支持,也就是Annotation(注解)
解释一下——元数据(MetaData),又称中介数据,中继数据,是描述信息的信息,主要描述数据的属性
Annotation其实是代码里的特殊标记,通过这些标记,可以在编译,类加载,运行时被读取,并执行相应的处理
Annotation提供了为程序元素设置元数据的方法,可以用来修饰包,类,构造器,方法,成员变量,参数,局部变量,这些信息被存储在Annotation的键值对中
Annotation是一个接口,程序可以通过反射来获取它,然后在获取到的Annotation对象中的元数据
Annotation是存储元素信息的,所以不会对程序运行造成影响,如果需要Annotation在运行时起到一定作用,需要通过某种配套工具对Annotation进行处理,
对Annotation中的信息进行访问,处理,的工具统称为APT(Annotation Processing Tool)
9.1 基本Annotation
java.lang包下提供的4个基本Annotation注解
@Override
@Deprecated
@SuppressWarnings
@SafeVarargs
9.1.1 限定重写父类方法:@Override
强制一个子类必须覆盖(重写)父类的方法
@Override注解,表示子类必须重写父类的方法,如果一个方法的名字特别难记,在重写时不小心写错了一个字母,编译运行不会发现错误,只有在程序崩溃时才会发现,排查又特别困难,所以用@Override就可以解决这个问题,如果需要被重写的方法,在编译时编译器没有发现,则会报错
9.1.2 标示已过时:@Deprecated
用于修饰程序的某个元素,如方法,类等,如果在别的程序中使用,被@Deprecated修饰的元素,编译器会警告,该元素以过期(废弃)
和这个注解类似的,还有一个注解
@deprecated 用于文档注释,表示一个方法或类等,已过期(废弃)
9.1.3 抑制编译器警告:@SuppressWarnings
@SuppressWarnings可以取消编译器的警告,并且被它修饰的元素,以及这个元素的所有子元素,都会取消(关闭)编译器警告
在使用时,要使用键值对来设置关闭的警告,
如:
@SuppressWarnings(values=“unchecked”)
取消(关闭)了所有的编译器警告
9.1.4 Java 7的“堆污染”警告与@SafeVarargs
将一个不带泛型信息的集合,赋值给一个带泛型的集合,陈为转换,反过来就是擦除
当程序发生 转换/擦除 时,往往会发生“堆污染”
当方法的形参是,泛型,又是个数可变的时,更容易发生堆污染
当方法的形参是个数可变的,则相当于形参是数组,并且又是泛型,但系统不允许创建泛型数组,所以这里默认有擦除
这种转换在赋值时不会发生错误,但只要访问,就会出错
可以使用一些方法抑制编译器的警告,但程序运行到错误的地方该停还会停
可以使用@SafeVarargs来修饰引发该警告的方法或构造器
或使用@SupperssWarnings(”unchecked“)修饰,
9.2 JDK的元Annotation
java还在java.lang.annotation包下提供了4个Meta Annotation(元Annotation),这个4个注解用于修饰其他注解
9.2.1 使用@Retention
用于修饰一个Annotation,指定被修饰的Annotation可以保存多长时间
使用格式 @Retention(值)
这个值有三种:
RerentionPolicy.CLASS 编译器把Annotation记录在class文件中,当运行时,JVM不再保留Anntation,默认值
RetentionPoilcy.RUNTIME 编译器把Annotation记录在class文件中,运行时,JVM会保留Annotation,程序可以通过反射获得Annotation
RetentionPolicy.SOURCE 只把Annotation保留在源码中,编译器直接丢弃Annotation
9.2.2 使用@Target
用于修饰一个Annotation,指定被修饰的Annotation可以修饰那些程序单元,
格式: @Target(值)
值的选择范围:
ElementType.ANNOTATION_TYPE 只能修饰Annotation
ElementType.CONSTRUCTOR 只能修饰构造器
ElementType.FIELD 只能修饰成员变量
ElementType.LOCAL_VARIABLE 只能修饰局部变量
ElementType.METHOD 只能修饰方法定义
ElementType.PACKAGE 只能修饰包定义
ElementType.PARAMETER 可以修饰参数
ElementType.TYPE 可以修饰类,接口(包括注解类型)或枚举类定义
9.2.3 使用@Documented
被修饰的注解,将会被javadoc文档注释识别,提取后出现在API文档中
9.2.4 使用@Inherited
被修饰的注解将具有继承性,如果有一个被修饰的注解A,注解A修饰了B类,B类的子类C将自动拥有注解A,因为注解A被@Inherited修饰
9.3 自定义Annotation
9.3.1 定义Annotation
定义一个注解,需要用到关键字@interface(就是接口的关键字,加了个@)
可以在定义的注解内定义成员变量,并赋初始值
格式为:类型 名字() default 默认值;
如果定义了成员变量,在使用该注解时,应该为变量赋值,如有默认值,也可忽略
基本格式:
权限修饰符 @interface 名字{
类型 名字()default 默认值;
……
}
定义的这个注解,其实继承了Annotation接口
根据Annotation内是否有成员变量,可分为两类:
标记Annotation 利用自身存在与否来提供信息
元数据Annotation 可以利用成员变量来提供更多数据
9.3.2 提取Annotation信息
当使用注解修饰一个程序元素后,注解并不会自己生效,而是需要开发者提供对应的工具来提取和处理这些Annotaion
java5在java.lang.reflect包下新增了Annotation接口,该接口可以接受注解的程序元素
该接口的实现类
Class 类定义
Constructor 构造器定义
Field 类的成员变量定义
Method 类的方法定义
Package 类的包定义
当一个注解被@Retention( RetentionPoilcy.RUNTIME)修饰时(运行时保存注解),才能在JVM运行时,通过反射获取注解信息
AnnotatedElement接口是所有程序元素(如Class、Method、Constructor等)的父接口
如果在运行时获取到了某个类的AnnotationElement对象(如Class,Method,Constructor),就可以使用方法来获取注解的信息
AnnotationElenemt对象的方法:
getAnnotation(Class<T>annotationClass) 返回程序元素上存在的指定类型的注解,如无,返回null
getAnnotaions() 返回该程序元素上存在的所有注解(返回值是一个Annotation数组)
isAnnotaionPresent(Class<? extends Annotation>annotationClass) 判断该程序元素上是否存在指定类型的注解(返回值为boolean类型)
9.3.3 使用Annotation的示例
这里的例子比较复杂,使用到了反射,大概简述一下
自己手动创建注解,使用java提供的4个Meta Annotaion来修饰,之后在程序加载到JVM中时,通过反射获取注解,然后执行对应的方法来处理注解修饰的内容
9.4 编译时处理Annotation
APT(注解处理器)是一种处理注解的工具,APT会在文件编译阶段,对源文件进行检测,找到Annotation并对其进行额外处理,
开发者可以自定义自己的Annotation处理器,两种方法
实现javax.annotation.processing包下的Processor接口
继承AbstractProcessor(推荐,开发量较小)
APT可以根据注解,(先编写java,后编译)生成class文件或其他文件,有一定的价值,那么我猜测,spring中的注解,其实就是自己的编写的Annotation处理器来生成类对象,从而实现控制反转
在java编译时时,javac.exe命令有 -processor选项,可以指定一个Annotation处理器
关于APT的扩展
注解处理器运行在它自己的 JVM 中。
是的,你没看错。javac 启动了一个完整的 java 虚拟机来运行注解处理器。
这意味着什么?你可以使用任何你在普通 java 程序中使用的东西。
使用 guava! 你可以使用依赖注入工具,比如dagger或者任何其他你想使用的类库
扩展-关于一个诡异的输出
String str = ",a,,b,";
String[] splitArr = str.split(",");
Arrays.stream(splitArr).forEach(System.out::println);
这段代码将遍历字符数组输出,a,b 其中的双冒号输出语句是函数式接口的简写
上面先是创建了一个字符串,通过split方法切割,形成了一个新的string数组
再通过Arrays.stream(),将一个字符串数组转换为“流”(值得注意的是,stream也是java8新增加的特性),再使用forEach 遍历
在java8时还增加了一种新的特性,称为 Lambda表达式和函数式接口,这种新特新规定
仅仅有一个未实现方法的接口,可以直接写作(参数列表) -> {方法体}这种形式
例如:@FunctionalInterface
public interface FuncA {
void doSomeThing(String str);}
那么上面这种接口就可以直接写作:
FuncA funcA = (str) -> {System.out.println(“hello”);};
forEach方法提供一个某种类型的Object,而System.out.println可以接受一个Object
因此,forEach提供的参数和System.out.println的参数类型是一致的,可以进行这种简写。
具体来说就是:原本应该写为:.forEach(element -> {System.out.println(element)})
但System.out.println的参数和传递的参数element 的类型完全匹配,
所以这样的时候就可以简化为:.forEach(System.out::println)
<br>
总结一下,这里就是 函数式接口 -> Lambda表达式 -> 方法引用
java的并行API发展
Java 的并行 API 演变历程基本如下:
- 1.0-1.4 中的 java.lang.Thread
- 5.0 中的 java.util.concurrent
- 6.0 中的 Phasers 等
- 7.0 中的 Fork/Join 框架
- 8.0 中的 Lambda
## 后言 月亮正亮的起劲,若此刻不想你,倒显得的我不解风情