詳解Java8函數(shù)式編程之收集器的應(yīng)用
收集器
收集器是一種通用的、從流生成復(fù)雜值的結(jié)構(gòu)??梢允褂盟鼜牧髦猩蒐ist、Set、Map等集合。 收集器都是在流的collect方法中調(diào)用,并且都在 Collectors類中。
java 的標(biāo)準(zhǔn)類庫提供了很多有用的收集器,當(dāng)然了,也可以自己自定義(這個(gè)對于使用者的要求很高)。
下面提供一個(gè)代碼,用于測試接下里要說的收集器:
提供了一個(gè)簡單的測試數(shù)據(jù)
學(xué)號 姓名 性別 語文 數(shù)學(xué) 英語 物理 政治 總分
09509002 節(jié)強(qiáng) 男 86 90 90 93 90
09509003 楊青 女 90 90 82 91 92
09509006 徐剛 男 78 92 83 90 87
09509111 馬力 男 77 88 99 90 88
09509001 武向麗 女 90 78 83 94 94
09509007 張文靜 女 85 90 79 94 88
09509005 徐小紅 女 78 85 88 93 92
09509009 李姝 女 92 80 75 90 88
09509004 李文華 男 68 59 70 85 90
09509008 夏婧 女 87 65 73 91 95
09509010 王洪 男 66 48 89 70 57
Student 實(shí)體類封裝數(shù)據(jù)
package com.cdragon;
public class Student implements Comparable<Student> {
private String number;
private String name;
private String sex;
private Integer chinese;
private Integer math;
private Integer english;
private Integer physics;
private Integer politics;
//省略getter和setter方法,這個(gè)使用IDE自動(dòng)生成特別方便。
//省略toString方法,同上。
@Override
public int compareTo(Student s) {
return s.getNumber().compareTo(this.getNumber());
}
}
LoadData 類加載數(shù)據(jù)到內(nèi)存
package com.cdragon;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
public class LoadData {
public static List<Student> readFromFile(File file) {
List<Student> students = new ArrayList<>();
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) {
String record = null;
String header = br.readLine();
//對于數(shù)據(jù)的第一行頭,暫時(shí)不做處理。
while ((record = br.readLine() ) != null) {
Student s = resolveLineToStudent(record);
students.add(s);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return students;
}
private static Student resolveLineToStudent(String record) {
String[] array = record.split("\\s+"); // \\s 和 \\s+ 還是有區(qū)別的!
Student s = new Student();
s.setNumber(array[0]);
s.setName(array[1]);
s.setSex(array[2]);
s.setChinese(Integer.parseInt(array[3]));
s.setMath(Integer.parseInt(array[4]));
s.setEnglish(Integer.parseInt(array[5]));
s.setPhysics(Integer.parseInt(array[6]));
s.setPolitics(Integer.parseInt(array[7]));
return s;
}
}
收集器應(yīng)用
將流轉(zhuǎn)換成其他集合
使用收集器是可以生成其他集合的,例如生成List、Set 和 Map等,下面來分別舉例:
//生成 List
List<Student> studentList = students.stream().collect(Collectors.toList());
//生成 Set
Set<Student> studentSet = students.stream().collect(Collectors.toSet());
//生成指定集合
TreeSet<Student> studentTreeSet = students.stream().collect(Collectors.toCollection(TreeSet::new));
//生成 Map
Map<String, Student> studentMap = students.stream().collect(Collectors.toMap(Student::getNumber, s->s));
studentMap.forEach((no, s)->{
System.out.println(no + "->" + s);
});
說明:通常的 toList() 和 toSet() 方法是不指定生成集合的具體類型,這是由系統(tǒng)來選擇最合適的類型,但是有時(shí)候我們必須返回特定的類型集合,這就用到了 toCollection() 方法,這個(gè)方法可以指定需要生成的集合的類型,這是使用方法引用進(jìn)行簡化代碼:TreeSet::new
測試結(jié)果:

注意:生成 Map 的方式較為復(fù)雜,因?yàn)樾枰瑫r(shí)指定鍵和值。
轉(zhuǎn)換成值
使用收集器生成一個(gè)值。
最大值和最小值
Collectors 類中的 maxBy 和 minBy 允許用戶按照某種特定順序生成一個(gè)值。它們的作用就如同它們的名字一樣,分別是尋找最大值和最小值。
我寫成一個(gè)方法,這樣調(diào)用比較方便。
/**
* 獲取單科最高分。
* */
public static Optional<Student> minOrMaxSubject(List<Student> students, Comparator<? super Student> comparator) {
return students.stream().collect(Collectors.maxBy(comparator));
}
說明:使用 maxBy 或者 minBy 必須傳入一個(gè) Comparator 對象作為參數(shù),即參數(shù)為一個(gè)比較器。
測試代碼
//這個(gè)文件的路徑應(yīng)該使用自己指定的
List<Student> students = LoadData.readFromFile(new File("src/grade.txt"));
//獲取數(shù)學(xué)最高分學(xué)生
Optional<Student> s1 = TestStream.minOrMaxSubject(students, Comparator.comparing(Student::getMath));
//獲取英語最高分
Optional<Student> s2 = TestStream.minOrMaxSubject(students, Comparator.comparing(Student::getEnglish));
System.out.println(s1.get());
System.out.println(s2.get());
測試結(jié)果

