注解的使用
元数据
什么是元数据
- 要想理解注解(Annotation)的作用,就要先理解Java中元数据的概念。
- 元数据是描述数据的数据。
- 在编程语言上下文中,元数据是添加到程序元素如方法、字段、类和包上的额外信息。对数据进行说明描述的数据。
元数据的作用
- 一般来说,元数据可以用于创建文档(根据程序元素上的注释创建文档),跟踪代码中的依赖性(可声明方法是重载,依赖父类的方法),执行编译时检查(可声明是否编译期检测),代码分析。
- 如下:
- 1)编写文档:通过代码里标识的元数据生成文档
- 2)代码分析:通过代码里标识的元数据对代码进行分析
- 3)编译检查:通过代码里标识的元数据让编译器能实现基本的编译检查
Java平台元数据
- 注解Annotation就是java平台的元数据,是J2SE5.0新增加的功能,该机制允许在Java代码中添加自定义注释,并允许通过反射(reflection),以编程方式访问元数据注释。
- 通过提供为程序元素(类、方法等)附加额外数据的标准方法,元数据功能具有简化和改进许多应用程序开发领域的潜在能力,其中包括配置管理、框架实现和代码生成。
注解
注解(Annotation)的概念
- 注解(Annotation)在JDK1.5之后增加的一个新特性,注解的引入意义很大,有很多非常有名的框架,比如Hibernate、Spring等框架中都大量使用注解。注解作为程序的元数据嵌入到程序。注解可以被解析工具或编译工具解析。
- 关于注解(Annotation)的作用,其实就是上述元数据的作用。
- 注意:Annotation能被用来为程序元素(类、方法、成员变量等)设置元数据。Annotaion不影响程序代码的执行,无论增加、删除Annotation,代码都始终如一地执行。如果希望让程序中的Annotation起一定的作用,只有通过解析工具或编译工具对Annotation中的信息进行解析和处理。
注解的属性分类
- 首先注解分为三类:
- 标准 Annotation
- 包括 Override, Deprecated, SuppressWarnings,是java自带的几个注解,他们由编译器来识别,不会进行编译, 不影响代码运行,至于他们的含义不是这篇博客的重点,这里不再讲述。
- 元 Annotation
- @Retention, @Target, @Inherited, @Documented,它们是用来定义 Annotation 的 Annotation。也就是当我们要自定义注解时,需要使用它们。
- 自定义 Annotation
- 根据需要,自定义的Annotation。而自定义的方式,下面我们会讲到。
- 标准 Annotation
注解运行时段分类
- 按照运行的时段,注解可以分为两大类
- 编译器注解
- 运行期注解
自定义注解分类
- 同样,自定义的注解也分为三类,通过元Annotation - @Retention 定义:
- @Retention(RetentionPolicy.SOURCE)
- 源码时注解,一般用来作为编译器标记。如Override, Deprecated, SuppressWarnings。
- 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;源码注解(RetentionPolicy.SOURCE)的生命周期只存在Java源文件这一阶段,是3种生命周期中最短的注解。当在Java源程序上加了一个注解,这个Java源程序要由javac去编译,javac把java源文件编译成.class文件,在编译成class时会把Java源程序上的源码注解给去掉。需要注意的是,在编译器处理期间源码注解还存在,即注解处理器Processor 也能处理源码注解,编译器处理完之后就没有该注解信息了。
- @Retention(RetentionPolicy.RUNTIME)
- 运行时注解,在运行时通过反射去识别的注解。
- 定义运行时注解,只需要在声明注解时指定@Retention(RetentionPolicy.RUNTIME)即可。
- 运行时注解一般和反射机制配合使用,相比编译时注解性能比较低,但灵活性好,实现起来比较简答。
- @Retention(RetentionPolicy.CLASS)
- 编译时注解,在编译时被识别并处理的注解,这是本章重点。
- 编译时注解能够自动处理Java源文件并生成更多的源码、配置文件、脚本或其他可能想要生成的东西。
- @Retention(RetentionPolicy.SOURCE)
实际注解案例
- 实际注解案例
- 运行时注解:retrofit
- 编译时注解:Dagger2, ButterKnife, EventBus3
Java注解解析
注解的成员变量
注解的属性也叫做成员变量。注解只有成员变量,没有方法。
注解的成员变量在注解的定义中以“无形参的方法”形式来声明
- 其方法名定义了该成员变量的名字
- 其返回值定义了该成员变量的类型。
- 在注解中定义属性时它的类型必须是 8 种基本数据类型外加 类、接口、注解及它们的数组。
1
2
3
4
5
6
7
8
9
public TestAnnotation {
int id();
String msg();
}上面代码定义了 TestAnnotation 这个注解中拥有 id 和 msg 两个属性。在使用的时候,我们应该给它们进行赋值。
赋值的方式是在注解的括号内以
value=””
形式,多个属性之前用 ,隔开。1
2
3
4
public class Test {
}注解中属性可以有默认值,默认值需要用 default 关键值指定。比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
public TestAnnotation {
// id 属性默认值为 -1
public int id() default -1;
// msg 属性默认值为 Hi。
public String msg() default "Hi";
}
//因为有默认值,所以无需要再在 @TestAnnotation 后面的括号里面进行赋值
public class Test {}如果一个注解内仅仅只有一个名字为 value 的属性时,应用这个注解时可以直接接属性值填写到括号内。
1
2
3
4
5
6
7
8
9
10
11
12public Check {
// Check 这个注解只有 value 这个属性
String value();
}
// 所以可以这样应用。
int a;
// 这和下面的效果是一样的
int a;注解没有任何属性。比如
1
2
3
4
5public Perform {}
// 应用这个注解的时候,括号都可以省略。
public void testMethod(){}
案例
创建自定义注解,与创建接口有几分相似,但注解需要以@开头。
1
2
3
4
5
6
7
8
9
public MyAnnotataion{
String name();
String website() default "hello";
int revision() default 1;
}自定义注解中定义成员变量的规则:
- 其定义是以无形参的方法形式来声明的。
- 注解方法不带参数,比如name(),website();
- 注解方法返回值类型:基本类型、String、Enums、Annotation以及前面这些类型的数组类型
- 注解方法可有默认值,比如default “hello”,默认website=”hello”
当然注解中也可以不存在成员变量,在使用解析注解进行操作时,仅以是否包含该注解来进行操作。当注解中有成员变量时,若没有默认值,需要在使用注解时,指定成员变量的值。
1
2
3
4
5
6
7
8
9
10
11
12public class AnnotationDemo {
public static void main(String[] args) {
System.out.println("I am main method");
}
public void demo(){
System.out.println("I am demo method");
}
}由于该注解的保留策略为
RetentionPolicy.RUNTIME
,故可在运行期通过反射机制来使用,否则无法通过反射机制来获取。这时候注解实现的就是元数据的第二个作用:代码分析。
注解常用方法
接下来,通过反射技术来解析自定义注解。
- 关于反射类位于包java.lang.reflect,其中有一个接口AnnotatedElement,该接口主要有如下几个实现类:Class,Constructor,Field,Method,Package。除此之外,该接口定义了注释相关的几个核心方法,如下:
因此,当获取了某个类的Class对象,然后获取其Field,Method等对象,通过上述4个方法提取其中的注解,然后获得注解的详细信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class AnnotationParser {
public static void main(String[] args) throws SecurityException, ClassNotFoundException {
String clazz = "com.lvr.annotation.AnnotationDemo";
Method[] demoMethod = AnnotationParser.class
.getClassLoader().loadClass(clazz).getMethods();
for (Method method : demoMethod) {
if (method.isAnnotationPresent(MyAnnotataion.class)) {
MyAnnotataion annotationInfo = method.getAnnotation(MyAnnotataion.class);
System.out.println("method: "+ method);
System.out.println("name= "+ annotationInfo.name() +
" , website= "+ annotationInfo.website()
+ " , revision= "+annotationInfo.revision());
}
}
}
}- 以上仅是一个示例,其实可以根据拿到的注解信息做更多有意义的事。
注解的使用
内建注解使用
Java提供了多种内建的注解,下面接下几个比较常用的注解:@Override、@Deprecated、@SuppressWarnings以及@FunctionalInterface这4个注解。
- 内建注解主要实现了元数据的第二个作用:编译检查。
@Override
- 用途:用于告知编译器,我们需要覆写超类的当前方法。如果某个方法带有该注解但并没有覆写超类相应的方法,则编译器会生成一条错误信息。如果父类没有这个要覆写的方法,则编译器也会生成一条错误信息。
- @Override可适用元素为方法,仅仅保留在java源文件中。
@Deprecated
用途:使用这个注解,用于告知编译器,某一程序元素(比如方法,成员变量)不建议使用了(即过时了)。
例如:Person类中的info()方法使用
@Deprecated
表示该方法过时了。1
2
3
4
5
6public class Person {
public void info(){
}
}调用info()方法会编译器会出现警告,告知该方法已过时。
@SuppressWarnings
- 用途:用于告知编译器忽略特定的警告信息,例在泛型中使用原生数据类型,编译器会发出警告,当使用该注解后,则不会发出警告。
- 注解类型分析:
@SuppressWarnings
可适合用于除注解类型声明和包名之外的所有元素,仅仅保留在java源文件中。 - 该注解有方法value(),可支持多个字符串参数,用户指定忽略哪种警告,例如:
1
@FunctionalInterface
- 用途:用户告知编译器,检查这个接口,保证该接口是函数式接口,即只能包含一个抽象方法,否则就会编译出错。
- 注解类型分析:
@FunctionalInterface
可适合用于注解类型声明,保留时长为运行时。
元Annotation使用
JDK除了在java.lang提供了上述内建注解外,还在java.lang。annotation包下提供了6个Meta Annotation(元Annotataion),其中有5个元Annotation都用于修饰其他的Annotation定义。其中@Repeatable专门用户定义Java 8 新增的可重复注解。
先介绍其中4个常用的修饰其他Annotation的元Annotation。在此之前,我们先了解如何自定义Annotation。
当一个接口直接继承java.lang.annotation.Annotation接口时,仍是接口,而并非注解。要想自定义注解类型,只能通过@interface关键字的方式,其实通过该方式会隐含地继承.Annotation接口。
@Documented
@Documented
用户指定被该元Annotation修饰的Annotation类将会被javadoc工具提取成文档,如果定义Annotation类时使用了@Documented
修饰,则所有使用该Annotation修饰的程序元素的API文档中将会包含该Annotation说明。例如:
1
2
3
4
5
public Deprecated {
}- 定义
@Deprecated
时使用了@Documented
,则任何元素使用@Deprecated修饰时,在生成API文档时,将会包含@Deprecated
的说明 - 以下是String的一个过时的构造方法:该注解实现了元数据的第一个功能:编写文档。
1
2
public String(byte[] ascii,int hibyte,int offset, int count)@Inherited
@Inherited
指定被它修饰的Annotation将具有继承性——如果某个类使用了@Xxx注解(定义该Annotation时使用了@Inherited
修饰)修饰,则其子类将自动被@Xxx修饰。1
2
3
4
5
6
7
8
9//当你的注解定义到类A上,此时,有个B类继承A,且没使用该注解。
//但是扫描的时候,会把A类设置的注解,扫描到B类上。
public Test {
//...
}
@Retention
@Retention
:表示该注解类型的注解保留的时长。当注解类型声明中没有@Retention
元注解,则默认保留策略为RetentionPolicy.CLASS。关于保留策略(RetentionPolicy)是枚举类型,共定义3种保留策略,如下表:策略 保留在.class文件 描述 场景 RetentionPolicy.SOURCE 否 一般用于在编译时,通过注解对即将生成的.class文件进行干预,以生成自身期望生成的.class文件的样子 Lombok的@Data注解,会生成成员变量的get/set方法。即.class中会包含get/set方法(源代码.java文件中不需要编写get/set方法) RetentionPolicy.CLASS 是 .class文件是用于生成对象的基础,当存在注解时,通过相应的逻辑干预,生成与原生.class文件不同的对象。 编织代理对象。 RetentionPolicy.RUNTIME 在运行环境VM中保留该注解,可以通过反射读取这些信息。 在运行时保留注解可以在执行时,通过判断方法等是否含有指定的注解,从而在运行时干预方法的执行逻辑,执行期望的过程。如动态代理。
@Target
@Target
:表示该注解类型的所适用的程序元素类型。当注解类型声明中没有@Target
元注解,则默认为可适用所有的程序元素。如果存在指定的@Target
元注解,则编译器强制实施相应的使用限制。关于程序元素(ElementType)是枚举类型,共定义8种程序元素,如下表:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
接口、类、枚举、注解
字段、枚举的常量
方法
方法参数
构造函数
局部变量
注解
包
// 查看该类可知是一个枚举
public enum ElementType {
TYPE,
FIELD,
METHOD,
PARAMETER,
CONSTRUCTOR,
LOCAL_VARIABLE,
ANNOTATION_TYPE,
PACKAGE,
TYPE_PARAMETER,
TYPE_USE;
private ElementType() {
}
}
注解的默认值
注解可以设置默认值,有默认值的参数可以不写。
1
2
3
4
5
6
7
8
9
10
11
public TestAni {
int id(); //注解参数
String name() default "default";
}
//使用
//name有默认值可以不写
class Test{
}
注解的继承
这里讲的继承并不是通过@Inherited修饰的注解。这个“继承”是一个注解的使用技巧,使用上的感觉类似于依赖倒置,来自于ButterKnife源码。
- 这是ButterKnife的OnClick 注解。特殊的地方在于**@OnClick修饰了注解@ListenerClass**,并且设置了一些只属于@OnClick的属性。
- 那这样的作用是什么呢?凡是修饰了@OnClick的地方,也就自动修饰了@ListenerClass。类似于@OnClick是@ListenerClass的子类。而ButterKnife有很多的监听注解@OnItemClick、@OnLongClick等等。这样在做代码生成时,不需要再单独考虑每一个监听注解,只需要处理@ListenerClass就OK。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public OnClick {
/** View IDs to which the method will be bound. */
int[] value() default { View.NO_ID };
}
自定义注解
看自定义注解部分内容代码,思考下面问题……
1
2
3
4
5
6
7
8
9
10//@Retention用来修饰这是一个什么类型的注解。这里表示该注解是一个编译时注解。
//@Target用来表示这个注解可以使用在哪些地方。
// 比如:类、方法、属性、接口等等。这里ElementType.METHOD 表示这个注解可以用来修饰:方法
//这里的interface并不是说OnceClick是一个接口。就像申明类用关键字class。申明注解用的就是@interface。
public OnceClick {
//返回值表示这个注解里可以存放什么类型值
int value();
}Annotation里面的方法为何不能是private?
只能用public或默认(default)这两个访问权修饰.例如,String value();不能是private;因为它是提供给外部使用的。
参数只能使用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和 String,Enum,Class,annotations等数据类型,以及这一些类型的数组.例如,String value();这里的参数类型就为String;
编译器注解应用
使用注解替代枚举
代码如下所示
- 具体的案例,可以看我视频播放器开源库:https://github.com/yangchong211/YCVideoPlayer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/**
* 播放模式
* -1 播放错误
* 0 播放未开始
* 1 播放准备中
* 2 播放准备就绪
* 3 正在播放
* 4 暂停播放
* 5 正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,缓冲区数据足够后恢复播放)
* 6 正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,此时暂停播放器,继续缓冲,缓冲区数据足够后恢复暂停
* 7 播放完成
*/
public @interface CurrentState{
int STATE_ERROR = -1;
int STATE_IDLE = 0;
int STATE_PREPARING = 1;
int STATE_PREPARED = 2;
int STATE_PLAYING = 3;
int STATE_PAUSED = 4;
int STATE_BUFFERING_PLAYING = 5;
int STATE_BUFFERING_PAUSED = 6;
int STATE_COMPLETED = 7;
}
运行期注解应用
创建一个注解
如下所示
1
2
3
4
5
public ContentView {
int value();
}关于代码解释
- 第一行:@Retention(RetentionPolicy.RUNTIME)
- @Retention用来修饰这是一个什么类型的注解。这里表示该注解是一个运行时注解。这样APT就知道啥时候处理这个注解了。
- 第二行:@Target({ElementType.TYPE})
- @Target用来表示这个注解可以使用在哪些地方。比如:类、方法、属性、接口等等。这里ElementType.TYPE 表示这个注解可以用来修饰:Class, interface or enum declaration。当你用ContentView修饰一个方法时,编译器会提示错误。
- 第三行:public @interface ContentView
- 这里的interface并不是说ContentView是一个接口。就像申明类用关键字class。申明枚举用enum。申明注解用的就是@interface。(值得注意的是:在ElementType的分类中,class、interface、Annotation、enum同属一类为Type,并且从官方注解来看,似乎interface是包含@interface的)
- /*_ Class, interface (including annotation type), or enum declaration _/
- TYPE,
- 第四行:int value();
- 返回值表示这个注解里可以存放什么类型值。比如我们是这样使用的
- @ContentView(R.layout.activity_home)
- R.layout.activity_home实质是一个int型id,如果这样用就会报错:
- @ContentView(“string”)
- 第一行:@Retention(RetentionPolicy.RUNTIME)
BaseActivity注解解析
注解的解析就在BaseActivity中。来看一下BaseActivity代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class BaseActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//注解解析
//遍历所有的子类
for (Class c = this.getClass(); c != Context.class; c = c.getSuperclass()) {
assert c != null;
//找到修饰了注解ContentView的类
ContentView annotation = (ContentView) c.getAnnotation(ContentView.class);
if (annotation != null) {
try {
//获取ContentView的属性值
int value = annotation.value();
//调用setContentView方法设置view
this.setContentView(value);
} catch (RuntimeException e) {
e.printStackTrace();
}
return;
}
}
}
}
2.3 实际运用案例
注解申明好了,但具体是怎么识别这个注解并使用的呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestActivity extends BaseActivity {
//@ContentView(R.layout.activity_test_video) 这种使用是错误的
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView tv_video = findViewById(R.id.tv_video);
tv_video.setOnClickListener(v -> startActivity(
new Intent(TestActivity.this,VideoActivity.class)));
}
}总结一下
- 这是一个很简单的案例。现在对运行时注解的使用一定有了一些理解了。也知道了运行时注解被人呕病的地方在哪。你可能会觉得*setContentView(R.layout.activity_home)和@ContentView(R.layout.activity_home)*没什么区别,用了注解反而还增加了性能问题。
注解代替枚举
- 在做内存优化时,推荐使用注解代替枚举,因为枚举占用的内存更高,如何说明枚举占用内存高呢?这是为什么呢?
案例
枚举案例代码
1
2
3
4
5public enum Numbers {
One,
Two,
Three
}使用枚举的场景
- 分组常量场景:归属于同一分组的常量,比如性别只有男和女,一周只有7天之类的
枚举案例说明
javac编译Numbers后生成字节码,想看看到底Numbers.class到底有什么。因为字节码比较晦涩难懂,还是想办法用jad反编译成Java吧,链接: http://www.javadecompilers.com/jad
反编译成Java
打开反编译生成的Numbers.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public final class Numbers extends Enum {
public static Numbers[] values() {
return (Numbers[])$VALUES.clone();
}
public static Numbers valueOf(String name) {
return (Numbers)Enum.valueOf(com/ycbjie/test/Numbers, name);
}
private Numbers(String s, int i) {
super(s, i);
}
public static final Numbers One;
public static final Numbers Two;
public static final Numbers Three;
private static final Numbers $VALUES[];
static {
One = new Numbers("One", 0);
Two = new Numbers("Two", 1);
Three = new Numbers("Three", 2);
$VALUES = (new Numbers[] {
One, Two, Three
});
}
}
使用枚举得出结论
结论
- 从上面得到如下结论
- 枚举类是继承于java.lang.Enum的类。
- 枚举的构造函数是私有的, 防止new出对象。
- 枚举值是类对象, 且是静态常量(被static final修饰)。
- 静态代码块内实例化枚举值,由于静态代码块的语法特性,该代码块只执行一次;
- 默认值0、1、2是在编译时生成的。
- 枚举类比常量更占内存, 因为一个Java对象至少占16个字节, 而Numbers包含了3个Java对象;而使用3个整型替换的话,只占用4 * 3即12个字节。
枚举损耗性能
先说结论
- 每一个枚举值都是一个对象,在使用它时会增加额外的内存消耗,所以枚举相比与 Integer 和 String 会占用更多的内存。
- 较多的使用 Enum 会增加 DEX 文件的大小,会造成运行时更多的开销,使我们的应用需要更多的空间。
- 如果你的应用使用很多的 Enum ,最好使用Integer 或 String 替代他们,但是这样还会有问题。
如何佐证,第一种比较文件大小
第一步,写一个Numbers3类,反编译,然后看一下class文件的大小。大小是443个字节。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//代码
public class Numbers3 {
private void number(int number){
switch (number){
case 1:
break;
case 2:
break;
case 3:
break;
}
}
}第二步,写一个Numbers2类,使用枚举,反编译,然后看一下class文件的大小。大小是743个字节。可以得知,添加枚举后,大小增加了300个字节。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class Numbers2 {
public enum NumbersInt {
One,
Two,
Three
}
private void number(NumbersInt number){
switch (number){
case One:
break;
case Two:
break;
case Three:
break;
}
}
}第三步,写一个Numbers4类,使用静态常量,反编译,然后看一下class文件的大小。大小是542个字节。可以得知,添加枚举后,大小增加了99个字节。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class Numbers4 {
public static final int ONE = 1;
public static final int TWO = 2;
public static final int THREE = 3;
private void number(int number){
switch (number){
case ONE:
break;
case TWO:
break;
case THREE:
break;
}
}
}由此可知,使用枚举的大小增长量是使用static int的3倍。
如何佐证,第二种,比较占用内存的大小[摘自网络]
有这样一份代码,编译之后的dex大小是2556bytes,在此基础之上,添加一些如下代码,这些代码使用普通static常量相关作为判断值:
- 增加下面那段代码之后,编译成dex的大小是2680 bytes,相比起之前的2556 bytes只增加124 bytes。
假如换做使用enum,情况如下:
使用enum之后的dex大小是4188 bytes,相比起2556增加了1632 bytes,增长量是使用static int的13倍。
反编译枚举
对Color类进行反编译,进入项目的目录,打开dos命令行,开始执行
javap -c NumbersInt.class
- NumbersInt类代码
1
2
3
4
5public enum NumbersInt {
One,
Two,
Three
}最终结果如下所示
- 本来是可以使用几个静态常量代替的NumbersInt类做了这么多额外的操作,分配了这么多内存,这也是Enum在Android不被建议使用的原因。
代码如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59public final class com.ycbjie.ycapt.NumbersInt extends java.lang.Enum<com.ycbjie.ycapt.NumbersInt> {
public static final com.ycbjie.ycapt.NumbersInt One;
public static final com.ycbjie.ycapt.NumbersInt Two;
public static final com.ycbjie.ycapt.NumbersInt Three;
public static com.ycbjie.ycapt.NumbersInt[] values();
Code:
0: getstatic #1 // Field $VALUES:[Lcom/ycbjie/ycapt/NumbersInt;
3: invokevirtual #2 // Method "[Lcom/ycbjie/ycapt/NumbersInt;".clone:()Ljava/lang/Object;
6: checkcast #3 // class "[Lcom/ycbjie/ycapt/NumbersInt;"
9: areturn
public static com.ycbjie.ycapt.NumbersInt valueOf(java.lang.String);
Code:
0: ldc #4 // class com/ycbjie/ycapt/NumbersInt
2: aload_0
3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #4 // class com/ycbjie/ycapt/NumbersInt
9: areturn
static {};
Code:
0: new #4 // class com/ycbjie/ycapt/NumbersInt
3: dup
4: ldc #7 // String One
6: iconst_0
7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #9 // Field One:Lcom/ycbjie/ycapt/NumbersInt;
13: new #4 // class com/ycbjie/ycapt/NumbersInt
16: dup
17: ldc #10 // String Two
19: iconst_1
20: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
23: putstatic #11 // Field Two:Lcom/ycbjie/ycapt/NumbersInt;
26: new #4 // class com/ycbjie/ycapt/NumbersInt
29: dup
30: ldc #12 // String Three
32: iconst_2
33: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
36: putstatic #13 // Field Three:Lcom/ycbjie/ycapt/NumbersInt;
39: iconst_3
40: anewarray #4 // class com/ycbjie/ycapt/NumbersInt
43: dup
44: iconst_0
45: getstatic #9 // Field One:Lcom/ycbjie/ycapt/NumbersInt;
48: aastore
49: dup
50: iconst_1
51: getstatic #11 // Field Two:Lcom/ycbjie/ycapt/NumbersInt;
54: aastore
55: dup
56: iconst_2
57: getstatic #13 // Field Three:Lcom/ycbjie/ycapt/NumbersInt;
60: aastore
61: putstatic #1 // Field $VALUES:[Lcom/ycbjie/ycapt/NumbersInt;
64: return
}
注解与反射
- 需要注意的是,如果一个注解要在运行时被成功提取,那么 @Retention(RetentionPolicy.RUNTIME) 是必须的。
获取类上的注解
注解通过反射获取。首先可以通过 Class 对象的
isAnnotationPresent()
方法判断它是否应用了某个注解1
2public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}
然后通过
getAnnotation()
方法来获取 Annotation 对象。返回指定类型的注解1
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {}
或者是
getAnnotations()
方法。返回注解到这个元素上的所有注解1
public Annotation[] getAnnotations() {}1212
如果获取到的 Annotation 如果不为 null,则就可以调用它们的属性方法了。比如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
public static void main(String[] args) {
boolean hasAnnotation = Test.class.isAnnotationPresent(TestAnnotation.class);
if ( hasAnnotation ) {
TestAnnotation testAnnotation = Test.class.getAnnotation(TestAnnotation.class);
System.out.println("id:"+testAnnotation.id());
System.out.println("msg:"+testAnnotation.msg());
}
}
}
//
程序的运行结果正是 TestAnnotation 中 id 和 msg 的默认值。 :
id:-1
msg:
获取属性、方法上的注解
代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class Test {
int a;
public void testMethod(){}
public void test1(){
Hero hero = new Hero();
hero.say();
hero.speak();
}
public static void main(String[] args) {
boolean hasAnnotation = Test.class.isAnnotationPresent(TestAnnotation.class);
if ( hasAnnotation ) {
TestAnnotation testAnnotation = Test.class.getAnnotation(TestAnnotation.class);
//获取类的注解
System.out.println("id:"+testAnnotation.id());
System.out.println("msg:"+testAnnotation.msg());
}
try {
Field a = Test.class.getDeclaredField("a");
a.setAccessible(true);
//获取一个成员变量上的注解
Check check = a.getAnnotation(Check.class);
if ( check != null ) {
System.out.println("check value:"+check.value());
}
Method testMethod = Test.class.getDeclaredMethod("testMethod");
if ( testMethod != null ) {
// 获取方法中的注解
Annotation[] ans = testMethod.getAnnotations();
for( int i = 0;i < ans.length;i++) {
System.out.println("method testMethod annotation:"+ans[i].annotationType().getSimpleName());
}
}
} catch (NoSuchFieldException e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.out.println(e.getMessage());
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.out.println(e.getMessage());
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.out.println(e.getMessage());
}
}
}它们的结果如下:
1
2
3
4id:-1
msg:hello
check value:hi
method testMethod annotation:Perform
自定义注解示例
我要写一个测试框架,测试程序员的代码有无明显的异常。
程序员 A : 我写了一个类,它的名字叫做 NoBug,因为它所有的方法都没有错误。
我:自信是好事,不过为了防止意外,让我测试一下如何?
程序员 A: 怎么测试?
我:把你写的代码的方法都加上 @Jiecha 这个注解就好了。
程序员 A: 好的。代码中,有些方法上面运用了 @Jiecha 注解。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30package ceshi;
import ceshi.Jiecha;
public class NoBug {
public void suanShu(){
System.out.println("1234567890");
}
public void jiafa(){
System.out.println("1+1="+1+1);
}
public void jiefa(){
System.out.println("1-1="+(1-1));
}
public void chengfa(){
System.out.println("3 x 5="+ 3*5);
}
public void chufa(){
System.out.println("6 / 0="+ 6 / 0);
}
public void ziwojieshao(){
System.out.println("我写的程序没有 bug!");
}
}定义注解
1
2
3
4
5
6
7
8
9package ceshi;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public Jiecha {
}然后,再编写一个测试类 TestTool 就可以测试 NoBug 相应的方法了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56package ceshi;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class TestTool {
public static void main(String[] args) {
// TODO Auto-generated method stub
NoBug testobj = new NoBug();
Class clazz = testobj.getClass();
Method[] method = clazz.getDeclaredMethods();
//用来记录测试产生的 log 信息
StringBuilder log = new StringBuilder();
// 记录异常的次数
int errornum = 0;
for ( Method m: method ) {
// 只有被 @Jiecha 标注过的方法才进行测试
if ( m.isAnnotationPresent( Jiecha.class )) {
try {
m.setAccessible(true);
m.invoke(testobj, null);
} catch (Exception e) {
// TODO Auto-generated catch block
//e.printStackTrace();
errornum++;
log.append(m.getName());
log.append(" ");
log.append("has error:");
log.append("\n\r caused by ");
//记录测试过程中,发生的异常的名称
log.append(e.getCause().getClass().getSimpleName());
log.append("\n\r");
//记录测试过程中,发生的异常的具体信息
log.append(e.getCause().getMessage());
log.append("\n\r");
}
}
}
log.append(clazz.getSimpleName());
log.append(" has ");
log.append(errornum);
log.append(" error.");
// 生成测试报告
System.out.println(log.toString());
}
}测试的结果是:
1
2
3
4
5
6
7
8
9
10
111234567890
1+1=11
1-1=0
3 x 5=15
chufa has error:
caused by ArithmeticException
/ by zero
NoBug has 1 error.这样,通过注解我完成了我自己的目的,那就是对别人的代码进行测试。 所以,再问我注解什么时候用?我只能告诉你,这取决于你想利用它干什么用。
注解应用实例
- 注解运用的地方太多了,因为我是 Android 开发者,所以我接触到的具体例子有下:
JUnit
JUnit 这个是一个测试框架,典型使用方法如下:
- @Test 标记了要进行测试的方法 addition_isCorrect().
1
2
3
4
5
6public class ExampleUnitTest {
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
ButterKnife
ButterKnife 是 Android 开发中大名鼎鼎的 IOC 框架,它减少了大量重复的代码。
1
2
3
4
5
6
7
8
9
10
11
12public class MainActivity extends AppCompatActivity {
TextView mTv;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
}
Retrofit
很牛逼的 Http 网络访问框架
1
2
3
4
5
6
7
8
9
10public interface GitHubService {
Call<List<Repo>> listRepos(; String user)
}
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
GitHubService service = retrofit.create(GitHubService.class);
总结
- 如果注解难于理解,你就把它类同于标签,标签为了解释事物,注解为了解释代码。
- 注解的基本语法,创建如同接口,但是多了个 @ 符号。
- 注解的元注解。
- 注解的属性。
- 注解主要给编译器及工具类型的软件用的。
- 注解的提取需要借助于 Java 的反射技术,反射比较慢,所以注解使用时也需要谨慎计较时间成本。