注解浅析

作者: SheHuan | 来源:发表于2018-03-26 17:34 被阅读369次

    注解(Annotation)是JDK1.5开始引入的,与类、接口、枚举是在同一个层次,虽然在平时开发中我们很少直接和它打交道,但是我们却经常用到它,例如 Java 本身已经为我们提供了几个常用注解:@Deprecated@Override@SuppressWarnings等,熟悉吧!除此之外,Android 中有名的ButterKnifeEventBusDagger2Retrofit都用到了注解,所以注解的重要性可见一斑!

    一、注解的主要作用

    • 代码格式检查,方便IDE检查出错误代码,例如资源类型注解
    • 减少重复的工作,例如ButterKnife,我们也可以用注解做类似的事情,提高开发效率
    • 信息配置,运行时动态处理,获取信息,可实现类似配置文件的功能

    二、注解的定义

    如何定义一个注解呢?可以先看看这些已有的注解是如何实现的,我们先以@Override为例:

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.SOURCE)
    public @interface Override {
    }
    

    可以看到注解是通过@interface关键字来定义的,和接口的定义类似,但是又多了@Target()@Retention(),这些是java中的元注解,元注解可以理解为内置的基础注解,用来限定、说明自定义注解。除了这两个元注解外,还有三个元注解@Inherited@Repeatable@Documented,后边会详细解释这些元注解的作用!

    再看看@SuppressWarnings注解的实现:

    @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface SuppressWarnings {
        String[] value();
    }
    

    @Override相比最大的区别就是注解体中多了String[] value();,它代表注解的属性!关于注解属性也会在后边详细的介绍!

    所以定义注解时,除了使用@interface还需要考虑元注解注解属性,一个自定义注解的伪码如下:

    @元注解0
    @元注解1
    @元注解2
    public @interface 注解名称 {
        类型 attr0();
        类型 attr1();
    }
    

    三、元注解

    @Retention

    代表注解的保留策略,即存活时间。可选的策略如下:

    • RetentionPolicy.SOURCE 注解只保留在源码,在编译器进行编译时会被忽略
    • RetentionPolicy.CLASS 注解由编译器保存在class文件中,但不需要在运行时由VM保留,无法通过反射读取,这是默认的策略。
    • RetentionPolicy.RUNTIME 注解由编译器保存在class文件中,并在运行时由VM保留,可以通过反射读取。

    @Target

    代表注解可能出现的语法位置,即可以在哪里使用定义的注解,可选的位置如下:

    • ElementType.TYPE 类、接口(包括注解类型)或枚举声明
    • ElementType.FIELD 字段声明
    • ElementType.METHOD 方法声明
    • ElementType.PARAMETER 方法的参数声明
    • ElementType.CONSTRUCTOR 类的构造法声明
    • ElementType.LOCAL_VARIABLE 局部变量声明
    • ElementType.ANNOTATION_TYPE 注解声明
    • ElementType.PACKAGE 包声明
    • ElementType.TYPE_PARAMETER JDK1.8新加的,类型参数声明
    • ElementType.TYPE_USE JDK1.8新加的,类型使用声明

    通过查看注解的@Target元注解,可以知道注解能在哪些地方使用!

    @Inherited

    表明注解类型是自动继承的,怎么理解呢?先看代码:

    @Inherited
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface InheritedTest {
    }
    
    @InheritedTest
    public class P {
    }
    
    public class P1 extends P {
    }
    

    我们自定义了一个被@Inherited注解的InheritedTest注解,同时用自定义的注解来注解P类,如果P类有一个子类P1,则该子类也继承了父类的InheritedTest注解,有点绕......

    @Repeatable

    JDK1.8新加的,表明当自定义注解的属性值有多个时,自定义注解可以多次使用,举个例子,手机可以有打电话、上网、相机等功能,可把这些功能看做一个个注解,并给手机类使用,使手机类具有对应功能,具体看代码:

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Funcs {
        Func[] value();
    }
    
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Repeatable(Funcs.class)
    public @interface Func {
        String name() default "";
    }
    
    @Func(name = "CALL")
    @Func(name = "INTERNET")
    @Func(name = "CAMERA")
    public class XPhone {
    }
    

    我们定义注解Func使用了@Repeatable(Funcs.class),其中Funcs也是我们自定义的注解:

    public @interface Funcs {
        Func[] value();
    }
    

    其中有一个数组类型的 value属性,而且名称必须为value,关于注解的属性后边会说到。由于@Func可以重复使用,@Funcs就相当于接收重复注解的容器。

    如果在AS中无法多次使用@Func,请确认是否配置了JDK1.8:

    android {
        ......
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
    }
    

    @Documented

    这个相对简单,说明该注解将被包含在javadoc中。

    四、注解的属性

    在注解中定义属性和在接口中定义方法的格式类似,例如:

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface TestAnnotation {
        String name();
        int age() default 18;
        String[] favour();
    }
    

    这样就给Test注解定义了nameage两个属性,并用default关键字指定age的默认值为18。可以这样使用定义好的注解:

    @TestAnnotation(name = "Tom", age = 12, favour = {"music", "sports"})
    public class Test {
    }
    

    由于age有默认值,可以在使用注解时不指定它的值。由于favour的类型为数组,所以当其有多个值时需要用{}包起来。
    如果自定义注解没有属性或者属性有默认值,则使用时可以直接写@TestAnnotation,省略后边的括号。

    注解的属性支持的数据类型如下:

    • 基本类型(byte、short、int、float、double、long、char、boolean),不包括其对应的包装类型
    • String
    • Class,即Class<?>
    • enum,例如enum staus {A, B, C}
    • 注解,例如Override test();
    • 上述类型对应的数组

    注解相关的语法糖就介绍到这里了,接下来要关注的是当一个类、方法、属性等使用了注解后,如何提取注解上的信息。

    五、注解与反射

    要提取注解上的信息,就要用到反射相关的知识了,下面看一个完整的例子,首先定义TestAnnotation注解,可以作用在类、字段、方法声明的地方,并可以在运行时被获取,以及三个属性:

    @Target(value = {ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface TestAnnotation {
        String name();
        int age() default 18;
        String[] favour();
    }
    

    Test的类、字段、方法声明的地方分别使用TestAnnotation注解:

    @TestAnnotation(name = "Test", age = 20, favour = {"music", "sports"})
    public class Test {
    
        @TestAnnotation(name = "testField", favour = {"reading", "sports"})
        private int testField;
    
        @TestAnnotation(name = "testMethod", age = 10, favour = {"dancing", "music"})
        public void testMethod() {
    
        }
    
        @TestAnnotation(name = "testMethod1", age = 12, favour = {"music"})
        public void testMethod1() {
    
        }
    }
    

    通过反射的方式提取注解信息:

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            resolve();
        }
    
        private void resolve() {
            // 解析类上的注解
            boolean isPresent = Test.class.isAnnotationPresent(TestAnnotation.class);
            if (isPresent) {
                TestAnnotation annotation = Test.class.getAnnotation(TestAnnotation.class);
                showAnnotation(annotation);
            }
            // 解析字段上的注解
            Field[] fields = Test.class.getDeclaredFields();
            for (Field field : fields) {
                if (field.isAnnotationPresent(TestAnnotation.class)) {
                    TestAnnotation annotation = field.getAnnotation(TestAnnotation.class);
                    showAnnotation(annotation);
                }
            }
            // 解析方法上的注解
            Method[] methods = Test.class.getDeclaredMethods();
            for (Method method : methods) {
                if (method.isAnnotationPresent(TestAnnotation.class)) {
                    TestAnnotation annotation = method.getAnnotation(TestAnnotation.class);
                    showAnnotation(annotation);
                }
            }
        }
    
        private void showAnnotation(TestAnnotation annotation) {
            Log.e("Annotation", annotation.name() + "#" + annotation.age() + "#" + Arrays.toString(annotation.favour()));
        }
    }
    

    运行后的效果如下:


    0

    其中涉及到了几个关键的方法,Class、Method、Field等类都有这样的方法:

    • boolean isAnnotationPresent(Class<? extends Annotation> annotation),用来判断是否使用了某个注解。
    • public <A extends Annotation> A getAnnotation(Class<A> annotation),获得指定名称的注解对象。
    • public Annotation[] getAnnotations(),返回对应元素的全部注解。
    • public Annotation[] getDeclaredAnnotations(),返回直接在对应元素上使用的注解,不包括父类的注解。

    六、枚举的替代方案

    由于性能、开销的问题,在Android中不建议直接使用枚举,要实现类似枚举的功能可以使用android.support.annotation包提供的@IntDef@StringDef注解可以解决这个问题。
    例如要表示一个Button在界面的位置,使用枚举可以这样定义:

    public enum Position {
        LEFT, TOP, RIGHT, BOTTOM
    }
    

    如果使用@IntDef注解,则这样实现:

    @IntDef({LEFT, TOP, RIGHT, BOTTOM})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Position {
        int LEFT = 0;
        int TOP = 1;
        int RIGHT = 2;
        int BOTTOM = 3;
    }
    

    如果使用@StringDef实现也类似,只是对应的位置常量为String类型。

    看一个简单的使用例子,在自定义View中提供一个setButtonPosition()方法,其参数使用了@Position注解:

    public class MyView extends RelativeLayout {
        @IntDef({LEFT, TOP, RIGHT, BOTTOM})
        @Retention(RetentionPolicy.SOURCE)
        public @interface Position {
            int LEFT = 0;
            int TOP = 1;
            int RIGHT = 2;
            int BOTTOM = 3;
        }
    
        public MyView(Context context) {
            this(context, null);
        }
    
        public MyView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        // 设置Button位置
        public void setButtonPosition(@Position int pos) {
            switch (pos) {
                case Position.BOTTOM:
                    break;
                case Position.LEFT:
                    break;
                case Position.RIGHT:
                    break;
                case Position.TOP:
                    break;
            }
        }
    }
    

    当调用该方法时可以保证传入的参数是类型安全的,即只能传入注解中定义的常量:

    new MyView(MainActivity.this).setButtonPosition(MyView.Position.BOTTOM);
    

    因为setButtonPosition()方法的参数使用了@Position注解,在AS中可以方便的完善switch-case语句:

    1

    七、常用的注解

    首先在Android中,android.support.annotation包中提供了许多好用的注解,以下列举部分:

    • 资源类型注解,用来标注元素必须为指定的资源类型,例如@AnimRes@ColorRes@IdRes@LayoutRes@LayoutRes@StringRes
    • 颜色值注解@ColorInt,代表ARGB颜色值,而不是颜色的资源id,注意和@ColorRes区分
    • 空值注解@Nullable:可以为空、@NonNull:不能为空
    • 类型定义注解@IntDef@StringDef
    • 值约束注解,即约束元素取值范围,@Size@IntRange@FloatRange,例如有如下方法,可以保证设置的month值为[1, 12]
    public void setMonth(@IntRange(from = 1, to = 12) int month){
    }
    
    • 权限注解@RequiresPermission,即检查某操作是否有必要的权限,例如:
    @RequiresPermission(Manifest.permission.CAMERA)
    public void takePhoto() {
    }
    
    • 线程注解,可以标注方法、构造函数、类、接口、枚举只能在指定的线程被调用,例如@MainThread@UiThread@WorkerThread
    • 重写方法注解@CallSuper,当子类重写父类方法时,要保证父方法也必须被调用,则对应父方法要使用该注解

    Java也为我们提供了一些常用的注解:

    • @Override,代表某方法是重写父类中的方法
    • @Deprecated,表示被标记的内容已经过时、不建议被使用,如果被使用编译器会报警告,但程序也能正常运行。
    • @SuppressWarnings,由于内容被@Deprecated标记后,编译器会有警告,如果想忽略警告可以使用@SuppressWarnings

    相关文章

      网友评论

      本文标题:注解浅析

      本文链接:https://www.haomeiwen.com/subject/uzpcqftx.html