01.30 java8實戰(函數式編程)筆記

函數式接口就是隻定義一個抽象方法的接口

函數式接口的抽象方法的簽名基本上就是Lambda表達式的簽名。我們將這種抽象方法叫作函數描述符。

@FunctionalInterface這個標註用於表示該接口會設計成一個函數式接口。如果你用 @FunctionalInterface定義了一個接口,而它卻不是函數式接口的話,編譯器將返回一個提示原因的錯誤。

三大函數式接口

Predicate ->boolean test(T t);

Consumer->void accept(T t);

Function->R apply(T t);

針對專門的輸入參數類型的函數式接口的名稱都要加上對應的原始類型前綴,比如DoublePredicate、 IntConsumer、 LongBinaryOperator、 IntFunction等。 Function接口還有針對輸出參數類型的變種: ToIntFunction、 IntToDoubleFunction等。

任何函數式接口都不允許拋出受檢異常( checked exception)。如果你需要Lambda表達式來拋出異常, 有兩種辦法: 定義一個自己的函數式接口,並聲明受檢異常,或者把Lambda包在一個try/catch塊中。

Lambda的類型是從使用Lambda的上下文推斷出來的。上下文(比如,接受它傳遞的方法的參數,或接受它的值的局部變量)中Lambda表達式需要的類型稱為目標類型。

java編譯器會從上下文(目標類型)推斷出用什麼函數式接口來配合Lambda表達式,這意味著它也可以推斷出適合Lambda的簽名,因為函數描述符可以通過目標類型來得到。這樣做的好處在於,編譯器可以瞭解Lambda表達式的參數類型,這樣就可以在Lambda語法中省去標註參數類型。

Lambda可以沒有限制地捕獲(也就是在其主體中引用)實例變量和靜態變量。但局部變量必須顯式聲明為final,或事實上是final。換句話說, Lambda表達式只能捕獲指派給它們的局部變量一次。

為什麼局部變量有這些限制。第一,實例變量和局部變量背後的實現有一個關鍵不同。實例變量都存儲在堆中,而局部變量則保存在棧上。如果Lambda可以直接訪問局部變量,而且Lambda是在一個線程中使用的,則使用Lambda的線程,可能會在分配該變量的線程將這個變量收回之後,去訪問該變量。因此, Java在訪問自由局部變量時,實際上是在訪問它的副本,而不是訪問原始變量。如果局部變量僅僅賦值一次那就沒有什麼區別了——因此就有了這個限制。

第二,這一限制不鼓勵使用改變外部變量的典型命令式編程模式

如何構建方法引用

方法引用主要有三類。

(1) 指向靜態方法的方法引用(例如Integer的parseInt方法, 寫作Integer::parseInt)。

(2) 指 向 任 意 類 型 實 例 方 法 的 方 法 引 用 ( 例 如 String 的 length 方 法 , 寫 作

String::length)。

(3) 指向現有對象的實例方法的方法引用(假設你有一個局部變量expensiveTransaction

用於存放Transaction類型的對象,它支持實例方法getValue,那麼你就可以寫expensiveTransaction::getValue)。

逆序:

inventory.sort(comparing(Apple::getWeight).reversed())

複合函數

andThen()

compose()

這兩個方法的執行順序剛好是相反的,例如f.compose(g)會先執行g函數,再將g函數的結果作為f函數的入參執行,而f.andThen(g)會先執行f函數,將f函數的結果作為g函數的入參,在執行g函數。

流簡介

filter——接受Lambda,從流中排除某些元素。

map——接受一個Lambda,將元素轉換成其他形式或提取信息。

limit——截斷流,使其元素不超過給定數量。

collect——將流轉換為其他形式。

流只能遍歷一次。遍歷完之後,這個流已經被消費掉

流是在概念上固定的數據結構(你不能添加或刪除元素),其元素則是按需計算的。流就

像是一個延遲創建的集合:只有在消費者要求的時候才會計算值。

流支持一個叫作distinct的方法,它會返回一個元素各異的流;流支持limit(n)方法,該方法會返回一個不超過給定長度的流;流還支持skip(n)方法,返回一個扔掉了前n個元素的流。如果流中元素不足n個,則返回一個空流;流支持

