強大的 Stream 函數(shù)式編程
前言
Java8(又稱為 Jdk1.8)是 Java 語言開發(fā)的一個主要版本。Oracle 公司于 2014 年 3 月 18 日發(fā)布 Java8,它支持函數(shù)式編程,新的 JavaScript 引擎,新的日期 API,新的 Stream API 等。Java8 API 添加了一個新的抽象稱為流 Stream,可以讓你以一種聲明的方式處理數(shù)據(jù)。Stream API 可以極大提高 Java 程序員的生產(chǎn)力,讓程序員寫出高效率、干凈、簡潔的代碼。
Java8 新特性
Lambda 表達式 ? Lambda 允許把函數(shù)作為一個方法的參數(shù)(函數(shù)作為參數(shù)傳遞進方法中)。
方法引用 ? 方法引用提供了非常有用的語法,可以直接引用已有 Java 類或對象(實例)的方法或構造器。與 lambda 聯(lián)合使用,方法引用可以使語言的構造更緊湊簡潔,減少冗余代碼。
默認方法 ? 默認方法就是一個在接口里面有了一個實現(xiàn)的方法。
新工具 ? 新的編譯工具,如:Nashorn 引擎 jjs、類依賴分析器 jdeps。
Stream API ? 新添加的 Stream API(java.util.stream)把真正的函數(shù)式編程風格引入到 Java 中。
Date Time API ? 加強對日期與時間的處理。
Optional 類 ? Optional 類已經(jīng)成為 Java8 類庫的一部分,用來解決空指針異常。
Nashorn JavaScript 引擎 ? Java8 提供了一個新的 Nashorn javascript 引擎,它允許我們在 JVM 上運行特定的 javascript 應用。
為什么需要 Steam?
Java8 中的 Stream 是對集合(Collection)對象功能的增強,它專注于對集合對象進行各種非常便利、高效的聚合操作,或者大批量數(shù)據(jù)操作。
StreamAPI 借助于同樣新出現(xiàn)的 Lambda 表達式,極大的提高編程效率和程序可讀性。同時它提供串行和并行兩種模式進行匯聚操作,并發(fā)模式能夠充分利用多核處理器的優(yōu)勢,使用 fork/join 并行方式來拆分任務和加速處理過程。
流的操作種類
中間操作
當數(shù)據(jù)源中的數(shù)據(jù)上了流水線后,這個過程對數(shù)據(jù)進行的所有操作都稱為“中間操作”。
中間操作仍然會返回一個流對象,因此多個中間操作可以串連起來形成一個流水線。
終端操作
當所有的中間操作完成后,若要將數(shù)據(jù)從流水線上拿下來,則需要執(zhí)行終端操作。
終端操作將返回一個執(zhí)行結果,這就是你想要的數(shù)據(jù)。
java.util.Stream 使用示例
定義一個簡單的學生實體類,用于后面的例子演示:
public class Student {
/** 學號 */
private long id;
/** 姓名 */
private String name;
/** 年齡 */
private int age;
/** 性別 */
private int grade;
/** 專業(yè) */
private String major;
/** 學校 */
private String school;
// 省略 getter 和 setter
}
// 初始化
List<Student> students = new ArrayList<Student>() {
{
add(new Student(20160001, "孔明", 20, 1, "土木工程", "武漢大學"));
add(new Student(20160002, "伯約", 21, 2, "信息安全", "武漢大學"));
add(new Student(20160003, "玄德", 22, 3, "經(jīng)濟管理", "武漢大學"));
add(new Student(20160004, "云長", 21, 2, "信息安全", "武漢大學"));
add(new Student(20161001, "翼德", 21, 2, "機械與自動化", "華中科技大學"));
add(new Student(20161002, "元直", 23, 4, "土木工程", "華中科技大學"));
add(new Student(20161003, "奉孝", 23, 4, "計算機科學", "華中科技大學"));
add(new Student(20162001, "仲謀", 22, 3, "土木工程", "浙江大學"));
add(new Student(20162002, "魯肅", 23, 4, "計算機科學", "浙江大學"));
add(new Student(20163001, "丁奉", 24, 5, "土木工程", "南京大學"));
}
};
forEach
Stream 提供了新的方法’forEach’來迭代流中的每個數(shù)據(jù)。ForEach 接受一個 function 接口類型的變量,用來執(zhí)行對每一個元素的操作。ForEach 是一個中止操作,它不返回流,所以我們不能再調(diào)用其他的流操作。
以下代碼片段使用 forEach 輸出了 10 個隨機數(shù):
// 隨機生成 10 個 0,100int 類型隨機數(shù)
new Random()
.ints(0, 100)
.limit(10)
.forEach(System.out::println);從集合 students 中篩選出所有武漢大學的學生:
List<Student> whuStudents = students
.stream()
.filter(student -> "武漢大學".equals(student.getSchool()))
.collect(Collectors.toList());filter/distinct
filter 方法用于通過設置的條件過濾出元素。Filter 接受一個 predicate 接口類型的變量,并將所有流對象中的元素進行過濾。該操作是一個中間操作,因此它允許我們在返回結果的基礎上再進行其他的流操作。
以下代碼片段使用 filter 方法過濾出空字符串:
// 獲取空字符串的數(shù)量
Arrays.asList("abc", "","bc","efg","abcd","", "jkl")
// stream() ? 為集合創(chuàng)建串行流
.stream()
.filter(string -> string.isEmpty())
.count();distinct 方法用于去除重復元素。
Arrays.asList("a", "c", "ac", "c", "a", "b")
.stream()
.distinct()
.forEach(System.out::println);anyMatch/allMatch/noneMatch
匹配操作有多種不同的類型,都是用來判斷某一種規(guī)則是否與流對象相互吻合的。所有的匹配操作都是終結操作,只返回一個 boolean 類型的結果。
anyMatch 方法用于判斷集合中是否有任一元素滿足條件。
// 集合中是否有任一元素匹配以'a'開頭
boolean result = Arrays.asList("abc", "","bc","efg","abcd","", "jkl")
.stream()
.anyMatch(s -> s.startsWith("a"));allMatch 方法用于判斷集合中是否所有元素滿足條件。
// 集合中是否所有元素匹配以'a'開頭
boolean result = Arrays.asList("abc", "","bc","efg","abcd","", "jkl")
.stream()
.allMatch(s -> s.startsWith("a"));noneMatch 方法用于判斷集合中是否所有元素不滿足條件。
// 集合中是否沒有元素匹配以'a'開頭
boolean result = Arrays.asList("abc", "","bc","efg","abcd","", "jkl")
.stream()
.noneMatch(s -> s.startsWith("a"));limit/skip
limit 方法用于返回前面 n 個元素。
Arrays.asList("abc", "","bc","efg","abcd","", "jkl")
.stream()
.filter(string -> !string.isEmpty())
.limit(3)
.forEach(System.out::println);skip 方法用于舍棄前 n 個元素。
Arrays.asList("abc", "","bc","efg","abcd","", "jkl")
.stream()
.filter(string -> !string.isEmpty())
.skip(1)
.forEach(System.out::println);sorted
sorted 方法用于對流進行排序。Sorted 是一個中間操作,能夠返回一個排過序的流對象的視圖。流對象中的元素會默認按照自然順序進行排序,除非你自己指定一個 Comparator 接口來改變排序規(guī)則。
以下代碼片段使用 filter 方法過濾掉空字符串,并對其進行自然順序排序:
List<String> strings = Arrays.asList("abc", "","bc","efg","abcd","", "jkl");
// 一定要記住, sorted 只是創(chuàng)建一個流對象排序的視圖, 而不會改變原來集合中元素的順序。
strings
.stream()
.filter(string -> !string.isEmpty())
.sorted()
.forEach(System.out::println);
// 輸出原始集合元素, sorted 只是創(chuàng)建排序視圖, 不影響原來集合順序
strings
.stream()
.forEach(System.out::println);
// 按照字符串長度進行排序, 若兩個字符串長度相同, 按照字母順序排列
strings
.stream()
.filter(string -> !string.isEmpty())
// 1. 首先根據(jù)字符串長度倒序排序; 2. 然后根據(jù)字母順序排列
.sorted(Comparator.comparing(String::length).reversed().thenComparing(String::compareTo))
.forEach(System.out::println);以下代碼片段根據(jù) Person 姓名倒序排序,然后利用 Collectors 返回列表新列表:
List<Person> persons = new ArrayList();
// 1. 生成 5 個 Person 對象
for (int i = 1; i <= 5; i++) {
Person person = new Person(i, "name" + i);
persons.add(person);
}
// 2. 對 Person 列表進行排序, 排序規(guī)則: 根據(jù) Person 姓名倒序排序, 然后利用 Collectors 返回列表新列表;
List<Person> personList = persons
.stream()
.sorted(Comparator.comparing(Person::getName).reversed())
.collect(Collectors.toList());parallel
流操作可以是順序的,也可以是并行的。順序操作通過單線程執(zhí)行,而并行操作則通過多線程執(zhí)行??墒褂貌⑿辛鬟M行操作來提高運行效率 parallelStream 是流并行處理程序的代替方法。
parallelStream()本質上基于 Java7 的 Fork-Join 框架實現(xiàn),F(xiàn)ork-Join 是一個處理并行分解的高性能框架,其默認的線程數(shù)為宿主機的內(nèi)核數(shù)。
以下實例我們使用 parallelStream 來輸出空字符串的數(shù)量:
// 獲取空字符串的數(shù)量[parallelStream 為 Collection 接口的一個默認方法]
Arrays.asList("abc", "","bc","efg","abcd","", "jkl")
// parallelStream() ? 為集合創(chuàng)建并行流
.parallelStream()
.filter(string -> string.isEmpty())
.count();parallelStream 中 forEachOrdered 與 forEach 區(qū)別:
List<String> strings = Arrays.asList("a", "b", "c");
strings.stream().forEachOrdered(System.out::print); //abc
strings.stream().forEach(System.out::print); //abc
strings.parallelStream().forEachOrdered(System.out::print); //abc
strings.parallelStream().forEach(System.out::print); //bca特別注意:1、千萬不要任意地并行 Stream pipeline,如果源頭是來自 stream.iterate,或者中間使用了中間操作的 limit,那么并行 pipeline 也不可能提升性能。因此,在 Stream 上通過并行獲取的性能,最好是通過 ArrayList、HashMap、HashSet 和 CouncurrentHashMap 實例,數(shù)組,int 范圍和 long 范圍等。這些數(shù)據(jù)結構的共性是,都可以被精確、輕松地分成任意大小的子范圍,使并行線程中的分工變得更加輕松。2、Stream pipeline 的終止操作本質上也影響了并發(fā)執(zhí)行的效率。并行的最佳操作是做減法,用一個 Stream 的 reduce 方法,將所有從 pipeline 產(chǎn)生的元素都合并在一起,或者預先打包想 min、max、count 和 sum 這類方法。驟死式操作如 anyMatch、allMatch 和 nonMatch 也都可以并行。由 Stream 的 collect 方法執(zhí)行的操作,都是可變的減法,不是并行的最好選擇,因此并行集合的成本非常高。3、一般來說,程序中所有的并行 Stream pipeline 都是在一個通用的 fork-join 池中運行的。只要有一個 pipeline 運行異常,都是損害到系統(tǒng)中其它不相關部分的性能。因此,如果對 Stream 進行不恰當?shù)牟⑿胁僮鳎赡軐е鲁绦蜻\行失敗,或者造成性能災難。
map
map 方法用于映射每個元素到對應的結果。map 是一個對于流對象的中間操作,通過給定的方法,它能夠把流對象中的每一個元素對應到另外一個對象上。
以下代碼片段使用 map 將集合元素轉為大寫 (每個元素映射到大寫)-> 降序排序 ->迭代輸出:
Arrays.asList("abc", "","bc","efg","abcd","", "jkl")
// 通過 stream()方法即可獲取流對象
.stream()
// 通過 filter()過濾元素
.filter(string -> !string.isEmpty())
// 通過 map()方法用于映射每個元素到對應的結果
.map(String::toUpperCase)
// 通過 sorted()方法用于對流進行排序
.sorted(Comparator.reverseOrder())
// 通過 forEach()方法迭代流中的每個數(shù)據(jù)
.forEach(System.out::println);篩選出所有專業(yè)為計算機科學的學生姓名:
List<String> names = students
.stream()
.filter(student -> "計算機科學".equals(student.getMajor()))
.map(Student::getName).collect(Collectors.toList());
計算所有專業(yè)為計算機科學學生的年齡之和:
int totalAge = students
.stream()
.filter(student -> "計算機科學".equals(student.getMajor()))
.mapToInt(Student::getAge).sum();
peek
peek 操作接收的是一個 Consumer<T> 函數(shù)。顧名思義 peek 操作會按照 Consumer<T> 函數(shù)提供的邏輯去消費流中的每一個元素,同時有可能改變元素內(nèi)部的一些屬性。
按照 Java 團隊的說法,peek() 方法存在的主要目的是用調(diào)試,通過 peek() 方法可以看到流中的數(shù)據(jù)經(jīng)過每個處理點時的狀態(tài)。
Stream.of("one", "two", "three","four").filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e))
.collect(Collectors.toList());
除去用于調(diào)試,peek() 在需要修改元素內(nèi)部狀態(tài)的場景也非常有用,比如我們想將所有 Student 的名字修改為大寫,當然也可以使用 map() 和 flatMap() 實現(xiàn),但是相比來說 peek() 更加方便,因為我們并不想替代流中的數(shù)據(jù)。
students
.stream()
.peek(student -> student.setName(student.getName().toUpperCase()))
.forEach(System.out::println);
那么 peek() 和 map() 有什么區(qū)別呢?peek 接收一個 Consumer,而 map 接收一個 Function。Consumer 是沒有返回值的,它只是對 Stream 中的元素進行某些操作,但是操作之后的數(shù)據(jù)并不返回到 Stream 中,所以 Stream 中的元素還是原來的元素。而 Function 是有返回值的,這意味著對于 Stream 的元素的所有操作都會作為新的結果返回到 Stream 中。
findFirst/findAny
findAny 能夠從流中隨便選一個元素出來,它返回一個 Optional 類型的元素。
Optional<String> optional = Arrays.asList("abc", "","bc","efg","abcd","", "jkl")
.stream()
.findAny();
findFirst 能夠從流中選第一個元素出來,它返回一個 Optional 類型的元素。
Optional<String> optional = Arrays.asList("abc", "","bc","efg","abcd","", "jkl")
.stream()
.findFirst();
collect
collect 方法是一個終端操作,它接收的參數(shù)是將流中的元素累積到匯總結果的各種方式(稱為收集器)。
Collectors 工具類提供了許多靜態(tài)工具方法來為大多數(shù)常用的用戶用例創(chuàng)建收集器,比如將元素裝進一個集合中、將元素分組、根據(jù)不同標準對元素進行匯總等。
Collectors.joining()
Collectors.joining()方法以遭遇元素的順序拼接元素。我們可以傳遞可選的拼接字符串、前綴和后綴。
List<String> strings = Arrays.asList("abc", "","bc","efg","abcd","", "jkl");
// 篩選列表
List<String> filtered = strings
.stream()
.filter(string -> !string.isEmpty())
.collect(Collectors.toList());
// 合并字符串
String mergedString = strings
.stream()
.filter(string -> !string.isEmpty())
.collect(Collectors.joining(","));
Collectors.groupingBy
Collectors.groupingBy 方法根據(jù)項目的一個屬性的值對流中的項目作問組,并將屬性值作為結果 Map 的鍵。
List 里面的對象元素,以某個屬性來分組。
// 按學校對學生進行分組:
Map<String, List<Student>> groups = students
.stream()
.collect(Collectors.groupingBy(Student::getSchool));
// 多級分組, 在按學校分組的基礎之上再按照專業(yè)進行分組
Map<String, Map<String, List<Student>>> groups2 = students
.stream()
.collect(
Collectors.groupingBy(Student::getSchool, // 一級分組,按學校
Collectors.groupingBy(Student::getMajor))); // 二級分組,按專業(yè)
統(tǒng)計 List 集合重復元素出現(xiàn)次數(shù)。
List<String> items = Arrays.asList("apple", "apple", "banana", "apple", "orange", "banana", "papaya");
// 方式一
Map<String, Long> result = items
.stream()
// Function.identity() 返回一個輸出跟輸入一樣的 Lambda 表達式對象, 等價于形如 t -> t 形式的 Lambda 表達式.
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
// 方式二
Map<String, Long> result2 = items
.stream()
// Collectors.counting() 計算流中數(shù)量
.collect(Collectors.groupingBy(String::toString, Collectors.counting()));
// Output :
// {papaya=1, orange=1, banana=2, apple=3}
統(tǒng)計每個組的個數(shù):
Map<String, Long> groups = students
.stream()
.collect(Collectors.groupingBy(Student::getSchool, Collectors.counting()));
累加求和
// 統(tǒng)計相同姓名, 總年齡大小
Map<String, Integer> sumMap = persons
.stream()
// Collectors.summingInt() 返回流中整數(shù)屬性求和
.collect(Collectors.groupingBy(Person::getName, Collectors.summingInt(Person::getAge)));
轉換
// 按照姓名對學生分布組,并只保留員工的年齡
Map<String, List<String>> nameMap = persons
.stream()
.collect(Collectors.groupingBy(Person::getName,
Collectors.mapping(Employee::getName, // 下游收集器
Collectors.toList()))); // 更下游的收集器
Collectors.toMap
Collectors.toMap 方法將 List 轉 Map。
// 根據(jù) Person 年齡生成 Map
Map<Integer, Person> personMap = persons
.stream()
.collect(Collectors.toMap(Person::getAge, person -> person));
// account -> account 是一個返回本身的 lambda 表達式, 其實還可以使用 Function 接口中的一個默認方法代替, 使整個方法更簡潔優(yōu)雅.
Map<Integer, Person> personMap = persons
.stream()
.collect(Collectors.toMap(Person::getAge, Function.identity()));
當 key 重復時,會拋出異常:java.lang.IllegalStateException: Duplicate key **
// 針對重復 key 的, 覆蓋之前的 value
Map<Integer, Person> personMap = persons
.stream()
.collect(Collectors.toMap(Person::getAge, Function.identity(), (person, person2) -> person2));
指定具體收集的 map:
Map<Integer, Person> personMap = persons
.stream()
.collect(Collectors.toMap(Person::getAge, Function.identity(), (person, person2) -> person2, LinkedHashMap::new));
當 value 為 null 時,會拋出異常:java.lang.NullPointerException[Collectors.toMap 底層是基于 Map.merge 方法來實現(xiàn)的,而 merge 中 value 是不能為 null 的,如果為 null,就會拋出空指針異常。]
Map<Integer, String> personMap = persons
.stream()
.collect(Collectors.toMap(Person::getAge, Person::getName, (person, person2) -> person2));
// 1. 解決方式 1: 用 for 循環(huán)的方式亦或是 forEach 的方式
Map<Integer, String> personMap = new HashMap<>();
for (Person person : persons) {
personMap.put(person.getAge(), person.getName());
}
// 2. 解決方式 2: 使用 stream 的 collect 的重載方法
Map<Integer, String> personMap = persons
.stream()
.collect(HashMap::new, (m, v) -> m.put(v.getAge(), v.getName()), HashMap::putAll);
Collectors.collectingAndThen
Collectors.collectingAndThen 方法主要用于轉換函數(shù)返回的類型。
List 里面的對象元素,以某個屬性去除重復元素。
List<Person> unique = persons
.stream()
.collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparingInt(Person::getAge))), ArrayList::new));
Collectors.partitioningBy
Collectors.partitioningBy 方法主要用于根據(jù)對流中每個項目應用謂詞的結果來對項目進行分區(qū)。
“年齡小于 18”進行分組后可以看到,不到 18 歲的未成年人是一組,成年人是另外一組。
Map<Boolean, List<Person>> groupBy = persons
.stream()
.collect(Collectors.partitioningBy(o -> o.getAge() >= 18));
Collectors 收集器靜態(tài)方法:

Collectors 收集器靜態(tài)方法

Collectors 收集器靜態(tài)方法
數(shù)值流的使用
在 Stream 里元素都是對象,那么,當我們操作一個數(shù)字流的時候就不得不考慮一個問題,拆箱和裝箱。雖然自動拆箱不需要我們處理,但依舊有隱含的成本在里面。Java8 引入了 3 個原始類型特化流接口來解決這個問題:IntStream、DoubleStream、LongStream,分別將流中的元素特化為 int、long、double,從而避免了暗含的裝箱成本。
將對象流映射為數(shù)值流
// 將對象流映射為數(shù)值流
IntStream intStream = persons
.stream()
.mapToInt(Person::getAge);
默認值 OptinalInt
由于數(shù)值流經(jīng)常會有默認值,比如默認為 0。數(shù)值特化流的終端操作會返回一個 OptinalXXX 對象而不是數(shù)值。
// 每種數(shù)值流都提供了數(shù)值計算函數(shù), 如 max、min、sum 等
OptionalInt optionalInt = persons
.stream()
.mapToInt(Person::getAge)
.max();
int max = optionalInt.orElse(1);
生成一個數(shù)值范圍流
// 創(chuàng)建一個包含兩端的數(shù)值流, 比如 1 到 10, 包含 10:
IntStream intStream = IntStream.rangeClosed(1, 10);
// 創(chuàng)建一個不包含結尾的數(shù)值流, 比如 1 到 9:
IntStream range = IntStream.range(1, 9);
將數(shù)值流轉回對象流
// 將數(shù)值流轉回對象流
Stream<Integer> boxed = intStream.boxed();
流的扁平化
案例:對給定單詞列表 [“Hello”,”World”],你想返回列表[“H”,”e”,”l”,”o”,”W”,”r”,”d”]
方法一:錯誤方式
String[] words = new String[]{"Hello", "World"};
List<String[]> a = Arrays.stream(words)
.map(word -> word.split(""))
.distinct()
.collect(Collectors.toList());
a.forEach(System.out::print);
// Output
// [Ljava.lang.String;@12edcd21[Ljava.lang.String;@34c45dca
返回一個包含兩個 String[]的 list,傳遞給 map 方法的 lambda 為每個單詞生成了一個 String[]。因此,map 返回的流實際上是 Stream<String[]>類型的。

方法二:正確方式
String[] words = new String[]{"Hello", "World"};
List<String> a = Arrays.stream(words)
.map(word -> word.split(""))
.flatMap(Arrays::stream)
.distinct()
.collect(Collectors.toList());
a.forEach(System.out::print);
// Output
// HeloWrd使用 flatMap 方法的效果是,各個數(shù)組并不是分別映射一個流,而是映射成流的內(nèi)容,所有使用 map(Array::stream)時生成的單個流被合并起來,即扁平化為一個流。

最近給大家找了 Vue進階
資源,怎么領?。?/span>
掃二維碼,加我微信,回復:Vue進階
注意,不要亂回復 沒錯,不是機器人 記得一定要等待,等待才有好東西
