Java高级技巧之注解(Annotation)

我又更新了,今天总结一下java开发中一个比较常见的东西-----------注解(Annotation),看起来很高大上的样子。
在这里插入图片描述
说明:版本Java8(最新版本14),差的辈分有点多

关于注解,在日常开发中是很常见的,比如@Override,这是覆写某个方法;@Deprecated表面方法已经过时,不建议使用(Java8有好多方法都被标记为已过时);还有在看一些源码的时候,很多代码都有注解的存在,比如在这里插入图片描述
这个注解表明Runnable是函数式接口(接口只能存在一个抽象方法)。
下面进入正题!

一、什么是注解

注解(Annotation),也叫java标注,从本质上来说也是一种类型,是jdk1.5以后引入的。它可以用来标注类、方法、变量、包等,可以通过反射来获取它(getAnnotation)。为了方便理解,你可以把注解想象成标签,作用相似。

定义

注解的定义格式是这样的:

modifiers @interface AnnotationName {
	elementDeclaration1,
	elementDeclaration1,
	......
}

每个元素的声明形式:type elementName()
或者type elementName() default value;
那么定义一个注解很简单,像这样

public @interface AnnotationTest {
  	String name() default "";
}

有点像接口的定义

应用

像这样就可以简单的应用刚才定义的注解

@AnnotationTest
public class TestForAnnotation {

}

但是这是这样的话,这个注解是没法工作的,要想它能够正常工作,需要用到元注解。
在这里插入图片描述

二、元注解

元注解就是注解的注解,说起来有点绕。假如普通注解是一张标签,那么元注解就是给普通的注解(标签)进行解释说明的。
元注解有@Retention,@Documented、@Target、@Inherited、@Repeatable 5种。

@Retention

Retention意思是保留。它标注在注解上时,声明了这个注解的生命周期。源代码如下,

public @interface Retention {
    RetentionPolicy value();
}

它的取值是RetentionPolicy,这是一个枚举类,有以下三种取值:

  • RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
  • RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
  • RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。

我们可以对上面定义的那个注解进行标注:

@Retention(RetentionPolicy.RUNTIME)
@interface AnnotationTest {
}

这样注解AnnotationTest的生命周期就会持续到程序运行。

@Documented

类和方法的注解默认是不出现在javadoc种的,但是使用@Documented修饰的注解会被包含进javadoc种。

@Target

翻译过来就是目标,@Target标明了注解被应用到的地方。
本来注解可以被应用到任何地方,但是被@Target修饰的话,那这个注解就只能被应用的指定的地方。它的源码如下:

public @interface Target {
    ElementType[] value();
}

它的取值是ElementType,也是一个枚举类型,[]表面它可以取多个值。取值如下:

  • ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
  • ElementType.CONSTRUCTOR 可以给构造方法进行注解
  • ElementType.FIELD 可以给属性进行注解
  • ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
  • ElementType.METHOD 可以给方法进行注解
  • ElementType.PACKAGE 可以给一个包进行注解
  • ElementType.PARAMETER 可以给一个方法内的参数进行注解
  • ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举

@Inherited

英文直译的话是遗传,那么放在java语言环境中就是继承。所以被@Inherited修饰的注解是可以被继承的。

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface AnnotationTest {
}

@AnnotationTest
class Parent {
}

class Child extends Parent {
}

注解AnnotationTest 被@Inherited修饰,父类Parent被@AnnotationTest 修饰,子类Child继承父类,那么子类也会拥有这个注释。
可以这样理解:
老子非常有钱,所以人们给他贴了一个标签–土豪
那么当老子的儿子长大以后,人们也会给他贴同样的土豪标签,因为他继承了他爸的有钱嘛。

@Repeatable

java1.8添加了这个注解,意思就是说该注解是可以重复使用的。
当一个注解的值可以是不同的时候,使用@Repeatable
比如一个人他既是老师又是作家还是一个老板,那么就可以这样来定义和取值:

@interface Persons {
    Person[] value();
}

@Repeatable(Persons.class)
@interface Person {
    String role() default "";
}
@Person(role = "老师")
@Person(role = "作家")
@Person(role = "老板")
class SuperMan {

}

上面Person被@Repeatable注解修饰,@Repeatable括号里的类相当于一个容器,能够包含当前所修饰注解的一个容器。这个容器本身也是一个注解,如示例中的

