Java 夯实基础之注解

不积跬步无以至千里,不积小流无以成江海。厚积才能薄发,水到自然渠成;

一如既往先提三个问题:注解是什么?注解怎么用?注解有什么作用?

注解是什么?

  注解就是对程序代码的补充说明,可以理解为标签,是对这段程序代码的解释,主要的目的就是提高我们的代码质量,和工作效率

注解怎么用?

1. 注解的语法

   同 classinterface 一样,注解也是一种类型,通过@interface 关键字定义,其语法如下:

public @interface TestAnnotation {

}

其定义形式类似于接口,只不过在前面多了一个 @ 符号,这里表示创建了一个TestAnnotation 的注解。

2. 注解的属性

   注解的属性也叫成员变量,注解只有成员变量,没有成员方法。 注解的成员变量在注解中以无形参的方法来声明,其方法名定义了该成员变量的名字,返回类型定义了该成员变量的类型,比如:

@Inherited//该注解可以它注解的类的子类继承
@Retention(RetentionPolicy.RUNTIME)//该注解的生命周期
@Target(ElementType.TYPE)
public @interface Test {
    int id() default -1;
    String msg() default "";
}

上述代码定义了一个叫 Test 的注解,它拥有 idmsg这两个成员属性,在使用时,通过在注解的()中以value= xxx的形式赋值,多个属性通过,隔开,如下所示:

@Test(id = 2, msg = "hello annotation")
public class SuperMan {
    @Check("hi")
    int a;

    @Perform
    @Person(role = "PM")
    public void testMethod() {
    }

    @SuppressWarnings("deprecation")
    public void test() {

    }
}

Tips:

  • 注解中只能用public或默认(default)这两个访问权修饰;
  • 成员属性只能是八大基本类型byte,short,char,int,long,float,double,booleanString,Enum,Class,annotations等数据类型,以及这一些类型的数组;
  • 如果只有一个成员属性,建议使用value()替代
  • 注解属性必须有确认的值,可以在定义注解时使用default 指定,或者在使用注解时指定,非基本类型的注解元素的值不可为null,常使用空字符串或负数作为默认值。
/**
 *自定义注解MyAnnotation
 */
@Target(ElementType.TYPE) //目标对象是类型
@Retention(RetentionPolicy.RUNTIME) //保存至运行时
@Documented //生成javadoc文档时,该注解内容一起生成文档
@Inherited //该注解被子类继承
public @interface MyAnnotation {
    public String value() default ""; //当只有一个元素时,建议元素名定义为value(),这样使用时赋值可以省略"value="
    String name() default "devin"; //String
    int age() default 18; //int
    boolean isStudent() default true; //boolean
    String[] alias(); //数组
    enum Color {GREEN, BLUE, RED,} //枚举类型
    Color favoriteColor() default Color.GREEN; //枚举值
}

3. 注解的应用

注解的分类
  • 根据成员个数分类
    1. 标记注解
       没有定义成员的注解类型,自身就代表了某类信息,比如@override
    2. 单成员注解
       只定义了一个成员,比如@SuppressWarnings定义了一个成员String[] value,使用value={...}大括号来声明数组值,一般也可以省略“value=”
    3. 多成员注解
       定义了多个成员,使用时以name=value对分别提供数据
  • 根据用途和功能分类
    1. 元注解
       元注解,可以理解为注解的注解,即解释注解信息的一些特殊标签,它们主要的功能是确定注解的生命周期@Retention,范围@Target,是否被子类继承@Inherited,是否可重复@Repeatable,是否生成到javac文档中@Documented
  • @Retention
       Retention 的英文意为保留期的意思。当 @Retention 应用到一个注解上的时候,它解释说明了这个注解的的存活时间。

它的取值如下:
   - RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
   - RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
   - RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。

   @Retention 相当于给一个注解盖了一张时间戳,时间戳指明了注解的时间周期。

  • @Target
      Target 是目标的意思,@Target 指定了注解运用的地方。当一个注解被 @Target 注解时,这个注解就被限定了运用的场景。

类比到标签,原本标签是你想张贴到哪个地方就到哪个地方,但是因为 @Target 的存在,它张贴的地方就非常具体了,比如只能张贴到方法上、类上、方法参数上等等。@Target 有下面的取值

  - 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)//该注解的生命周期
@Target(ElementType.TYPE)
public @interface Test {
    int id();
    String msg();
}

@Person(role = "产品经理")
@Person(role = "PM")
@Test(id = 2, msg = "hello annotation")
public class SuperMan {
    @Check("hi")
    int a;

