如何应对Android面试官->Java中的注解、反射、手写ButterKnife核心实现

本文介绍了Java注解的概念、定义、元注解、分类(按保留级别)、应用场景,包括APT技术、IDE语法检查和字节码增强。重点讲解了注解处理程序的创建和运行机制,以及如何使用自定义注解进行语法检查和Android中的反射实践。
摘要由CSDN通过智能技术生成

注解

注解的作用和意义

注解本身没有任何意义,单独的注解就是一种注解,它需要结合其他如反射、插桩、架构设计、框架等才有意义;

Java 注解又称 Java 标注,是 JDK1.5 引入的一种注释机制,是元数据的一种形式,提供有关于程序但又不属于程序本身的数据。注解对它们注解的代码的操作没有直接影响。

如何定义注解

@Target(ElementType.TYPE) // 声明注解作用域
@Retention(RetentionPolicy.CLASS) // 声明注解的生命周期 存在时间
public @interface BindPath {    
    String value() default "";  // 可以定义默认值
    
    // String id();
}

@interface 关键字来声明一个注解;

使用 @BindPath 作用在类上面的时候,需要指定 value 的值, key 的值可以不用指定,因为有默认值;

@BindPath("")
public class MainActivity extends Activity {

}
// 当使用 value 关键字的时候,可以不用指定value作为 key 来赋值
// 如果不使用 value,例如使用 id 的时候,那么需要指定 id 作为key
// @BindPath(id = "")
// 多个元素的时候,需要都指定了
// @BindPath(value = "", id = "")

元注解

定义:注解上的注解;

@Target、@Retention 注解就是一个元注解,这个注解就是用来限定注解可以作用在哪些上面,例如:  类,属性,方法,方法参数等等;

当不指定 @Target 的时候,默认是可以作用在任务区域(类、方法、属性、方法参数)

ElementType.TYPE  // 作用在类上面
ElementType.FIELD // 作用在属性上面
ElementType.METHOD  // 作用在方法上面
ElementType.PARAMETER // 作用在参数上面
ElementType.CONSTRUCTOR // 作用在构造方法上面
ElementType.LOCAL_VARIABLE // 作用在局部变量上面
ElementType.ANNOTATION_TYPE
ElementType.PACKAGE
ElementType.TYPE_PARAMETER
ElementType.TYPE_USE
ElementType.MOUDLE

注解的分类(按照保留级别来分类 @Retention)

  1. SOURCE 阶段 ;源码阶段,经过编译器 javac 变成 class 之后,就会被抹除掉;
  2. CLASS 阶段;  字节码阶段,保留在 class 阶段,但是会在 JVM 加载这个 class 的时候 被忽略掉;
  3. RUNTIME 阶段;运行阶段,保留在运行时阶段;

SOURCE  < CLASS < RUNTIME,即:CLASS 包含了 SOURCE, RUNTIME 包含了 SOURCE 和 CLASS

注解的应用场景

  1. SOURCE阶段
    • APT技术,在编译器能够获取注解与注解声明的类包括类中所有成员信息,一般用于生成额外的辅助类;
    • IDE语法检查;
  2. CLASS阶段
    • 字节码增强,在编译出 class 后,通过修改 class 数据以实现修改代码逻辑的目的。对于是否修改的区分或者修改为不同逻辑的判断可以使用注解;
  3. 运行时阶段
    • 反射,在程序运行期间,通过反射技术动态获取注解与其元素,从而完成不同的逻辑判定;

SOURCE阶段应用场景-APT技术

如何创建一个注解处理程序

  1. Android工程下 创建一个 Java module
  2. 创建一个普通的 Java 类继承 AbstractProcessor,继承之后 这个类就是一个注解处理程序,Javac在编译的时候会对齐进行实例化,然后调用里面的process方法;
  3. 指定要处理的注解;
    • @SupportedAnnonationTypes("com.llc.example.BindPath")

  4. 将这个普通类进行注册,使其成为一个可用的注解处理器;
    • 在 main 下面 创建 resources 文件路径,在 resources 下面 创建 META-INF 路径,在META-INF 路径下创建 services 路径;
    • 在这个路径下创建 文件名为:javax.annotation.processing.Processer 的文件;
    • 文件中写入注解处理程序的全类名;
  5. app 模块引入我们的注解处理程序
    • annotationProcessor project(':compiler')

在 Task : app:compileDebugJavaWithJavac 这个 Task 中,执行 process 方法的调用;

javac 会先调用注解处理程序,处理完注解之后,才会将 .java 处理成 .class

PS:也可以使用 autoservice,但是 autoservice 在 gradle5.0+ 的版本上 会有兼容性问题;

注解处理程序是怎么运行的?

注解处理程序运行在编译阶段(在编译的时候,javac 会帮我们实例化 LanceProcesser 这个类,并调用 process 方法)。

javac 在将 .java 编译成 .class 的时候,就会采集所有的注解信息,包装成一个 Element 节点,再由 javac 调起我们创建的注解处理程序。所以 注解处理程序不需要我们手动调用,它是由 javac 调用的;我们只需要给 javac 指明让它去执行注解处理程序。

