Java8之Stream流(一)基礎體驗
Java8之Stream流(二)關鍵知識點
Java8之Stream流(三)縮減操作
Java8之Stream流(四)并行流
Java8之Stream流(六)收集
Java8之Stream流(七)流與迭代器
經過了前面四篇文章的學習,相信大家對Stream流已經是相當的熟悉了,同時也掌握了一些高級功能了,如果你之前有閱讀過集合框架的基石Collection
接口,是不是在經過前面的學習,以前看不懂的東西,突然之間就恍然大悟了呢?今天我們的主角是Stream流里面的映射。由于之前,映射并沒有再我們的Demo,例子中出現過,所以對大家來說可能會稍微有一點點陌生的,但通過這一篇文章,我相信能解決你的疑問。
在正式開始之前,我和大家繼續說說流API操作,不知道大家有沒有注意到,其實我們所有的流API操作都是針對流中的元素進行的,并且都是基于同一流里面的,大家有沒有這樣的疑問,怎么樣把一個流的元素弄到另一個流里面呢?怎么把流中的一些滿足條件的元素放到一個新流里面呢?通過這一節,你將會掌握解決剛才問題的本領。另外再提一點,如果流操作只有中間操作,沒有終端操作,那么這些中間操作是不會執行的,換句話說,只有終端操作才能觸發中間操作的運行。
我們為什么需要映射?
因為在很多時候,將一個流的元素映射到另一個流對我們是非常有幫助的。比如有一個包含有名字,手機號碼和錢的數據庫構成的流,可能你只想要映射錢這個字段到另一個流,這時候可能之前學到的知識就還不能解決,于是映射就站了出來了。另外,如果你希望對流中的元素應用一些轉換,然后把轉換的元素映射到一個新流里面,這時候也可以用映射。
我們先來看看流API庫給我們提供了什么樣的支持
public interface Stream<T> extends BaseStream<T, Stream<T>> {
<R> Stream<R> map(Function<? super T, ? extends R> mapper);//line2
IntStream mapToInt(ToIntFunction<? super T> mapper);//line3
LongStream mapToLong(ToLongFunction<? super T> mapper);//line4
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);//line5
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);//line6
IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper);//line7
LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper);//line8
DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper);//line9
}
我和大家分析一個最具有一般性的映射方法map(),相信大家就能舉一反三了,map()定義如下,
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
其中,R指定新流的元素類型,T指定調用流的元素類型,mapper是完成映射的Function實例,被稱為映射函數,映射函數必須是無狀態和不干預的(大家對這二個約束條件應該很熟悉了吧)。因為map()方法會返回一個新流,因此它是一個中間操作。
Function
是 java.util.function
包中聲明的一個函數式接口,聲明如下:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
在map()的使有過程中,T是調用流的元素類型,R是映射的結果類型。其中,apply(T t)中的t是對被映射對象的引用,被返回映射結果。下面我們將上一篇中的例子進行變形,用映射來完成他:
假設List里面有三個Integer類型的元素分別為1,2,3。現在的需求是分別讓List里面的每個元素都放大兩倍后,再求積。這個需求的正確答案應該是48;
private static void learnMap() {
List<Integer> lists = new ArrayList<>();
lists.add(1);
lists.add(2);
lists.add(3);
//使用并行流來處理
Integer product = lists.parallelStream().reduce(1, (a, b) -> a * (b * 2),
(a, b) -> a * b);
System.out.println("product:" + product);//48
//使用映射來處理
//Integer productMap = lists.parallelStream().map((a) -> a * 2).reduce(1, (a, b) -> a * b);
Stream<Integer> productNewMapStream = lists.parallelStream().map((a) -> a * 2);
Integer productMap = productNewMapStream.reduce(1, (a, b) -> a * b);
System.out.println("productMap:" + productMap);//48
}
與使用并行流不同,在使用映射處理的時候,元素擴大2倍發生時機不一樣了,使用并行流元素擴大是在縮減的過程當中的,而使用映射處理時,元素擴大是發生在映射過程中的。因此映射過程完程之后,不需要reduce()提供合并器了。
上面的這個例子還是簡單了一點,下面再舉一個例子,王者榮耀團隊經濟計算:
#玩家使用的英雄以及當前獲得的金幣數
public class HeroPlayerGold {
/** 使用的英雄名字 */
private String hero;
/** 玩家的ID */
private String player;
/** 獲得的金幣數 */
private int gold;
public HeroPlayerGold(String hero, String player, int gold) {
this.hero = hero;
this.player = player;
this.gold = gold;
}
//省略get/set/toString
}
#玩家獲得的金幣數
public class Gold {
/** 獲得的金幣數 */
private int gold;
public Gold(int gold) {
this.gold = gold;
}
//省略get/set/toString
}
#測試類
public class Main {
public static void main(String[] args) {
learnMap2th();
}
private static void learnMap2th() {
List<HeroPlayerGold> lists = new ArrayList<>();
lists.add(new HeroPlayerGold("蓋倫", "RNG-Letme", 100));
lists.add(new HeroPlayerGold("諸葛亮", "RNG-Xiaohu", 300));
lists.add(new HeroPlayerGold("露娜", "RNG-MLXG", 300));
lists.add(new HeroPlayerGold("狄仁杰", "RNG-UZI", 500));
lists.add(new HeroPlayerGold("牛頭", "RNG-Ming", 500));
//計算團隊經濟
int teamMoney = lists.stream()
.map(player -> new Gold(player.getGold()))//note1
.mapToInt(Gold::getGold)
.reduce(0, (a, b) -> a + b);
System.out.println("團隊經濟:" + teamMoney);//1700
//計算團隊經濟2
double teamMoney2 = lists.stream()
.mapToDouble(HeroPlayerGold::getGold)
.reduce(0, (a, b) -> a + b);
System.out.println("團隊經濟:" + teamMoney2);//1700.0
}
}
代碼應該不難理解,通過代碼,大家應該知道我們假設的場景了。我們的RNG去參加王者榮耀比賽了,像這種團隊游戲,觀眾在經濟方面關注更多的可能是團隊經濟,而不是個人經濟。在我們HeroPlayerGold
類里面存有明星玩家,使用的英雄,和這局比賽某個玩家當前獲得的金幣數,我們另有一個專們管理金幣的Gold
類,我們第一種計算團隊經濟的方式,使把HeroPlayerGold
里面的gold
字段轉換到Gold
里面了 //note1
,這里產生的新流只包含了原始流中選定的gold
字段,因為我們的原始流中包含了hero
、player
、gold
,三個字段,我們只選取了gold
字段(因為我們只關心這個字段),所以其它的兩個字段被丟棄了。然后從新流取出Gold
里面的gold
字段并把他轉成一個IntStream
,然后我們就要以通過縮減操作完成我們的團隊經濟計算了,第一種方式,大家需要好好理解,理解了,我相信你們的項目中,很多很多地方可以用得上了,再也不需要動不動就查數據庫了,怎樣效率高怎樣來,只是一種建議。第二種只是快速計算團隊經濟而已,沒什么值得講的。
接下來講一下他的擴展方向:大家還記得我在第二篇中介紹中間操作概念的時候嗎?中間操作會產生另一個流。因此中間操作可以用來創建執行一系列動作的管道。
我們可以把多個中間操作放到管道中,所以我們很容易就創建出很強大的組合操作了,發揮你的想象,打出你們的組合拳;
我現在舉一個例子:比如現在相統計團隊里面兩個C位的經濟占了多少,代碼看起來可能就是這樣了:
private static void learnMap2th() {
List<HeroPlayerGold> lists = new ArrayList<>();
lists.add(new HeroPlayerGold("蓋倫", "RNG-Letme", 100));
lists.add(new HeroPlayerGold("諸葛亮", "RNG-Xiaohu", 300));
lists.add(new HeroPlayerGold("露娜", "RNG-MLXG", 300));
lists.add(new HeroPlayerGold("狄仁杰", "RNG-UZI", 500));
lists.add(new HeroPlayerGold("牛頭", "RNG-Ming", 500));
//計算兩個C位的經濟和
lists.stream()
.filter(player-> "RNG-Xiaohu".equals(player.getPlayer()) || "RNG-UZI".equals(player.getPlayer()))
.map(player->new Gold(player.getGold()))
.mapToInt(Gold::getGold)
.reduce((a,b)->a+b)
.ifPresent(System.out::println);//800
}
大家有沒有感覺,這種操作怎么帶有點數據庫的風格啊?其實在創建數據庫查詢的時候,這種過濾操作十分常見,如果你經常在你的項目中使用流API,這幾個條件算什么?等你們把流API用熟了之后,你們完全可以通過這種鏈式操作創建出非常復雜的查詢,合并和選擇的操作。
通過前面的例子,我們已經把map()
,mapToInt()
,mapToLong()
,mapToDouble
都講了。那么剩下的就是flatMap()方法了。本來想讓大家自行去理解這個方法的,因為怕這篇文章寫得太長了。但是后面想想,還是我來給大家分析一下吧。
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
通過前面的學習我們知道mapper
是一個映射函數,它和map()方法也一樣也會返回一個新流,我們把返回的新流稱為映射流。我們提供的映射函數會處理原始流中的每一個元素,而映射流中包含了所有經過我們映射函數處理后產生的新元素。加粗部份需要重點理解。
我們來看一下源碼對flatMap()的注釋:The flatMap() operation has the effect of applying a one-to-many transformation to the elements of the stream, and then flattening the resulting elements into a new stream.
大意就是:flatMap()操作能把原始流中的元素進行一對多的轉換,并且將新生成的元素全都合并到它返回的流里面。根據我們所學的知識,他的這種一對多的轉換功能肯定就是映射函數提供的,這一點沒有疑問吧!然后源碼的注釋上面還提供了一個例子,通過注釋加例子,我相信大家都能非常清楚地理解flatMap()了。
/* <p>If {@code orders} is a stream of purchase orders, and each purchase
* order contains a collection of line items, then the following produces a
* stream containing all the line items in all the orders:
* <pre>{@code
* orders.flatMap(order -> order.getLineItems().stream())...
* }</pre>
*/
如果orders是一批采購訂單對應的流,并且每一個采購訂單都包含一系列的采購項,那么orders.flatMap(order -> order.getLineItems().stream())...
生成的新流將包含這一批采購訂單中所有采購項。我們用偽代碼來就更加清晰了 Stream<Orders<OrderItem>> ====>Stream<OrderItem>。大家能理解了嗎?還沒理解?再來一個例子:
private static void learnFlatMap() {
//(廣州 深圳 上海 北京)的全拼的一些組合,下面我們就把每一個城市都劃分一下
List<String> citys = Arrays.asList("GuangZhou ShangHai", "GuangZhou ShenZhen",
"ShangHai ShenZhen", "BeiJing ShangHai", "GuangZhou BeiJing", "ShenZhen BeiJing");
//這里打印的數組對應的地址
citys.stream().map(mCitys -> Arrays.stream(mCitys.split(" "))).forEach(System.out::println);//note1
System.out.println();
//流里面的元素還是一個數組
citys.stream()
.map(mCities -> Arrays.stream(mCities.split(" ")))//流里面的每個元素還是數組
.forEach(cities ->cities.forEach(city-> System.out.print(city+" ")));//note2
System.out.println();
System.out.println();
//直接一個flatMap()就把數組合并到映射流里面了
citys.stream().flatMap(mCities->Arrays.stream(mCities.split(" "))).forEach(System.out::println);//note3
System.out.println();
//使用distinct()方法去重!
citys.stream().flatMap(mCities->Arrays.stream(mCities.split(" "))).distinct().forEach(System.out::println);//note4
}
其中 //note1
處是無法打印元素的,使用map()打印元素的方式在 //note2
,原因也在注釋中交待了,但是使用了flatMap()方法后,直接就可以打印了 //note3
,到這里,應該就能理解**如果orders是一批采購訂單對應的流,并且每一個采購訂單都包含一系列的采購項,那么
orders.flatMap(order -> order.getLineItems().stream())...生成的新流將包含這一批采購訂單中所有采購項。**
了吧。最后 //note4
是一個去重的方法,大家運行一遍吧。
小結一下
通過這一篇文章,相信大家對流API中的映射已經不再陌生了,其實最需要注意的一個點是,map()和flatMap()的區別,我也一步步地帶著大家理解和應用了。其實在流API這一塊中,大家單單掌握概念是沒什么用的,一定要去實戰了,一個項目里面,集合框架這種東西用得還是特別多的,用到集合框架的大部份情況,其實都可以考慮一下用Stream流去操作一下,不僅增加效率,還可以增加業務流程的清晰度。