    @Perform
    @Person(role = "PM")
    public void testMethod() {
    }

    @SuppressWarnings("deprecation")
    public void test() {

    }
}

public class Man extends SuperMan {
}

public class TestClass {
    public static void main(String[] args) {
        //首先可以通过 Class 对象的 isAnnotationPresent() 方法判断它是否应用了某个注解
        boolean hasAnnotation = SuperMan.class.isAnnotationPresent(Test.class);
        if (hasAnnotation) {
            //返回指定类型的注解
            Test annotation = SuperMan.class.getAnnotation(Test.class);
            System.out.println("id:" + annotation.id());
            System.out.println("msg:" + annotation.msg());
            //返回注解到这个元素上的所有注解。
            Annotation[] annotations = SuperMan.class.getAnnotations();
            for (Annotation an : annotations) {
                System.out.println("class SuperMan annotation:" + an.annotationType().getSimpleName());
            }
        }
        //首先可以通过 Class 对象的 isAnnotationPresent() 方法判断它是否应用了某个注解
        boolean manHasAnnotation = Man.class.isAnnotationPresent(Test.class);
        if (manHasAnnotation) {
            //返回指定类型的注解
            Test annotation = Man.class.getAnnotation(Test.class);
            System.out.println("Man id:" + annotation.id());
            System.out.println("Man msg:" + annotation.msg());
            //返回注解到这个元素上的所有注解。
            Annotation[] annotations = Man.class.getAnnotations();
            for (Annotation an : annotations) {
                System.out.println("class Man annotation:" + an.annotationType().getSimpleName());
            }
        }
}

结果:
在这里插入图片描述
注解 Test@Inherited 修饰,之后类 SuperMan@Test 注解,类 man 继承 SuperMan ,类 man也拥有 @Test 这个注解。

  • @Repeatable
    Repeatable 自然是可重复的意思。@Repeatable 是 Java 1.8 才加进来的,所以算是一个新的特性。(注:只有在java 1.8 中才能如下使用)
    • 什么样的注解会多次应用呢?通常是注解的值可以同时取多个。
    • 举个例子,一个人他既是程序员又是产品经理,同时他还是个画家。
@Retention(RetentionPolicy.RUNTIME)
public @interface Persons {
   Person[] value();
}

@RequiresApi(api = Build.VERSION_CODES.N)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Persons.class)
public @interface Person {
  String role() default "";
}

@Person(role = "产品经理")
@Person(role = "PM")
@Test(id = 2, msg = "hello annotation")
public class SuperMan {
    @Check("hi")
    int a;

    @Perform
    @Person(role = "PM")
    public void testMethod() {
    }

    @SuppressWarnings("deprecation")
    public void test() {

    }
}

注意上面的代码,@Repeatable 注解了 Person。而 @Repeatable 后面括号中的类相当于一个容器注解
什么是容器注解呢?就是用来存放其它注解的地方。它本身也是一个注解。
我们再看看代码中的相关容器注解。

@Retention(RetentionPolicy.RUNTIME)
public @interface Persons {
   Person[] value();
}

  按照规定,它里面必须要有一个 value 的属性,属性类型是一个被 @Repeatable 注解过的注解数组**,注意它是数组。编译环境为 jdk 1.8,低于1.8 会报错
如果不好理解的话,可以这样理解。Persons 是一张总的标签,上面贴满了 Person 这种同类型但内容不一样的标签。把 Persons 给一个 SuperMan 贴上,相当于同时给他贴了程序员、产品经理、画家的标签。

  • @Documented
    它的作用是在生成javadoc文档的时候将该Annotation也写入到文档中

2. 内置注解

  • @Override 限定重写父类方法
  • @Deprecated用于标记已过时
  • @SuppressWarnnings 抑制编译器警告

3. 自定义注解
使用@interface自定义注解,在定义注解时,不能继承其他的注解或接口。比如:

/**
 *自定义注解MyAnnotation
 */
@Target(ElementType.TYPE) //目标对象是类型
@Retention(RetentionPolicy.RUNTIME) //保存至运行时
@Documented //生成javadoc文档时,该注解内容一起生成文档
@Inherited //该注解被子类继承
public @interface MyAnnotation {
    public String value() default ""; //当只有一个元素时,建议元素名定义为value(),这样使用时赋值可以省略"value="
    String name() default "devin"; //String
    int age() default 18; //int
    boolean isStudent() default true; //boolean
    String[] alias(); //数组
    enum Color {GREEN, BLUE, RED,} //枚举类型
    Color favoriteColor() default Color.GREEN; //枚举值
}

注解的获取

