1. 程式人生 > >Java 8 之實戰篇

Java 8 之實戰篇

1. Function

JDK 1.8 API包含了很多內建的函式式介面,比如在老Java中常用到的Comparator或者Runnable介面,這些介面都增加了@FunctionalInterface註解以便能用在lambda上。Java8中java.util.function包下的Function, Supplier, Consumer, Predicate和其他函式式介面廣泛用在支援Lambda表示式的API中。

1.1 Function<T , R>

該介面用來根據一個型別的資料得到另一個型別的資料,前者稱為前置條件,後者稱為後置條件,有進有出。
Function

類包含四種方法,其中一個抽象方法apply(),兩個default方法andThen()compose(),以及一個靜態方法identity()
例項化Function的時候需要實現其中的apply()方法,apply方法接收一個模板型別T作為輸入引數, 返回模板型別R作為輸出引數。
測試用例:

public class FunctionDemo {
    public static void main(String[] args){
        // 整數字符串
        String str = "123";
        // 將字串轉換為整型資料 123
        // 使用匿名內部類建立Function介面的實現類物件
        Function<String,Integer> f = new Function<String,Integer>(){
            @Override
            public Integer apply(String s) {
                return Integer.parseInt(s);
            }
        };
        // 呼叫apply方法進行轉換
        int num01 = f.apply(str);
        System.out.println(num01);
        
        // 使用lambda表示式簡化
        Function<String,Integer> ff = s ‐> Integer.parseInt(s);
        int num02 = ff.apply(str);
        System.out.println(num02);
        
        // 使用方法引用簡化lambda表示式
        Function<String,Integer> fff = Integer::parseInt;
        int num03 = fff.apply(str);
        System.out.println(num03);
	}
}

andThen方法接收一個Function類的例項,通過andThen可以將任意多個Functionapply方法呼叫連線起來。
測試用例:

Function<String, String> function1 = string -> {
    System.out.println("function1輸出了: " + string);
    return string;
};
Function<String, String> function2 = string -> {
    System.out.println("function2輸出了: " + string);
    return string;
};
function1.andThen(function2).apply("hello world");

輸出結果

function1輸出了: hello world
function2輸出了: hello world

可以看到呼叫順序是先呼叫funtion1然後呼叫function2。

compose方法和andThen方法一樣,接收一個另一個Function作為引數,但是順序與andThen恰恰相反。
接下來是測試用例,保持前面的不變,只把最後一句由andThen改成
function1.compose(function2).apply("hello world");
輸出結果:

function2輸出了: hello world
function1輸出了: hello world

可以看到這次先執行了function2之後再執行function1

identity方法是一個靜態方法,作用是返回一個Function物件,返回的物件總是返回它被傳入的值。

1.2 Consumer

Consumer類包含兩個方法,一個accept方法用來對輸入的引數進行自定義操作,因為是個抽象方法,所以需要例項化物件的時候進行Override,另一個andThen方法跟Function的方法一樣是一個default方法,已經有內部實現所以不需要使用者重寫,並且具體功能也跟Function差不多。Consumer的中文意思是消費者,意即通過傳遞進一個引數來對引數進行操作。
示例程式碼:

import java.util.function.Consumer;

public class Test {
    public static void main(String[] args) {
        Foo f = new Foo();
        f.foo(new Consumer<Integer>() {
                @Override
                public void accept(Integer integer) {
                    System.out.println(integer);
            }
        });
    }
}

class Foo {
    private int[] data = new int[10];

    public Foo() {
        for(int i = 0; i < 10; i++) {
            data[i] = i;
        }
    }

    public void foo(Consumer<Integer> consumer) {
        for(int i : data)
            consumer.accept(i);
    }
}

在上面的程式碼中,由於Java8引入的LambdaLambda表示式,所以其中的

        f.foo(new Consumer<Integer>() {
                @Override
                public void accept(Integer integer) {
                    System.out.println(integer);
            }
        });

可以簡寫成
f.foo(integer -> System.out.println(integer));
或者進一步簡寫成
f.foo(System.out::println);

1.3 Predicate

Predicate類包含5個方法,最重要的是test方法,這是一個抽象方法,需要程式設計者自己去Override,其他的三個default方法裡都使用到了這個方法,這三個方法分別是and方法,negate方法和or方法,其中andor方法與前面兩個類的andThen方法類似,這兩個方法都接受另一個Predicate物件作為引數,and方法返回這兩個物件分別呼叫test方法之後得到的布林值的並,相當於predicate1.test() && predicate2.test()or方法返回這兩個物件分別呼叫test方法之後得到的布林值的或,相當於predicate1.test() || predicate2.test()

示例程式碼:

Predicate<Integer> predicate1 = new Predicate<Integer>() {
    @Override
    public boolean test(Integer integer) {
        return integer <= 0;
    }
};
Predicate<Integer> predicate2 = new Predicate<Integer>() {
    @Override
    public boolean test(Integer integer) {
        return integer > 0;
    }
};
System.out.println("and: " + predicate1.and(predicate2).test(1));
System.out.println("or: " + predicate1.or(predicate2).test(1));
System.out.println("negate: " + predicate1.negate().test(1));

輸出結果:

and: false
or: true
negate: true

同樣,可以簡化成Lambda表示式

Predicate<Integer> predicate1 = integer -> integer <= 0;
Predicate<Integer> predicate2 = integer -> integer > 0;

1.4 Supplier

Supplier的中文意思是提供者,跟Consumer類相反,Supplier類用於提供物件,它只有一個get方法,是一個抽象方法,需要程式設計者自定義想要返回的物件。
示例程式碼:

Supplier<Integer> supplier = new Supplier<Integer>() {
    @Override
    public Integer get() {
        return new Random().nextInt(100);
    }
};

int[] ints = new int[10];
for(int i = 0; i < 10; i++) {
    ints[i] = supplier.get();
}
Arrays.stream(ints).forEach(System.out::println);

首先自定義了一個Supplier物件,對於其get方法,每次都返回一個100以內的隨機數,並在之後利用這個物件給一個長度為10的int陣列賦值並輸出。

util裡的function包裡並不僅僅只有這四個類,只是其中絕大部分都是由這四種衍生而來的,這個包主要是用於實現Java8最大的特性——函數語言程式設計,所以在很多其他的包的一些類中的很多地方都用到了這個function包裡的類,更多的情況是作為方法中的一個匿名物件引數來使用,配合簡潔的Lambda表示式使得程式可讀性變得更好。

2. Stream

2.1 內部迭代和外部迭代