說明:如果想要測試最低分,只要把上面的 maxBy 改成 minBy 就行了,或者直接更進(jìn)一步,修改參數(shù)為collect里面?zhèn)魅氲暮瘮?shù),不過那樣就會顯得格外復(fù)雜,而且不止可以查最高分和最低分了。
平均值
上面看過了最大值和最小值,現(xiàn)在來看看平均值。 下面這個(gè)方法是用來求單科平均分的。
/**
* 獲取單科平均分
* */
public static double averageScore(List<Student> students, ToIntFunction<? super Student> mapper) {
return students.stream().collect(Collectors.averagingInt(mapper));
}
測試代碼
List<Student> students = LoadData.readFromFile(new File("src/grade.txt"));
double math = TestStream.averageScore(students, Student::getMath);
System.out.println("數(shù)學(xué)的單科平均分:" + math);
測試結(jié)果

數(shù)據(jù)分塊
數(shù)據(jù)分塊是指收集器將流分為兩個(gè)集合,注意分塊是只能分成兩塊。這里標(biāo)準(zhǔn)類庫提供了一個(gè)收集器 partitioningBy,它接受一個(gè)流,并將其分為兩個(gè)部分。返回的結(jié)果為一個(gè) Map,鍵只有兩種:true 或者 false,值是滿足對應(yīng)條件的集合。
例如我想知道某們成績 90分以上和一下的學(xué)生分別是哪些。
/**
* 以特定分?jǐn)?shù)劃分不同學(xué)生,例如90分以上(含90分)和90分一下。
* 結(jié)果是一個(gè)Map集合,只有兩個(gè)元素,true false 個(gè)對應(yīng)一個(gè)集合。
* */
public static Map<Boolean, List<Student>> splitScore(List<Student> students, Predicate<? super Student> predicate) {
return students.stream().collect(Collectors.partitioningBy(predicate));
}
說明:partitioningBy的參數(shù)為一個(gè) Predicate 對象,這個(gè)和過濾器的很相似,功能上可以對比學(xué)習(xí)。
測試代碼
//數(shù)學(xué)成績以90分來劃分學(xué)生
Map<Boolean, List<Student>> booleanListMap = TestStream.splitScore(students, stu->stu.getMath()>=90);
booleanListMap.forEach((bool, list)->{
System.out.println("數(shù)學(xué)成績大于90分:" + bool);
list.forEach(System.out::println);
System.out.println("========================");
});
測試結(jié)果

數(shù)據(jù)分組
數(shù)據(jù)分組是一種更為自然的分割數(shù)據(jù)操作,與將數(shù)據(jù)分成true和false兩部分不同,可以使用任意值對數(shù)據(jù)分組。比如使用性別對學(xué)生進(jìn)行分組。這很像SQL中的 groupBy 操作。
/**
* 數(shù)據(jù)分組
* 這里以性別來分組
* */
public static Map<String, List<Student>> groupBy(List<Student> students) {
return students.stream().collect(Collectors.groupingBy(Student::getSex));
}
測試代碼
Map<String, List<Student>> stringListMap = TestStream.groupBy(students);
stringListMap.forEach((sex, list)->{
System.out.println("性別:" + sex);
list.forEach(System.out::println);
System.out.println("============");
});
測試結(jié)果

