Java 注解(Annotation)

850 阅读4分钟

注解是什么

Java 注解又称 Java 标注,是 Java 语言 5.0 版本开始支持加入源代码的特殊语法元数据。
Java 语言中的类、方法、变量、参数和包等都可以被标注。Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容。 当然它也支持自定义 Java 标注。

通俗的说,注解就是打标记,例如,给一个方法打个标记,方便后续对这个方法的操作。

定义注解

使用@interface 关键字定义一个注解

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface TestAnnotation {

}

注解的应用范围

注解的使用范围通过@Target 指定,在未指定 Target 时,默认此注解可以作用在以下列出的任何地方
Target 的取值有以下几种

ElementType 含义
TYPE 类、接口(包括注解类型)或枚举声明
FIELD 字段声明(包括枚举常量声明)
PARAMETER 参数声明
ANNOTATION_TYPE 注解类型声明
CONSTRUCTOR 构造方法声明
LOCAL_VARIABLE 局部变量声明
PACKAGE 包声明
TYPE_PARAMETER 类型参数声明 Java8 新增
TYPE_USE 类型使用声明 Java8 新增

元注解

当注解的 Target 指定了 ANNOTATION_TYPE 后,此注解便可以应用于注解上,这种能应用在注解上的注解被称为元注解。例如,@Target 可以使用在注解上,所以@Target 是一个元注解。

保留级别

注解的保留级别通过@Retention 指定,注解分为三个保留级别:

  • SOURCE:源码级别,只在源码保留,编译时会被抹除
  • CLCASS:字节码级别,运行时无法获取
  • RUNTIME:运行时级别,可以在运行时通过反射获取(通过反射获取注意性能消耗)

注解的属性

注解中可以存在成员变量,但没有方法。注解中成员变量的定义采用”无形参的方法“的格式来声明,其方法名便是成员变量的名字,其返回值类型就是该成员变量的类型,如下便定义个一个 int 类型的成员变量 value,并在 Test 类上使用该注解,传入 1 表示为 value 赋值为 1

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface TestAnnotation {
    int value();
}

@TestAnnotation(1)
class Test{}

当成员变量名为 value 时,可以直接在括号中赋值,但如果成员变量名不为 value 或同时有多个成员变量,则必须使用以下方式赋值

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface TestAnnotation {
    String name();
    int age();
}

@TestAnnotation(name="张三",age=18)
class Test{}

注解+反射实现自动调用 findViewById()(类似 ButterKinfe 效果)

定义注解,指定可以应用在字段上,并指定保留级别为 RUNTIME(需要在运行时通过反射获取注解)

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
    int value();
}

通过反射获取被 Inject 标记的变量,并根据注解的 value 进行 findViewById(),再将得到的 view 赋值给 activity 的字段

public class InjectViewUtils {
    public static void inject(Activity activity) {
        //获取此activity中所有成员变量(包含被private修饰的变量,不包含在父类中定义的变量)
        Field[] declaredFields = activity.getClass().getDeclaredFields();
        for (Field declaredField : declaredFields) {
            //判断此成员变量是否被@Inject注解标记
            if (declaredField.isAnnotationPresent(Inject.class)) {
                //此成员变量被@Inject标记,获取这个Inject对象
                Inject annotation = declaredField.getAnnotation(Inject.class);
                //获取Inject中的属性value,此时的value为控件的id
                int value = annotation.value();
                //根据View的id,获取View对象
                View viewById = activity.findViewById(value);
                //设置此变量的权限,如果此变量被private修饰,不设置此权限会报错
                declaredField.setAccessible(true);
                try {
                    //将获取到的view赋值给activity的变量
                    declaredField.set(activity,viewById);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Actvity 中使用 Inject 注解标记需要 findViewByid 的字段,并在 onCreate 中调用 InjectViewUtils.inject(this);

public class MainActivity extends AppCompatActivity {
    @Inject(R.id.tv_test)
    TextView mTvTest;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        InjectViewUtils.inject(this);

        mTvTest.setText("成功为TextView初始化");
    }
}
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/transparent">

    <TextView
        android:id="@+id/tv_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:textColor="@android:color/black"
        android:textSize="30sp" />
</RelativeLayout>

说明

以上示例实现依赖运行时通过反射获取被注解标记的字段,Butterkinfe 早期版本是这样实现的,但考虑到反射对性能的损耗,Butterknife 的实现已经改用了 APT 技术,不再使用 RUNTIME 注解。