有趣的 sorted()

这是我参与更文挑战的第4天,活动详情查看: 更文挑战

对于列表的排序,可以说是我们比较常见的场景了。Java 8 中引入了 lambda 以及 流式(Stream)计算,其中有一个排序的方法 sorted()List 对象本身也是实现了这个方法,可谓是排序好助手,今天我们就来写写关于这个排序方法的一些代码,不知道你是不是都用过。

1. 简单的列表排序

首先我们先创建一个测试的整型的列表:

    private List<Integer> getTestList() {
        List<Integer> aList = new ArrayList<>();
        aList.add(2);
        aList.add(3);
        aList.add(9);
        aList.add(8);
        aList.add(5);
        aList.add(1);
        aList.add(4);
        aList.add(7);
        aList.add(6);
        return aList;
    }
复制代码

可以使用 Stream 的方式进行直接排序,这里是创建了一个新的排序后的列表:

    @Test
    public void listSort() {
        List<Integer> aList = getTestList();

        System.out.println("排序前:");
        aList.forEach(a -> System.out.printf("%4d", a));
        System.out.println("\n排序后:");
        // 创建一个新列表
        aList = aList.stream().sorted().collect(Collectors.toList());

        aList.forEach(a -> System.out.printf("%4d", a));
        System.out.println();
    }
复制代码

结果为:

排序前:
   2   3   9   8   5   1   4   7   6
排序后:
   1   2   3   4   5   6   7   8   9
复制代码

同样的,基于 lambda 我们可以用 List 实现的 sort() 方法来实现排序,我们可以看一下 List.sort() 的源码:

    @SuppressWarnings({"unchecked", "rawtypes"})
    default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }
复制代码

可以看到,其参数是一个函数式接口(由 @FunctionalInterface 修饰的接口类) Comparator ,具体的实现方式大家可以自行研究,那么我们在使用的时候,就只需要考虑实现这个比较的接口就可以了,即按照我们的策略来实现 Comparator.comparing(Function<? super T, ? extends U> keyExtractor) 即可,这里因为是整型,所以我们可以直接按照默认顺序排序即可:

    @Test
    public void listSort() {
        List<Integer> aList = getTestList();

        System.out.println("排序前:");
        aList.forEach(a -> System.out.printf("%4d", a));
        System.out.println("\n排序后:");
        // 使用 List 实现的 sort 方法,comparing() 接收的是一个函数
        aList.sort(Comparator.comparing(a -> a));

        aList.forEach(a -> System.out.printf("%4d", a));
        System.out.println();
    }
复制代码

这样我们也可以得到同样的结果:

排序前:
   2   3   9   8   5   1   4   7   6
排序后:
   1   2   3   4   5   6   7   8   9
复制代码

2. 列表数据里有 null

作为一名 Java 程序员,NPE ,AKA NullPointException,可谓人生大敌,当列表中出现 null 时,应该如何排序呢?创建一个列表先:

    private List<Integer> getTestListWithNull() {
        List<Integer> aList = new ArrayList<>();
        aList.add(2);
        aList.add(3);
        aList.add(9);
        aList.add(null);
        aList.add(8);
        aList.add(5);
        aList.add(null);
        aList.add(1);
        aList.add(4);
        aList.add(null);
        aList.add(7);
        aList.add(6);
        aList.add(null);
        return aList;
    }
复制代码

可以看到,null 值穿插其中,这时候我们就要考虑相关的需求了,你是想要 null 值排在前面还是后面呢?Comparatorcomparing 其中的一种实现源码如下:

public static <T, U> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor,
            Comparator<? super U> keyComparator)
    {
        Objects.requireNonNull(keyExtractor);
        Objects.requireNonNull(keyComparator);
        return (Comparator<T> & Serializable)
            (c1, c2) -> keyComparator.compare(keyExtractor.apply(c1),
                                              keyExtractor.apply(c2));
    }
复制代码

可以看到,除了接收的第一个 函数参数 外,还会接收另外一个子比较器 Comparator 接口,然后就找到了 nullsLastnullsFirst 这两个方法,代码如下:

    @Test
    public void listNullTest() {
        List<Integer> aList = getTestListWithNull();

        System.out.println("排序前:");
        aList.forEach(a -> {
            if (a != null) {
                System.out.printf("%4d", a);
            } else {
                System.out.printf("%8s", a);
            }
        });
        System.out.println("\n排序后:");
        // 正常的排序会报错
        // aList.sort(Comparator.comparing(a -> a)); // throw NPE
        aList = aList.stream().sorted(
                // nullsLast() / nullsFirst()
                // 对应的整个列表的顺序为: Comparator.naturalOrder() 正序;Comparator.reverseOrder() 反序
                Comparator.comparing(a -> a, Comparator.nullsLast(Comparator.naturalOrder()))
        ).collect(Collectors.toList());
        aList.forEach(a -> {
            if (a != null) {
                System.out.printf("%4d", a);
            } else {
                System.out.printf("%8s", a);
            }
        });
        System.out.println();
    }
