Java基础进阶--注解

Java基础进阶--注解

    • 什么是注解?
    • 元注解
      • @Target
      • @Retention
    • 注解的应用场景
      • APT
      • IDE输入限定检查
      • 字节码编码
      • 注解与反射

什么是注解?

注解就是一个标签,可以放在任何地方,一个类,一个方法,一个变量,都可以用注解来标注。注解的定义也非常简单:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface User {
    int age();

    String id() default "1";
}

在注解中定义属性,要以方法的形式来定义也就是说 需要(),这里是可以设置默认值的,在()后边使用default关键字再加上我们要默认的值。
然后在这里我们使用这个注解:

@User(age = 0,id = "1")
public class MainActivity extends AppCompatActivity {
	@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
}

这里有一点特殊的地方需要注意,就是当注解的属性名为value的时候,我们在使用这个注解,并且只需要传入value这个值的时候,我们的value = 这个属性名是可以省略的。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface User {
    int value();

    String id() default "1";
}
@User(0)
public class MainActivity extends AppCompatActivity {
	@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
}

这里在使用的时候,我们传入值的时候只需要一个值就可以了,不需要再写value = 了。
这就是value这个属性的特殊之处,这里一定要注意。

但是,如果需要传入两个值呢,如果我们以上注解的第二个参数并没有设置默认值的情况下呢?那么,这时候的value = 是必须要写的。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface User {
    int value();

    String id();
}
@User(value = 0, id = "1")
public class MainActivity extends AppCompatActivity {
	@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
}

元注解

大家有没有发现,在我们定义的注解上边,还有两个注解@Target和@Retention。我们将这种定义在注解上边的注解,统称为元注解。默认的元注解一共有4个:

  • @Targe
  • @Retention
  • @Documented
  • @Inherited

这里我们常用的就是前边两个,@Documented和@Inherited我们是很少用到的。前者用于被javadoc工具提取成文档,后者表示允许子类继承父类中定义的注解。这里我们简单了解一下就好,这两个注解这里不做深入研究。

在jdk8又引入了一个新的元注解,@Repeatable,注解正常情况下,只可以使用一个,例如这样的情况是不行的

@Test
@Test
Class A

使用了这个新的元注解之后,同一个注解就可以重复定义了。

@Target

@Target这个注解的概念,就是说,我定义的注解要放在什么地方,也就是说要给什么样的成员去使用。我们知道注解可以用在类上边,方法上边,成员变量上边等等,那么这个注解就是规定这个作用域的。这里列出这个注解的作用域:

  • ElementType.ANNOTATION_TYPE 可以应用于注解类型
  • ElementType.CONSTRUCTOR 可以应用于构造函数
  • ElementType.FILED 可以应用于字段或属性
  • ElementType.LOCAL_VARIABLE 可以应用于局部变量
  • ElementType.METHOD 可以应用于方法
  • ElementType.PACKAGE 可以应用于包声明
  • ElementType.PARAMETER 可以应用于方法
  • ElementType.TYPE 可以应用于类

@Retention

@Retation这个注解。就是说这个注解保留的级别,限定了保留的级别后,只能在对应级别保留,在更高一层的级别,会被忽略掉。这里我们有三个对应的保留级别:

  • RetentionPolicy.SOURCE 保留在源码
  • RetentionPolicy.CLASS 保留在.class文件
  • RetentionPolicy.RUNTIME 保留在运行时

这里我们还是通过反编译成字节码的方式,来看一下这三种保存级别在字节码中的体现。

@User(value = 0, id = "1")
public class MainActivity extends AppCompatActivity {
...
}

接下来是字节码

// class version 51.0 (51)
// access flags 0x21
public class com/example/enjoyjava/MainActivity extends androidx/appcompat/app/AppCompatActivity {

  // compiled from: MainActivity.java
  // access flags 0x4019
  public final static enum INNERCLASS com/example/enjoyjava/MainActivity$WeekDay com/example/enjoyjava/MainActivity WeekDay

  // access flags 0x2
  private Landroid/widget/TextView; tv

  // access flags 0x2
  private Landroid/widget/ImageView; iv

  // access flags 0x2
  private Lcom/example/enjoyjava/MainActivity$WeekDay; weekDay

我们可以看到当保留级别为SOURCE的时候,在字节码里看不到了,被忽略掉了。也就是说被这个属性限定的注解只保存在源码之中。
接下来我们看一看保留级别为CLASS的表现。

public class com/example/enjoyjava/MainActivity extends androidx/appcompat/app/AppCompatActivity {

