使用 Lambda 表达式实现超强的排序功能!

共 6433字,需浏览 13分钟

 ·

2021-12-17 15:46

我们在系统开发过程中,对数据排序是很常见的场景。一般来说,我们可以采用两种方式:

  1. 借助存储系统(SQL、NoSQL、NewSQL 都支持)的排序功能,查询的结果即是排好序的结果
  2. 查询结果为无序数据,在内存中排序。

今天要说的是第二种排序方式,在内存中实现数据排序。

首先,我们定义一个基础类,后面我们将根据这个基础类演示如何在内存中排序。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
    private String name;
    private int age;

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

基于Comparator排序

在 Java8 之前,我们都是通过实现Comparator接口完成排序,比如:

new Comparator() {
    @Override
    public int compare(Student h1, Student h2) {
        return h1.getName().compareTo(h2.getName());
    }
};

这里展示的是匿名内部类的定义,如果是通用的对比逻辑,可以直接定义一个实现类。使用起来也比较简单,如下就是应用:

@Test
void baseSortedOrigin() {
    final List students = Lists.newArrayList(
            new Student("Tom"10),
            new Student("Jerry"12)
    );
    Collections.sort(students, new Comparator() {
        @Override
        public int compare(Student h1, Student h2) {
            return h1.getName().compareTo(h2.getName());
        }
    });
    Assertions.assertEquals(students.get(0), new Student("Jerry"12));
}

这里使用了 Junit5 实现单元测试,用来验证逻辑非常适合。

因为定义的Comparator是使用name字段排序,在 Java 中,String类型的排序是通过单字符的 ASCII 码顺序判断的,J排在T的前面,所以Jerry排在第一个。


使用 Lambda 表达式替换Comparator匿名内部类

使用过 Java8 的 Lamdba 的应该知道,匿名内部类可以简化为 Lambda 表达式为:

Collections.sort(students, (Student h1, Student h2) -> h1.getName().compareTo(h2.getName()));

在 Java8 中,List类中增加了sort方法,所以Collections.sort可以直接替换为:

students.sort((Student h1, Student h2) -> h1.getName().compareTo(h2.getName()));

根据 Java8 中 Lambda 的类型推断,我们可以将指定的Student类型简写:

students.sort((h1, h2) -> h1.getName().compareTo(h2.getName()));

至此,我们整段排序逻辑可以简化为:

@Test
void baseSortedLambdaWithInferring() {
    final List students = Lists.newArrayList(
            new Student("Tom"10),
            new Student("Jerry"12)
    );
    students.sort((h1, h2) -> h1.getName().compareTo(h2.getName()));
    Assertions.assertEquals(students.get(0), new Student("Jerry"12));
}

通过静态方法抽取公共的 Lambda 表达式

我们可以在Student中定义一个静态方法:

public static int compareByNameThenAge(Student s1, Student s2) {
    if (s1.name.equals(s2.name)) {
        return Integer.compare(s1.age, s2.age);
    } else {
        return s1.name.compareTo(s2.name);
    }
}

这个方法需要返回一个int类型参数,在 Java8 中,我们可以在 Lambda 中使用该方法:

@Test
void sortedUsingStaticMethod() {
    final List students = Lists.newArrayList(
            new Student("Tom"10),
            new Student("Jerry"12)
    );
    students.sort(Student::compareByNameThenAge);
    Assertions.assertEquals(students.get(0), new Student("Jerry"12));
}

借助Comparatorcomparing方法

在 Java8 中,Comparator类新增了comparing方法,可以将传递的Function参数作为比较元素,比如:

@Test
void sortedUsingComparator() {
    final List students = Lists.newArrayList(
            new Student("Tom"10),
            new Student("Jerry"12)
    );
    students.sort(Comparator.comparing(Student::getName));
    Assertions.assertEquals(students.get(0), new Student("Jerry"12));
}

多条件排序

我们在静态方法一节中展示了多条件排序,还可以在Comparator匿名内部类中实现多条件逻辑:

@Test
void sortedMultiCondition() {
    final List students = Lists.newArrayList(
            new Student("Tom"10),
            new Student("Jerry"12),
            new Student("Jerry"13)
    );
    students.sort((s1, s2) -> {
        if (s1.getName().equals(s2.getName())) {
            return Integer.compare(s1.getAge(), s2.getAge());
        } else {
            return s1.getName().compareTo(s2.getName());
        }
    });
    Assertions.assertEquals(students.get(0), new Student("Jerry"12));
}

从逻辑来看,多条件排序就是先判断第一级条件,如果相等,再判断第二级条件,依次类推。在 Java8 中可以使用comparing和一系列thenComparing表示多级条件判断,上面的逻辑可以简化为:

