Java8 使用
鏈接:https://www.jianshu.com/p/936d97ba0362
鏈接:https://www.jianshu.com/p/41de7b5ac7b9
本文主要總結了《Java8實戰》,適用於學習 Java8 的同學,也可以作為一個 API 手冊文檔適用,平時使用時可能由於不熟練,忘記 API 或者語法。
Java8 新特性:
- Lambda 表達式 − Lambda允許把函數作為一個方法的參數(函數作為參數傳遞進方法中)。
- 方法引用 − 方法引用提供了非常有用的語法,可以直接引用已有Java類或對象(實例)的方法或構造器。與lambda聯合使用,方法引用可以使語言的構造更緊湊簡潔,減少冗余代碼。
- 默認方法 − 默認方法就是一個在接口里面有了一個實現的方法。
新工具 − 新的編譯工具,如:Nashorn引擎 jjs、 類依賴分析器jdeps。 - Stream API −新添加的Stream API(java.util.stream) 把真正的函數式編程風格引入到Java中。
- Date Time API − 加強對日期與時間的處理。
- Optional 類 − Optional 類已經成為 Java 8 類庫的一部分,用來解決空指針異常。
- Nashorn, JavaScript 引擎 − Java 8提供了一個新的Nashorn javascript引擎,它允許我們在JVM上運行特定的javascript應用。
Lambda 表達式
Lambda 表達式,也可稱為閉包,它是推動 Java 8 發布的最重要新特性。
Lambda 允許把函數作為一個方法的參數(函數作為參數傳遞進方中)。
使用 Lambda 表達式可以使代碼變的更加簡潔緊湊。
以下是lambda表達式的重要特征:
- 可選類型聲明:不需要聲明參數類型,編譯器可以統一識別參數值。
- 可選的參數圓括號:一個參數無需定義圓括號,但多個參數需要定義圓括號。
- 可選的大括號:如果主體包含了一個語句,就不需要使用大括號。
- 可選的返回關鍵字:如果主體只有一個表達式返回值則編譯器會自動返回值,大括號需要指定明表達式返回了一個數值。
方法引用
- 方法引用通過方法的名字來指向一個方法。
- 方法引用可以使語言的構造更緊湊簡潔,減少冗余代碼。
- 方法引用使用一對冒號 :: 。
// jdk1.8接口 @FunctionalInterface public interface Supplier <T> { T get(); } public class Car { //Supplier是jdk1.8的接口,這里和lamda一起使用了 public static Car create(final Supplier <Car> supplier) { return supplier.get(); } }
構造器引用
它的語法是Class::new,或者更一般的Class< T >::new實例如下:
final Car car = Car.create(Car::new); final List <Car> cars = Arrays.asList(car);
無參構造
Supplier <Apple> c1 = Apple::new; Apple a1 = c1.get();
一個參數構造
Function <Integer, Apple> c2 = Apple::new; Apple a2 = c2.apply(110);
兩個參數構造
BiFunction <String, Integer, Apple> c3 = Apple::new; Apple c3 = c3.apply("green", 110);
多個參數構造
可自定義Function實現,如
public interface TriFunction <T, U, V, R> { R apply(T t, U u, V v); }
靜態方法引用
它的語法是Class::static_method,實例如下:
cars.forEach(Car::collide);
特定類的任意對象的方法引用
它的語法是Class::method實例如下:
cars.forEach(Car::repair);
特定對象的方法引用
它的語法是instance::method實例如下:
final Car police = Car.create(Car::new); cars.forEach(police::follow);
函數式接口
函數式接口(Functional Interface)就是一個有且僅有一個抽象方法,但是可以有多個非抽象方法的接口。
函數式接口可以被隱式轉換為lambda表達式。
函數式接口可以現有的函數友好地支持 lambda。
JDK 1.8之前已有的函數式接口:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.security.PrivilegedAction
- java.util.Comparator
- java.io.FileFilter
- java.nio.file.PathMatcher
- java.lang.reflect.InvocationHandler
- java.beans.PropertyChangeListener
- java.awt.event.ActionListener
- javax.swing.event.ChangeListener
JDK 1.8 新增加的函數接口: - java.util.function
java.util.function 它包含了很多類,用來支持 Java的 函數式編程,該包中的函數式接口有:
接口名 | 參數 | 返回值 | 用途 |
---|---|---|---|
Predicate | T | boolean | 斷言 |
Consumer | T | void | 消費 |
Function<T,R> | T | R | 函數 |
Supplier | None | T | 工廠方法 |
UnaryOperator | T | T | 邏輯非 |
BinaryOperator | (T,T) | T | 二元操作 |
函數式接口各類介紹:
接口 | 描述 |
---|---|
BiConsumer<T,U> | 代表了一個接受兩個輸入參數的操作,並且不返回任何結果 |
BiFunction<T,U,R> | 代表了一個接受兩個輸入參數的方法,並且返回一個結果 |
BinaryOperator<T> | 代表了一個作用於於兩個同類型操作符的操作,並且返回了操作符同類型的結果 |
BiPredicate<T,U> | 代表了一個兩個參數的boolean值方法 |
BooleanSupplier | 代表了boolean值結果的提供方 |
Consumer<T> | 代表了接受一個輸入參數並且無返回的操作 |
DoubleBinaryOperator | 代表了作用於兩個double值操作符的操作,並且返回了一個double值的結果 |
DoubleConsumer | 代表一個接受double值參數的操作,並且不返回結果 |
DoubleFunction<R> | 代表接受一個double值參數的方法,並且返回結果 |
DoublePredicate | 代表一個擁有double值參數的boolean值方法 |
DoubleSupplier | 代表一個double值結構的提供方 |
DoubleToIntFunction | 接受一個double類型輸入,返回一個int類型結果 |
DoubleToLongFunction | 接受一個double類型輸入,返回一個long類型結果 |
DoubleUnaryOperator | 接受一個參數同為類型double,返回值類型也為double |
Function<T,R> | 接受一個輸入參數,返回一個結果 |
IntBinaryOperator | 接受兩個參數同為類型int,返回值類型也為int |
IntConsumer | 接受一個int類型的輸入參數,無返回值 |
IntFunction<R> | 接受一個int類型輸入參數,返回一個結果 |
IntPredicate | 接受一個int輸入參數,返回一個布爾值的結果 |
IntSupplier | 無參數,返回一個int類型結果 |
IntToDoubleFunction | 接受一個int類型輸入,返回一個double類型結果 |
IntToLongFunction | 接受一個int類型輸入,返回一個long類型結果 |
IntUnaryOperator | 接受一個參數同為類型int,返回值類型也為int |
LongBinaryOperator | 接受兩個參數同為類型long,返回值類型也為long |
LongConsumer | 接受一個long類型的輸入參數,無返回值 |
LongFunction<R> | 接受一個long類型輸入參數,返回一個結果 |
LongPredicate | R接受一個long輸入參數,返回一個布爾值類型結果 |
LongSupplier | 無參數,返回一個結果long類型的值 |
LongToDoubleFunction | 接受一個long類型輸入,返回一個double類型結果 |
LongToIntFunction | 接受一個long類型輸入,返回一個int類型結果 |
LongUnaryOperator | 接受一個參數同為類型long,返回值類型也為long |
ObjDoubleConsumer<T> | 接受一個object類型和一個double類型的輸入參數,無返回值 |
ObjIntConsumer<T> | 接受一個object類型和一個int類型的輸入參數,無返回值 |
ObjLongConsumer<T> | 接受一個object類型和一個long類型的輸入參數,無返回值。 |
Predicate<T> | 接受一個輸入參數,返回一個布爾值結果 |
Supplier<T> | 無參數,返回一個結果 |
ToDoubleBiFunction<T,U> | 接受兩個輸入參數,返回一個double類型結果 |
ToDoubleFunction<T> | 接受一個輸入參數,返回一個double類型結果 |
ToIntBiFunction<T,U> | 接受兩個輸入參數,返回一個int類型結果 |
ToIntFunction<T> | 接受一個輸入參數,返回一個int類型結果 |
ToLongBiFunction<T,U> | 接受兩個輸入參數,返回一個long類型結果 |
ToLongFunction<T> | 接受一個輸入參數,返回一個long類型結果 |
UnaryOperator<T> | 接受一個參數為類型T,返回值類型也為T |
函數式接口實例
@FunctionalInterface
這個標注用於表示該接口會設計成一個函數式接口。如果你用 @FunctionalInterface 定義了一個接口,而它卻不是函數式接口的話,編譯器將返回一個提示原因的錯誤。例如,錯誤消息可能是“Multiple non-overriding abstract methods found in interface Foo” , 表明存在多個抽象方法。 請注意, @FunctionalInterface 不是必需的, 但對於為此設計的接口而言, 使用它是比較好的做法。 它就像是 @Override標注表示方法被重寫了。
// 定義函數式接口 @FunctionalInterface public interface BufferedReaderProcessor { String process(BufferedReader b) throws IOException; } // 定義方法 public static String processFile(BufferedReaderProcessor p) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) { return p.process(br); } } // 調用 String result = processFile(br -> br.readLine() + br.readLine());
Predicate
Predicate <T> 接口是一個函數式接口,它接受一個輸入參數 T,返回一個布爾值結果。
該接口包含多種默認方法來將Predicate組合成其他復雜的邏輯(比如:與,或,非)。
該接口用於測試對象是 true 或 false。
與:predicate.and()
或:predicate.or()
非:predicate.negate()
a.or(b).and(c) 可以看作 (a || b) && c
我們可以通過以下實例(Java8Tester.java)來了解函數式接口 Predicate <T> 的使用:
public class Java8Tester { public static void main(String args[]) { List < Integer > list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); // Predicate<Integer> predicate = n -> true // n 是一個參數傳遞到 Predicate 接口的 test 方法 // n 如果存在則 test 方法返回 true System.out.println("輸出所有數據:"); // 傳遞參數 n eval(list, n -> true); // Predicate<Integer> predicate1 = n -> n%2 == 0 // n 是一個參數傳遞到 Predicate 接口的 test 方法 // 如果 n%2 為 0 test 方法返回 true System.out.println("輸出所有偶數:"); eval(list, n -> n % 2 == 0); // Predicate<Integer> predicate2 = n -> n > 3 // n 是一個參數傳遞到 Predicate 接口的 test 方法 // 如果 n 大於 3 test 方法返回 true System.out.println("輸出大於 3 的所有數字:"); eval(list, n -> n > 3); } public static void eval(List < Integer > list, Predicate < Integer > predicate) { for (Integer n: list) { if (predicate.test(n)) { System.out.println(n + " "); } } } }
Consumer
java.util.function.Consumer<T> 定義了一個名叫 accept 的抽象方法,它接受泛型 T 的對象,沒有返回( void ) 。你如果需要訪問類型 T 的對象,並對其執行某些操作,就可以使用這個接口。
public static <T> void forEach(List <T> list, Consumer <T> c) { for (T i: list) { c.accept(i); } } forEach(Arrays.asList(1, 2, 3, 4, 5), System.out::println);
Function
java.util.function.Function<T, R> 接口定義了一個叫作 apply 的方法,它接受一個泛型 T 的對象,並返回一個泛型 R 的對象。
public static <T, R> List <R> map(List <T> list, Function <T, R> f) { List <R> result = new ArrayList < > (); for (T s: list) { result.add(f.apply(s)); } return result; } // [7, 2, 6] List <Integer> l = map(Arrays.asList("lambdas", "in", "action"), String::length);
Function 接口所代表的Lambda表達式復合起來。 Function 接口為此配了 andThen 和 compose 兩個默認方法,它們都會返回 Function 的一個實例。
andThen 方法會返回一個函數,它先對輸入應用一個給定函數,再對輸出應用另一個函數。
比如,假設有一個函數 f 給數字加1 (x -> x + 1) ,另一個函數 g 給數字乘2,你可以將它們組合成一個函數 h ,先給數字加1,再給結果乘2:
Function <Integer, Integer> f = x -> x + 1; Function <Integer, Integer> g = x -> x * 2; Function <Integer, Integer> h = f.andThen(g); int result = h.apply(1); // 4
數學上會寫作 g(f(x)) 或(g o f)(x)
compose 方法
Function < Integer, Integer > f = x -> x + 1; Function < Integer, Integer > g = x -> x * 2; Function < Integer, Integer > h = f.compose(g); int result = h.apply(1); // 3
數學上會寫作 f(g(x)) 或 (f o g)(x)
Stream
Stream主要用於操作集合,更方便的去處理數據。
java.util.stream.Stream 中的 Stream 接口定義了許多操作。它們可以分為兩大類:可以連接起來的流操作稱為中間操作,關閉流的操作稱為終端操作。可以理解為有返回值是Stream的方法是中間操作,返回值非Stream的方法是終端操作。
中間操作會返回另一個流,但是如果沒有終端操作,中間操作不會執行任何處理。
Stream調用了終端操作之后,如果再調用,拋出以下異常:
java.lang.IllegalStateException: stream has already been operated upon or closed
Stream只能被消費一次!!!
外部迭代與內部迭代
使用 Collection 接口需要用戶去做迭代(比如用 for-each ) ,這稱為外部迭代。 相反,Streams庫使用內部迭代。
外部迭代:外部迭代實際是使用Iterator對象。
開發中如何選擇兩種迭代方式:
- 如果循環體需要引用外部變量,或者需要拋Checked Exception,並不可try catch的情況下,推薦使用外部迭代,否則,隨意。
- 如果對循環結果無順序要求,循環之間沒有使用操作共同數據,並對執行效率有要求,可以使用
內部迭代-parallelStream。 - 如果需要對集合數據進行處理、分組、過濾等操作,可以使用內部迭代-stream。
流的使用一般包括三件事
- 一個數據源(如集合)來執行一個查詢;
- 一個中間操作鏈,形成一條流的流水線;
- 一個終端操作,執行流水線,並能生成結果。
篩選(filter)
該方法會接受一個謂詞(Predicate)(一個返回boolean 的函數)作為參數,並返回一個包括所有符合謂詞(Predicate)的元素的流。
List <Dish> vegetarianMenu = menu.stream().filter(Dish::isVegetarian).collect(toList());
切片(limit)
該方法會返回一個不超過給定長度的流。所需的長度作為參數傳遞給 limit 。如果流是有序的,則最多會返回前 n 個元素。如果流是無序的,limit的結果不會以任務順序排列。
List <Dish> dishes = menu.stream().filter(d -> d.getCalories() > 300).limit(3).collect(toList());
去重(distinct )
該方法會返回一個元素各異(根據流所生成元素的hashCode 和equals 方法實現)的流。
List <Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4); numbers.stream().filter(i -> i % 2 == 0).distinct() .forEach(System.out::println);
跳過元素(skip)
返回一個扔掉了前 n 個元素的流。如果流中元素不足 n 個,則返回一個空流。請注意, limit(n) 和 skip(n) 是互補的!
List <Dish> dishes = menu.stream().filter(d -> d.getCalories() > 300).skip(2).collect(toList());
映射(map和flatMap)
map:
一個非常常見的數據處理套路就是從某些對象中選擇信息。比如在SQL里,你可以從表中選擇一列。
可以改變原有流的類型,重新返回一個新的類型集合。
List <String> dishNames = menu.stream().map(Dish::getName).collect(toList());
flatMap:
一個用於把 Stream<String[]> 轉換成Stream<String> 的操作方法,將流扁平化。
Arrays.stream() 的方法可以接受一個數組並產生一個流。
如果需要把一個String[] arrayOfWords = {"Hello", "World"};轉換成[G, o, o, d, b, y, e, W, o, r, l, d]
String[] arrayOfWords = {"Hello", "World"}; Stream <String> streamOfwords = Arrays.stream(arrayOfWords); streamOfwords.map(word -> word.split("")).flatMap(Arrays::stream).collect(Collectors.toList());
匹配( allMatch和anyMatch和noneMatch )
檢查是否至少匹配一個元素(anyMatch ):
boolean isVegetarian = menu.stream().anyMatch(Dish::isVegetarian);
檢查是否匹配所有元素(allMatch):
boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);
檢查是否所有元素不匹配(noneMatch ):
boolean isHealthy = menu.stream().noneMatch(d -> d.getCalories() >= 1000);
查找(findFirst和findAny)
返回當前流中的任意元素(findAny):
Optional <Dish> dish = menu.stream().filter(Dish::isVegetarian).findAny();
獲取集合中第一個元素(findFirst):
Optional <Dish> dish = menu.stream().filter(Dish::isVegetarian).findFirst();
何時使用 findFirst 和 findAny
你可能會想,為什么會同時有 findFirst 和 findAny 呢?答案是並行。找到第一個元素在並行上限制更多。如果你不關心返回的元素是哪個,請使用 findAny ,因為它在使用並行流時限制較少。
歸約(reduce)
reduce 接受兩個參數:
- 一個初始值,這里是0;
- 一個 BinaryOperator<T> 來將兩個元素結合起來產生一個新值,這里我們用的是lambda (a, b) -> a + b 。
求和
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
等同於
int sum = numbers.stream().mapToInt(n -> n).sum();
最大值
Optional <Integer> max = numbers.stream().reduce(Integer::max);
等同於
int max = numbers.stream().mapToInt(n -> n).max();
最小值
Optional <Integer> min = numbers.stream().reduce(Integer::min);
等同於
int min = numbers.stream().mapToInt(n -> n).min();
執行原理:
0 作為Lambda( a )的第一個參數,從流中獲得 4 作為第二個參數( b ) 。 0 + 4 得到 4 ,它成了新的累積值。然后再用累積值和流中下一個元素 5 調用Lambda,產生新的累積值 9 。接下來,再用累積值和下一個元素 3調用Lambda,得到 12 。最后,用 12 和流中最后一個元素 9 調Lambda,得到最終結果 21 。
ps:reduce如果不設置初始值,會返回一個 Optional 對象。
流操作:無狀態和有狀態
無狀態:操作集合數據時,每一個元素之間數據不相互影響,如map或者filter等操作。
有狀態:操作集合數據時,元素之間數據有影響,如sort或者distinct等操作,需要知道每個元素值才能執行處理。
操 作 | 類 型 | 返回類型 | 使用的類型/函數式接口 | 函數描述符 |
---|---|---|---|---|
filter | 中間 | Stream<T> | Predicate<T> | T -> boolean |
distinct | 中間 (有狀態-無界) | Stream<T> | ||
skip | 中間 (有狀態-有界) | Stream<T> | long | |
limit | 中間 (有狀態-有界) | Stream<T> | long | |
map | 中間 | Stream<T> | Function<T, R> | T -> R |
flatMap | 中間 | Stream<T> | Function<T, Stream<R>> | T -> Stream<R> |
sorted | 中間 (有狀態-無界) | Stream<T> | Comparator<T> | (T, T) -> int |
anyMatch | 終端 | boolean | Predicate<T> | T -> boolean |
noneMatch | 終端 | boolean | Predicate<T> | T -> boolean |
allMatch | 終端 | boolean | Predicate<T> | T -> boolean |
findAny | 終端 | Optional<T> | ||
findFirst | 終端 | Optional<T> | ||
forEach | 終端 | void | Consumer<T> | T -> void |
collect | 終端 | R | Collector<T, A, R> | |
reduce | 終端 (有狀態-有界) | Optional<T> | BinaryOperator<T> | (T, T) -> T |
count | 終端 | long |
原始類型流轉換
IntStream 、 DoubleStream 和 LongStream ,分別將流中的元素特化為 int 、 long 和 double,從而避免了暗含的裝箱成本。轉換的原因並不在於流的復雜性,而是裝箱造成的復雜性——即類似 int 和 Integer 之間的效率差異。將流轉換為轉換的常用方法是 mapToInt 、 mapToDouble 和 mapToLong 。
轉換回對象流:
要把原始流轉換成一般流(每個 int 都會裝箱成一個Integer ) ,可以使用 boxed 方法。
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream <Integer> stream = intStream.boxed();
默認值 OptionalInt:
Optional 可以用Integer 、 String 等參考類型來參數化。對於三種原始流特化,也分別有一個 Optional 原始類型特化版本: OptionalInt 、 OptionalDouble 和 OptionalLong 。
OptionalInt maxCalories = menu.stream().mapToInt(Dish::getCalories).max();
int max = maxCalories.orElse(1);
數值范圍(range 和 rangeClosed):
range 和 rangeClosed這兩個方法都是第一個參數接受起始值,第二個參數接受結束值。但
range 是不包含結束值的,而 rangeClosed 則包含結束值。
由值創建流(Stream.of):
Stream.of 通過顯式值創建一個流,它可以接受任意數量的參數。
Stream <String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
也可以創建一個空流:
Stream <String> emptyStream = Stream.empty();
由數組創建流(Arrays.stream):
Arrays.stream 從數組創建一個流,它接受一個數組作為參數。
int[] numbers = {2, 3, 5, 7, 11, 13}; int sum = Arrays.stream(numbers).sum();
由文件生成流:
Java中用於處理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。
java.nio.file.Files 中的很多靜態方法都會返回一個流。例如,一個很有用的方法是Files.lines ,它會返回一個由指定文件中的各行構成的字符串流。
long uniqueWords = 0; try (Stream <String> lines =Files.lines(Paths.get("data.txt"), Charset.defaultCharset())) { uniqueWords = lines.flatMap(line - > Arrays.stream(line.split(" "))) .distinct().count(); } catch (IOException e) {}
由函數生成流:創建無限流:
Stream API提供了兩個靜態方法來從函數生成流: Stream.iterate 和 Stream.generate 。這兩個操作可以創建所謂的無限流:不像從固定集合創建的流那樣有固定大小的流。由 iterate和 generate 產生的流會用給定的函數按需創建值,因此可以無窮無盡地計算下去!一般來說,應該使用 limit(n) 來對這種流加以限制,以避免打印無窮多個值。
Stream.iterate:
Stream.iterate(0, n - > n + 2).limit(10).forEach(System.out::println);
Stream.generate:
Stream.generate(Math::random).limit(5).forEach(System.out::println);
收集器
匯總(Collectors.counting 、 Collectors.summingInt、 Collectors.summingLong 、Collectors.summingDouble、 Collectors.averagingInt、 Collectors.averagingLong 、Collectors.averagingDouble、Collectors.summarizingInt、Collectors.summarizingLong、Collectors.summarizingDouble):
總數:
Collectors.counting:
long howManyDishes = menu.stream().collect(Collectors.counting());
等同於
long howManyDishes = menu.stream().count();
求和:
Collectors.summingInt:
int totalCalories = menu.stream().collect(Collectors.summingInt(Dish::getCalories));
等同於
int sum = menu.stream().mapToInt(Dish::getCalories).sum();
Collectors.summingLong:
long totalCalories = menu.stream().collect(Collectors.summingLong(Dish::getCalories));
等同於
long sum = menu.stream().mapToLong(Dish::getCalories).sum();
Collectors.summingDouble:
double totalCalories = menu.stream().collect(Collectors.summingDouble(Dish::getCalories));
等同於
double sum = menu.stream().mapToDouble(Dish::getCalories).sum();
平均數:
Collectors.averagingInt:
int avgCalories = menu.stream().collect(Collectors.averagingInt(Dish::getCalories));
Collectors.averagingLong:
long avgCalories = menu.stream().collect(Collectors.averagingLong(Dish::getCalories));
Collectors.averagingDouble:
double avgCalories = menu.stream().collect(Collectors.averagingDouble(Dish::getCalories));
匯總(總和、平均值、最大值和最小值):
Collectors.summarizingInt:
IntSummaryStatistics menuStatistics = menu.stream().collect(Collectors.summarizingInt(Dish::getCalories));
Collectors.summarizingLong:
LongSummaryStatistics menuStatistics = menu.stream().collect(Collectors.summarizingLong(Dish::getCalories));
Collectors.summarizingDouble:
DoubleSummaryStatistics menuStatistics = menu.stream().collect(Collectors.summarizingDouble(Dish::getCalories));
查找流中的最大值和最小值(Collectors.maxBy 和 Collectors.minBy):
Collectors.maxBy :
Comparator <Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories); Optional <Dish> mostCalorieDish = menu.stream().collect(Collectors.maxBy(dishCaloriesComparator));
連接字符串(Collectors.joining):
joining 工廠方法返回的收集器會把對流中每一個對象應用 toString 方法得到的所有字符串連接成一個字符串。 joining 在內部使用了 StringBuilder 來把生成的字符串逐個追加起來。 joining 工廠方法有一個重載版本可以接受元素之間的分界符。
String shortMenu = menu.stream().map(Dish::getName).collect(Collectors.joining());
分隔符:
String shortMenu = menu.stream().map(Dish::getName).collect(Collectors.joining(”, “));
分組(Collectors.groupingBy):
普通的單參數 groupingBy(f) (其中 f 是分類函數)實際上是 groupingBy(f, toList()) 的簡便寫法。
第一個參數是指定以什么分組
第二個參數是指定使用的Map
第三個參數是指定Collector
Map <Dish.Type, List <Dish>> dishesByType = menu.stream().collect(Collectors.groupingBy(Dish::getType));
多級分組:
Map<Dish.Type, Map<Long, List<Dish>>> dishesByTypeCaloricLevel = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.groupingBy(Dish::getCalories)));
分組匯總:
Map <Dish.Type, Long> typesCount = menu.stream().collect(Collectors.groupingBy(Dish::getType, counting()));
分組匯總最大值:
Map Dish.Type, Optional <Dish>> mostCaloricByType = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.maxBy(Comparator.comparingInt(Dish::getCalories))));
分組結果包裝Optional轉換具體值(Collectors.collectingAndThen)
Map <Dish.Type, Dish> mostCaloricByType = menu.stream().collect(Collectors.groupingBy(Dish::getType,Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)), Optional::get)));
分組類型轉換(Collectors.mapping):
這個方法接受兩個參數:一個函數對流中的元素做變換,另一個則將變換的結果對象收集起來。
Map <Dish.Type, Set <CaloricLevel>> caloricLevelsByType = menu.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.mapping( dish - > { if (dish.getCalories() <= 400) return CaloricLevel.DIET; else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL; else return CaloricLevel.FAT; }, Collectors.toSet())));
分區(Collectors.partitioningBy)
分區是分組的特殊情況:由一個謂詞(返回一個布爾值的函數)作為分類函數,它稱分區函數。分區函數返回一個布爾值,這意味着得到的分組 Map 的鍵類型是 Boolean ,於是它最多可以分為兩組—— true 是一組, false 是一組。
Map <Boolean, List <Dish>> partitionedMenu = menu.stream().collect(Collectors.partitioningBy(Dish::isVegetarian));
排序(Comparator )
Comparator 接口現在同時包含了默認方法和靜態方法。可以使用靜態方法 Comparator.comparing 返回一個 Comparator 對象,該對象提供了一個函數可以提取排序關鍵字。
- reversed —— 對當前的 Comparator 對象進行逆序排序,並返回排序之后新的Comparator 對象。
- thenComparing —— 當兩個對象相同時,返回使用另一個 Comparator 進行比較的Comparator 對象。
- thenComparingInt 、 thenComparingDouble 、 thenComparingLong —— 這些方法的工作方式和 thenComparing 方法類似,不過它們的處理函數是特別針對某些基本數據類型(分別對應於 ToIntFunction 、 ToDoubleFunction 和 ToLongFunction )的。
- comparingInt 、 comparingDouble 、 comparingLong —— 它們的工作方式和 comparing 類似,但接受的函數特別針對某些基本數據類型(分別對應於 ToIntFunction 、ToDoubleFunction 和 ToLongFunction ) 。
- naturalOrder —— 對 Comparable 對象進行自然排序,返回一個 Comparator 對象。
- nullsFirst 、 nullsLast —— 對空對象和非空對象進行比較,你可以指定空對象(null)比非空對象(non-null)小或者比非空對象大,返回值是一個 Comparator 對象。
- reverseOrder —— 倒序,和 naturalOrder().reversed() 方法類似。
並行流
使用並行流可以通過parallelStream或者parallel方法。對順序流調用 parallel 方法並不意味着流本身有任何實際的變化。它在內部實際上就是設了一個 boolean 標志,表示你想讓調用 parallel 之后進行的所有操作都並行執行。並行流轉換成順序流使用sequential方法。
並行化並不是沒有代價的。並行化過程本身需要對流做遞歸划分,把每個子流的歸納操作分配到不同的線程,然后把這些操作的結果合並成一個值。但在多個內核之間移動數據的代價也可能比你想的要大, 所以很重要的一點是要保證在內核中並行執行工作的時間比在內核之間傳輸數據的時間長。總而言之,很多情況下不可能或不方便並行化。然而,在使用並行 Stream 加速代碼之前,你必須確保用得對;如果結果錯了,算得快就毫無意義了。讓我們來看一個常見的陷阱。
分支/合並框架
分支/合並框架的目的是以遞歸方式將可以並行的任務拆分成更小的任務,然后將每個子任務的結果合並起來生成整體結果。它是 ExecutorService 接口的一個實現,它把子任務分配給線程池(稱為 ForkJoinPool )中的工作線程。首先來看看如何定義任務和子任務。
RecursiveTask:
要把任務提交到這個池, 必須創建 RecursiveTask<R> 的一個子類, 其中 R 是並行化任務 (以及所有子任務)產生的結果類型,或者如果任務不返回結果,則是 RecursiveAction 類型(當然它可能會更新其他非局部機構) 。要定義 RecursiveTask, 只需實現它唯一的抽象方法compute :
protected abstract R compute();
這個方法同時定義了將任務拆分成子任務的邏輯,以及無法再拆分或不方便再拆分時,生成單個子任務結果的邏輯。正由於此,這個方法的實現類似於下面的偽代碼:
if (任務足夠小或不可分) { 順序計算該任務 } else { 將任務分成兩個子任務 遞歸調用本方法,拆分每個子任務,等待所有子任務完成 合並每個子任務的結果 }
使用分支/合並框架的最佳做法:
雖然分支/合並框架還算簡單易用,不幸的是它也很容易被誤用。以下是幾個有效使用它的最佳做法。
- 對一個任務調用 join 方法會阻塞調用方,直到該任務做出結果。因此,有必要在兩個子任務的計算都開始之后再調用它。否則,你得到的版本會比原始的順序算法更慢更復雜,因為每個子任務都必須等待另一個子任務完成才能啟動。
- 不應該在 RecursiveTask 內部使用 ForkJoinPool 的 invoke 方法。相反,你應該始終直接調用 compute 或 fork 方法,只有順序代碼才應該用 invoke 來啟動並行計算。
- 對子任務調用 fork 方法可以把它排進 ForkJoinPool 。同時對左邊和右邊的子任務調用它似乎很自然,但這樣做的效率要比直接對其中一個調用 compute 低。這樣做你可以為其中一個子任務重用同一線程,從而避免在線程池中多分配一個任務造成的開銷。
- 調試使用分支/合並框架的並行計算可能有點棘手。特別是你平常都在你喜歡的IDE里面看棧跟蹤(stack trace)來找問題,但放在分支合並計算上就不行了,因為調用 compute的線程並不是概念上的調用方,后者是調用 fork 的那個。
- 和並行流一樣,你不應理所當然地認為在多核處理器上使用分支/合並框架就比順序計算快。我們已經說過,一個任務可以分解成多個獨立的子任務,才能讓性能在並行化時有所提升。所有這些子任務的運行時間都應該比分出新任務所花的時間長;一個慣用方法是把輸入/輸出放在一個子任務里,計算放在另一個里,這樣計算就可以和輸入/輸出同時進行。此外,在比較同一算法的順序和並行版本的性能時還有別的因素要考慮。就像任何其他Java代碼一樣,分支/合並框架需要“預熱”或者說要執行幾遍才會被JIT編譯器優化。這就是為什么在測量性能之前跑幾遍程序很重要,我們的測試框架就是這么做的。同時還要知道,編譯器內置的優化可能會為順序版本帶來一些優勢(例如執行死碼分析——刪去從未被使用的計算) 。
Spliterator:
Spliterator 是Java 8中加入的另一個新接口;這個名字代表“可分迭代器” (splitable iterator) 。和 Iterator 一樣, Spliterator 也用於遍歷數據源中的元素,但它是為了並行執行而設計的。雖然在實踐中可能用不着自己開發 Spliterator ,但了解一下它的實現方式會讓你對並行流的工作原理有更深入的了解。Java 8已經為集合框架中包含的所有數據結構提供了一個默認的 Spliterator 實現。 集合實現了 Spliterator 接口, 接口提供了一個 spliterator 方法。
public interface Spliterator<T> { boolean tryAdvance(Consumer<? super T> action); Spliterator<T> trySplit(); long estimateSize(); int characteristics(); }
與往常一樣, T 是 Spliterator 遍歷的元素的類型。 tryAdvance 方法的行為類似於普通的Iterator ,因為它會按順序一個一個使用 Spliterator 中的元素,並且如果還有其他元素要遍歷就返回 true 。 但 trySplit 是專為 Spliterator 接口設計的, 因為它可以把一些元素划出去分給第二個 Spliterator (由該方法返回) ,讓它們兩個並行處理。 Spliterator 還可通過estimateSize 方法估計還剩下多少元素要遍歷,因為即使不那么確切,能快速算出來是一個值也有助於讓拆分均勻一點。
流拆分過程:
將 Stream 拆分成多個部分的算法是一個遞歸過程,第一步是對第一個Spliterator 調用 trySplit ,生成第二個 Spliterator 。第二步對這兩個 Spliterator 調用trySplit ,這樣總共就有了四個 Spliterator 。這個框架不斷對 Spliterator 調用 trySplit直到它返回 null ,表明它處理的數據結構不能再分割,最后,這個遞歸拆分過程到第四步就終止了,這時所有的 Spliterator 在調用 trySplit 時都返回了 null 。

