Effective Java 第三版——47. 優先使用Collection而不是Stream來做爲方法的返回類型

Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必不少人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到如今已經將近8年的時間,但隨着Java 6,7,8,甚至9的發佈,Java語言發生了深入的變化。
在這裏第一時間翻譯成中文版。供你們學習分享之用。
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些代碼裏方法是基於Java 9 API中的,因此JDK 最好下載 JDK 9以上的版本。可是Java 9 只是一個過渡版本,因此建議安裝JDK 10。java

Effective Java, Third Edition

47. 優先使用Collection而不是Stream來做爲方法的返回類型

許多方法返回元素序列(sequence)。在Java 8以前,一般方法的返回類型是CollectionSetList這些接口;還包括Iterable和數組類型。一般,很容易決定返回哪種類型。規範(norm)是集合接口。若是該方法僅用於啓用for-each循環,或者返回的序列不能實現某些Collection方法(一般是contains(Object)),則使用迭代(Iterable)接口。若是返回的元素是基本類型或有嚴格的性能要求,則使用數組。在Java 8中,將流(Stream)添加到平臺中,這使得爲序列返回方法選擇適當的返回類型的任務變得很是複雜。git

你可能據說過,流如今是返回元素序列的明顯的選擇,可是正如條目 45所討論的,流不會使迭代過期:編寫好的代碼須要明智地結合流和迭代。若是一個API只返回一個流,而且一些用戶想用for-each循環遍歷返回的序列,那麼這些用戶確定會感到不安。這尤爲使人沮喪,由於Stream接口在Iterable接口中包含惟一的抽象方法,Stream的方法規範與Iterable兼容。阻止程序員使用for-each循環在流上迭代的惟一緣由是Stream沒法繼承Iterable。程序員

遺憾的是,這個問題沒有好的解決方法。 乍一看,彷佛能夠將方法引用傳遞給Stream的iterator方法。 結果代碼可能有點嘈雜和不透明,但並不是不合理:github

// 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) {

爲了使代碼編譯,必須將方法引用強制轉換爲適當參數化的Iterable類型:框架

// Hideous workaround to iterate over a stream

for  (ProcessHandle ph : (Iterable<ProcessHandle>)ProcessHandle.allProcesses()::iterator)

此代碼有效,但在實踐中使用它太嘈雜和不透明。 更好的解決方法是使用適配器方法。 JDK沒有提供這樣的方法,可是使用上面的代碼片斷中使用的相同技術,很容易編寫一個方法。 請注意,在適配器方法中不須要強制轉換,由於Java的類型推斷在此上下文中可以正常工做:ide

// Adapter from  Stream<E> to Iterable<E>

public static <E> Iterable<E> iterableOf(Stream<E> stream) {

    return stream::iterator;

}

使用此適配器,可使用for-each語句迭代任何流:性能

for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) {

    // Process the process

}

注意,條目 34中的Anagrams程序的流版本使用Files.lines方法讀取字典,而迭代版本使用了scannerFiles.lines方法優於scannerscanner在讀取文件時無聲地吞噬全部異常。理想狀況下,咱們也會在迭代版本中使用Files.lines。若是API只提供對序列的流訪問,而程序員但願使用for-each語句遍歷序列,那麼他們就要作出這種妥協。學習

相反,若是一個程序員想要使用流管道來處理一個序列,那麼一個只提供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);

}

若是你正在編寫一個返回對象序列的方法,而且它只會在流管道中使用,那麼固然能夠自由地返回流。相似地,返回僅用於迭代的序列的方法應該返回一個Iterable。可是若是你寫一個公共API,它返回一個序列,你應該爲用戶提供哪些想寫流管道,哪些想寫for-each語句,除非你有充分的理由相信大多數用戶想要使用相同的機制。

Collection接口是Iterable的子類型,而且具備stream方法,所以它提供迭代和流訪問。 所以,Collection或適當的子類型一般是公共序列返回方法的最佳返回類型。 數組還使用Arrays.asListStream.of方法提供簡單的迭代和流訪問。 若是返回的序列小到足以容易地放入內存中,那麼最好返回一個標準集合實現,例如ArrayListHashSet。 可是不要在內存中存儲大的序列,只是爲了將它做爲集合返回

若是返回的序列很大但能夠簡潔地表示,請考慮實現一個專用集合。 例如,假設返回給定集合的冪集(power set:就是原集合中全部的子集(包括全集和空集)構成的集族),該集包含其全部子集。 {a,b,c}的冪集爲{{},{a},{b},{c},{a,b},{a,c},{b,c},{a,b , C}}。 若是一個集合具備n個元素,則冪集具備2n個。 所以,你甚至不該考慮將冪集存儲在標準集合實現中。 可是,在AbstractList的幫助下,很容易爲此實現自定義集合。

