Java进阶 - 注解(注解、注解与反射的应用、注解处理器、@Target使用场景)

1,466 阅读17分钟

注解基础知识

1.注解定义

  • 官方:Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。Java 注解是从 Java5 开始添加到 Java 的。

  • 自己的理解:注解=标签,用来描述一个东西的一些信息,比如超市里的商品标签,标签里面就声明了一些物品的信息,价格、产地、日期等等,而且这些标签如果单独存在的话是没有任何意义的,就是一张纸,Java的注解也是一样的,单独声明一个注解是没有任何意义的,只有他标注在类上和结合反射或者插庄等技术配合使用的话,他的意义才得以体现。

2.注解的声明方式及语法

  • 注解和 classinterface 一样,也是属于一种类型,声明类通过class关键字,而声明一个注解则使用@interface 关键字,这里注意它和 interface 关键字是有区别的。
//创建一个注解
public @interface BiaoQian {
}
  • 注解的使用:可以简单的理解我们吧BiaoQian这个注解张贴到了TuDou这个类上了
@BiaoQian
public class TuDou {
}

3.元注解

  • 元注解就是可以注解到注解上的注解,还是用超市的标签举例,元注解就是可以贴到标签上的标签,比如说有个蔬菜标签上有重量、味道和生产日期,那如果工作人员不清楚的话把这个标签贴到生活用品上就麻烦了,但是如果这个标签上还贴着一个标签,写着:只能贴蔬菜类,那么这样的话就限制了工作人员随意乱贴的问题,同样元注解其实就是用来解释和限制一个注解的。
  • Java内置的元注解有5个
  • @Retention
  • @Documented
  • @Target
  • @Inherited
  • @Repeatable

@Retention

  • Retention 的英文意为保留期的意思。当 @Retention 应用到一个注解上的时候,它解释说明了这个注解的的存活时间。
Type 保留级别
RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。(编译成class文件不存在)
RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。 (编译成class文件存在,但JVM会忽略)
RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。
  • 我们可以这样的方式来加深理解,@Retention 去给一张标签解释的时候,它指定了这张标签张贴的时间。@Retention 相当于给一张标签上面盖了一张时间戳,时间戳指明了标签张贴的时间周期。
@Retention(RetentionPolicy.SOURCE)
public @interface BiaoQian {
}

根据注解的保留级别不同,对注解自然存在不同的使用场景

级别 技术 说明
源码 APT、语法检查 在编译期能够获取注解与注解声明的类包括类中的所有成员的信息,一般用于生成额外的辅助类,语法检查的话主要是通过注解限制一些代码编写
字节码 字节码增强 在编译出class后,通过修改Class数据以实现修改代码逻辑目的。对于是否需要修改的区分或者修改为不同逻辑的判断可以使用注解。
运行时 反射 在程序运行期间,通过反射技术动态的获取注解及其元素,从而完成不同的逻辑判定。

@Documented

  • 顾名思义,这个元注解肯定是和文档有关。它的作用是能够将注解中的元素包含到 Javadoc 中去。

@Target

  • Target 是目标的意思,@Target 指定了注解运用的地方。
  • 你可以这样理解,当一个注解被 @Target 注解时,这个注解就被限定了运用的场景。
  • 类比到标签,原本标签是你想张贴到哪个地方就到哪个地方,但是因为 @Target 的存在,它张贴的地方就非常具体了,比如只能张贴到方法上、类上、方法参数上等等。@Target 有下面的取值
type 作用场景
ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
ElementType.CONSTRUCTOR 可以给构造方法进行注解
ElementType.FIELD 可以给属性进行注解
ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
ElementType.METHOD 可以给方法进行注解
ElementType.PACKAGE 可以给一个包进行注解
ElementType.PARAMETER 可以给一个方法内的参数进行注解
ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举

@Inherited

  • Inherited 是继承的意思,但是它并不是说注解本身可以继承,而是说如果一个超类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解。 说的比较抽象。代码来解释。
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface BiaoQian {
}

@BiaoQian
public class Vegetables {}

public class TuDou extends Vegetables {}


注解 BiaoQian 被 @Inherited 修饰,之后类 Vegetables  被 BiaoQian 注解,
类 TuDou 继承 Vegetables ,类 TuDou  也拥有 BiaoQian 这个注解。

可以这样理解:

老子非常有钱,所以人们给他贴了一张标签叫做富豪。

