泛型学完后,再来学习下java的另外两个自己用的不多的玩意,注解和反射。这东西其实从三四年前就听说很牛逼哄哄的,那些用到的比较频繁的如黄油刀,Retrofit等框架的实现都是基于反射注解完成的,但自己常年属于安卓接口工程师(写个页面接口获取个数据展示出来这种毫无技术含量的工程师),这玩意根本用不上啊。比起接口工程师的复制粘贴大法写代码,这两个知识点能让你另辟一条路,有着不一样的开发感受,达到另一种开发的G点。
本篇先来看看注解这玩意吧。
注解
什么是注解
首先要明白一点,注解不是注释,注释仅仅只是为了方便别人去看,和代码一点关系都没有,编译的时候注释也是会全部清掉的。而注解呢,就比注释高了一个等级,虽然也是方便人看的,但是注解本身是可以存储数据的,对编写代码和运行代码起了一定的指导作用和关联作用。
举个例子:
@NonNull
private static String t(){
return null;
}
上面这个方法中由于设置了@NonNull这个注解,所以当直接返回null时,你的IDEA工具就会报黄,警告你这个方法不要返回空,这是由于开发工具识别到了代码中的这个注解而在开发工具的界面上做了一个警示处理(侧面说明这个IDEA开发工具的实现用到了java)。
再举个例子:
@POST("oauth/auth/uaa/Login")
Observable<FgLoginResponse> fgLogin(@Body FgLoginRequest request);
这个是利用Retrofit请求登录接口的一段代码,上面的注解@POST里面存的数据就是接口url,这也说明注解本身是可以存数据的,但是注解本身无法使用这个数据,那怎么才能使用注解所存的数据呢,这个Retrofit的网络请求是什么意思呢,在下面注解的使用当中再细说吧。
怎么写注解
注解的写法很简单,它和class、interface、enum是同级的:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface MyAnnotation {
String value();
int id();
}
也就是使用@interface表示这个类是一个注解类。注解类当中没有任何方法的实现,只能定义若干个变量,变量的定义方式为变量类型+变量名称+();定义的变量类型只能为基础类型,String,Class,enum,Annotation以及这些类型的数组。
再来看看注解的使用的写法:
@MyAnnotation(value = "p", id = 2)
private static void t(){
}
可以看到使用的注解后面的括号内需要把注解变量的实际值写出来。如果注解的变量只有value一个的话,使用的时候是可以简写为只把实际值写出来就可以了:
@MyAnnotation("p")
private static void t(){
}
还有一点比较重要,就是声明注解类的上面写的注解,这个叫做元注解。
元注解
很简单看出来,元注解就是对自定义注解类上面的注解。java目前提供了五种元注解@Target、@Retention、@Documented、@Inherited、@Repeatable,一般用的最频繁的两种元注解是@Target和@Retention。(安卓也提供了很多元注解,比如用于语法检查使用的@IntDef)
@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 | 类型使用 |
上面的例子中用的就是普通方法注解,再举几个常用的例子进行对照看下吧:
//定义在类上面的注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface TypeAnnotation {
int value();
}
//定义在变量上面的注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface FieldAnnotation {
int value();
}
//定义在方法形参上面的注解
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.SOURCE)
public @interface ParameterAnnotation {
int value();
}
对应的使用代码如下:
@TypeAnnotation(2)
public class Test {
@FieldAnnotation(2)
private String t;
private void p(@ParameterAnnotation(2) int a){
}
}
@Retention
这种注解的是设置自定义注解的保留级别,也就是该自定义注解在什么时候失效,我自己理解为这个元注解决定了注解可以存在的时间位置。下面是该注解取值所对应的含义:
RetentionPolicy.SOURCE | 注解仅保留在源码级别中, 编译器转class时自动忽略该注解 |
RetentionPolicy.CLASS | 注解会被编译器保留, 转class字节码时也存在该注解, java虚拟机(JVM)运行时会忽略该注解 |
RetentionPolicy.RUNTIME | 注解会被JVM保留 因此程序运行中也能读取到该注解 |
因为这个设置是在时间上的设置,也就是说设置Runtime肯定能完成Class和Source的事,设置Class肯定能完成Source的事,而反过来则不可以。
注解的写法也就是这么简单,设置若干变量(可以不设置),设置时间和空间的限定条件,就完成可以使用了,现在问题就是为啥我们要用注解,为啥说通过注解能完成很多我们平时正常开发无法完成的事情,注解的应用场景是什么?接下来我们就开始正式分析注解的使用!
注解有什么用
首先要说明的是,如果不和其他的技术结合,注解是没有任何作用的,还不如直接写个注释来的方便,而且注释写的还可以更随意。我这里说自己的一种不太靠谱的理解,把注解看成一种病毒,它本身存储着一些数据,通过某些方法让注解和宿主程序结合以后,注解能通过这些方法取出自己携带的数据把宿主程序的执行行为改掉,而我们研究注解也不应该是研究注解本身,而是研究把注解和程序结合的技术和方法,从而自己可以通过注解这种媒介控制程序的运行。接下来我们从注解保留的时间层面来看注解的使用场景,也就是不同的@Retention所对应的不同的用处。
源码级别的注解(SOURCE)
因为该时间设置下,注解只在java源码上存在,通过javac编译成的class后,注解信息就被抹除了,所以就要求我们在生成class之前就要使用到这个注解。一般这种情况有两个应用场景:APT技术,IDE语法检查。
先来说说APT技术,全名叫Annotation Processing Tool,也就是注解处理工具,具体就是通过编译期中javac解析java代码中的注解,并且根据具体方法生成Java代码的一种技术。接下来就说一下一个简单的APT工具的创建和使用。
注解处理工具必须是一个java lib的工程,android lib工程是不可以的。所以首先要建一个java lib模块,写一个类继承AbstractProcessor就表明此类为注解处理程序:
@SupportedAnnotationTypes("com.demo.MyAnnotation")
public class MyProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Messager messager = processingEnv.getMessager();
messager.printMessage(Diagnostic.Kind.NOTE, "kevin print");
return false;
}
}
这个类当中的process方法就是当javac读到指定注解时进行的操作,可以打印日志(需要用上面示例的方式输出日志,日志展示在build过程中,非程序运行的logcat),可以做Http请求,可以操作文件等,而一般都是在这里生成新的类或其他文件来改变原有程序的执行方式。上面的SupportedAnnotationTypes注解则是指定处理的注解的全路径。
然而仅仅这样还是不够的,要在编译阶段让javac找到并执行这个注解处理器,就需要对这个注解处理器进行一个注册,具体方法是在java lib工程下创建resources/META-INF/services目录结构,并在此目录下创建javax.annotation.processing.Processor文件,文件中写明自定义注解编译器的全路径。
这样注册就完成了,最后在安卓工程的build.gradle文件里annotationProcessor依赖这个java lib工程就完成了
annotationProcessor project(':java_demo')
当你去编译安卓工程时,javac读取到安卓工程代码里面对应的注解代码就会执行自定义注解处理器当中的process方法。
APT技术就此打住,更多使用先不说了,之后应该也会单独把APT技术整理出来写一篇博文,到时候把链接再放到这个地方,需要了解更多的可以先自行百度。
再来说说IDE语法检查,举一种情况,我们平时要写一个多种状态码使用例如:
public static final int STATUS_SUCCESS = 0;
public static final int STATUS_FAIL = 1;
public static void main(String[] args) {
setStatus(STATUS_SUCCESS);
}
public static void setStatus(int status){
//TODO
}
可以看出setStatus的方法其实是想让我们传定义的两个常量其中的一个,但是方法的入参却是一个int,这样就会存在一个隐患,你在setStatus这个方法中传3,4,5,6,7...任意一个int类型编译器都不会报错,开发工具甚至都不会对这种操作做出警告,虽然这样的例子在源码当中也是比比皆是,但这确实对后期的维护造成了一定的操作难度,不是一种很完美的开发方式。那该怎么办呢,可以通过定义枚举解决,这样限定用户的传入只能是两种状态当中的一种:
enum STATUS{
STATUS_SUCCESS, STATUS_FAIL
}
public static void main(String[] args) {
setStatus(STATUS.STATUS_SUCCESS);
}
public static void setStatus(STATUS status){
//TODO
}
但是枚举里面每一个元素都会去创建一个对象,在性能上肯定是会打点折扣的。想要性能不受影响,接下来就是用注解的语法检查功能了。
我们需要创建一个语法检查的注解:
@IntDef({MyClass.STATUS_SUCCESS, MyClass.STATUS_FAIL})
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.SOURCE)
public @interface StatusCheck {
}
@IntDef是安卓提供的语法检查的元注解,在里面声明检查的引用常量,在使用的时候若传参不是引用的常量之一,IDEA工具就会给红色下划线错误提示(仅仅是警告不影响运行)。
public static final int STATUS_SUCCESS = 0;
public static final int STATUS_FAIL = 1;
public static void main(String[] args) {
setStatus(0); //AS会触发红色下划线警告提示
setStatus(STATUS_SUCCESS);
}
public static void setStatus(@StatusCheck int status){
//TODO
}
这个检查操作是我们的开发工具读到了注解来完成的。
针对于安卓的元注解有很多,比较常用的有@IntDef,@NonNull,@IdRes等,以后用到再具体说吧,想了解更多可以自行百度。
字节码级别注解(CLASS)
这个级别,注解会被javac编译到class文件当中,但是JVM运行的时候不会读取该注解,所以就要求我们在class字节码当中去读取注解并据此修改字节码,让JVM虚拟机运行新的字节码,从而改变了以前的程序代码的运行,这个运用的技术就是字节码增强,编程方式就是面向切面编程,即AOP。这种技术的一般运用的实际场景就是近几年比较火的热更新技术,由于目前我掌握的还不是特别多,在这里也就不误人子弟了,等到后面再深入研究以后再单独出来说一说。
运行时级别注解(RUNTIME)
这个级别就是注解的最高级别了,满足前面两种级别的要求外,还能运行在JVM当中被JVM读取到,一般是运用到的技术是反射,在运行当中反射获取到注解和注解携带的信息,再进行相关代码操作。
早期的黄油刀实现findviewById就是通过这种方式进行的,下面我们用findviewById举例看一下运行时注解操作代码。
首先创建一个注解FindView:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FindView {
@IdRes int value();
}
空间位置设置为只允许变量属性使用,时间位置保留至运行时也存在,然后定义一个value字段用于存储id信息就完成了。这里的@IdRes也是一个元注解,用于检测此value的传入格式,若不是在R文件中注册过的id就会报错。
接下来是activity中使用这个注解:
public class MainActivity extends AppCompatActivity {
@FindView(R.id.tv_main)
private TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv.setText("runtime view");
}
}
这样的话很显然还不行的,此时tv是一个null,运行会报空指针,所以我们接下来就要建一个通过反射读取注解的工具类,用来实际找到View对象,并在View使用前运行此工具类的方法。
public class AnnotationUtil {
public static void initFindView(Activity activity){
//获取activity的Class对象
Class<? extends Activity> cls = activity.getClass();
//获取所有成员变量的数组
Field[] declaredFields = cls.getDeclaredFields();
//遍历成员变量
for (Field field : declaredFields){
//判断变量是否有FindView注解
if (field.isAnnotationPresent(FindView.class)){
//获取此注解
FindView findView = field.getAnnotation(FindView.class);
//获取此注解所携带的数据,即id
int id = findView.value();
//通过此id find出View的实例
View view = activity.findViewById(id);
//设置访问权限,允许操作private类型
field.setAccessible(true);
try {
//修改属性的值,即把view实例赋予这个变量
field.set(activity, view);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
反射的细节在下一篇再细说,这个用到的看注解就好,最后在TextView设置值之前写上这个工具类的初始化就大功告成了。
public class MainActivity extends AppCompatActivity {
@FindView(R.id.tv_main)
private TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
AnnotationUtil.initFindView(this);
tv.setText("runtime view");
}
}
注解这玩意就到此为止了,其实注解本身是个很简单的东西,但是太多牛逼哄哄的技术会把它拿去用,就感觉它变得高大上了。三月的第一天,小伙伴们一起加油吧。