Java 8之前的集合類庫主要依賴於外部迭代(external iteration)。 Collection實現 Iterable介面,從而使得使用者可以依次遍歷集合的元素。比如我們需要把一個集合中的形狀都設定成紅色,那麼可以這麼寫:

for (Shape shape : shapes) {
	shape.setColor(RED);
}

這個例子演示了外部迭代:for-each迴圈呼叫 shapes的 iterator()方法進行依次遍歷。外部迴圈的程式碼非常直接,但它有如下問題:

  • java的for迴圈是序列的,而且必須按照集合中元素的順序進行依次處理;
  • 集合框架無法對控制流進行優化,例如通過排序、並行、短路(short-circuiting)求值以及惰性求值改善效能。

儘管有時for-each迴圈的這些特性(序列,依次)是我們所期待的,但它對改善效能造成了阻礙。
我們可以使用內部迭代(internal iteration)替代外部迭代,使用者把對迭代的控制權交給類庫,並向類庫傳遞迭代時所需執行的程式碼, java 8中的內部迭代通過訪問者模式(Visitor)實現。
下面是前例的內部迭代程式碼:
shapes.forEach(s -> s.setColor(RED));
儘管看起來只是一個小小的語法改動,但是它們的實際差別非常巨大。使用者把對操作的控制權交還給類庫,從而允許類庫進行各種各樣的優化(例如亂序執行、惰性求值和並行等等)。總的來說,內部迭代使得外部迭代中不可能實現的優化成為可能。
外部迭代同時承擔了 做什麼(把形狀設為紅色)和 怎麼做(得到 Iterator例項然後依次遍歷)兩項職責,而內部迭代只負責 做什麼,而把 怎麼做 留給類庫。通過這樣的職責轉變:使用者的程式碼會變得更加清晰,而類庫則可以進行各種優化,從而使所有使用者都從中受益。

2.2 什麼是流

Java 8 API添加了一個新的抽象稱為StreamStream使用一種類似用 SQL 語句從資料庫查詢資料的直觀方式來提供一種對 Java 集合運算和表達的高階抽象。
這種風格將要處理的元素集合看作一種流, 流在管道中傳輸, 並且可以在管道的節點上進行處理, 比如篩選, 排序,聚合等。
Stream 不是集合元素,它不是資料結構並不儲存資料,它是有關演算法和計算的,它更像一個高階版本的 Iterator。原始版本的 Iterator,使用者只能顯式地一個一個遍歷元素並對其執行某些操作;高階版本的 Stream,使用者只要給出需要對其包含的元素執行什麼操作,比如 “過濾掉長度大於 10 的字串”、“獲取每個字串的首字母”等,Stream 會隱式地在內部進行遍歷,做出相應的資料轉換。
Stream 就如同一個迭代器(Iterator),單向,不可往復,資料只能遍歷一次,遍歷過一次後即用盡了,就好比流水從面前流過,一去不復返。
而和迭代器又不同的是,Stream 可以並行化操作,迭代器只能命令式地、序列化操作。顧名思義,當使用序列方式去遍歷時,每個 item 讀完後再讀下一個 item。而使用並行去遍歷時,資料會被分成多個段,其中每一個都在不同的執行緒中處理,然後將結果一起輸出。Stream 的並行操作依賴於 Java7 中引入的 Fork/Join 框架來拆分任務和加速處理過程。

2.3 流的構成

當我們使用一個流的時候,通常包括三個基本步驟:
獲取一個數據源(source)→ 資料轉換→執行操作獲取想要的結果,每次轉換原有 Stream 物件不改變,返回一個新的 Stream 物件(可以有多次轉換),這就允許對其操作可以像鏈條一樣排列,變成一個管道,如下圖所示:
流的構成
有多種方式生成 Stream Source:

  • 從 Collection 和陣列

    • Collection.stream()
    • Collection.parallelStream()
    • Arrays.stream(T array) or Stream.of()
    • 從 BufferedReader
    • java.io.BufferedReader.lines()
  • 靜態工廠

    • java.util.stream.IntStream.range()
    • java.nio.file.Files.walk()
  • 自己構建

    • java.util.Spliterator
  • 其它

    • Random.ints()
    • BitSet.stream()
    • Pattern.splitAsStream(java.lang.CharSequence)
    • JarFile.stream()

流的操作型別分為兩種:

  • Intermediate:一個流可以後面跟隨零個或多個 intermediate 操作。其目的主要是開啟流,做出某種程度的資料對映/過濾,然後返回一個新的流,交給下一個操作使用。這類操作都是惰性化的(lazy),就是說,僅僅呼叫到這類方法,並沒有真正開始流的遍歷。
  • Terminal:一個流只能有一個 terminal 操作,當這個操作執行後,流就被使用“光”了,無法再被操作。所以這必定是流的最後一個操作。Terminal 操作的執行,才會真正開始流的遍歷,並且會生成一個結果,或者一個 side effect。

在對於一個 Stream 進行多次轉換操作 (Intermediate 操作),每次都對 Stream 的每個元素進行轉換,而且是執行多次,這樣時間複雜度就是 N(轉換次數)個 for 迴圈裡把所有操作都做掉的總和嗎?其實不是這樣的,轉換操作都是 lazy 的,多個轉換操作只會在 Terminal 操作的時候融合起來,一次迴圈完成。我們可以這樣簡單的理解,Stream 裡有個操作函式的集合,每次轉換操作就是把轉換函式放入這個集合中,在 Terminal 操作的時候迴圈 Stream 對應的集合,然後對每個元素執行所有的函式。

還有一種操作被稱為 short-circuiting(短路)。用以指:

  • 對於一個 intermediate 操作,如果它接受的是一個無限大(infinite/unbounded)的 Stream,但返回一個有限的新 Stream。
  • 對於一個 terminal 操作,如果它接受的是一個無限大的 Stream,但能在有限的時間計算出結果。

當操作一個無限大的 Stream,而又希望在有限時間內完成操作,則在管道內擁有一個 short-circuiting 操作是必要非充分條件。
一個流操作的示例

int sum = widgets.stream()
.filter(w -> w.getColor() == RED)
.mapToInt(w -> w.getWeight())
.sum();

stream() 獲取當前小物件的 sourcefiltermapToIntintermediate 操作,進行資料篩選和轉換,最後一個 sum()terminal 操作,對符合條件的全部小物件作重量求和。

2.4 流的構造與轉換

下面提供最常見的幾種構造 Stream 的樣例。