我们主要通过反射来获取运行时注解,常用API 如下:

<T extends Annotation> T getAnnotation(Class<T> annotationClass) :返回该程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null;

Annotation[] getDeclaredAnnotation(Class<T>):返回该程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null;与此接口中的其他方法不同,该方法将忽略继承的注解;

Annotation[] getAnnotations():返回该程序元素上存在的所有注解;

Annotation[] getDeclaredAnnotations():返回直接存在于此元素上的所有注释。与此接口中的其他方法不同,该方法将忽略继承的注解;

Annotation[] getAnnotationsByType(Class<T>):返回直接存在于此元素上指定注解类型的所有注解;

Annotation[] getDeclaredAnnotationsByType(Class<T>):返回直接存在于此元素上指定注解类型的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注解;

boolean isAnnotationPresent(Class<?extends Annotation> annotationClass):判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false;

例如:

public class TestClass {
    public static void main(String[] args) {
        //首先可以通过 Class 对象的 isAnnotationPresent() 方法判断它是否应用了某个注解
        boolean hasAnnotation = SuperMan.class.isAnnotationPresent(Test.class);
        if (hasAnnotation) {
            //返回指定类型的注解
            Test annotation = SuperMan.class.getAnnotation(Test.class);
            System.out.println("id:" + annotation.id());
            System.out.println("msg:" + annotation.msg());
            //返回注解到这个元素上的所有注解。
            Annotation[] annotations = SuperMan.class.getAnnotations();
            for (Annotation an : annotations) {
                System.out.println("class SuperMan annotation:" + an.annotationType().getSimpleName());
            }
        }
        //首先可以通过 Class 对象的 isAnnotationPresent() 方法判断它是否应用了某个注解
        boolean manHasAnnotation = Man.class.isAnnotationPresent(Test.class);
        if (manHasAnnotation) {
            //返回指定类型的注解
            Test annotation = Man.class.getAnnotation(Test.class);
            System.out.println("Man id:" + annotation.id());
            System.out.println("Man msg:" + annotation.msg());
            //返回注解到这个元素上的所有注解。
            Annotation[] annotations = Man.class.getAnnotations();
            for (Annotation an : annotations) {
                System.out.println("class Man annotation:" + an.annotationType().getSimpleName());
            }
        }

        try {
            Field a = SuperMan.class.getDeclaredField("a");
            a.setAccessible(true);
            //获取一个成员变量上的注解
            Check check = a.getAnnotation(Check.class);
            if (null != check) {
                System.out.println("check value:" + check.value());
            }

            Method testMethod = SuperMan.class.getDeclaredMethod("testMethod");
            if (null != testMethod) {
                Annotation[] ans = testMethod.getAnnotations();
                for (Annotation an : ans) {
                    System.out.println("method testMethod annotation:" + an.annotationType().getSimpleName());
                }
            }
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }


}

运行结果:
在这里插入图片描述

注解的使用

栗子:

Android 中的 findViewById 可以用注解这样写:

  @BindView(R.id.hello_tv)
    TextView helloTv;

点击事件可以这样写:

    @OnClick({R.id.toast_btn, R.id.change_btn})
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.toast_btn:
                Toast.makeText(MainActivity.this, "click toast button", Toast.LENGTH_SHORT).show();
                break;
            case R.id.change_btn:
                helloTv.setText(getResources().getString(R.string.changebtn));
                break;
            default:
                Toast.makeText(MainActivity.this, "default is called ", Toast.LENGTH_SHORT).show();
                break;
        }
    }
BindView

  这里我们定义了一个 @BindView 注解,因为是使用在属性上的,所以声明Targe 注解为 Field,又因为是在程序运行中使用,故Retention 指定为 RUNTIME,这里我们需要传入了控件id,所以需要再定义一个int 类型的属性接收,完整代码如下所示:

@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
    int value();
}
OnClick
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
    int[] value();
	//setOnClickListener 为点击事件的set 方法默认名
    String listenerSetter() default "setOnClickListener";
	//OnClickListener 点击事件监听的类型
    Class listenerType() default View.OnClickListener.class;
	//点击事件的默认方法名
    String methodName() default "onClick";

}
ViewHandler

  点击事件动态代理类

public class ViewHandler implements InvocationHandler {
    private Object mObject;
    private String mMethodName;
    private Method mMethod;

    public ViewHandler(Object object, String methodName, Method method) {
        mObject = object;
        this.mMethodName = methodName;
        this.mMethod = method;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().equals(mMethodName)) {
            return mMethod.invoke(mObject, args);
        }
        return null;
    }
}

