1. 程式人生 > >2、流

2、流

abc rtt ted 第一個元素 ack fur 列表 當前 程序員

流Stream

1、外部叠代到內部叠代。
求總數的代碼:
int count = 0;
for(Artist artist : allArtists){
if(artist.isForm("Lodon")){
count++;
}
}
這樣的操作存在幾個問題:
1、每次叠代集合元素都需要寫很多樣板代碼
2、將for循環改成並行很麻煩
3、for循環過大不一定能很好傳達程序員意圖
forEach是一個語法糖,實際操作如下:
int count = 0;
Iterator<Artist> iterator = allArtist.iterator();
while(iterator.hasNext()){
Artist artist = iterator.next();
if(artist.isFrom("Lodon")){
count++;
}
}
內部叠代操作:
long count = allArtist.stream()
.filter(artist -> artist.isFrom("London"))
.count();
2、實現機制
在上面的例子,過程分解為兩個簡單的操作:過濾和計數。
兩次操作是否需要兩次循環?實際上,只需要叠代一次。
java中通常調用一個方法,計算機會立即執行操作。Stream裏面的一些方法卻有些不同,雖然是普通的java方法,但返回的Stream對象卻不是一個新集合,而是創建新集合的配方。
allArtist.stream()
.filter(artist -> artist.isForm("London"))
這代碼其實並沒有做什麽實質工作,只是刻畫出了stream,但沒有產生新的集合。類似filter這樣只描述filter,最終不產生新集合的方法叫做惰性求值方法。類似count這樣最終從Stream產生值的方法叫做及早求值方法。
3、常用的流操作
collect(toList()):由Stream裏面的值生成一個列表。
of方法適用一組初始值生成新的strean
public void tranList(){
Stream<String> stream = Stream.of("a","b","ab","c");
List<String> list = stream.collect(Collectors.toList());
}

map
如果有一個函數可以將一種類型的值轉換成另一種類型,map操作就可以使用該函數,將一個流中的值轉換成另一個流。
public void testMap(){
/** 原操作 **/
List<String> collected = new ArrayList<>();
for(String string : Arrays.asList("a","b","hello")){
String upperCaseString = string.toUpperCase();
collected.add(upperCaseString);
}

/** 現操作 **/
List<String> collected1 = Stream.of("a", "b", "ab")
.map(string -> string.toUpperCase())
.collect(Collectors.toList());
}
傳給例子中的傳給map的Lambda表達式只接受一個String類型的參數,返回一個新的String。參數和返回值不必屬於同一種類型,但是Lambda表達式必須是Function接口的一個實例。接受T,返回R

filter
遍歷並檢查其中的元素時,可以嘗試使用stream中提供的新方法filter
public void testFilter(){
List<String> beginningWithNumbers = Stream.of("a", "1abc", "abc1")
.filter(value -> Character.isDigit(value.charAt(0)))
.collect(Collectors.toList());
}
和map一樣,filter接受一個函數作為參數,函數用Lambda表示。函數返回boolean,返回true的函數被保留。該Lambda表達式就是Predicate

flatMap
flatMap方法可以為Stream替換值,然後將多個Stream連接成一個Stream
public void testFlatMap(){
List<Integer> together = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4))
.flatMap(numbers -> numbers.stream())
.collect(Collectors.toList());
}
調用stream方法,將每個列表轉換成stream對象,其余部分由flatMap方法處理。
flatMap方法的相關函數接口和map方法的一樣,都是Function接口,只是方法的返回值限定為stream類罷了。

max和min
Stream上常用的操作之一是求最大值和最小值。Stream API中的max和min操作足以解決這一問題。
public void testMinAndMax(){
List<Track> tracks = Arrays.asList(new Track("Bakai", 524),
new Track("Violets for Your Furs", 378),
new Track("Time Was", 451));
Track shortTestTrack = tracks.stream()
.min(Comparator.comparing(track -> track.getName().length()))
.get();
}

通用模式
max和min方法都屬於更通用的一種編程模式。要看到這種編程模式,最簡單的方法是使用for循環重寫上面例子中的代碼。
public void usuallyModel(){
List<Track> tracks = Arrays.asList(new Track("Bakai", 524),
new Track("Violets for Your Furs", 378),
new Track("Time Was", 451));
Track sortTestTrack = tracks.get(0);
for(Track track: tracks){
if(track.getName().length() < sortTestTrack.getName().length()){
sortTestTrack = track;
}
}
}
先使用第一個元素初始化一個變量,然後遍歷列表,如果找到更適合的,則更新外面的變量,最後找到的就是最適合的變量
Stream API中的reduce可以達到。

reduce
reduce操作可以實現從一組值中生成一個值。上面例子中的count、min、max,因為過於常用被納入標準庫。其實,這些方法都是reduce操作。
public void testReduce(){
int count = Stream.of(1, 2, 3)
.reduce(0, (acc, element) -> acc + element);
}
Lambda表達式的返回值是最新的acc,是上一輪acc的值和當前元素相加的結果。reduce類型是BinaryOperator類型。
將reduce代碼展開,就是下面的代碼:
BinaryOperator<Integer> accumulator = (acc, element) -> acc + element;
int count1 = accumulator.apply(
accumulator.apply(accumulator.apply(0, 1),
2),
3);
原來的命令編程寫法:
int acc = 0;
for(Integer element : Arrays.asList(1, 2, 3)){
acc = acc + element;
}