// 1. Individual values
Stream stream = Stream.of("a", "b", "c");
// 2. Arrays
String [] strArray = new String[] {"a", "b", "c"};
stream = Stream.of(strArray);
stream = Arrays.stream(strArray);
// 3. Collections
List<String> list = Arrays.asList(strArray);
stream = list.stream();

需要注意的是,對於基本數值型,目前有三種對應的包裝型別 Stream:
IntStreamLongStreamDoubleStream。當然我們也可以用 StreamStream >、Stream,但是 boxing 和 unboxing 會很耗時,所以特別為這三種基本數值型提供了對應的 Stream
Java 8 中還沒有提供其它數值型 Stream,因為這將導致擴增的內容較多。而常規的數值型聚合運算可以通過上面三種 Stream 進行。
數值流的構造

IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);
IntStream.range(1, 3).forEach(System.out::println);
IntStream.rangeClosed(1, 3).forEach(System.out::println);

流轉換為其它資料結構

// 1. Array
String[] strArray1 = stream.toArray(String[]::new);
// 2. Collection
List<String> list1 = stream.collect(Collectors.toList());
List<String> list2 = stream.collect(Collectors.toCollection(ArrayList::new));
Set set1 = stream.collect(Collectors.toSet());
Stack stack1 = stream.collect(Collectors.toCollection(Stack::new));
// 3. String
String str = stream.collect(Collectors.joining()).toString();

一個 Stream 只可以使用一次,上面的程式碼為了簡潔而重複使用了數次。

2.5 流的操作

接下來,當把一個數據結構包裝成 Stream 後,就要開始對裡面的元素進行各類操作了。常見的操作可以歸類如下。

  • Intermediate: map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
  • Terminal:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
  • Short-circuiting:anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

我們下面看一下 Stream 的比較典型用法。

2.5.1 map/flatMap

我們先來看 map。如果熟悉 scala 這類函式式語言,對這個方法應該很瞭解,它的作用就是把 input Stream 的每一個元素,對映成 output Stream 的另外一個元素。

List<String> output = wordList.stream().
map(String::toUpperCase).
collect(Collectors.toList());
//這段程式碼把所有的單詞轉換為大寫

List<Integer> nums = Arrays.asList(1, 2, 3, 4);
List<Integer> squareNums = nums.stream().
map(n -> n * n).
collect(Collectors.toList());
//這段程式碼生成一個整數 list 的平方數 {1, 4, 9, 16}

從上面例子可以看出,map 生成的是個 1:1 對映,每個輸入元素,都按照規則轉換成為另外一個元素。還有一些場景,是一對多對映關係的,這時需要 flatMap。

Stream<List<Integer>> inputStream = Stream.of(
 Arrays.asList(1),
 Arrays.asList(2, 3),
 Arrays.asList(4, 5, 6)
 );
Stream<Integer> outputStream = inputStream.
flatMap((childList) -> childList.stream());

flatMap 把 input Stream 中的層級結構扁平化,就是將最底層元素抽出來放到一起,最終 output 的新 Stream 裡面已經沒有 List 了,都是直接的數字。

2.5.2 filter

filter 對原始 Stream 進行某項測試,通過測試的元素被留下來生成一個新 Stream

Integer[] sixNums = {1, 2, 3, 4, 5, 6};
Integer[] evens =
Stream.of(sixNums).filter(n -> n%2 == 0).toArray(Integer[]::new);
//經過條件“被 2 整除”的 filter,剩下的數字為 {2, 4, 6}

 List<String> output = reader.lines().
 flatMap(line -> Stream.of(line.split(REGEXP))).
 filter(word -> word.length() > 0).
 collect(Collectors.toList());
//這段程式碼首先把每行的單詞用 flatMap 整理到新的 Stream,然後保留長度不為 0 的,就是整篇文章中的全部單詞了

2.5.3 forEach

forEach 方法接收一個 Lambda 表示式,然後在 Stream 的每一個元素上執行該表示式。

// Java 8
roster.stream()
 .filter(p -> p.getGender() == Person.Sex.MALE)
 .forEach(p -> System.out.println(p.getName()));
 
// Pre-Java 8
for (Person p : roster) {
 if (p.getGender() == Person.Sex.MALE) {
 System.out.println(p.getName());
 }
}

對一個人員集合遍歷,找出男性並列印姓名。可以看出來,forEach 是為 Lambda 而設計的,保持了最緊湊的風格。而且 Lambda 表示式本身是可以重用的,非常方便。當需要為多核系統優化時,可以 parallelStream().forEach(),只是此時原有元素的次序沒法保證,並行的情況下將改變序列時操作的行為,此時 forEach 本身的實現不需要調整,而 Java8 以前的 for 迴圈 code 可能需要加入額外的多執行緒邏輯。
但一般認為,forEach 和常規 for 迴圈的差異不涉及到效能,它們僅僅是函式式風格與傳統 Java 風格的差別。
另外一點需要注意,forEach 是 terminal 操作,因此它執行後,Stream 的元素就被“消費”掉了,你無法對一個 Stream 進行兩次 terminal 運算。下面的程式碼是錯誤的:

stream.forEach(element -> doOneThing(element));
stream.forEach(element -> doAnotherThing(element));

相反,具有相似功能的 intermediate 操作 peek 可以達到上述目的。如下是出現在該 api javadoc 上的一個示例。
peek 對每個元素執行操作並返回一個新的 Stream

Stream.of("one", "two", "three", "four")
 .filter(e -> e.length() > 3)
 .peek(e -> System.out.println("Filtered value: " + e))
 .map(String::toUpperCase)
 .peek(e -> System.out.println("Mapped value: " + e))
 .collect(Collectors.toList());

forEach 不能修改自己包含的本地變數值,也不能用 break/return 之類的關鍵字提前結束迴圈。

2.5.4 findFirst

這是一個 termimalshort-circuiting操作,它總是返回 Stream 的第一個元素,或者空。
這裡比較重點的是它的返回值型別:Optional。這也是一個模仿 Scala 語言中的概念,作為一個容器,它可能含有某值,或者不包含。使用它的目的是儘可能避免 NullPointerException
Optional 的兩個用例

String strA = " abcd ", strB = null;
print(strA);
print("");
print(strB);
getLength(strA);
getLength("");
getLength(strB);
public static void print(String text) {
 // Java 8
 Optional.ofNullable(text).ifPresent(System.out::println);
 
 // Pre-Java 8
 if (text != null) {
 System.out.println(text);
 }
 }
public static int getLength(String text) {
 // Java 8
return Optional.ofNullable(text).map(String::length).orElse(-1);

 // Pre-Java 8
// return if (text != null) ? text.length() : -1;
 };

