Stream流式運算
一、Stream的定義
Java 8 是一個非常成功的版本,這個版本新增的Stream
,配合同版本出現的 Lambda
,給我們操作集合(Collection)提供了極大的便利。
那么什么是Stream
?
Stream
將要處理的元素集合看作一種流,在流的過程中,借助Stream API
對流中的元素進行操作,比如:篩選、排序、聚合等。
二、對流的操作
Stream
可以由數組或集合創建,對流的操作分為兩種:
- 中間操作,每次返回一個新的流,可以有多個。
- 終端操作,每個流只能進行一次終端操作,終端操作結束后流無法再次使用。終端操作會產生一個新的集合或值。
另外,Stream
有幾個特性:
- stream不存儲數據,而是按照特定的規則對數據進行計算,一般會輸出結果。
- stream不會改變數據源,通常情況下會產生一個新的集合或一個值。
- stream具有延遲執行特性,只有調用終端操作時,中間操作才會執行。
可以理解為:一次性流
三、一次性的流
流和迭代器類似,只能迭代一次。
Stream<String> stream = list.stream().map(Person::getName).sorted().limit(10);
List<String> newList = stream.collect(Collectors.toList());
List<String> newList2 = stream.collect(Collectors.toList());
上面代碼中第三行會報錯,因為第二行已經使用過這個流,這個流已經被消費掉了。
整個流操作就是一條流水線,將元素放在流水線上一個個地進行處理。所以可以理解為叫做流
。
其中數據源便是原始集合,然后將如 List 的集合轉換為 Stream 類型的流,並對流進行一系列的中間操作,比如過濾保留部分元素、對元素進行排序、類型轉換等;最后再進行一個終端操作,可以把 Stream 轉換回集合類型,也可以直接對其中的各個元素進行處理,比如打印、比如計算總數、計算最大值等等。
很重要的一點是,很多流操作本身就會返回一個流,所以多個操作可以直接連接起來,我們來看看一條 Stream 操作的代碼:
如果是以前,進行這么一系列操作,你需要做個迭代器或者 foreach 循環,然后遍歷,一步步地親力親為地去完成這些操作;但是如果使用流,你便可以直接聲明式地下指令,流會幫你完成這些操作。
四、Stream與SQL語言類似
1.均為聲明式方式
Stream 中文稱為 “流”,通過將集合轉換為這么一種叫做 “流” 的元素序列,通過聲明式方式,能夠對集合中的每個元素進行一系列並行或串行的流水線操作。
換句話說,你只需要告訴流你的要求,流便會在背后自行根據要求對元素進行處理,而你只需要 “坐享其成”。
就像 SQL 語句一樣,只要直接聲明式地下指令,SQL語句就會完成操作。例如: select username from user where id = 1,你只要說明:“我需要 id 是 1 (id = 1)的用戶(user)的用戶名(username )”,那么就可以得到自己想要的數據,而不需要自己親自去數據庫里面循環遍歷查找。
2.均可操作
並且SQL可以操作的,而一般流式計算也可以操作的。比如篩選、映射、統計等操作,都是和SQL語句十分相似的。
五、Stream 和集合的差異
1.計算時間不同
Stream 和集合的其中一個差異在於什么時候進行計算。
一個集合,它會包含當前數據結構中所有的值,你可以隨時增刪,但是集合里面的元素毫無疑問地都是已經計算好了的。
流則是按需計算,你可以想象一個水龍頭,假設你需要一個奇數流,從 1 開始,那么這個水龍頭會源源不斷地流出你需要的數據,假設你只需要 10 個,那么這個流便會按需生成 10 個奇數,換句話來說,就是在用戶要求的時候才會計算值,只要你需要,你便可以打開這個水龍頭。
又比方說我們通過搜索引擎進行搜索,搜索出來的條目並不是全部呈現出來的,而且先顯示最符合的前 10 條或者前 20 條,只有在點擊 “下一頁” 的時候,才會再輸出新的 10 條。
再比方在線觀看電影和你硬盤里面的電影,也是差不多的道理。
2.迭代不同
外部迭代和內部迭代
Stream 和集合的另一個差異在於迭代。
我們可以把集合比作一個工廠的倉庫,一開始工廠比較落后,要對貨物作什么修改,只能工人親自走進倉庫對貨物進行處理,有時候還要將處理后的貨物放到一個新的倉庫里面。在這個時期,我們需要親自去做迭代,一個個地找到需要的貨物,並進行處理,這叫做外部迭代。
后來工廠發展了起來,配備了流水線作業,只要根據需求設計出相應的流水線,然后工人只要把貨物放到流水線上,就可以等着接收成果了,而且流水線還可以根據要求直接把貨物輸送到相應的倉庫。這就叫做內部迭代,流水線已經幫你把迭代給完成了,你只需要說要干什么就可以了(即設計出合理的流水線)。
Java 8 引入 Stream 很大程度是因為,流的內部迭代可以自動選擇一種合適你硬件的數據表示和並行實現;而以往程序員自己進行 foreach 之類的時候,則需要自己去管理並行等問題。
六、Optional 類
Optional
類是一個可以為null
的容器對象。如果值存在則isPresent()
方法會返回true
,調用get()
方法會返回該對象。
NullPointerException 可以說是每一個 Java 程序員都非常討厭看到的一個詞,針對這個問題, Java 8 引入了一個新的容器類 Optional,可以代表一個值存在或不存在,這樣就不用返回容易出問題的 null。之前文章的代碼中就經常出現這個類,也是針對這個問題進行的改進。
Optional 類比較常用的幾個方法有:
- isPresent() :值存在時返回 true,反之 flase
- get() :返回當前值,若值不存在會拋出異常
- orElse(T) :值存在時返回該值,否則返回 T 的值
Optional 類還有三個特化版本 OptionalInt,OptionalLong,OptionalDouble,在后面的數值流中的 max 方法返回的類型便是這個。
八、Stream的使用
案例中使用的員工類:
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, "male", "New York"));
personList.add(new Person("Jack", 7000, "male", "Washington"));
personList.add(new Person("Lily", 7800, "female", "Washington"));
personList.add(new Person("Anni", 8200, "female", "New York"));
personList.add(new Person("Owen", 9500, "male", "New York"));
personList.add(new Person("Alisa", 7900, "female", "New York"));
class Person {
private String name; // 姓名
private int salary; // 薪資
private int age; // 年齡
private String sex; //性別
private String area; // 地區
// 構造方法
public Person(String name, int salary, int age,String sex,String area) {
this.name = name;
this.salary = salary;
this.age = age;
this.sex = sex;
this.area = area;
}
// 省略了get和set,請自行添加
}
1.遍歷forEach()
返回結果為 void,很明顯我們可以通過它來干什么了,比方說:
//打印各個元素:
list.stream().forEach(System.out::println);
12
再比如說 MyBatis 里面訪問數據庫的 mapper 方法:
//向數據庫插入新元素:
list.stream().forEach(PersonMapper::insertPerson);
Stream
也是支持類似集合的遍歷和匹配元素的,只是Stream
中的元素是以Optional
類型存在的。Stream
的遍歷、匹配非常簡單。
2.發現Find()
2.1 findAny() 和 findFirst()
- findAny():找到其中一個元素 (使用 stream() 時找到的是第一個元素;使用 parallelStream()並行時找到的是其中一個元素)
- findFirst():找到第一個元素
值得注意的是,這兩個方法返回的是一個 Optional 對象,它是一個容器類,能代表一個值存在或不存在。
3.匹配Match
3.1 anyMatch(T -> boolean)
流中是否有一個元素匹配給定的 T -> boolean 條件
是否存在一個 person 對象的 age 等於 20:
boolean b = list.stream().anyMatch(person -> person.getAge() == 20);
3.2 allMatch(T -> boolean)
流中是否所有元素都匹配給定的 T -> boolean 條件
boolean result = list.stream().allMatch(Person::isStudent);
3.3 noneMatch(T -> boolean)
流中是否沒有元素匹配給定的 T -> boolean 條件
boolean result = list.stream().noneMatch(Person::isStudent);
案例
// import已省略,請自行添加,后面代碼亦是
public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(7, 6, 9, 3, 8, 2, 1);
// 遍歷輸出符合條件的元素
list.stream().filter(x -> x > 6).forEach(System.out::println);
// 匹配第一個
Optional<Integer> findFirst = list.stream().filter(x -> x > 6).findFirst();
// 匹配任意(適用於並行流)
Optional<Integer> findAny = list.parallelStream().filter(x -> x > 6).findAny();
// 是否包含符合特定條件的元素
boolean anyMatch = list.stream().anyMatch(x -> x > 6);
System.out.println("匹配第一個值:" + findFirst.get());
System.out.println("匹配任意一個值:" + findAny.get());
System.out.println("是否存在大於6的值:" + anyMatch);
}
}
4.篩選filter(T -> boolean)
filter(T -> boolean):保留 boolean 為 true 的元素。
篩選,是按照一定的規則校驗流中的元素,將符合條件的元素提取到新的流中的操作。
案例一:篩選出Integer
集合中大於7的元素,並打印出來
public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(6, 7, 3, 8, 1, 2, 9);
Stream<Integer> stream = list.stream();
stream.filter(x -> x > 7).forEach(System.out::println);
}
}
預期結果:
8 9
案例二: 篩選員工中工資高於8000的人,並形成新的集合。 形成新集合依賴collect
(收集),后文有詳細介紹。
public class StreamTest {
public static void main(String[] args) {
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, 23, "male", "New York"));
personList.add(new Person("Jack", 7000, 25, "male", "Washington"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));
personList.add(new Person("Anni", 8200, 24, "female", "New York"));
personList.add(new Person("Owen", 9500, 25, "male", "New York"));
personList.add(new Person("Alisa", 7900, 26, "female", "New York"));
List<String> fiterList = personList.stream().filter(x -> x.getSalary() > 8000).map(Person::getName)
.collect(Collectors.toList());
System.out.print("高於8000的員工姓名:" + fiterList);
}
}
運行結果:
高於8000的員工姓名:[Tom, Anni, Owen]
5.聚合(max/min/count)
max
、min
、count
這些字眼你一定不陌生,沒錯,在mysql中我們常用它們進行數據統計。Java stream中也引入了這些概念和用法,極大地方便了我們對集合、數組的數據統計工作。
案例一:獲取String
集合中最長的元素。
public class StreamTest {
public static void main(String[] args) {
List<String> list = Arrays.asList("adnm", "admmt", "pot", "xbangd", "weoujgsd");
Optional<String> max = list.stream().max(Comparator.comparing(String::length));
System.out.println("最長的字符串:" + max.get());
}
}
輸出結果:
最長的字符串:weoujgsd
案例二:獲取Integer
集合中的最大值。
public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(7, 6, 9, 4, 11, 6);
// 自然排序
Optional<Integer> max = list.stream().max(Integer::compareTo);
// 自定義排序
Optional<Integer> max2 = list.stream().max(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
});
System.out.println("自然排序的最大值:" + max.get());
System.out.println("自定義排序的最大值:" + max2.get());
}
}
輸出結果:
自然排序的最大值:11
自定義排序的最大值:11
案例三:獲取員工工資最高的人。
public class StreamTest {
public static void main(String[] args) {
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, 23, "male", "New York"));
personList.add(new Person("Jack", 7000, 25, "male", "Washington"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));
personList.add(new Person("Anni", 8200, 24, "female", "New York"));
personList.add(new Person("Owen", 9500, 25, "male", "New York"));
personList.add(new Person("Alisa", 7900, 26, "female", "New York"));
Optional<Person> max = personList.stream().max(Comparator.comparingInt(Person::getSalary));
System.out.println("員工工資最大值:" + max.get().getSalary());
}
}
輸出結果:
員工工資最大值:9500
案例四:計算Integer
集合中大於6的元素的個數。
import java.util.Arrays;
import java.util.List;
public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(7, 6, 4, 8, 2, 11, 9);
long count = list.stream().filter(x -> x > 6).count();
System.out.println("list中大於6的元素個數:" + count);
}
}
輸出結果:
list中大於6的元素個數:4
6.映射(map/flatMap)
映射,可以將一個流的元素按照一定的映射規則映射到另一個流中。分為map
和flatMap
:
map
:接收一個函數作為參數,該函數會被應用到每個元素上,並將其映射成一個新的元素。flatMap
:接收一個函數作為參數,將流中的每個值都換成另一個流,然后把所有流連接成一個流。
6.1 map(T -> R)
將流中的每一個元素 T 映射為 R(類似類型轉換)
List<String> newlist = list.stream().map(Person::getName).collect(Collectors.toList());
newlist 里面的元素為 list 中每一個 Person 對象的 name 變量。
6.2 flatMap(T -> Stream)
將流中的每一個元素 T 映射為一個流,再把每一個流連接成為一個流。
List<String> list = new ArrayList<>();
list.add("aaa bbb ccc");
list.add("ddd eee fff");
list.add("ggg hhh iii");
list = list.stream().map(s -> s.split(" ")).flatMap(Arrays::stream).collect(toList());
上面例子中,我們的目的是把 List 中每個字符串元素以” “分割開,變成一個新的 List。
首先 map 方法分割每個字符串元素,但此時流的類型為 Stream。
案例一:英文字符串數組的元素全部改為大寫。整數數組每個元素+3。
public class StreamTest {
public static void main(String[] args) {
String[] strArr = { "abcd", "bcdd", "defde", "fTr" };
List<String> strList = Arrays.stream(strArr).map(String::toUpperCase).collect(Collectors.toList());
List<Integer> intList = Arrays.asList(1, 3, 5, 7, 9, 11);
List<Integer> intListNew = intList.stream().map(x -> x + 3).collect(Collectors.toList());
System.out.println("每個元素大寫:" + strList);
System.out.println("每個元素+3:" + intListNew);
}
}
輸出結果:
每個元素大寫:[ABCD, BCDD, DEFDE, FTR]
每個元素+3:[4, 6, 8, 10, 12, 14]
案例二:將員工的薪資全部增加1000。
public class StreamTest {
public static void main(String[] args) {
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, 23, "male", "New York"));
personList.add(new Person("Jack", 7000, 25, "male", "Washington"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));
personList.add(new Person("Anni", 8200, 24, "female", "New York"));
personList.add(new Person("Owen", 9500, 25, "male", "New York"));
personList.add(new Person("Alisa", 7900, 26, "female", "New York"));
// 不改變原來員工集合的方式
List<Person> personListNew = personList.stream().map(person -> {
Person personNew = new Person(person.getName(), 0, 0, null, null);
personNew.setSalary(person.getSalary() + 10000);
return personNew;
}).collect(Collectors.toList());
System.out.println("一次改動前:" + personList.get(0).getName() + "-->" + personList.get(0).getSalary());
System.out.println("一次改動后:" + personListNew.get(0).getName() + "-->" + personListNew.get(0).getSalary());
// 改變原來員工集合的方式
List<Person> personListNew2 = personList.stream().map(person -> {
person.setSalary(person.getSalary() + 10000);
return person;
}).collect(Collectors.toList());
System.out.println("二次改動前:" + personList.get(0).getName() + "-->" + personListNew.get(0).getSalary());
System.out.println("二次改動后:" + personListNew2.get(0).getName() + "-->" + personListNew.get(0).getSalary());
}
}
輸出結果:
一次改動前:Tom–>8900
一次改動后:Tom–>18900
二次改動前:Tom–>18900
二次改動后:Tom–>18900
案例三:將兩個字符數組合並成一個新的字符數組。
public class StreamTest {
public static void main(String[] args) {
List<String> list = Arrays.asList("m,k,l,a", "1,3,5,7");
List<String> listNew = list.stream().flatMap(s -> {
// 將每個元素轉換成一個stream
String[] split = s.split(",");
Stream<String> s2 = Arrays.stream(split);
return s2;
}).collect(Collectors.toList());
System.out.println("處理前的集合:" + list);
System.out.println("處理后的集合:" + listNew);
}
}
輸出結果:
處理前的集合:[m-k-l-a, 1-3-5]
處理后的集合:[m, k, l, a, 1, 3, 5]
7.歸約(reduce)
歸約,也稱縮減,顧名思義,是把一個流縮減成一個值,能實現對集合求和、求乘積和求最值操作。
7.1 reduce((T, T) -> T) 和 reduce(T, (T, T) -> T)
歸約是將集合中的所有元素經過指定運算,折疊成一個元素輸出,如:求最值、平均數等,這些操作都是將一個集合的元素折疊成一個元素輸出。
在流中,reduce函數能實現歸約。
reduce函數接收兩個參數:
- 初始值
- 進行歸約操作的Lambda表達式
用於組合流中的元素,如求和,求積,求最大值等
int age = list.stream().reduce(0, (person1,person2)->person1.getAge()+person2.getAge());
//計算年齡總和:
int sum = list.stream().map(Person::getAge).reduce(0, (a, b) -> a + b);
//與之相同:
int sum = list.stream().map(Person::getAge).reduce(0, Integer::sum);
其中,reduce 第一個參數 0 代表起始值為 0,lambda (a, b) -> a + b 即將兩值相加產生一個新值
同樣地:
//計算年齡總乘積:
int sum = list.stream().map(Person::getAge).reduce(1, (a, b) -> a * b);
當然也可以
Optional<Integer> sum = list.stream().map(Person::getAge).reduce(Integer::sum);
即不接受任何起始值,但因為沒有初始值,需要考慮結果可能不存在的情況,因此返回的是 Optional 類型。
案例一:求Integer
集合的元素之和、乘積和最大值。
public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 3, 2, 8, 11, 4);
// 求和方式1
Optional<Integer> sum = list.stream().reduce((x, y) -> x + y);
// 求和方式2
Optional<Integer> sum2 = list.stream().reduce(Integer::sum);
// 求和方式3
Integer sum3 = list.stream().reduce(0, Integer::sum);
// 求乘積
Optional<Integer> product = list.stream().reduce((x, y) -> x * y);
// 求最大值方式1
Optional<Integer> max = list.stream().reduce((x, y) -> x > y ? x : y);
// 求最大值寫法2
Integer max2 = list.stream().reduce(1, Integer::max);
System.out.println("list求和:" + sum.get() + "," + sum2.get() + "," + sum3);
System.out.println("list求積:" + product.get());
System.out.println("list求和:" + max.get() + "," + max2);
}
}
輸出結果:
list求和:29,29,29
list求積:2112
list求和:11,11
案例二:求所有員工的工資之和和最高工資。
public class StreamTest {
public static void main(String[] args) {
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, 23, "male", "New York"));
personList.add(new Person("Jack", 7000, 25, "male", "Washington"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));
personList.add(new Person("Anni", 8200, 24, "female", "New York"));
personList.add(new Person("Owen", 9500, 25, "male", "New York"));
personList.add(new Person("Alisa", 7900, 26, "female", "New York"));
// 求工資之和方式1:
Optional<Integer> sumSalary = personList.stream().map(Person::getSalary).reduce(Integer::sum);
// 求工資之和方式2:
Integer sumSalary2 = personList.stream().reduce(0, (sum, p) -> sum += p.getSalary(),
(sum1, sum2) -> sum1 + sum2);
// 求工資之和方式3:
Integer sumSalary3 = personList.stream().reduce(0, (sum, p) -> sum += p.getSalary(), Integer::sum);
// 求最高工資方式1:
Integer maxSalary = personList.stream().reduce(0, (max, p) -> max > p.getSalary() ? max : p.getSalary(),
Integer::max);
// 求最高工資方式2:
Integer maxSalary2 = personList.stream().reduce(0, (max, p) -> max > p.getSalary() ? max : p.getSalary(),
(max1, max2) -> max1 > max2 ? max1 : max2);
System.out.println("工資之和:" + sumSalary.get() + "," + sumSalary2 + "," + sumSalary3);
System.out.println("最高工資:" + maxSalary + "," + maxSalary2);
}
}
輸出結果:
工資之和:49300,49300,49300
最高工資:9500,9500
8.收集(collect)
collect
,收集方法,我們很常用的是 collect(toList()),當然還有 collect(toSet()) 等,參數是一個收集器接口。收集方法可以說是內容最繁多、功能最豐富的部分了。從字面上去理解,就是把一個流收集起來,最終可以是收集成一個值也可以收集成一個新的集合。
coollect 方法作為終端操作,接受的是一個 Collector 接口參數,能對數據進行一些收集歸總操作。
collect
主要依賴java.util.stream.Collectors
類內置的靜態方法。
8.1 歸集(toList/toSet/toMap)
因為流不存儲數據,那么在流中的數據完成處理后,需要將流中的數據重新歸集到新的集合里。toList
、toSet
和toMap
比較常用,即把流中所有元素收集到一個 List, Set 或 Collection 中,另外還有toCollection
、toConcurrentMap
等復雜一些的用法。
下面用一個案例演示toList
、toSet
和toMap
:
public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 6, 3, 4, 6, 7, 9, 6, 20);
List<Integer> listNew = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toList());
Set<Integer> set = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toSet());
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, 23, "male", "New York"));
personList.add(new Person("Jack", 7000, 25, "male", "Washington"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));
personList.add(new Person("Anni", 8200, 24, "female", "New York"));
Map<?, Person> map = personList.stream().filter(p -> p.getSalary() > 8000)
.collect(Collectors.toMap(Person::getName, p -> p));
System.out.println("toList:" + listNew);
System.out.println("toSet:" + set);
System.out.println("toMap:" + map);
}
}
運行結果:
toList:[6, 4, 6, 6, 20]
toSet:[4, 20, 6]
toMap:
9.統計(count/averaging)
Collectors
提供了一系列用於數據統計的靜態方法:
- 計數:
count
。返回流中元素個數,結果為 long 類型。 - 平均值:
averagingInt
、averagingLong
、averagingDouble
- 最值:
maxBy
、minBy
- 求和:
summingInt
、summingLong
、summingDouble
- 統計以上所有:
summarizingInt
、summarizingLong
、summarizingDouble
案例:統計員工人數、平均工資、工資總額、最高工資。
public class StreamTest {
public static void main(String[] args) {
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, 23, "male", "New York"));
personList.add(new Person("Jack", 7000, 25, "male", "Washington"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));
// 求總數
Long count = personList.stream().collect(Collectors.counting());
// 求平均工資
Double average = personList.stream().collect(Collectors.averagingDouble(Person::getSalary));
// 求最高工資
Optional<Integer> max = personList.stream().map(Person::getSalary).collect(Collectors.maxBy(Integer::compare));
// 求工資之和
Integer sum = personList.stream().collect(Collectors.summingInt(Person::getSalary));
// 一次性統計所有信息
DoubleSummaryStatistics collect = personList.stream().collect(Collectors.summarizingDouble(Person::getSalary));
System.out.println("員工總數:" + count);
System.out.println("員工平均工資:" + average);
System.out.println("員工工資總和:" + sum);
System.out.println("員工工資所有統計:" + collect);
}
}
運行結果:
員工總數:3
員工平均工資:7900.0
員工工資總和:23700
員工工資所有統計:DoubleSummaryStatistics
10.groupingBy 分組
groupingBy 用於將數據分組,最終返回一個 Map 類型
Map<Integer, List<Person>> map = list.stream().collect(groupingBy(Person::getAge));
例子中我們按照年齡 age 分組,每一個 Person 對象中年齡相同的歸為一組。
Map<String,List<Person>> result = list.stream()
.collect(Collectors.groupingby((person)->{
if(person.getAge()>60)
return "老年人";
else if(person.getAge()>40)
return "中年人";
else
return "青年人";
}));
另外可以看出,Person::getAge
決定 Map 的鍵(Integer 類型),list 類型決定 Map 的值(List 類型)
10.1多級分組
groupingBy 可以接受一個第二參數實現多級分組:
Map<Integer, Map<T, List<Person>>> map = list.stream().collect(groupingBy(Person::getAge, groupBy(...)));
其中返回的 Map 鍵為 Integer 類型,值為 Map
10.2按組收集數據
Map<Integer, Integer> map = list.stream().collect(groupingBy(Person::getAge, summingInt(Person::getAge)));
該例子中,我們通過年齡進行分組,然后 summingInt(Person::getAge)) 分別計算每一組的年齡總和(Integer),最終返回一個 Map
groupingBy(Person::getAge)
其實等同於:
groupingBy(Person::getAge, toList())
11.partitioningBy 分區
分區與分組的區別在於,分區是按照 true 和 false 來分的,因此partitioningBy 接受的參數的 lambda 也是 T -> boolean
。
//根據年齡是否小於等於20來分區
Map<Boolean, List<Person>> map = list.stream()
.collect(partitioningBy(p -> p.getAge() <= 20));
//打印輸出
{
false=[Person{name='mike', age=25}, Person{name='tom', age=30}],
true=[Person{name='jack', age=20}]
}
同樣地 partitioningBy 也可以添加一個收集器作為第二參數,進行類似 groupBy 的多重分區等等操作。
11.1分組與分區對比
- 分區:將
stream
按條件分為兩個Map
,比如員工按薪資是否高於8000分為兩部分。 - 分組:將集合分為多個Map,比如員工按性別分組。有單級分組和多級分組。
案例:將員工按薪資是否高於8000分為兩部分;將員工按性別和地區分組
public class StreamTest {
public static void main(String[] args) {
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, "male", "New York"));
personList.add(new Person("Jack", 7000, "male", "Washington"));
personList.add(new Person("Lily", 7800, "female", "Washington"));
personList.add(new Person("Anni", 8200, "female", "New York"));
personList.add(new Person("Owen", 9500, "male", "New York"));
personList.add(new Person("Alisa", 7900, "female", "New York"));
// 將員工按薪資是否高於8000分組
Map<Boolean, List<Person>> part = personList.stream().collect(Collectors.partitioningBy(x -> x.getSalary() > 8000));
// 將員工按性別分組
Map<String, List<Person>> group = personList.stream().collect(Collectors.groupingBy(Person::getSex));
// 將員工先按性別分組,再按地區分組
Map<String, Map<String, List<Person>>> group2 = personList.stream().collect(Collectors.groupingBy(Person::getSex, Collectors.groupingBy(Person::getArea)));
System.out.println("員工按薪資是否大於8000分組情況:" + part);
System.out.println("員工按性別分組情況:" + group);
System.out.println("員工按性別、地區:" + group2);
}
}
輸出結果:
員工按薪資是否大於8000分組情況:{false=[mutest.Person@2d98a335, mutest.Person@16b98e56, mutest.Person@7ef20235], true=[mutest.Person@27d6c5e0, mutest.Person@4f3f5b24, mutest.Person@15aeb7ab]}
員工按性別分組情況:{female=[mutest.Person@16b98e56, mutest.Person@4f3f5b24, mutest.Person@7ef20235], male=[mutest.Person@27d6c5e0, mutest.Person@2d98a335, mutest.Person@15aeb7ab]}
員工按性別、地區:{female={New York=[mutest.Person@4f3f5b24, mutest.Person@7ef20235], Washington=[mutest.Person@16b98e56]}, male={New York=[mutest.Person@27d6c5e0, mutest.Person@15aeb7ab], Washington=[mutest.Person@2d98a335]}}
12.接合(joining)
joining 連接字符串,也是一個比較常用的方法,對流里面的字符串元素進行連接,其底層實現用的是專門用於字符串連接的 StringBuilder。
joining
可以將stream中的元素用特定的連接符(沒有的話,則直接連接)連接成一個字符串。
String s = list.stream().map(Person::getName).collect(joining());
//結果:jackmiketom
String s = list.stream().map(Person::getName).collect(joining(","));
//結果:jack,mike,tom
joining 還有一個比較特別的重載方法:
String s = list.stream().map(Person::getName).collect(joining(" and ", "Today ", " play games."));
//結果:Today jack and mike and tom play games.
即 Today 放開頭,play games. 放結尾,and 在中間連接各個字符串。
案例:
public class StreamTest {
public static void main(String[] args) {
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, 23, "male", "New York"));
personList.add(new Person("Jack", 7000, 25, "male", "Washington"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));
String names = personList.stream().map(p -> p.getName()).collect(Collectors.joining(","));
System.out.println("所有員工的姓名:" + names);
List<String> list = Arrays.asList("A", "B", "C");
String string = list.stream().collect(Collectors.joining("-"));
System.out.println("拼接后的字符串:" + string);
}
}
運行結果:
所有員工的姓名:Tom,Jack,Lily
拼接后的字符串:A-B-C
13.歸約(reducing)
Collectors
類提供的reducing
方法,相比於stream
本身的reduce
方法,增加了對自定義歸約的支持。
public class StreamTest {
public static void main(String[] args) {
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, 23, "male", "New York"));
personList.add(new Person("Jack", 7000, 25, "male", "Washington"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));
// 每個員工減去起征點后的薪資之和(這個例子並不嚴謹,但一時沒想到好的例子)
Integer sum = personList.stream().collect(Collectors.reducing(0, Person::getSalary, (i, j) -> (i + j - 5000)));
System.out.println("員工扣稅薪資總和:" + sum);
// stream的reduce
Optional<Integer> sum2 = personList.stream().map(Person::getSalary).reduce(Integer::sum);
System.out.println("員工薪資總和:" + sum2.get());
}
}
運行結果:
員工扣稅薪資總和:8700
員工薪資總和:23700
14.排序(sorted)
sorted,中間操作。有兩種排序:【sorted() / sorted((T, T) -> int)】
- sorted():自然排序,流中元素需實現Comparable接口
- sorted(Comparator com):Comparator排序器自定義排序
注意:如果流中的元素的類實現了 Comparable 接口,即有自己的排序規則,那么可以直接調用 sorted() 方法對元素進行排序,如 Stream。反之, 需要調用 sorted((T, T) -> int) 實現 Comparator 接口
首先我們先創建一個 Person 泛型的 List
List<Person> list = new ArrayList<>();
list.add(new Person("jack", 20));
list.add(new Person("mike", 25));
list.add(new Person("tom", 30));
Person 類包含年齡和姓名兩個成員變量
private String name;
private int age;
比如:根據年齡大小來比較:
list = list.stream()
.sorted((p1, p2) -> p1.getAge() - p2.getAge())
.collect(Collectors.toList());
// private int age;
當然這個可以簡化為
list = list.stream()
.sorted(Comparator.comparingInt(Person::getAge))
.collect(Collectors.toList());
14.1 數字排序
/**
* 數字排序
*/
public static void testIntegerSort() {
List<Integer> list = Arrays.asList(4, 2, 5, 3, 1);
System.out.println(list);//執行結果:[4, 2, 5, 3, 1]
//升序
list.sort((a, b) -> a.compareTo(b.intValue()));
System.out.println(list);//執行結果:[1, 2, 3, 4, 5]
//降序
list.sort((a, b) -> b.compareTo(a.intValue()));
System.out.println(list);//執行結果:[5, 4, 3, 2, 1]
}
14.2 字符串排序
/**
* 字符串排序
*/
public static void testStringSort() {
List<String> list = new ArrayList<>();
list.add("aa");
list.add("cc");
list.add("bb");
list.add("ee");
list.add("dd");
System.out.println(list);//執行結果:aa, cc, bb, ee, dd
//升序
list.sort((a, b) -> a.compareTo(b.toString()));
System.out.println(list);//執行結果:[aa, bb, cc, dd, ee]
//降序
list.sort((a, b) -> b.compareTo(a.toString()));
System.out.println(list);//執行結果:[ee, dd, cc, bb, aa]
}
14.3字符串排序
/**
* 字符串排序
*/
public static void testStringSort() {
List<String> list = new ArrayList<>();
list.add("aa");
list.add("cc");
list.add("bb");
list.add("ee");
list.add("dd");
System.out.println(list);//執行結果:aa, cc, bb, ee, dd
//升序
list.sort((a, b) -> a.compareTo(b.toString()));
System.out.println(list);//執行結果:[aa, bb, cc, dd, ee]
//降序
list.sort((a, b) -> b.compareTo(a.toString()));
System.out.println(list);//執行結果:[ee, dd, cc, bb, aa]
}
14.4 對象字段排序
class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
/**
* 對象串排序
*/
public void testObjectSort() {
List<Person> list = new ArrayList<>();
list.add(new Person("三炮", 48));
list.add(new Person("老王", 35));
list.add(new Person("小明", 8));
list.add(new Person("叫獸", 70));
System.out.println(list); //執行結果:[Person{name='三炮', age=48}, Person{name='老王', age=35}, Person{name='小明', age=8}, Person{name='叫獸', age=70}]
//按年齡升序
list.sort((a, b) -> Integer.compare(a.age, b.getAge()));
System.out.println(list);//執行結果:[Person{name='小明', age=8}, Person{name='老王', age=35}, Person{name='三炮', age=48}, Person{name='叫獸', age=70}]
//按年齡降序
list.sort((a, b) -> Integer.compare(b.age, a.getAge()));
System.out.println(list);//執行結果:[Person{name='叫獸', age=70}, Person{name='三炮', age=48}, Person{name='老王', age=35}, Person{name='小明', age=8}]
//如果按姓名排序,其實就是按字符串排序一樣
}
案例:將員工按工資由高到低(工資一樣則按年齡由大到小)排序
public class StreamTest {
public static void main(String[] args) {
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Sherry", 9000, 24, "female", "New York"));
personList.add(new Person("Tom", 8900, 22, "male", "Washington"));
personList.add(new Person("Jack", 9000, 25, "male", "Washington"));
personList.add(new Person("Lily", 8800, 26, "male", "New York"));
personList.add(new Person("Alisa", 9000, 26, "female", "New York"));
// 按工資升序排序(自然排序)
List<String> newList = personList.stream().sorted(Comparator.comparing(Person::getSalary)).map(Person::getName)
.collect(Collectors.toList());
// 按工資倒序排序
List<String> newList2 = personList.stream().sorted(Comparator.comparing(Person::getSalary).reversed())
.map(Person::getName).collect(Collectors.toList());
// 先按工資再按年齡升序排序
List<String> newList3 = personList.stream() .sorted(Comparator.comparing(Person::getSalary).thenComparing(Person::getAge)).map(Person::getName)
.collect(Collectors.toList());
// 先按工資再按年齡自定義排序(降序)
List<String> newList4 = personList.stream().sorted((p1, p2) -> {
if (p1.getSalary() == p2.getSalary()) {
return p2.getAge() - p1.getAge();
} else {
return p2.getSalary() - p1.getSalary();
}
}).map(Person::getName).collect(Collectors.toList());
System.out.println("按工資升序排序:" + newList);
System.out.println("按工資降序排序:" + newList2);
System.out.println("先按工資再按年齡升序排序:" + newList3);
System.out.println("先按工資再按年齡自定義降序排序:" + newList4);
}
}
運行結果:
按工資升序排序:[Lily, Tom, Sherry, Jack, Alisa]
按工資降序排序:[Sherry, Jack, Alisa, Tom, Lily]
先按工資再按年齡升序排序:[Lily, Tom, Sherry, Jack, Alisa]
先按工資再按年齡自定義降序排序:[Alisa, Jack, Sherry, Tom, Lily]
15.去重:distinct()
去除重復元素,這個方法是通過類的 equals
方法來判斷兩個元素是否相等的。如例子中的 Person 類,需要先定義好 equals 方法,不然類似[Person{name='jack', age=20}, Person{name='jack', age=20}]
這樣的情況是不會處理的。
16.限制:limit(long n)
返回前 n 個元素
list = list.stream()
.limit(2)
.collect(Collectors.toList());
//打印輸出 [Person{name='jack', age=20}, Person{name='mike', age=25}]
17.去除(跳過)skip(long n)
去除前 n 個元素
list = list.stream()
.skip(2)
.collect(Collectors.toList());
//打印輸出 [Person{name='tom', age=30}]
tips:
- skip(m)用在 limit(n) 前面時,先去除前 m 個元素再返回剩余元素的前 n 個元素。
- limit(n) 用在 skip(m) 前面時,先返回前 n 個元素再在剩余的 n 個元素中去除 m 個元素。
list = list.stream()
.limit(2)
.skip(1)
.collect(Collectors.toList());
//打印輸出 [Person{name='mike', age=25}]
案例:
public class StreamTest {
public static void main(String[] args) {
String[] arr1 = { "a", "b", "c", "d" };
String[] arr2 = { "d", "e", "f", "g" };
Stream<String> stream1 = Stream.of(arr1);
Stream<String> stream2 = Stream.of(arr2);
// concat:合並兩個流 distinct:去重
List<String> newList = Stream.concat(stream1, stream2).distinct().collect(Collectors.toList());
// limit:限制從流中獲得前n個數據
List<Integer> collect = Stream.iterate(1, x -> x + 2).limit(10).collect(Collectors.toList());
// skip:跳過前n個數據
List<Integer> collect2 = Stream.iterate(1, x -> x + 2).skip(1).limit(5).collect(Collectors.toList());
System.out.println("流合並:" + newList);
System.out.println("limit:" + collect);
System.out.println("skip:" + collect2);
}
}
運行結果:
流合並:[a, b, c, d, e, f, g]
limit:[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
skip:[3, 5, 7, 9, 11]
好,以上就是全部內容,能堅持看到這里,你一定很有收獲,那么動一動拿offer的小手,點個贊再走吧,聽說這么做的人2021年都交了好運!
18.無序unordered()
還有這個比較不起眼的方法,返回一個等效的無序流,當然如果流本身就是無序的話,那可能就會直接返回其本身。
九、數值流
前面介紹的如
int sum = list.stream().map(Person::getAge).reduce(0, Integer::sum);
計算元素總和的方法其中暗含了裝箱成本,map(Person::getAge)
方法過后流變成了 Stream 類型,而每個 Integer 都要拆箱成一個原始類型再進行 sum 方法求和,這樣大大影響了效率。
針對這個問題 Java 8 有良心地引入了數值流 IntStream, DoubleStream, LongStream,這種流中的元素都是原始數據類型,分別是 int,double,long。
9.1流與數值流的轉換
(1)流轉換為數值流
- mapToInt(T -> int) : return IntStream
- mapToDouble(T -> double) : return DoubleStream
- mapToLong(T -> long) : return LongStream
IntStream intStream = list.stream().mapToInt(Person::getAge);
當然如果是下面這樣便會出錯
LongStream longStream = list.stream().mapToInt(Person::getAge);
因為 getAge 方法返回的是 int 類型(返回的如果是 Integer,一樣可以轉換為 IntStream)
(2)數值流轉換為流
很簡單,就一個 boxed
Stream<Integer> stream = intStream.boxed();
9.2數值流方法
下面這些方法作用不用多說,看名字就知道:
- sum()
- max()
- min()
- average() 等…
9.3數值范圍
IntStream 與 LongStream 擁有 range 和 rangeClosed 方法用於數值范圍處理。
- IntStream : rangeClosed(int, int) / range(int, int)
- LongStream : rangeClosed(long, long) / range(long, long)
這兩個方法的區別在於一個是閉區間,一個是半開半閉區間:
- rangeClosed(1, 100) :[1, 100]
- range(1, 100) :[1, 100)
我們可以利用 IntStream.rangeClosed(1, 100)
生成 1 到 100 的數值流。
//求 1 到 10 的數值總和:
IntStream intStream = IntStream.rangeClosed(1, 10);
int sum = intStream.sum();
十、Stream的創建
之前我們得到一個流是通過一個原始數據源轉換而來,其實我們還可以直接構建得到流。
10.1值創建流
- Stream.of(T…) : Stream.of(“aa”, “bb”) 生成流
//生成一個字符串流
Stream<String> stream = Stream.of("aaa", "bbb", "ccc");
Stream
可以通過集合數組創建。
Stream.empty() : 生成空流。
10.2數組創建流
根據參數的數組類型創建對應的流:
- Arrays.stream(T[ ])
- Arrays.stream(int[ ])
- Arrays.stream(double[ ])
- Arrays.stream(long[ ])
值得注意的是,還可以規定只取數組的某部分,用到的是Arrays.stream(T[], int, int)
// 只取索引第 1 到第 2 位的:
int[] a = {1, 2, 3, 4};
Arrays.stream(a, 1, 3).forEach(System.out :: println);
// 打印 2 ,3
10.3集合創建流
通過 java.util.Collection.stream()
方法用集合創建流。
List<String> list = Arrays.asList("a", "b", "c");
// 創建一個順序流
Stream<String> stream = list.stream();
// 創建一個並行流
Stream<String> parallelStream = list.parallelStream();
10.4文件生成流
Stream<String> stream = Files.lines(Paths.get("data.txt"));
每個元素是給定文件的其中一行。
10.5函數生成流
兩個方法(均為靜態方法):
- iterate : 依次對每個新生成的值應用函數
- generate :接受一個函數,生成一個新的值
Stream.iterate(0, (x) -> x + 3).limit(4);
//生成流,首元素為 0,之后依次加 3,取前4位
Stream.generate(Math :: random)
//生成流,為 0 到 1 的隨機雙精度數
Stream.generate(() -> 1)
//生成流,元素全為 1
十一、匯總
1.counting
用於計算總和:
(推薦第二種)
long l = list.stream().collect(counting());
沒錯,你應該想到了,下面這樣也可以:
long l = list.stream().count();
2.summingInt ,summingLong ,summingDouble
summing,沒錯,也是計算總和,不過這里需要一個函數參數
計算 Person 年齡總和:
(推薦第二種)
int sum = list.stream().collect(summingInt(Person::getAge));
當然,這個可以也簡化為:
int sum = list.stream().mapToInt(Person::getAge).sum();
除了上面兩種,其實還可以:
int sum = list.stream().map(Person::getAge).reduce(Interger::sum).get();
由此可見,函數式編程通常提供了多種方式來完成同一種操作。
3.averagingInt,averagingLong,averagingDouble
看名字就知道,求平均數
Double average = list.stream().collect(averagingInt(Person::getAge));
當然也可以這樣寫
OptionalDouble average = list.stream().mapToInt(Person::getAge).average();
不過要注意的是,這兩種返回的值是不同類型的。
4.summarizingInt,summarizingLong,summarizingDouble
這三個方法比較特殊,比如 summarizingInt 會返回 IntSummaryStatistics 類型
IntSummaryStatistics l = list.stream().collect(summarizingInt(Person::getAge));
IntSummaryStatistics 包含了計算出來的平均值,總數,總和,最值,可以通過下面這些方法獲得相應的數據
十二、取最值
maxBy,minBy 兩個方法,需要一個 Comparator 接口作為參數
Optional<Person> optional = list.stream().collect(maxBy(comparing(Person::getAge)));
我們也可以直接使用 max 方法獲得同樣的結果
Optional<Person> optional = list.stream().max(comparing(Person::getAge));
十三、並行流parallelStream
我們通過 list.stream()
將 List 類型轉換為流類型,我們還可以通過 list.parallelStream()
轉換為並行流。
並行流就是把內容分成多個數據塊,使用不同的線程分別處理每個數據塊的流。這也是流的一大特點,要知道,在 Java 7 之前,並行處理數據集合是非常麻煩的,你得自己去將數據分割開,自己去分配線程,必要時還要確保同步避免競爭。
Stream 讓程序員能夠比較輕易地實現對數據集合的並行處理,但要注意的是,不是所有情況的適合,有些時候並行甚至比順序進行效率更低,而有時候因為線程安全問題,還可能導致數據的處理錯誤,因此並行的性能問題非常值得我們思考。
比方說下面這個例子
int i = Stream.iterate(1, a -> a + 1).limit(100).parallel().reduce(0, Integer::sum);
我們通過這樣一行代碼來計算 1 到 100 的所有數的和,我們使用了 parallel 來實現並行。
但實際上是,這樣的計算,效率是非常低的,比不使用並行還低!一方面是因為裝箱問題,這個前面也提到過,就不再贅述,還有一方面就是 iterate 方法很難把這些數分成多個獨立塊來並行執行,因此無形之中降低了效率。
13.1 流的可分解性
這就說到流的可分解性問題了,使用並行的時候,我們要注意流背后的數據結構是否易於分解。比如眾所周知的 ArrayList 和 LinkedList,明顯前者在分解方面占優。
我們來看看一些數據源的可分解性情況
數據源 | 可分解性 |
---|---|
ArrayList | 極佳 |
LinkedList | 差 |
IntStream.range | 極佳 |
Stream.iterate | 差 |
HashSet | 好 |
TreeSet | 好 |
13.2 順序性
除了可分解性,和剛剛提到的裝箱問題,還有一點值得注意的是一些操作本身在並行流上的性能就比順序流要差,比如:limit,findFirst,因為這兩個方法會考慮元素的順序性,而並行本身就是違背順序性的,也是因為如此 findAny 一般比 findFirst 的效率要高。
13.3 stream和parallelStream
stream
和parallelStream
的簡單區分: stream
是順序流,由主線程按順序對流執行操作,而parallelStream
是並行流,內部以多線程並行執行的方式對流進行操作,但前提是流中的數據處理沒有順序要求。例如篩選集合中的奇數,兩者的處理不同之處:
如果流中的數據量足夠大,並行流可以加快處速度。
除了直接創建並行流,還可以通過parallel()
把順序流轉換成並行流:
Optional<Integer> findFirst = list.stream().parallel().filter(x->x>6).findFirst();