java注解机制_Java注解机制基础

最近整体研究了一下ButterKnife这个框架,顺带着系统回顾梳理了一遍Java注解相关的知识点。本文大致整理了一下注解的几个核心部分,内容参考了很多现有的资料,在参考文献中做出了说明。希望通过本文能对注解的基本原理做一个简要的梳理,为后续两篇文章深入的分析ButterKnife实现原理做好铺垫。

1.注解概述

(1)定义

注解(Annotation)是Java1.5中引入的一个重大修改之一,为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便的使用这些数据。

注解在一定程度上是把元数据与源代码结合在一起,而不是保存在外部文档中。

注解的含义可以理解为java中的元数据。元数据是描述数据的数据。如下所示:

5.4

这里的”app_version”就是描述数据”5.4”的数据,这是在配置文件中写的,注解是在源码中写的。另外一种开发人员很熟悉的元数据是注释(comment)。注释用来描述源代码中的类,域和方法的作用等。

(2)注解与注释的不同注解会影响代码的行为,而注释不会;

编译器对代码进行处理时,注释会被直接删除,而注解可能会被保留在字节代码中;

(3)注解的作用格式检查:告诉编译器信息,比如被@Override标记的方法如果不是父类的某个方法,IDE会报错;

减少配置:运行时动态处理,得到注解信息,实现代替配置文件的功能;

减少重复工作:减轻编写“样板”代码的负担,比如第三方框架ButterKnife,通过注解@BindView减少对findViewById的调用,类似的还有(Lombok,Dagger,AndroidAnnotation等);

(4)注解是如何工作的

注解仅仅是元数据,和业务逻辑无关,所以查看注解类时,发现里面没有任何逻辑处理:

@Retention(RetentionPolicy.CLASS)

@Target({ElementType.FIELD})

public @interface InjectView {

int value();

}

如果注解不包含业务逻辑处理,必然有人来实现这些逻辑。注解的逻辑实现是元数据的用户来处理的,注解仅仅提供它定义的属性(类/方法/变量/参数/包)的信息,注解的用户来读取这些信息并实现必要的逻辑。当使用java中的注解时(比如@Override、@Deprecated、@SuppressWarnings)JVM就是用户,由编译器来负责处理。如果是程序中自定义的注解,就需要开发者或者第三方库自己去进行处理了。

(5)注解和配置文件的区别

通过上面的描述可以发现,其实注解干的很多事情,通过配置文件也可以干,比如为类设置配置属性;但注解和配置文件是有很多区别的,在实际编程过程中,注解和配置文件配合使用在工作效率、低耦合、可拓展性方面才会达到权衡。

配置文件:

a.使用场合

外部依赖的配置,比如build.gradle中的依赖配置;

同一个项目团队内部达成一致的时候;

非代码类的资源文件(比如图片,布局,数据,签名文件等);

b.优点

降低耦合,配置集中,容易扩展,比如Android应用多语言支持;

对象之间的关系一目了然,比如strings.xml;

xml配置文件比注解功能齐全,支持的类型更多,比如drawable、style等;

c.缺点

繁琐;

类型不安全,比如R.java中的都是资源ID,用TextView的setText方法时传入int值时无法检测出该值是否为资源ID,但@StringRes可以;

注解:

a.使用场合

动态配置信息;

代为实现程序逻辑(比如ButterKnife中的@BindView代替实现findViewById);

代码格式检查,比如@Override、@Deprecated、@NonNull、@StringRes等,便于IDE能够检查出代码错误;

b.优点

在class文件中,提高程序的内聚性;

减少重复工作,提升开发效率,比如findViewById;

c.缺点

如果对Annotation修改,需要重新编译整个工程;

业务类之间的关系不如xml配置那样一目了然;

程序中过多的Annotation,对于代码的简洁度有一定影响;

扩展性较差;

(6)注解类型

很多资料对Java Annotation进行了分类,大部分是分为:Java内置、元注解、自定注解。这里因为考虑到Android库提供的注解,本文将其总结归纳为如下几种类型:

Java标准库中内置的注解(Built-in Java Annotations)

元注解(Meta-annotations)

Android Support Annotations

自定义注解

2.Java标准库中内置的注解

Java内置注解;定义在java.lang中,Java1.5中内置了三个:@Deprecated、@Overrride、@SuppressWarnings。java1.7中增加了@SafeVarargs,java1.8中增加了@FunctionsalInterface。详细介绍如下:

(1)java.lang.Override