在更復雜的 if (xx != null) 的情況中,使用 Optional 程式碼的可讀性更好,而且它提供的是編譯時檢查,能極大的降低 NPE 這種 Runtime Exception 對程式的影響,或者迫使程式設計師更早的在編碼階段處理空值問題,而不是留到執行時再發現和除錯。
Stream 中的 findAnymax/minreduce 等方法等返回 optional 值。還有例如 IntStream.average()返回 OptionalDouble 等等。

2.5.5 reduce

這個方法的主要作用是把 Stream元素組合起來。它提供一個起始值(種子),然後依照運算規則(BinaryOperator),和前面 Stream 的第一個、第二個、第 n 個元素組合。從這個意義上說,字串拼接、數值的 summinmaxaverage 都是特殊的 reduce。例如 Streamsum 就相當於

Integer sum = integers.reduce(0, (a, b) -> a+b); 
//or
Integer sum = integers.reduce(0, Integer::sum);

也有沒有起始值的情況,這時會把 Stream 的前面兩個元素組合起來,返回的是 Optional。
reduce 的用例

// 字串連線,concat = "ABCD"
String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat); 
// 求最小值,minValue = -3.0
double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min); 
// 求和,sumValue = 10, 有起始值
int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
// 求和,sumValue = 10, 無起始值
sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();
// 過濾,字串連線,concat = "ace"
concat = Stream.of("a", "B", "c", "D", "e", "F").
 filter(x -> x.compareTo("Z") > 0).
 reduce("", String::concat);

上面程式碼例如第一個示例的 reduce(),第一個引數(空白字元)即為起始值,第二個引數(String::concat)為 BinaryOperator。這類有起始值的 reduce()都返回具體的物件。而對於第四個示例沒有起始值的 reduce(),由於可能沒有足夠的元素,返回的是 Optional,請留意這個區別。

2.5.6 limit/skip

limit 返回 Stream 的前面 n 個元素;skip 則是扔掉前 n 個元素(它是由一個叫 subStream 的方法改名而來)。
limitskip 對執行次數的影響

public void testLimitAndSkip() {
 List<Person> persons = new ArrayList();
 for (int i = 1; i <= 10000; i++) {
 Person person = new Person(i, "name" + i);
 persons.add(person);
 }
List<String> personList2 = persons.stream().
map(Person::getName).limit(10).skip(3).collect(Collectors.toList());
 System.out.println(personList2);
}
private class Person {
 public int no;
 private String name;
 public Person (int no, String name) {
 this.no = no;
 this.name = name;
 }
 public String getName() {
 System.out.println(name);
 return name;
 }
}

這是一個有 10,000 個元素的 Stream,但在 short-circuiting 操作 limitskip 的作用下,管道中 map 操作指定的 getName() 方法的執行次數為 limit 所限定的 10 次,而最終返回結果在跳過前 3 個元素後只有後面 7 個返回。
有一種情況是 limit/skip 無法達到 short-circuiting 目的的,就是把它們放在 Stream 的排序操作後,原因跟 sorted 這個 intermediate 操作有關:此時系統並不知道 Stream 排序後的次序如何,所以 sorted 中的操作看上去就像完全沒有被 limit 或者 skip 一樣。
limitskipsorted 後的執行次數無影響

List<Person> persons = new ArrayList();
 for (int i = 1; i <= 5; i++) {
 Person person = new Person(i, "name" + i);
 persons.add(person);
 }
List<Person> personList2 = persons.stream().sorted((p1, p2) -> 
p1.getName().compareTo(p2.getName())).limit(2).collect(Collectors.toList());
System.out.println(personList2);

上面的程式碼首先對 5 個元素的 Stream 排序,然後進行 limit 操作。
即雖然最後的返回元素數量是 2,但整個管道中的 sorted 表示式執行次數沒有像前面例子相應減少。
最後有一點需要注意的是,對一個 parallel 的 Steam 管道來說,如果其元素是有序的,那麼 limit操作的成本會比較大,因為它的返回物件必須是前 n 個也有一樣次序的元素。取而代之的策略是取消元素間的次序,或者不要用 parallel Stream。

2.5.7 sorted

對 Stream 的排序通過 sorted 進行,它比陣列的排序更強之處在於可以首先對 Stream 進行各類 mapfilterlimitskip 甚至 distinct來減少元素數量後,再排序,這能幫助程式明顯縮短執行時間。
優化:排序前進行 limit 和 skip

List<Person> persons = new ArrayList();
 for (int i = 1; i <= 5; i++) {
 Person person = new Person(i, "name" + i);
 persons.add(person);
 }
List<Person> personList2 = persons.stream().limit(2).sorted((p1, p2) -> p1.getName().compareTo(p2.getName())).collect(Collectors.toList());
System.out.println(personList2);

當然,這種優化是有 business logic 上的侷限性的:即不要求排序後再取值。

2.5.8 min/max/distinct

minmax的功能也可以通過對 Stream 元素先排序,再 findFirst 來實現,但前者的效能會更好,為 O(n),而 sorted 的成本是 O(n log n)。同時它們作為特殊的 reduce 方法被獨立出來也是因為求最大最小值是很常見的操作。
找出最長一行的長度

BufferedReader br = new BufferedReader(new FileReader("c:\\SUService.log"));
int longest = br.lines().
 mapToInt(String::length).
 max().
 getAsInt();
br.close();
System.out.println(longest);

下面的例子則使用 distinct 來找出不重複的單詞。
找出全文的單詞,轉小寫,並排序

List<String> words = br.lines().
 flatMap(line -> Stream.of(line.split(" "))).
 filter(word -> word.length() > 0).
 map(String::toLowerCase).
 distinct().
 sorted().
 collect(Collectors.toList());
br.close();
System.out.println(words);

2.5.9 Match

Stream 有三個 match 方法,從語義上說:

  • allMatch:Stream 中全部元素符合傳入的 predicate,返回 true
  • anyMatch:Stream 中只要有一個元素符合傳入的 predicate,返回 true
  • noneMatch:Stream 中沒有一個元素符合傳入的 predicate,返回 true

它們都不是要遍歷全部元素才能返回結果。例如 allMatch 只要一個元素不滿足條件,就 skip 剩下的所有元素,返回 false。

List<Person> persons = new ArrayList();
persons.add(new Person(1, "name" + 1, 10));
persons.add(new Person(2, "name" + 2, 21));
persons.add(new Person(3, "name" + 3, 34));
persons.add(new Person(4, "name" + 4, 6));
persons.add(new Person(5, "name" + 5, 55));
boolean isAllAdult = persons.stream().
 allMatch(p -> p.getAge() > 18);
System.out.println("All are adult? " + isAllAdult);
boolean isThereAnyChild = persons.stream().
 anyMatch(p -> p.getAge() < 12);
System.out.println("Any child? " + isThereAnyChild);

輸出結果:

All are adult? false
Any child? true

2.5.10 Stream.generate

通過實現 Supplier 介面,可以自己來控制流的生成。這種情形通常用於隨機數、常量的 Stream,或者需要前後元素間維持著某種狀態資訊的 Stream。把 Supplier 例項傳遞給 Stream.generate() 生成的 Stream,預設是序列(相對 parallel 而言)但無序的(相對 ordered 而言)。由於它是無限的,在管道中,必須利用 limit 之類的操作限制 Stream 大小。
生成 10 個隨機整數

Random seed = new Random();
Supplier<Integer> random = seed::nextInt;
Stream.generate(random).limit(10).forEach(System.out::println);
//Another way
IntStream.generate(() -> (int) (System.nanoTime() % 100)).
limit(10).forEach(System.out::println);

Stream.generate() 還接受自己實現的 Supplier。例如在構造海量測試資料的時候,用某種自動的規則給每一個變數賦值;或者依據公式計算 Stream 的每個元素值。這些都是維持狀態資訊的情形。
自實現 Supplier

Stream.generate(new PersonSupplier()).
limit(10).
forEach(p -> System.out.println(p.getName() + ", " + p.getAge()));
private class PersonSupplier implements Supplier<Person> {
 private int index = 0;
 private Random random = new Random();
 @Override
 public Person get() {
 return new Person(index++, "StormTestUser" + index, random.nextInt(100));
 }
}

2.5.11 Stream.iterate

iteratereduce 操作很像,接受一個種子值,和一個 UnaryOperator(例如 f)。然後種子值成為 Stream的第一個元素,f(seed) 為第二個,f(f(seed)) 第三個,以此類推。
生成一個等差數列

Stream.iterate(0, n -> n + 3).limit(10). forEach(x -> System.out.print(x + " "));.

Stream.generate 相仿,在 iterate 時候管道必須有 limit 這樣的操作來限制 Stream大小。

3. 並行資料處理與效能

3.1 並行流

Stream介面可以通過收集源呼叫parallelStream方法來把集合轉換為並行流。並行流就是把一個內容分成多個數據塊,並用不同的執行緒分別處理每個資料塊的流。
並行流內部使用了預設的ForkJoinPool,它預設的執行緒數量就是處理器數量,這個值是由Runtime.getRunTime().availableProcessors()得到的。但是可以通過系統屬性 java.util.concurrent.ForkJoinPool.common.parallelism來改變執行緒池大小。如下:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
使用正確的資料結構然後使其並行化工作能保證最佳的效能。特別注意原始型別的裝箱和拆箱操作。
一些幫你決定某個特定情況下是否有必要使用並行流的建議:

  • 有疑問,測量。把順序流轉化成並行流輕而易舉,但卻不一定是好事。並行流並不總是比順序流快。
  • 留意裝箱。自動裝箱和拆箱操作會大大降低效能。Java8中有原始型別流(IntStream,LongStream,DoubleStream)來避免這種操作,但凡有可能應該使用這些流。
  • 有些操作本身在並行流上的效能就比順序流差。特別是limitfindFirst等依賴於元素順序的操作。
  • 還要考慮流的操作流水線的總計算成本。
  • 對於較小的資料量,選擇並行幾乎從來都不是一個好的決定。並行處理少數幾個元素的好處還抵不上並行化造成的額外開銷。
  • 要考慮流背後的資料結構是否易於分解。如ArrayList的拆分效率比LinkList高得多,因為前者用不著遍歷就可以平均拆分,而後者則必須遍歷。另外,用range工廠方法建立的原始型別流也可以快速分解。

3.2 分支/合併框架

分支合併框架的目的是以遞迴方式將可以並行的任務拆分成更小的任務,然後將每個子任務的結果合併起來生成整體結果。這是ExecutorService介面的一個實現,它把子任務分配給執行緒池(稱為ForkJoinPool)中的工作執行緒。
要把任務提交到這個池,必須建立RecursiveTask的一個子類,其中R是並行化任務(以及所有子任務)產生的結果型別,或者如果任務不返回結果,則是RecursiveAction型別(當然它可能會更新其他非區域性機構)。要定義RecursiveTask,只需要實現它唯一的抽象方法compute;
protected abstract R compute();
這個方法同時定義了將任務拆分成子任務的邏輯,以及無法再拆分或不方便再拆分時,生成單個子任務結果的邏輯。虛擬碼:

if(任務足夠小或不可分){
    順序計算該任務
}else{
    將任務分成兩個子任務
    遞迴呼叫本方法,拆分每個子任務,等待所有子任務完成
    合併每個子任務的結果
}

選個例子為基礎,讓我們試著用這個框架為一個數字範圍(這裡用一個long[]陣列表示)求和。需要先為RecursiveTask類做一個實現:

package com.tim.test

public class ForkJoinSumCalculator
        extends java.util.concurrent.RecursiveTask<Long> {
    private final long[] numbers;
    private final int start;
    private final int end;
    public static final long THRESHOLD = 10_000;

    public ForkJoinSumCalculator(long[] numbers) {
        this(numbers, 0, numbers.length);
    }

    private ForkJoinSumCalculator(long[] numbers, int start, int end) {
        this.numbers = numbers;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        int length = end - start;
        if (length <= THRESHOLD) {
            return computeSequentially();
        }
        ForkJoinSumCalculator leftTask =
                new ForkJoinSumCalculator(numbers, start, start + length / 2);
        leftTask.fork();
        ForkJoinSumCalculator rightTask =
                new ForkJoinSumCalculator(numbers, start + length / 2, end);
        Long rightResult = rightTask.compute();
        Long leftResult = leftTask.join();
        return leftResult + rightResult;
    }

    private long computeSequentially() {
        long sum = 0;
        for (int i = start; i < end; i++) {
            {
                sum += numbers[i];
            }
            return sum;
        }
    }

測試方法:

public static long forkJoinSum(long n) {
        long[] numbers = LongStream.rangeClosed(1, n).toArray();
        ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);
        return new ForkJoinPool().invoke(task);
}

