有趣的 sorted()

這是我參與更文挑戰的第4天,活動詳情查看: 更文挑戰java

對於列表的排序,能夠說是咱們比較常見的場景了。Java 8 中引入了 lambda 以及 流式(Stream)計算,其中有一個排序的方法 sorted()List 對象自己也是實現了這個方法,可謂是排序好助手,今天咱們就來寫寫關於這個排序方法的一些代碼,不知道你是否是都用過。git

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() 的源碼:markdown

@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) 便可,這裏由於是整型,因此咱們能夠直接按照默認順序排序便可:app

@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 其中的一種實現源碼以下:oop

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 此類),那麼就能夠實現你想要的排序規則或者其餘的什麼需求,函數式編程的樂趣大概就在於本身能夠創造屬於本身的規則吧。

連接

相關文章
相關標籤/搜索