Comparator.comparing()与空指针异常

本文讲述了将MySQL逻辑迁移到Java代码时遇到的问题,涉及到对日期字段排序时的空指针异常,通过分析原因并引入`Comparator.nullsFirst()`解决,强调在处理Java比较器时要考虑数据边界和null值的处理.
摘要由CSDN通过智能技术生成

1. 问题背景

因内部一些原因,需要将在 mysql 上实现的逻辑迁移到 Java 代码中来。逻辑如下:

1.按照一个整型字段分组
2.分组内的数据按照一个日期字段排序
3.每个分组取时间最晚的一条数据
#问题就出现在 Java 代码中的排序上

问题代码 demo

/**
 * @author jk
 * @date 2024/4/15
 * @Description 描述:比较器排序异常记录
 */
public class ComparatorSortExceptionDemo {
    public static void main(String[] args) {
    	// dateObjList 正常从DB查询得到,这里手动添加演示
        List<DateObj> dateObjList = new ArrayList<>();
        dateObjList.add(new DateObj(new Date()));
        dateObjList.add(new DateObj(null));

        dateObjList.sort(Comparator.comparing(DateObj::getDate));
        DateObj lastDate = dateObjList.get(dateObjList.size() - 1);
    }

    @AllArgsConstructor
    @Getter
    static class DateObj {
        private Date date;
    }
}

空指针异常
空指针异常

2. 原因分析

比较器在进行两个元素的排序比较时{即日期 Date 对象比较},未做 null 值的判断和处理。以下是比较器的源码:

// JDK 1.8  java.util.Comparator
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor)
{
   Objects.requireNonNull(keyExtractor);
   // keyExtractor.apply(c1),当 c1 == null 时,c1.getDate() 就报空指针了
   return (Comparator<T> & Serializable)
       (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

3. 解决

在 mysql5.7 中,null 值参与 datetime 类型字段排序时会被认为是最小值。为了与 mysql 的逻辑统一,Java 代码中我们将比较器也设置为 null 值最小。修改后的代码 Demo 如下:

// 省略 DateObj 类型的声明
public class ComparatorSortExceptionDemo {
    public static void main(String[] args) {
        // dateObjList 正常从DB查询得到
        List<DateObj> dateObjList = new ArrayList<>();
        dateObjList.add(new DateObj(new Date(System.currentTimeMillis() + 5000)));
        dateObjList.add(new DateObj(null));
        dateObjList.add(new DateObj(new Date(System.currentTimeMillis() - 5000)));
        
        // 多层包装的比较器,首先会经过 NullComparator,然后是 NaturalOrderComparator。
        // NullComparator 先做处理,就避免了空指针的问题。
        // NaturalOrderComparator 则做的是真正的日期比较 Date#compareTo
        dateObjList.sort(Comparator.comparing(DateObj::getDate, Comparator.nullsFirst(Comparator.naturalOrder())));
        dateObjList.forEach(obj -> {
            System.out.println(obj.getDate());
        });
    }
}

源码分析:

  • Comparator.nullsFirst(comparator)
// java.util.Comparators
final static class NullComparator<T> implements Comparator<T>, Serializable {
    private static final long serialVersionUID = -7569533591570686392L;
    private final boolean nullFirst;
    private final Comparator<T> real;
	// real 就是真正做排序的比较器,NullComparator 只是为了我们的特殊值即 null 的处理
    @SuppressWarnings("unchecked")
    NullComparator(boolean nullFirst, Comparator<? super T> real) {
        this.nullFirst = nullFirst;
        this.real = (Comparator<T>) real;
    }

	// 逻辑清晰,就是先做了 null 值的判断,最后两个值都不为 null 时才调用真正的比较器
    @Override
    public int compare(T a, T b) {
        if (a == null) {
            return (b == null) ? 0 : (nullFirst ? -1 : 1);
        } else if (b == null) {
            return nullFirst ? 1: -1;
        } else {
            return (real == null) ? 0 : real.compare(a, b);
        }
    }
    // ...
}
  • Comparator.naturalOrder()
// java.util.Comparator
public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
    return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
}

// java.util.Comparators
// 可以看到是声明了一个枚举单例,重点是 compare 方法
enum NaturalOrderComparator implements Comparator<Comparable<Object>> {
   INSTANCE;
   // 这里就是我们想要的原始排序,执行到这里就是 date1.compareTo(date2)
   @Override
   public int compare(Comparable<Object> c1, Comparable<Object> c2) {
       return c1.compareTo(c2);
   }
   // ...
}

4. 总结

  • 简单的代码可能会有致命的问题,不能因为简单而轻视;
  • Java 使用比较器时,一定要确定数据内容是否有 null 值,是否要做 null 处理;
  • 对于数据库中允许为 null 的字段要格外注意,要关注数据的边界情况,自测不要只考虑有值;
  • 写完代码多做自我检视,本次在提测前就发现了问题,靠的就是自我检视代码,要养成好习惯;
  • 10
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值