執行ForkJoinSumCalculator 當把ForkJoinSumCalculator任務傳給ForkJoinPool時,這個任務就由池中的一個執行緒執行,這個執行緒會呼叫任務的compute方法。該方法會檢查任務是否小到足以順序執行,如果不夠小則會把要求和的陣列分成兩半,分給兩個新的ForkJoinSumCalculator,而它們也由ForkJoinPool安排執行。因此,這一過程可以遞迴重複,把原任務分為更小的任務,直到滿足不方便或不可能再進一步拆分的條件。這時會順序計算每個任務的結果,然後由分支過程建立的(隱含的)任務二叉樹遍歷回到它的根。接下來會合並每個子任務的部分結果,從而得到總任務的結果。

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

3.3 Spliterator

Spliterator是Java 8中加入的另一個新介面;這個名字代表“可分迭代器”(splitable iterator)。和Iterator一樣,Spliterator也用於遍歷資料來源中的元素,但它是為了並行執行而設計的。雖然在實踐中可能用不著自己開發Spliterator,但瞭解一下它的實現方式會讓你對並行流的工作原理有更深入的瞭解。Java8已經為集合框架中包含的所有資料結構提供了一個預設的Spliterator實現。集合實現了Spliterator介面,介面提供了一個spliterator方法。這個介面定義了若干方法,如下面的程式碼清單所示。

public interface Spliterator<T> {
    boolean tryAdvance(Consumer<? super T> action);
    Spliterator<T> trySplit();
    long estimateSize();
    int characteristics();
}

與往常一樣,T是Spliterator遍歷的元素的型別。tryAdvance方法的行為類似於普通的因為它會按順序一個一個使用Spliterator中的元素,並且如果還有其他元素要遍歷就返回true。但trySplit是專為Spliterator介面設計的,因為它可以把一些元素劃出去分給第二個Spliterator(由該方法返回),讓它們兩個並行處理。Spliterator還可通過estimateSize方法估計還剩下多少元素要遍歷,因為即使不那麼確切,能快速算出來是一個值也有助於讓拆分均勻一點。

4. Collector

前面的程式碼中可以發現,stream裡有一個collect(Collector c)方法,接收一個Collector例項, 其功能是把stream歸約成一個value的操作,這裡的value可以是一個CollectionMap等物件。
歸約,就是對中間操作(過濾,轉換等)的結果進行收集歸一化的步驟,當然也可以對歸約結果進行再歸約,這就是歸約的嵌套了。中間操作不消耗流,歸約會消耗流,而且只能消費一次,就像把流都吃掉了。

4.1 轉換成其他集合

4.1.1 toList

示例:

List<Integer> collectList = Stream.of(1, 2, 3, 4)
        .collect(Collectors.toList());
System.out.println("collectList: " + collectList);

4.1.2 toSet

示例:

Set<Integer> collectSet = Stream.of(1, 2, 3, 4)
        .collect(Collectors.toSet());
System.out.println("collectSet: " + collectSet);

4.1.3 toCollection

通常情況下,建立集合時需要呼叫適當的建構函式指明集合的具體型別:
List<Artist> artists = new ArrayList<>();
但是呼叫toList或者toSet方法時,不需要指定具體的型別,Stream類庫會自動推斷並生成合適的型別。當然,有時候我們對轉換生成的集合有特定要求,比如,希望生成一個TreeSet,而不是由Stream類庫自動指定的一種型別。此時使用toCollection,它接受一個函式作為引數, 來建立集合。

值得我們注意的是,看Collectors的原始碼,因為其接受的函式引數必須繼承於Collection,也就是意味著Collection並不能轉換所有的繼承類,最明顯的就是不能通過toCollection轉換成Map*

4.1.4 toMap

如果生成一個Map,我們需要呼叫toMap方法。由於Map中有KeyValue這兩個值,故該方法與toSettoList等的處理方式是不一樣的。toMap最少應接受兩個引數,一個用來生成key,另外一個用來生成value

public Map<Long, Account> getIdAccountMap(List<Account> accounts) {
    return accounts.stream().collect(Collectors.toMap(Account::getId, account -> account));
}

account -> account是一個返回本身的Lambda表示式,其實還可以使用Function介面中的一個預設方法代替,使整個方法更簡潔優雅:

public Map<Long, Account> getIdAccountMap(List<Account> accounts) {
    return accounts.stream().collect(Collectors.toMap(Account::getId, Function.identity()));
}

重複key的情況
程式碼如下:

public Map<String, Account> getNameAccountMap(List<Account> accounts) {
    return accounts.stream().collect(Collectors.toMap(Account::getUsername, Function.identity()));
}

這個方法可能報錯(java.lang.IllegalStateException: Duplicate key),因為name是有可能重複的。toMap有個過載方法,可以傳入一個合併的函式來解決key衝突問題:

public Map<String, Account> getNameAccountMap(List<Account> accounts) {
    return accounts.stream().collect(Collectors.toMap(Account::getUsername, Function.identity(), (key1, key2) -> key2));
}

這裡只是簡單的使用後者覆蓋前者來解決key重複問題。

指定具體收集的map
toMap還有另一個過載方法,可以指定一個Map的具體實現,來收集資料:

public Map<String, Account> getNameAccountMap(List<Account> accounts) {
    return accounts.stream().collect(Collectors.toMap(Account::getUsername, Function.identity(), (key1, key2) -> key2, LinkedHashMap::new));

4.2 轉成值

使用collect可以將Stream轉換成值。maxBy和minBy允許使用者按照某個特定的順序生成一個值。

  • averagingDouble:求平均值,Stream的元素型別為double
  • averagingInt:求平均值,Stream的元素型別為int
  • averagingLong:求平均值,Stream的元素型別為long
  • counting:Stream*元素個數
  • maxBy:在指定條件下的,Stream的最大元素
  • minBy:在指定條件下的,Stream的最小元素
  • reducing: reduce操作
  • summarizingDouble:統計Stream的資料(double)狀態,其中包括count,min,max,sum和平均。
  • summarizingInt:統計Stream的資料(int)狀態,其中包括count,min,max,sum和平均。
  • summarizingLong:統計Stream的資料(long)狀態,其中包括count,min,max,sum和平均。
  • summingDouble:求和,Stream的元素型別為double
  • summingInt:求和,Stream的元素型別為int
  • summingLong:求和,Stream的元素型別為long

示例:

Optional<Integer> collectMaxBy = Stream.of(1, 2, 3, 4)
            .collect(Collectors.maxBy(Comparator.comparingInt(o -> o)));
System.out.println("collectMaxBy:" + collectMaxBy.get());

4.3 資料分組

collect的一個常用操作將Stream分解成兩個集合。假如一個數字的Stream,我們可能希望將其分割成兩個集合,一個是偶數集合,另外一個是奇數集合。我們首先想到的就是過濾操作,通過兩次過濾操作,很簡單的就完成了我們的需求。
但是這樣操作起來有問題。首先,為了執行兩次過濾操作,需要有兩個流。其次,如果過濾操作複雜,每個流上都要執行這樣的操作, 程式碼也會變得冗餘。
這裡我們就不得不說Collectors庫中的partitioningBy方法,它接受一個流,並將其分成兩部分:使用Predicate物件,指定條件並判斷一個元素應該屬於哪個部分,並根據布林值返回一個Map到列表。因此對於key為true所對應的List中的元素,滿足Predicate物件中指定的條件;同樣,key為false所對應的List中的元素,不滿足Predicate物件中指定的條件。
這樣,使用partitioningBy,我們就可以將數字的Stream分解成奇數集合和偶數集合了。

Map<Boolean, List<Integer>> collectParti = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
            .collect(Collectors.partitioningBy(it -> it % 2 == 0));
System.out.println("collectParti : " + collectParti);

groupingBy是一種更自然的分割資料操作,與將資料分成true和false兩部分不同,groupingBy可以使用任意值對資料分組。groupingBy接受一個分類函式Function,用來對資料分組。
根據數字除以3的餘數進行分組:

Map<Integer, List<Integer>> collectParti2 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
            .collect(Collectors.groupingBy(t -> t % 3));
System.out.println("collectParti2 : " + collectParti2);

4.4 字串處理

有時候,我們將Stream的元素(String型別)最後生成一組字串。比如在Stream.of("1", "2","3", "4")中,將Stream格式化成“1,2,3,4”。
如果不使用Stream,我們可以通過for迴圈迭代實現。

ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);

StringBuilder sb = new StringBuilder();

for (Integer it : list) {
    if (sb.length() > 0) {
        sb.append(",");
    }
    sb.append(it);

}
System.out.println(sb.toString());

在Java 1.8中,我們可以使用Stream來實現。這裡我們將使用 Collectors.joining 收集Stream中的值,該方法可以方便地將Stream得到一個字串。joining函式接受三個引數,分別表示允(用以分隔元素)、字首和字尾。
示例:

String strJoin = Stream.of("1", "2", "3", "4")
        .collect(Collectors.joining(",", "[", "]"));
System.out.println("strJoin: " + strJoin);

4.5 組合Collector

前面,我們已經瞭解到Collector的強大,而且非常的實用。如果將他們組合起來,是不是更厲害呢?看前面舉過的例子,在資料分組時,我們是得到的分組後的資料列表 collectGroup : {false=[1, 2, 3], true=[4]}。如果我們的要求更高點,我們不需要分組後的列表,只要得到分組後列表的個數就好了。
這時候,很多人下意識的都會想到,便利Map就好了,然後使用list.size(),就可以輕鬆的得到各個分組的列表個數。

// 分割資料塊
Map<Boolean, List<Integer>> collectParti = Stream.of(1, 2, 3, 4)
        .collect(Collectors.partitioningBy(it -> it % 2 == 0));

Map<Boolean, Integer> mapSize = new HashMap<>();
collectParti.entrySet()
        .forEach(entry -> mapSize.put(entry.getKey(), entry.getValue().size()));

System.out.println("mapSize : " + mapSize);

在partitioningBy方法中,有這麼一個變形:

Map<Boolean, Long> partiCount = Stream.of(1, 2, 3, 4)
        .collect(Collectors.partitioningBy(it -> it.intValue() % 2 == 0,
                Collectors.counting()));
System.out.println("partiCount: " + partiCount);

partitioningBy方法中,我們不僅傳遞了條件函式,同時傳入了第二個收集器,用以收集最終結果的一個子集,這些收集器叫作下游收集器。收集器是生成最終結果的一劑配方,下游收集器則是生成部分結果的配方,主收集器中會用到下游收集器。這種組合使用收集器的方式, 使得它們在 Stream 類庫中的作用更加強大。
那些為基本型別特殊定製的函式,如averagingIntsummarizingLong等,事實上和呼叫特殊Stream上的方法是等價的,加上它們是為了將它們當作下游收集器來使用的。

4.6 介面分析

Collectors.toList 工廠方法返回一個收集器,它會把流中的所有元素收整合一個 List。使用廣泛而且寫起來比較直觀,通過仔細研究這個收集器是怎麼實現的,可以很好地瞭解 Collector 介面是怎麼定義的,以及它的方法所返回的函式在內部是如何為collect 方法所用的。
首先讓我們在下面的列表中看看 Collector介面的定義,它列出了介面的簽名以及宣告的五個方法。

public interface Collector<T, A, R> {
        Supplier<A> supplier();
        BiConsumer<A, T> accumulator();
        Function<A, R> finisher();
        BinaryOperator<A> combiner();
        Set<Characteristics> characteristics();
}

Collector<T, A, R>接受三個泛型引數,對可變減少操作的資料型別作相應限制:

  • T:表示流中每個元素的型別。
  • A:表示中間結果容器的型別。
  • R:表示最終返回的結果型別。

Collector介面聲明瞭4個函式,這四個函式一起協調執行以將元素目累積到可變結果容器中,並且可以選擇地對結果進行最終的變換.

現在我們可以一個個來分析 Collector 介面宣告的五個方法了。通過分析發現,前四個方法都會返回一個會被 collect 方法呼叫的函式,而第五個方法characteristics 則提供了一系列特徵,也就是一個提示列表,告訴 collect 方法在執行歸約操作的時候可以應用哪些優化(比如並行化)。

4.6.1 建立新的結果容器: supplier 方法

supplier 方法必須返回一個結果為空的 Supplier ,也就是一個無引數函式,在呼叫時它會建立一個空的累加器例項,供資料收集過程使用。很明顯,對於將累加器本身作為結果返回的收集器,比如我們的 ToListCollector ,在對空流執行操作的時候,這個空的累加器也代表了收集過程的結果。在我們的 ToListCollector 中, supplier 返回一個空的 List ,如下所示:

@Override
public Supplier<List<T>> supplier() {
    return () -> new ArrayList<>();
}

也可以只傳遞一個建構函式引用:

@Override
public Supplier<List<T>> supplier() {
    return ArrayList::new;
}

4.6.2 將元素新增到結果容器: accumulator 方法

