這是我參與更文挑戰的第4天,活動詳情查看: 更文挑戰java
對於列表的排序,能夠說是咱們比較常見的場景了。Java 8 中引入了 lambda
以及 流式(Stream)計算,其中有一個排序的方法 sorted()
,List
對象自己也是實現了這個方法,可謂是排序好助手,今天咱們就來寫寫關於這個排序方法的一些代碼,不知道你是否是都用過。git
首先咱們先建立一個測試的整型的列表:程序員
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
複製代碼
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
值排在前面仍是後面呢?Comparator
中 comparing
其中的一種實現源碼以下: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
接口,而後就找到了 nullsLast
和 nullsFirst
這兩個方法,代碼以下:
@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
複製代碼
有時候,規則須要咱們本身定,並且計算機並不能很好的理解和計算咱們制定的計算規則,好比我把一堆數據按照四季分好了四組,可是順序是隨機的,怎麼才能按照「春」、「夏」、「秋」、「冬」的順序排序呢?上面咱們提到過,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();
}
複製代碼
這樣就能夠按照咱們想法進行排序:
排序前:
夏 冬 春 秋
通常排序後:
冬 夏 春 秋
按照固定順序排序後:
春 夏 秋 冬
複製代碼
後端程序員應該對 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(中間操做),好比 filter
、map
、peek
等等,這類操做都是惰性(Lazy)化的,僅僅調用到這類方法,並無真正執行流的遍歷;Terminal(終止操做),好比 toList
、min
、forEach
等等,一個流只能有一個終止操做,當這個操做執行後,流就被使用「光」了,沒法再被操做;Short-circuiting(短路操做/驟死操做),好比 limit
、anyMatch
、findFirst
等等,對於一個 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
此類),那麼就能夠實現你想要的排序規則或者其餘的什麼需求,函數式編程的樂趣大概就在於本身能夠創造屬於本身的規則吧。