开发小技巧系列 - 如何避免NullPointerException?(二)

简介: NPE问题处理之二,引入了optional来处理,还有空对象

开发小技巧系列文章,是本人对过往平台系统的设计开发及踩坑的记录与总结,给初入平台系统开发的开发人员提供参考与帮助。

先来回答上篇文章中“思考”的2个问题。
  • 1、 这段程序为什么不用“==”号了? “==”号在数字较大时会有什么问题?

答:在对象类型中,使用“==”时,一般是比较的地址,而不是具体的值,当然Integer(int)有点除外,它会将-128~127的值缓存起来,在这个范围内,使用“==”进行比较,是没有问题,但是超过这个范围,使用“==”号,就会返回不是预期的效果。因此,在程序中,尽量使用equals,来避免埋下的坑。(建议使用Objects.equals),这样可以防止xx.equals(...)表达式中的 xx 为NPE的情况。

  • 2、 @NotNull 有什么作用?

答:这个@NotNull起到修饰说明的作用,提醒使用者,注意传入的参数值,对程序运行不起作用。悬浮时会有如下的提示:
image.png

案例二:

在开发的过程中,不可避免地需要从对象中获取属性的值,比如order.orderNo(订单对象.订单号),那这时相信很多小伙伴就在头疼了,order对象到底会不会为null呢? 想到头昏脑涨,最后还是把 null != order加上,比如下面的代码:
MemberService.java

/**
     * 传统的定法,就是先判断对象是否为null,不为null,
     * 则进行转换操作,否则,返回null
     * @param member
     *      输入的会员对象
     * @return
     */
    public MemberDTO transfer(Member member){
   
   
        if(null != member){
   
   
            MemberDTO memberDTO = new MemberDTO()
                    .setMemberId(member.getMemberId()) 
                   .setGender(member.getGender())
                    .setGenderName(ProgrammerA.getGender2(member.getGender()))
                    .setNickName(member.getNickName())
                    .setRealName(member.getRealName());
            return memberDTO;
        }
        return null;
    }

这只是一个小例子,当程序中需要对多种业务对象进行操作时,肯定会有一堆这样的判断。那么有什么更好的解决办法呢?

在JDK8 中有一个新的特性:Optional, 是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。使用它,可以让代码更加简单,可读性跟高,代码写起来更高效。

结合JDK8的 function,可以设计一个对象互转的模板方式,来应从对象A转到对象B,模板方法通过接收一个function来实现个性化的方法,由具体的调用方来提供,比如下面的例子,有会员,部门的对象的转换。

OptionalUtils.java

/**
     * 属性互转模板方法
     * @param source
     *  原对象(需要比较的对象)
     * @param function
     *  对象E->R的赋值过程
     * @param defaultObject
     *  默认值
     * @param <E>
     *      原始对象
     * @param <R>
     *      结果对象
     * @return
     */
    public static <E, R> R transfer(E source, Function<E, R> function, R defaultObject){
   
   
        Optional<E> optional = Optional.ofNullable(source);
        if(Optional.ofNullable(source).isPresent()){
   
   
            return function.apply(optional.get());
        }
        return defaultObject;
    }

然后编写如下代码:
MemberService.java

/**
     * 通过Optional的方式来处理转换,减少编写 xx != null 这样的表达式
     * @param member
     *      输入的会员信息
     * @param defaultDto
     *      默认的值(如果为null时)
     * @return
     */
    public MemberDTO transferByOptional(Member member, MemberDTO defaultDto){
   
   
        return OptionalUtils.transfer(member, MemberService::convert, defaultDto);
    }    /**
     * 将member的属性 赋值给memberDTO
     * @param member
     *      输入的会员信息
     * @return
     */
    public static MemberDTO convert(Member member){
   
   
        MemberDTO memberDTO = new MemberDTO()
                 .setMemberId(member.getMemberId()) 
                 .setGender(member.getGender())
                 .setGenderName(ProgrammerA.getGender2(member.getGender()))
                .setNickName(member.getNickName())
                .setRealName(member.getRealName());
        return memberDTO;
    }
    /**
     * 将部门对象转换成DTO
     * @param dept
     * @return
     */
    public static DeptDTO cover(Dept dept){
   
   
        DeptDTO deptDTO = new DeptDTO();
        deptDTO.setDeptId(dept.getDeptId()); 
        deptDTO.setParentId(dept.getParentId());
        deptDTO.setDeptName(dept.getDeptName());
        return deptDTO;
    }

编写测试用例:
OptionalTest.java

