注解属于比较高级的Java开发技术,前面介绍的内置注解专用于编译器检查代码,另一些注解则由各大框架定义与调用,像Web开发常见的Spring框架、Mybatis框架,Android开发常见的ButterKnife框架等等,都使用了大量的注解。为了更好地弄清注解的应用原理,接下来不妨尝试自定义注解,并在实际开发中对自定义的注解加以运用。
以前介绍异常预防的时候,为了不出现空指针异常,可谓是八仙过海各显神通,一路试验了多项新技术。其中校验某个字段非空尤为是个难点,案例中的苹果类共有四个字段,包括名称、颜色、重量、价格等,假若要求这些字段均非空值才算有效记录的话,就得四个字段一一判断过去。那么采起for循环进行非空检查的常规代码示例以下:html
// 常规的for循环校验,对每一个对象及其每一个属性都进行空指针判断 private static void getRedAppleByFor(List<Apple> list) { List<Apple> redAppleList = new ArrayList<Apple>(); if (list != null) { // 判断清单非空 for (Apple item : list) { // 对每一个字段依次进行空指针判断 if (item!=null && item.getName()!=null && item.getColor()!=null && item.getWeight()!=null && item.getPrice()!=null) { if (item.isRedApple()) { // 判断是否为红苹果 redAppleList.add(item); } } } } System.out.println("常规的For循环校验以后的红苹果清单=" + redAppleList.toString()); }
从以上代码可见,对每一个字段依次进行空指针判断,这里的条件语句拖得老长。假若给苹果类新增一个字段,那么此处的条件语句还得补上新字段的非空校验。即便采用Java8引入的可选器Optional,也没有更好的办法,如此窘境简直叫人一筹莫展。
现在有了注解技术,号称能够自动检查代码,总算出现解决问题的一缕曙光。具体的处理过程大体分为四个步骤:自定义新的非空注解、给非空字段添加非空注解、利用反射机制校验被非空注解修饰了的全部字段、在业务须要的地方调用校验方法,下面分别进行详细描述。java
一、自定义新的非空注解
首先定义一个名叫“NotNull”的注解,并规定它用于在程序运行过程当中检查字段是否为空。这里有两点值得特别关注:第一点,该注解的生效期间位于程序运行过程中,意味着须要将它保留至运行阶段;第二点,该注解用于检查字段是否为空,意味着它的做用目标正好是字段。据此可编写以下所示的注解定义代码:程序员
import java.lang.annotation.*; @Documented // 该注解归入到Java开发手册 @Target({ ElementType.FIELD }) // 该注解的做用目标是字段(属性) @Retention(RetentionPolicy.RUNTIME) // 该注解保留至运行阶段,这样可以经过反射机制调用 //定义了一个注解,在interface前面加上符号“@”,表示这是个注解 public @interface NotNull {}
二、给非空字段添加非空注解
接着修改苹果类的定义代码,在每一个不能为空的字段上方添加注解“@NotNull”,表示这是个特殊字段,它必须有值而不容许是空指针,简而言之,该字段必须是非空字段。修改后的苹果类代码片断示例以下:app
//定义一个苹果类 public class Apple { @NotNull // 经过注解声明该字段不可为空 private String name; // 名称 @NotNull // 经过注解声明该字段不可为空 private String color; // 颜色 @NotNull // 经过注解声明该字段不可为空 private Double weight; // 重量 @NotNull // 经过注解声明该字段不可为空 private Double price; // 价格 // 此处省略苹果类的剩余代码定义 }
三、利用反射机制校验被非空注解修饰了的全部字段
而后还要经过反射技术去检查非空字段,这里才是整个流程的关键之处。在进行反射调用的时候,又可分为主要的三个步骤:首先调用Class对象的getDeclaredFields方法,得到该类中声明的全部字段;其次依次遍历这些字段,并调用字段对象的isAnnotationPresent方法,判断当前字段是否存在非空注解;再次,假若存在非空注解,则调用字段对象的get方法,得到对应的字段值并判断该字段是否为空指针。如此一来,某个添加了非空注解的字段,要是它的字段值被检查出为空指针,立刻就能判定包含该字段的对象是个无效记录。
按照如上所述的反射调用步骤,编写而来的非空校验代码以下所示:框架
//演示如何利用注解进行字段为空的校验 public class NullCheck { // 对指定对象进行空指针校验。返回true表示该对象跟它的每一个字段都非空,返回false表示对象为空或者至少一个字段为空 public static boolean isValid(Object obj) { if (obj == null) { System.out.println("校验对象为空"); return false; } Class cls = obj.getClass(); // 得到对象实例的基因类型 // 声明一个字符串清单,用来保存非空校验失败的无效字段名称 List<String> invalidList = new ArrayList<String>(); try { // 获取对象的全部属性(若是使用getFields,就没法获取到private的属性) Field[] fields = cls.getDeclaredFields(); for (Field field : fields) { // 依次遍历每一个对象属性 // 若是该属性声明了NotNull注解,就进行字段非空的校验 if (field.isAnnotationPresent(NotNull.class)) { if (field != null) { field.setAccessible(true); // 将该字段设置为容许访问 Object value = field.get(obj); // 获取某实例的字段值 if (value == null) { // 若是发现该字段为空 // 就把该字段的名称添加到无效清单中 invalidList.add(field.getName()); } } } } } catch (Exception e) { // 捕捉到了任何一种异常(错误除外) e.printStackTrace(); } if (invalidList.size() > 0) { // 无效清单非空,表示至少有一个字段没经过非空校验 String desc = String.format("%s类非空校验不经过的字段有:%s", cls.getName(), invalidList.toString()); System.out.println(desc); return false; } else { return true; } } }
为了方便程序员寻找非法字段,上面的代码特地将未经过非空校验的全部字段都打印出来,比起普通的空指针判断要智能许多。优化
下面来个简单的例子,验证一下加了注解的非空校验是否正常运行。实验用的苹果对象除了名称字段有值,其他三个字段均为null,完整的实验代码见下:编码
// 经过注解检查某个对象内部字段的空指针 private static void testSingle() { Apple apple = new Apple("苹果", null, null, null); // NullCheck的isValid方法经过注解与反射技术来校验空指针 boolean isValid = NullCheck.isValid(apple); System.out.println("apple isValid="+isValid); }
运行以上的实验代码,观察到如下的日志信息,果真找到了三个空指针字段:spa
com.addition.annotation.Apple类非空校验不经过的字段有:[color, weight, price]
四、在业务须要的地方调用校验方法
最后把原来for循环那条冗长的空指针判断语句改成调用新的校验方法,改写后的红苹果挑选代码变成了这样:指针
// 把for循环内部的空指针校验改成经过注解校验 private static void getRedAppleByForWithNullCheck(List<Apple> list) { List<Apple> redAppleList = new ArrayList<Apple>(); if (list != null) { // 判断清单非空 for (Apple item : list) { // NullCheck的isValid方法经过注解与反射技术来校验空指针 if (NullCheck.isValid(item)) { if (item.isRedApple()) { // 判断是否为红苹果 redAppleList.add(item); } } } } System.out.println("For循环,非空校验以后的红苹果清单=" + redAppleList.toString()); }
瞧瞧,本来长长的一条if语句,如今缩短为“if (NullCheck.isValid(item))”,看上去真是清爽宜人。更加剧要的是,假如之后苹果类增长了新的非空字段,那也只需修改苹果类的代码,没必要修改此处的校验代码了。
不但采起for循环的处理代码得以优化,并且采起流式处理的新式代码派上用场,不过是挑选非空校验经过的正常苹果么,只要在原代码中补充形如“.filter(NullCheck::isValid)”的过滤方法就好了,补充过滤以后的流式代码示例以下:日志
// 联合运用Optional校验、流式处理,以及注解校验 private static void getRedAppleByStreamWithNullCheck(List<Apple> list) { List<Apple> redAppleList = new ArrayList<Apple>(); // ifPresent表示list非空时候的处理 Optional.ofNullable(list).ifPresent(apples -> { // 从原始清单中筛选出红苹果清单。注意“NullCheck::isValid”为静态方法引用的写法 redAppleList.addAll(apples.stream().filter(NullCheck::isValid).filter(Apple::isRedApple).collect(Collectors.toList())); }); System.out.println("流式处理,非空校验以后的红苹果清单=" + redAppleList.toString()); }
乖乖,改进以后的流式代码不得了了,短短几行代码居然同时运用了多项黑科技,包括但不限于:可选器、Lambda表达式、流式处理、方法引用、反射技术、注解技术。要是能熟练掌握这些开发技能,想必你的Java编码水准已经达到了至关的高度。
更多Java技术文章参见《Java开发笔记(序)章节目录》