  // compiled from: MainActivity.java

  @Lcom/example/enjoyjava/User;(value=0, id="1") // invisible
  // access flags 0x4019
  public final static enum INNERCLASS com/example/enjoyjava/MainActivity$WeekDay com/example/enjoyjava/MainActivity WeekDay

  // access flags 0x2
  private Landroid/widget/TextView; tv

  // access flags 0x2
  private Landroid/widget/ImageView; iv

  // access flags 0x2
  private Lcom/example/enjoyjava/MainActivity$WeekDay; weekDay

可以看到,当保存级别为class的时候,我们在字节码中看到了我们的User注解。
这里要注意保留级别的等级关系,是一个包含的关系。也就是说CLASS包含SOURCE,RUNTIME包含CLASS和SOURCE。

注解的应用场景

我们现在知道注解是存在这三种保留级别的,那么,这三种级别分别应用在哪些场景呢?
SOURCE:APT(注解处理器)、IDE输入限定检查。
CLASS:字节码编码。
RUNTIME:通过反射机制获取注解,来进行任意的操作。

APT

这里我们创建一个注解处理器,来加深一下理解。

@SupportedAnnotationTypes("com.example.enjoyjava.User")
public class UserProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Messager messager = processingEnv.getMessager();
        messager.printMessage(Diagnostic.Kind.NOTE, "=====================");
        return false;
    }
}

创建一个类继承自AbstractProcessor这个类,这样的类就是注解处理器。
这里Messager就是注解处理器打印log的sdk,类似android中的Log,这里不要使用System.out.print。

由于注解处理器会获取所有的注解来进行处理,这里我们希望只处理我们自己定义的注解,所以这里在类的上边加入注解@SupportedAnnotationTypes(“com.example.enjoyjava.User”),这个就表示我们的注解处理器只需要处理User这个注解就可以了。

但是就像我们的MainActivity,继承了Activity之后,不能直接使用,需要在manifest文件中配置,才可以在界面上显示出来。这里同样需要配置一个文件,来使我们的注解处理器生效。

main
	java
		resource
			META-INF
				services
					javax.annotation.processing.Processor

我们以这样的目录结构,最后创建这个文件javax.annotation.processing.Processor,这样我们在编译的时候就会以这个配置文件中的配置去加载我们的注解处理器。

这里,这个文件的配置也非常简单,就是将我们的注解的全类名写在这里就可以了

com.example.compiler.UserProcessor

接下来我们编译一项我们的项目。

> Task :app:compileDebugJavaWithJavac
The following annotation processors are not incremental: jetified-compiler.jar (project :compiler).
Make sure all annotation processors are incremental to improve your build speed.
警告: No SupportedSourceVersion annotation found on com.example.compiler.UserProcessor, returning RELEASE_6.
警告: 来自注释处理程序 'org.gradle.api.internal.tasks.compile.processing.TimeTrackingProcessor' 的受支持 source 版本 'RELEASE_6' 低于 -source '1.7': =====================: =====================

在编译的时候我们会发现,打印了我们设置好的log。这里我们看到,打印log的时机就是在 运行compileDebugJavaWithJavac 这个Task的时候,也就是说运行javac命令的时候,会调起我们定义好的注解处理器。

在注解处理器里获取了我们定义好的注解,那么就可以做任何我们像要做的事情,在这里不再深入探讨,在后续的文章中,会结合各种技术,来研究注解处理器的各种用途。

IDE输入限定检查

在源码中保留的注解,除了可以提供给APT来使用之外,还可以进行输入类型的检查。这里我们还是举一个例子:

public class MainActivity extends AppCompatActivity {

    private TextView tv;
    private ImageView iv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setId(111);//ide错误提示,不影响编译
        setId(R.drawable.ic_launcher_background);
    }

    public void setId(@DrawableRes int id) {
        iv.setImageResource(id);
    }
}

我们在设置一个ImageView资源id的时候,一般都是这样来写iv.setImageResource(),这里参数是一个int类型的数据,这里如果我们不用@DrawableRes,在参数上设置一个注解。在setId这个方法里,我们传入任何一个int类型的值都是可以的。显然这样是有问题的,因为我们只想要drawable的id值来设置我们的图片。这里在方法参数前边加上@DrawableRes这个注解后,在set(111)的时候,会报错。这个错误就是ide在通过注解的定义来检查你输入的值。这个错误是不会影响编译的,编译还是可以成功的。

