在看静态代理和动态代理的区别时了解到反射,上一篇文章对反射技术做了总结。
而之前看一些文章总是提到使用
注解能解决反射导致的性能问题
。因此想通过一篇文章来了解注解。但还是太年轻,一篇文章没法全面解释清楚Java注解
。目前就先记录当下我所了解的注解。
Java注解产生背景
在Java1.5
之前的开发中,配置文件只能是通过xml
文件来配置 这种方式的优点是
集中管理对象和对象之间的组合关系,易于阅读
遵循开闭原则,修改配置文件即可进行功能扩展
弱耦合
但是也有它的不足
需要同时维护
xml 配置文件
和Java 代码文件
,开发速度慢xml 配置文件
过多,会导致维护变得困难xml 解析
会影响到应用程序的性能编译时很难检查出错误
运行中的错误很难定位,调试难度较大
对于上述的问题,Java1.5
后提供了注解的方式给开发人员更多的选择余地 相对于 xml 配置文件,注解的优势是
注解与代码在一起,只要维护
Java
代码即可,开发速度快编译期间容易发现错误的出处
还可结合注解处理器生成字节码
当然注解也不是用来替代xml
配置的文件。注解也存在一些不足
修改的话比较麻烦。如果需要对注解进行修改的话,就需要对整个项目重新编译
处理业务类之间的复杂关系,不像
xml
那样容易修改,也不及xml
那样明了如果后来的人对注解不了解,会给维护带来成本
高耦合
小结:xml 配置文件
适合做一些全局的、与代码无关的配置。如:全局的配置
注解
适合做一些和代码联系紧密的操作。如:页面跳转,那么路径就是和代码紧密相关的,因此可以用注解来配置路径。
Java 注解是什么
Java
注解(Annotation
)又称之为Java 标注元数据
,是Java 1.5
之后加入的一种特殊语法。元数据
(metadata)
是指 描述数据的数据。举例:一个 图片文件 可能会包括描述图片大小、色彩深度、图片分辨率、图片创建时间 等资料的元数据。
作用是什么
为编译器提供信息:编译器可以使用注解来检查错误或抑制警告,增强代码可读性。如:
@NonNull
编译时处理:可以生成代码、XML、文件等。
运行时处理:注解可以在运行时检查
我们知道,代码只是我们跟机器沟通的一种语言。我们编写代码首先会经过编译器的编译,编译器觉得没有问题才继续执行,执行完了机器才会知道我们要表达的内容。这个内容有可能是正确的,也可能是错误的,但是起码我们能和机器沟通了。
就像比划猜词游戏:出题人就是编译器,比划词的人是写代码的人,而猜词的人就是机器。
编译时注解
编译时:编译器将源代码翻译成机器能识别的代码。比如说 当你语法写错时编译器能告诉你代码写错了。
未添加@Retention(RetentionPolicy.CLASS)
标识的就是编译时注解,默认的就是 RetentionPolicy.CLASS
运行时注解
运行时:通俗点说就是代码在机器上执行了。比如说
Android
开发中的空指针异常,写代码时编译器是不知道那块会出现空指针异常,只有在代码执行出现空指针异常App
才会报错。
在你的代码中加上这一行,就是运行时注解。表示该注解可以保留到运行期间
@Retention(RetentionPolicy.RUNTIME)
比如:
@Retention(RetentionPolicy.RUNTIME)
@Entity(id=10) // 自定义注解,暂时不用管
public class User(){}
上面的代码在运行时可以通过反射获取到@Entity(id=10)
中的数据
二者的区别
编译时注解作用范围仅在编译阶段,在代码运行阶段注解数据会被擦除。而运行时注解在代码运行阶段注解数据不会被擦除
编译时注解的本质是生成辅助文件提供给程序运行时使用,运行时注解的本质是反射
编译时注解会增加编译时间,运行时注解会使程序执行更耗时
本质是什么
The common interface extended by all annotation types
Java
文档中上面的一句话说明了注解的本质。所有的注解类型都继承自这个普通的接口(Annotation
)。所以注解的本质就是一个继承了
Annotation
接口的接口
如:@Override
注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
本质上是继承了Annotation
接口的接口:
public interface Override extends Annotation{
}
注解的分类
预定义注解
Java
或者 Android
中提供的注解。下面仅罗列几个看看。
@Override
说明方法是子类重写父类的方法。(用于编译时检查)。
@Deprecated
说明方法已经过时,不建议使用,同时需要给出建议使用的方法。
@SuppressWarnings
忽略警告。
@SafeVarargs
忽略调用参数为泛型的方法的警告。
@FunctionInterface
标注一个接口是函数式接口。(只有一个方法的接口)
元注解
在注解上面的注解称为元注解(meta-annotations
),如:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface UserField {
int age();
String name();
}
@Target
用来描述注解的使用范围,也就是用来约束自定义注解可以注解
Java
的哪些元素。
也就是该注解要使用到哪个位置,具体由枚举 ElementType 中定义,具体如下:
public enum ElementType {
TYPE, //类、接口、注解、枚举
FIELD, //属性(包括枚举常量)
METHOD, //方法
PARAMETER, //参数
CONSTRUCTOR, //构造方法
LOCAL_VARIABLE, //局部变量
ANNOTATION_TYPE,//注解, 也就是元注解
PACKAGE, //包
}
@Retention
指定注解的存储方式,表明标记的注解可以多次应用于同一声明或类型使用。
我们由 RetentionPolicy.java
(是一个枚举)可知,如:
public enum RetentionPolicy {
SOURCE, // 标记的注解仅保留在源级别中,并被编译器忽略。
CLASS, // 标记的注解在编译时由编译器保留,但 Java 虚拟机(JVM)会忽略。
RUNTIME // 标记的注解由 JVM 保留,因此运行时环境可以使用它。
}
@Documented
表示使用了指定的注解,将使用 Javadoc
工具记录这些元素。
比如我们用@Document
注解了我们的自定义注解:
import java.lang.annotation.Documented;
@Documented
public @interface MyAnnotation {
}
如果一个类使用了这个注解:
@MyAnnotation
public class MySuperClass { ... }
那么当生成MySuperClass
的JavaDoc的时候,@MyAnnotation
也会出现在JavaDoc
当中。
@Inherited
表示注解类型可以从父类继承。
比如有一个自定义注解:
java.lang.annotation.Inherited
@Inherited
public @interface MyAnnotation {
}
如果有一个类使用了上面这个注解:
@MyAnnotation
public class MySuperClass { ... }
那么这个类的子类也会继承这个注解:
因为MySubClass
继承了MyClass
,而MyClass
的注解@MyAnnotation
是可继承的,最终MySubClass
也会有@MyAnnotation
注解。
public class MySubClass extends MySuperClass { ... }
自定义注解
注解的定义类似于接口的定义,在关键字
interface
前加上@
如:
public @interface UserField {
int age();
String name();
}
int age()
和 String name()
是注解类型(annotation type),它们也可以定义可选的默认值,如:
public @interface UserField {
int age();
String name() default "张三";
}
在使用注解时,如果定义的注解的注解类型没有默认值,则必须进行赋值,如:
// 需声明运行时保留,不然获取不到数据
@Retention(RetentionPolicy.RUNTIME)
public @interface UserField {
int age();
String name() default "张三";
}
@UserField(age = 23,name = "李四")
class UserBean {
}
val usrField = UserBean::class.java.getAnnotation(UserField::class.java)
val age = usrField?.age
val name = usrField?.name
Log.e(TAG,"usrField 注解age:$age")
Log.e(TAG,"usrField 注解name:$name")
输出:
usrField 注解age:23
usrField 注解name:李四
注解使用示例
Android
开发中使用的第三方库就大量使用了注解,如:
运行时注解:Retrofit
编译时注解:Dagger2, ButterKnife, EventBus3
1. @NonNull
作用:用来标识特定的参数或者返回值不可以为 null
实现:
fun getMsg(@NonNull msg:String) {
// msg=null 时会报 NullPointerException
msg.toString();
}
// 编译时 提示参数不能为 null
// Null can not be a value of a non-null type String
getMsg(null);
2. 代替简单枚举
枚举类比常量更占内存, 因为一个
Java
对象至少占16
个字节, 而枚举类包含了3个Java
对象;因此可以考虑用注解代替简单枚举
作用:使用注解实现语法检查。
实现:
// 枚举
object WeekDayDemo{
private lateinit var weekDay: WeekDay
enum class WeekDay{
SATURDAY,SUNDAY
}
fun getWeekDay():WeekDay{
return weekDay
}
fun setWeekDay(weekDay: WeekDay){
this.weekDay = weekDay
}
}
// 调用
WeekDayDemo.setWeekDay(WeekDayDemo.WeekDay.SATURDAY)
WeekDayDemo.getWeekDay()
// 使用注解,现在参数
public class WeekDayAnnotationDemo {
public static final String SATURDAY = "SATURDAY";
public static final String SUNDAY = "SUNDAY";
private static String weekDay;
@StringDef({SATURDAY, SUNDAY})
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.SOURCE)
@interface WeekDay{
//自定义一个 WeekDay 注解
}
public static void setWeekDay(@WeekDay String weekDay) {
WeekDayAnnotationDemo.weekDay = weekDay;
}
public static String getWeekDay() {
return weekDay;
}
}
WeekDayAnnotationDemo.setWeekDay(WeekDayAnnotationDemo.SATURDAY)
WeekDayAnnotationDemo.getWeekDay()
3. Butterknife @BindView 实现
作用:替代 findViewById
如果使用 kotlin
开发, 推荐使用方式一。
方式一:使用kotlin插件自动生成
引入kotlin扩展插件
apply plugin: 'kotlin-android-extensions'
引入kotlin自动生成的相关布局文件
import kotlinx.android.synthetic.main.activity_main.*
在UI中使用
tv_hello_annotation.text = "hello annotation"
方式二:使用注解
object InjectView {
fun init(activity:Activity){
try {
val aClass = activity.javaClass
val declaredFields = aClass.declaredFields
for ( field in declaredFields){
if (field.isAnnotationPresent(BindView::class.java)){
val bindView = field.getAnnotation(BindView::class.java)
val id = bindView?.value
if (id != null){
val view = activity.findViewById<View>(id)
field.isAccessible = true
field.set(activity,view)
}
}
}
}catch (e:Exception){
// exception
}
}
}
// 创建 @BindView 注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
@IdRes int value(); // 限制只能传 id 资源
}
// 在 activity 使用
class MainActivity : AppCompatActivity() {
@BindView(R.id.tv_hello_annotation)
private var tvData: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
InjectView.init(this)
tvData?.text = "hello annotation"
}
}
4. AopArms 项目源码
AopArms
项目基于沪江aspect插件库
同时利用注解的特性实现一些特定额业务场景。如:日志、拦截(登录)、异步处理、缓存、SP、延迟操作、定时任务、重试机制、try-catch
安全机制、过滤频繁点击等。
具体可点击下方链接查看。(微信不支持外链,可点击原文查看)
源码:
https://github.com/aicareles/AopArms
文章介绍:
https://mp.weixin.qq.com/s/cjGADwzfb3hWtfYvm0J2LQ
其他问题
注释和注解的区别
注解用于机器去理解代码,是代码的一部分。注释用于人去理解代码,不是代码的一部分。
注解是如何解决反射的问题?
不知道你是否听说过:
使用注解可以解决反射的性能问题
这里说的注解大部分使用的是编译时注解,也有用运行时注解但是内部肯定做了缓存或者索引优化。
总结
注解不是为了替代
xml
,而是提供另一种代码和配置文件之间联系的方式。注解的作用:
在编译时给编译器提示
在编译时可配合注解处理器生成辅助文件
在运行时提供数据
注解的本质是继承了
annatation
接口的接口注解的分类
源码级注解:
@Retention(RetentionPolicy.SOURCE)
编译时注解:默认,使用
@Retention(RetentionPolicy.CLASS)
定义注解运行时注解:
@Retention(RetentionPolicy.RUNTIME)
预定义注解:
Java 常见的 @Override,@Deprecated,@SuppressWarmings,Android常见的 @NonNull
元注解:
@Target, @Retention, @Documented, @Inherited
自定义注解:
使用 @interface
声明
注解是和代码有关是能提供给机器查看的,注释是代码无关的仅提供给人查看的
大部分框架都用了注解去解决反射问题,这里的注解是结合注解处理器在编译阶段生成辅助代码并提供给运行时使用。这就导致编译时间会变长,但由于没有使用反射,所以不会出现反射的性能问题。
参考
Android coder 需要理解的注解、反射和动态代理
Java | 这是一篇全面的注解使用攻略(含 Kotlin)
Java注解(Annotation)详解
AopArms一款基于AOP的Android注解框架
Android 如何编写基于编译时注解的项目
yangchong211/YCApt (自定义路由注解)