字符串
收集流中的數(shù)據(jù)最后生成一個(gè)字符串,這是一個(gè)很平常的操作。 例如一個(gè)所有學(xué)生的姓名列表,使用傳統(tǒng)的迭代列表操作代碼如下:
/**
* 獲取所有學(xué)生姓名的字符串
* 傳統(tǒng)的迭代操作
* 格式如下:[張三,李四]
* */
public static String nameStr1(List<Student> students) {
StringBuilder builder = new StringBuilder("[");
for (Student stu : students) {
if (builder.length() > 1){
builder.append(",");
}
String name = stu.getName();
builder.append(name);
}
builder.append("]");
return builder.toString();
}
然后是使用收集器進(jìn)行操作,代碼如下: 這里我添加一些細(xì)節(jié)處理,學(xué)生的排名按照學(xué)生的總成績從高到底排列,這是很符合習(xí)慣的。
/**
* 獲取所有學(xué)生姓名的字符串
* 函數(shù)式方法
* 格式如下:[張三,李四]
*
* 注意,他只能連接字符串,所有這里使用 map 操作,將 Student 轉(zhuǎn)成 String(學(xué)生姓名)
* */
public static String nameStr2(List<Student> students) {
return students.stream()
.sorted(Comparator.comparing(s -> {
return s.getChinese() + s.getMath() + s.getEnglish()
+ s.getPhysics() + s.getPolitics();
}, Comparator.reverseOrder())) //(sum1, sum2)-> sum2.compareTo(sum1)
.map(Student::getName)
.collect(Collectors.joining(",","[","]"));
}
說明:這里的 sorted 需要傳入一個(gè) Comparator 對象,但是可以使用靜態(tài)方法 Compring 進(jìn)行簡化,但是它只是指定需要排序的標(biāo)準(zhǔn),并沒有說是從小到大還是從大到小,后來才發(fā)現(xiàn),這個(gè)是默認(rèn)的:大小到大排序。但是我需要的是使用從大到小,然后發(fā)現(xiàn)原來 compring 還有重載方法,具有兩個(gè)參數(shù),另一個(gè)參數(shù)是可以指定大小順序的,所以第二個(gè)參數(shù)我傳入了一個(gè) Lambda 表達(dá)式:
(sum1, sum2)-> sum2.compareTo(sum1)
但是如果這樣使用的話,還不如直接使用 Lambda 表達(dá)式創(chuàng)建 Comparator 對象方便呢,后來發(fā)現(xiàn)這個(gè) IDE 比較智能,它指出這句代碼,可以被替換為:
Comparator.reverseOrder();// 看意思就知道是 反序的意思。
這樣看來使用 Comparator 靜態(tài)的 comparing 方法還是比直接創(chuàng)建 Comparator 對象簡單一些。
注意:如果不需要排序的話,就只有一個(gè)map方法和join方法了。這個(gè)map方法的作用是映射(我一開始把它和map集合總是搞混了),將Student對象映射為name字符串,然后使用 join 方法進(jìn)行連接。
組合收集器
收集器還可以組合起來使用,這個(gè)和 SQL 感覺更像了,幾乎具有函數(shù)式編程的語言,都有SQL那種處理數(shù)據(jù)的方式,例如最大值、最小值和分組等操作。 考慮對于學(xué)生按照性別分組,然后再分別統(tǒng)計(jì)男女生的人數(shù)。(這個(gè)在 SQL 里面也是一個(gè)基本的練習(xí)。)
/**
* 組合收集器
* 這里以性別來分組,再分別計(jì)數(shù)
* */
public static Map<String, Long> combination(List<Student> students) {
return students.stream().collect(Collectors.groupingBy(Student::getSex, Collectors.counting()));
}
測試代碼
Map<String, Long> stringLongMap = TestStream.combination(students);
stringLongMap.forEach((sex, count)->{
System.out.println("性別:" + sex + ", 人數(shù):" + count);
});
測試結(jié)果

