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 的字段要格外注意,要关注数据的边界情况,自测不要只考虑有值;
- 写完代码多做自我检视,本次在提测前就发现了问题,靠的就是自我检视代码,要养成好习惯;