@Test
void sortedMultiConditionUsingComparator() {
    final List students = Lists.newArrayList(
            new Student("Tom"10),
            new Student("Jerry"12),
            new Student("Jerry"13)
    );
    students.sort(Comparator.comparing(Student::getName).thenComparing(Student::getAge));
    Assertions.assertEquals(students.get(0), new Student("Jerry"12));
}

这里的thenComparing方法是可以有多个的,用于表示多级条件判断,这也是函数式编程的方便之处。

Stream中进行排序

Java8 中,不但引入了 Lambda 表达式,还引入了一个全新的流式 API:Stream API,其中也有sorted方法用于流式计算时排序元素,可以传入Comparator实现排序逻辑:

@Test
void streamSorted() {
    final List students = Lists.newArrayList(
            new Student("Tom"10),
            new Student("Jerry"12)
    );
    final Comparator comparator = (h1, h2) -> h1.getName().compareTo(h2.getName());
    final List sortedStudents = students.stream()
            .sorted(comparator)
            .collect(Collectors.toList());
    Assertions.assertEquals(sortedStudents.get(0), new Student("Jerry"12));
}

同样的,我们可以通过 Lambda 简化书写:

@Test
void streamSortedUsingComparator() {
    final List students = Lists.newArrayList(
            new Student("Tom"10),
            new Student("Jerry"12)
    );
    final Comparator comparator = Comparator.comparing(Student::getName);
    final List sortedStudents = students.stream()
            .sorted(comparator)
            .collect(Collectors.toList());
    Assertions.assertEquals(sortedStudents.get(0), new Student("Jerry"12));
}

倒序排列

调转排序判断

排序就是根据compareTo方法返回的值判断顺序,如果想要倒序排列,只要将返回值取返即可:

@Test
void sortedReverseUsingComparator2() {
    final List students = Lists.newArrayList(
            new Student("Tom"10),
            new Student("Jerry"12)
    );
    final Comparator comparator = (h1, h2) -> h2.getName().compareTo(h1.getName());
    students.sort(comparator);
    Assertions.assertEquals(students.get(0), new Student("Tom"10));
}

可以看到,正序排列的时候,我们是h1.getName().compareTo(h2.getName()),这里我们直接倒转过来,使用的是h2.getName().compareTo(h1.getName()),也就达到了取反的效果。在 Java 的Collections中定义了一个java.util.Collections.ReverseComparator内部私有类,就是通过这种方式实现元素反转。

借助Comparatorreversed方法倒序

在 Java8 中新增了reversed方法实现倒序排列,用起来也是很简单:

@Test
void sortedReverseUsingComparator() {
    final List students = Lists.newArrayList(
            new Student("Tom"10),
            new Student("Jerry"12)
    );
    final Comparator comparator = (h1, h2) -> h1.getName().compareTo(h2.getName());
    students.sort(comparator.reversed());
    Assertions.assertEquals(students.get(0), new Student("Tom"10));
}

Comparator.comparing中定义排序反转

comparing方法还有一个重载方法,java.util.Comparator#comparing(java.util.function.Function, java.util.Comparator),第二个参数就可以传入Comparator.reverseOrder(),可以实现倒序:

@Test
void sortedUsingComparatorReverse() {
    final List students = Lists.newArrayList(
            new Student("Tom"10),
            new Student("Jerry"12)
    );
    students.sort(Comparator.comparing(Student::getName, Comparator.reverseOrder()));
    Assertions.assertEquals(students.get(0), new Student("Jerry"12));
}

Stream中定义排序反转

Stream中的操作与直接列表排序类似,可以反转Comparator定义,也可以使用Comparator.reverseOrder()反转。实现如下:

@Test
void streamReverseSorted() {
    final List students = Lists.newArrayList(
            new Student("Tom"10),
            new Student("Jerry"12)
    );
    final Comparator comparator = (h1, h2) -> h2.getName().compareTo(h1.getName());
    final List sortedStudents = students.stream()
            .sorted(comparator)
            .collect(Collectors.toList());
    Assertions.assertEquals(sortedStudents.get(0), new Student("Tom"10));
}

@Test
void streamReverseSortedUsingComparator() {
    final List students = Lists.newArrayList(
            new Student("Tom"10),
            new Student("Jerry"12)
    );
    final List sortedStudents = students.stream()
            .sorted(Comparator.comparing(Student::getName, Comparator.reverseOrder()))
            .collect(Collectors.toList());
    Assertions.assertEquals(sortedStudents.get(0), new Student("Tom"10));
}

null 值的判断

