注解属于比较高级的Java开发技术,前面介绍的内置注解专用于编译器检查代码,另外一些注解则由各大框架定义与调用,像Web开发常见的Spring框架、Mybatis框架,Android开发常见的ButterKnife框架等等,都使用了大量的注解。为了更好地弄清注解的应用原理,接下来不妨尝试自定义注解,并在实际开发中对自定义的注解加以运用。
之前介绍异常预防的时候,为了避免出现空指针异常,可谓是八仙过海各显神通,一路试验了多项新技术。其中校验某个字段非空尤其是个难点,案例中的苹果类共有四个字段,包括名称、颜色、重量、价格等,倘若要求这些字段均非空值才算有效记录的话,就得四个字段一一判断过去。那么采取for循环进行非空检查的常规代码示例如下:

	// 常规的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,也没有更好的办法,如此窘境简直叫人束手无策。
如今有了注解技术,号称可以自动检查代码,总算出现解决问题的一缕曙光。具体的处理过程大致分为四个步骤:自定义新的非空注解、给非空字段添加非空注解、利用反射机制校验被非空注解修饰了的所有字段、在业务需要的地方调用校验方法,下面分别进行详细描述。

1、自定义新的非空注解
首先定义一个名叫“NotNull”的注解,并规定它用于在程序运行过程中检查字段是否为空。这里有两点值得特别关注:第一点,该注解的生效期间位于程序运行过程当中,意味着需要将它保留至运行阶段;第二点,该注解用于检查字段是否为空,意味着它的作用目标正好是字段。据此可编写如下所示的注解定义代码:

import java.lang.annotation.*;

@Documented // 该注解纳入到Java开发手册
@Target({ ElementType.FIELD }) // 该注解的作用目标是字段(属性)
@Retention(RetentionPolicy.RUNTIME) // 该注解保留至运行阶段,这样能够通过反射机制调用
//定义了一个注解,在interface前面加上符号“@”,表示这是个注解
public @interface NotNull {}

  

2、给非空字段添加非空注解
接着修改苹果类的定义代码,在每个不能为空的字段上方添加注解“@NotNull”,表示这是个特殊字段,它必须有值而不允许是空指针,简而言之,该字段必须是非空字段。修改后的苹果类代码片段示例如下:

//定义一个苹果类
public class Apple {
@NotNull // 通过注解声明该字段不可为空
private String name; // 名称
@NotNull // 通过注解声明该字段不可为空
private String color; // 颜色
@NotNull // 通过注解声明该字段不可为空
private Double weight; // 重量
@NotNull // 通过注解声明该字段不可为空
private Double price; // 价格 // 此处省略苹果类的剩余代码定义
}

  

3、利用反射机制校验被非空注解修饰了的所有字段
然后还要通过反射技术去检查非空字段,这里才是整个流程的关键之处。在进行反射调用的时候,又可分为主要的三个步骤:首先调用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);
}

运行以上的实验代码,观察到以下的日志信息,果然找到了三个空指针字段:

com.addition.annotation.Apple类非空校验不通过的字段有:[color, weight, price]

  

4、在业务需要的地方调用校验方法
最后把原来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开发笔记(序)章节目录

