JDK8新特性詳解(二)
Stream流的使用
流操作是Java8提供一個重要新特性,它允許開發人員以宣告性方式處理集合,其核心類庫主要改進了對集合類的 API和新增Stream操作。Stream類中每一個方法都對應集合上的一種操作。將真正的函數語言程式設計引入到Java中,能 讓程式碼更加簡潔,極大地簡化了集合的處理操作,提高了開發的效率和生產力。
同時stream不是一種資料結構,它只是某種資料來源的一個檢視,資料來源可以是一個數組,Java容器或I/O channel等。在Stream中的操作每一次都會產生新的流,內部不會像普通集合操作一樣立刻獲取值,而是惰性 取值,只有等到使用者真正需要結果的時候才會執行。並且對於現在呼叫的方法,本身都是一種高層次構件,與執行緒模型無關。因此在並行使用中,開發者們無需再去操 心執行緒和鎖了。Stream內部都已經做好了
如果剛接觸流操作的話,可能會感覺不太舒服。其實理解流操作的話可以對比資料庫操作。把流的操作理解為對資料庫中 資料的查詢操作
集合=資料表
元素=表中的每條資料
屬性=每條資料的列
流API=sql查詢
流操作詳解
Stream流介面中定義了許多對於集合的操作方法,總的來說可以分為兩大類:中間操作和終端操作。
-
中間操作:會返回一個流,通過這種方式可以將多箇中間操作連線起來,形成一個呼叫鏈,從而轉換為另外 一個流。除非呼叫鏈後存在一個終端操作,否則中間操作對流不會進行任何結果處理。
-
終端操作:會返回一個具體的結果,如boolean、list、integer等。
1、篩選
對於集合的操作,經常性的會涉及到對於集中符合條件的資料篩選,Stream中對於資料篩選兩個常見的API: filter(過濾)、distinct(去重)
1.1基於filter()實現資料過
該方法會接收一個返回boolean的函式作為引數,終返回一個包括所有符合條件元素的流。
案例:獲取所有年齡20歲以下的學生
/** * @author 我是七月呀 * @date 2020/12/22 */ public class FilterDemo { public static void main(String[] args) { //獲取所有年齡20歲以下的學生 ArrayList<Student> students = new ArrayList<>(); students.add(new Student(1,19,"張三","M",true)); students.add(new Student(1,18,"李四","M",false)); students.add(new Student(1,21,"王五","F",true)); students.add(new Student(1,20,"趙六","F",false)); students.stream().filter(student -> student.getAge()<20); } }
原始碼解析
此處可以看到filter方法接收了Predicate函式式介面。
首先判斷predicate是否為null,如果為null,則丟擲NullPointerException;構建Stream,重寫opWrapsink方法。引數flags:下一個sink的標誌位,供優化使用。引數sink:下一個sink,通過此引數將sink構造成單鏈。此時流已經構建好,但是因為begin()先執行,此時是無法確定流中後續會存在多少元素的,所以傳遞-1,代表無法確定。最後呼叫Pridicate中的test,進行條件判斷,將符合條件資料放入流中。
1.2基於distinct實現資料去重
/**
* @author 我是七月呀
* @date 2020/12/22
*/
public class DistinctDemo {
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2);
integers.stream().distinct().collect(Collectors.toList());
}
}
原始碼解析
根據其原始碼,我們可以知道在distinct()內部是基於LinkedHashSet對流中資料進行去重,並終返回一個新的流。
2、切片
2.1基於limit()實現資料擷取
該方法會返回一個不超過給定長度的流
案例:獲取陣列的前五位
/**
* @author 我是七月呀
* @date 2020/12/22
*/
public class LimitDemo {
public static void main(String[] args) {
//獲取陣列的前五位
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2);
integers.stream().limit(5);
}
}
原始碼解析:
對於limit方法的實現,它會接收擷取的長度,如果該值小於0,則丟擲異常,否則會繼續向下呼叫 SliceOps.makeRef()。該方法中this代表當前流,skip代表需要跳過元素,比方說本來應該有4個元素,當跳過元素 值為2,會跳過前面兩個元素,獲取後面兩個。maxSize代表要擷取的長度
在makeRef方法中的unorderedSkipLimitSpliterator()中接收了四個引數Spliterator,skip(跳過個數)、limit(擷取 個數)、sizeIfKnown(已知流大小)。如果跳過個數小於已知流大小,則判斷跳過個數是否大於0,如果大於則取擷取 個數或已知流大小-跳過個數的兩者小值,否則取已知流大小-跳過個數的結果,作為跳過個數。
後對集合基於跳過個數和擷取個數進行切割。
2.2基於skip()實現資料跳過
案例:從集合第三個開始擷取5個數據
/**
* @author 我是七月呀
* @date 2020/12/22
*/
public class LimitDemo {
public static void main(String[] args) {
//從集合第三個開始擷取5個數據
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2);
List<Integer> collect = integers.stream().skip(3).limit(5).collect(Collectors.toList());
collect.forEach(integer -> System.out.print(integer+" "));
}
}
結果4 4 5 5 6
案例:先從集合中擷取5個元素,然後取後3個
/**
* @author 我是七月呀
* @date 2020/12/22
*/
public class LimitDemo {
public static void main(String[] args) {
//先從集合中擷取5個元素,然後取後3個
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2);
List<Integer> collect = integers.stream().limit(5).skip(2).collect(Collectors.toList());
collect.forEach(integer -> System.out.print(integer+" "));
}
}
結果:3 4 4
原始碼分析:
在skip方法中接收的n代表的是要跳過的元素個數,如果n小於0,丟擲非法引數異常,如果n等於0,則返回當前 流。如果n小於0,才會呼叫makeRef()。同時指定limit引數為-1.
此時可以發現limit和skip都會進入到該方法中,在確定limit值時,如果limit<0,則獲取已知集合大小長度-跳過的長度。最終進行資料切割。
3、對映
在對集合進行操作的時候,我們經常會從某些物件中選擇性的提取某些元素的值,就像編寫sql一樣,指定獲取表 中特定的資料列
#指定獲取特定列 SELECTnameFROMstudent
在Stream API中也提供了類似的方法,map()。它接收一個函式作為方法引數,這個函式會被應用到集合中每一個 元素上,並終將其對映為一個新的元素。
案例:獲取所有學生的姓名,並形成一個新的集合
/**
* @author 我是七月呀
* @date 2020/12/22
*/
public class MapDemo {
public static void main(String[] args) {
//獲取所有學生的姓名,並形成一個新的集合
ArrayList<Student> students = new ArrayList<>();
students.add(new Student(1,19,"張三","M",true));
students.add(new Student(1,18,"李四","M",false));
students.add(new Student(1,21,"王五","F",true));
students.add(new Student(1,20,"趙六","F",false));
List<String> collect = students.stream().map(Student::getName).collect(Collectors.toList());
collect.forEach(s -> System.out.print(s + " "));
}
}
結果:張三 李四 王五 趙六
原始碼解析:
內部對Function函式式介面中的apply方法進行實現,接收一個物件,返回另外一個物件,並把這個內容存入當前 流中,後返回
4、匹配
在日常開發中,有時還需要判斷集合中某些元素是否匹配對應的條件,如果有的話,在進行後續的操作。在 Stream API中也提供了相關方法供我們進行使用,如anyMatch、allMatch等。他們對應的就是&&和||運算子。
4.1基於anyMatch()判斷條件至少匹配一個元素
anyMatch()主要用於判斷流中是否至少存在一個符合條件的元素,它會返回一個boolean值,並且對於它的操作, 一般叫做短路求值
案例:判斷集合中是否有年齡小於20的學生
/**
* @author 我是七月呀
* @date 2020/12/22
*/
public class AnyMatchDemo {
public static void main(String[] args) {
//判斷集合中是否有年齡小於20的學生
ArrayList<Student> students = new ArrayList<>();
students.add(new Student(1,19,"張三","M",true));
students.add(new Student(1,18,"李四","M",false));
students.add(new Student(1,21,"王五","F",true));
students.add(new Student(1,20,"趙六","F",false));
if(students.stream().anyMatch(student -> student.getAge() < 20)){
System.out.println("集合中有年齡小於20的學生");
}else {
System.out.println("集合中沒有年齡小於20的學生");
}
}
}
根據上述例子可以看到,當流中只要有一個符合條件的元素,則會立刻中止後續的操作,立即返回一個布林值,無需遍歷整個流。
原始碼解析:
內部實現會呼叫makeRef(),其接收一個Predicate函式式介面,並接收一個列舉值,該值代表當前操作執行的是 ANY。
如果test()抽象方法執行返回值==MatchKind中any的stopOnPredicateMatches,則將stop中斷置為true,value 也為true。並終進行返回。無需進行後續的流操作。
4.2基於allMatch()判斷條件是否匹配所有元素
allMatch()的工作原理與anyMatch()類似,但是anyMatch執行時,只要流中有一個元素符合條件就會返回true, 而allMatch會判斷流中是否所有條件都符合條件,全部符合才會返回true
案例:判斷集合所有學生的年齡是否都小於20
/**
* @author 我是七月呀
* @date 2020/12/22
*/
public class AllMatchDemo {
public static void main(String[] args) {
//判斷集合所有學生的年齡是否都小於20
ArrayList<Student> students = new ArrayList<>();
students.add(new Student(1,19,"張三","M",true));
students.add(new Student(1,18,"李四","M",false));
students.add(new Student(1,21,"王五","F",true));
students.add(new Student(1,20,"趙六","F",false));
if(students.stream().allMatch(student -> student.getAge() < 20)){
System.out.println("集合所有學生的年齡都小於20");
}else {
System.out.println("集合中有年齡大於20的學生");
}
}
}
原始碼解析:與anyMatch類似,只是其列舉引數的值為ALL
5、查詢
對於集合操作,有時需要從集合中查詢中符合條件的元素,Stream中也提供了相關的API,findAny()和 findFirst(),他倆可以與其他流操作組合使用。findAny用於獲取流中隨機的某一個元素,findFirst用於獲取流中的 第一個元素。至於一些特別的定製化需求,則需要自行實現。
5.1基於findAny()查詢元素
案例:findAny用於獲取流中隨機的某一個元素,並且利用短路在找到結果時,立即結束
/**
* @author 我是七月呀
* @date 2020/12/22
*/
public class FindAnyDemo {
public static void main(String[] args) {
//findAny用於獲取流中隨機的某一個元素,並且利用短路在找到結果時,立即結束
ArrayList<Student> students = new ArrayList<>();
students.add(new Student(1,19,"張三1","M",true));
students.add(new Student(1,18,"張三2","M",false));
students.add(new Student(1,21,"張三3","F",true));
students.add(new Student(1,20,"張三4","F",false));
students.add(new Student(1,20,"張三5","F",false));
students.add(new Student(1,20,"張三6","F",false));
Optional<Student> student1 = students.stream().filter(student -> student.getSex().equals("F")).findAny();
System.out.println(student1.toString());
}
}
結果:Optional[Student{id=1, age=21, name='張三3', sex='F', isPass=true}]
此時我們將其迴圈100次
/**
* @author 我是七月呀
* @date 2020/12/22
*/
public class FindAnyDemo {
public static void main(String[] args) {
//findAny用於獲取流中隨機的某一個元素,並且利用短路在找到結果時,立即結束
ArrayList<Student> students = new ArrayList<>();
students.add(new Student(1,19,"張三1","M",true));
students.add(new Student(1,18,"張三2","M",false));
students.add(new Student(1,21,"張三3","F",true));
students.add(new Student(1,20,"張三4","F",false));
students.add(new Student(1,20,"張三5","F",false));
students.add(new Student(1,20,"張三6","F",false));
for (int i = 0; i < 100; i++) {
Optional<Student> student1 = students.stream().filter(student -> student.getSex().equals("F")).findAny();
System.out.println(student1.toString());
}
}
}
結果:
由於數量較大,只截取了部分截圖,全部都是一樣的,不行的小夥伴可以自己測試一下
這時候我們改為序列流在執行一下
/**
* @author 我是七月呀
* @date 2020/12/22
*/
public class FindAnyDemo {
public static void main(String[] args) {
//findAny用於獲取流中隨機的某一個元素,並且利用短路在找到結果時,立即結束
ArrayList<Student> students = new ArrayList<>();
students.add(new Student(1,19,"張三1","M",true));
students.add(new Student(1,18,"張三2","M",false));
students.add(new Student(1,21,"張三3","F",true));
students.add(new Student(1,20,"張三4","F",false));
students.add(new Student(1,20,"張三5","F",false));
students.add(new Student(1,20,"張三6","F",false));
for (int i = 0; i < 100; i++) {
Optional<Student> student1 = students.parallelStream().filter(student -> student.getSex().equals("F")).findAny();
System.out.println(student1.toString());
}
}
}
結果:
現在我們通過原始碼解析來分析下這是為什麼?
根據這一段原始碼介紹,findAny對於同一資料來源的多次操作會返回不同的結果。但是,我們現在的操作是序列的, 所以在資料較少的情況下,一般會返回第一個結果,但是如果在並行的情況下,那就不能確保返回的是第一個了。 這種設計主要是為了獲取更加高效的效能。並行操作後續會做詳細介紹。
傳遞引數,指定不必須獲取第一個元素
在該方法中,主要用於判斷對於當前的操作執行並行還是序列。
在該方法中的wrapAndCopyInto()內部做的會判斷流中是否存在符合條件的元素,如果有的話,則會進行返回。結 果終會封裝到Optional中的IsPresent中。
總結:當為序列流且資料較少時,獲取的結果一般為流中第一個元素,但是當為並流行的時 候,則會隨機獲取。
5.2基於findFirst()查詢元素
findFirst使用原理與findAny類似,只是它無論序列流還是並行流都會返回第一個元素,這裡不做詳解
6、歸約
到現在截止,對於流的終端操作,我們返回的有boolean、Optional和List。但是在集合操作中,我們經常會涉及 對元素進行統計計算之類的操作,如求和、求大值、小值等,從而返回不同的資料結果。
6.1基於reduce()進行累積求和
案例:對集合中的元素求和
/**
* @author 我是七月呀
* @date 2020/12/22
*/
public class ReduceDemo {
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2);
Integer reduce = integers.stream().reduce(0, (integer1, integer2) -> integer1 + integer2);
System.out.println(reduce);
}
}
結果:53
在上述程式碼中,在reduce裡的第一個引數宣告為初始值,第二個引數接收一個lambda表示式,代表當前流中的兩 個元素,它會反覆相加每一個元素,直到流被歸約成一個終結果
Integer reduce = integers.stream().reduce(0,Integer::sum);
優化成這樣也是可以的。當然,reduce還有一個不帶初始值引數的過載方法,但是要對返回結果進行判斷,因為如果流中沒有任何元素的話,可能就沒有結果了。具體方法如下所示
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2);
Optional<Integer> reduce = integers.stream().reduce(Integer::sum);
if(reduce.isPresent()){
System.out.println(reduce);
}else {
System.out.println("資料有誤");
}
原始碼解析:兩個引數的reduce方法
在上述方法中,對於流中元素的操作,當執行第一個元素,會進入begin方法,將初始化的值給到state,state就 是後的返回結果。並執行accept方法,對state和第一個元素根據傳入的操作,對兩個值進行計算。並把終計 算結果賦給state。
當執行到流中第二個元素,直接執行accept方法,對state和第二個元素對兩個值進行計算,並把終計算結果賦 給state。後續依次類推。
可以按照下述程式碼進行理解
Tresult=identity;
for(Telement:thisstream){
result=accumulator.apply(result,element)
}
returnresult;
原始碼解析:單個引數的reduce方法
在這部分實現中,對於匿名內部類中的empty相當於是一個開關,state相當於結果。
對於流中第一個元素,首先會執行begin()將empty置為true,state為null。接著進入到accept(),判斷empty是否 為true,如果為true,則將empty置為false,同時state置為當前流中第一個元素,當執行到流中第二個元素時, 直接進入到accpet(),判斷empty是否為true,此時empty為false,則會執行apply(),對當前state和第二個元素進 行計算,並將結果賦給state。後續依次類推。
當整個流操作完之後,執行get(), 如果empty為true,則返回一個空的Optional物件,如果為false,則將後計算 完的state存入Optional中。
可以按照下述程式碼進行理解:
booleanflag=false;
Tresult=null;
for(Telement:thisstream){
if(!flag){
flag=true;
result=element;
}else{
result=accumulator.apply(result,element);
}
}
returnflag?Optional.of(result):Optional.empty();
6.2獲取流中元素的最大值、最小值
案例:獲取集合中元素的最大值、最小值
/**
* @author 我是七月呀
* @date 2020/12/22
*/
public class MaxDemo {
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2);
/**
* 獲取集合中的最大值
*/
//方法一
Optional<Integer> max1 = integers.stream().reduce(Integer::max);
if(max1.isPresent()){
System.out.println(max1);
}
//方法二
Optional<Integer> max2 = integers.stream().max(Integer::compareTo);
if(max2.isPresent()){
System.out.println(max2);
}
/**
* 獲取集合中的最小值
*/
//方法一
Optional<Integer> min1 = integers.stream().reduce(Integer::min);
if(min1.isPresent()){
System.out.println(min1);
}
//方法二
Optional<Integer> min2 = integers.stream().min(Integer::compareTo);
if(min2.isPresent()){
System.out.println(min2);
}
}
}
結果:
Optional[8]
Optional[8]
Optional[1]
Optional[1]
7、收集器
通過使用收集器,可以讓程式碼更加方便的進行簡化與重用。其內部主要核心是通過Collectors完成更加複雜的計算 轉換,從而獲取到終結果。並且Collectors內部提供了非常多的常用靜態方法,直接拿來就可以了。比方說: toList。
/**
* @author 我是七月呀
* @date 2020/12/22
*/
public class CollectDemo {
public static void main(String[] args) {
ArrayList<Student> students = new ArrayList<>();
students.add(new Student(1,19,"張三","M",true));
students.add(new Student(1,18,"李四","M",false));
students.add(new Student(1,21,"王五","F",true));
students.add(new Student(1,20,"趙六","F",false));
//通過counting()統計集合總數 方法一
Long collect = students.stream().collect(Collectors.counting());
System.out.println(collect);
//結果 4
//通過count()統計集合總數 方法二
long count = students.stream().count();
System.out.println(count);
//結果 4
//通過maxBy求最大值
Optional<Student> collect1 = students.stream().collect(Collectors.maxBy(Comparator.comparing(Student::getAge)));
if(collect1.isPresent()){
System.out.println(collect1);
}
//結果 Optional[Student{id=1, age=21, name='王五', sex='F', isPass=true}]
//通過max求最大值
Optional<Student> max = students.stream().max(Comparator.comparing(Student::getAge));
if(max.isPresent()){
System.out.println(max);
}
//結果 Optional[Student{id=1, age=21, name='王五', sex='F', isPass=true}]
//通過minBy求最小值
Optional<Student> collect2 = students.stream().collect(Collectors.minBy(Comparator.comparing(Student::getAge)));
if(collect2.isPresent()){
System.out.println(collect2);
}
//結果 Optional[Student{id=1, age=18, name='李四', sex='M', isPass=false}]
//通過min求最小值
Optional<Student> min = students.stream().min(Comparator.comparing(Student::getAge));
if(min.isPresent()){
System.out.println(min);
}
//結果 Optional[Student{id=1, age=18, name='李四', sex='M', isPass=false}]
//通過summingInt()進行資料彙總
Integer collect3 = students.stream().collect(Collectors.summingInt(Student::getAge));
System.out.println(collect3);
//結果 78
//通過averagingInt()進行平均值獲取
Double collect4 = students.stream().collect(Collectors.averagingInt(Student::getAge));
System.out.println(collect4);
//結果 19.5
//通過joining()進行資料拼接
String collect5 = students.stream().map(Student::getName).collect(Collectors.joining());
System.out.println(collect5);
//結果 張三李四王五趙六
//複雜結果的返回
IntSummaryStatistics collect6 = students.stream().collect(Collectors.summarizingInt(Student::getAge));
double average = collect6.getAverage();
long sum = collect6.getSum();
long count1 = collect6.getCount();
int max1 = collect6.getMax();
int min1 = collect6.getMin();
}
}
8、分組
在資料庫操作中,經常會通過group by對查詢結果進行分組。同時在日常開發中,也經常會涉及到這一類操作, 如通過性別對學生集合進行分組。如果通過普通編碼的方式需要編寫大量程式碼且可讀性不好。
對於這個問題的解決,java8也提供了簡化書寫的方式。通過 Collectors。groupingBy()即可。
//通過性別對學生進行分組
Map<String, List<Student>> collect = students.stream().collect(Collectors.groupingBy(Student::getSex));
結果 {
F=[Student{id=1, age=21, name='王五', sex='F', isPass=true}, Student{id=1, age=20, name='趙六', sex='F', isPass=false}],
M=[Student{id=1, age=19, name='張三', sex='M', isPass=true}, Student{id=1, age=18, name='李四', sex='M', isPass=false}]
}
8.1多級分組
剛才已經使用groupingBy()完成了分組操作,但是隻是通過單一的sex進行分組,那現在如果需求發生改變,還要 按照是否及格進行分組,能否實現?答案是可以的。對於groupingBy()它提供了兩個引數的過載方法,用於完成這 種需求。
這個過載方法在接收普通函式之外,還會再接收一個Collector型別的引數,其會在內層分組(第二個引數)結果,傳 遞給外層分組(第一個引數)作為其繼續分組的依據。
//現根據是否通過考試對學生分組,在根據性別分組
Map<String, Map<Boolean, List<Student>>> collect1 = students.stream().collect(Collectors.groupingBy(Student::getSex, Collectors.groupingBy(Student::getPass)));
結果: {
F={
false=[Student{id=1, age=20, name='趙六', sex='F', isPass=false}],
true=[Student{id=1, age=21, name='王五', sex='F', isPass=true}]
},
M={
false=[Student{id=1, age=18, name='李四', sex='M', isPass=false}],
true=[Student{id=1, age=19, name='張三', sex='M', isPass=true}]}
}
8.2多級分組變形
在日常開發中,我們很有可能不是需要返回一個數據集合,還有可能對資料進行彙總操作,比方說對於年齡18歲 的通過的有多少人,未及格的有多少人。因此,對於二級分組收集器傳遞給外層分組收集器的可以任意資料型別, 而不一定是它的資料集合。
//根據年齡進行分組,獲取並彙總人數
Map<Integer, Long> collect2 = students.stream().collect(Collectors.groupingBy(Student::getAge, Collectors.counting()));
System.out.println(collect2);
結果:{18=1, 19=1, 20=1, 21=1}
//要根據年齡與是否及格進行分組,並獲取每組中年齡的學生
Map<Integer, Map<Boolean, Student>> collect3 = students.stream().collect(Collectors.groupingBy(Student::getAge, Collectors.groupingBy(Student::getPass,
Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparing(Student::getAge)), Optional::get))));
System.out.println(collect3.toString());
結果:{
18={false=Student{id=1, age=18, name='李四', sex='M', isPass=false}},
19={true=Student{id=1, age=19, name='張三', sex='M', isPass=true}},
20={false=Student{id=1, age=20, name='趙六', sex='F', isPass=false}},
21={true=Student{id=1, age=21, name='王五', sex='F', isPass=true}}}