map方法,它會接受一個函數作為參數。這個函數會被應用到每個元素上,並將其映射成一個新的元素。

使用flatMap方法的效果是,各個數組並不是分別映射成一個流,而是映射成流的內容。所有使用map(Arrays::stream)時生成的單個流都被合併起來,即扁平化為一個流。flatmap方法讓你把一個流中的每個值都換成另一個流,然後把所有的流連接起來成為一個流。

給定兩個數字列表,如何返回所有的數對呢?例如,給定列表[1, 2, 3]和列表[3, 4],應

該返回[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)]。

<code>List<integer> numbers1 = Arrays.asList(1, 2, 3);
List<integer> numbers2 = Arrays.asList(3, 4);
List pairs = numbers1.stream().
flatMap(i -> numbers2.stream().map(j -> new int[]{i, j}))
.collect(toList());
/<integer>/<integer>/<code>

anyMatch方法可以回答“流中是否有一個元素能匹配給定的謂詞”。

allMatch方法的工作原理和anyMatch類似,但它會看看流中的元素是否都能匹配給定的謂詞

和allMatch相對的是noneMatch。

reduce接受兩個參數:一個初始值,一個BinaryOperator來將兩個元素結合起來產生一個新值,這裡我們用的是lambda (a, b) -> a + b。

reduce還有一個重載的變體,它不接受初始值,但是會返回一個Optional對象:

Optional<integer> sum = numbers.stream().reduce((a, b) -> (a + b));/<integer>

如果流中沒有任何元素的情況。 reduce操作無法返回其和,因為它沒有初始值。這就是為什麼結果被包裹在一個Optional對象裡,以表明結果可能不存在。

Optional裡面幾種可以迫使你顯式地檢查值是否存在或處理值不存在的情形的方法也不錯。

isPresent()將在Optional包含值的時候返回true, 否則返回false。

ifPresent(Consumer block)會在值存在的時候執行給定的代碼塊。

T get()會在值存在時返回值,否則拋出一個NoSuchElement異常。

T orElse(T other)會在值存在時返回值,否則返回一個默認值。

將流轉換為特化版本的常用方法是mapToInt、 mapToDouble和mapToLong。這些方法和前面說的map方法的工作方式一樣,只是它們返回的是一個特化流,而不是Stream

Stream<integer> stream = intStream.boxed();將數值流轉換為stream/<integer>

區分沒有元素的流和最大值真的是0的流使用OptionalInt、 OptionalDouble和OptionalLong。

IntStream.rangeClosed(1, 100)包含100

IntStream.range(1, 100)不包含100

值、數組、文件都可以生成流

斐波納契數列是著名的經典編程練習。下面這個數列就是斐波納契數列的一部分: 0, 1, 1,

2, 3, 5, 8, 13, 21, 34, 55…數列中開始的兩個數字是0和1,後續的每個數字都是前兩個數字之和。

斐波納契元組序列與此類似,是數列中數字和其後續數字組成的元組構成的序列: (0, 1),

(1, 1), (1, 2), (2, 3), (3, 5), (5, 8), (8, 13), (13, 21) …

用iterate方法生成斐波納契元組序列中的前20個元素。

<code>Stream.iterate(new int[]{0, 1}, 
t -> new int[]{t[1], t[0]+t[1]}).limit(20)/<code>

收集器

對流調用collect方法將對流中的元素觸發一個歸約操作

歸約和彙總

在需要將流項目重組成集合時,一般會使用收集器,但凡要把流中所有的項目合併成一個結果時就可以用。這個結果可以是任何類型。

long howManyDishes = menu.stream().collect(Collectors.counting());

這還可以寫得更為直接:

long howManyDishes = menu.stream().count();

獲取流中最大值:

Optional<dish> mostCalorieDish =menu.stream().collect(maxBy(dishCaloriesComparator));/<dish>

彙總:

int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));

Collectors.summingLong和Collectors.summingDouble方法的作用完全一樣,可以用於求和字段為long或double的情況。彙總不僅僅是求和;還有Collectors.averagingInt,連同對應的averagingLong和averagingDouble可以計算數值的平均數。

IntSummaryStatistics工廠方法返回的收集器。例如,通過一次summarizing操作你可以就數出菜單中元素的個數,並得到總和、平均值、最大值和最小值。