注解处理程序有什么作用?

APT技术,在编译器能够获取注解与注解声明的类包括类中所有成员信息,一般用于生成额外的辅助类;

SOURCE阶段应用场景-IDE语法检查

IDE语法检查

IDE中声明的一些注解,用来做语法检查。当我们调用 setDrawable 方法的时候,会对传入的值进行校验;

使用自定义注解来进行语法检查

private static final int SUNDAY = 0;
private static final int MONDY = 1;

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

使用时,IDE就会在编辑的时候 进行语法检查:

CLASS阶段应用场景-字节码增强

字节码增强

字节码中写代码,本质就是 .class -> IO -> byte[]  按照 .class 格式进行修改;

反射

概念

一般情况下,我们使用某个类时必定知道它是什么类,是用来做什么的,并且能够获得此类的引用。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作。

反射则是一开始并不知道我们要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了,这时候,我们使用 JDK 提供的反射 API 进行反射调用。反射就是在运行状态中,对于任意一个类,都能够知道这个类所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变她的属性。是 Java 被视为动态语言的关键。

获得 class 对象

  1. 通过类名获取 类名.class
  2. 通过对象获取 对象名.getClass()

  3. 通过全类名获取 Class.forName(全类名) classLoader.loadClass(全类名)

判断是否为某个类的实例

一般地,我们用 instanceof 关键字来判断是否为某个类的实例。同时我们也可以借助反射中 Class 对象的 isInstance() 方法来判断是否为某个类的实例,它是一个 native 方法;

public native boolean isInstance(Object obj);

判断是否为某个类的类型

public boolean isAssignableFrom(Class<?> cls);

创建实例

通过反射来生成对象主要有两种方式

  • 使用Class对象的newInstance()方法来创建Class对象对应类的实例。
    • Class<?> clazz = String.class;
      Object strObj = class.newInstance();

  • 先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例。这种方法可以用指定的构造器构造类的实例。

  • //获取String所对应的Class对象
    Class<?> c = String.class;// 获取String类带一个String参数的构造器
    Constructor constructor = c.getConstructor(String.class);//根据构造器创建实例
    Object obj = constructor.newInstance("23333");
    System.out.println(obj); 

获取构造器信息

得到构造器的方法

Constructor getConstructor(Class[] params) -- 获得使用特殊的参数类型的public构造函数(包括父类)
Constructor[] getConstructors() -- 获得类的所有公共构造函数
Constructor getDeclaredConstructor(Class[] params) -- 获得使用特定参数类型的构造函数(包括私有)
Constructor[] getDeclaredConstructors() -- 获得类的所有构造函数(与接入级别无关)

获取类构造器的用法与上述获取方法的用法类似。主要是通过Class类的getConstructor方法得到Constructor类的一个实例,而Constructor类有一个newInstance方法可以创建一个对象实例:

public T newInstance(Object ... initargs);

获取类的成员变量(字段)信息

获得字段信息的方法

Field getField(String name) -- 获得命名的公共字段
Field[] getFields() -- 获得类的所有公共字段
Field getDeclaredField(String name) -- 获得类声明的命名的字段
Field[] getDeclaredFields() -- 获得类声明的所有字段

调用方法

获得方法信息的方法

Method getMethod(String name, Class[] params) -- 使用特定的参数类型,获得命名的公共方法
Method[] getMethods() -- 获得类的所有公共方法
Method getDeclaredMethod(String name, Class[] params) -- 使用特写的参数类型,获得类声明的命名的方法
Method[] getDeclaredMethods() -- 获得类声明的所有方法

当我们从类中获取了一个方法后,我们就可以用 invoke() 方法来调用这个方法。invoke 方法的原型为:

public Object invoke(Object obj, Object... args)

Android中声明的 Parcelable 类型数组 如何反射获取?

Android 中 声明 Parcelable 类型数组,在反射的时候需要先获取field 中的 ComponentType,判断这个 Type 是不是 Parcelable 然后创建一个新的数组进行数据的复制之后,才能进行反射设值;

Class<?> componentType = field.getType().getComponentType();

if(field.getType.isArray() && Parcelable.class.isAssignableForm(componentType)) {
	Objcet[] objs = (Object[]) obj;
	Objcet[] objects = Arrays.copyOf(objs, objs.length, 
                        (Class<? extends Object[]>)field.getType);
	obj = objects;
}

field.setAccessible(true);
filed.set(activity, obj);



获取数组中的单个元素类型:
Class<?> componentType = field.getType().getComponentType();

注解 + 反射 实战(自动 findViewById)

声明需要被处理的注解

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

使用这个注解

public class MyActivity extends Activity {
    
    @InjectView(R.id.tv)
    TextView txtView
    
    public void onCreate(Bundle saveInstance) {
        super.onCreate(saveInstance);
        setContentView(R.layout.activity_main);
        InjectUtils.inectView(this)
    }
}

写一个 Utils 来处理这个注解,并调用 findViewById 方法

public class InjectUtils {
    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();
                }
            }
        }
    }
}

欢迎三连

来都来了,点个关注、点个赞吧~~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值