前言
平时在开发中接触过许多的注解,如@Override,@Nullable等,但自己代码中还没怎么用过。所以,就想着学习学习,然后用一下。
什么是注解?
注解是一种将元数据与程序元素关联在一起的安全的类似注释的机制。那什么是元数据?元数据就是描述数据的数据,比如下面的代码,toString方法是数据,@Override就是描述toString方法的元数据。
@Override
public String toString() {
return super.toString();
}
那程序元素都是指哪些呢?除了上面示例中说的方法,还可以是指类、变量、方法参数、构造方法以及包声明(用于对包声明进行说明的注解,我见的不多)。
注解的作用
上面说了,注解是可以与程序元素进行关联,也可以对程序元素进行描述(注意,这里说的描述可不仅仅是指对程序元素进行说明,也会对程序元素有约束作用)。那有这些能力具体有什么用呢?首先,它可以用来生成文档,一般我们在写方法注释时,可能会在其中引入一些其它的类、方法、成员变量等进行说明,这时候会用到@link,也可能会对方法的参数、返回值类型进行说明,这就用到了@param和@return。比如handler的某个obtainMessage方法,其注释时如下这样的:
/**
*
* Same as {@link #obtainMessage()}, except that it also sets the what and obj members
* of the returned Message.
*
* @param what Value to assign to the returned Message.what field.
* @param obj Value to assign to the returned Message.obj field.
* @return A Message from the global message pool.
*/
public final Message obtainMessage(int what, Object obj){
...
}
这个注释就包含了对注解如@link,@param,@return的使用。注解的另一个作用是,“跟踪代码的依赖性,实现替代配置文件的功能”。比如说某个字段使用了注解,就可将其实例化的对象赋值给它,或者将android布局文件中的view对象赋值给它。还有一个作用就比较简单了,就是配合编译器在编译阶段对代码的检查,比如@Override,父类及其祖类如果没有此方法而子类的方法使用了该注解,编译器就会报错了。
其实,用于描述数据的元数据可以使用注解的形式,也可以使用xml的形式。xml可以做到配置分离,即代码和配置文件分开,比较适合给应用设置很多的常量或参数;而注解是紧耦合的,当需要元数据与代码紧密耦合在一起时使用注解就比较好。java中的一个示例,把某个方法声明为服务,使用注解就比较好。另外,元数据使用xml形式有可能由于其配置分离的不耦合或松耦合导致xml文件过多难于维护,使用注解就可以有效避免这个问题。
什么是元注解?
上面说的是注解及其作用,而注解是怎么定的呢?这就要先了解一下元注解。所谓“元注解”,其实就是用于定义注解的注解。java总共给出了我们4种元注解:
- @Documented,注解是否将包含在javaDoc中
- @Retention,什么时候使用该注解
- @Target,注解用于什么地方
- @Inherited,是否允许子类继承该注解
@Documented,是否将注解信息添加到java文档中;
@Retention,定义注解的生命周期的。有三个取值:
- RetentionPolicy.SOURCE,这个值表示的是此注解在源码阶段使用,在代码编译阶段就被丢弃了,不会被写到字节码中。示例就是@Override,@SuppressWarnings。
- RetentionPolicy.CLASS,这种注解会被写到字节码中,在字节码阶段使用,在类加载器时丢弃,也就是说字节码文件被加载进内存之后的Class对象通过反射是不能获取到此注解的。比较重要的一点,如果定义注解时没有使用@Retention,那默认就是这种CLASS类型的。
- RetentionPolicy.RUNTIME,在程序运行阶段使用,即便是字节码加载进内存之后的Class对象也会有该注解,通过反射也会获取到此注解,这种注解就是一直存在着的。需要注意的一点是,一般我们自己写的自定义注解需要设置成这个值。
@Target,表示注解作用于程序的哪个元素(类,成员变量,构造方法,方法,方法参数,局部变量,包声明)。一般的取值有下面这么多,定义注解但不使用@Target,那就表明这个注解可以作用在任何程序元素上。(后面会发现,还有注解类型等)
- ElementType.TYPE,作用目标为类、接口、enum声明;
- ElementType.FIELD,作用于成员变量(包括enum实例);
- ElementType.CONSTRUCTOR,作用于构造方法;
- ElementType.METHOD,作用于方法;
- ElementType.PARAMETER,作用于方法参数;
- ElementType.LOCAL_VARIABLE,作用于局部变量;
- ElementType.PACKAGE,作用于包声明;
@Inherited,使用了@Inherited的注解不仅作用于当前该程序元素,还会作用于继承或重写该程序元素的子程序元素。比如说,使用了@Inherited的某注解是作用于类的,那么此注解也会约束到该类的子类。
注解是怎么定义呢?
上面主要说的是,定义注解的注解-元注解。那一般一个注解是怎么定义的呢?以@Override为例:
package java.lang;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Target的取值是ElementType.METHOD,说明此注解是作用于类中方法上的;@Retention的取值是RetentionPolicy.SOURCE,说明此注解在源码阶段使用,在编译阶段就会被丢弃。使用@interface定义了一个注解,但注解的内容却啥都没有。那它是怎么去校验当前类的父类及祖类有没有声明此方法的?这个校验的具体实现在哪里呢?这里要说的是,注解元数据与业务逻辑是无关的,它本身在声明时定义属性信息(类、方法、包、域),在被使用时给这些属性信息赋值,而拿到这个注解及其属性信息并进行一些逻辑操作的是另一些代码。这样说,有些抽象。拿一个别人的例子来讲吧:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Todo {
//这两个枚举是提供常量值的,下面那三个是注解的方法,自带默认值
public enum Priority {LOW, MEDIUM, HIGH}
public enum Status {STARTED, NOT_STARTED}
String author() default "Yash";
Priority priority() default Priority.LOW;
Status status() default Status.NOT_STARTED;
}
上面是一个自定义注解的声明,作用于方法,运行期间也一直都会存在,然后其中定义了一些属性:author、priority、status(3个带默认值的方法)。下面这段代码是对上述自定义注解的使用,也就是给属性赋值的阶段:
//以此给注解的各属性赋值
@Todo(priority = Todo.Priority.MEDIUM, author = "Yashwant", status = Todo.Status.STARTED)
public void incompleteMethod1() {
//Some business logic is written
//But it’s not complete yet
}
我们可以通过反射的方式对这些注解属性值进行使用
//这个类是使用了@Todo注解的方法所在的类
Class businessLogicClass = BusinessLogic.class;
//获取此类中的可访问到的方法并进行遍历
for(Method method : businessLogicClass.getMethods()) {
//当前遍历到的此方法是否有Todo这个注解,有此注解就输出一些信息
Todo todoAnnotation = (Todo)method.getAnnotation(Todo.class);
if(todoAnnotation != null) {
System.out.println(" Method Name : " + method.getName());
//调用了注解的各个方法
System.out.println(" Author : " + todoAnnotation.author());
System.out.println(" Priority : " + todoAnnotation.priority());
System.out.println(" Status : " + todoAnnotation.status());
}
}
说明几点:在反射获取此注解时,注解的表现就像一个正常的类,有Todo.class,也有Todo类型;获取方法的注解使用的是Method的getAnnotation方法;原来注解中声明的方法,在获取了注解对象后直接注解对象.声明的方法就可以调用得到。
继续回到上述问题:@Override校验当前类的父类及祖类有没有声明此方法的具体代码在哪里?上面的代码,Todo注解是我们自定义的,被使用的具体实现也是在我们自己写的代码,而@Override这样的注解,是java本来就有的,在编译阶段丢弃,在编译(javac)阶段之前使用,那么基本可以猜测,其具体实现是由jdk或jre的某一部分来实现的(这是废话,只是目前我也不能确定具体是哪部分)。另外,注解一般是跟编译器配合做出警告提示的,这里面可能就涉及到编译器和jdk或jre的交互了(又是猜的)。
自定义注解在使用时还需注意的点
怎么定义一个注解?
public @interface 注解名 {定义体}
使用@interface声明注解时,就自动继承了java.lang.annotation.Annotation接口,其它的细节会由编译器去完成。
注解属性都支持哪些类型呢?
- 所有基本数据类型(int,float,boolean,byte,double,char,long,short)
- String
- Class(获取Class对象)
- enum
- Annotation(注解类型)
- 以上所有类型的数组类型
另外,注解属性只能使用public和default两个修饰符,另外当某注解只定义了一个属性时,最好使用下面的参数声明和使用:
//定义只有一个属性的注解
@interface Author{
String value();
}
//使用此注解
@Author("Yashwant")
public void someMethod() {
}
android中的注解库是什么?常见的注解都有哪些?
android的注解库就是support annotations,是从support Library 19.1版本开始引入的。其中的注解大致可分为下面几类:
- Nullness注解
- 资源类型注解
- Typedef注解
- Value Constraints注解
- 线程相关注解
- 颜色注解
- 权限注解
- 覆写方法注解
- 返回值注解
- 其它一些注解
下面逐一对上述分类进行说明:
Nullness注解
这个类型的注解有两个:@Nullable和@NonNull。前者表示目标程序元素可为空,后者表示目标程序元素不能为空。它们的作用范围是一样的,都可作用于方法、方法参数、成员变量、局部变量、注解、包声明。如果传入参数不合法,就会报出警告提示。
这类注解很好理解,就是用来限制目标程序元素是否可传空的。
资源类型注解
这里说的资源是app中诸如字符串、style、layout、drawable等资源,它们向程序代码引用它们的地方提供的是整数,R.string.xxx的值为整数,R.style.yyy的值也为整数,这样就可能向需要R.string.xxx的地方提供了R.style.yyy造成错误。为了避免这种错误,我们可以对引用资源处的参数添加资源类型注解,这样该传递style整数的只能穿style整数,否则编译器会报错。因为资源类型有很多种,所以资源类型的注解也有很多种,共22种。
- AnimatorRes:animator资源类型
- AnimRes:ani资源类型
- AnyRes:任意资源类型
- ArrayRes:array资源类型
- AttrRes:attr资源类型
- BoolRes:boolean资源类型
- ColorRes:color资源类型
- DimenRes:dimen资源类型
- DrawableRes:drawable资源类型
- FractionRes:fraction资源类型
- IdRes:id资源类型
- IntegerRes:integer资源类型
- InterpolatorRes:interpolator资源类型
- LayoutRes:layout资源类型
- MenuRes:menu资源类型
- PluralsRes:plural资源类型
- RawRes:raw资源类型
- StringRes:string资源类型
- StyleableRes:styleable资源类型
- StyleRes:style资源类型
- TransitionRes:transition资源类型
- XmlRes:xml资源类型
各种各样的注解,用来标注各种各样的资源类型。注解标注了某参数,那么该参数就只能接受该资源类型的参数,否则编译器就会报错。另外,需要说的一点是,资源类型的目标程序元素一般是方法、方法参数、成员变量、局部变量。
资源类型注解,主要作用是约束传入资源的类型的,指定了该类型就只能传入该类型的资源进来。
Typedef注解
这种类型的注解有两种:@IntDef和@StringDef。这两个注解可以用来定义另一个注解,定义时提供一个常量列表,如下所示:
private final static int GET=0;
private final static int POST=1;
private final static int DELETE=2;
private final static int PUT=3;
@IntDef({GET, POST, DELETE,PUT})
@Retention(RetentionPolicy.SOURCE)
public @interface ReqType{}
上述代码只拿@IntDef来举例,定义好的注解@ReqType作用到目标程序元素时,目标程序元素就只能接受GET、POST、DELETE、PUT中的某个值。@StringDef与@IntDef的使用时类似的,这里不再多说。
Typedef注解的主要作用是,约束目标程序元素接受的值,只允许几个允许的常量传给目标程序元素。
Value Constraints注解
可以翻译成“值约束注解”,这种类型的注解有三种@Size、@IntRange和@FloatRange。
- @Size,限制集合或字符串的长度或大小
- @IntRange,限制目标程序元素的取值范围(整数)
- @FloatRange,限制目标程序元素的取值范围(浮点数)
@Size(min=1),表示集合或者字符串的最小长度不能为空;@Size(max=10),表示集合或者字符串的最大长度不能长于10;@Size(5),表示集合或者字符串的长度只能为5;@Size(multiple=2),表示集合或者字符串的长度必须是2的倍数。另外,括号内的内容,可以组合使用。比如@Size(min = 2, max = 3),表示集合或者字符串的长度最小为2,最大为3.
@IntRange与@FloatRange的使用方法类似,这里只拿@IntRange举例。@IntRange(from = 2, to = 10),表示被作用的目标程序元素的取值只能为2-10之间的整数(包含2,10)。
@Size的主要作用是,限制目标集合或者字符串的长度或大小;@IntRange和@FloatRange,限制目标程序元素的取值在一个范围内。
线程相关注解
这类注解总共有4个:@UiThread、@MainThread、@WorkerThread、@BinderThread。这4个注解的作用范围都是为类、构造方法、方法。它们含义如下:
- @UiThread,要求作用的目标程序元素要运行在UI线程中;
- @MainThread,要求作用的目标程序元素要运行在主线程中;
- @WorkerThread,要求作用的目标程序元素要运行在子线程中;
- @BinderThread,要求作用的目标程序元素要运行在binder线程中。
经查资料,UI线程和主线程在android上是同一个概念。所以,@UiThread与@MainThread的作用应该是一样的。@WorkerThread是运行在子线程中,这个好理解,方法或者类要运行在子线程中才可以。@BinderThread,要求方法或类运行在binder线程中。关于binder线程的理解,我们知道app进程用来接收从AMS传递过来的信息的ApplicationThread就是运行在Binder线程的,主线程进入无限循环时,给主线程发消息唤醒主线程处理消息的很多都是Binder线程。有些方法或者类存在的意义就是为了服务Binder线程,这些方法和类就可以使用注解@BinderThread。
线程相关的注解就是为了约束程序执行在哪个线程的。
颜色注解
这类注解有一个:@ColorInt。这个意思是,有时候我们的方法需要的一个颜色值(是一个整型值),而不是资源颜色值,这时候就可用此注解。
@ColorInt的主要作用就是约束目标程序元素只接收颜色值。
权限注解
这类注解有一个:@RequiresPermission。有些方法我们需要调用方取得了某种权限之后才能调用。如下:
@RequiresPermission(Manifest.permission.SET_WALLPAPER)
public abstract void setWallpaper(Bitmap bitmap) throws IOException;
上面这段代码的意思是,setWallpaper方法需要调用方取得SET_WALLPAPER权限后才能够调用。如果被调用方法是要求几种权限中的至少一种,可以像下面这样写
@RequiresPermission(anyOf = {
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION})
public abstract Location getLastKnownLocation(String provider);
如果被调用方法是要求全部几种权限,则可以像下面这样写
@RequiresPermission(allOf = {
Manifest.permission.READ_HISTORY_BOOKMARKS,
Manifest.permission.WRITE_HISTORY_BOOKMARKS})
public static final void updateVisitedHistory(ContentResolver cr, String url, boolean real) ;
@RequiresPermission也可以作用到字段上,表示使用此字段需要获取什么权限。比如Intent常量字符串字段
@RequiresPermission(android.Manifest.permission.BLUETOOTH)
public static final String ACTION_REQUEST_DISCOVERABLE =
"android.bluetooth.adapter.action.REQUEST_DISCOVERABLE";
@RequiresPermission对字段的作用可以进一步扩展,可以做到对此字段的读写分别设定相应权限才能访问。
@RequiresPermission.Read(@RequiresPermission(READ_HISTORY_BOOKMARKS))
@RequiresPermission.Write(@RequiresPermission(WRITE_HISTORY_BOOKMARKS))
public static final Uri BOOKMARKS_URI = Uri.parse("content://browser/bookmarks");
权限注解的主要作用是约束程序中的调用方对所调方法、所调字段、所调方法参数的访问,有权限才能访问,无权限不能访问。
覆写方法注解
这类注解也是有一个:@CallSuper。如果父类的方法需要子类去调用,那可以在父类方法上使用此注解。子类如果没在覆写方法中调用父类编译器就会报错。
覆写方法注解的主要作用是约束子类的方法必须调用父类父方法。
返回值注解
返回值注解也是有一个:@CheckResult。你写了一个方法,你要求调用此方法的调用方必须要使用该方法的返回值,不使用就报错,那你可以使用此@CheckResult注解。
返回值注解,主要作用是约束调用方必须使用方法的返回值。
其他注解
@Keep,在混淆时,使用此注解的方法应该被保留;@Keep,在混淆时,使用此注解的方法应该被保留;@VisibleForTesting,使类、方法、方法变量拥有更多的可见性,以便于测试。
常见的注解说明
@Override
java.lang.Override是一个标记类型注解,它被用作标注方法。它说明了被标注的方法重载了父类的方法,起到了断言的作用。如果我们使用了这种注解在一个没有覆盖父类方法的方法时,java编译器将以一个编译错误来警示
@Override,简单来说,就是用来表明当前的方法是从父类方法中覆写而来的。
@Deprecated
Deprecated也是一种标记类型注解。当一个类型或者类型成员使用@Deprecated修饰的话,编译器将不鼓励使用这个被标注的程序元素。所以使用这种修饰具有一定的“延续性”:如果我们在代码中通过继承或者覆盖的方式使用了这个过时的类型或者成员,虽然继承或者覆盖后的类型或者成员并不是被声明为@Deprecated,但编译器仍然要报警
@Deprecated,简单来说,标注某个方法或字段已经过时了,以后不要再用了。
@SuppressWarnings
SuppressWarning不是一个标记类型注解。它有一个类型为String[]的成员,这个成员的值为被禁止的警告名。对于javac编译器来讲,被-Xlint选项有效的警告名也同样对@SuppressWarings有效,同时编译器忽略掉无法识别的警告名
@SuppressWarnings("unchecked")
@SuppressWarnings,简单来说就是用来屏蔽某些警告的。
@TargetApi (Build.VERSION_CODES.XX)
用于屏蔽某一新api中才能使用的方法报的lint检查出现的错误。简单来说,@TargetApi是给使用了此注解的方法添加访问权限,跟上面说的@RequiresPermission有点儿类似。
@SuppressLint(“NewApi”)
屏蔽一切新api中才能使用的方法报的android lint错误。简单来说,应用中最低sdk版本不能使用的api会报lint错误,使用此注解,可以使它不报这个错误。
@SdkConstant
表示一个常量字段应该被输出在SDK工具中使用,例如,添加一个自定义的Action
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_MY_TEST = "android.intent.action.MY_TEST";
然后这么使用
Intent myTest = new Intent(Intent.ACTION_MY_TEST);
mContext.sendBroadcast(myTest);
这个用的少,感觉不太重要。
@Widget
用于类的注解,表示该类是自定义的Widget类
总结
一开始,对注解的理解是将元数据与程序元素关联在一起进行或约束或说明的机制。后来的理解更具体,注解是通过元数据定义属性信息,在使用此注解时给这些属性信息赋值,然后通过某种方式(比如反射)使用这些属性信息做操作的机制。
android本身有注解库:support annotation,它里面有Nullness注解,可以约束目标程序元素可空或不可为空;有资源类型注解,可以约束目标程序元素只能传递某种资源类型的整数;有Typedef注解,可以修饰其它注解,约束目标程序元素只能接受规定的几个整数或字符串中的某一个值;有Value Constraints注解,可以约束目标集合或目标字符串的长度,或约束目标程序元素只能接受在一个整数范围或浮点数范围内的值;有线程相关注解,可以约束目标程序元素只能运行在主线程、子线程或binder线程内;有权限注解,可以约束调用目标程序元素的调用方要具备某种权限才能进行调用访问;有覆写方法注解,要求子类覆写方法必须在其方法内调用父类的该父方法;有返回值注解,要求调用某方法的调用方必须使用该方法的返回值等等。
参考资料
深入理解Java:注解(Annotation)自定义注解入门
使用Android Support Annotations优化你的代码
Android Support库——support annotations