之前看了許多介紹Java8 Stream的文章,但是初次接觸真的是難以理解(我悟性比較低),沒辦法只能"死記硬背",但是昨天我打王者榮耀(那一局我贏了,牛魔全場MVP)的時候,突然迸發了靈感,感覺之前沒有理解透徹的一下子就理解透徹了。所以決定用簡單的方式來回憶下我認為的java8 Stream.
lambda表示式
語法
lambda表示式是Stream API的基石,所以想要學會Stream API的使用,必須先要理解lambda表示式,這裡對lambda做一個簡單回顧。
我們常常會看到這樣的程式碼
Arrays.sort(new Integer[]{1, 8, 7, 4}, new Comparator<Integer>() {
@Override
public int compare(Integer first, Integer second) {
return first.compareTo(second);
}
});
複製程式碼
上面這種寫法就是使用了匿名類,我們經常會使用匿名類的方式,因為我們只執行一次,不想它一直存在。雖然說lambda表示式是為了什麼所謂的函數語言程式設計,也是大家在社群千呼萬喚才出來的,但是在我看來就是為了方(偷)便(懶)。
上面的程式碼寫著麻煩,但是轉換成下面這樣的呢?
Arrays.sort(new Integer[]{1, 8, 7, 4},
(first,second) -> first.compareTo(second));
複製程式碼
這樣看著多清爽,而且把一些不必要的細節都遮蔽了。對於這種只包含一個抽象方法的介面,你可以通過lambda介面來建立該介面的物件,這種介面被稱為函式式介面。
lambda表示式引入了一個新的操作符:->,它把lambda表示式分為了2部分
(n) -> n*n
複製程式碼
左側指定表示式所需的引數,如果不需要引數,也可以為空。右側是lambda程式碼塊,它指定lambda表示式的動作。
需要注意的是如果方法中只有一個返回的時候不用宣告,預設會返回。如果有分支返回的時候需要都進行宣告。
(n) -> {
if( n <= 10)
return n*n;
return n * 10;
}
複製程式碼
方法引用以及構造器引用
方法引用
有些時候,先要傳遞給其他程式碼的操作已經有實現的方法了。比如GUI中先要在按鈕被點選時列印event物件,那麼可以這樣呼叫
button.setOnAction(event -> System.out.println(event));
複製程式碼
這個時候我想偷懶,我不想寫event引數,因為只有一個引數,jvm不能幫幫我嗎?下面是修改好的程式碼
button.setOnAction(System.out::println);
複製程式碼
表示式System.out::println
是一個方法引用,等同於lambda表示式x -> System.out.println(x)
。**::**操作符將方法名和物件或類的名字分割開來,以下是三種主要的使用情況:
- 物件::例項方法
- 類::靜態方法
- 類::例項方法
前兩種情況,方法引用等同於提供方法引數的lambda表示式。比如Math::pow ==== (x,y) -> Math.pow(x,y)
。
第三種情況,第一個引數會稱為執行方法的物件。比如String::compareToIgnoreCase ==== (x,y) -> x.compareToIgnoreCase(y)
。
還有this::equals ==== x -> this.equals(x)
,super::equals ==== super.equals(x)
。
構造器引用
List<String> strList = Arrays.asList("1","2","3");
Stream<Integer> stream = strList.stream().map(Integer::new);
複製程式碼
上面程式碼的Integer::new
就是構造器引用,不同的是在構造器引用中方法名是new。如果存在多個構造器,編譯器會從上下文推斷並找出合適的那一個。
StreamAPI
Stream這個單詞翻譯過來就是流的意思,溪流的流,水流的流。
在我看來stream就像是上面的圖一樣,最開始的資料就是小水滴,它經過各種"攔截器"的處理之後,有的小水滴被丟棄,有的變大了,有的加上了顏色,有的變成了三角形。最後它們都變成了帶有顏色的圓。最後被我們放到結果集中。我們很多時候寫的程式碼是這樣的:遍歷一個集合,然後對集合的元素進行判斷或者轉換,滿足條件的加入到新的集合裡面去,這種處理方式就和上面的圖是一樣的。先來看一段程式碼
Map<String,Map<String,Integer>> resultMap = new HashMap<>();
Map<String,Integer> maleMap = new HashMap<>();
Map<String,Integer> femaleMap = new HashMap<>();
resultMap.put("male", maleMap);
resultMap.put("female",femaleMap);
for(int i = 0; i < list.size(); i++) {
Person person = list.get(i);
String gender = person.getGender();
String level = person.getLevel();
switch (gender) {
case "male":
Integer maleCount;
if("gold".equals(level)) {
maleCount = maleMap.get("gold");
maleMap.put("gold", null != maleCount ? maleCount + 1 : 1);
} else if("soliver".equals(level)){
maleCount = maleMap.get("soliver");
maleMap.put("soliver", null != maleCount ? maleCount + 1 : 1);
}
break;
case "female":
Integer femaleCount;
if("gold".equals(level)) {
femaleCount = femaleMap.get("gold");
femaleMap.put("gold", null != femaleCount ? femaleCount + 1 : 1);
} else if("soliver".equals(level)){
femaleCount = femaleMap.get("soliver");
femaleMap.put("soliver", null != femaleCount ? femaleCount + 1 : 1);
}
break;
}
}
複製程式碼
上面的程式碼作用是統計不同性別的工程師職級的人數,在Java StreamAPI出來之前,這樣類似的業務程式碼在系統中應該是隨處可見的,手打上面的程式碼我大概花了兩分鐘,有了Stream之後,我偷了個懶
Map<String,Map<String,Integer>> result = list.stream().collect(
Collectors.toMap(
person -> person.getGender(),
person -> Collections.singletonMap(person.getLevel(), 1),
(existValue,newValue) -> {
HashMap<String,Integer> newMap = new HashMap<>(existValue);
newValue.forEach((key,value) ->{
if(newMap.containsKey(key)) {
newMap.put(key, newMap.get(key) + 1);
} else {
newMap.put(key, value);
}
});
return newMap;
})
);
複製程式碼
或者改成這樣的程式碼
Map<String,Map<String,Integer>> result = stream.collect(
Collectors.groupingBy(
Person::getGender,
Collectors.toMap(
person->person.getLevel(),
person -> 1,
(existValue,newValue) -> existValue + newValue
)
)
);
複製程式碼
不僅程式碼塊減少了許多,甚至邏輯也更清晰了。真的是用stream一時爽,一直用一直爽呀。
Stream作為流,它可以是有限的可以是無限的,當然我們用得最多的還是有限的流(for迴圈就是有限的流),如上面那張圖一樣,我們可以對流中的元素做各種各樣常見的處理。比如求和,過濾,分組,最大值,最小值等常見處理,所以現在就開始使用Stream吧
Stream的特性
- Stream自己不會儲存元素,元素可能被儲存在底層集合中,或者被生產出來。
- Stream操作符不會改變源物件,相反,他們會返回一個持有新物件的stream
- Stream操作符是延遲執行的,可能會等到需要結果的時候才去執行。
Stream API
函式式介面 | 引數型別 | 返回型別 | 抽象方法名 | 描述 | 其他方法 |
---|---|---|---|---|---|
Runnable | 無 | void | run | 執行一個沒有引數和返回值的操作 | 無 |
Supplier<T> | 無 | T | get | 提供一個T型別的值 | |
Counsumer<T> | T | void | accept | 處理一個T型別的值 | chain |
BiConsumer<T,U> | T,U | void | accept | 處理T型別和U型別的值 | chain |
Function<T,R> | T | R | apply | 一個引數型別為T的函式 | compose,andThen,identity |
BiFunction<T,U,R> | T,U | R | apply | 一個引數型別為T和U的函式 | andThen |
UnaryOperator<T> | T | T | apply | 對型別T進行的一元操作 | compose,andThen,identity |
BinaryOperator<T> | T,T | T | apply | 對型別T進行二元操作 | andThen |
Predicate<T> | T | boolean | test | 一個計算boolean值的函式 | And,or,negate,isEqual |
BiPredicate<T,U> | T,U | boolean | test | 一個含有兩個引數,計算boolean值的函式 | and,or,negate |
map()和flatMap()的區別
使用map方法的時候,相當於對每個元素應用一個函式,並將返回的值收集到新的Stream中。
Stream<String[]> -> flatMap -> Stream<String>
Stream<Set<String>> -> flatMap -> Stream<String>
Stream<List<String>> -> flatMap -> Stream<String>
Stream<List<Object>> -> flatMap -> Stream<Object>
{{1,2}, {3,4}, {5,6} } -> flatMap -> {1,2,3,4,5,6}
複製程式碼
中間操作以及結束操作
Stream上的所有操作分為兩類:中間操作和結束操作,中間操作只是一種標記(呼叫到這類方法,並沒有真正開始流的遍歷。),只有結束操作才會觸發實際計算。簡單的說就是API返回值仍然是Stream的就是中間操作,否則就是結束操作。
如何debug
- 請使用程式碼段,比如
IntStream.of(1,2,3,4,5).fiter(i -> {return i%2 == 0;})
將斷點打在程式碼段上即可。 - 引用方法也可以進行除錯,在isDouble中打上斷點比如
IntStream.of(1,2,3,4,5).fiter(MyMath::isDouble)
那些不好理解的API
- reduce() 我們以前做累加是如何完成的呢?
int sum = 0;
for(int value in values) {
sum = sum + value;
}
複製程式碼
現在改成stream的方式來實現
values.stream().reduce(Integer::sum);
複製程式碼
這個reduce()方法就是一個二元函式:從流的前兩個元素開始,不斷將它應用到流中的其他元素上。
如何寫好Stream程式碼
stream API就是為了方便而設計的,在sql層面並不方便處理的資料可以通過stream來實現分組,聚合,最大值,最小值,排序,求和等等操作。所以不要把它想得太複雜,只管寫就好了。總有那麼一天你熟練了就可以寫出簡潔得程式碼。或者從現在開始把你專案中的大量for迴圈改造成stream方式。
程式碼示例
本來想寫大段程式碼來樣式到stream API的轉換,但是想了想完全沒有必要,github上找了hutool工具類的部分程式碼來完成轉換示例。(可以通過這種方式來提高stream api的能力)
- 計算每個元素出現的次數(請先想象下jdk7怎麼實現)
程式碼效果:[a,b,c,c,c] -> a:1,b:1,c:3
Arrays.asList("a","b","c","c","c").stream().collect(Collectors.groupingBy(str->str, Collectors.counting()));
複製程式碼
- 以特定分隔符將集合轉換為字串,並新增字首和字尾(請先想象下jdk7怎麼實現)
List<String> myList = Arrays.asList("a","b","c","c","c");
myList.stream().collect(Collectors.joining(",","{","}"));
複製程式碼
- 判斷列表不全為空(請先想象下jdk7怎麼實現)
myList.stream().anyMatch(s -> !s.isEmpty());
複製程式碼