现在我们已经可以拿到控件的id 和注解,但此时我们还不能直接使用这个注解,因为它和我们的控件还没什么关联,现在我们通过反射来实现注解的注入。如下:

public class AnnoUtils {
    private static final String TAG = "AnnoUtils";

    public static void inject(Activity activity) {
        if (null == activity) {
            return;
        }
        try {
            //获取注入的 activity 的Class 对象
            Class clazz = activity.getClass();
            //获取该 Aty 的所有属性
            Field[] fields = clazz.getDeclaredFields();
            if (null == fields || fields.length <= 0) {
                return;
            }
            //遍历所有属性
            for (Field field : fields) {
                //判断当前字段是否支持 BindView 注解
                if (field.isAnnotationPresent(BindView.class)) {
                    //获取注解对象
                    BindView annotation = field.getAnnotation(BindView.class);
                    if (null != annotation) {
                        //获取注解内容
                        int id = annotation.value();
                        View view = activity.findViewById(id);
                        if (view != null) {
                            //把view赋给字段filed
                            field.setAccessible(true);
                            field.set(activity, view);

                        }
                    }
                }
            }


            Method[] methods = clazz.getDeclaredMethods();
            if (null == methods || methods.length <= 0) {
                return;
            }
            for (Method method : methods) {
                if (method.isAnnotationPresent(OnClick.class)) {
                    OnClick annotation = method.getAnnotation(OnClick.class);
                    if (annotation != null) {
                        //获取所有的id
                        int[] ids = annotation.value();
                        for (int id : ids) {
                            //找到相应的view
                            View view = activity.findViewById(id);
                            if (view != null) {
                                method.setAccessible(true);

                                String listenerSetter = annotation.listenerSetter();
                                Class listenerType = annotation.listenerType();
                                String methodName = annotation.methodName();
                                ViewHandler viewHandler = new ViewHandler(activity, methodName, method);
                                //获取onClickListener代理对象
                                Object onClickListener = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType}, viewHandler);
                                //获取view的setOnClickListener方法
                                Method setOnListenerMethod = view.getClass().getMethod(listenerSetter, listenerType);
                                //实现setOnClickListener方法
                                setOnListenerMethod.invoke(view,onClickListener);
                            }
                        }
                    }
                }
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

具体意思代码中都有注释,就不多说了,接着看下我们的Activity代码

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.hello_tv)
    TextView helloTv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        AnnoUtils.inject(this);
        helloTv.setText(getResources().getString(R.string.change_by_annotation));

    }

    @OnClick({R.id.toast_btn, R.id.change_btn})
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.toast_btn:
                Toast.makeText(MainActivity.this, "click toast button", Toast.LENGTH_SHORT).show();
                break;
            case R.id.change_btn:
                helloTv.setText(getResources().getString(R.string.changebtn));
                break;
            default:
                Toast.makeText(MainActivity.this, "default is called ", Toast.LENGTH_SHORT).show();
                break;
        }
    }
}

XML 代码:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/hello_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/change_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/change"
        android:textAllCaps="false"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/hello_tv" />

    <Button
        android:id="@+id/toast_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/toast"
        android:textAllCaps="false"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/change_btn" />

</androidx.constraintlayout.widget.ConstraintLayout>

效果图如下:
效果图
GitHub demo链接

注解有什么作用?

  • 编写文档:通过代码里标识的元数据生成文档。
  • 代码分析:通过代码里标识的元数据对代码进行分析。
  • 编译检查:通过代码里标识的元数据让编译器能实现基本的编译检查。
  • 通过不同注解明规范功能和一些统一的操作,比如retrofit、Junit、dagger2
    个人思考:
      注解它的主要功能就是提供它承载的信息。其实我们在项目开发中,主要有几个指标,一是开发效率要高,二是代码质量要高,三是开发完项目的维护成本要低,其实注解就能很好的承担这些角色,比如提高开发效率的RertofitButterknife 这些框架中的注解,提高代码质量的有Junit,降低维护成本的比如内置注解@Deprecated@Override这些,他们都是为了帮助我们理解代码,分析代码,统一规范行为,提高代码质量,因为一个无法避免的问题就是随着项目的迭代,项目的可读性、可靠性会下降,它的维护开发成本会提升,注解是解决这些问题的一个很好的方式,但是注解的提取需要借助于 Java 的反射技术,反射比较慢,所以注解使用时也需要谨慎计较时间成本。

参考文章:
https://blog.csdn.net/vv_bug/article/details/64500453
https://blog.csdn.net/briblue/article/details/73824058

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值