老子的儿子长大后,只要没有和老子断绝父子关系,虽然别人没有给他贴标签,但是他自然也是富豪。

老子的孙子长大了,自然也是富豪。

这就是人们口中戏称的富一代,富二代,富三代。虽然叫法不同,好像好多个标签,但其实事情的本质也就是他们有一张共同的标签,也就是老子身上的那张富豪的标签。

@Repeatable

  • Repeatable 允许一个注解可以被使用一次或者多次。@Repeatable 是 Java 1.8 才加进来的,所以算是一个新的特性。
  • Repeatable什么时候应用呢?(重要的事情说3遍)
    • 通常是注解的值可以同时取多个(也就是在一个地方使用多次),而且必须在JDK-1.8。
    • 通常是注解的值可以同时取多个(也就是在一个地方使用多次),而且必须在JDK-1.8。
    • 通常是注解的值可以同时取多个(也就是在一个地方使用多次),而且必须在JDK-1.8。
  • 举个例子,一个人他既是程序员又是产品经理,同时他还是个画家。
@Retention(RetentionPolicy.RUNTIME)                     //声明注解保留时  
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})//声明注解可注解在类和注解上
@Repeatable(Professions.class)                          //通过@Repeatable声明容器注解
public @interface Profession {
    String role() default "";
}

//这是容器注解他可以单独存在
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.ANNOTATION_TYPE})
public @interface Professions {
    Profession[] value();
}

@Profession(role =  "PM")
@Profession(role =  "Coder")
@Profession(role =  "Artist")
public static class Person {
}
  • 我们可能对于 @Profession(role=”PM”) 括号里面的内容感兴趣,它其实就是给 Profession 这个注解的 role 属性赋值为 PM ,大家不明白正常,马上就讲到注解元素这一块。
  • 注意上面的代码,@Repeatable 注解了 Profession。而 @Repeatable 后面括号中的类相当于一个容器注解。
  • 什么是容器注解呢?就是用来存放其它注解的地方。它本身也是一个注解。
  • 我们再看看代码中的相关容器注解。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.ANNOTATION_TYPE})
public @interface Professions {
    Profession[] value();
}
  • 按照规定,它里面必须要有一个 value 的属性,属性类型是一个被 @Repeatable 注解过的注解数组,注意它是数组。
  • 如果不好理解的话,可以这样理解。Professions 是一张总的标签,上面贴满了 Profession 这种同类型但内容不一样的标签。把 Professions 给一个 SuperMan 贴上,相当于同时给他贴了程序员、产品经理、画家的标签。
  • 为什么说容器注解也可以单独存在?因为它也可以这样用:
@Professions({@Profession(role = "PM"),
        @Profession(role = "Coder"),
        @Profession(role = "Artist")})
public static class SuperMan {
}

4.注解元素

注解元素的声明

  • 注解元素也叫做成员变量。注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
    int id();
    String msg();
}

上面代码定义了 TestAnnotation 这个注解中拥有 id 和 msg 两个属性。在使用的时候,我们应该给它们进行赋值。 赋值的方式是在注解的括号内以 value=”” 形式,多个属性之前用 ,隔开。

@TestAnnotation(id=3,msg="hello annotation")
public class Test {

}

