注解基础知识
1.注解定义
-
官方:Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。Java 注解是从 Java5 开始添加到 Java 的。
-
自己的理解:注解=标签,用来描述一个东西的一些信息,比如超市里的商品标签,标签里面就声明了一些物品的信息,价格、产地、日期等等,而且这些标签如果单独存在的话是没有任何意义的,就是一张纸,Java的注解也是一样的,单独声明一个注解是没有任何意义的,只有他标注在类上和结合反射或者插庄等技术配合使用的话,他的意义才得以体现。
2.注解的声明方式及语法
- 注解和
class和interface一样,也是属于一种类型,声明类通过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资源引用。 |