第四十七條:Stream要優(yōu)先用Collection作為返回類型

許多方法都返回元素的序列投慈。在Java8之前承耿,這類方法明顯的返回類型是集合接口Collection冠骄、Set和List;Iterable加袋;以及數(shù)組類型凛辣。一般來說,很容易確定要返回這其中哪一種類型职烧。標(biāo)準(zhǔn)是一個集合接口扁誓。如果某個方法只為for-each循環(huán)或者返回序列而存在,無法用它來實現(xiàn)一些Collection方法(一般是contains(Objetc))蚀之,那么就用Iterable接口吧蝗敢。如果返回的元素是基本類型值,或者有嚴(yán)格的性能要求足删,就是用數(shù)組寿谴。在Java8中增加了Strema,本質(zhì)上導(dǎo)致給序列化返回的方法選擇適當(dāng)返回類型的任務(wù)變得更復(fù)雜了失受。

或許你曾聽說過讶泰,現(xiàn)在Stream是返回元素序列最明顯的選擇了,但如第45條所述贱纠,Stream并沒有淘汰迭代:要編寫出優(yōu)秀的代碼必須巧妙地將Stream與迭代結(jié)合起來使用峻厚。如果一個API只返回一個Stream,那么想要用for-each循環(huán)遍歷返回序列地用戶肯定會失望了谆焊。因為Stream接口只在Iterable接口中包含了唯一一個抽象方法惠桃,Stream對于該方法地規(guī)范也適用于Iterable的。唯一可以讓程序員避免用for-each循環(huán)遍歷Stream的是Stream無法擴(kuò)展Iterable接口辖试。

遺憾的是辜王,這個問題還沒有適當(dāng)?shù)慕鉀Q辦法。咋看之下罐孝,好像給Stream的iterator方法傳入一個方法引用可以解決呐馆。這樣得到的代碼可能有點雜亂、不清晰莲兢、但也不算難以理解:

// Won't compile, due to limitations on Java's type inference
for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) {
  // Process the process 
}

遺憾的是汹来,如果想要編譯這端代碼,就會得到一條報錯的信息:

Test.java:6: error: method reference not expected here
for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) {

為了使代碼能夠進(jìn)行編譯改艇,必須將方法引用轉(zhuǎn)換成適當(dāng)參數(shù)化的Iterable:

// Hideous workaround to iterate over a stream
for (ProcessHandle ph : 
                    (Iterable<ProcessHandle>) ProcessHandle.allProcesses()::iterator)

這個客戶端代碼是可行的收班,但是實際上使用時過于雜亂、不清晰谒兄。更好的解決辦法是使用適配器
方法摔桦。JDK沒有提供這樣的方法,但是編寫起來很容易承疲,使用在上述代碼中內(nèi)嵌的相同方法即可邻耕。注意鸥咖,在適配器方法沒有必要進(jìn)行轉(zhuǎn)換,因為Java的類型引用在這里正好派上了用場:

// Adapter from Stream<E> to Iterable<E>
public static <E> Iterable<E> iterableOf(Stream<E> stream) { 
  return stream::iterator;
}

有了這個適配器兄世,就可以利用for-each語句遍歷任何Stream:

for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) { 
  // Process the process
}

注意啼辣,第34條中Anagrams程序的Stream版本是使用Files.lines方法讀取詞典,而迭代版本則使用了掃描器(scanner)碘饼。Files.lines方法優(yōu)于掃描器熙兔,因為后者默默的吞掉了在讀取文件過程中遇到的所有異常。最理想的方式是在迭代版本中也使用Files.lines艾恼。這是程序員在特定情況下所做的一種妥協(xié)住涉,比如當(dāng)API只有Stream能訪問序列,而他們想通過for-each語句遍歷該序列的時候钠绍。

反過來說舆声,想要利用Stream pipeline處理序列的程序員,也會被只提供Iterable的API搞得束手無策柳爽。同樣的媳握,JDK沒有提供適配器,但是編寫起來也很容易:

// Adapter from Iterable<E> to Stream<E>
public static <E> Stream<E> streamOf(Iterable<E> iterable) { 
  return StreamSupport.stream(iterable.spliterator(), false);
}

如果在編寫一個返回對象序列的方法時磷脯,就知道它只在Stream pipeline中使用蛾找,當(dāng)然就可以放心的返回Stream了。同樣的赵誓,當(dāng)返回序列的方法只在迭代中使用時打毛,則應(yīng)該返回Iterable。但如果是用公共的API返回序列俩功,則應(yīng)該為那些想要編寫Stream pipeline幻枉,以及想要編寫for-each語句的用戶分別提供,除非有足夠的理由相信大多數(shù)用戶都想要使用相同的機(jī)制诡蜓。