@interface Persons {
    Person[] value();
}

上面有出现role = ”老板“这样的语句,这是用来给注解中元素赋值的。下面讲解注解中的元素:

三、注解中的元素

注解中的元素可以是:

  • 基本类型
  • String类型
  • Class(具有一个可选的参数,例如Class< ? extends MyClass>)
  • enum类型
  • 注解类型
  • 由上述类型组成的数组(有数组组成的数组不和合法类型)

使用注解

每个注解的使用都具有如下格式:@AnnotationTest(elementName1=value1,elementName2=value2,......)
举例,我们先来定义一个注解Test

@interface Test {
    String name() default "";
    int age() default 0;
}

在使用注解的时侯@Test(name="wang",age=12)
元素的顺序并没有什么影响,所以也可以这样@Test(age=12,name="wang")。因为有默认值,所以也可以
@Test(age=12),这样name的默认值就是”“,甚至可以不写括号,直接@Test,这样他们都会使用默认值。
对于特殊的注解@interface Test {};使用的时候不需要带括号@Test

特殊项

对于只有一个元素的注解也叫单值注解,在书写的时候可以忽略元素名和等号。对于下面的注解

@interface Test {
	String value();
}

使用的时候可以直接写成@Test("wang")

如果碰到元素为数组类型,那么数组的取值需要用括号括起来,对于注解:

@interface ArrayTest {
    String[] value();
}

使用的时候@ArrayTest(value = {"wang","li","zhang"})

四、Java预置的注解

@Deprecated

将目标标记为已过时的,它的定义是这样的:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

Retention表明它会保留到程序运行的时候,Target表明它可以用在类型,方法,参数,包等。我在前面讲过Java的日期表示
,对于Date来说,很多的方法都会声明为了@Deprecated,表现就是画一条横线:
在这里插入图片描述
我们看其中的一个方法吧:
在这里插入图片描述
所以过时的方法就不建议再使用了,都会有它更好的代替。

@Override

这个想必大家很熟悉了,就是子类要覆写父类的方法

@SuppressWarnings

就是阻止警告。比如使用被@Deprecated标注的方法,编译器就会发出警告,如果你要忽略这种警告就需要使用到它,像这样@SuppressWarnings("deprecation")

@SafeVarargs

它的Target有两个值,Method和Constructor,有关于参数类型的注解。使用这个注解说明程序员断言参数不会被进行不安全的操作。
下面是官方文档中的一个例子:

@SafeVarargs 
	static void m(List<String>... stringLists) {
	Object[] array = stringLists;
	List<Integer> tmpList = Arrays.asList(42);
	array[0] = tmpList; 
	String s = stringLists[0].get(0);
	//java.lang.ClassCastException
}

上述代码在编译阶段不报错,但是在运行时会报ClassCastException

@FunctionalInterface

函数式接口的标志,所谓函数式接口就是只有一个抽象方法的接口。先看看它在源码中的应用,比如常见的Runable接口:
在这里插入图片描述
其实如果接口只有一个抽象方法,那么无论你加不加这个注解,它就是一个函数式接口。
但是反过来当你给一个接口添加了这个注解以后,如果内部方法超过一个就会报错!像这样:
在这里插入图片描述
可以帮助你编写满足预期的代码。

那有同学肯定要问了,我平时写的时候还看到过@NotNull, @Resource等等,这些你怎么没说。主要是在说Java.lang.annotation包下的Annotation,至于@NotNull则属于Check Framework,而@Resource等则属于javax.annotation,希望大家区分一下。

五、注解和反射

注解可以通过反射获取,当然前提是RetentionPolicy.RUNTIME
通过Class对象的isAnnotationPresent可以判断是否应用了某个注解

public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}

通过可以获取getAnnotation可以获取Annotation对象

 public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {}

或者getAnnotations

 public Annotation[] getAnnotations()  {}

先定义一个注解:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.FIELD})
@interface AnnotationTest {
    String place();
}

测试代码如下:

@AnnotationTest(place = "home")
public class TestForAnnotation {
    public static void main(String[] args) {
        Class target = TestForAnnotation.class;
        AnnotationTest annotation = (AnnotationTest) target.getAnnotation(AnnotationTest.class);
        String place = annotation.place();
        System.out.println("place = " + place);
    }
}