整合操作
Stream接口的方法如此之多,有時會讓人難以選擇,不知道使用什麽更好。
例子:
需求:找出某張專輯上所有樂隊的國籍。藝術家列表裏既有個人,也有樂隊。利用一點領域知識,假定一般樂隊名以定冠詞The開頭。
步驟:
1、找出專輯上的所有表演者
2、分辨出那些表演者是樂隊
3、找出每個樂隊的國籍
4、將找出的國籍放入一個集合
performers.stream()
.filter(performer -> performer.getName().startWith("The"))
.map(performer - performer.getCountry())
.collect(Collection.toSet());
一個思考,操作的時候,你真的需要暴露整個List或Set這樣的集合對象嗎?可能一個Stream工廠才是更好的選擇。通過Stream暴露集合的最大優點在於,它很好的封裝了內部的數據結構。僅暴露一個Stream接口,用戶在實際操作中無論如何使用,都不會影響內部的List或Set。

4、重構遺留代碼
如何將一段使用循環進行集合操作的代碼,重構成基於Stream的操作。
需求:
選定一組專輯,找出其中所有長度大於1分鐘的曲目名稱。
遺留代碼:
public Set<String> findLongTracks(List<Album> albums){
Set<String> trackNames = new HashSet<>();
for(Album album : albums){
for(Track track :album.getTrackList()){
if(track.getLength() > 60){
String name = track.getName();
trackNames.add(name);
}
}
}
return trackNames;
}
代碼分析:
1、聲明外部裝載容器
2、遍歷專輯
3、遍歷專輯中的曲目
4、找出專輯中曲目長度大於60的
5、將長度大於60的裝進外部集合
Stream API實現:
1、專輯得到流Stream
2、流操作結束得到一個集合。reduce操作。
3、reduce操作分解:
輸入空集合A,流B 得到 新集合A
子reduce操作:
filter () 篩選流
map 映射替換得到名字
合並集合
代碼實現:
/**
Set<String> names = new HashSet<String>();
albums.stream()
.reduce(names,album -> {
album.getTrackList().stream()
.filter(track -> track.getLength() > 60)
.map(trace -> trace.getName())
.reduce(names,name -> names.add(name))
})
.collect(Collection.toSet());
前面的代碼是錯誤的,因為我忽略了一個問題,匿名函數裏面的外部成員必須是final
**/
albums.stream()
.flatMap(album -> album.getTracks())
.stream()
.filter(track -> track.getLength() > 60)
.map(trace -> trace.getName())
.collect(Collection.toSet());
5、多次調用流操作。
用戶也可以選擇每一步強制對函數求值,而不是將所有的方法調用鏈接在一起,但是最好不要如此操作。
List<Artist> musiciams = album.getMusicians().collect(toList());
List<Artist> bands = musicans.stream()
.filter(artist -> artist.getName().startsWith("The"))
.collect(toList());
Set<String> origins = binds.stream()
.map(artist -> artist.getNationality())
.collect(toSet());
符合Stream使用習慣的鏈式調用:
Set<String> origins = alubm.getMusicans()
.filter(artist -> artist.getName().startsWith("The"))
.map(artist -> artist.getNationality())
.collect(toSet());
多次調用相比鏈式調用的缺點:
1、代碼可讀性茶,隱藏真正業務邏輯
2、效率差,每一步都要對流及早求值,生成新的集合
3、代碼充斥一堆垃圾變量
3、難於自動並行化處理

6、高階函數
本章中不斷出現被函數式編程程序員稱為高階函數的操作。高階函數式指能接受另一個函數作為參數,或返回一個函數的函數。
map是一個高階函數。實際上,本章介紹的Stream接口中幾乎所有的函數都是高階函數。之前的排序例子中用到了comparing函數,它接受一個函數作為參數,獲取對應值,同時返回一個Comparator。Comparator可能被誤認為是一個對象,但它有且只有一個抽象方法,所以實際上時一個函數接口。

7、正確使用Lambda表達式
開始介紹Lambda表達式時,能輸出一些信息的回調函數為示例。回調函數式一個合法的Lambda表達式,但並不能真正幫助用戶寫出更簡單、更抽象的代碼,因為它仍然在只會計算機執行一個操作。
本章的介紹能讓用戶寫出更簡單的代碼,因為這些概念描述了數據上的操作,明確了要達成什麽轉化,而不是說明如何轉化,這樣子寫出的代碼,潛在的缺陷更少。
沒有副作用的函數不會改變程序或外界的狀態。書中的第一個Lambda表達式是由副作用的,它向控制臺輸出了信息————一個可觀測到的副作用。無論何時,將Lambda表達式傳給Stream上的高階函數,都應該盡量避免副作用。唯一的例外是forEach方法,它是一個終結方法。

2、流