Java 夯实基础之注解
不积跬步无以至千里,不积小流无以成江海。厚积才能薄发,水到自然渠成;
一如既往先提三个问题:注解是什么?注解怎么用?注解有什么作用?
注解是什么?
注解就是对程序代码的补充说明,可以理解为标签,是对这段程序代码的解释,主要的目的就是提高我们的代码质量,和工作效率
注解怎么用?
1. 注解的语法
同 class
和 interface
一样,注解也是一种类型,通过@interface
关键字定义,其语法如下:
public @interface TestAnnotation {}
其定义形式类似于接口,只不过在前面多了一个 @
符号,这里表示创建了一个TestAnnotation
的注解。
2. 注解的属性
注解的属性也叫成员变量,注解只有成员变量,没有成员方法。 注解的成员变量在注解中以无形参的方法
来声明,其方法名定义了该成员变量的名字,返回类型定义了该成员变量的类型,比如:
@Inherited//该注解可以它注解的类的子类继承
@Retention(RetentionPolicy.RUNTIME)//该注解的生命周期
@Target(ElementType.TYPE)
public @interface Test {int id() default -1;String msg() default "";
}
上述代码定义了一个叫 Test
的注解,它拥有 id
和msg
这两个成员属性,在使用时,通过在注解的()
中以value= xxx
的形式赋值,多个属性通过,
隔开,如下所示:
@Test(id = 2, msg = "hello annotation")
public class SuperMan {@Check("hi")int a;@Perform@Person(role = "PM")public void testMethod() {}@SuppressWarnings("deprecation")public void test() {}
}
Tips:
- 注解中只能用public或默认(default)这两个访问权修饰;
- 成员属性只能是八大基本类型
byte,short,char,int,long,float,double,boolean
和String,Enum,Class,annotations
等数据类型,以及这一些类型的数组; - 如果只有一个成员属性,建议使用
value()
替代 - 注解属性必须有确认的值,可以在定义注解时使用
default
指定,或者在使用注解时指定,非基本类型的注解元素的值不可为null,常使用空字符串或负数作为默认值。
/***自定义注解MyAnnotation*/
@Target(ElementType.TYPE) //目标对象是类型
@Retention(RetentionPolicy.RUNTIME) //保存至运行时
@Documented //生成javadoc文档时,该注解内容一起生成文档
@Inherited //该注解被子类继承
public @interface MyAnnotation {public String value() default ""; //当只有一个元素时,建议元素名定义为value(),这样使用时赋值可以省略"value="String name() default "devin"; //Stringint age() default 18; //intboolean isStudent() default true; //booleanString[] alias(); //数组enum Color {GREEN, BLUE, RED,} //枚举类型Color favoriteColor() default Color.GREEN; //枚举值
}
3. 注解的应用
注解的分类
- 根据成员个数分类
1. 标记注解
没有定义成员的注解类型,自身就代表了某类信息,比如@override
2. 单成员注解
只定义了一个成员,比如@SuppressWarnings
定义了一个成员String[] value
,使用value={...}
大括号来声明数组值,一般也可以省略“value=”
3. 多成员注解
定义了多个成员,使用时以name=value对分别提供数据 - 根据用途和功能分类
1. 元注解
元注解,可以理解为注解的注解,即解释注解信息的一些特殊标签,它们主要的功能是确定注解的生命周期@Retention
,范围@Target
,是否被子类继承@Inherited
,是否可重复@Repeatable
,是否生成到javac文档中@Documented
- @Retention
Retention 的英文意为保留期的意思。当 @Retention 应用到一个注解上的时候,它解释说明了这个注解的的存活时间。
它的取值如下:
- RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
- RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
- RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。
@Retention 相当于给一个注解盖了一张时间戳,时间戳指明了注解的时间周期。
- @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 是继承的意思,但是它并不是说注解本身可以继承,而是说如果一个超类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解。 比如:
@Inherited//该注解可以它注解的类的子类继承
@Retention(RetentionPolicy.RUNTIME)//该注解的生命周期
@Target(ElementType.TYPE)
public @interface Test {int id();String msg();
}@Person(role = "产品经理")
@Person(role = "PM")
@Test(id = 2, msg = "hello annotation")
public class SuperMan {@Check("hi")int a;@Perform@Person(role = "PM")public void testMethod() {}@SuppressWarnings("deprecation")public void test() {}
}public class Man extends SuperMan {
}public class TestClass {public static void main(String[] args) {//首先可以通过 Class 对象的 isAnnotationPresent() 方法判断它是否应用了某个注解boolean hasAnnotation = SuperMan.class.isAnnotationPresent(Test.class);if (hasAnnotation) {//返回指定类型的注解Test annotation = SuperMan.class.getAnnotation(Test.class);System.out.println("id:" + annotation.id());System.out.println("msg:" + annotation.msg());//返回注解到这个元素上的所有注解。Annotation[] annotations = SuperMan.class.getAnnotations();for (Annotation an : annotations) {System.out.println("class SuperMan annotation:" + an.annotationType().getSimpleName());}}//首先可以通过 Class 对象的 isAnnotationPresent() 方法判断它是否应用了某个注解boolean manHasAnnotation = Man.class.isAnnotationPresent(Test.class);if (manHasAnnotation) {//返回指定类型的注解Test annotation = Man.class.getAnnotation(Test.class);System.out.println("Man id:" + annotation.id());System.out.println("Man msg:" + annotation.msg());//返回注解到这个元素上的所有注解。Annotation[] annotations = Man.class.getAnnotations();for (Annotation an : annotations) {System.out.println("class Man annotation:" + an.annotationType().getSimpleName());}}
}
结果:
注解 Test
被 @Inherited
修饰,之后类 SuperMan
被 @Test
注解,类 man
继承 SuperMan
,类 man
也拥有 @Test
这个注解。
- @Repeatable
Repeatable 自然是可重复的意思。@Repeatable 是 Java 1.8 才加进来的,所以算是一个新的特性。(注:只有在java 1.8 中才能如下使用)- 什么样的注解会多次应用呢?通常是注解的值可以同时取多个。
- 举个例子,一个人他既是程序员又是产品经理,同时他还是个画家。
@Retention(RetentionPolicy.RUNTIME)
public @interface Persons {Person[] value();
}@RequiresApi(api = Build.VERSION_CODES.N)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Persons.class)
public @interface Person {String role() default "";
}@Person(role = "产品经理")
@Person(role = "PM")
@Test(id = 2, msg = "hello annotation")
public class SuperMan {@Check("hi")int a;@Perform@Person(role = "PM")public void testMethod() {}@SuppressWarnings("deprecation")public void test() {}
}
注意上面的代码,@Repeatable
注解了 Person
。而 @Repeatable
后面括号中的类相当于一个容器注解。
什么是容器注解呢?就是用来存放其它注解的地方。它本身也是一个注解。
我们再看看代码中的相关容器注解。
@Retention(RetentionPolicy.RUNTIME)
public @interface Persons {Person[] value();
}
按照规定,它里面必须要有一个 value
的属性,属性类型是一个被 @Repeatable
注解过的注解数组**,注意它是数组。编译环境为 jdk 1.8,低于1.8 会报错
如果不好理解的话,可以这样理解。Persons
是一张总的标签,上面贴满了 Person
这种同类型但内容不一样的标签。把 Persons
给一个 SuperMan
贴上,相当于同时给他贴了程序员、产品经理、画家的标签。
- @Documented
它的作用是在生成javadoc文档的时候将该Annotation也写入到文档中
2. 内置注解
@Override
限定重写父类方法@Deprecated
用于标记已过时@SuppressWarnnings
抑制编译器警告
3. 自定义注解
使用@interface
自定义注解,在定义注解时,不能继承其他的注解或接口。比如:
/***自定义注解MyAnnotation*/
@Target(ElementType.TYPE) //目标对象是类型
@Retention(RetentionPolicy.RUNTIME) //保存至运行时
@Documented //生成javadoc文档时,该注解内容一起生成文档
@Inherited //该注解被子类继承
public @interface MyAnnotation {public String value() default ""; //当只有一个元素时,建议元素名定义为value(),这样使用时赋值可以省略"value="String name() default "devin"; //Stringint age() default 18; //intboolean isStudent() default true; //booleanString[] alias(); //数组enum Color {GREEN, BLUE, RED,} //枚举类型Color favoriteColor() default Color.GREEN; //枚举值
}
注解的获取
我们主要通过反射来获取运行时注解,常用API 如下:
<T extends Annotation> T getAnnotation(Class<T> annotationClass) :返回该程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null;Annotation[] getDeclaredAnnotation(Class<T>):返回该程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null;与此接口中的其他方法不同,该方法将忽略继承的注解;Annotation[] getAnnotations():返回该程序元素上存在的所有注解;Annotation[] getDeclaredAnnotations():返回直接存在于此元素上的所有注释。与此接口中的其他方法不同,该方法将忽略继承的注解;Annotation[] getAnnotationsByType(Class<T>):返回直接存在于此元素上指定注解类型的所有注解;Annotation[] getDeclaredAnnotationsByType(Class<T>):返回直接存在于此元素上指定注解类型的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注解;boolean isAnnotationPresent(Class<?extends Annotation> annotationClass):判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false;
例如:
public class TestClass {public static void main(String[] args) {//首先可以通过 Class 对象的 isAnnotationPresent() 方法判断它是否应用了某个注解boolean hasAnnotation = SuperMan.class.isAnnotationPresent(Test.class);if (hasAnnotation) {//返回指定类型的注解Test annotation = SuperMan.class.getAnnotation(Test.class);System.out.println("id:" + annotation.id());System.out.println("msg:" + annotation.msg());//返回注解到这个元素上的所有注解。Annotation[] annotations = SuperMan.class.getAnnotations();for (Annotation an : annotations) {System.out.println("class SuperMan annotation:" + an.annotationType().getSimpleName());}}//首先可以通过 Class 对象的 isAnnotationPresent() 方法判断它是否应用了某个注解boolean manHasAnnotation = Man.class.isAnnotationPresent(Test.class);if (manHasAnnotation) {//返回指定类型的注解Test annotation = Man.class.getAnnotation(Test.class);System.out.println("Man id:" + annotation.id());System.out.println("Man msg:" + annotation.msg());//返回注解到这个元素上的所有注解。Annotation[] annotations = Man.class.getAnnotations();for (Annotation an : annotations) {System.out.println("class Man annotation:" + an.annotationType().getSimpleName());}}try {Field a = SuperMan.class.getDeclaredField("a");a.setAccessible(true);//获取一个成员变量上的注解Check check = a.getAnnotation(Check.class);if (null != check) {System.out.println("check value:" + check.value());}Method testMethod = SuperMan.class.getDeclaredMethod("testMethod");if (null != testMethod) {Annotation[] ans = testMethod.getAnnotations();for (Annotation an : ans) {System.out.println("method testMethod annotation:" + an.annotationType().getSimpleName());}}} catch (NoSuchFieldException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();}}}
运行结果:
注解的使用
栗子:
在Android
中的 findViewById
可以用注解这样写:
@BindView(R.id.hello_tv)TextView helloTv;
点击事件可以这样写:
@OnClick({R.id.toast_btn, R.id.change_btn})public void onClick(View view) {switch (view.getId()) {case R.id.toast_btn:Toast.makeText(MainActivity.this, "click toast button", Toast.LENGTH_SHORT).show();break;case R.id.change_btn:helloTv.setText(getResources().getString(R.string.changebtn));break;default:Toast.makeText(MainActivity.this, "default is called ", Toast.LENGTH_SHORT).show();break;}}
BindView
这里我们定义了一个 @BindView
注解,因为是使用在属性上的,所以声明Targe
注解为 Field
,又因为是在程序运行中使用,故Retention
指定为 RUNTIME
,这里我们需要传入了控件id
,所以需要再定义一个int
类型的属性接收,完整代码如下所示:
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {int value();
}
OnClick
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {int[] value();//setOnClickListener 为点击事件的set 方法默认名String listenerSetter() default "setOnClickListener";//OnClickListener 点击事件监听的类型Class listenerType() default View.OnClickListener.class;//点击事件的默认方法名String methodName() default "onClick";}
ViewHandler
点击事件动态代理类
public class ViewHandler implements InvocationHandler {private Object mObject;private String mMethodName;private Method mMethod;public ViewHandler(Object object, String methodName, Method method) {mObject = object;this.mMethodName = methodName;this.mMethod = method;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if (method.getName().equals(mMethodName)) {return mMethod.invoke(mObject, args);}return null;}
}
现在我们已经可以拿到控件的id
和注解,但此时我们还不能直接使用这个注解,因为它和我们的控件还没什么关联,现在我们通过反射来实现注解的注入。如下:
public class AnnoUtils {private static final String TAG = "AnnoUtils";public static void inject(Activity activity) {if (null == activity) {return;}try {//获取注入的 activity 的Class 对象Class clazz = activity.getClass();//获取该 Aty 的所有属性Field[] fields = clazz.getDeclaredFields();if (null == fields || fields.length <= 0) {return;}//遍历所有属性for (Field field : fields) {//判断当前字段是否支持 BindView 注解if (field.isAnnotationPresent(BindView.class)) {//获取注解对象BindView annotation = field.getAnnotation(BindView.class);if (null != annotation) {//获取注解内容int id = annotation.value();View view = activity.findViewById(id);if (view != null) {//把view赋给字段filedfield.setAccessible(true);field.set(activity, view);}}}}Method[] methods = clazz.getDeclaredMethods();if (null == methods || methods.length <= 0) {return;}for (Method method : methods) {if (method.isAnnotationPresent(OnClick.class)) {OnClick annotation = method.getAnnotation(OnClick.class);if (annotation != null) {//获取所有的idint[] ids = annotation.value();for (int id : ids) {//找到相应的viewView view = activity.findViewById(id);if (view != null) {method.setAccessible(true);String listenerSetter = annotation.listenerSetter();Class listenerType = annotation.listenerType();String methodName = annotation.methodName();ViewHandler viewHandler = new ViewHandler(activity, methodName, method);//获取onClickListener代理对象Object onClickListener = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType}, viewHandler);//获取view的setOnClickListener方法Method setOnListenerMethod = view.getClass().getMethod(listenerSetter, listenerType);//实现setOnClickListener方法setOnListenerMethod.invoke(view,onClickListener);}}}}}} catch (IllegalAccessException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}}
}
具体意思代码中都有注释,就不多说了,接着看下我们的Activity
代码
public class MainActivity extends AppCompatActivity {@BindView(R.id.hello_tv)TextView helloTv;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);AnnoUtils.inject(this);helloTv.setText(getResources().getString(R.string.change_by_annotation));}@OnClick({R.id.toast_btn, R.id.change_btn})public void onClick(View view) {switch (view.getId()) {case R.id.toast_btn:Toast.makeText(MainActivity.this, "click toast button", Toast.LENGTH_SHORT).show();break;case R.id.change_btn:helloTv.setText(getResources().getString(R.string.changebtn));break;default:Toast.makeText(MainActivity.this, "default is called ", Toast.LENGTH_SHORT).show();break;}}
}
XML 代码:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><TextViewandroid:id="@+id/hello_tv"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="Hello World!"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent" /><Buttonandroid:id="@+id/change_btn"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/change"android:textAllCaps="false"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toBottomOf="@+id/hello_tv" /><Buttonandroid:id="@+id/toast_btn"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/toast"android:textAllCaps="false"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toBottomOf="@+id/change_btn" /></androidx.constraintlayout.widget.ConstraintLayout>
效果图如下:
GitHub demo链接
注解有什么作用?
- 编写文档:通过代码里标识的元数据生成文档。
- 代码分析:通过代码里标识的元数据对代码进行分析。
- 编译检查:通过代码里标识的元数据让编译器能实现基本的编译检查。
- 通过不同注解明规范功能和一些统一的操作,比如retrofit、Junit、dagger2
个人思考:
注解它的主要功能就是提供它承载的信息。其实我们在项目开发中,主要有几个指标,一是开发效率要高,二是代码质量要高,三是开发完项目的维护成本要低,其实注解就能很好的承担这些角色,比如提高开发效率的Rertofit
、Butterknife
这些框架中的注解,提高代码质量的有Junit
,降低维护成本的比如内置注解@Deprecated
、@Override
这些,他们都是为了帮助我们理解代码,分析代码,统一规范行为,提高代码质量,因为一个无法避免的问题就是随着项目的迭代,项目的可读性、可靠性会下降,它的维护开发成本会提升,注解是解决这些问题的一个很好的方式,但是注解的提取需要借助于 Java 的反射技术,反射比较慢,所以注解使用时也需要谨慎计较时间成本。
参考文章:
https://blog.csdn.net/vv_bug/article/details/64500453
https://blog.csdn.net/briblue/article/details/73824058
Java 夯实基础之注解相关推荐
- java夯实基础系列:反射
一.反射定义&通俗理解 反射之中包含了一个"反"的概念,所以要想解释反射就必须先从"正"开始解释,一般而言,当用户使用一个类的时候,应该先知道这个类 ...
- java夯实基础-基本数据类型
学习任何一门语言,基础永远都是最重要的.本文测试java中八种基本数据类型(int,short,long,float,double,char,boolean,byte),包括其占用空间大小,能够存储的 ...
- java夯实基础:final脑图
final的作用从两个方面理解:基本的锁定功能,帮助JVM实现效率和安全.关于final的解释非常多,本文仅做整理的总结. 基本用法:锁定 final的锁定功能通过final修饰的对象来具体实现,fi ...
- java 夯实基础_夯实基础-java equals使用,和 == 的区别
在研究hashcode的用法,发现自己对equals的理解还不够深,废话少说,继续专研,记录如下: 要想搞清楚equals和==的区别,就先要搞清楚JAVA的数据类型: java的数据类型只要分两大类 ...
- java夯实基础-关键字
本文总结了java中51个关键字,分别对他们的含义.作用有一定说明. 1.访问权限 public 共有权限 可跨包访问 protected 保护权限 同一包可访问:不同 ...
- java夯实基础-输入输出流
本文测试java中的字节流.字符流.缓冲流.随机流.文件锁. 一.字符流:Reader和Writer package com.wllfengshu.test;import java.io.File; ...
- java夯实基础-运算符
本文测试了 算术运算符: + - * / % += -= *= /= %= :逻辑运算符: && || ! & | :关系运算符:< ...
- final关键字_夯实基础:Java中final关键字的几种用法
导语 在java的关键字中,static和final是两个我们必须掌握的关键字.不同于其他关键字,他们都有多种用法,而且在一定环境下使用,可以提高程序的运行性能,优化程序的结构. 关于static请查 ...
- 黑马就业班(01.JavaSE Java语言基础-11.Java基础加强)——基础加强:Junit单元测试、反射、注解
1.Junit单元测试 测试分类: 1. 黑盒测试:不需要写代码,给输入值,看程序是否能够输出期望的值. 2. 白盒测试:需要写代码的.关注程序具体的执行流程. Junit使用:白盒测试 步骤: ...
- Java学习路线-夯实基础
Java学习路线-夯实基础 第一部分:网络与操作系统 第二部分:数据结构与算法 第三部分:Java基础 第四部分:Java设计模式 第五部分:数据库 第六部分:Redis 第七部分:并发与多线程 第八 ...
最新文章
- QIIME 2教程. 15样品分类和回归q2-sample-classifier(2020.11)
- 【 MATLAB 】信号处理工具箱的信号产生函数之 square 函数简记
- pytorch 归一化与反归一化
- Django 的视图层
- 用C语言实现控制台模拟显示时
- Django基础篇之MVC与MTV模型
- 用我对HTML的点点理解来做个简单的百度首页
- MongoDB初学者教程
- omnicppcomplete php,vim 中OmniCppComplete的安装和使用
- label之间展示间距_工法样板如何做?碧桂园质量工法样板展示区做法标准
- QCA9531修改寄存器值控制GPIO
- Android webview缓存机制
- 输出0~1000内的质数 C语言
- 1 10000以内的质数表C语言,求1万以内的质数表,有急用
- 程序员面试时会遇上哪些问题
- 笔记:Linux系统调用在文件中的分布情况
- Docker容器内部无法访问外网原因之一
- 剑走偏锋——老女人教你另类情人节攻略
- linux 磁盘碎片整理
- 梦幻模拟战手游服务器维护,梦幻模拟战手游11月21日更新公告 执行者降临限时召唤开启[多图]...