复制代码

这样,我们就可以实现按照空值在前或者空值在后实现排序:

排序前:
   2   3   9    null   8   5    null   1   4    null   7   6    null
排序后:
   1   2   3   4   5   6   7   8   9    null    null    null    null
复制代码

3. 我有我自己的想法

有时候,规则需要我们自己定,而且计算机并不能很好的理解和计算我们制定的计算规则,比如我把一堆数据按照四季分好了四组,但是顺序是随机的,怎么才能按照“春”、“夏”、“秋”、“冬”的顺序排序呢?上面我们提到过,comparing() 的 函数参数 是可以按照我们想要的方式自己实现的,有点像策略模式,策略可以自己定。那么这时候,就需要一个辅助列表(需要的顺序规则),然后通过 indexOf 函数,按照下标的大小进行排序:

    @Test
    public void listInSomeOrderTest() {
        List<String> seasons = new ArrayList<>();
        seasons.add("夏");
        seasons.add("冬");
        seasons.add("春");
        seasons.add("秋");

        System.out.println("排序前:");
        seasons.forEach(s -> System.out.printf("%4s", s));
        System.out.println("\n一般排序后:");
        seasons = seasons.stream().sorted().collect(Collectors.toList());
        seasons.forEach(s -> System.out.printf("%4s", s));
        System.out.println();

        // 固定顺序
        List<String> theOrders = Arrays.asList("春", "夏", "秋", "冬");
        // 按照 theOrders 排序
        seasons = seasons.stream().sorted(Comparator.comparing(theOrders::indexOf)).collect(Collectors.toList());
        System.out.println("按照固定顺序排序后:");
        seasons.forEach(s -> System.out.printf("%4s", s));
        System.out.println();
    }
复制代码

这样就可以按照我们想法进行排序:

排序前:
   夏   冬   春   秋
一般排序后:
   冬   夏   春   秋
按照固定顺序排序后:
   春   夏   秋   冬
复制代码

4. 多属性排序

后端程序员应该对 SQL 都不陌生,有时候我们在做统计的时候,应该遇到过这样的需求:一批工人里,拉一个单子,所有的工人年龄从小到大排,薪水从高到低排。翻译成 SQL 即为:查一批数据,同时按照年龄升序排列以及薪水倒序排列。想必在脑子里你已经写完这段 SQL 了,不过今天我们不写 SQL,同样地,先建一批数据:

    private List<Worker> getTestDatas() {
        List<Worker> workers = new ArrayList<>();
        workers.add(new Worker() {{
            setId(1);
            setAge(20);
            setSalary(1000);
        }});
        workers.add(new Worker() {{
            setId(2);
            setAge(22);
            setSalary(1200);
        }});
        workers.add(new Worker() {{
            setId(3);
            setAge(20);
            setSalary(800);
        }});
        workers.add(new Worker() {{
            setId(4);
            setAge(20);
            setSalary(700);
        }});
        workers.add(new Worker() {{
            setId(5);
            setAge(22);
            setSalary(1800);
        }});
        workers.add(new Worker() {{
            setId(6);
            setAge(21);
            setSalary(1100);
        }});
        workers.add(new Worker() {{
            setId(7);
            setAge(22);
            setSalary(1600);
        }});
        workers.add(new Worker() {{
            setId(8);
            setAge(21);
            setSalary(1200);
        }});
        workers.add(new Worker() {{
            setId(9);
            setAge(20);
            setSalary(600);
        }});
        workers.add(new Worker() {{
            setId(10);
            setAge(20);
            setSalary(1200);
        }});
        workers.add(new Worker() {{
            setId(11);
            setAge(21);
            setSalary(1500);
        }});
        workers.add(new Worker() {{
            setId(12);
            setAge(20);
            setSalary(400);
        }});
        workers.add(new Worker() {{
            setId(13);
            setAge(20);
            setSalary(1100);
        }});
        workers.add(new Worker() {{
            setId(14);
            setAge(21);
            setSalary(1500);
        }});
        workers.add(new Worker() {{
            setId(15);
            setAge(21);
            setSalary(1600);
        }});
        return workers;
    }

    @Data
    private static class Worker {
        /**
         * ID
         */
        private Integer id;
        /**
         * 年纪
         */
        private Integer age;
        /**
         * 薪水
         */
        private Integer salary;
    }