這個拆分過程也受 Spliterator 本身的特性影響,而特性是通過 characteristics 方法聲明的,它將返回一個 int ,代表 Spliterator 本身特性集的編碼。使用 Spliterator 的客戶可以用這些特性來更好地控制和優化它的使用。
異步編程
Future
Future 接口在Java 5中被引入,設計初衷是對將來某個時刻會發生的結果進行建模。它建模了一種異步計算,返回一個執行運算結果的引用,當運算結束后,這個引用被返回給調用方。在Future 中觸發那些潛在耗時的操作把調用線程解放出來,讓它能繼續執行其他有價值的工作,不再需要呆呆等待耗時的操作完成。
ExecutorService executor = Executors.newCachedThreadPool();
Future < Double > future = executor.submit(new Callable <Double> () { public Double call() { return doSomeLongComputation(); } }); doSomethingElse(); try { Double result = future.get(1, TimeUnit.SECONDS); } catch (ExecutionException ee) { // 計算拋出一個異常 } catch (InterruptedException ie) { // 當前線程在等待過程中被中斷 } catch (TimeoutException te) { // 在Future對象完成之前超過已過期 }
這種編程方式讓你的線程可以在 ExecutorService 以並發方式調用另一個線程執行耗時操作的同時,去執行一些其他的任務。
局限性:
Future 接口提供了方法來檢測異步計算是否已經結束(使用isDone 方法) ,等待異步操作結束,以及獲取計算的結果。
- 將兩個異步計算合並為一個——這兩個異步計算之間相互獨立,同時第二個又依賴於第一個的結果。
- 等待 Future 集合中的所有任務都完成。
- 僅等待 Future 集合中最快結束的任務完成(有可能因為它們試圖通過不同的方式計算同一個值) ,並返回它的結果。
- 通過編程方式完成一個 Future 任務的執行(即以手工設定異步操作結果的方式) 。
- 應對 Future 的完成事件(即當 Future 的完成事件發生時會收到通知,並能使用 Future計算的結果進行下一步的操作,不只是簡單地阻塞等待操作的結果) 。
CompletableFuture
CompletableFuture 的 completeExceptionally 方法將導致 CompletableFuture 內發生問題的異常拋出。客戶端現在會收到一個 ExecutionException 異常,該異常接收了一個包含失敗原因的Exception 參數。
使用工廠方法 supplyAsync 創建 CompletableFuture:
public Future <Double> getPriceAsync(String product) { return CompletableFuture.supplyAsync(() -> calculatePrice(product)); }
supplyAsync 方法接受一個生產者( Supplier )作為參數,返回一個 CompletableFuture對象, 該對象完成異步執行后會讀取調用生產者方法的返回值。 生產者方法會交由 ForkJoinPool池中的某個執行線程( Executor )運行,但是你也可以使用 supplyAsync 方法的重載版本,傳遞第二個參數指定不同的執行線程執行生產者方法。一般而言,向 CompletableFuture 的工廠方法傳遞可選參數,指定生產者方法的執行線程是可行的。
CompletableFuture和stream組合使用:
public List <String> findPrices(String product) { List < CompletableFuture <String>> priceFutures = shops.stream() .map(shop -> CompletableFuture.supplyAsync(() - > shop.getName() + " price is " + shop.getPrice(product))) .collect(Collectors.toList()); return priceFutures.stream() .map(CompletableFuture::join) .collect(Collectors.toList()); }
利用 CompletableFutures 向其提交任務執行是個不錯的主意。處理需大量使用異步操作的情況時,這幾乎是最有效的策略。
構造同步和異步操作:
public List <String> findPrices(String product) { List <CompletableFuture <String>> priceFutures = shops.stream() .map(shop -> CompletableFuture.supplyAsync( () -> shop.getPrice(product), executor)) .map(future -> future.thenApply(Quote::parse)) .map(future -> future.thenCompose(quote -> CompletableFuture.supplyAsync( () -> Discount.applyDiscount(quote), executor))) .collect(Collectors.toList()); return priceFutures.stream() .map(CompletableFuture::join) .collect(Collectors.toList()); }

Java 8的 CompletableFuture API提供了名為 thenCompose 的方法,它就是專門為這一目的而設計的, thenCompose 方法允許你對兩個異步操作進行流水線,第一個操作完成時,將其結果作為參數傳遞給第二個操作。創建兩個 CompletableFutures 對象,對第一個 CompletableFuture 對象調用 thenCompose ,並向其傳遞一個函數。當第一個CompletableFuture 執行完畢后,它的結果將作為該函數的參數,這個函數的返回值是以第一個 CompletableFuture 的返回做輸入計算出的第二個 CompletableFuture 對象。thenCompose 方法像 CompletableFuture 類中的其他方法一樣,也提供了一個以 Async 后綴結尾的版本 thenComposeAsync 。通常而言,名稱中不帶 Async的方法和它的前一個任務一樣,在同一個線程中運行;而名稱以 Async 結尾的方法會將后續的任務提交到一個線程池,所以每個任務是由不同的線程處理的。
方法名 | 描述 |
---|---|
allOf(CompletableFuture<?>... cfs) | 等待所有任務完成,構造后CompletableFuture完成 |
anyOf(CompletableFuture<?>... cfs) | 只要有一個任務完成,構造后CompletableFuture就完成 |
runAsync(Runnable runnable) | 使用ForkJoinPool.commonPool()作為它的線程池執行異步代碼 |
runAsync(Runnable runnable, Executor executor) | 使用指定的thread pool執行異步代碼 |
supplyAsync(Supplier<U> supplier) | 使用ForkJoinPool.commonPool()作為它的線程池執行異步代碼,異步操作有返回值 |
supplyAsync(Supplier<U> supplier,Executor executor) | 使用指定的thread pool執行異步代碼,異步操作有返回值 |
complete(T t) | 完成異步執行,並返回future的結果 |
completeExceptionlly(Throwable ex) | 異步執行不正常的結束 |
cancel(boolean mayInterruptIfRunning) | 取消任務的執行。參數指定是否立即中斷任務執行,或者等等任務結束 |
isCancelled() | 任務是否已經取消,任務正常完成前將其取消,則返回 true |
isDone() | 任務是否已經完成。需要注意的是如果任務正常終止、異常或取消,都將返回true |
get() | throws InterruptedException, ExecutionException 等待任務執行結束,然后獲得V類型的結果。InterruptedException 線程被中斷異常, ExecutionException任務執行異常,如果任務被取消,還會拋出CancellationException |
get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException | 同上面的get功能一樣,多了設置超時時間。參數timeout指定超時時間,uint指定時間的單位,在枚舉類TimeUnit中有相關的定義。如果計 算超時,將拋出TimeoutException |
thenApply(Function<? super T,? extends U> fn) | 轉換一個新的CompletableFuture對象 |
thenApplyAsync(Function<? super T,? extends U> fn) | 異步轉換一個新的CompletableFuture對象 |
thenApplyAsync(Function<? super T,? extends U> fn, Executor executor) | 使用指定的thread pool執行異步代碼,異步轉換一個新的CompletableFuture對象 |
thenCompose(Function<? super T, ? extends CompletionStage<U>> fn) | 在異步操作完成的時候對異步操作的結果進行一些操作,並且仍然返回CompletableFuture類型 |
thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn) | 在異步操作完成的時候對異步操作的結果進行一些操作,並且仍然返回CompletableFuture類型。使用ForkJoinPool |
thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn, Executor executor) | 在異步操作完成的時候對異步操作的結果進行一些操作,並且仍然返回CompletableFuture類型。使用指定的線程池 |
thenAccept(Consumer<? super T> action) | 當CompletableFuture完成計算結果,只對結果執行Action,而不返回新的計算值 |
thenAcceptAsync(Consumer<? super T> action) | 當CompletableFuture完成計算結果,只對結果執行Action,而不返回新的計算值,使用ForkJoinPool |
thenAcceptAsync(Consumer<? super T> action,Executor executor) | 當CompletableFuture完成計算結果,只對結果執行Action,而不返回新的計算值 |
thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn) | 當兩個CompletableFuture都正常完成后,執行提供的fn,用它來組合另外一個CompletableFuture的結果 |
thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn) | 當兩個CompletableFuture都正常完成后,執行提供的fn,用它來組合另外一個CompletableFuture的結果,使用ForkJoinPool |
thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn, Executor executor) | 當兩個CompletableFuture都正常完成后,執行提供的fn,用它來組合另外一個CompletableFuture的結果,使用指定的線程池 |
thenAcceptBoth(CompletionStage<? extends U> other, BiConsumer<? super T,? super U> action) | 當兩個CompletableFuture都正常完成后,執行提供的action,用它來組合另外一個CompletableFuture的結果 |
thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T,? super U> action) | 當兩個CompletableFuture都正常完成后,執行提供的action,用它來組合另外一個CompletableFuture的結果,使用ForkJoinPool |
thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T,? super U> action, Executor executor) | 當兩個CompletableFuture都正常完成后,執行提供的action,用它來組合另外一個CompletableFuture的結果,使用指定的線程池 |
whenComplete(BiConsumer<? super T,? super Throwable> action) | 當CompletableFuture完成計算結果時對結果進行處理,或者當CompletableFuture產生異常的時候對異常進行處理 |
whenCompleteAsync(BiConsumer<? super T,? super Throwable> action) | 當CompletableFuture完成計算結果時對結果進行處理,或者當CompletableFuture產生異常的時候對異常進行處理,使用ForkJoinPool |
whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor) | 當CompletableFuture完成計算結果時對結果進行處理,或者當CompletableFuture產生異常的時候對異常進行處理,使用指定的線程池。 |
handle(BiFunction<? super T, Throwable, ? extends U> fn) | 當CompletableFuture完成計算結果或者拋出異常的時候,執行提供的fn |
handleAsync(BiFunction<? super T, Throwable, ? extends U> fn) | 當CompletableFuture完成計算結果或者拋出異常的時候,執行提供的fn,使用ForkJoinPool |
handleAsync(BiFunction<? super T, Throwable, ? extends U> fn, Executor executor) | 當CompletableFuture完成計算結果或者拋出異常的時候,執行提供的fn,使用指定的線程池 |
- thenApply的功能相當於將CompletableFuture<T>轉換成CompletableFuture<U>
- thenCompose可以用於組合多個CompletableFuture,將前一個結果作為下一個計算的參數,它們之間存在着先后順序
- 現在有CompletableFuture<T>、CompletableFuture<U>和一個函數(T,U)->V,thenCompose就是將CompletableFuture<T>和CompletableFuture<U>變為CompletableFuture<V>
- 使用thenCombine()之后future1、future2之間是並行執行的,最后再將結果匯總。這一點跟thenCompose()不同
- thenAcceptBoth跟thenCombine類似,但是返回CompletableFuture<Void>類型
- handle()的參數是BiFunction,apply()方法返回R,相當於轉換的操作
- whenComplete()的參數是BiConsumer,accept()方法返回void
- thenAccept()是只會對計算結果進行消費而不會返回任何結果的方法
時間API
Clock
Clock類提供了訪問當前日期和時間的方法,Clock是時區敏感的,可以用來取代 System.currentTimeMillis() 來獲取當前的微秒數。某一個特定的時間點也可以使用Instant類來表示,Instant類也可以用來創建老的java.util.Date對象。
Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();
Instant instant = clock.instant();
Date legacyDate = Date.from(instant);
LocalDate
該類的實例是一個不可變對象,它只提供了簡單的日期,並不含當天的時間信息。另外,它也不附帶任何與時區相關的信息。通過靜態工廠方法 of 創建一個 LocalDate 實例。 LocalDate 實例提供了多種方法來讀取常用的值,比如年份、月份、星期幾等。
LocalDate date = LocalDate.of(2018, 10, 1); int year = date.getYear(); Month month = date.getMonth(); int day = date.getDayOfMonth(); DayOfWeek dow = date.getDayOfWeek(); int len = date.lengthOfMonth(); boolean leap = date.isLeapYear();
等同於
int year = date.get(ChronoField.YEAR); int month = date.get(ChronoField.MONTH_OF_YEAR); int day = date.get(ChronoField.DAY_OF_MONTH);
獲取當前時間:
LocalDate today = LocalDate.now();
LocalTime
一天中的時間,比如13:45:20,可以使用 LocalTime 類表示。你可以使用 of 重載的兩個工廠方法創建 LocalTime 的實例。 第一個重載函數接收小時和分鍾, 第二個重載函數同時還接收秒。同 LocalDate 一樣, LocalTime 類也提供了一些 getter 方法訪問這些變量的值。
LocalTime time = LocalTime.of(13, 45, 20); int hour = time.getHour(); int minute = time.getMinute(); int second = time.getSecond();
LocalDate 和 LocalTime 都可以通過解析代表它們的字符串創建。使用靜態方法 parse:
LocalDate date = LocalDate.parse("2018-03-18"); LocalTime time = LocalTime.parse("13:45:20");
可以向 parse 方法傳遞一個 DateTimeFormatter 。該類的實例定義了如何格式化一個日
期或者時間對象。它是替換老版 java.util.DateFormat 的推薦替代品。一旦傳遞的字符串參數無法被解析為合法的 LocalDate 或 LocalTime 對象, 這兩個 parse 方法都會拋出一個繼承自 RuntimeException 的 DateTimeParseException 異常。
LocalDateTime
這個復合類名叫 LocalDateTime ,是 LocalDate 和 LocalTime 的合體。它同時表示了日期和時間, 但不帶有時區信息, 你可以直接創建, 也可以通過合並日期和時間對象構造。
LocalDateTime dt1 = LocalDateTime.of(2018, Month.MARCH, 18, 13, 45, 20);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);
通過它們各自的 atTime 或者 atDate 方法,向 LocalDate 傳遞一個時間對象,或者向LocalTime 傳遞一個日期對象的方式,你可以創建一個 LocalDateTime 對象。你也可以使用toLocalDate 或者 toLocalTime 方法,從 LocalDateTime 中提取 LocalDate 或者 LocalTime組件:
LocalDate date1 = dt1.toLocalDate();
LocalTime time1 = dt1.toLocalTime();
Instant
可以通過向靜態工廠方法 ofEpochSecond 傳遞一個代表秒數的值創建一個該類的實例。 靜態工廠方法 ofEpochSecond 還有一個增強的重載版本,它接收第二個以納秒為單位的參數值,對傳入作為秒數的參數進行調整。重載的版本會調整納秒參數,確保保存的納秒分片在0到999 999999之間。
Instant.ofEpochSecond(3); Instant.ofEpochSecond(3, 0); Instant.ofEpochSecond(2, 1_000_000_000); // 2秒 之 后 再 加上100萬納秒(1秒) Instant.ofEpochSecond(4, -1_000_000_000); // 4秒之前的100萬納秒(1秒)
修改操作:
如果你已經有一個 LocalDate 對象, 想要創建它的一個修改版, 最直接也最簡單的方法是使用 withAttribute 方法。 withAttribute 方法會創建對象的一個副本,並按照需要修改它的屬性。
LocalDate date1 = LocalDate.of(2014, 3, 18); // 2014-03-18 LocalDate date2 = date1.withYear(2011); // 2011-03-18 LocalDate date3 = date2.withDayOfMonth(25); // 2011-03-25 LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 9); // 2011-09-25 LocalDate date1 = LocalDate.of(2014, 3, 18); // 2014-03-18 LocalDate date2 = date1.plusWeeks(1); // 2014-03-25 LocalDate date3 = date2.minusYears(3); // 2011-03-25 LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS); // 2011-09-25
LocalDate 、 LocalTime 、 LocalDateTime 以及 Instant通用方法
方法名 | 是否是靜態方法 | 描述 |
---|---|---|
from | 是 | 依據傳入的 Temporal 對象創建對象實例 |
now | 是 | 依據系統時鍾創建 Temporal 對象 |
of | 是 | 由 Temporal 對象的某個部分創建該對象的實例 |
parse | 是 | 由字符串創建 Temporal 對象的實例 |
atOffset | 否 | 將 Temporal 對象和某個時區偏移相結合 |
atZone | 否 | 將 Temporal 對象和某個時區相結合 |
format | 否 | 使用某個指定的格式器將 Temporal 對象轉換為字符串 ( Instant 類不提供該方法) |
get | 否 | 讀取 Temporal 對象的某一部分的值 |
minus | 否 | 創建 Temporal 對象的一個副本, 通過將當前 Temporal 對象的值減去一定的時長創建該副本 |
plus | 否 | 創建 Temporal 對象的一個副本, 通過將當前 Temporal 對象的值加上一定的時長創建該副本 |
with | 否 | 以該 Temporal 對象為模板,對某些狀態進行修改創建該對象的副本 |
LocalDate date = LocalDate.of(2014, 3, 18); date = date.with(ChronoField.MONTH_OF_YEAR, 9); date = date.plusYears(2).minusDays(10); date.withYear(2011);
答案: 2016-09-08 。
每個動作都會創建一個新的 LocalDate 對象,后續的方法調用可以操縱前一方法創建的對象。這段代碼的最后一句不會產生任何我們能看到的效果,因為它像前面的那些操作一樣,會創建一個新的 LocalDate 實例,不過我們並沒有將這個新創建的值賦給任何的變量。
Duration
用於比較LocalTime之間的時間差, Duration 類主要用於以秒和納秒衡量時間的長短。
LocalTime time1 = LocalTime.now();
LocalTime time2 = LocalTime.of(11, 0, 0);
Duration d1 = Duration.between(time1, time2);
Period
用於比較LocalDate之間的時間差, Period類主要用於以年月日衡量時間的長短。
Period tenDays = Period.between(LocalDate.of(2014, 3, 8), LocalDate.of(2014, 3, 18));
Duration和Period通用方法
方法名 | 是否是靜態方法 | 方法描述 |
---|---|---|
between | 是 | 創建兩個時間點之間的 interval |
from | 是 | 由一個臨時時間點創建 interval |
of | 是 | 由它的組成部分創建 interval的實例 |
parse | 是 | 由字符串創建 interval 的實例 |
addTo | 否 | 創建該 interval 的副本,並將其疊加到某個指定的 temporal 對象 |
get | 否 | 讀取該 interval 的狀態 |
isNegative | 否 | 檢查該 interval 是否為負值,不包含零 |
isZero | 否 | 檢查該 interval 的時長是否為零 |
minus | 否 | 通過減去一定的時間創建該 interval 的副本 |
multipliedBy | 否 | 將 interval 的值乘以某個標量創建該 interval 的副本 |
negated | 否 | 以忽略某個時長的方式創建該 interval 的副本 |
plus | 否 | 以增加某個指定的時長的方式創建該 interval 的副本 |
subtractFrom | 否 | 從指定的 temporal 對象中減去該 interval |
TemporalAdjuster
將日期調整到下個周日、下個工作日,或者是本月的最后一天。這時,你可以使用重載版本的 with 方法, 向其傳遞一個提供了更多定制化選擇的 TemporalAdjuster 對象,更加靈活地處理日期。
LocalDate date1 = LocalDate.of(2014, 3, 18); LocalDate date2 = date1.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)); LocalDate date3 = date2.with(TemporalAdjusters.lastDayOfMonth());
方法名 | 描述 |
---|---|
dayOfWeekInMonth | 創建一個新的日期,它的值為同一個月中每一周的第幾天 |
firstDayOfMonth | 創建一個新的日期,它的值為當月的第一天 |
firstDayOfNextMonth | 創建一個新的日期,它的值為下月的第一天 |
firstDayOfNextYear | 創建一個新的日期,它的值為明年的第一天 |
firstDayOfYear | 創建一個新的日期,它的值為當年的第一天 |
firstInMonth | 創建一個新的日期,它的值為同一個月中,第一個符合星期幾要求的值 |
lastDayOfMonth | 創建一個新的日期,它的值為當月的最后一天 |
lastDayOfNextMonth | 創建一個新的日期,它的值為下月的最后一天 |
lastDayOfNextYear | 創建一個新的日期,它的值為明年的最后一天 |
lastDayOfYear | 創建一個新的日期,它的值為今年的最后一天 |
lastInMonth | 創建一個新的日期,它的值為同一個月中,最后一個符合星期幾要求的值 |
next/previous | 創建一個新的日期,並將其值設定為日期調整后或者調整前,第一個符合指定星期幾要求的日期 |
nextOrSame/previousOrSame | 創建一個新的日期,並將其值設定為日期調整后或者調整前,第一個符合指定星期幾要求的日期,如果該日期已經符合要求,直接返回該對象 |
DateTimeFormatter
處理日期和時間對象時,格式化以及解析日期時間對象是另一個非常重要的功能。新的java.time.format 包就是特別為這個目的而設計的。這個包中,最重要的類是 DateTime-Formatter 。 創建格式器最簡單的方法是通過它的靜態工廠方法以及常量。 像 BASIC_ISO_DATE和 ISO_LOCAL_DATE 這 樣 的 常 量 是 DateTimeFormatter 類 的 預 定 義 實 例 。 所 有 的DateTimeFormatter 實例都能用於以一定的格式創建代表特定日期或時間的字符串。
LocalDate date = LocalDate.of(2014, 3, 18); String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); // 20140318 String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2014-03-18
等同於
LocalDate date1 = LocalDate.parse("20140318", DateTimeFormatter.BASIC_ISO_DATE); LocalDate date2 = LocalDate.parse("2014-03-18", DateTimeFormatter.ISO_LOCAL_DATE);
和老的 java.util.DateFormat 相比較,所有的 DateTimeFormatter 實例都是線程安全的。所以,你能夠以單例模式創建格式器實例,就像 DateTimeFormatter 所定義的那些常量,並能在多個線程間共享這些實例。 DateTimeFormatter 類還支持一個靜態工廠方法,它可以按照某個特定的模式創建格式器。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy") LocalDate date1 = LocalDate.of(2014, 3, 18); String formattedDate = date1.format(formatter); LocalDate date2 = LocalDate.parse(formattedDate, formatter);
如果還需要更加細粒度的控制, DateTimeFormatterBuilder 類還提供了更復雜的格式器,你可以選擇恰當的方法,一步一步地構造自己的格式器。另外,它還提供了非常強大的解析功能,比如區分大小寫的解析、柔性解析(允許解析器使用啟發式的機制去解析輸入,不精確地匹配指定的模式) 、填充,以及在格式器中指定可選節。
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder() .appendText(ChronoField.DAY_OF_MONTH) .appendLiteral(". ") .appendText(ChronoField.MONTH_OF_YEAR) .appendLiteral(" ") .appendText(ChronoField.YEAR) .parseCaseInsensitive() .toFormatter(Locale.ITALIAN);
ZoneId
之前看到的日期和時間的種類都不包含時區信息。時區的處理是新版日期和時間API新增加的重要功能,使用新版日期和時間API時區的處理被極大地簡化了。新的 java.time.ZoneId類是老版 java.util.TimeZone 的替代品。它的設計目標就是要讓你無需為時區處理的復雜和繁瑣而操心,比如處理日光時(Daylight Saving Time,DST)這種問題。跟其他日期和時間類一樣, ZoneId 類也是無法修改的。
ZoneId romeZone = ZoneId.of("Europe/Rome");
地區ID都為 “{區域}/{城市}” 的格式, 這些地區集合的設定都由英特網編號分配機構 (IANA)的時區數據庫提供。你可以通過Java 8的新方法 toZoneId 將一個老的時區對象轉換為 ZoneId :
ZoneId zoneId = TimeZone.getDefault().toZoneId();
一旦得到一個 ZoneId 對象,你就可以將它與 LocalDate 、 LocalDateTime 或者是 Instant對象整合起來,構造為一個 ZonedDateTime 實例,它代表了相對於指定時區的時間點。
LocalDate date = LocalDate.of(2014, Month.MARCH, 18);
ZonedDateTime zdt1 = date.atStartOfDay(romeZone);
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
ZonedDateTime zdt2 = dateTime.atZone(romeZone);
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone);
ZonedDateTime