/**
     * 正常情况下的调用测试(非null)
     */
    @Test
    public void optionalTest(){
   
   
        Member member = new Member();
        member.setId(1);
        member.setMemberId(1000);
        member.setNickName("测试");
        member.setGender(1);
        MemberDTO memberDTO = memberService.transferByOptional(member, null);
        log.debug("MemberDTO: {}", memberDTO);
        //声明一个部门
        Dept dept = new Dept();
        dept.setId(1);
        dept.setDeptId(100);
        dept.setParentId(0);
        dept.setDeptName("部门");        DeptDTO deptDTO = OptionalUtils.transfer(dept, MemberService::cover, null);
    }
    //程序输出结果:
    // [main] DEBUG net.jhelp.demo.OptionalTest - MemberDTO(memberId=1000, nickName=测试, realName=null, gender=1, genderName=男)
    // [main] DEBUG net.jhelp.demo.OptionalTest - deptDTO : DeptDTO(deptId=100, parentId=0, deptName=部门)
      /**
       *  测试传入对象是null的情况
      */
    @Test
    public void optionalWithNullTest(){
   
   
        MemberDTO memberDTO = memberService.transferByOptional(null, null);
        log.debug("optionalWithNullTest:{}", memberDTO);
    }
    //输出结果:
    //DEBUG net.jhelp.demo.OptionalTest - optionalWithNullTest:null

使用Optional的特性,可以减少编写众多null != obj来防止NPE的问题,通过“模板方法”的方式,可以减少更多的重复代码的编写。上面的“模板方法”的类,小伙伴可以拿到项目中直接使用,只需要编写一个赋值的方法就可以。

案例三:

上面的场景是用于对象和对象之间互转,但有时候在开发的过程中,只需要获取对象中的某个属性的值,可能程序中的不同业务/方法,需要用到不同属性的值。可能都需要编写类似如下的代码:

//以上面订单为例//想获取订单的orderNo(订单号)
if(null != order){
   
   
  return order.getOrderNo();
}//另外的方法想获取金额
if(null != order){
   
   
  return order.getAmount();
}//获取订单上的买家名称
if(null != order){
   
   
  return order.getBuyerName();
}//可能还会有其他的需求,获取不同的值

这种现象相信在程序中是无处不在的,有什么办法来简化这个过程,减少if(null != obj)这样的代码的编写呢,答案是有的,可以使用Optional来编写一个模板方法。
OptionalUtils.java

/**
     * 获取对象的属性(带判断null)
     * @param source
     *  原对象(需要比较的对象)
     * @param supplier
     *  工厂方法
     * @param defaultObject
     *  默认值
     * @param <E>
     *     输入的对象
     * @param <R>
     *     输出的值
     * @return
     */
    public static <E, R> R getAttr(E source, Supplier<R> supplier, R defaultObject) {
   
   
        if(Optional.ofNullable(source).isPresent()) {
   
   
            return supplier.get();
        }else{
   
   
            return defaultObject;
        }
    }

这里用到了JDK1.8中提供的Supplier,这个相当于是一个工厂的方法,与function不同的是它不接受参数,直接为我们生产指定的结果,有点像生产者模式。Supplier 接口可以理解为一个容器,用于装数据的,Supplier 接口有一个 get 方法,可以返回值。

来编写一个测试用例,看下上面模板方法的效果。
OptionalTest.java

    /**
     * 测试从对象中获取某个值
     */
    @Test
    public void propertiesGetTest(){
   
   
        Member member = new Member();
        member.setId(1);
        member.setMemberId(1000);
        member.setNickName("测试");
        member.setGender(1);
        member.setRealName("java");
        String attr = OptionalUtils.getAttr(member, ()-> member.getNickName(), null);
        log.debug("nickName: {} ", attr);        String realName = OptionalUtils.getAttr(member, ()-> member.getRealName(), null);
        log.debug("realName: {} ", realName);        Integer memberId = OptionalUtils.getAttr(member, ()-> member.getMemberId(), null);
        log.debug("memberId: {} ", memberId);
    }

执行的结果

11:12:14.156 [main] DEBUG net.jhelp.demo.OptionalTest - nickName: 测试 11:12:14.171 [main] DEBUG net.jhelp.demo.OptionalTest - realName: java 11:12:14.172 [main] DEBUG net.jhelp.demo.OptionalTest - memberId: 1000

从测试的结果上看,完全能满足预期的效果,来测试下,如果对象是Null时情况,代码如下:

    /**
     * 测试从对象中获取某个值(对象是NULL)
     */
    @Test
    public void propertiesGetWithNullTest(){
   
   
        Member member = null;        String attr = OptionalUtils.getAttr(member, ()-> member.getNickName(), null);
        log.debug("nickName: {} ", attr);        String realName = OptionalUtils.getAttr(member, ()-> member.getRealName(), null);
        log.debug("realName: {} ", realName);        Integer memberId = OptionalUtils.getAttr(member, ()-> member.getMemberId(), null);
        log.debug("memberId: {} ", memberId);
    }