使用流的其他操作
對于流的使用,應(yīng)該達(dá)到一個(gè)較為熟練的地步,但是由于沒有什么機(jī)會實(shí)踐,還是比較陌生。下面介紹幾個(gè)我寫的方法,來看看流的操作:
//通過過濾器選擇特定的學(xué)生,過濾器用于過濾,然后選擇第一個(gè)學(xué)生。
//這里應(yīng)該加一個(gè)排序操作比較好。
public static Optional<Student> selectStudent(List<Student> students, Predicate<? super Student> pre) {
return students.stream().filter(pre).findFirst();
}
public static List<Student> orderBy(List<Student> students, Comparator<? super Student> comparator) {
if (comparator != null){
return students.stream().sorted(comparator).collect(Collectors.toList());
} else {
return students.stream().sorted().collect(Collectors.toList());
}
}
//獲取一列數(shù)據(jù)。不是一行學(xué)生記錄,是一列。
public static List<?> getAColumn(List<Student> students, Function<? super Student, ?> mapper) {
return students.stream().map(mapper).collect(Collectors.toList());
}
/**
* 獲取所有學(xué)生的總分和學(xué)號
* */
public static Map<String, Integer> getSum(List<Student> students) {
return students.stream().collect(Collectors.toMap(Student::getNumber, stu->{
return stu.getChinese() + stu.getEnglish() + stu.getMath() + stu.getPhysics() + stu.getPolitics();
}));
}
/**
* peek 和 forEach 的區(qū)別
* peek 是一個(gè)中間操作,forEach 是一個(gè)終結(jié)操作。
*
* 假如實(shí)現(xiàn)一個(gè)功能:每個(gè)學(xué)生的某門科目分?jǐn)?shù)進(jìn)行修改。
*
* peek 操作后得到的仍然是一個(gè) stream,此時(shí)可以進(jìn)一步操作,
* 但是 forEach 是終結(jié)操作,操作結(jié)束,流就結(jié)束了,如果需要進(jìn)一步處理,
* 必須再次進(jìn)行得到流的操作。
* */
public static List<Student> addScore1(List<Student> students, Consumer<? super Student> action) {
return students.stream().peek(action).collect(Collectors.toList());
}
public static void addScore2(List<Student> students, Consumer<? super Student> action) {
students.stream().forEach(action);
}
//指定返回類型為 LinkedList,這時(shí)一個(gè)測試,并不是說需要這樣寫。
//多數(shù)情況下,我們還是應(yīng)該使用 ArrayList
public static List<Student> addScore3(List<Student> students, Consumer<? super Student> action) {
return students.stream().peek(action).collect(Collectors.toCollection(LinkedList::new));
}
對于其中的幾個(gè)進(jìn)行測試(不是全部方法,如果感興趣,可以自己嘗試。):
//對于學(xué)生進(jìn)行排序,參數(shù)為一個(gè)比較器,參數(shù)為空的話,使用默認(rèn)的 sorted 排序。
//測試代碼 按照學(xué)號排序(默認(rèn)從小到大)
TestStream.orderBy(students,Comparator.comparing(Student::getNumber)).forEach(System.out::println);
//按照學(xué)號排序(從大到?。?
TestStream.orderBy(students,Comparator.comparing(Student::getNumber, Comparator.reverseOrder())).forEach(System.out::println);
//使用默認(rèn)的排序
TestStream.orderBy(students).forEach(System.out::println);
//獲取一列學(xué)生的記錄,例如這里是英語成績,這里返回值我使用通配符應(yīng)該沒有錯(cuò)吧
//因?yàn)榉祷財(cái)?shù)據(jù)可能為 String 也可能是 Integer
TestStream.getAColumn(students,Student::getEnglish).forEach(System.out::println);
//測試學(xué)生的總分
TestStream.getSum(students).forEach((no, stu)->{
System.out.println(no + " -> " + stu);
});
總結(jié)
雖然這個(gè)收集器(也可以說Java的函數(shù)式編程)有的使用起來感覺很簡單、簡潔(當(dāng)然了它的目的也是如此),但是內(nèi)部實(shí)現(xiàn)看著還是感覺無從下手,大量使用了泛型、通配符上下限這些方面的知識,這對于使用來說可以忽視,但是如果想要深入了解的話,還是很有難度的,我就先看到這里吧,消化消化知識再說,哈哈!
到此這篇關(guān)于詳解Java8函數(shù)式編程之收集器的應(yīng)用的文章就介紹到這了,更多相關(guān)Java函數(shù)式編程收集器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot2.3.0配置JPA的實(shí)現(xiàn)示例
這篇文章主要介紹了SpringBoot2.3.0配置JPA的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
springboot項(xiàng)目數(shù)據(jù)庫密碼如何加密
在我們?nèi)粘i_發(fā)中,我們可能很隨意把數(shù)據(jù)庫密碼直接明文暴露在配置文件中,今天就來聊聊在springboot項(xiàng)目中如何對數(shù)據(jù)庫密碼進(jìn)行加密,感興趣的可以了解一下2021-07-07
SpringCloud中的斷路器(Hystrix)和斷路器監(jiān)控(Dashboard)
本篇主要介紹的是SpringCloud中的斷路器(Hystrix)和斷路器指標(biāo)看板(Dashboard)的相關(guān)使用知識,需要的朋友可以參考下2019-06-06
Spring中@Import的各種用法以及ImportAware接口詳解
這篇文章主要介紹了Spring中@Import的各種用法以及ImportAware接口詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10

