我又更新了,今天总结一下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)你可以这样学