初識Java8新特性Lambda(二) 之collections

背景(Background)

若是從一開始就將lambda表達式(閉包)做爲Java語言的一部分,那麼咱們的Collections API確定會與今天的外觀有所不一樣。隨着Java語言得到做爲JSR 335一部分的lambda表達式,這具備使咱們的Collections接口看起來更加過期的反作用。儘管可能很想從頭開始並構建替換的Collection框架(「 Collections II」),可是替換Collection框架將是一項主要任務,由於Collections接口遍及JDK庫。相反,咱們將繼續增長擴展方法,現有的接口(如進化策略Collection,List或Iterable),或者以新的接口(如「流」)被改裝到現有的類,使許多但願的成語,而沒有讓人們買賣其值得信賴的ArrayListS和HashMap秒。(這並非說Java永遠不會有一個新的Collections框架;很明顯,現有的Collections框架存在侷限性,而不只僅是爲lambda設計的。除此之外,建立一個通過改進的Collections框架是一個很好的考慮因素。 JDK的將來版本。)html

並行性是這項工做的重要推進力。所以,要鼓勵那些成語是很是重要的兩個 sequential-和並行友好。咱們主要經過較少地關注就地突變而更多地關注產生新值的計算來實現這一目標。在使並行變得容易但不能使其變得不可見之間取得平衡也是重要的; 咱們的目標是 爲新舊系列提供明確但不干擾的並行性。java

內部與外部迭代(Internal vs external iteration)

Collections框架依賴於外部迭代的概念,其中a Collection 提供了一種方法來爲其客戶端枚舉其元素(Collectionextends Iterable),而且客戶端使用它來順序地遍歷一個集合的元素。例如,若是咱們想將一組塊中的每一個塊的顏色設置爲紅色,則能夠這樣寫:算法

for (Block b : blocks) {
		b.setColor(RED);
	}

這個例子說明了外部迭代。for-each循環調用的iterator() 方法blocks,而後一步一步地遍歷集合。外部迭代很是簡單,可是存在幾個問題:編程

  • 它本質上是串行的(除非該語言提供了用於並行迭代的結構,而Java沒有提供),而且須要按集合指定的順序處理元素。
  • 它使庫方法失去了管理控制流的機會,該方法可能可以利用數據的從新排序,並行性,短路或惰性來提升性能。

有時須要嚴格指定for-each loop (sequential, in-order) ,但這有時會妨礙性能。數組

外部迭代的替代方法是內部迭代,它不是控制迭代,而是將其委託給庫,並傳遞代碼片斷以在計算的各個點執行。緩存

上一個示例的內部迭代等效項是:數據結構

blocks.forEach(b -> { b.setColor(RED); });

這種方法將控制流管理從客戶端代碼轉移到庫代碼,從而使庫不只能夠對通用控制流操做進行抽象,並且還可使它們潛在地使用延遲,並行和無序執行來提升性能。 。(是否實現forEach這些操做其實是由庫實現forEach者決定的,可是對於內部迭代,至少是可能的,而對於外部迭代,則不是。)閉包

內部迭代使其具備一種編程樣式,其中能夠將操做「管道化」在一塊兒。例如,若是咱們只想將藍色塊塗成紅色,咱們能夠說:app

blocks.filter(b -> b.getColor() == BLUE)
		.forEach(b -> { b.setColor(RED); });

該濾波器操做產生匹配所提供的條件值的流,而且所述濾波操做的結果被管道輸送到forEach。框架

若是咱們想將藍色塊收集到一個新的中List,咱們能夠說:

List<Block> blue = blocks.filter(b -> b.getColor() == BLUE)
                         .into(new ArrayList<>());

若是每一個方框都包含在一個Box中,而且咱們想知道哪些方框至少包含一個藍色方框,咱們能夠說:

Set<Box> hasBlueBlock = blocks.filter(b -> b.getColor() == BLUE)
                              .map(b -> b.getContainingBox())
                              .into(new HashSet<>());

若是咱們但願將藍色塊的總重量相加,則能夠表示爲:

int sum = blocks.filter(b -> b.getColor() == BLUE)
                .map(b -> b.getWeight())
                .sum();