再举个例子,比如我们有一个属性记录了一个星期的某一天,一般我们会这样做

class enum WeekDay{
	Monday,
	Sunday
}

set weekDay(WeekDay weekday)

这样我们就可以限定传入的值,只可能是Monday,或者Sunday。但是,由于是枚举类型,这里我们实际上是生成了Monday和Sunday两个对象。而对象占用的内存,至少会有一个12字节的头部,这里也就是至少占用了24字节。这里还是可以通过字节码看到。

public final enum com/example/enjoyjava/MainActivity$WeekDay extends java/lang/Enum {

  // compiled from: MainActivity.java
  // access flags 0x4019
  public final static enum INNERCLASS com/example/enjoyjava/MainActivity$WeekDay com/example/enjoyjava/MainActivity WeekDay

  // access flags 0x4019
  public final static enum Lcom/example/enjoyjava/MainActivity$WeekDay; MonDay

  // access flags 0x4019
  public final static enum Lcom/example/enjoyjava/MainActivity$WeekDay; SunDay

所以考虑到性能的原因,这里我们不使用枚举,我们一般会这么做

public static final int MONDAY = 0;
public static final int SUNDAY = 1;

public void setWeekDay(int weekDay)

设置两个常量,在传值的时候,传入这两个常量。但是,这里没有做任何的规定,还是可以传入任何一个int值,不传入MONDAY或SUNDAY当然也是可以的,我们为了避免这种情况发生,这里可以使用注解来警告使用这个方法的人,这里只能传入MONDAY或是SUNDAY。
我们定义这样一个注解:

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

}

这里定义注解使用@IntDeft元注解,参数传入我们设定的范围。
然后我们的setWeekDay参数中使用该注解,这样,在使用这个方法的时候,如果你传入的不是MONDAY或者SUNDAY,就会提示一个输入类型错误的警告,会提示只可以输入以上两种。

字节码编码

字节码编码在后续的文章会提到,比如热修复技术,性能监控啊,等等,这里不做探讨。

注解与反射

由于我们的注解的保留级别为运行时,那么就可以通过反射拿到成员的注解,通过注解拿到属性的值,做各种操作。下面还是用代码来举例,来实现通过反射的方法获取注解,从而注入我们的TextView和ImageView。
这里,我们先定义一个变量使用的注解:

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

注解中,只有一个int值,传入我们的资源id。
在MainActivity中我们这样来使用:

public class MainActivity extends AppCompatActivity {

    @InjectView(R.id.tv)
    private TextView tv;
    @InjectView(R.id.iv)
    private ImageView iv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewInjectUtil.inject(this);
        tv.setText("After Inject View");
        iv.setImageResource(R.drawable.ic_launcher_background);
    }
}

这里我们定义了两个控件,TextView和ImageView,xml布局这里就不做展示了,就是简单的放了一个文字和一个图片。接下来写我们的注解处理的类:

public class ViewInjectUtil {
    public static void inject(Activity activity) {
        Class<? extends Activity> c = activity.getClass();
        Field[] declaredFields = c.getDeclaredFields();
        for (Field field : declaredFields) {
            if (field.isAnnotationPresent(InjectView.class)) {
                InjectView injectView = field.getAnnotation(InjectView.class);
                int id = injectView.value();
                View view = activity.findViewById(id);
                field.setAccessible(true);
                try {
                    field.set(activity, view);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

这里我们通过传入的activity,拿到class的类型,就可以通过反射拿到类中的各个成员变量,然后通过变量拿到注解,最后通过注解的属性值,设置我们的成员变量。由于注解有多种类型所以,这里我们用if (field.isAnnotationPresent(InjectView.class))这个方法,就可以过滤出我们想要处理的注解。

这里使用了大量反射的sdk,由于这里我们关注的只是注解在运行时的使用,所以这里不对反射做过多的探讨,在后续的文章中,会详细的探讨反射的使用方法,这里只是了解注解可以通过反射调用就可以了。

注解,本身是没有任何意义的,它只有结合各种应用场景,才会实现各式各样的复杂操作,这里只是简单的了解了一下注解的基本使用方法,后续还会有大量的框架分析,源码分析,会涉及到注解的使用,这里打好基础,在接下来的分析中才能更好的去理解。

上一篇:Java基础进阶–泛型.
下一篇:Java基础进阶–反射.

你可能感兴趣的:(Java基础进阶,java)