公子奇帶你進入Java8流的世界(二)
在上一篇中我們帶領大家簡單的瞭解流的概念及使用場景,知道了流的本質操作是將外部迭代轉為了內部迭代,由此程式併發性上得到了很大的優化。不過我們在面對複雜的需求是又如何通過流來操作呢?在本節我們就來好好的介紹流的常見用法,以此來處理複雜資料,以及流的其他型別:數值流、檔案流以及無限流等內容。
一、篩選和切片
對於一串流,我們有時需要取出我們需要的流中某些元素,例如:符合某些條件的元素、只需要流中的某一段等。主要是通過謂詞篩選。
1、我們如何去除流中重複的元素,以及什麼樣的元素是重複元素呢?流中提供了一個distinct方法來實現。
2、如何取出流中某一段長度的流,流中提供了一個limit方法來滿足我們的需求。
3、若我們需要跳過流中的某一段,又該如何做呢?同樣Java8中也提供了一個skip方法來滿足我們跳過固定長度的流。
下面我們帶著這三個疑問及提示用程式碼來說明問題。
首先定義一個POJO,後續操作都基於此來例項化元素。
1 package com.hz; 2 3 /** 4 * 民警實體類 5 */ 6 public class Police { 7 /** 8 * 民警警號 9 */ 10 private String policeNo; 11 /** 12 * 民警姓名 13 */ 14 private String policeName; 15 /** 16 * 民警年齡 17 */ 18 private Integer policeAge; 19 /** 20 * 是否退休 21 */ 22 private boolean policeIsRetire; 23 24 public Police(String policeNo, String policeName, Integer policeAge, boolean policeIsRetire) { 25 this.policeNo = policeNo; 26 this.policeName = policeName; 27 this.policeAge = policeAge; 28 this.policeIsRetire = policeIsRetire; 29 } 30 31 public String getPoliceNo() { 32 return policeNo; 33 } 34 35 public void setPoliceNo(String policeNo) { 36 this.policeNo = policeNo; 37 } 38 39 public String getPoliceName() { 40 return policeName; 41 } 42 43 public void setPoliceName(String policeName) { 44 this.policeName = policeName; 45 } 46 47 public Integer getPoliceAge() { 48 return policeAge; 49 } 50 51 public void setPoliceAge(Integer policeAge) { 52 this.policeAge = policeAge; 53 } 54 55 public boolean isPoliceIsRetire() { 56 return policeIsRetire; 57 } 58 59 public void setPoliceIsRetire(boolean policeIsRetire) { 60 this.policeIsRetire = policeIsRetire; 61 } 62 63 @Override 64 public String toString() { 65 return "Police{" + 66 "policeNo='" + policeNo + '\'' + 67 ", policeName='" + policeName + '\'' + 68 ", policeAge=" + policeAge + 69 ", policeIsRetire='" + policeIsRetire + '\'' + 70 '}'; 71 } 72 73 @Override 74 public boolean equals(Object o) { 75 if (this == o) return true; 76 if (!(o instanceof Police)) return false; 77 78 Police police = (Police) o; 79 80 if (policeIsRetire != police.policeIsRetire) return false; 81 if (!policeNo.equals(police.policeNo)) return false; 82 if (!policeName.equals(police.policeName)) return false; 83 return policeAge.equals(police.policeAge); 84 } 85 86 @Override 87 public int hashCode() { 88 int result = policeNo.hashCode(); 89 result = 31 * result + policeName.hashCode(); 90 result = 31 * result + policeAge.hashCode(); 91 result = 31 * result + (policeIsRetire ? 1 : 0); 92 return result; 93 } 94 }
1 package com.hz; 2 3 import java.util.Arrays; 4 import java.util.List; 5 6 import static java.util.stream.Collectors.toList; 7 8 /** 9 * 篩選某些元素 10 * 包含:filter / distinct / limit / skip 11 */ 12 public class FilterAndLimitStreamDemo { 13 public static void main(String[] args) { 14 List<Police> policeList = Arrays.asList( 15 new Police("P001", "餘警官", 27, false), 16 new Police("P002", "李警官", 32, false), 17 new Police("P003", "程警官", 25, false), 18 new Police("P004", "楊警官", 35, false), 19 new Police("P005", "張警官", 70, true), 20 new Police("P006", "王警官", 68, true), 21 new Police("P007", "趙警官", 77, true), 22 new Police("P008", "劉警官", 64, true), 23 new Police("P008", "劉警官", 64, true) 24 ); 25 26 //***** 1-使用謂詞篩選 27 List<Police> filterPolices = policeList.stream().filter(Police::isPoliceIsRetire).collect(toList()); 28 System.out.println("結果1: " + filterPolices); 29 30 System.out.println("---------------- 分割線 ---------------------"); 31 32 //***** 2-篩選 大於60歲 並 去除重複的資料(是否重複 根據HashCode和equal決定 兩者同時) 33 policeList.stream().filter(p -> p.getPoliceAge() > 60).distinct().forEach(System.out :: println); 34 35 System.out.println("---------------- 分割線 ---------------------"); 36 37 //***** 3-截流 擷取前三位退休的民警 38 policeList.stream().filter(Police :: isPoliceIsRetire).limit(3).forEach(System.out :: println); 39 40 System.out.println("---------------- 分割線 ---------------------"); 41 42 //***** 4-跳過幾個元素 獲取退休民警 並跳過前兩位退休民警 43 policeList.stream().filter(Police :: isPoliceIsRetire).skip(2).forEach(System.out :: println); 44 } 45 }
二、對映
對映即將流中的元素進行處理後返回新的流,即從某些物件中選擇資訊。在SQL語言中,我們通常會取出表中的某個欄位資訊,將其返回給頁面展示,若在Java語言中,如何實現同樣的功能呢?語言也提供了兩個方法讓我們來實現。
1、map:該方法會對流中的每個元素進行處理,我們可以理解為將流中的每個元素進行轉換後返回一個新的流。例如我們如何取出所有民警的姓名。
2、flatMap:既然有了map方法,Java又為什麼提供一個flatMap呢?此為流的扁平化。我們之前講解過,小的流和組成一個大的流,若我們有個大的流,大的流中有多少小的流我們並不知道,此時我們無法對每個小的流再次進行處理,而flatMap方法即可解決我們的問題,它可以將大流中的小流的值組成一個新的大流,之後再對該流進行元素處理。例如我們需要將多個單詞重複的字母給去除,此時每個單詞即可組裝為一個小流,若我們使用map來處理,只會將每個單詞自己的重複字母給去除,兩個單詞之間的重複字母無法去除。
1 package com.hz; 2 3 import java.util.Arrays; 4 import java.util.List; 5 import java.util.stream.Stream; 6 7 import static java.util.stream.Collectors.toList; 8 9 /** 10 * 流中對映 11 */ 12 public class StreamMapDemo { 13 public static void main(String[] args) { 14 List<Police> policeList = Arrays.asList( 15 new Police("P001", "餘警官", 27, false), 16 new Police("P002", "李警官", 32, false), 17 new Police("P003", "程警官", 25, false), 18 new Police("P004", "楊警官", 35, false), 19 new Police("P005", "張警官", 70, true), 20 new Police("P006", "司馬警官", 68, true), 21 new Police("P007", "趙科", 77, true), 22 new Police("P008", "劉警官", 64, true), 23 new Police("P008", "劉警官", 64, true) 24 ); 25 26 //***** 1-對流元素中的欄位進行處理 獲取民警的姓名 和 獲取民警姓名長度 27 policeList.stream().map(Police :: getPoliceName).forEach(System.out :: println); 28 29 List<Integer> policesNameLength = policeList.stream().map(Police::getPoliceName).map(String::length).collect(toList()); 30 System.out.println("結果: " + policesNameLength); 31 32 System.out.println("------------ 分割線 ----------------"); 33 34 //***** 2-流的扁平化 將一組單詞 重複的字母去除 35 String[] words = {"gong", "zi", "chuan", "qi"}; 36 Stream<String> wordsStream = Arrays.stream(words); //將陣列轉為流物件 37 Stream<String> wordsStream2 = Arrays.stream(words); //將陣列轉為流物件 38 39 //此時為 將每個單詞為一個數組轉為一個流即四個流 流本身沒有重複 兩個流之間是存在重複的 40 List<Stream<String>> streamsList = wordsStream.map(s -> s.split("")).map(Arrays::stream).distinct().collect(toList()); 41 System.out.println(streamsList); 42 //wordsStream為一個流 不可多次使用 43 // java8中有個扁平化的處理 為將流中的值組成一個大的流 flatMap 44 List<String> disWordsList = wordsStream2.map(s -> s.split("")).flatMap(Arrays::stream).distinct().collect(toList()); 45 System.out.println("結果:" + disWordsList); 46 47 System.out.println("---------------- 分割線 --------------------"); 48 49 //***** 3-一個例項 給定兩個數字列表,如何返回所有的數對呢? 50 // 例如,給定列表[1, 2, 3]和列表[3, 4],應該返回[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)] 51 Integer[] ints1 = {1, 2, 3}; 52 Integer[] ints2 = {3, 4}; 53 54 // List<List<int[]>> result = Arrays.stream(ints1).map(i -> { 55 // List<int[]> tempList = Arrays.stream(ints2).map(j -> { 56 //// int[] temp = new int[2]; 57 //// temp[0] = i; 58 //// temp[1] = j; 59 //// return temp; 60 // return new int[]{i, j}; 61 // }).collect(toList()); 62 // return tempList; 63 // }).collect(toList()); 64 65 //更簡化 66 List<List<int[]>> result = Arrays.stream(ints1).map(i -> Arrays.stream(ints2).map(j -> new int[]{i, j}).collect(toList())).collect(toList()); 67 68 result.forEach(l -> l.forEach(is -> System.out.println((is[0] + "," + is[1])))); 69 } 70 }
三、查詢和匹配
有時我們需要檢視資料集中的某些元素是否匹配一個給定的屬性。對於該需求,Java中分別提供了 allMatch 、 anyMatch 、 noneMatch 、 findFirst 和 findAny 方法來滿足我們。
1、allMatch:該方法表示流中的所有元素都滿足我們的條件。例如我們想知道當前的民警容器中是不是所有的民警都為退休民警。
2、anyMatch:該方法表示流中的元素至少有一個滿足我們的條件。例如我們想知道有沒有民警退休了。
3、noneMatch:表示所有的元素都不滿足。它與allMatch正好相反。
4、findFirst:在流中找出第一個元素,該方法我們一般會配合篩選來使用。
5、findAny:在流中隨意找出一個元素,一般情況下方法返回給我們的都為第一個元素。
1 package com.hz; 2 3 import java.util.*; 4 5 /** 6 * 流中查詢和匹配 7 */ 8 public class FindAndMatchDemo { 9 public static void main(String[] args) { 10 List<Police> policeList = Arrays.asList( 11 new Police("P001", "餘警官", 27, false), 12 new Police("P002", "李警官", 32, false), 13 new Police("P003", "程警官", 25, false), 14 new Police("P004", "楊警官", 35, false), 15 new Police("P005", "張警官", 70, true), 16 new Police("P006", "司馬警官", 68, true), 17 new Police("P007", "趙科", 77, true), 18 new Police("P008", "劉警官", 64, true), 19 new Police("P008", "劉警官", 64, true) 20 ); 21 22 //***** 1-檢查流中元素至少匹配一個 民警列表中至少有一名退休民警 23 System.out.println("--------------- anyMatch分割線 ------------------"); 24 if (policeList.stream().anyMatch(Police :: isPoliceIsRetire)) { 25 System.out.println("列表存在退休民警..."); 26 } else { 27 System.out.println("當前沒有退休民警..."); 28 } 29 30 System.out.println("----------- allMatch分割線 ------------"); 31 32 //***** 2-檢查流中是否匹配所有元素 是否為退休民警列表 33 if (policeList.stream().allMatch(Police :: isPoliceIsRetire)) { 34 System.out.println("該列表為一個退休民警列表..."); 35 } else { 36 System.out.println("該列表中存在未退休的民警..."); 37 } 38 39 System.out.println("------------- noneMatch分割線 -----------"); 40 //與allMatch對應的是noneMatch 即所有都不匹配 41 if (policeList.stream().noneMatch(Police::isPoliceIsRetire)) { 42 System.out.println("該列表都不是退休民警"); 43 } else { 44 System.out.println("該列表存在退休民警"); 45 } 46 47 System.out.println("--------------- 查詢分割線 --------------------"); 48 49 //***** 3-查詢元素 從民警列表中找出任一元素 50 Optional<Police> anyPolice = policeList.stream().findAny(); 51 System.out.println(anyPolice); 52 //在這裡Optional這是一個容器類,該容器中只能儲存一個物件 其中Optional中實現了一些方法用來操作和判斷該容器是否有值 53 //這裡我們簡單瞭解下,詳細後面我們在好好介紹下該類 54 //***** 案例:找出任意一個退休民警 並列印該民警姓名 ifPresent若有值則執行 55 policeList.stream().filter(Police :: isPoliceIsRetire).findAny().ifPresent(p -> System.out.println(p.getPoliceName())); 56 boolean hasValue = policeList.stream().filter(Police::isPoliceIsRetire).findAny().isPresent(); //是否有值isPresent 57 System.out.println("容器中是否有匹配的值:" + hasValue); 58 // 若容器中有值則返回,否則返回一個預設的值 59 Police police = policeList.stream().filter(Police :: isPoliceIsRetire).findAny().orElse(new Police("","",0, false)); 60 System.out.println("返回民警: " + police); 61 //獲取匹配的民警 有則返回 沒有則異常java.util.NoSuchElementException: No value present 62 Police police1 = policeList.stream().filter(Police::isPoliceIsRetire).findAny().get(); 63 System.out.println("獲取民警: " + police1); 64 65 System.out.println("---------------"); 66 //***** 4-查詢流中第一個元素 67 Police police2 = policeList.stream().filter(Police::isPoliceIsRetire).findFirst().get(); 68 System.out.println("結果: " + police2); 69 } 70 }
四、歸約
以上提供的方法,我們會發現返回給我們的都是一個boolean型別、無返回或為一個容器,可是我們有時需要將流中的每個元素重新組合來得到結果。這就需要將流中所有的元素結合著來處理,此時就需要歸約。
1、例如將流中所有民警的年齡求和。當然使用for-each可以實現,可在這裡我們還有更好的方式嗎?
2、我們需要當前監獄年齡最大的民警和年齡最小的民警。此時又該如何實現呢?我們總不至於要將所有的民警挨個遍歷一篇吧!
以上兩個問題其實都是可以通過歸約得到很好的解決,除此以外,我們會發現年齡都是與數值相關的,這也將引出我們普通流的變種,即數值流。下面以程式碼來說明問題:
package com.hz; import java.util.Arrays; import java.util.List; import java.util.Optional; public class ReduceDemo { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(5, 4, 3, 9); //***** 1-將一組數值求和 Integer sum = numbers.stream().reduce(0, (a, b) -> a + b); System.out.println("sum: " + sum); //若reduce未指定第一個引數,則不知實際返回型別,此時語言本身返回一個Optional容器物件 Optional<Integer> sumOptional = numbers.stream().reduce((a, b) -> a + b); System.out.println("sum-optional: " + sumOptional); //變化 sum = numbers.stream().reduce(0, Integer :: sum); System.out.println("sum2: " + sum); System.out.println("------------ 分割線 --------------"); //***** 2-獲取一組數的最大和最小值 Optional<Integer> max = numbers.stream().reduce(Integer::max); System.out.println("max: " + max); Optional<Integer> min = numbers.stream().reduce(Integer :: min); System.out.println("min: " + min); System.out.println("---------- 分割線 --------"); //**** 3-一個案例:使用map和reduce統計民警列表中有多少民警 List<Police> policeList = Arrays.asList( new Police("P001", "餘警官", 27, false), new Police("P002", "李警官", 32, false), new Police("P003", "程警官", 25, false), new Police("P004", "楊警官", 35, false), new Police("P005", "張警官", 70, true), new Police("P006", "司馬警官", 68, true), new Police("P007", "趙科", 77, true), new Police("P008", "劉警官", 64, true) ); Integer policeNum = policeList.stream().map(p -> 1).reduce(0, Integer::sum); System.out.println("列表中民警數量: " + policeNum); } }
五、一個例項
關於流的這麼多操作,我們上面都說了很多,但是在實際需求中我們應該如何的靈活運用呢?下面我們通過一些問題案例來總結一下流運用。
(1) 找出2019年入職的民警,並按年齡排序(從低到高)。
(2) 民警籍貫都在哪裡?
(3) 查詢所有來自於浙江的民警,並按姓名排序。
(4) 返回所有民警的警號,並按警號順序排序。
(5) 有沒有民警籍貫是在安徽的?
(6) 獲取籍貫為浙江的民警的年齡之和
(7) 所有民警中,年齡最大的是多少?
(8) 找到民警中年齡最小的民警
package com.hz; import java.util.*; /** * 關於流操作的一個案例 */ public class StreamDemo { public static void main(String[] args) { List<Police> polices = Arrays.asList( new Police("P001", "Y警官", 27, "浙江", 2019), new Police("P002", "L警官", 32, "安徽", 2019), new Police("P003", "C警官", 25, "安徽", 2015), new Police("P004", "Y警官", 35, "浙江", 2015), new Police("P005", "Z警官", 31, "上海", 2018), new Police("P006", "W警官", 42, "浙江", 2018), new Police("P007", "Z警官", 31, "浙江", 2019), new Police("P009", "Z警官", 32, "浙江", 2019), new Police("P008", "L警官", 49, "浙江", 2019) ); // (1) 找出2019年入職的民警,並按年齡排序(從低到高)。 polices.stream().filter(p -> p.getPoliceEntryYear() == 2019).sorted(Comparator.comparing(Police::getPoliceAge)).forEach(System.out::println); System.out.println("---------------- 分割線 --------------------------"); // (2) 民警籍貫都在哪裡? polices.stream().map(Police::getPoliceNativePlace).distinct().forEach(System.out::println); System.out.println("---------------- 分割線 --------------------------"); // (3) 查詢所有來自於浙江的民警,並按姓名排序。 polices.stream().filter(p -> "浙江".equals(p.getPoliceNativePlace())).sorted(Comparator.comparing(Police::getPoliceName)).forEach(System.out::println); System.out.println("---------------- 分割線 --------------------------"); // (4) 返回所有民警的警號,並按警號順序排序。 polices.stream().map(Police::getPoliceNo).sorted().forEach(System.out::println); System.out.println("---------------- 分割線 --------------------------"); // (5) 有沒有民警籍貫是在安徽的? if (polices.stream().anyMatch(p -> "安徽".equals(p.getPoliceNativePlace()))) { System.out.println("存在籍貫為安徽的民警..."); } else { System.out.println("不存在籍貫為安徽的民警..."); } System.out.println("---------------- 分割線 --------------------------"); // (6) 獲取籍貫為浙江的民警的年齡之和 Integer ageSum = polices.stream().filter(p -> "浙江".equals(p.getPoliceNativePlace())).map(Police::getPoliceAge).reduce(0, Integer::sum); //以上方式暗中存在一個裝箱操作 其實stream中有個數值流可以省去這個隱藏操作 ageSum = polices.stream().filter(p -> "浙江".equals(p.getPoliceNativePlace())).mapToInt(Police::getPoliceAge).sum(); System.out.println("所有浙江民警年齡總和:" + ageSum); System.out.println("---------------- 分割線 --------------------------"); // (7) 所有民警中,年齡最大的是多少? Optional<Integer> ageMaxOptional = polices.stream().map(Police::getPoliceAge).reduce(Integer::max); // 或使用流的max方法 ageMaxOptional = polices.stream().max(Comparator.comparing(Police::getPoliceAge)).map(Police::getPoliceAge); //同樣 該方式也可以轉為數值流計算 OptionalInt maxAgeOp = polices.stream().mapToInt(Police::getPoliceAge).max(); System.out.println(maxAgeOp.getAsInt()); if (ageMaxOptional.isPresent()) { System.out.println("所有民警最大年齡為: " + ageMaxOptional.get()); } else { System.out.println("沒有民警..."); } System.out.println("---------------- 分割線 --------------------------"); // (8) 找到民警中年齡最小的民警 Optional<Police> ageMinPoliceOptional = polices.stream().reduce((p1, p2) -> p1.getPoliceAge() < p2.getPoliceAge() ? p1 : p2); if (ageMinPoliceOptional.isPresent()) { System.out.println("年齡最小的民警: " + ageMinPoliceOptional); } else { System.out.println("列表中沒有民警..."); } //其實還有更加簡單的方式,流中有個min方法 ageMinPoliceOptional = polices.stream().min(Comparator.comparing(Police::getPoliceAge)); System.out.println(ageMinPoliceOptional); } static class Police { private String policeNo; private String policeName; private Integer policeAge; private Integer policeEntryYear; private String policeNativePlace; public Police(String policeNo, String policeName, Integer policeAge, String policeNativePlace, Integer policeEntryYear) { this.policeNo = policeNo; this.policeName = policeName; this.policeAge = policeAge; this.policeNativePlace = policeNativePlace; this.policeEntryYear = policeEntryYear; } public String getPoliceNo() { return policeNo; } public void setPoliceNo(String policeNo) { this.policeNo = policeNo; } public String getPoliceName() { return policeName; } public void setPoliceName(String policeName) { this.policeName = policeName; } public Integer getPoliceAge() { return policeAge; } public void setPoliceAge(Integer policeAge) { this.policeAge = policeAge; } public String getPoliceNativePlace() { return policeNativePlace; } public void setPoliceNativePlace(String policeNativePlace) { this.policeNativePlace = policeNativePlace; } public Integer getPoliceEntryYear() { return policeEntryYear; } public void setPoliceEntryYear(Integer policeEntryYear) { this.policeEntryYear = policeEntryYear; } @Override public String toString() { return "Police{" + "policeNo='" + policeNo + '\'' + ", policeName='" + policeName + '\'' + ", policeAge=" + policeAge + ", policeEntryYear='" + policeEntryYear + '\'' + ", policeNativePlace='" + policeNativePlace + '\'' + '}'; } } }
六、數值流及構建流
關於Java中流的介紹,我們也說明的差不多了,流對於表達資料處理查詢是非常強大而有用的。不過在上面我們對流的建立都是基於集合之後做集合轉換,可是除了集合之外,我們還有其他建立流的方式嗎?答案是肯定的。關於數值流我們在上節例項中已經做了簡單說明,在流的建立過程中,有時我們並不知道流的內容,此時需要根據變化來建立流物件。
1 package com.hz; 2 3 import java.io.IOException; 4 import java.nio.charset.Charset; 5 import java.nio.file.Files; 6 import java.nio.file.Paths; 7 import java.util.ArrayList; 8 import java.util.Arrays; 9 import java.util.List; 10 import java.util.stream.Stream; 11 12 /** 13 * 我們之前建立的流都是通過一個List物件或陣列 14 * 其他方式也是可以建立流的 15 */ 16 public class CreateStreamDemo { 17 public static void main(String[] args) { 18 // 1-List建立流 19 List<String> list = new ArrayList<>(); 20 Stream<String> stream = list.stream(); 21 22 // 2-陣列建立流 23 String[] strings = {"Gong", "Zi", "Qi"}; 24 Stream<String> stream1 = Arrays.stream(strings); 25 26 // 3-靜態方法建立流 27 Stream<String> stream2 = Stream.of("Gong", "Zi", "Qi"); 28 Stream.iterate(0, i -> i + 2).limit(10).forEach(System.out::println); 29 Stream<Object> stream4 = Stream.generate(() -> "GongZiQi"); 30 //說明: iterate 和 generate 在生成流是需要注意擷取,它們都可以做無限流的生成 如下 31 // Stream.iterate(0, i -> i + 2).forEach(System.out::println); //生成所有偶數 注意 32 // Stream.generate(Math::random).forEach(System.out::println); //生成所有隨機數 0~1之間 注意 33 34 // 4-檔案生成流 35 try { 36 Stream<String> stream3 = Files.lines(Paths.get("data.txt"), Charset.defaultCharset()); 37 } catch (IOException e) { 38 e.printStackTrace(); 39 } 40 } 41 }