前面的例子中都是有值元素排序,能够覆盖大部分场景,但有时候我们还是会碰到元素中存在null的情况:

  1. 列表中的元素是 null
  2. 列表中的元素参与排序条件的字段是 null

如果还是使用前面的那些实现,我们会碰到NullPointException异常,即 NPE,简单演示一下:

@Test
void sortedNullGotNPE() {
    final List students = Lists.newArrayList(
            null,
            new Student("Snoopy"12),
            null
    );
    Assertions.assertThrows(NullPointerException.class,
            () -> students.sort(Comparator.comparing(Student::getName)))
;
}

所以,我们需要考虑这些场景。

元素是 null 的笨拙实现

最先想到的就是判空:

@Test
void sortedNullNoNPE() {
    final List students = Lists.newArrayList(
            null,
            new Student("Snoopy"12),
            null
    );
    students.sort((s1, s2) -> {
        if (s1 == null) {
            return s2 == null ? 0 : 1;
        } else if (s2 == null) {
            return -1;
        }
        return s1.getName().compareTo(s2.getName());
    });

    Assertions.assertNotNull(students.get(0));
    Assertions.assertNull(students.get(1));
    Assertions.assertNull(students.get(2));
}

我们可以将判空的逻辑抽取出一个Comparator,通过组合方式实现:

class NullComparator<Timplements Comparator<T{
    private final Comparator real;

    NullComparator(Comparatorsuper T> real) {
        this.real = (Comparator) real;
    }

    @Override
    public int compare(T a, T b) {
        if (a == null) {
            return (b == null) ? 0 : 1;
        } else if (b == null) {
            return -1;
        } else {
            return (real == null) ? 0 : real.compare(a, b);
        }
    }
}

在 Java8 中已经为我们准备了这个实现。

使用Comparator.nullsLastComparator.nullsFirst

使用Comparator.nullsLast实现null在结尾:

@Test
void sortedNullLast() {
    final List students = Lists.newArrayList(
            null,
            new Student("Snoopy"12),
            null
    );
    students.sort(Comparator.nullsLast(Comparator.comparing(Student::getName)));
    Assertions.assertNotNull(students.get(0));
    Assertions.assertNull(students.get(1));
    Assertions.assertNull(students.get(2));
}

使用Comparator.nullsFirst实现null在开头:

@Test
void sortedNullFirst() {
    final List students = Lists.newArrayList(
            null,
            new Student("Snoopy"12),
            null
    );
    students.sort(Comparator.nullsFirst(Comparator.comparing(Student::getName)));
    Assertions.assertNull(students.get(0));
    Assertions.assertNull(students.get(1));
    Assertions.assertNotNull(students.get(2));
}

是不是很简单,接下来我们看下如何实现排序条件的字段是 null 的逻辑。

排序条件的字段是 null

这个就是借助Comparator的组合了,就像是套娃实现了,需要使用两次Comparator.nullsLast,这里列出实现:

@Test
void sortedNullFieldLast() {
    final List students = Lists.newArrayList(
            new Student(null10),
            new Student("Snoopy"12),
            null
    );
    final Comparator nullsLast = Comparator.nullsLast(
            Comparator.nullsLast( // 1
                    Comparator.comparing(
                            Student::getName,
                            Comparator.nullsLast( // 2
                                    Comparator.naturalOrder() // 3
                            )
                    )
            )
    );
    students.sort(nullsLast);
    Assertions.assertEquals(students.get(0), new Student("Snoopy"12));
    Assertions.assertEquals(students.get(1), new Student(null10));
    Assertions.assertNull(students.get(2));
}

代码逻辑如下:

  1. 代码 1 是第一层 null-safe 逻辑,用于判断元素是否为 null;
  2. 代码 2 是第二层 null-safe 逻辑,用于判断元素的条件字段是否为 null;
  3. 代码 3 是条件Comparator,这里使用了Comparator.naturalOrder(),是因为使用了String排序,也可以写为String::compareTo。如果是复杂判断,可以定义一个更加复杂的Comparator,组合模式就是这么好用,一层不够再套一层。

文末总结

本文演示了使用 Java8 中使用 Lambda 表达式实现各种排序逻辑,新增的语法糖真香。

程序汪资料链接

程序汪接的7个私活都在这里,经验整理

Java项目分享 最新整理全集,找项目不累啦 06版

堪称神级的Spring Boot手册,从基础入门到实战进阶

卧槽!字节跳动《算法中文手册》火了,完整版 PDF 开放下载!

卧槽!阿里大佬总结的《图解Java》火了,完整版PDF开放下载!

字节跳动总结的设计模式 PDF 火了,完整版开放下载!


欢迎添加程序汪个人微信 itwang009  进粉丝群或围观朋友圈

浏览 36
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报