用于表示一个方法声明覆写父类型中的对应方法。Override注解的作用是避免开发人员没有正确区分方法重载和覆写而带来的错误。当需要覆写一个方法的时候,可以在方法声明前面加上Override注解。如果这个方法实际上并没有覆写父类型中的方法,而是进行了重载,那么编译器将产生相应的错误信息。如下所示的代码:

class User{

public boolean equals(User user){

return true;

}

}

User类的equals方法本意是覆写Object类中的equals方法,提供自己的对象比较方式的实现,而实际上User类中的equals方法并没有覆写Object类的equals方法,而是提供了一个重载的形式。这种错误在编程过程中很难发现。如果加上了Override注解,那么编译器会给出错误的信息,开发人员会意识到这个错误,并根据需要进行相应的修改。

@Target({ElementType.METHOD})

@Retention(RetentionPolicy.SOURCE)

public @interface Override {

}

(2)java.lang.Deprecated

属于标记注解,不需要设置属性值;可以对构造方法、变量、方法、包、参数标记,告知用户和编译器被标记的内容已不建议被使用,如果被使用,编译器会报警告,但不会报错,程序也能正常运行:

@Documented

@Retention(RetentionPolicy.RUNTIME)

@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.LOCAL_VARIABLE, ElementType.METHOD, ElementType.PACKAGE, ElementType.PARAMETER, ElementType.TYPE})

public @interface Deprecated {

}

(3)java.lang.SuppressWarnings

可以对构造方法、变量、方法、包、参数标记,用于告知编译器忽略指定的警告,不用在编译完成后出现警告信息:

@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.LOCAL_VARIABLE})

@Retention(RetentionPolicy.SOURCE)

public @interface SuppressWarnings {

String[] value();

}

(4)@SafeVarargs(Java SE 7)

作用:Claims to the compiler that the annotation target does nothing potentially unsafe to its varargs argument.。

@SafeVarargs用于指明那些使用可变长度参数的方法和构造器是安全的。这些方法能被传入长度可变的参数。这些参数可以是泛型的。如果它们是泛型参数,使用@SafeVarargs注释就可以抑制警告信息。使用该注解的前提是,开发人员必须确保这个方法的实现中对泛型类型参数的处理不会引发类型安全问题。

使用示例:

@SafeVarargs

public static void displayElements(T... array){

for (T element : array) {

System.out.println(element.getClass().getName() + ":" + element);

}

}

在这里,方法displayElements的形参是可变长度的泛型类型。

(5)@FunctionalInterface(Java SE 8)

作用:An informative annotation type used to indicate that an interface type declaration is intended to be a functional interface as defined by the Java Language Specification. 帮助编译器检查函数式接口的合法性。

3.Java标准库中的元注解(Meta-annotations)

所谓元注解,就是负责注解其他注解的,主要是描述注解的一些属性,任何注解都离不开元注解,元注解的用户是JDK,JDK已经帮助我们实现了这些元注解的逻辑。常见的四个类型是@Documented、@Inherited、@Retention、@Target。在Java 8新增了@Repeatable。具体描述如下:

(1)@Target:

作用:用于描述注解的使用范围,即被描述的注解可以用在什么地方(Indicates the contexts in which an annotation type is applicable);

取值:

CONSTRUCTOR:构造器;

FIELD:实例;

LOCAL_VARIABLE:局部变量;

METHOD:方法;

PACKAGE:包;

PARAMETER:参数;

TYPE:类、接口(包括注解类型) 或enum声明;

ElementType 常量在 Target 注解中至多只能出现一次,如下是非法的:@Target({ElementType.FIELD, ElementType.METHOD, ElementType.FIELD})

(2)@Retention:

作用:表示需要在什么级别保存该注解信息,用于描述注解的生命周期,即被描述的注解在什么范围内有效(Indicates how long annotations with the annotated type are to be retained.);

取值:

RetentionPolicy.SOURCE:

The marked annotation is retained only in the source level and is ignored by the compiler.

该注解只保留到代码层,编译器将对其忽略。因此其不会出现在生成的class文件中;

RetentionPolicy.CLASS:

The marked annotation is retained by the compiler at compile time, but is ignored by the Java Virtual Machine (JVM).

该注解保留在编译器中,因此能出现在生成的class文件中,但是被JVM忽略,所以不能在运行时获取注解内容。

RetentionPolicy.RUNTIME:

The marked annotation is retained by the JVM so it can be used by the runtime environment.

该注解能保留在JVM中,可以在运行时通过反射的方法获取具体内容。

如果自定义注解时不进行指定,默认为RetentionPolicy.CLASS。