oining工廠方法返回的收集器會把對流中每一個對象應用toString方法得到的所有字符串連接成一個字符串。

String shortMenu = menu.stream().map(Dish::getName).collect(joining(","));

分組

Map<dish.type>> dishesByType =menu.stream().collect(groupingBy(Dish::getType));/<dish.type>

二級分組

menu.stream().collect(groupingBy(Dish::getType,

groupingBy(dish -> {

if (dish.getCalories() <= 400) return CaloricLevel.DIET;

else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;

else return CaloricLevel.FAT;

} )));

第二個參數不一定是groupBy()

menu.stream().collect(groupingBy(Dish::getType, counting()));

其結果是下面的Map:

{MEAT=3, FISH=2, OTHER=4}

分區是分組的特殊情況:由一個謂詞(返回一個布爾值的函數)作為分類函數,它稱分區函數。

這意味著得到的分組Map的鍵類型是Boolean,於是它最多可以分為兩組true一組, false一組

menu.stream().collect(partitioningBy(Dish::isVegetarian));

Collector接口:

<code>public interface Collector {    a mutable result container.     *     * @return a function which folds a value into a mutable result container     */    BiConsumer/<code>

1. 建立新的結果容器:supplier方法必須返回一個結果為空的Supplier,也就是一個無參數函數,在調用時它會創建一個空的累加器實例,供數據收集過程使用。例如

public Supplier<list>> supplier() {/<list>

return () -> new ArrayList();

}

2. 將元素添加到結果容器: accumulator方法會返回執行歸約操作的函數。當遍歷到流中第n個元素時,這個函數執行時會有兩個參數:保存歸約結果的累加器, 還有第n個元素本身。

該函數將返回void,因為累加器是原位更新,即函數的執行改變了它的內部狀態以體現遍歷的元素的效果。例如

public BiConsumer<list>, T> accumulator() {/<list>

return (list, item) -> list.add(item);

}

3. 對結果容器應用最終轉換: finisher方法在遍歷完流後, 必須返回在累積過程的最後要調用的一個函數,以便將累加器對象轉換為整個集合操作的最終結果。

public Function<list>, List> finisher() {/<list>

return Function.identity();

}

4. 合併兩個結果容器: combiner方法會返回一個供歸約操作使用的函數,它定義了對流的各個子部分進行並行處理時,各個子部分歸約所得的累加器要如何合併。對於toList而言,這個方法的實現非常簡單,只要把從流的第二個部分收集到的項目列表加到遍歷第一部分時得到的列表後面就行了:

public BinaryOperator<list>> combiner() {/<list>

return (list1, list2) -> {

list1.addAll(list2);

return list1; }

}

5. characteristics方法會返回一個不可變的Characteristics集合,它定義了收集器的行為,尤其是關於流是否可以並行歸約,以及可以使用哪些優化的提示。

Characteristics是一個包含三個項目的枚舉。

 UNORDERED——歸約結果不受流中項目的遍歷和累積順序的影響。

 CONCURRENT——accumulator函數可以從多個線程同時調用,且該收集器可以並行歸約流。如果收集器沒有標為UNORDERED,那它僅在用於無序數據源時才可以並行歸約。

 IDENTITY_FINISH——這表明完成器方法返回的函數是一個恆等函數,可以跳過。這種情況下,累加器對象將會直接用作歸約過程的最終結果。這也意味著,將累加器A不加檢查地轉換為結果R是安全的

並行流就是一個把內容分成多個數據塊,並用不同的線程分別處理每個數據塊的流。這樣一來,你就可以自動把給定操作的工作負荷分配給多核處理器的所有內核。

Stream.iterate(1L, i -> i + 1).limit(n).parallel().reduce(0L, Long::sum);

並行流內部使用了默認的ForkJoinPool,它默認的線 程 數 量 就 是 你 的 處 理 器 數 量 , 這 個 值 是 由 Runtime.getRuntime().availableProcessors()得到的。但 是 你 可 以 通 過 系 統 屬 性 java.util.concurrent.ForkJoinPool.common.parallelism來改變線程池大小,如下所示:

System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");

這是一個全局設置,因此它將影響代碼中所有的並行流。反過來說,目前還無法專為某個並行流指定這個值。一般而言,讓ForkJoinPool的大小等於處理器數量是個不錯的默認值,除非你有很好的理由,否則建議不要修改它。

並行化並不是沒有代價的。並行化過程本身需要對流做遞歸劃分,把每個子流的歸納操作分配到不同的線程,然後把這些操作的結果合併成一個值。但在多個內核之間移動數據的代價也可能比你想的要大,所以很重要的一點是要保證在內核中並行執行工作的時間比在內核之間傳輸數據的時間長。

要避免共享可變狀態,確保並行Stream得到正確的結果。

留意裝箱。自動裝箱和拆箱操作會大大降低性能。 Java 8中有原始類型流IntStream、LongStream、 DoubleStream來避免這種操作,但凡有可能都應該用這些流。

分支/合併框架的目的是以遞歸方式將可以並行的任務拆分成更小的任務,然後將每個子任務的結果合併起來生成整體結果。它是ExecutorService接口的一個實現,它把子任務分配給線程池(稱為ForkJoinPool)中的工作線程。

對一個任務調用join方法會阻塞調用方,直到該任務做出結果。因此,有必要在兩個子任務的計算都開始之後再調用它。否則,你得到的版本會比原始的順序算法更慢更復雜,因為每個子任務都必須等待另一個子任務完成才能啟動。

 不應該在RecursiveTask內部使用ForkJoinPool的invoke方法。相反,你應該始終直接調用compute或fork方法,只有順序代碼才應該用invoke來啟動並行計算。

 對子任務調用fork方法可以把它排進ForkJoinPool。同時對左邊和右邊的子任務調用它似乎很自然,但這樣做的效率要比直接對其中一個調用compute低。這樣做你可以為其中一個子任務重用同一線程,從而避免在線程池中多分配一個任務造成的開銷。

 和並行流一樣,你不應理所當然地認為在多核處理器上使用分支/合併框架就比順序計算快。一個任務可以分解成多個獨立的子任務,才能讓性能在並行化時有所提升。所有這些子任務的運行時間都應該比分出新任務所花的時間長;一個慣用方法是把輸入/輸出放在一個子任務裡,計算放在另一個裡,這樣計算就可以和輸入/輸出同時進行。此外,在比較同一算法的順序和並行版本的性能時還有別的因素要考慮。就像任何其他Java代碼一樣,分支/合併框架需要“預熱”或者說要執行幾遍才會被JIT編譯器優化。這就是為什麼在測量性能之前跑幾遍程序很重要。

分支/合併框架工程用一種稱為工作竊取( work stealing)的技術來解決任務分配不均的問題。在實際應用中,這意味著這些任務差不多被平均分配到ForkJoinPool中的所有線程上。每個線程都為分配給它的任務保存一個雙向鏈式隊列,每完成一個任務,就會從隊列頭上取出下一個任務開始執行。基於前面所述的原因,某個線程可能早早完成了分配給它的所有任務,也就是它的隊列已經空了,而其他的線程還很忙。這時,這個線程並沒有閒下來,而是隨機選了一個別的線程,從隊尾“偷走”一個任務。這個過程一直繼續下去,直到所有的任務都執行完畢,所有的隊列都清空。這就是為什麼要劃成許多小任務而不是少數幾個大任務,這有助於更好地在工作線程之間平衡負載。

在Lambda中,this關鍵字代表的是包含類

基礎類型的Optional對象,以及為什麼應該避免使用它們:Optional 也 提 供 了類 似的 基 礎類

型——OptionalInt、 OptionalLong以及OptionalDouble。因為基礎類型的Optional不支持map、flatMap以及filter方法,而這些卻是Optional類最有用的方法。


如果你進行的是計算密集型的操作,並且沒有I/O,那麼推薦使用Stream接口,因為實現簡單,同時效率也可能是最高的(如果所有的線程都是計算密集型的,那就沒有必要創建比處理器核數更多的線程)。

❑ 反之,如果並行的工作單元還涉及等待I/O的操作(包括網絡連接等待),那麼使用CompletableFuture靈活性更好,依據等待/計算,設定需要使用的線程數。這種情況不使用並行流的另一個原因是,處理流的流水線中如果發生I/O等待,流的延遲特性會讓我們很難判斷到底什麼時候觸發了等待。


java8實戰(函數式編程)筆記


分享到:


相關文章: