Java 8 API添加了一個(gè)新的抽象稱為流Stream,可以讓你以一種聲明的方式處理數(shù)據(jù)。
Stream 使用一種類似用 SQL 語(yǔ)句從數(shù)據(jù)庫(kù)查詢數(shù)據(jù)的直觀方式來(lái)提供一種對(duì) Java 集合運(yùn)算和表達(dá)的高階抽象。
這種風(fēng)格將要處理的元素集合看作一種流, 流在管道中傳輸, 并且可以在管道的節(jié)點(diǎn)上進(jìn)行處理, 比如篩選, 排序,聚合等。
元素流在管道中經(jīng)過(guò)中間操作(intermediate operation)的處理,最后由最終操作(terminal operation)得到前面處理的結(jié)果。
+--------------------+ +------+ +------+ +---+ +-------+
| stream of elements +-----> |filter+-> |sorted+-> |map+-> |collect|
+--------------------+ +------+ +------+ +---+ +-------+
以上的流程轉(zhuǎn)換為 Java 代碼為:
List<Integer> transactionsIds =
widgets.stream()
.filter(b -> b.getColor() == RED)
.sorted((x,y) -> x.getWeight() - y.getWeight())
.mapToInt(Widget::getWeight)
.sum();
一、什么是 Stream?
Stream(流)是一個(gè)來(lái)自數(shù)據(jù)源的元素隊(duì)列并支持聚合操作
元素是特定類型的對(duì)象,形成一個(gè)隊(duì)列。 Java中的Stream并不會(huì)存儲(chǔ)元素,而是按需計(jì)算。
數(shù)據(jù)源 流的來(lái)源。 可以是集合,數(shù)組,I/O channel, 產(chǎn)生器generator 等。
聚合操作 類似SQL語(yǔ)句一樣的操作, 比如filter, map, reduce, find, match, sorted等。
和以前的Collection操作不同, Stream操作還有兩個(gè)基礎(chǔ)的特征:
- Pipelining: 中間操作都會(huì)返回流對(duì)象本身。 這樣多個(gè)操作可以串聯(lián)成一個(gè)管道, 如同流式風(fēng)格(fluent style)。 這樣做可以對(duì)操作進(jìn)行優(yōu)化, 比如延遲執(zhí)行(laziness)和短路( short-circuiting)。
- 內(nèi)部迭代: 以前對(duì)集合遍歷都是通過(guò)Iterator或者For-Each的方式, 顯式的在集合外部進(jìn)行迭代, 這叫做外部迭代。 Stream提供了內(nèi)部迭代的方式, 通過(guò)訪問(wèn)者模式(Visitor)實(shí)現(xiàn)。
二、Stream的使用流程
首先當(dāng)我們?cè)谑褂靡粋€(gè)流的時(shí)候通常包括三個(gè)基本的步驟:獲取一個(gè)數(shù)據(jù)源(Stream Source)→ 數(shù)據(jù)轉(zhuǎn)換→執(zhí)行操作獲取想要的結(jié)果。
第一部分:獲取一個(gè)數(shù)據(jù)源(Stream Source)
想要對(duì)流進(jìn)行操作,首先你必須獲取一個(gè)流的對(duì)象,獲取流的對(duì)象就要有一個(gè)能夠生成流的數(shù)據(jù)源(Stream Source),流的數(shù)據(jù)源有很多,比較常用的兩個(gè)數(shù)據(jù)源:集合(Collection)、數(shù)組。
第二部分:流的構(gòu)造與轉(zhuǎn)換
構(gòu)造成流的幾種常見(jiàn)的方法:
//常用的構(gòu)造流的幾種方法
public static void StreamInitDemo(){
//1. 直接用Stream類生成Stream對(duì)象
Stream stream = Stream.of("a","b","c");
//2. 通過(guò)Arrays來(lái)獲取Stream對(duì)象
String [] stringAry = new String[] {"a","b","c"};
stream = Arrays.stream(stringAry);
//3. 通過(guò)集合對(duì)象來(lái)獲取Stream對(duì)象
List<String> list = Arrays.asList(stringAry);
stream = list.stream();
}
以上都是講數(shù)據(jù)源轉(zhuǎn)化為流,得到流對(duì)象后我們會(huì)進(jìn)行一些相應(yīng)的操作,那么操作完以后我們想要的不會(huì)是流對(duì)象,其只是一個(gè)工具,最后我們想要會(huì)是String或者一個(gè)數(shù)組或者一個(gè)集合,也有可能是一個(gè)map,這就涉及到了流的轉(zhuǎn)換,接下來(lái)請(qǐng)看演示代碼:
//流轉(zhuǎn)換為其他數(shù)據(jù)結(jié)構(gòu)
public static void StreamTransDemo(){
Stream stream = Stream.of("a", "b", "c");
//1. Stream類對(duì)象轉(zhuǎn)成數(shù)組
String[] strings = (String[]) stream.toArray(String[]::new);
//2. Stream轉(zhuǎn)成集合
List<String> list1 = (List<String>) stream.collect(Collectors.toList());
List<String> list2 = (List<String>) stream.collect(Collectors.toCollection(ArrayList::new));
Set set = (Set) stream.collect(Collectors.toSet());
Stack stack = (Stack) stream.collect(Collectors.toCollection(Stack::new));
//Stream轉(zhuǎn)成String
String str = stream.collect(Collectors.joining()).toString();
}
第三部分:Stream的操作

