許多方法都返回元素的序列投慈。在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處理和迭代寡痰。