accumulator 方法會返回執行歸約操作的函式。當遍歷到流中第n個元素時,這個函式執行時會有兩個引數:儲存歸約結果的累加器(已收集了流中的前 n-1 個專案),還有第n個元素本身。該函式將返回void ,因為累加器是原位更新,即函式的執行改變了它的內部狀態以體現遍歷的元素的效果。對於ToListCollector ,這個函式僅僅會把當前專案新增至已經遍歷過的專案的列表:

@Override
public BiConsumer<List<T>, T> accumulator() {
    return (list, item) -> list.add(item);
}

也可以使用方法引用,這會更為簡潔:

@Override
public BiConsumer<List<T>, T> accumulator() {
    return List::add;
}

4.6.3 對結果容器應用最終轉換: finisher 方法

在遍歷完流後, finisher 方法必須返回在累積過程的最後要呼叫的一個函式,以便將累加器物件轉換為整個集合操作的最終結果。通常,就像 ToListCollector 的情況一樣,累加器物件恰好符合預期的最終結果,因此無需進行轉換。所以 finisher 方法只需返回 identity 函式:

@Override
public Function<List<T>, List<T>> finisher() {
    return Function.identity();
}

這三個方法已經足以對流進行循序規約。實踐中的實現細節可能還要複雜一點,一方面是應為流的延遲性質,可能在collect操作之前還需完成其他中間操作的流水線,另一方面則是理論上可能要進行並行規約。

4.6.4 合併兩個結果容器: combiner 方法

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

@Override
public BinaryOperator<List<T>> combiner() {
    return (list1, list2) -> {
        list1.addAll(list2);
        return list1;
    };
}

有了這第四個方法,就可以對流進行並行歸約了。它會用到Java7中引入的分支/合併框架和Spliterator抽象。

4.6.5 characteristics 方法

最後一個方法—— characteristics 會返回一個不可變的 Characteristics 集合,它定義了收集器的行為——尤其是關於流是否可以並行歸約,以及可以使用哪些優化的提示。Characteristics 是一個包含三個專案的列舉。

  • UNORDERED ——歸約結果不受流中專案的遍歷和累積順序的影響。
  • CONCURRENT —— accumulator 函式可以從多個執行緒同時呼叫,且該收集器可以並行歸約流。所以只有在並行流且收集器不具備CONCURRENT特性時,combiner方法返回的Lambda表示式才會執行(中間結果容器只有一個就無需合併)。
  • IDENTITY_FINISH ——這表明完成器方法返回的函式是一個恆等函式,可以跳過,中間結果容器型別與最終結果型別一致,此時finiser方法不會被呼叫。

4.6.6 融會貫通

前一小節中談到的五個方法足夠我們開發自己的 ToListCollector 了。現在可以把它們都融合起來,如下面的程式碼清單所示。

public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
    @Override
    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }

    @Override
    public BiConsumer<List<T>, T> accumulator() {
        return List::add;
    }

    @Override
    public BinaryOperator<List<T>> combiner() {
        return (list1, list2) -> {
            list1.addAll(list2);
            return list1;
        };
    }

    @Override
    public Function<List<T>, List<T>> finisher() {
        return Function.identity();
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH, Characteristics.CONCURRENT));
    }
}

請注意,這個是實現與*Collections.toList()*方法並不完全相同,但區別僅僅是一些小的優化。
對於 IDENTITY_FINISH 的收集操作,還有一種方法可以得到同樣的結果而無需從頭實現新的 Collectors 介面。 Stream 有一個過載的 collect 方法可以接受另外三個函式—— supplieraccumulatorcombiner ,其語義和 Collector 介面的相應方法返回的函式完全相同。所以比如說,我們可以像下面這樣把菜餚流中的專案收集到一個 List 中:

List<Dish> dishes = menuStream.collect(
                ArrayList::new,
                List::add,
                List::addAll);

我們認為,這第二種形式雖然比前一個寫法更為緊湊和簡潔,卻不那麼易讀。此外,以恰當的類來實現自己的自定義收集器有助於重用並可避免程式碼重複。另外值得注意的是,這第二個collect 方法不能傳遞任何 Characteristics ,所以它永遠都是一個 IDENTITY_FINISH 和CONCURRENT 但並非 UNORDERED 的收集器。

5. 日期時間API

Java 8通過釋出新的Date-Time API (JSR 310)來進一步加強對日期與時間的處理。
在舊版的 Java 中,日期時間 API 存在諸多問題,其中有:

  • 非執行緒安全 − java.util.Date 是非執行緒安全的,所有的日期類都是可變的,這是Java日期類最大的問題之一。
  • 設計很差 − Java的日期/時間類的定義並不一致,在java.util和java.sql的包中都有日期類,此外用於格式化和解析的類在java.text包中定義。java.util.Date同時包含日期和時間,而java.sql.Date僅包含日期,將其納入java.sql包並不合理。另外這兩個類都有相同的名字,這本身就是一個非常糟糕的設計。還有月份是從0開始這種反人類的設定等等。
  • 時區處理麻煩 − 日期類並不提供國際化,沒有時區支援,因此Java引入了java.util.Calendar和java.util.TimeZone類,但他們同樣存在上述所有的問題。

在新的時間API中,Instant表示一個精確的時間點,DurationPeriod表示兩個時間點之間的時間量。 LocalDate表示日期,即xx年xx月xx日,即不包括時間也不帶時區。LocalTimeLocalDate類似, 但只包含時間。LocalDateTime則包含日期和時間。ZoneDateTime表示一個帶時區的時間。 DateTimeFormatter提供格式化和解析功能。下面詳細的介紹使用方法。

5.1 LocalDate

LocalDate 表示像 2017-01-01這樣的日期。它包含有年份、月份、當月天數,它不不包含一天中的時間,以及時區資訊。由於上面的這些特點,所以LocalDate不能表示一個準確的時間點。
有很多時間的計算是不需要時區的,而且有一些情況下使用時區會導致一些問題,例如你在中國設定了一個2017-01-01 UT+8:00 的放假提醒,但之後你去了美國,到了2017-01-01 UT+8:00時間時你收到了提醒,但是此時美國還沒到放假的時間。
API的設計者推薦使用不帶時區的時間,除非真的希望表示絕對的時間點。
可以使用靜態方法now()of()建立LocalDatejava.util.Date使用0作為月份的開始,年份從1990年開始算起,而新的API中完全是用生活中一樣的方式來表示年和月份。

//獲取當前日期
LocalDate now = LocalDate.now();
//2017-01-01
LocalDate newYear = LocalDate.of(2017, 1, 1);

可以通過一些方法對日期做一些運算。

//三天後
now.plu