(3)@Documented:

作用:指示该注解是否默认通过Javadoc或者类似的工具进行文档化,是一种语义元注解(Indicates that annotations with a type are to be documented by javadoc and similar tools by default)。

取值:它属于标记注解,没有成员;

(4)@Inherited:

作用:指示注解类型被自动继承。如果在解析注解时发现了该字段,并且在该类中没有该类型的注解,则对其父类进行查询。举个例子,如果一个类中,没有A注解,但是其父类是有A注解的,并且A注解是被@Inherited注解的(不妨认为保留时态是Runtime),那么使用反射获取子类的A注解时,因为获取不到,所以会去其父类查询到A注解。使用了@Inherited注解的类,这个注解是可以被用于其子类。

取值:它属于标记注解,没有成员;

(5)@Repetable(Java 8中新增加的):

作用:指示该注解是否可以多次使用(used to indicate that the annotation type whose declaration it (meta-)annotates is repeatable.)。在这个注解出现前,一个位置要想注两个相同的注解,是不可能的,编译会出错误。所以要想使一个注解可以被注入两次,需要声明一个高级注解,这个注解中的成员类型为需要多次注入的注解的注解数组。

取值:它属于标记注解,没有成员;

使用示例:

在没有该注解以前:

public @interface Authority {

String role();

}

public class RepeatAnnotationUseOldVersion{

@Authority(role="Admin")

@Authority(role="Manager")

public void doSomeThing(){

}

}

类似于这样的使用编译器是会报错的。

通常的做法是:

public @interface Authority {

String role();

}

public @interface Authorities {

Authority[] value();

}

public class RepeatAnnotationUseOldVersion{

@Authorities({@Authority(role="Admin"),@Authority(role="Manager")})

public void doSomeThing(){

}

}

在java 8以后:

@Repeatable(Authorities.class)

public @interface Authority {

String role();

}

public @interface Authorities {

Authority[] value();

}

public class RepeatAnnotationUseNewVersion{

@Authority(role="Admin")

@Authority(role="Manager")

public void doSomeThing(){ }

}

在注解Authority上告诉该注解,如果多次用Authority注解了某个方法,则自动把多次注解Authority作为Authorities注解的成员数组的一个值,当取注解时,可以直接取Authorities,即可取到两个Authority注解。要求:@Repeatable注解的值的注解类Authorities.class,成员变量一定是被注解的注解Authority的数组。

不同的地方是,创建重复注解Authority时,加上@Repeatable,指向存储注解Authorities,在使用时候,直接可以重复使用Authority注解。从上面例子看出,java 8里面做法更适合常规的思维,可读性强一点。其实从原理上看和第一种是一模一样的,只是增加了可读性。

其实在Java 8中,Annotation还得到了很多很好的扩展。更多Java 8对Annotation提供的改进和支持可参阅:Java 8 Annotation 新特性在软件质量和开发效率方面的提升

4.Android Support Annotations

Android support library从19.1版本开始引入了一个新的注解库,它包含很多有用的元注解,开发者可以用他们来修饰代码,帮助发现bug。Support library自己本身也用到了这些注解,所以作为support library的用户,Android Studio已经基于这些注解校验了开发者的代码并且标注其中潜在的问题。

这些注解是作为一个support包提供给开发者使用,要使用他们,需要在build.gradle中添加对android support-annotations的依赖:

compile 'com.android.support:support-annotations:22.2.0'

提供的注释概览如下:

2017_01_09_android_annotation_ovewview.png

下面分类说明如下:

(1)Nullness Annotations:

作用:check the nullness of a given variable, parameter, or return value.

@Nullable:用于标记方法参数或者返回值可以为空;

@NonNull:用于标记方法参数或者返回值不能为空,如果为空编译器会报警告

(2)Resource Annotations:

主要用于标记方法的参数必须要是指定的资源类型,如果不是,IDE就会给出警告。因为资源文件都是静态的,所以在编写代码时IDE就知道传值是否错误,可以避免传的资源id错误导致运行时异常。如下所示:

public abstract void setTitle(@StringRes int resId){ … }

在代码检查阶段,如果传递的引用类型不是R.string类型,编辑器会给出警告。

资源类型注解包括:

@AnimatorRes、@AnimRes、@AnyRes、@ArrayRes、@BoolRes、@ColorRes、@DimenRes、@DrawableRes、@FractionRes、@IdRes、@IntgerRes、@InterpolatorRes、@LayoutRes、@MenuRes、@PluralsRes、@RawRes、@StringRes、@StyleableRes、@StyleRes、@TransitionRes、@XmlRes。