訣竅是使用冪集中每一個元素的索引做爲位向量(bit vector),其中索引中的第n位指示源集合中是否存在第n個元素。 本質上,從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方法會引起異常。 這突出了使用Collection做爲返回類型而不是StreamIterable的缺點:Collection有int返回類型的size的方法,該方法將返回序列的長度限制爲Integer.MAX_VALUE或231-1。Collection規範容許size方法返回231 - 1,若是集合更大,甚至無限,但這不是一個徹底使人滿意的解決方案。

爲了在AbstractCollection上編寫Collection實現,除了Iterable所需的方法以外,只須要實現兩種方法:containssize。 一般,編寫這些方法的有效實現很容易。 若是不可行,多是由於在迭代發生以前未預先肯定序列的內容,返回Stream仍是Iterable的,不管哪一種感受更天然。 若是選擇,可使用兩種不一樣的方法分別返回。

有時,你會僅根據實現的易用性選擇返回類型。例如,假設但願編寫一個方法,該方法返回輸入列表的全部(連續的)子列表。生成這些子列表並將它們放到標準集合中只須要三行代碼,可是保存這個集合所需的內存是源列表大小的二次方。雖然這沒有指數冪集那麼糟糕,但顯然是不可接受的。實現自定義集合(就像咱們對冪集所作的那樣)會很乏味,由於JDK缺乏一個框架Iterator實現來幫助咱們。

然而,實現輸入列表的全部子列表的流是直截了當的,儘管它確實須要一點的洞察力(insight)。 讓咱們調用一個子列表,該子列表包含列表的第一個元素和列表的前綴。 例如,(a,b,c)的前綴是(a),(a,b)和(a,b,c)。 相似地,讓咱們調用包含後綴的最後一個元素的子列表,所以(a,b,c)的後綴是(a,b,c),(b,c)和(c)。 洞察力是列表的子列表只是前綴的後綴(或相同的後綴的前綴)和空列表。 這一觀察直接展示了一個清晰,合理簡潔的實現:

// 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方法(條目 45)用於生成由全部前綴的全部後綴組成的單個流。 最後,經過映射IntStream.rangeIntStream.rangeClosed返回的連續int值流來生成前綴和後綴。這個習慣用法,粗略地說,流等價於整數索引上的標準for循環。所以,咱們的子列表實現似於明顯的嵌套for循環:

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循環直接轉換爲流。結果比咱們之前的實現更簡潔,但可能可讀性稍差。它相似於條目 45中的笛卡爾積的使用流的代碼:

// Returns a stream of all the sublists of its input list

public static <E> Stream<List<E>> of(List<E> list) {

   return IntStream.range(0, list.size())

      .mapToObj(start ->

         IntStream.rangeClosed(start + 1, list.size())

            .mapToObj(end -> list.subList(start, end)))

      .flatMap(x -> x);

}

與以前的for循環同樣,此代碼不會包換空列表。 爲了解決這個問題,可使用concat方法,就像咱們在以前版本中所作的那樣,或者在rangeClosed調用中用(int) Math.signum(start)替換1。

這兩種子列表的流實現均可以,但都須要一些用戶使用流-迭代適配器( Stream-to-Iterable adapte),或者在更天然的地方使用流。流-迭代適配器不只打亂了客戶端代碼,並且在個人機器上使循環速度下降了2.3倍。一個專門構建的Collection實現(此處未顯示)要冗長,但運行速度大約是個人機器上基於流的實現的1.4倍。

總之,在編寫返回元素序列的方法時,請記住,某些用戶可能但願將它們做爲流處理,而其餘用戶可能但願迭代方式來處理它們。 儘可能適應兩個羣體。 若是返回集合是可行的,請執行此操做。 若是已經擁有集合中的元素,或者序列中的元素數量足夠小,能夠建立一個新的元素,那麼返回一個標準集合,好比ArrayList。 不然,請考慮實現自定義集合,就像咱們爲冪集程序裏所作的那樣。 若是返回集合是不可行的,則返回流或可迭代的,不管哪一個看起來更天然。 若是在未來的Java版本中,Stream接口聲明被修改成繼承Iterable,那麼應該隨意返回流,由於它們將容許流和迭代處理。

相關文章
相關標籤/搜索