复制代码

注:创建对象的方法为匿名类方式创建,仅测试代码中使用,不建议生产代码中使用。

我们知道,流(Stream)范式的操作方法分为三类:Intermediate(中间操作),比如 filtermappeek 等等,这类操作都是惰性(Lazy)化的,仅仅调用到这类方法,并没有真正执行流的遍历;Terminal(终止操作),比如 toListminforEach 等等,一个流只能有一个终止操作,当这个操作执行后,流就被使用「光」了,无法再被操作;Short-circuiting(短路操作/骤死操作),比如 limitanyMatchfindFirst 等等,对于一个 terminal 操作,如果它接受的是一个无限大的流,但能在有限的时间计算出结果。当操作一个无限大的流,而又希望在有限时间内完成操作,则在管道内拥有一个 short-circuiting 操作是必要非充分条件。

好,通过上面的介绍我们可以知道,一个流只有一个终止操作,只有终止操作执行的时候才会进行流的遍历,而且只会遍历一次,时间复杂度为 O(N),那么,我们可以放置两个中间操作 sorted() 来实现我们的多属性排序,代码如下:

    @Test
    public void listWithFieldsTest() throws JsonProcessingException {
        // 根据年纪升序,根据薪水降序,得到一个有序的列表
        // 排序条件逆序设置:先排序的条件放在后面,后排序的条件放前面
        ObjectMapper objectMapper = new ObjectMapper();
        List<Worker> testWorkers = getTestDatas();
        // 原顺序
        System.out.println("排序前:");
        for (Worker testWorker : testWorkers) {
            System.out.println(objectMapper.writeValueAsString(testWorker));
        }
        // 排序后
        List<Worker> sortedWorkers = testWorkers.stream()
          			// 薪水倒序
                .sorted(Comparator.comparing(Worker::getSalary, Comparator.reverseOrder()))
                .sorted(Comparator.comparing(Worker::getAge))
                .collect(Collectors.toList());
        System.out.println("排序后:");
        for (Worker sortedWorker : sortedWorkers) {
            System.out.println(objectMapper.writeValueAsString(sortedWorker));
        }
    }
复制代码

然后,结果如预期:

排序前:
{"id":1,"age":20,"salary":1000}
{"id":2,"age":22,"salary":1200}
{"id":3,"age":20,"salary":800}
{"id":4,"age":20,"salary":700}
{"id":5,"age":22,"salary":1800}
{"id":6,"age":21,"salary":1100}
{"id":7,"age":22,"salary":1600}
{"id":8,"age":21,"salary":1200}
{"id":9,"age":20,"salary":600}
{"id":10,"age":20,"salary":1200}
{"id":11,"age":21,"salary":1500}
{"id":12,"age":20,"salary":400}
{"id":13,"age":20,"salary":1100}
{"id":14,"age":21,"salary":1500}
{"id":15,"age":21,"salary":1600}
排序后:
{"id":10,"age":20,"salary":1200}
{"id":13,"age":20,"salary":1100}
{"id":1,"age":20,"salary":1000}
{"id":3,"age":20,"salary":800}
{"id":4,"age":20,"salary":700}
{"id":9,"age":20,"salary":600}
{"id":12,"age":20,"salary":400}
{"id":15,"age":21,"salary":1600}
{"id":11,"age":21,"salary":1500}
{"id":14,"age":21,"salary":1500}
{"id":8,"age":21,"salary":1200}
{"id":6,"age":21,"salary":1100}
{"id":5,"age":22,"salary":1800}
{"id":7,"age":22,"salary":1600}
{"id":2,"age":22,"salary":1200}
复制代码

这里我们留一个疑问:为什么先排序的条件放在后面,后排序的条件放前面呢?希望评论区给出你的见解。

结论

看完这些,你还有哪些独特的使用方式呢?也欢迎给出。通过本文我们可以知道,只要实现了自己的策略方法(indexOf 此类),那么就可以实现你想要的排序规则或者其他的什么需求,函数式编程的乐趣大概就在于自己可以创造属于自己的规则吧。

链接

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享