將 LocalDateTime 轉換為 Instant :
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
Instant instantFromDateTime = dateTime.toInstant(romeZone);
將 Instant 轉換為 LocalDateTime :
Instant instant = Instant.now();
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone);
計算時區
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
日歷系統
Java 8中另外還提供了4種其他的日歷系統。這些日歷系統中的每一個都有一個對應的日志類,分別是 ThaiBuddhistDate 、MinguoDate 、 JapaneseDate 以及 HijrahDate 。所有這些類以及 LocalDate 都實現了ChronoLocalDate 接口,能夠對公歷的日期進行建模。利用 LocalDate 對象,你可以創建這些類的實例。
LocalDate date = LocalDate.of(2014, Month.MARCH, 18); JapaneseDate japaneseDate = JapaneseDate.from(date);
等同於
Chronology japaneseChronology = Chronology.ofLocale(Locale.JAPAN);
ChronoLocalDate now = japaneseChronology.dateNow();
java8類庫
@Repeatable
如果一個注解在設計之初就是可重復的,你可以直接使用它。但是,如果你提供的注解是為用戶提供的,那么就需要做一些工作,說明該注解可以重復。
新增方法
類/接口 | 新方法 |
---|---|
Map | getOrDefault , forEach , compute , computeIfAbsent , computeIfPresent , merge ,putIfAbsent , remove(key,value) , replace , replaceAll |
Iterable | forEach , spliterator |
Iterator | forEachRemaining |
Collection | removeIf , stream , parallelStream |
List | replaceAll , sort |
BitSet | stream |
Map
forEach 該方法簽名為void forEach(BiConsumer<? super K,? super V> action),作用是對Map中的每個映射執行action指定的操作,其中BiConsumer是一個函數接口,里面有一個待實現方法void accept(T t, U u)。
java8之前寫法:
HashMap<Integer, String> map = new HashMap<>(); map.put(1, "one"); map.put(2, "two"); map.put(3, "three"); for (Map.Entry<Integer, String> entry : map.entrySet()) { System.out.println(entry.getKey() + "=" + entry.getValue()); }
java8:
HashMap<Integer, String> map = new HashMap<>(); map.put(1, "one"); map.put(2, "two"); map.put(3, "three"); map.forEach((k, v) -> System.out.println(k + "=" + v));
getOrDefault 方法就可以替換現在檢測 Map 中是否包含給定鍵映射的慣用方法。如果 Map 中不存在這樣的鍵映射,你可以提供一個默認值,方法會返回該默認值。
java8之前寫法:
Map<String, Integer> carInventory = new HashMap<>(); Integer count = 0; if (map.containsKey("Aston Martin")) { count = map.get("Aston Martin"); }
java8:
Integer count = map.getOrDefault("Aston Martin", 0);
putIfAbsent 方法簽名為V putIfAbsent(K key, V value),作用是只有在不存在key值的映射或映射值為null時,才將value指定的值放入到Map中,否則不對Map做更改.該方法將條件判斷和賦值合二為一,使用起來更加方便。
remove(Object key, Object value)方法,只有在當前Map中key正好映射到value時才刪除該映射,否則什么也不做。
replace 在Java7及以前,要想替換Map中的映射關系可通過put(K key, V value)方法實現,該方法總是會用新值替換原來的值.為了更精確的控制替換行為,Java8在Map中加入了兩個replace()方法,分別如下:
- replace(K key, V value),只有在當前Map中key的映射存在時才用value去替換原來的值,否則什么也不做。
- replace(K key, V oldValue, V newValue),只有在當前Map中key的映射存在且等於oldValue時才用newValue去替換原來的值,否則什么也不做。
replaceAll 該方法簽名為replaceAll(BiFunction<? super K,? super V,? extends V> function),作用是對Map中的每個映射執行function指定的操作,並用function的執行結果替換原來的value,其中BiFunction是一個函數接口,里面有一個待實現方法R apply(T t, U u)。
java8之前寫法:
HashMap<Integer, String> map = new HashMap<>(); map.put(1, "one"); map.put(2, "two"); map.put(3, "three"); for (Map.Entry<Integer, String> entry : map.entrySet()) { entry.setValue(entry.getValue().toUpperCase()); }
java8:
HashMap<Integer, String> map = new HashMap<>(); map.put(1, "one"); map.put(2, "two"); map.put(3, "three"); map.replaceAll((k, v) -> v.toUpperCase());
merge 該方法簽名為merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction),作用是:
如果Map中key對應的映射不存在或者為null,則將value(不能是null)關聯到key上;
否則執行remappingFunction,如果執行結果非null則用該結果跟key關聯,否則在Map中刪除key的映射。
Map<String, String> myMap = new HashMap<>(); myMap.put("A", "str01A"); myMap.merge("A", "merge01", String::concat); // str01A merge01
compute 該方法簽名為compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction) ,如果map里有這個key,那么remappingFunction輸入的v就是現在的值,返回的是對應value,如果沒有這個key,那么輸入的v是null。
map.compute(key, (k, v) -> v == null ? newMsg : v.concat(newMsg));
computeIfAbsent 該方法簽名為V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction),作用是:只有在當前Map中不存在key值的映射或映射值為null時,才調用mappingFunction,並在mappingFunction執行結果非null時,將結果跟key關聯。
java8之前寫法:
Map<Integer, Set<String>> map = new HashMap<>(); if (map.containsKey(1)) { map.get(1).add("one"); } else { Set<String> valueSet = new HashSet<String>(); valueSet.add("one"); map.put(1, valueSet); }
java8:
map.computeIfAbsent(1, v -> new HashSet<String>()).add("yi");
computeIfPresent 該方法簽名為V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction),作用跟computeIfAbsent()相反,即,只有在當前Map中存在key值的映射且非null時,才調用remappingFunction,如果remappingFunction執行結果為null,則刪除key的映射,否則使用該結果替換key原來的映射。
Collection
removeIf 該方法簽名為boolean removeIf(Predicate<? super E> filter),作用是刪除容器中所有滿足filter指定條件的元素,其中Predicate是一個函數接口,里面只有一個待實現方法boolean test(T t),同樣的這個方法的名字根本不重要,因為用的時候不需要書寫這個名字。
java8之前寫法:
// 使用迭代器刪除列表元素 ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too")); Iterator<String> it = list.iterator(); while (it.hasNext()) { if (it.next().length()>3) { // 刪除長度大於3的元素 it.remove(); } }
java8:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too")); // 刪除長度大於3的元素 list.removeIf(str -> str.length() > 3);
replaceAll 該方法簽名為void replaceAll(UnaryOperator<E> operator),作用是對每個元素執行operator指定的操作,並用操作結果來替換原來的元素。其中UnaryOperator是一個函數接口,里面只有一個待實現函數T apply(T t)。
java8之前寫法:
// 使用下標實現元素替換 ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too")); for (int i=0; i<list.size(); i++) { String str = list.get(i); if (str.length()>3) { list.set(i, str.toUpperCase()); } }
java8:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too")); list.replaceAll(str -> { if (str.length() > 3) { return str.toUpperCase(); } return str; });
sort 該方法定義在List接口中,方法簽名為void sort(Comparator<? super E> c),該方法根據c指定的比較規則對容器元素進行排序。Comparator接口我們並不陌生,其中有一個方法int compare(T o1, T o2)需要實現,顯然該接口是個函數接口。
java8之前寫法:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too")); Collections.sort(list, new Comparator<String>() { @Override public int compare(String str1, String str2) { return str1.length() - str2.length(); } });
java8:
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too")); list.sort((str1, str2) -> str1.length() - str2.length());
spliterator 方法簽名為Spliterator<E> spliterator(),該方法返回容器的可拆分迭代器。從名字來看該方法跟iterator()方法有點像,我們知道Iterator是用來迭代容器的,Spliterator也有類似作用,但二者有如下不同:
- Spliterator既可以像Iterator那樣逐個迭代,也可以批量迭代。批量迭代可以降低迭代的開銷。
- Spliterator是可拆分的,一個Spliterator可以通過調用Spliterator<T> trySplit()方法來嘗試分成兩個。一個是this,另一個是新返回的那個,這兩個迭代器代表的元素沒有重疊。
可通過(多次)調用Spliterator.trySplit()方法來分解負載,以便多線程處理。
stream和parallelStream 分別返回該容器的Stream視圖表示,不同之處在於parallelStream()返回並行的Stream。Stream是Java函數式編程的核心類。
並發包
原子操作
java.util.concurrent.atomic 包提供了多個對數字類型進行操作的類,比如 AtomicInteger 和 AtomicLong ,它們支持對單一變量的原子操作。這些類在Java 8中新增了更多的方法支持。
- getAndUpdate —— 以原子方式用給定的方法更新當前值,並返回變更之前的值。
- updateAndGet —— 以原子方式用給定的方法更新當前值,並返回變更之后的值。
- getAndAccumulate —— 以原子方式用給定的方法對當前及給定的值進行更新,並返回變更之前的值。
- accumulateAndGet —— 以原子方式用給定的方法對當前及給定的值進行更新,並返回變更之后的值。
Adder 和 Accumulator:
多線程的環境中,如果多個線程需要頻繁地進行更新操作,且很少有讀取的動作(比如,在統計計算的上下文中) ,Java API文檔中推薦大家使用新的類 LongAdder 、 LongAccumulator 、DoubleAdder 以及 DoubleAccumulator ,盡量避免使用它們對應的原子類型。這些新的類在設計之初就考慮了動態增長的需求,可以有效地減少線程間的競爭。
LongAddr 和 DoubleAdder 類都支持加法操作,而 LongAccumulator 和 DoubleAccumulator 可以使用給定的方法整合多個值。
LongAdder adder = new LongAdder(); adder.add(10); long sum = adder.sum();
等同於
LongAccumulator acc = new LongAccumulator(Long::sum, 0); acc.accumulate(10); long result = acc.get();
ConcurrentHashMap
ConcurrentHashMap 類的引入極大地提升了 HashMap 現代化的程度,新引入的ConcurrentHashMap 對並發的支持非常友好。 ConcurrentHashMap 允許並發地進行新增和更新操作,因為它僅對內部數據結構的某些部分上鎖。因此,和另一種選擇,即同步式的 Hashtable 比較起來,它具有更高的讀寫性能。
- 性能
為了改善性能,要對 ConcurrentHashMap 的內部數據結構進行調整。典型情況下, map 的條目會被存儲在桶中,依據鍵生成哈希值進行訪問。但是,如果大量鍵返回相同的哈希值,由於桶是由 List 實現的,它的查詢復雜度為O(n),這種情況下性能會惡化。在Java 8中,當桶過於臃腫時,它們會被動態地替換為排序樹(sorted tree) ,新的數據結構具有更好的查詢性能(排序樹的查詢復雜度為O(log(n))) 。注意,這種優化只有當鍵是可以比較的(比如 String 或者 Number類)時才可能發生。 - 類流操作
ConcurrentHashMap 支持三種新的操作,這些操作和你之前在流中所見的很像:
- forEach ——對每個鍵值對進行特定的操作
- reduce ——使用給定的精簡函數(reduction function) ,將所有的鍵值對整合出一個結果
- search ——對每一個鍵值對執行一個函數,直到函數的返回值為一個非空值
以上每一種操作都支持四種形式,接受使用鍵、值、 Map.Entry 以及鍵值對的函數: - 使用鍵和值的操作( forEach 、 reduce 、 search )
- 使用鍵的操作( forEachKey 、 reduceKeys 、 searchKeys )
- 使用值的操作 ( forEachValue 、 reduceValues 、 searchValues )
- 使用 Map.Entry 對象的操作( forEachEntry 、 reduceEntries 、 searchEntries )
注意,這些操作不會對 ConcurrentHashMap 的狀態上鎖。它們只會在運行過程中對元素進行操作。應用到這些操作上的函數不應該對任何的順序,或者其他對象,抑或在計算過程發生變化的值,有依賴。
除此之外,你需要為這些操作指定一個並發閾值。如果經過預估當前 map 的大小小於設定的閾值,操作會順序執行。使用值 1 開啟基於通用線程池的最大並行。使用值 Long.MAX_VALUE 設定程序以單線程執行操作。
下面這個例子中,我們使用 reduceValues 試圖找出 map 中的最大值:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); Optional<Integer> maxValue = Optional.of(map.reduceValues(1, Integer::max));
注意,對 int 、 long 和 double ,它們的 reduce 操作各有不同(比如 reduceValuesToInt 、reduceKeysToLong 等) 。
- 計數
ConcurrentHashMap 類提供了一個新的方法,名叫 mappingCount ,它以長整型 long 返回map 中映射的數目。我們應該盡量使用這個新方法,而不是老的 size 方法, size 方法返回的類型為 int 。這是因為映射的數量可能是 int 無法表示的。 - 集合視圖
ConcurrentHashMap 類還提供了一個名為 KeySet 的新方法,該方法以 Set 的形式返回ConcurrentHashMap 的一個視圖(對 map 的修改會反映在該 Set 中,反之亦然) 。你也可以使用新的靜態方法 newKeySet ,由 ConcurrentHashMap 創建一個 Set 。
Arrays
使用 parallelSort
parallelSort 方法會以並發的方式對指定的數組進行排序,你可以使用自然順序,也可以為數組對象定義特別的 Comparator 。
使用 setAll 和 parallelSetAll
setAll 和 parallelSetAll 方法可以以順序的方式也可以用並發的方式,使用提供的函數計算每一個元素的值,對指定數組中的所有元素進行設置。該函數接受元素的索引,返回該索引元素對應的值。由於 parallelSetAll 需要並發執行,所以提供的函數必須沒有任何副作用。
int[] evenNumbers = new int[10]; Arrays.setAll(evenNumbers, i -> i * 2); // 0, 2, 4, 6...
使用 parallelPrefix
parallelPrefix 方法以並發的方式, 用用戶提供的二進制操作符對給定數組中的每個元素進行累積計算。
int[] ones = new int[10];
Arrays.fill(ones, 1);
Arrays.parallelPrefix(ones, (a, b) -> a + b);
Number
Number 類中新增的方法如下。
- Short 、 Integer 、 Long 、 Float 和 Double 類提供了靜態方法 sum 、 min 和 max 。
- Integer 和 Long 類提供了 compareUnsigned 、 divideUnsigned 、 remainderUnsigned 和 toUnsignedLong 方法來處理無符號數。
- Integer 和 Long 類也分別提供了靜態方法 parseUnsignedInt 和 parseUnsignedLong將字符解析為無符號 int 或者 long 類型。
- Byte 和 Short 類提供了 toUnsignedInt 和 toUnsignedLong 方法通過無符號轉換將參數轉化為 int 或 者 long 類型 。 類似地 , Integer 類現在也提供了靜態方法toUnsignedLong 。
- Double 和 Float 類提供了靜態方法 isFinite ,可以檢查參數是否為有限浮點數。
- Boolean 類現在提供了靜態方法 logicalAnd 、 logicalOr 和 logicalXor ,可以在兩個boolean 之間執行 and 、 or 和 xor 操作。
- BigInteger 類提供了 byteValueExact 、 shortValueExact 、 intValueExact 和longValueExact 可以將 BigInteger 類型的值轉換為對應的基礎類型。不過,如果在轉換過程中有信息的丟失,方法會拋出算術異常。
Math
如果 Math 中的方法在操作中出現溢出, Math 類提供了新的方法可以拋出算術異常。支持這一異常的方法包括使用 int 和 long 參數的 addExact 、 subtractExact 、 multipleExact 、incrementExact 、 decrementExact 和 negateExact 。此外, Math 類還新增了一個靜態方法toIntExact , 可以將 long 值轉換為 int 值。 其他的新增內容包括靜態方法 floorMod 、 floorDiv和 nextDown 。
Files
Files 類最引人注目的改變是,你現在可以用文件直接產生流。通過 Files.lines 方法你可以以延遲方式讀取文件的內容,並將其作為一個流。此外,還有一些非常有用的靜態方法可以返回流。
- Files.list —— 生成由指定目錄中所有條目構成的 Stream<Path> 。這個列表不是遞歸包含的。由於流是延遲消費的,處理包含內容非常龐大的目錄時,這個方法非常有用。
- Files.walk —— 和 Files.list 有些類似,它也生成包含給定目錄中所有條目的Stream<Path> 。不過這個列表是遞歸的,你可以設定遞歸的深度。注意,該遍歷是依照深度優先進行的。
- Files.find —— 通過遞歸地遍歷一個目錄找到符合條件的條目,並生成一個Stream<Path> 對象。
Reflection
Relection 接口的另一個變化是新增了可以查詢方法參數信息的API,比如,你現在可以使用新增的 java.lang.reflect.Parameter 類查詢方法參數的名稱和修飾符,這個類被新的java.lang.reflect.Executable 類所引用, 而 java.lang.reflect.Executable 通用函數和構造函數共享的父類。
String
String 類也新增了一個靜態方法,名叫 join 。你大概已經猜出它的功能了,它可以用一個分隔符將多個字符串連接起來。
String authors = String.join(", ", "Raoul", "Mario", "Alan");
PS
泛型
Java類型要么是引用類型(比如 Byte 、 Integer 、 Object 、 List ) ,要么是原始類型(比如 int 、 double 、 byte 、 char ) 。但是泛型(比如 Consumer<T> 中的 T )只能綁定到引用類型。這是由泛型內部的實現方式造成的。因此,在Java里有一個將原始類型轉換為對應的引用類型的機制。這個機制叫作裝箱(boxing)。相反的操作,也就是將引用類型轉換為對應accept 方法的實現Lambda是 Function接口的 apply 方法的實現的原始類型,叫作拆箱(unboxing) 。Java還有一個自動裝箱機制來幫助程序員執行這一任務:裝箱和拆箱操作是自動完成的。
工具類庫
Guava、Apache和lambdaj
廣義歸約( Collectors.reducing)
它需要三個參數。
第一個參數是歸約操作的起始值,也是流中沒有元素時的返回值,所以很顯然對於數值和而言 0 是一個合適的值。
第二個參數就是轉換成一個表示其所含熱量的 int 。
第三個參數是一個 BinaryOperator ,將兩個項目累積成一個同類型的值。
求和:
int totalCalories = menu.stream().collect(Collectors.reducing(0, Dish::getCalories, (i, j) -> i + j));
找出集合中最大值:
Optional <Dish> mostCalorieDish = menu.stream().collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
Collectors 類的靜態工廠方法
工廠方法 | 返回類型 | 描 述 | 使用示例 |
---|---|---|---|
toList | List<T> | 把流中所有項目收集到一個 List | List<Dish> dishes = menuStream.collect(Collectors.toList()); |
toSet | Set<T> | 把流中所有項目收集到一個 Set ,刪除重復項 | Set<Dish> dishes = menuStream.collect(Collectors.toSet()); |
toMap | Map<T, K> | 把流中所有項目收集到一個 Map ,刪除重復項,默認情況下,出現重復數據會報錯 | Map<Long, Dish> dishesMap = menuStream.collect(Collectors.toMap(Dish::getCalories));如有重復數據,可以設置使用哪一個數據 Map<Long, Dish> dishesMap = menuStream.collect(Collectors.toMap(Dish::getCalories, d -> d, (d1, d2) -> d1, LinkedHashMap::new)); |
toCollection | Collection<T> | 把流中所有項目收集到給定的供應源創建的集合 | Collection<Dish> dishes = menuStream.collect(Collectors.toCollection(), ArrayList::new); |
counting | Long | 計算流中元素的個數 | long howManyDishes = menuStream.collect(Collectors.counting()); |
summingInt | Integer | 對流中項目的一個整數屬性求和 | int totalCalories = menuStream.collect(Collectors.summingInt(Dish::getCalories)); |
averagingInt | Integer | 計算流中項目 Integer 屬性的平均值 | int avgCalories = menuStream.collect(Collectors.averagingInt(Dish::getCalories)); |
summarizingInt | IntSummaryStatistics | 收集關於流中項目 Integer 屬性的統計值,例如最大、最小、總和與平均值 | IntSummaryStatistics menuStatistics = menuStream.collect(Collectors.summarizingInt(Dish::getCalories)); |
joining | String | 連接對流中每個項目調用 toString 方法所生成的字符串 | String shortMenu = menuStream.map(Dish::getName).collect(Collectors.joining(", ")); |
maxBy | Optional<T> | 一個包裹了流中按照給定比較器選出的最大元素的 Optional ,或如果流為空則為 Optional.empty() | Optional<Dish> fattest = menuStream.collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories))); |
minBy | Optional<T> | 一個包裹了流中按照給定比較器選出的最小元素的 Optional ,或如果流為空則為 Optional.empty() | Optional<Dish> lightest = menuStream.collect(Collectors.minBy(Comparator.comparingInt(Dish::getCalories))); |
reducing | 歸約操作產生的類型 | 從一個作為累加器的初始值開始,利用 BinaryOperator 與流中的元素逐個結合,從而將流歸約為單個值 | int totalCalories = menuStream.collect(Collectors.reducing(0, Dish::getCalories, Integer::sum)); |
collectingAndThen | 轉換函數返回的類型 | 包裹另一個收集器,對其結果應用轉換函數 | int howManyDishes = menuStream.collect(Collectors.collectingAndThen(toList(), List::size)); |
groupingBy | Map<K, List<T>> | 根據項目的一個屬性的值對流中的項目分組組,並將屬性值分組結果 Map 的鍵 | Map<Dish.Type,List<Dish>> dishesByType = menuStream.collect(Collectors.groupingBy(Dish::getType)); |
partitioningBy | Map<Boolean,List<T>> | 根據對流中每個項目應用謂詞的結果來對項目進行分區 | Map<Boolean,List<Dish>> vegetarianDishes = menuStream.collect(Collectors.partitioningBy(Dish::isVegetarian)); |
Optional介紹
Optional<T> 類( java.util.Optional )是一個容器類,代表一個值存在或不存在。
- isPresent() 將在 Optional 包含值的時候返回 true , 否則返回 false 。
- ifPresent(Consumer<T> block) 會在值存在的時候執行給定的代碼塊。
- T get() 會在值存在時返回值,否則拋出一個 NoSuchElement 異常。
- T orElse(T other) 會在值存在時返回值,否則返回一個默認值。
線程個數計算方式
如果線程池中線程的數量過多,最終它們會競爭稀缺的處理器和內存資源,浪費大量的時間在上下文切換上。反之,如果線程的數目過少,正如你的應用所面臨的情況,處理器的一些核可能就無法充分利用。Brian Goetz建議,線程池大小與處理器的利用率之比可以使用下面的公式進行估算:
N threads = N CPU * U CPU * (1 + W/C)
其中:
- N CPU 是處理器的核的數目,可以通過 Runtime.getRuntime().availableProcessors() 得到
- U CPU 是期望的CPU利用率(該值應該介於0和1之間)
- W/C 是等待時間與計算時間的比率
並行——使用parallelStream還是 CompletableFutures ?
目前為止, 你已經知道對集合進行並行計算有兩種方式: 要么將其轉化為parallelStream, 利用map這樣的操作開展工作,要么枚舉出集合中的每一個元素,創建新的線程,在 Completable-Future 內對其進行操作。后者提供了更多的靈活性,你可以調整線程池的大小,而這能幫助你確保整體的計算不會因為線程都在等待I/O而發生阻塞。
我們對使用這些API的建議如下。
如果你進行的是計算密集型的操作,並且沒有I/O,那么推薦使用 Stream 接口,因為實現簡單,同時效率也可能是最高的(如果所有的線程都是計算密集型的,那就沒有必要創建比處理器核數更多的線程) 。
反之,如果你並行的工作單元還涉及等待I/O的操作(包括網絡連接等待) ,那么使用CompletableFuture 靈活性更好,你可以像前文討論的那樣,依據等待/計算,或者W/C的比率設定需要使用的線程數。這種情況不使用並行流的另一個原因是,處理流的流水線中如果發生I/O等待, 流的延遲特性會讓我們很難判斷到底什么時候觸發了等待。
配置並行流使用的線程池
並行流內部使用了默認的 ForkJoinPool,它默認的線程數量就是你的處理器數量 , 這個值是由Runtime.getRuntime().available-Processors() 得到的。
但是可以通 過系統屬性java.util.concurrent.ForkJoinPool.common.parallelism 來改變線程池大小,如下所示:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
這是一個全局設置,因此它將影響代碼中所有的並行流。反過來說,目前還無法專為某個
並行流指定這個值。一般而言,讓 ForkJoinPool 的大小等於處理器數量是個不錯的默認值,
除非你有很好的理由,否則我們強烈建議你不要修改它。
測量流性能
我們聲稱並行求和方法應該比順序和迭代方法性能好。然而在軟件工程上,靠猜絕對不是什么好辦法!特別是在優化性能時,你應該始終遵循三個黃金規則:測量,測量,再測量。
- 並行流並不總是比順序流快。
有些操作本身在並行流上的性能就比順序流差。特別是 limit 和 findFirst 等依賴於元素順序的操作,它們在並行流上執行的代價非常大。例如, findAny 會比 findFirst 性能好,因為它不一定要按順序來執行。你總是可以調用 unordered 方法來把有序流變成無序流。那么,如果你需要流中的n個元素而不是專門要前n個的話,對無序並行流調用limit 可能會比單個有序流(比如數據源是一個 List )更高效。 - 留意裝箱。自動裝箱和拆箱操作會大大降低性能。Java 8中有原始類型流( IntStream 、LongStream 、 DoubleStream )來避免這種操作,但凡有可能都應該用這些流。
- 考慮流的操作流水線的總計算成本。設N是要處理的元素的總數,Q是一個元素通過流水線的大致處理成本,則N*Q就是這個對成本的一個粗略的定性估計。Q值較高就意味着使用並行流時性能好的可能性比較大。
- 對於較小的數據量,選擇並行流幾乎從來都不是一個好的決定。並行處理少數幾個元素的好處還抵不上並行化造成的額外開銷。
- 考慮流背后的數據結構是否易於分解。例如, ArrayList 的拆分效率比 LinkedList 高得多,因為前者用不着遍歷就可以平均拆分,而后者則必須遍歷。另外,用 range 工廠方法創建的原始類型流也可以快速分解。
可分解性總結了一些流數據源適不適於並行:
源 | 可分解性 |
---|---|
ArrayList | 極佳 |
LinkedList | 差 |
IntStream.range | 極佳 |
Stream.iterate | 差 |
HashSet | 好 |
TreeSet | 好 |