Collection接口時Iterable的一個子類型熬甫,它有一個stream方法,因此提供了迭代和stream訪問蔓罚。對于公共的椿肩、返回序列的方法,Collection或者適當(dāng)?shù)淖宇愋屯ǔJ亲罴训姆祷仡愋?/code>豺谈。數(shù)組也通過Arrays.asList和Stream.of方法提供了簡單的迭代和stream訪問覆旱。如果返回的序列足夠小,容易存儲核无,或許最好返回標(biāo)準(zhǔn)的集合實現(xiàn),如ArrayList或者HashSet藕坯。但是千萬別在內(nèi)存中保存巨大的序列团南,將它作為集合返回即可噪沙。

如果返回的序列很大,但是能被準(zhǔn)確表述吐根,可以考慮實現(xiàn)一個專用的集合正歼。假設(shè)想要返回一個指定集合的冪集,其中包括所有的子集拷橘。{a, b, c}的冪集是:{{}, {a}, 局义, {c}, {a、b}, {a, c}, {b, c}, {a, b, c}}冗疮。如果集合中有n個元素萄唇,它的冪集就有2n個。因此术幔,不必考慮將冪集保存在標(biāo)準(zhǔn)的集合實現(xiàn)中另萤。但是,有了AbstracList的協(xié)助诅挑,為此實現(xiàn)定制集合就很容易了四敞。

技巧在于,用冪集中每個元素的索引作為位向量拔妥,在索引中排第n位忿危,表示源集合中第n位元素存在或者不存在,實質(zhì)上没龙,在二進(jìn)制數(shù)0至2n-1和有n位元素的集合的冪集之間铺厨,有一個自然映射。代碼如下:

// Returns the power set of an input set as custom collection
public class PowerSet {
  public static final <E> Collection<Set<E>> of(Set<E> s) { 
    List<E> src = new ArrayList<>(s);
    if (src.size() > 30)
      throw new IllegalArgumentException("Set too big " + s); 
    return new AbstractList<Set<E>>() {
        @Override public int size() {
            return 1 << src.size(); // 2 to the power srcSize
        }
        @Override public boolean contains(Object o) { 
            return o instanceof Set && src.containsAll((Set)o);
        }
        @Override public Set<E> get(int index) { 
            Set<E> result = new HashSet<>();
            for (int i = 0; index != 0; i++, index >>= 1)
                if ((index & 1) == 1) result.add(src.get(i));
                    return result; 
            }
        }; 
    }
}

注意, 如果輸入集有超過30個元素兜畸,PowerSet.of 拋出異常努释。這突出了使用集合作為返回類型而不是流或 Iterable 的缺點: 集合有一個 int 返回大小方法,它將返回序列的長度限制為整數(shù)咬摇。Integer.MAX_VALUE伐蒂,或者2(31 - 1)。集合規(guī)范確實允許 size 方法返回2(31 - 1)(如果集合更大肛鹏,甚至是無窮大)逸邦,但這不是一個完全令人滿意的解決方案。

為了在 AbstractCollection 之上編寫集合實現(xiàn)在扰,除了 Iterable 所需的方法之外缕减,您只需要實現(xiàn)兩個方法: contains 和 size。通常很容易編寫這些方法的有效實現(xiàn)芒珠。如果它不可行桥狡,可能是因為序列的內(nèi)容在迭代發(fā)生之前沒有預(yù)先確定,那么返回一個流或 iterable,無論哪個更自然裹芝。如果選擇部逮,可以使用兩個單獨的方法返回。

有時嫂易,您將僅根據(jù)實現(xiàn)的簡單程度來選擇返回類型兄朋。例如,假設(shè)您希望編寫一個方法來返回輸入列表的所有(連續(xù)的)子列表怜械。只需要三行代碼就可以生成這些子列表并將它們放入一個標(biāo)準(zhǔn)集合中颅和,但是保存這個集合所需的內(nèi)存是源列表大小的兩倍。雖然這沒有冪集那么糟糕缕允,冪集是指數(shù)的峡扩,但顯然是不可接受的。實現(xiàn)自定義集合(就像我們在 power 集中所做的那樣)將是冗長乏味的灼芭,因為 JDK 缺少一個框架迭代器實現(xiàn)來幫助我們有额。

但是,實現(xiàn)一個輸入列表的所有子列表的流是很簡單的彼绷,盡管它需要一點洞察力巍佑。讓我們將包含列表的第一個元素的子列表稱為列表的前綴。例如寄悯,(a, b, c) 的前綴是(a)萤衰, (a, b),和 (a, b, c)猜旬。同樣,我們叫一個子列表,其中包含后綴,最后一個元素的后綴(a, b, c) (a, b, c)脆栋、(b, c)和(c),洞察力是列表的子列表只是后綴的前綴(或相同的前綴后綴)和空列表。這一觀察直接導(dǎo)致了一個清晰洒擦、合理椿争、簡潔的實現(xiàn):