注解元素可用的类型:

  • 所有基本类型(int、float、boolean等)
  • String
  • Class
  • enum
  • Annotation
  • 以上类型的数组
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationElementType {
    //枚举类型
    enum Status {FIXED,NORMAL};

    //声明枚举
    Status status() default Status.FIXED;

    //布尔类型
    boolean showSupport() default false;

    //String类型
    String name()default "";

    //class类型
    Class<?> testCase() default Void.class;

    //注解嵌套
    Reference reference() default @Reference(next=true);

    //数组类型
    long[] value();
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Reference{
    boolean next() default false;
}

如果你使用了其他类型,编译器就会报错。注意,也不允许使用任何包装类型,但是由于自动装箱的存在,这不算是什么限制。注解也可以作为元素的类型。

注解元素的使用与限制:

  • 注解元素要么有默认值,要么在使用的时候提供元素的值
  • 只有一个value元素的注解,可以直接使用 @XXXX("xx")
  • 如果是容器注解,那么在给元素提供值时,应该是@XXX({"xx","xx","xx"})

@Target注解保留时浅析

在注解的基础知识中我们明白@Targer元注解用来声明一个注解的保留时也就是声明周期,那么不同的生命周期有哪些不同应用场景呢?我们来一个个看一下

SOURCE级别的使用场景

SOURCE级别的注解,生命周期只在编译期,且不会保留到.class文件中,运行时也会忽略。

  • 注解处理器APT(Annotation Processor Tools):在程序的编译时期,通过自定义注解处理器,拿到注解信息,来帮我们生成一些辅助类。

    关于注解处理器的知识和如何自定义注解处理器,可以看我的这篇文章:

  • 语法检查:通过注解对代码的语法进行检查,提示。

如下:我们声明一个DataUtils

public class DateUtils {
    enum WeekDay {
        SUNDAY, MONDAY
    }
    private WeekDay currentDate;

    public void setCurrentDate(WeekDay weekDay) {
        currentDate = weekDay;
    }
}

如果我们使用setCurrentDate()传值,只能传WeekDay枚举类型,但是我们知道枚举占内存是要比常量大的,枚举类型它编译完其实就是一个Object,包含12个字节对象头,成员大小和8字节对齐等。那么现在我们要对代码进行优化,如下:

public class DateUtils {
    private final int SUNDAY = 1;
    private final int MONDAY = 2;
    private int currentDate;

    public void setCurrentDate(int weekDay) {
        currentDate = weekDay;
    }
}

优化完后方法参数变成的int类型,虽然占用内存的问题解决了,但是有个新的问题是,我们封装这个接口的目的是想让使用方法者传递日期也就是SUNDAY或者MONDAY,但由于是int类型,那么调用者就可以传123或者456任意int类型的参数。

所以这是我们可以通过注解来进行语法检查,限制传参类型:

public class DateUtils {
    @IntDef({SUNDAY, MONDAY})
    @Target({ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.SOURCE)
    @interface WeekDay {

    }

    private static final int SUNDAY = 1;
    private static final int MONDAY = 2;

    @WeekDay
    private int currentDate;

    public void setCurrentDate(@WeekDay int weekDay) {
        currentDate = weekDay;
    }
}

CLASS级别的使用场景

CLASS 级别的注解,生命周期会保留到.class文件中,但是会被虚拟机忽略(即无法在运行期反射获取注解)。此时完全符合,此种注解的应用场景为字节码操作。

  • 字节码增强:说白了就是在字节码中写代码。因为class也是有格式的,按照特定的方式记录排列,只要你懂他的格式和字节码,那么既可以通过注解在.class中去插入代码。
    • AspectJ
    • 热修复
  • 所谓字节码操作即为,直接修改字节码.class文件以达到修改代码执行逻辑的目的。大概的思路是 .class文件 → IO读取 → byte [] → 按照格式插入
  • 关于字节码增强的知识可参考:美团技术团队-字节码增强技术探索

RUNTIME级别的使用场景

  • 运行时:也就是在代码运行阶段,通过反射拿到注解的信息,然后来做一些事情。
  • 反射:不需要实例化某个对象,就可以获取到对象及对象中的成员进行操作,这就是反射。
  • 关于反射想了解更多可查看我的这篇文章:Java进阶 - 反射

小需求:

  • MainActivity中通过注解@InjectView取代代码中的finViewById().
  • MainActivity通过Intent传值,给SecondActivity,然后通过注解和反射拿到传递的值,而不需要getIntent()去一一获取。

第一步,先把两个运行时注解声明出来

//注入View的注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectView {
    @IdRes int value();
}

//赋值Intent传值的注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IntentExtraParam {
    String key();
}

第二步,创建注入工具类进行反射注入

