Java学习之注解
一、什么是注解
注解简单点就可以理解为是一种标签,标识某物是什么,用途场景又是什么……我们可以联想下工厂里的物料仓库,为了区分各种各样的物料,往往会对物料进行不同的分类,标识物料名称、类型、用途、数量等等。其实Java中的注解也是类似的,在代码中是一种特殊标记,这种标记是在编译时、运行时、还是类加载时被读取,并执行相应的处理。
二、注解语法(自定义注解)
注解的定义
定义一个新的注解类型使用@interface关键字,有点类型定义接口,不过在interface关键字前添加了@符号
//定义一个@Test的新注解
public @interface Test{
}
一个注解可以理解成一张标签,下面就是注解的使用了
注解的使用
//使用@Test注解修饰类
@Test
public class MyTest{
//使用@Test注解修饰方法
@Test
private void info(){
}
}
三、元注解
元注解是用于修饰其他的注解的,能够运用到其他注解上面去。
元注解相当于一种特殊标记,是可以给其他普通的标签进行解释说明;再简单的点,就是一款产品的说明书,描述产品各个细节的要点。
元标签有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 种。@Retention
Retention 的英文意为保留期的意思。当 @Retention 应用到一个注解上的时候,它解释说明了这个注解的的存活时间。
它的取值如下:
- RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
- RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
- RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。
我们可以这样的方式来加深理解,@Retention 去给一张标签解释的时候,它指定了这张标签张贴的时间。@Retention 相当于给一张标签上面盖了一张时间戳,时间戳指明了标签张贴的时间周期。
@Documented
顾名思义,这个元注解肯定是和文档有关。它的作用是能够将注解中的元素包含到 Javadoc 中去。
@Target
Target 是目标的意思,@Target 指定了注解运用的地方。
你可以这样理解,当一个注解被 @Target 注解时,这个注解就被限定了运用的场景。
类比到标签,原本标签是你想张贴到哪个地方就到哪个地方,但是因为 @Target 的存在,它张贴的地方就非常具体了,比如只能张贴到方法上、类上、方法参数上等等。@Target 有下面的取值
- ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
- ElementType.CONSTRUCTOR 可以给构造方法进行注解
- ElementType.FIELD 可以给属性进行注解
- ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
- ElementType.METHOD 可以给方法进行注解
- ElementType.PACKAGE 可以给一个包进行注解
- ElementType.PARAMETER 可以给一个方法内的参数进行注解
- ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举
@Inherited
@Inherited
指定被它修饰的Annotation将具有继承性——如果某个类使用了@Xxx注解(定义该Annotation时使用了@Inherited
修饰)修饰,则其子类将自动被@Xxx修饰。
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface Test {}
@Test
public class A {}
public class B extends A {}
注解 Test 被 @Inherited 修饰,之后类 A 被 Test 注解,类 B 继承 A,类 B 也拥有 Test 这个注解可以输出验证:
System.out.println(B.class.isAnnotationPresent(Test.class));
输出结果是true,如果将public @interface Test{}上面的@Inherited删除,再运行则会输出false,说明@Inherited具有继承性。四、常用基本注解
在Java中提供5个基本的注解Annotaion,在使用注解时要在其前面添加@符号,用类修饰其支持相应的类型元素。
- @Override
- @Deprecated
- @SuppressWarnings
- @SafeVarargs
- @FunctionalInterface
@Override:
用来指定方法重写的,可以强制一个子类必须覆盖父类的方法(也可以不重写),只能修饰方法,不能修饰其他元素;使用此注解的好处是便于代码阅读,同时可以检验出@Override修饰的方法在父类中是否存在,如果在父类不存在该方法,在子类则会编译出错。
public class Father {
public void info() {
System.out.println("来自父类的信息……");
}
}
public class Children extends Father{
//使用了@Override指定重写了父类的info()方法
@Override
public void info() {
super.info();
}
}
public class AnnotationTest {
public static void main(String[] args) {
Children children = new Children();
}
}
//输出结果
来自父类的信息……
从上面的程序可能很难看出@Override的作用,此时我把Father类的info()方法名修改成info2(),而子类Children类保持不变,你会发现Chidren类的info()方法会编译出错,提示
The method info() of type Children must override a superclass method
大概的意思:方法不会覆盖或者实现超类型的方法,意思是说你使用该注解方式,在父类找不到此方法;可见@Override修饰一定程度上能够降低一些低级错误。
@Deprecated
用来表示某个元素(类、方法、变量)已过时了,当其他程序在调用的过程中编译器会发出警告。
//使用@Deprecated标识元素已过时
@Deprecated
public class Father {
@Deprecated
public String desc;
@Deprecated
public void info() {
System.out.println("来自父类的信息……");
}
}
public class Children extends Father{
@Override
public void info() {
super.info();
}
}
public class AnnotationTest {
public static void main(String[] args) {
Children children = new Children();
children.desc="";
}
}
效果如下:
@SuppressWarnings
阻止编译警告的作用,上面提到了@Deprecated 是用来提示警告的,有时候开发中并不想有类似这样的警告出现,想通过某种方式进行消除这种编译器警告,于是@SuppressWarnings修饰就可以起到这样的作用了。
@SuppressWarnings("unused")
public class AnnotationTest {
@SuppressWarnings({ "unchecked", "rawtypes", "deprecation" })
public static void main(String[] args) {
Children children = new Children();
children.desc="";
List<String> list=new ArrayList();
}
}
使用@SuppressWarnings修饰可以关闭类里所有的编译警告,在程序上看不到任何的编译器警告。
@SafeVarargs
在jdk5.0中引入了一个新特性就是允许在方法声明中使用可变长度的参数。一个方法的最后一个形参可以被指定为代表任意多个相同类型的参数,在调用的过程中,这些参数是以数组的形式来传递的;在方法体内可以按照数组的特性来操作使用这些参数。如:
public static void sum(int... args){
for (int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
}
//调用如下:
sum(4,6,8);
以上可变长度的参数方法调用是不会产生任何编译上的警告,但是如果把可变长度的参数设置成泛型,这时编译器就会产生unchecked类型的警告,如下
public static <T>void sum(T... args){
for (int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
}
//调用如下
sum(4,"6",false);
在每次调用sum()方法过程,由于传递的不是一个具体化类型参数,编译器都会产生unchecked这样的警告信息;如果希望禁止这个警告信息,需要使用@SuppressWarnings("unchecked")修饰声明。
这主要原因是可变长度的方法参数的实际值是通过数组来传递的,而数组中存储的是不可具体化的泛型类对象,自身存在类型安全问题。因此编译器会给出相应的警告信息。
虽然可以在每次调用方法时都要使用@SuppressWarnings都抑制编译器的警告信息,并不是一个很好的方法。为了解决这个问题,在JDK 7 引入了新的注解@SafeVarargs,来处理类似的警告信息
@SafeVarargs
public static <T>void sum(T... args){
for (int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
}
@FunctionalInterface
在JDK 8引入的,用来指定某个接口是函数式接口,只能修饰接口,不能修饰其他类型元素
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
五、注解的提取
我们把注解比作了标签,前面都是讲的都是怎样贴标签,标签贴在哪里的问题,那如何获取标签的内容,也就是注解解析提取,可以这样理解前面我们把标签合适贴上去了,下面怎样合适把标签撕下来查看内容信息。
注解的解析,可通过反射的方式进行提取,只有在定义注解的时使用了@Retention(RetentionPolicy.RUNTIME)修饰,这样就可以在运行时可见。
上面是一些常用提取方法,下面测试部分注解提取测试:
// 定义一个@Test的新注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Test {
}
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodTest {
// 定义了带成员变量的注解
String name();
}
@Test
class BaseTest {
// 使用带成员变量的注解时,需要为成员变量赋值
@MethodTest(name = "baseInfo")
public void baseInfo() {
}
}
public class MyTest extends BaseTest {
@Deprecated
private String info;
// 使用带成员变量的注解时,需要为成员变量赋值
@MethodTest(name = "info")
@Deprecated
private void info() {
}
}
public static void testAnnmtation() throws InstantiationException,
IllegalAccessException, NoSuchMethodException, SecurityException,
NoSuchFieldException {
// 判断MyTest类是否存在@Test注解,如果存在返回true。
Class<MyTest> clazz = MyTest.class;
boolean annotationPresent = clazz.isAnnotationPresent(Test.class);
if (annotationPresent) {
// 得到MyTest类上的@Test注解对象,存在则返回该对象,不存在则为null
Test annotationTest = clazz.getAnnotation(Test.class);
System.out.println("@Test: "
+ annotationTest.annotationType().getSimpleName());
// 获取方法上的所有注解
Method declaredMethod = clazz.getDeclaredMethod("info");
Annotation[] annotations = declaredMethod.getAnnotations();
for (int i = 0; i < annotations.length; i++) {
Annotation annotation = annotations[i];
if (annotation instanceof MethodTest) {
// 获取注解对象MethodTest的成员变量的值
MethodTest methodTest = (MethodTest) annotation;
System.out.println("Method: "
+ methodTest.annotationType().getSimpleName()
+ "--value--" + methodTest.name());
} else {
System.out.println("Method "
+ annotation.annotationType().getSimpleName());
}
}
// 获取成员变量的注解
Field declaredField = clazz.getDeclaredField("info");
Deprecated annotation = declaredField
.getAnnotation(Deprecated.class);
System.out.println("Field: "
+ annotation.annotationType().getSimpleName());
}
}
输出结果:
@Test: Test
Method: MethodTest--value--info
Method: Deprecated
Field: Deprecated
六、注解实例
下面写一个测试框架为例,测试哪些方法是否存在bug,哪些方法参与测试
1. 定义一个没有任何成员变量的注解,用来标记哪些方法可参与测试
// 定义一个标记注解,使用JDK元注解,在运行时可获取
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
}
2. 在测试实体类中定义6个方法,其中5个方法使用了@TestAnnotation注解来标记这些方法是可以测试的
public static class TestBug {
public TestBug() {
super();
// TODO Auto-generated constructor stub
}
@TestAnnotation
public void m1() {
System.out.println("1+1= " + (1 + 1));
}
@TestAnnotation
public void m2() {
System.out.println("1+8= " + (1 + 8));
}
@TestAnnotation
public void m3() {
throw new NullPointerException("参数为null");
}
@TestAnnotation
public void m4() {
System.out.println("是否正确: " + (0 / 6));
}
@TestAnnotation
public void m5() {
System.out.println("是否正确: " + (6 / 0));
}
public void m6() {
System.out.println("你真棒");
}
}
3. 最后使用反射来操作测试方法
public static void testBug() throws InstantiationException,
IllegalAccessException {
// 用来统计有多个个方法测试通过
int passed = 0;
// 用来统计有多少个方法存在bug
int failed = 0;
Class<TestBug> clazz = TestBug.class;
TestBug testBug = new AnnotationTest.TestBug();
// 获取clazz对应类中的所有的方法
Method[] methods = clazz.getDeclaredMethods();
for (int i = 0; i < methods.length; i++) {
Method method = methods[i];
// 判断方法中是否存在注解@TestAnnotation
if (method.isAnnotationPresent(TestAnnotation.class)) {
try {
// 调用方法,给方法传值
method.invoke(testBug, null);
// 测试成功,无bug,加1
passed++;
} catch (Exception e) {
System.out.println("方法:" + method + "运行失败,异常:"
+ e.getCause());
// 测试异常,加1
failed++;
// e.printStackTrace();
}
}
}
// 统计最终测试结果
System.out.println("共运行了: " + (passed + failed) + "个方法,其中:\n" + "失败了:"
+ failed + "个\n成功了:" + passed + "个");
}
运行输出结果:
方法:public void annotation.AnnotationTest$TestBug.m3()运行失败,异常:java.lang.NullPointerException: 参数为null
1+1= 2
方法:public void annotation.AnnotationTest$TestBug.m5()运行失败,异常:java.lang.ArithmeticException: / by zero
是否正确: 0
1+8= 9
共运行了: 5个方法,其中:
失败了:2个
成功了:3个
七、注解应用实例
在安卓中 应用很广泛的IOC注解框架ButterKnife,简化大量的初始化代码
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_test)
TextView mTv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
}
还有很多如:Dagger2、Retrofit……………………
八、总结
- 注解可以理解是一种标签,就是解释说明用途,主要是给编辑器和文档用的
- 注解的定义有点类似接口的定义,不同的是interface前面添加了@符号
- 元注解可以理解是所有注解的解释说明,是注解上的注解
- 注解的解析提取需要用到反射技术,反射的速度比较慢,会给性能带来影响。