到目前爲止,咱們尚未寫下這些操做的簽名-這些將在後面顯示。此處的示例僅說明了內部迭代能夠輕鬆解決的問題類型,並說明了咱們但願在集合中公開的功能。

惰性做用(The role of laziness)

像過濾或映射這樣的操做,能夠「急切地」執行(過濾在從過濾方法返回時完成),也能夠「懶惰地」執行(過濾只在開始迭代過濾方法結果的元素時完成)。將本身應用於懶惰的實現,這一般會致使顯著的性能改進。咱們能夠將這些操做視爲「天然懶惰」,無論它們是否被實現。另外一方面,像積累這樣的操做,或者產生反作用,好比把結果傾注到一個集合中或者爲每個元素作一些事情(好比打印出來),都是「天然渴望的」。

基於對許多現有循環的檢查,能夠從數據源(數組或集合)中繪製大量操做,進行一系列的惰性操做(過濾、映射等),而後進行一個急切操做(如過濾器映射累加),重述(一般在過程當中明顯變小)。所以,大多數天然懶惰操做傾向於用來計算臨時中間結果,而且咱們能夠利用這個屬性來生成更高效的庫。(例如,一個延遲地進行過濾或映射的庫能夠將filter map accumulate之類的管道融合到數據的一個傳遞中,而不是三個不一樣的傳遞中;一個急切地進行過濾或映射的庫不能。相似地,若是咱們在尋找與某一特性匹配的第一個元素,那麼一個懶惰的方法讓咱們獲得了檢查較少元素的答案。

這個觀察結果提供了一個關鍵的設計選擇:filter和map的返回值應該是什麼?其中一個候選者是list.filter返回一個新的list,這將推進咱們朝着一個盡心盡力的方向前進。這是直截了當的,但最終可能作得比咱們真正須要的更多。另外一種方法是爲顯式懶惰建立一個全新的抽象集——LaZyLIST、LaZySeT等(但請注意,懶惰的集合仍然具備觸發急切計算的操做——例如大小)。而且,這種方法有可能演變成像MutableSynchronizedLazySortedSet等類型的組合爆炸。

咱們首選的方法是將天然懶惰操做看成返回一個流(例如迭代)而不是一個新的集合(不管如何它可能被下一個流水線階段丟棄)。將此應用於上面的示例,過濾器從源(多是另外一個流)中提取並生成與所提供的謂詞匹配的值流。在大多數潛在的懶惰操做被應用到聚合的狀況下,這剛好是咱們想要的——一個能夠傳遞到流水線中的下一個階段的值流。目前,迭代是流的抽象,但這是一個明確的臨時選擇,咱們將很快從新訪問,可能建立一個流抽象,它沒有迭代問題(固有檢查而後行爲;假設底層源的可變異性;生活在Java.Lang.)中。

流方法的優勢是,當用於源代碼惰性惰性渴望管道時,惰性一般是看不見的,由於管道兩端都是「密封的」,可是在不顯著增長庫的概念表面積的狀況下,能夠得到良好的可用性和性能。

流(Streams)

下面顯示了一組stream操做。這些方法本質上是順序的,在上游迭代器返回的順序中處理元素(遇到順序)。在當前的實現中,咱們使用Iterable做爲這些方法的宿主。返回Iterable的方法是懶惰的;那些不急於返回的方法是懶惰的。全部這些操做均可以經過默認的方法單獨實現Iterator(),所以現有Collection實現不須要額外的工做來獲取新的功能。還請注意,Stream功能僅與集合成切線關係;若是備用收集框架想要獲取這些方法,則它們所須要作的只是實現 Iterable。

流(Stream)在幾個方面與集合(Collections )不一樣:

  • 沒有存儲空間。 流沒有存儲值。它們經過一系列操做從數據結構中攜帶值。
  • 本質上是功能性的。 對流的操做會產生結果,但不會修改其基礎數據源。能夠將Collection用做流的源(取決於適當的無干擾要求,請參見下文)。
  • 懶惰尋求。 許多流操做(例如過濾,映射,排序或重複刪除)均可以延遲實施,這意味着咱們只須要檢查流中要查找所需答案的元素數量便可。例如,「查找第一個大於20個字符的字符串」不須要檢查全部輸入字符串。
  • 邊界可選。 有不少問題能夠明智地表達爲無限流,讓客戶消費價值直到滿意爲止。(若是咱們要枚舉完美的數字,則能夠很容易地將其表示爲對全部整數流進行過濾的操做。)集合不容許您這樣作,可是流能夠這樣作。

下面顯示了一組基本的流操做,表示爲上的擴展方法Iterable。

public interface Iterable<T> {
    // Abstract methods
    Iterator<T> iterator();

    // Lazy operations
    Iterable<T> filter(Predicate<? super T> predicate) default ...

    <U> Iterable<U> map(Mapper<? super T, ? extends U> mapper) default ...

    <U> Iterable<U> flatMap(Mapper<? super T, ? extends Iterable<U>> mapper) default ...

    Iterable<T> cumulate(BinaryOperator<T> op) default ...

    Iterable<T> sorted(Comparator<? super T> comparator) default ...

    <U extends Comparable<? super U>> Iterable<T> sortedBy(Mapper<? super T, U> extractor) default ...

    Iterable<T> uniqueElements() default ...

    <U> Iterable<U> pipeline(Mapper<Iterable<T>, ? extends Iterable<U>> mapper) default ...

    <U> BiStream<T, U> mapped(Mapper<? super T, ? extends U> mapper) default ...
    <U> BiStream<U, Iterable<T>> groupBy(Mapper<? super T, ? extends U> mapper) default ...
    <U> BiStream<U, Iterable<T>> groupByMulti(Mapper<? super T, ? extends Iterable<U>> mapper) default ...

    // Eager operations

    boolean isEmpty() default ...;
    long count() default ...

    T getFirst() default ...
    T getOnly() default ...
    T getAny() default ...

    void forEach(Block<? super T> block) default ...

    T reduce(T base, BinaryOperator<T> reducer) default ...

    <A extends Fillable<? super T>> A into(A target) default ...

    boolean anyMatch(Predicate<? super T> filter) default ...
    boolean noneMatch(Predicate<? super T> filter) default ...
    boolean allMatch(Predicate<? super T> filter) default ...

    <U extends Comparable<? super U>> T maxBy(Mapper<? super T, U> extractor) default ...
    <U extends Comparable<? super U>> T minBy(Mapper<? super T, U> extractor) default ...
}

懶惰和短路(Laziness and short-circuiting)

相似anyMatch的方法,雖然是急性的,但一旦能夠肯定最終結果,即可以使用short-circuiting來中止處理-它只須要對足夠多的元素進行謂詞評估就能夠找到該謂詞爲真的單個元素。

在像這樣的傳輸中:

int sum = blocks.filter(b -> b.getColor() == BLUE)
                .map(b -> b.getWeight())
                .sum();

在filter和map操做是惰性的。這意味着在sum步驟開始以前,咱們不會從源頭開始繪製元素,從而最大程度地減小了管理中間元素所需的簿記成本。

另外,給定一個相似的傳輸方式:

Block firstBlue = blocks.filter(b -> b.getColor() == BLUE)
                        .getFirst();

因爲篩選器步驟是惰性的,所以該getFirst步驟將僅在上游進行,Iterator直到得到一個元素爲止,這意味着咱們只須要對元素上的謂詞求值,直到找到該謂詞爲真的元素爲止,而不是全部元素都爲真。

請注意,用戶沒必要詢問懶惰,甚至沒必要考慮太多。正確的事情發生了,庫安排了儘量少的計算。

用戶能夠按如下方式調用:

Iterable<Block> it = blocks.filter(b -> b.getColor() == BLUE);

並從中得到一個Iterator,儘管咱們嘗試將功能集設計爲不須要這種用法。在這種狀況下,此操做只會建立一個Iterable,但除了保留對上游Iterable(blocks)及其Predicate過濾對象的引用以外,不會作任何其餘工做。Iterator 從this得到an之後,全部工做都將完成Iterable。

通用功能接口(Common functional interfaces)

Java中的Lambda表達式將轉換爲一種方法接口(功能性接口)的實例。該軟件包java.util.functions包含功能接口的「入門套件」:

  • Predicate -- 做爲參數傳遞的對象的屬性
  • Block -- 將對象做爲參數傳遞時要執行的操做
  • Mapper -- 將T轉換爲U
  • UnaryOperator -- 來自T-> T的一元運算符
  • BinaryOperator -- 來自(T,T)-> T的二進制運算符

出於性能緣由,可能須要提供這些核心接口的專門的原始版本。(在這種狀況下,可能不須要完整的原始特徵的補充;若是咱們提供Integer、Long和Double,則能夠經過轉換來容納其餘原始類型。)。

不干擾假設(Non-interference assumptions)

由於Iterable能夠描述一個可變的集合,因此若是在遍歷集合時修改它,就有可能產生干擾。Iterable上的新操做將在操做期間保持基礎源不變的狀況下使用。(這種狀況通常容易維持;若是集合僅限於當前線程,只需確保傳遞給filter、map等的lambda表達式不會改變底層集合。這個條件與當前迭代集合的限制沒有本質的不一樣;若是一個集合在迭代時被修改,大多數實現都會拋出ConcurrentModificationException。)在上面的示例中,咱們經過過濾一個集合來建立一個Iterable,遍歷過濾後的Iterable時遇到的元素是基於底層集合的迭代器返回的元素。所以,對iterator()的重複調用將致使對上游迭代的重複遍歷;這裏沒有緩存延遲計算的結果。(由於大多數管道看起來都是源代碼-延遲-延遲-等待,因此大多數時候底層集合只會被遍歷一次。)

實例(Examples)

下面是JDK類class (getEnclosingMethod方法)的一個例子,它遍歷全部聲明的方法、匹配的方法名、返回類型以及參數的數量和類型。原始代碼以下:

for (Method m : enclosingInfo.getEnclosingClass().getDeclaredMethods()) {
     if (m.getName().equals(enclosingInfo.getName()) ) {
         Class<?>[] candidateParamClasses = m.getParameterTypes();
         if (candidateParamClasses.length == parameterClasses.length) {
             boolean matches = true;
             for(int i = 0; i < candidateParamClasses.length; i++) {
                 if (!candidateParamClasses[i].equals(parameterClasses[i])) {
                     matches = false;
                     break;
                 }
             }

             if (matches) { // finally, check return type
                 if (m.getReturnType().equals(returnType) )
                     return m;
             }
         }
     }
 }

 throw new InternalError("Enclosing method not found");

使用filter和getFirst,咱們能夠消除全部臨時變量,並將控制邏輯移到庫中。咱們從反射中獲取方法列表,將其轉換爲一個可迭代的數組。asList(咱們也能夠向數組類型中注入相似流的接口),而後使用一系列過濾器來拒毫不匹配名稱、參數類型或返回類型的過濾器:

Method matching =
     Arrays.asList(enclosingInfo.getEnclosingClass().getDeclaredMethods())
        .filter(m -> Objects.equals(m.getName(), enclosingInfo.getName())
        .filter(m ->  Arrays.equals(m.getParameterTypes(), parameterClasses))
        .filter(m -> Objects.equals(m.getReturnType(), returnType))
        .getFirst();
if (matching == null)
    throw new InternalError("Enclosing method not found");
return matching;

這個版本的代碼更緊湊,更不易出錯。

流操做對於集合上的特別查詢很是有效。考慮一個假設的「音樂庫」應用程序,其中一個庫有一個專輯列表,一個專輯有一個標題和一個曲目列表,一個曲目有一個名稱、歌手和評級。

考慮這樣的查詢「爲我找到至少有一首排名在4或4以上的專輯的名字,按名字排序。」爲了構造這個集合,咱們能夠這樣寫:

List<Album> favs = new ArrayList<>();
	for (Album a : albums) {
		boolean hasFavorite = false;
		for (Track t : a.tracks) {
			if (t.rating >= 4) {
				hasFavorite = true;
				break;
			}
		}
		if (hasFavorite)
			favs.add(a);
	}
	Collections.sort(favs, new Comparator<Album>() {
                           public int compare(Album a1, Album a2) {
                               return a1.name.compareTo(a2.name);
                           }});

咱們可使用流操做來簡化三個主要步驟中的每個——肯定專輯中的任何曲目是否至少在(anyMatch)上有一個評級,排序,以及將符合咱們標準的專輯集合放入一個列表:

List<Album> sortedFavs =
  	albums.filter(a -> a.tracks.anyMatch(t -> (t.rating >= 4)))
        .sortedBy(a -> a.name)
        .into(new ArrayList<>());

非線性流(Nonlinear streams)

如上所述,「obvious」的流形狀是簡單的線性值流,例如能夠由數組或Collection管理的值。咱們可能還想表示其餘常見的形狀,例如(鍵,值)對的流(可能限制了鍵的惟一性。)

將雙值流表示爲值流可能會很方便Pair<X,Y>。這將很容易,並容許咱們重用現有的流機制,但會產生一個新問題:若是咱們可能想對鍵值流執行新操做(例如將其拆分爲a keys或 values流),則擦除操做會進入方式-咱們沒法表達僅在類的類型變量知足某些約束(例如a)的狀況下存在的方法Pair。(這是C#靜態擴展方法的一個優勢,它被注入實例化的泛型類型而不是類中,這是毫無價值的。)此外,將雙值流建模爲的流。Pair對象可能會有大量的「裝箱」開銷。一般,每一個不一樣的流「形狀」可能都須要其本身的流抽象,但這並非不合理的,由於每一個不一樣的形狀將具備在該形狀上有意義的本身的一組操做。

所以,咱們使用一個單獨的抽象爲雙值流建模,咱們將其暫時稱爲BiStream。所以咱們的流庫具備兩個基本的流形狀:linear (Iterable) 和map-shaped (BiStream),就像Collections框架具備兩個基本形狀(Collection and Map)同樣。

雙值流能夠對「 zip」運算的結果,地圖的內容或分組運算的結果(其中結果爲BiStream<U, Stream<V>>)進行建模。例如,考慮構造的直方圖的問題。文檔中單詞的長度。若是咱們將文檔建模爲單詞流,則能夠對流進行「分組」操做(按長度分組),而後對與給定鍵關聯的值進行「reduce」(sum)操做以得到從單詞長度映射到具備該長度的單詞數:

Map<Integer, Integer>
    counts = document.words()                             // stream of strings
                     .groupBy(s -> s.length())            // bi-stream length -> stream of words with that length
                     .mapValues(stream -> stream.count()) // bi-stream length -> count of words
                     .into(new HashMap<>());              // Map length -> count

並行性(Parallelism)

雖然內部迭代的使用使得操做能夠並行完成,可是咱們不但願給用戶帶來任何「透明的並行性」。相反,用戶應該可以以一種顯式但不顯眼的方式選擇並行性。咱們經過容許客戶顯式地請求集合的「並行視圖」來實現這一點,集合的操做是並行執行的;這是經過parallel()方法在集合上公開的。若是咱們想要並行計算咱們的「藍色塊的權重和」查詢,咱們只須要添加一個調用parallel():

int sum = blocks.parallel()
                .filter(b -> b.getColor() == BLUE)
                .map(b -> b.getWeight())
                .sum();

這看起來與串行版本很是類似,可是被明確地標識爲並行的,而沒有並行機制壓倒代碼。

有了Java SE 7中添加的Fork/Join框架,咱們就有了實現並行操做的高效機制。然而,這項工做的目標之一是減小相同計算的串行和並行版本之間的差距,目前使用Fork/Join並行化計算與串行代碼看起來很是不一樣(並且比串行代碼大得多)——這是並行化的障礙。經過公開流操做的並行版本,並容許用戶顯式地在串行和並行執行之間進行選擇,咱們能夠極大地縮小這一差距。

使用Fork/Join實現並行計算所涉及的步驟是:將問題劃分爲子問題,按順序解決子問題,並組合子問題的結果。Fork/Join機制被設計成自動化這個過程。

咱們對Fork/Join的結構需求進行了建模,並使用了一個稱爲Splittable的分割抽象,它描述了能夠進一步分割成更小塊的子聚合,或者其元素能夠按順序迭代的子聚合。

public interface Splittable<T, S extends Splittable<T, S>> {
    /** Return an {@link Iterator}  for the elements of this split.   In general, this method is only called
     * at the leaves of a decomposition tree, though it can be called at any level.  */
    Iterator<T> iterator();

    /** Decompose this split into two splits, and return the left split.  If further splitting is impossible,
     * {@code left} may return a {@code Splittable} representing the entire split, or an empty split.
     */
    S left();

    /** Decompose this split into two splits, and return the right split.  If further splitting is impossible,
     * {@code right} may return a {@code Splittable} representing the entire split, or an empty split.
     */
    S right();

    /**
     * Produce an {@link Iterable} representing the contents of this {@code Splittable}.  In general, this method is
     * only called at the top of a decomposition tree, indicating that operations that produced the {@code Spliterable}
     * can happen in parallel, but the results are assembled for sequential traversal.  This is designed to support
     * patterns like:
     *     collection.filter(t -> t.matches(k))
     *               .map(t -> t.getLabel())
     *               .sorted()
     *               .sequential()
     *               .forEach(e -> println(e));
     * where the filter / map / sort operations can occur in parallel, and then the results can be traversed
     * sequentially in a predicatable order.
     */
    Iterable<T> sequential();
}

爲常見的數據結構(如基於數組的列表、二叉樹和映射)實現Splittable很是簡單。

咱們使用Iterable來描述順序集合,這意味着一個集合知道如何按順序分配它的成員。Iterable的並行模擬體現了可拆分的行爲,以及相似於Iterable上的聚合操做。咱們目前將其稱爲ParallelIterable。

public interface ParallelIterable<T> extends Splittable<T, ParallelIterable<T>> {
    // Lazy operations
    ParallelIterable<T> filter(Predicate<? super T> predicate) default ...

    <U> ParallelIterable<U> map(Mapper<? super T, ? extends U> mapper) default ...

    <U> ParallelIterable<U> flatMap(Mapper<? super T, ? extends Iterable<U>> mapper) default ...

    ParallelIterable<T> cumulate(BinaryOperator<T> op) default ...

    ParallelIterable<T> sorted(Comparator<? super T> comparator) default ...

    <U extends Comparable<? super U>> ParallelIterable<T> sortedBy(Mapper<? super T, U> extractor) default ...

    ParallelIterable<T> uniqueElements() default ...

    // Eager operations

    boolean isEmpty() default ...;
    long count() default ...

    T getFirst() default ...
    T getOnly() default ...
    T getAny() default ...

    void forEach(Block<? super T> block) default ...

    T reduce(T base, BinaryOperator<T> reducer) default ...

    <A extends ParallelFillable<? super T>> A into(A target) default ...
    <A extends Fillable<? super T>> A into(A target) default ...

    boolean anyMatch(Predicate<? super T> filter) default ...
    boolean noneMatch(Predicate<? super T> filter) default ...
    boolean allMatch(Predicate<? super T> filter) default ...

    <U extends Comparable<? super U>> T maxBy(Mapper<? super T, U> extractor) default ...
    <U extends Comparable<? super U>> T minBy(Mapper<? super T, U> extractor) default ...
}

您將注意到ParallelIterable上的操做集與Iterable上的操做很是類似,只是延遲操做返回的是ParallelIterable而不是Iterable。這意味着順序集合上的操做管道也將以相同的方式(僅以並行方式)在並行集合上工做。

須要的最後一步是從(順序的)集合中得到ParallelIterable的方法;這是新的parallel()方法在集合上返回的結果。

interface Collection<T> {
		....
		ParallelIterable<T> parallel();
	}

咱們在這裏實現的是將遞歸分解的結構特性與可在可分解數據結構上並行執行的算法分離開來。數據結構的做者只須要實現Splittable方法,而後就能夠當即訪問filter、map和friends的並行實現。相似地,向ParallelIterable添加新方法能夠當即在任何知道如何分割自身的數據結構上使用。

變異運算(Mutative operations)

對集合進行批量操做的許多用例會產生新的值、集合或反作用。然而,有時咱們確實但願對集合進行就地修改。咱們打算在採集上添加的主要原位突變有:

  • 刪除與謂詞(Collection)匹配的全部元素
  • 用新元素(List)替換與謂詞匹配的全部元素
  • 對列表進行排序(List)

這些將做爲擴展方法添加到適當的接口上。

官方原文 State of the Lambda: Libraries Edition

相關文章
相關標籤/搜索