如圖,流的操作類型分為三種:Intermediate、Termianl、Short-circuiting。
-
Intermediate表示中間的,這類方法的
返回值還是一個(gè)Stream流對(duì)象,因此你可以再對(duì)其進(jìn)行操作,比如你想先進(jìn)行過(guò)濾,然后再過(guò)濾等操作。 -
Terminal 則表示這是流的最終操作,當(dāng)這種類型的對(duì)流進(jìn)行操作后,就
無(wú)法對(duì)流進(jìn)行操作了。 -
Short-circuiting 表示:對(duì)于一個(gè) intermediate 操作,如果它接受的是一個(gè)無(wú)限大(infinite/unbounded)的 Stream,但返回一個(gè)有限的新 Stream。對(duì)于一個(gè) terminal 操作,如果它接受的是一個(gè)無(wú)限大的 Stream,但能在
有限的時(shí)間計(jì)算出結(jié)果。
那么它們這三種類型都包含哪些方法呢?
Intermediate:
map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unorderedTerminal:
forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iteratorShort-circuiting:
anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit
1.map
map的作用就是把放入Stream中的每一個(gè)元素都執(zhí)行一般操作后再返回一個(gè)Stream。這就相當(dāng)于隱式的迭代器(iterator)。
//這個(gè)操作將 wordList 集合中的元素全部轉(zhuǎn)換成大寫(xiě)然后再調(diào)用
//collect方法獲得一個(gè)list集合。
List<String> output = wordList.stream().map(String::toUpperCase).collect(Collectors.toList());
在這個(gè)方法中map是一個(gè)Intermediate方法,其返回一個(gè)Stream,但是通常情況下我們想要其轉(zhuǎn)成list 或者 數(shù)組,因此調(diào)用 collect 方法,它是Terminal 最后獲得一個(gè)List集合。再比如:
//獲取 nums 中每個(gè)元素的平方
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
List<Integer> squareNums = nums.stream().map(n -> n * n).collect(Collectors.toList());
從上面例子可以看出,map 生成的是個(gè) 1:1 映射,每個(gè)輸入元素,都按照規(guī)則轉(zhuǎn)換成為另外一個(gè)元素。還有一些場(chǎng)景,是一對(duì)多映射關(guān)系的,這時(shí)需要 flatMap。
map可以把Stream中的元素按照給定的Function進(jìn)行轉(zhuǎn)換,新生成的Stream只包含轉(zhuǎn)換生成的元素。但如果Stream中的元素是集合,則無(wú)能為力,這個(gè)時(shí)候就需要flatmap。
flatMap:和map類似,不同的是其每個(gè)元素轉(zhuǎn)換得到的是Stream對(duì)象,會(huì)把子Stream中的元素壓縮到父集合中,就好像多個(gè)流都被合并起來(lái),扁平化為一個(gè)流。
flat:水平的; 平坦的; 平滑的;
2.flatMap
Stream<List<Integer>> inputStream = Stream.of(
Arrays.asList(1),
Arrays.asList(2, 3),
Arrays.asList(4, 5, 6)
);
Stream<Integer> outputStream = inputStream.flatMap((childList) -> childList.stream());
flatMap 把放入 Stream 中的層級(jí)結(jié)構(gòu)扁平化,就是將最底層元素抽出來(lái)放到一起,最終 output 的新 Stream 里面已經(jīng)沒(méi)有 List 了,都是直接的數(shù)字。
3.filter
filter 對(duì)原始 Stream 進(jìn)行某項(xiàng)測(cè)試,通過(guò)測(cè)試的元素被留下來(lái)生成一個(gè)新 Stream。
//篩選出集合中的所有的偶數(shù)
Integer[] sixNums = {1, 2, 3, 4, 5, 6};
Integer[] evens = Stream.of(sixNums).filter(n -> n%2 == 0).toArray(Integer[]::new);
4.forEach
forEach 方法接收一個(gè) Lambda 表達(dá)式,然后在 Stream 的每一個(gè)元素上執(zhí)行該表達(dá)式。
//打印出Person 中的姓名字段
//java 8的代碼
roster.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.forEach(p -> System.out.println(p.getName()));
//java 8之前的代碼
for (Person p : roster) {
if (p.getGender() == Person.Sex.MALE) {
System.out.println(p.getName());
}
}
對(duì)一個(gè)人員集合遍歷,找出男性并打印姓名。可以看出來(lái),forEach 是為 Lambda 而設(shè)計(jì)的,保持了最緊湊的風(fēng)格。而且 Lambda 表達(dá)式本身是可以重用的,非常方便。當(dāng)需要為多核系統(tǒng)優(yōu)化時(shí),可以 parallelStream().forEach(),只是此時(shí)原有元素的次序沒(méi)法保證,并行的情況下將改變串行時(shí)操作的行為,此時(shí) forEach 本身的實(shí)現(xiàn)不需要調(diào)整,而 Java8 以前的 for 循環(huán) code 可能需要加入額外的多線程邏輯。
但一般認(rèn)為,forEach 和常規(guī) for 循環(huán)的差異不涉及到性能,它們僅僅是函數(shù)式風(fēng)格與傳統(tǒng) Java 風(fēng)格的差別。另外一點(diǎn)需要注意,forEach 是 terminal 操作,因此它執(zhí)行后,Stream 的元素就被“消費(fèi)”掉了,你無(wú)法對(duì)一個(gè) Stream 進(jìn)行兩次 terminal 運(yùn)算。下面的代碼是錯(cuò)誤的:
stream.forEach(element -> doOneThing(element));
stream.forEach(element -> doAnotherThing(element));
相反,具有相似功能的 intermediate 操作 peek 可以達(dá)到上述目的。如下是出現(xiàn)在該 api javadoc 上的一個(gè)示例。
5.peek
//peek 對(duì)每個(gè)元素執(zhí)行操作并返回一個(gè)新的 Stream
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());
6.findFirst
這是一個(gè) termimal 兼 short-circuiting 操作,它總是返回 Stream 的第一個(gè)元素,或者空。
這里比較重點(diǎn)的是它的返回值類型:Optional。這也是一個(gè)模仿 Scala 語(yǔ)言中的概念,作為一個(gè)容器,它可能含有某值,或者不包含。使用它的目的是盡可能避免 NullPointerException。
String strA = " abcd ", strB = null;
print(strA);
print("");
print(strB);
getLength(strA);
getLength("");
getLength(strB);
public static void print(String text) {
// Java 8代碼實(shí)現(xiàn)
Optional.ofNullable(text).ifPresent(System.out::println);
// Java 8以前的代碼實(shí)現(xiàn)
if (text != null) {
System.out.println(text);
}
}
public static int getLength(String text) {
// Java 8代碼實(shí)現(xiàn)
return Optional.ofNullable(text).map(String::length).orElse(-1);
// Java 8以前的代碼實(shí)現(xiàn)
// return if (text != null) ? text.length() : -1;
};
在更復(fù)雜的 if (xx != null) 的情況中,使用 Optional 代碼的可讀性更好,而且它提供的是編譯時(shí)檢查,能極大的降低 NPE 這種 Runtime Exception 對(duì)程序的影響,或者迫使程序員更早的在編碼階段處理空值問(wèn)題,而不是留到運(yùn)行時(shí)再發(fā)現(xiàn)和調(diào)試。
Stream 中的 findAny、max/min、reduce 等方法等返回 Optional 值。還有例如 IntStream.average() 返回 OptionalDouble 等等。
7.reduce
這個(gè)方法的主要作用是把 Stream 元素組合起來(lái)。它提供一個(gè)起始值(種子),然后依照運(yùn)算規(guī)則(BinaryOperator),和前面 Stream 的第一個(gè)、第二個(gè)、第 n 個(gè)元素組合。從這個(gè)意義上說(shuō),字符串拼接、數(shù)值的 sum、min、max、average 都是特殊的 reduce。例如 Stream 的 sum 就相當(dāng)于
Integer sum = integers.reduce(0, (a, b) -> a+b);
或
Integer sum = integers.reduce(0, Integer::sum);
也有沒(méi)有起始值的情況,這時(shí)會(huì)把 Stream 的前面兩個(gè)元素組合起來(lái),返回的是 Optional。
// 字符串連接,concat = "ABCD"
String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat);
// 求最小值,minValue = -3.0
double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min);
// 求和,sumValue = 10, 有起始值
int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
// 求和,sumValue = 10, 無(wú)起始值
sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();
// 過(guò)濾,字符串連接,concat = "ace"
concat = Stream.of("a", "B", "c", "D", "e", "F").
filter(x -> x.compareTo("Z") > 0).
reduce("", String::concat);
上面代碼例如第一個(gè)示例的 reduce(),第一個(gè)參數(shù)(空白字符)即為起始值,第二個(gè)參數(shù)(String::concat)為 BinaryOperator。這類有起始值的 reduce() 都返回具體的對(duì)象。而對(duì)于第四個(gè)示例沒(méi)有起始值的 reduce(),由于可能沒(méi)有足夠的元素,返回的是 Optional,請(qǐng)留意這個(gè)區(qū)別。
8.limit/skip
limit 返回 Stream 的前面 n 個(gè)元素;skip 則是扔掉前 n 個(gè)元素(它是由一個(gè)叫 subStream 的方法改名而來(lái))。
//limit 和 skip 對(duì)運(yùn)行次數(shù)的影響
public void testLimitAndSkip() {
List<Person> persons = new ArrayList();
for (int i = 1; i <= 10000; i++) {
Person person = new Person(i, "name" + i);
persons.add(person);
}
List<String> personList2 = persons.stream().map(Person::getName).limit(10).skip(3).collect(Collectors.toList());
System.out.println(personList2);
}
private class Person {
public int no;
private String name;
public Person (int no, String name) {
this.no = no;
this.name = name;
}
public String getName() {
System.out.println(name);
return name;
}
}
輸出結(jié)果為:
name1
name2
name3
name4
name5
name6
name7
name8
name9
name10
[name4, name5, name6, name7, name8, name9, name10]
這是一個(gè)有 10,000 個(gè)元素的 Stream,但在 short-circuiting 操作 limit 和 skip 的作用下,管道中 map 操作指定的 getName() 方法的執(zhí)行次數(shù)為 limit 所限定的 10 次,而最終返回結(jié)果在跳過(guò)前 3 個(gè)元素后只有后面 7 個(gè)返回。
有一種情況是 limit/skip 無(wú)法達(dá)到 short-circuiting 目的的,就是把它們放在 Stream 的排序操作后,原因跟 sorted 這個(gè) intermediate 操作有關(guān):此時(shí)系統(tǒng)并不知道 Stream 排序后的次序如何,所以 sorted 中的操作看上去就像完全沒(méi)有被 limit 或者 skip 一樣。
//limit 和 skip 對(duì) sorted 后的運(yùn)行次數(shù)無(wú)影響
List<Person> persons = new ArrayList();
for (int i = 1; i <= 5; i++) {
Person person = new Person(i, "name" + i);
persons.add(person);
}
List<Person> personList2 = persons.stream().sorted((p1, p2) -> p1.getName().compareTo(p2.getName())).limit(2).collect(Collectors.toList());
System.out.println(personList2);
name2
name1
name3
name2
name4
name3
name5
name4
[stream.StreamDW$Person@816f27d, stream.StreamDW$Person@87aac27]
即雖然最后的返回元素?cái)?shù)量是 2,但整個(gè)管道中的 sorted 表達(dá)式執(zhí)行次數(shù)沒(méi)有像前面例子相應(yīng)減少。
最后有一點(diǎn)需要注意的是,對(duì)一個(gè) parallel 的 Steam 管道來(lái)說(shuō),如果其元素是有序的,那么 limit 操作的成本會(huì)比較大,因?yàn)樗姆祷貙?duì)象必須是前 n 個(gè)也有一樣次序的元素。取而代之的策略是取消元素間的次序,或者不要用 parallel Stream。
9.sorted
對(duì) Stream 的排序通過(guò) sorted 進(jìn)行,它比數(shù)組的排序更強(qiáng)之處在于你可以首先對(duì) Stream 進(jìn)行各類 map、filter、limit、skip 甚至 distinct 來(lái)減少元素?cái)?shù)量后,再排序,這能幫助程序明顯縮短執(zhí)行時(shí)間。
//優(yōu)化:排序前進(jìn)行 limit 和 skip
List<Person> persons = new ArrayList();
for (int i = 1; i <= 5; i++) {
Person person = new Person(i, "name" + i);
persons.add(person);
}
List<Person> personList2 = persons.stream().limit(2).sorted((p1, p2) -> p1.getName().compareTo(p2.getName())).collect(Collectors.toList());
System.out.println(personList2);
結(jié)果會(huì)簡(jiǎn)單很多:
name2
name1
[stream.StreamDW$Person@6ce253f1, stream.StreamDW$Person@53d8d10a]
當(dāng)然,這種優(yōu)化是有 business logic 上的局限性的:即不要求排序后再取值。
10.min/max/distinct
min 和 max 的功能也可以通過(guò)對(duì) Stream 元素先排序,再 findFirst 來(lái)實(shí)現(xiàn),但前者的性能會(huì)更好,為 O(n),而 sorted 的成本是 O(n log n)。同時(shí)它們作為特殊的 reduce 方法被獨(dú)立出來(lái)也是因?yàn)榍笞畲笞钚≈凳呛艹R?jiàn)的操作。
//找出最長(zhǎng)一行的長(zhǎng)度
BufferedReader br = new BufferedReader(new FileReader("c:\\SUService.log"));
int longest = br.lines().mapToInt(String::length).max().getAsInt();
br.close();
System.out.println(longest);
下面的例子則使用 distinct 來(lái)找出不重復(fù)的單詞。
//找出全文的單詞,轉(zhuǎn)小寫(xiě),并排序
BufferedReader br = new BufferedReader(new FileReader("c:\\SUService.log"));
List<String> words = br.lines().flatMap(line -> Stream.of(line.split(" "))).
filter(word -> word.length() > 0).
map(String::toLowerCase).
distinct().
sorted().
collect(Collectors.toList());
br.close();
System.out.println(words);
11.Match
Stream 有三個(gè) match 方法,從語(yǔ)義上說(shuō):
- allMatch:Stream 中全部元素符合傳入的
predicate,返回 true - anyMatch:Stream 中只要有一個(gè)元素符合傳入的
predicate,返回 true - noneMatch:Stream 中沒(méi)有一個(gè)元素符合傳入的
predicate,返回 true
Predicate是個(gè)斷言式接口其參數(shù)是<T,boolean>,也就是給一個(gè)參數(shù)T,返回boolean類型的結(jié)果。跟Function一樣,Predicate的具體實(shí)現(xiàn)也是根據(jù)傳入的lambda表達(dá)式來(lái)決定的。
它們都不是要遍歷全部元素才能返回結(jié)果。例如 allMatch 只要一個(gè)元素不滿足條件,就 skip 剩下的所有元素,返回 false。
List<Person> persons = new ArrayList();
persons.add(new Person(1, "name" + 1, 10));
persons.add(new Person(2, "name" + 2, 21));
persons.add(new Person(3, "name" + 3, 34));
persons.add(new Person(4, "name" + 4, 6));
persons.add(new Person(5, "name" + 5, 55));
boolean isAllAdult = persons.stream(). allMatch(p -> p.getAge() > 18);
System.out.println("All are adult? " + isAllAdult);
boolean isThereAnyChild = persons.stream().anyMatch(p -> p.getAge() < 12);
System.out.println("Any child? " + isThereAnyChild);
輸出結(jié)果:
All are adult? false
Any child? true
其它demo:
//Map通過(guò)Stream轉(zhuǎn)換為set集合
public static void demo(){
//新創(chuàng)建一個(gè)map,然后給map賦值
Map<String,Object> map = new HashMap<>();
map.put("a","張三");
map.put("b","李四");
map.put("c","王五");
map.put("d","趙六");
map.put("e","孫七");
map.put("f","陳八");
//根據(jù)map中的key過(guò)濾得到key不是a的結(jié)果然后獲取value值
List<Object> relult = map.entrySet().
stream().
filter((e)->!e.getKey().equals("a")).
map((e)->e.getValue()).
collect(Collectors.toList());
System.out.println(relult.toString());
}
運(yùn)行結(jié)果:
[李四,王五,趙六,孫七,陳八]