运行结果(没有见到异常信息)

11:15:18.027 [main] DEBUG net.jhelp.demo.OptionalTest - nickName: null 11:15:18.041 [main] DEBUG net.jhelp.demo.OptionalTest - realName: null 11:15:18.042 [main] DEBUG net.jhelp.demo.OptionalTest - memberId: null

上面的案例二,案例三的处理方法,是可以很好的防止及减少重复代码的编写,但是如果对象就是NULL,那返回的结果还是为NULL,下游的开发人员来是要来处理这个NULL。其实,也可以通过引入一个"默认对象(值)"的概念。可以采用以下规则:

  • 对象是NULL的,可以返回一个“空对象(EmtpyObject)”;
  • 值为空的,可以返回默认值,比如空字符串(""), 数字类型的(0);

空对象

可以在DTO对象上,定义一个默认空对象,然后在程序中,返回这个空对象,那下游的开发人员,就不用去担心NULL,不用在加上 null != obj 这样的判断。还是以上面的“会员DTO”为例,来看下怎么写:
MemberDTO.java

   /**
     * 定义一个空的对象
     */
    public static final MemberDTO EMPTY_MEMBER = new MemberDTO();

这样,就拥有一个“空对象”,在后台数据没有对应的“会员”时,就可以返回这个“空对象”,下游的开发人员就不会因为调用

member.getNickName(); //

而报空指(NPE)的错误。
但是这样又会有一个新的问题,下游的开发人员,怎么知道方法返回的对象是“正常对象”,还是“空对象”呢?当然,从业务的角度来说,对象肯定有它的唯一属性,可以通过判断它的值,来确认是不是“空对象”。比如会员的唯一属性是“会员ID”,可以通过它来判断是否是“空对象”。

if(Objects.isNull(member.getMemberId())){
   
   
  //这是一个空对象  //看上去,是不是又回到原来的判断NULL去b
}

但是从这个代码上,感觉又陷入的死胡同里,方法返回了空对象,下游开发人员又在又要去针对对象的特定属性进行判断(null != obj)。那么有没有更好的办法呢?来看下面的代码
EmptyDTO.java

@Data
public abstract class EmptyDTO {
   
   
    /**
    *  标志对象是否为空,默认是“非空”对象。
     */
    private Boolean empty = false;    public EmptyDTO(){
   
   }    public EmptyDTO(Boolean empty){
   
   
        this.empty = empty;
    }
}

然后对原来MemberDTO类进行调整,继续EmptyDTO,添加一个构造方法,变成MemberDTO2.Java

/**
     * 带参数的构造函数(是否空)
     * @param empty
     */
    public MemberDTO2(Boolean empty){
   
   
        setEmpty(empty);
    }

    /**
     * 定义一个空的对象(通过 isEmpty来判断是否为空)
     */
    public static final MemberDTO2 EMPTY_MEMBER = new MemberDTO2(true);

从上面的代码看,添加了一个属性,来标准当前的对象,是否是“空对象”,减少下游开发人员的烦脑,只要需调用一下isEmpty()方法就可以了,是不是问题就变得简单多了。

/**
     *  测试传入对象是null的情况
     */
    @Test
    public void optionalWithNullTest2(){
   
   
        MemberDTO2 memberDTO = memberService.transferByOptional2(null, MemberDTO2.EMPTY_MEMBER);
        log.debug("optionalWithNullTest2:{}", memberDTO);
        log.debug("是否是空对象:{}", memberDTO.isEmpty());
    }
11:00:05.242 [main] DEBUG net.jhelp.demo.OptionalTest - optionalWithNullTest2:MemberDTO2(memberId=null, nickName=null, realName=null, gender=null, genderName=null)11:00:05.247 [main] DEBUG net.jhelp.demo.OptionalTest - 是否是空对象:true

总结一下

本篇主要是对对象是否为NULL(NULL!=obj)这情况,及对象转换成另外的对象的过程进行总结,通过JDK1.8 提供的新特性,来解决程序中大量的判断语句,让程序更简洁清晰;另外一个引入一个“空对象”的概念,来更好的解决程序与程序调过程中的引藏的NULL。

    1. 对象向对角之间赋值的NULL检查;
    1. 从对象中获取属性时NULL的检查;
    1. 运用Optional, function 的新特殊,模板方法。
    1. 空对象的引入,减少NPE的出现。
  • 如果想要上面的代码,可以访问此仓库。
    gitee.com/TianXiaoSe_…