public class InjectUtils {
    /**
     * 通过反射注入View
     *
     * @param activity
     */
    public static void injectView(Activity activity) {
        Class<? extends Activity> cls = activity.getClass();
        //获得此类所有的成员
        Field[] declaredFields = cls.getDeclaredFields();
        for (Field filed : declaredFields) {
            // 判断属性是否被InjectView注解声明
            if (filed.isAnnotationPresent(InjectView.class)) {
                InjectView injectView = filed.getAnnotation(InjectView.class);
                //获得了注解中设置的id
                int  id   = injectView.value();
                View view = activity.findViewById(id);
                //反射设置 属性的值
                filed.setAccessible(true); //设置访问权限,允许操作private的属性
                try {
                    //反射赋值
                    filed.set(activity, view);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    /**
     * 通过反射拿到 Intent 传的值,并赋值
     *
     * @param activity
     */
    public static void injectIntentExtra(Activity activity) {
        Class<? extends Activity> aClass = activity.getClass();
        //获取数据
        Intent intent = activity.getIntent();
        Bundle extras = intent.getExtras();
        if (extras == null) {
            return;
        }
        //获得所有成员
        Field[] declaredFields = aClass.getDeclaredFields();
        for (Field field : declaredFields) {
            //判断被IntentExtraParam注解的成员
            if (field.isAnnotationPresent(IntentExtraParam.class)) {
                IntentExtraParam intentExtraParam = field.getAnnotation(IntentExtraParam.class);
                //获得key(如果注解的value为null,则按照成员的名去获取key)
                String key = TextUtils.isEmpty(intentExtraParam.key()) ? field.getName() : intentExtraParam.key();
                //TODO Parcelable数组不能直接赋值,需要进行类型转换
                //判断是Bundle中是否存在符合key的值
                if (extras.containsKey(key)) {
                    //根据key获取数据(这里直接用get方法根据key获得value,不然还得一个一个类型进行判断)
                    Object obj = extras.get(key);
                    //获取数组单个元素类型
                    Class<?> componentType = field.getType().getComponentType();
                    //当前属性是数组,并且元素是Parcelable子类型
                    if (field.getType().isArray() && Parcelable.class.isAssignableFrom(componentType)) {
                        //强转为数组
                        Object[] objects = (Object[]) obj;
                        //并且吧元素一个个转换成Parcelable类型
                        objects = Arrays.copyOf(objects, objects.length, (Class<? extends Object[]>) field.getType());
                        obj = objects;
                    }
                    field.setAccessible(true);//设置访问权限
                    try {
                        //通过反射赋值
                        field.set(activity, obj);
                    } catch (Exception e) {

                    }
                }
            }
        }
    }
}

第三步,创建两个Activity

public class MainActivity extends AppCompatActivity {
    @InjectView(R.id.v_tv)
    private TextView mTv;

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

        mTv.setText("运行时反射 注入虽然很香,但性能会有影响");

        ArrayList<UserParcelable> userParcelableList = new ArrayList<>();
        userParcelableList.add(new UserParcelable("陈皮的柚子"));

        Intent intent = new Intent(this, SecondActivity.class);
        intent.putExtra("name", "陈皮的柚子");
        intent.putExtra("age", 18);
        intent.putExtra("array", new int[]{1,2,3,4});
        intent.putExtra("userSerializable", new UserSerializable("柚子怪"));
        intent.putExtra("userParcelables", userParcelableList);
        intent.putExtra("strArray", new String[]{"A","B","C"});
        startActivity(intent);
    }
}

public class SecondActivity extends Activity {
    @IntentExtraParam()
    private String name;

    @IntentExtraParam()
    private int age;

    @IntentExtraParam()
    private int array[];

    @IntentExtraParam()
    private UserSerializable userSerializable;

    @IntentExtraParam()
    private ArrayList<UserParcelable> userParcelables;

    @IntentExtraParam()
    private String strArray[];

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

        System.out.println(toString());

    }
    @Override
    public String toString() {
        return "SecondActivity{" + "\n" +
                "name='" + name + '\'' + "\n" +
                "age=" + age + "\n" +
                "array=" + Arrays.toString(array) + "\n" +
                "userSerializable=" + userSerializable + "\n" +
                "userParcelables=" + userParcelables + "\n" +
                "strArray=" + Arrays.toString(strArray) + "\n" +
                '}';
    }
}

运行结果:

I/System.out: SecondActivity{
I/System.out: name='陈皮的柚子'
I/System.out: age=18
I/System.out: array=[1, 2, 3, 4]
I/System.out: userSerializable=UserSerializable{name='柚子怪'}
I/System.out: userParcelables=[UserParcelable{name='陈皮的柚子'}]
I/System.out: strArray=[A, B, C]
I/System.out: }

Java内置注解

注解 作用
@Deprecated                                 用来标记过时的元素,可以是类、接口、方法等.....
(jdk1.5更新)
@Override 子类要复写父类中被 @Override 修饰的方法
(jdk1.5更新)
@SuppressWarnings 阻止编译器的一些警告@SuppressWarnings("XXX")
(jdk1.5更新)
@SuppressWarnings("rawtypes") 表示:抑制编译器警告(这里清除)rawtypes(单类型)
@SuppressWarnings(value={"unchecked", "rawtypes"}) {"unchecked", "rawtypes"} :(多类型)
@SuppressWarnings("all") all:(所有)
@FunctionalInterface 函数式接口 (Functional Interface) 就是一个具有一个方法的普通接口,比如Java中的Runnable 接口。
(jdk1.8更新)

Androidx内置注解

参考:Android 注解指南


注解 作用
@AnimatorRes                                 表示整数参数,字段或方法返回值应该是动画资源引用

(例如 android.R.animator.fade_in)
@AnimRes 表示整数参数,字段或方法返回值应该是anim资源引用

(例如 android.R.anim.fade_in )
@AnyRes 表示整数参数,字段或方法返回值应该是任何类型的资源引用

(如果已知需要的类型,那么应该用更详细的资源类型注解 )
@AnyThread 表示可以从任何线程调用带注释的方法(例如,
@ArrayRes 表示整数参数,字段或方法返回值应该是数组资源引用(例如,
@AttrRes 表示整型参数,字段或方法返回值应该是属性引用(例如,
@BinderThread 表示只应在活页夹线程上调用带注释的方法。
@BoolRes 表示整数参数,字段或方法的返回值应该是一个布尔资源引用。
@CallSuper 表示任何重写方法都应该调用此方法。
@CheckResult 表示注释的方法返回的结果通常是要忽略的错误。
@ColorInt 表示带注释的元素表示打包的颜色int, AARRGGBB 。
@ColorRes 表示整数参数,字段或方法返回值应该是一个颜色资源引用(例如,
@DimenRes 表示整数参数,字段或方法返回值应该是维度资源引用(例如,
@Dimension 表示整数参数,字段或方法返回值预计表示维度。
@DrawableRes 表示一个整数参数,字段或方法的返回值应该是一个可绘制的资源引用(例如
@FloatRange 表示注释的元素应该是给定范围内的float或double
@FractionRes 表示整数参数,字段或方法返回值应该是分数资源引用。
@IdRes 表示整数参数,字段或方法返回值应该是一个id资源引用(例如,
@IntDef 表示整数类型的注释元素表示一个逻辑类型,并且它的值应该是明确命名的常量之一。
@IntegerRes 表示整数参数,字段或方法返回值应该是整数资源引用(例如,
@InterpolatorRes 表示整数参数,字段或方法返回值预计是插值器资源引用(例如,
@IntRange 表示注释的元素应该是给定范围内的int或long
@Keep 表示在构建时将代码缩小时,不应删除带注释的元素。
@LayoutRes 表示整数参数,字段或方法返回值预计为布局资源引用(例如,
@MainThread 表示仅应在主线程上调用带注释的方法。
@MenuRes 表示整数参数,字段或方法返回值应该是菜单资源引用。
@NonNull 表示参数,字段或方法返回值不能为空。
@Nullable 表示参数,字段或方法返回值可以为null。
@PluralsRes 表示整数参数,字段或方法返回值应该是复数资源引用。
@Px 表示整数参数,字段或方法返回值预计表示像素维度。
@RawRes 表示整数参数,字段或方法返回值应该是原始资源引用。
@RequiresApi 表示注释元素只应在给定的API级别或更高级别上调用。
@RequiresPermission 表示注释元素需要(或可能需要)一个或多个权限。
@RequiresPermission.Read 指定读取操作需要给定权限。
@RequiresPermission.Write 指定写操作需要给定权限。
@Size 表示注释元素应具有给定的大小或长度。
@StringDef 表示带注释的String元素表示一个逻辑类型,并且其值应该是明确命名的常量之一。
@StringRes 表示整数参数,字段或方法返回值应该是一个字符串资源引用(例如,
@StyleableRes 表示整数参数,字段或方法返回值应该是一个可修改的资源引用(例如,
@StyleRes 表示整数参数,字段或方法返回值应该是样式资源引用(例如,
@TransitionRes 表示整数参数,字段或方法返回值应该是转换资源引用。
@UiThread 表示注释的方法或构造函数只应在UI线程上调用。
@VisibleForTesting 表示类,方法或字段的可见性放宽,因此它比代码可测试的其他必要条件更为广泛可见。
@WorkerThread 表示仅应在工作线程上调用带注释的方法。
@XmlRes 表示整数参数,字段或方法返回值应该是XML资源引用。