// Returns a stream of all the sublists of its input list
public class SubLists {
  public static <E> Stream<List<E>> of(List<E> list) {
    return Stream.concat(Stream.of(Collections.emptyList()), prefixes(list).flatMap(SubLists::suffixes));
  }
  private static <E> Stream<List<E>> prefixes(List<E> list) { 
    return IntStream.rangeClosed(1, list.size()).mapToObj(end -> list.subList(0, end)); 
  }
  private static <E> Stream<List<E>> suffixes(List<E> list) { 
    return IntStream.range(0, list.size()).mapToObj(start -> list.subList(start, list.size())); 
  }
}

注意 Stream.concat 方法用于將空列表添加到返回的流中。還要注意熟嫩,flatMap方法(item 45)用于生成由所有前綴的所有后綴組成的單一流秦踪。最后镣煮,請注意执泰,我們通過映射 IntStream 返回的連續(xù) int 值流來生成前綴和后綴政冻。范圍和 IntStream.rangeClosed昵骤。粗略地說,這個習(xí)慣用法相當(dāng)于整數(shù)索引上的標(biāo)準(zhǔn) for 循環(huán)孽查。因此终息,我們的子列表實現(xiàn)在本質(zhì)上類似于明顯的嵌套 for 循環(huán):

for (int start = 0; start < src.size(); start++)
  for (int end = start + 1; end <= src.size(); end++)
     System.out.println(src.subList(start, end));

像前面的for循環(huán)一樣斥难,這段代碼也沒有發(fā)出空列表逗鸣。為了修正這個錯誤合住,也應(yīng)該使用concat绰精,如前一個版本中那樣,或者用rangeClosed調(diào)用中的(int)Math.signum(start)代替1聊疲。

子列表的這些Stream實現(xiàn)都很好茬底,但這兩者都需要用戶在任何更適合跌打的地方,采用Stream-to-Iterable適配器获洲,或者用Stream。Stream-to-Iterable適配器不僅打亂了客戶端代碼殿如,在我的機(jī)器上循環(huán)的速度還降低了2.3倍贡珊。專門構(gòu)建的Collection實現(xiàn)(此處沒有展示)相當(dāng)煩瑣,但是運行速度在我的機(jī)器上比基于Stream的實現(xiàn)快了1.4倍涉馁。

總而言之门岔,在編寫返回一系列元素方法時,要記住有些用戶可能想要當(dāng)作Stream處理烤送,而其他用戶可能想要使用迭代寒随。要盡量兩邊兼顧。如果可以返回集合帮坚,就返回集合妻往。如果集合中已經(jīng)有元素,或者序列中的元素數(shù)量很少试和,足以創(chuàng)建一個新的集合讯泣,那么就返回一個標(biāo)準(zhǔn)的集合,如ArrayList阅悍。否則就要考慮實現(xiàn)一個定制的集合好渠。如冪集范例中所示。如果無法返回集合节视,就返回Stream或者Iterable拳锚,感覺哪一種更自然即可。如果在未來的Java發(fā)行版本中寻行,Stream接口聲明被修改成擴(kuò)展了Iterable接口霍掺,就可以放心的返回Stream了,因為他們允許進(jìn)行Stream處理和迭代寡痰。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末抗楔,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子拦坠,更是在濱河造成了極大的恐慌连躏,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贞滨,死亡現(xiàn)場離奇詭異入热,居然都是意外死亡拍棕,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進(jìn)店門勺良,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绰播,“玉大人,你說我怎么就攤上這事尚困〈缆幔” “怎么了?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵事甜,是天一觀的道長谬泌。 經(jīng)常有香客問我,道長逻谦,這世上最難降的妖魔是什么掌实? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮邦马,結(jié)果婚禮上贱鼻,老公的妹妹穿的比我還像新娘。我一直安慰自己滋将,他們只是感情好邻悬,可當(dāng)我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著耕渴,像睡著了一般拘悦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上橱脸,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天础米,我揣著相機(jī)與錄音,去河邊找鬼添诉。 笑死屁桑,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的栏赴。 我是一名探鬼主播蘑斧,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼须眷!你這毒婦竟也來了竖瘾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤花颗,失蹤者是張志新(化名)和其女友劉穎捕传,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扩劝,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡庸论,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年职辅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片聂示。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡域携,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鱼喉,到底是詐尸還是另有隱情秀鞭,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布蒲凶,位于F島的核電站气筋,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏旋圆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一麸恍、第九天 我趴在偏房一處隱蔽的房頂上張望灵巧。 院中可真熱鬧,春花似錦抹沪、人聲如沸刻肄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽敏弃。三九已至,卻和暖如春噪馏,著一層夾襖步出監(jiān)牢的瞬間麦到,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工欠肾, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留瓶颠,地道東北人。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓刺桃,卻偏偏與公主長得像粹淋,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子瑟慈,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內(nèi)容