如果注解是标注在属性上,则先获取属性,再获取属性对应的注解。如果属性私有,则通过setAccessible(true)设置可获取。

六、注解的使用场景

虽然说了这么多,但是很多同学还是一脸懵逼,那要这玩意儿有啥用,我平时不也没用,还不是代码写的贼6!
在这里插入图片描述
先来看一下官方文档里对注解的解释是使用场景:

注解是一系列元数据,它提供数据用来解释程序代码,但是注解并非是所解释的代码本身的一部分。注解对于代码的运行效果没有直接影响。

注解的使用场景如下:

1、提供信息给编译器: 编译器可以利用注解来探测错误和警告信息
2、编译阶段时的处理:软件工具可以用来利用注解信息来生成代码、Html文档或者做其它相应处理。
3、运行时的处理: 某些注解可以在程序运行的时候接受代码的提取

所以它主要是给编译器或APT(处理提取和处理 Annotation 的代码)使用的。

下面我们举个栗子:
注:例子来自《Java核心技术卷2》

在用户界面编程中,经常需要 在组件上添加监听器,像下面这样的形式:

myButton.addActionListener(()->doSomething);

这个例子就是为了简化这个操作而进行的,可以像这样完成事件监听:

@ActionListenerFor(source = ”myButton") 
void dosomething(){

}

首先定义注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ActionListenerFor {
    String source();
}

定义处理注解,以及实现动态绑定的类:
这里面用到了反射和动态代理,我会在接下来的文章中讲到

public class ActionListenerInstaller {

    public static void processAnnotations(Object object) {
        try {
            Class<?> cl = object.getClass();
            for (Method m : cl.getDeclaredMethods()) {
                ActionListenerFor a = m.getAnnotation(ActionListenerFor.class);
                if (a != null) {
                    Field f = cl.getDeclaredField(a.source());
                    f.setAccessible(true);
                    addListener(f.get(object), object, m);
                }
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    public static void addListener(Object source, final Object param, final Method m) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                return m.invoke(param);
            }
        };
        Object listener = Proxy.newProxyInstance(null, new Class[]{java.awt.event.ActionListener.class}, handler);
        Method adder = source.getClass().getMethod("addActionListener", ActionListener.class);
        adder.invoke(source, listener);
    }
}

定义应用注解的测试类:

public class ButtonFrame extends JFrame {
    private static final int DEFAULT_WIDTH = 300;
    private static final int DEFAULT_HEIGHT = 200;

    private JPanel panel;
    private JButton yellowButton;
    private JButton blueButton;
    private JButton redButton;

    public ButtonFrame() {
        setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);

        panel = new JPanel();
        add(panel);
        yellowButton = new JButton("yellow");
        blueButton = new JButton("Blue");
        redButton = new JButton("Red");
        panel.add(yellowButton);
        panel.add(blueButton);
        panel.add(redButton);
        ActionListenerInstaller.processAnnotations(this);
        setVisible(true);
        setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    }

    public static void main(String[] args) {
        new ButtonFrame();
    }

    @ActionListenerFor(source = "yellowButton")
    public void yellowBackgroud() {
        panel.setBackground(Color.YELLOW);
    }

    @ActionListenerFor(source = "blueButton")
    public void blueBackgroud() {
        panel.setBackground(Color.BLUE);
    }

    @ActionListenerFor(source = "redButton")
    public void redBackgroud() {
        panel.setBackground(Color.RED);
    }
}

运行的结果如图:
在这里插入图片描述
点击对应的按钮就会执行相应的事件。

其实很多的框架和应用中都使用了注解,比如Java开发经常用到的lombok、Check Framework,Spring框架等等。所以学会使用注解还是非常有用的。

七、总结

  • 注解和标签的作用有相似之处,可以借用标签来理解注解
  • 怎么定义和使用注解
  • 元注解的类型和使用
  • java中预置的一些注解
  • 如何使用反射来获取注解的内容
  • 一个例子来展示注解的强大之处

最后,如果有任何错误或者意见,欢迎大家留言或私信~~


2020/4/29 下午
在这里插入图片描述

参考文章:
《Java核心技术卷2》第八章
秒懂,Java 注解 (Annotation)你可以这样学

猜你喜欢

转载自blog.csdn.net/machine_Heaven/article/details/105821986