@AnyRes:表示可以是任何资源类型;

(3)Thread Annotations:

作用:用于标记指定的方法、类(如果一个类中的所有方法都有相同的线程需求,就可以对这个类进行注解,比如View.java就被@UIThread所标记)只能在指定的线程类中被调用。

类型:

@MainThread

@UiThread

@WorkerThread

@BinderThread

@AnyThread

典型使用:A common use of the thread annotation is to validate method overrides in the AsyncTask class class performs background operations and publishes results only on the UI thread.

(4)Value Constraint Annotations(值约束注解):

用于标记参数必须是指定类型的值,并且值的范围必须在约束的范围内,包括@Size、@IntRange、@FloatRange。

@IntRange

标识一个interger或者lang型的参数在一个指定的范围之内;

public void setAlpha(@IntRange(from=0,to=255) int alpha){ … }

@FloatRange

标识一个float或者double型的参数在一个指定的范围之内;

public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha){...}

@Size

对于数据、集合以及字符串,可以用@Size注解参数来限定集合的大小(当参数是字符串的时候,可以限定字符串的长度)。

集合不能为空: @Size(min=1);

字符串最大只能有23个字符: @Size(max=23);

数组只能有2个元素: @Size(2);

(5)Permission Annotations(权限注解):

如果方法需要调用者有特定的权限,可以使用@RequiresPermission注解。

@RequiresPermission(Manifest.permission.SET_WALLPAPER)

public abstract void setWallpaper(Bitmap bitmap) throws IOException;

如果至少需要权限集合中的一个,可以使用anyOf属性:

@RequiresPermission(anyOf = {

Manifest.permission.ACCESS_COARSE_LOCATION,

Manifest.permission.ACCESS_FINE_LOCATION})

public abstract Location getLastKnownLocation(String provider);

如果你同时需要多个权限,可以用allOf属性:

@RequiresPermission(allOf = {

Manifest.permission.READ_HISTORY_BOOKMARKS,

Manifest.permission.WRITE_HISTORY_BOOKMARKS})

public static final void updateVisitedHistory(ContentResolver cr, String url, boolean real)

(6)CallSuper Annotations(复写方法注解):

如果你的API允许使用者重写你的方法,但你又需要你自己的方法(父方法)在重写的时候也被调用,这时候你可以使用@CallSuper标注:

@CallSuper

protected void onCreate(Bundle savedInstanceState){

}

用了这个后,当重写的方法没有调用父方法时,工具就会给予警告提示。

5.自定义注解

(1)基本格式:元注解

public @interface 注解名{

定义体;

}

(2)相关说明

注解类型是一种特殊的接口,在声明时使用的是“@interface”而不是“interface”。

一个注解类型中可以包含多个元素。每个元素都可以看做注解中的配置项,通过方法声明的形式来定义。这些方法声明中不能有任何形式参数或类型参数,也不能有抛出受检异常的throws声明。方法的名称是元素的名称,方法的返回值类型决定了元素的类型。可以支持的返回值类型如下:

(1)所有基本数据类型(int,float,boolean,byte,double,char,long,short);

(2)String类型;

(3)Class类型;

(4)enum类型;

(5)Annotation类型;

(6)以上所有类型的数组。

注意点:

(1)注解类中的方法只能用public或者默认这两个访问权修饰,不写public就是默认;

(2)如果注解类中只有一个成员,最好把方法名设置为”value”;

(3)注解元素必须有确定的值,要么在定义注解的默认值中指定,要么在使用注解时指定,非基本类型的注解元素的值不可为null。因此,使用空字符串或0作为默认值是一种常用的做法。;

注解类型中可以没有任何元素,这种注解类型称为标记注解类型,是作标记用的。如果注解类型中只有一个元素,那么这个元素的名称应该根据惯例使用value。使用value的好处是可以简化注解的使用方式,可以为注解类型设定默认值。通过在方法声明的default关键词来指定。

(3)注解的解析

当注解被添加到java源代码中后,并不会自动产生作用。某些注解甚至不会在字节代码中出现。创建和使用注解只是完成了第一步,重要的是如何对注解进行相应的处理。在一般情况下,创建和处理注解是Java标准库和第三方库应该做的事情,开发人员只需要使用注解就可以。

根据生存周期不同,可以分为两种解析方式:运行时解析和编译时解析。后续会对这两种解析技术进行详细的分析说明,并分别基于运行时解析和编译时解析简单的模拟实现ButterKnife的效果。

参考文献

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值