题外话,可能在开发过程中,经常需要去判断属性是否有值,没值要给个默认值,比如如下的代码,基本上是三元表过式(这是比较的方法,而大部分初入开发,可能都是if(...){}else{}这样的结构),这种有没有更好的解决方式呢?

//假设有一个销售数据的对象(里面有订单金额,订单量,交易金额,成交商品数,成交客户数,客单值等
//需木给前端返回值对象(如是NULL,则返回0)
//可能的代码会是如下:
//销售数据对象
SellDataInfo sellDataInfo = ...;
//返回给前端的
DTORevenueIndicatorDTO dto = new RevenueIndicatorDTO();
dto.setOrderCount(sellDataInfo.getOrderCount() != null ? sellDataInfo.getOrderCount() : 0);
dto.setGmvAmount(sellDataInfo.getGmvAmount() != null ? sellDataInfo.getGmvAmount() : 0);
dto.setBuyerCount(sellDataInfo.getBuyerCount() != null ? sellDataInfo.getBuyerCount() : 0);....

开发小技巧系列文章:
1、开发小技巧系列 - 库存超卖,库存扣成负数?
2、开发小技巧系列 - 重复生成订单
3、开发小技巧系统 - Java实现树形结构的方式有那些?
4、开发小技巧系列 - 如何避免NullPointerException?(一)

目录
相关文章
|
6月前
|
Java 程序员 测试技术
我有一个朋友写出了17种触发NPE的代码!避免这些坑
我有一个朋友,写代码的时候常常遭到NPE背刺,痛定思痛,总结了NPE出没的17个场景,哪一个你还没有遇到过?
|
Java 编译器 程序员
【JavaSE专栏67】谈谈异常的那些事,学会预判异常、捕获异常、转移异常
【JavaSE专栏67】谈谈异常的那些事,学会预判异常、捕获异常、转移异常
【JavaSE专栏67】谈谈异常的那些事,学会预判异常、捕获异常、转移异常
|
3月前
|
Java UED 开发者
Java异常处理新玩法:throw关键字,你的错误管理利器!
Java异常处理新玩法:throw关键字,你的错误管理利器!
35 1
|
3月前
|
Java API
JDK版本特性问题之使用 ofNullable 方法来预防 NullPointerException,如何解决
JDK版本特性问题之使用 ofNullable 方法来预防 NullPointerException,如何解决
|
5月前
|
Java UED 开发者
【技术解密】Java异常处理新玩法:throw关键字,你的错误管理利器!
【6月更文挑战第19天】Java异常处理关键在于`throw`,它用于主动抛出异常,确保程序健壮性。例如,当年龄验证失败时,`IllegalArgumentException`被`throw`,提供错误详情。自定义异常如`CustomException`能增强错误信息。此外,通过构建异常链,如在`DataProcessingException`中嵌套`IOException`,保持原始堆栈信息,提供更全面的错误上下文。掌握`throw`能提升错误管理,打造稳定软件。
49 5
|
6月前
|
存储 Java 开发者
探索Java开发中触发空指针异常的场景
作为一名后端开发者在Java编程的世界中,想必大家对空指针并不陌生,空指针异常是一种常见而又令人头疼的问题,它可能会在我们最不经意的时候突然出现,给我们的代码带来困扰,甚至导致系统的不稳定性,而且最可怕的是有时候不能及时定位到它的具体位置。针对这个问题,我们需要深入了解触发空指针异常的代码场景,并寻找有效的方法来识别和处理这些异常情况,而且我觉得空指针异常是每个Java开发者都可能面临的挑战,但只要我们深入了解它的触发场景,并采取适当的预防和处理措施,我们就能够更好地应对这个问题。那么本文就来分享一下实际开发中一些常见的触发空指针异常的代码场景,并分享如何有效地识别和处理这些异常情况。
100 1
探索Java开发中触发空指针异常的场景
项目中常见NPE空指针异常
项目中常见NPE空指针异常
|
6月前
|
安全 Java 程序员
Dating Java8系列之巧用Optional之优雅规避NPE问题
Dating Java8系列之巧用Optional之优雅规避NPE问题
58 0
|
SQL 前端开发 Java
开发小技巧系列 - 如何避免NullPointerException?(一)
NPE是JAVA界面的常见问题,如何来避免呢?
100 0
|
Java 测试技术 API
开发小技巧系列 - 如何避免NPE,去掉if...else(四)
利用optional来处理各种IF-ELSE的判断
107 0