Java开发笔记(八十三)利用注解技术检查空指针的更多相关文章

  1. Java开发笔记(十三)利用关系运算符比较大小

    前面在<Java开发笔记(九)赋值运算符及其演化>中提到,Java编程中的等号“=”表示赋值操作,并非数学上的等式涵义.Java通过等式符号“==”表示左右两边相等,对应数学的等号“=”: ...

  2. Java开发笔记(序)章节目录

    现将本博客的Java学习文章整理成以下笔记目录,方便查阅. 第一章 初识JavaJava开发笔记(一)第一个Java程序Java开发笔记(二)Java工程的帝国区划Java开发笔记(三)Java帝国的 ...

  3. Java开发笔记(八十二)注解的基本单元——元注解

    Java的注解非但是一种标记,还是一种特殊的类型,并且拥有专门的类型定义.前面介绍的五种内置注解,都可以找到对应的类型定义代码,例如查看注解@Override的源码,发现它的代码定义是下面这样的: @ ...

  4. Java开发笔记(八十一)如何使用系统自带的注解

    之前介绍继承的时候,提到对于子类而言,父类的普通方法可以重写也可以不重写,但是父类的抽象方法是必须重写的,如果不重写,编译器就直接在子类名称那里显示红叉报错.例如,以前演示抽象类用法之时,曾经把Chi ...

  5. Java开发笔记(三十八)利用正则表达式校验字符串

    前面多次提到了正则串.正则表达式,那么正则表达式究竟是符合什么定义的字符串呢?正则表达式是编程语言处理字符串格式的一种逻辑式子,它利用若干保留字符定义了形形色色的匹配规则,从而通过一个式子来覆盖满足了 ...

  6. Java开发笔记(八十)利用反射技术操作私有方法

    前面介绍了如何利用反射技术读写私有属性,不单是私有属性,就连私有方法也能通过反射技术来调用.为了演示反射的逆天功能,首先给Chicken鸡类增加下列几个私有方法,简单起见弄来了set***/get** ...

  7. Java开发笔记(九十八)利用Callable启动线程

    前面介绍了如何利用Runnable接口构建线程任务,该方式确实方便了线程代码的复用与共享,然而Runnable不像公共方法那样有返回值,也就无法将线程代码的处理结果传给外部,造成外部既不知晓该线程是否 ...

  8. Java开发笔记(八十九)缓存字节I/O流

    文件输出流FileOutputStream跟FileWriter同样有个毛病,每次调用write方法都会直接写到磁盘,使得频繁的写操作性能极其低下.正如FileWriter搭上了缓存兄弟Buffere ...

  9. Java开发笔记(八十八)文件字节I/O流

    前面介绍了如何使用字符流读写文件,并指出字符流工具的处理局限,进而给出随机文件工具加以改进.随机文件工具除了支持访问文件内部的任意位置,更关键的一点是通过字节数组读写文件数据,采取字节方式比起字符方式 ...

随机推荐

  1. Spring Boot 2.0 图文教程 | 集成邮件发送功能

    文章首发自个人微信公众号: 小哈学Java 个人网站: https://www.exception.site/springboot/spring-boots-send-mail 大家好,后续会间断地奉 ...

  2. HTTP传输编码增加了传输量,只为解决这一个问题 | 实用 HTTP

    题图:by @Olga Hi,大家好,我是承香墨影! HTTP 协议在网络知识中占据了重要的地位,HTTP 协议最基础的就是请求和响应的报文,而报文又是由报文头(Header)和实体组成.大多数 HT ...

  3. c# 多种方法调整屏幕亮度

    Github: https://github.com/CHNMaxGor/AjustScreenBrightness 方法一: 使用网上常说的 Gdi32.dll 下的 SetDeviceGammaR ...

  4. php7连接mysql测试代码

    php7连接mysql测试代码 <?php $mysqli = new mysqli("localhost", "root", "passwor ...

  5. Microsoft Edge浏览器下载文件乱码修复方法(二)

    之前有写过"Microsoft Edge浏览器下载文件乱码修复方法",发现很多情况下下载文件乱码问题还是存在,这里对之前内容做简单补充,希望可以帮到大家. 方法二: 默认如果提示下 ...

  6. Windows -- cmd命令: ipconfig 和 nbtstat

    1. ipconfig 命令格式及参数如下: 2. nbtstat 命令格式及参数如下:

  7. ReactNative之参照具体示例来看RN中的FlexBox布局

    今天是重阳节,祝大家节日快乐,今天继续更新RN相关的博客.上篇博客<ReactNative之从HelloWorld中看环境搭建.组件封装.Props及State>中我们通过一个HelloW ...

  8. 解决MUI阻止a标签默认跳转事件—方法总结

    用过mui的小伙伴们一定不会陌生,有时候真的很烦mui本身会阻止a标签默认跳转.一般只要用了mui的ui组件,比如头部,底部或者弹框,你就不能在用a标签进行跳转了. 注:项目中引用了mui后,可能也会 ...

  9. mysql id从n 开始

    mysql 全部删除数据后设置 id从1开始: truncate table table_name mysql  删除部分数据后设置 id从n开始 ALTER TABLE user auto_incr ...

  10. 面向对象(__item__)

    #Author : Kelvin #Date : 2019/1/20 21:37 class People: def __getitem__(self, item): print("geti ...