JavaRush /Java Blog /Random-TW /Java 8 指南。1 部分。
ramhead
等級 13

Java 8 指南。1 部分。

在 Random-TW 群組發布

“Java 仍然存在——人們開始理解它。”

歡迎閱讀我對 Java 8 的介紹。本指南將引導您逐步了解該語言的所有新功能。透過簡短的程式碼範例,您將學習如何使用介面預設方法lambda 表達式引用方法可重複註解。讀完本文後,您將熟悉 API 的最新更改,例如流、函數介面、關聯擴充功能和新的 Date API。沒有無聊的文字牆——只有一堆附註解的程式碼片段。享受!

介面的預設方法

Java 8 讓我們可以透過使用default 關鍵字來新增在介面中實作的非抽象方法。此功能也稱為 擴充方法。這是我們的第一個範例: interface Formula { double calculate(int a); default double sqrt(int a) { return Math.sqrt(a); } } 除了抽象方法 calculate之外, Formula介面還定義了一個預設方法 sqrt。實作 Formula介面的類別僅實作抽象 計算方法。預設的 sqrt方法可以直接使用。 公式 Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } }; formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0 物件作為匿名物件實作。程式碼相當令人印象深刻:6 行程式碼就可以簡單地計算 sqrt(a * 100)。正如我們將在下一節中進一步看到的,在 Java 8 中實作單一方法物件有一種更具吸引力的方法。

Lambda 表達式

讓我們從一個簡單的範例開始,說明如何在 Java 的早期版本中對字串陣列進行排序: 統計輔助方法 Collections.sort採用一個列表和一個比較器 來對給定列表的元素進行排序。經常發生的情況是您建立匿名比較器並將它們傳遞給排序方法。Java 8 不再總是建立匿名對象,而是讓您能夠使用更少的語法、 lambda 表達式: 如您所見,程式碼更短且更易於閱讀。但這裡它變得更短: 對於單行方法,您可以去掉 {}大括號和 return關鍵字。但這裡的程式碼變得更短: Java 編譯器知道參數的類型,因此您也可以將它們省略。現在讓我們更深入地了解如何在現實生活中使用 lambda 表達式。 List names = Arrays.asList("peter", "anna", "mike", "xenia"); Collections.sort(names, new Comparator () { @Override public int compare(String a, String b) { return b.compareTo(a); } }); Collections.sort(names, (String a, String b) -> { return b.compareTo(a); }); Collections.sort(names, (String a, String b) -> b.compareTo(a)); Collections.sort(names, (a, b) -> b.compareTo(a));

功能介面

lambda 表達式如何適應 Java 類型系統?每個 lambda 對應於介面定義的給定類型。而所謂的函數式介面必須恰好包含一個宣告的抽象方法。每個給定類型的 lambda 表達式都會對應這個抽象方法。由於預設方法不是抽象方法,因此您可以自由地將預設方法新增到函數式介面中。我們可以使用任意介面作為 lambda 表達式,前提是該介面僅包含一個抽象方法。為了確保您的介面符合這些條件,您必須新增 @FunctionalInterface註解。該註解將通知編譯器該介面必須僅包含一個方法,如果在該介面中遇到第二個抽象方法,則會拋出錯誤。範例: 請記住,即使尚未聲明 @FunctionalInterface註釋,此程式碼也將有效。 @FunctionalInterface interface Converter { T convert(F from); } Converter converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123

對方法和建構函數的引用

透過使用統計方法參考可以進一步簡化上面的範例: Java 8 允許您使用 ::關鍵字符號 傳遞對方法和建構函式的參考。上面的例子展示如何使用統計方法。但我們也可以引用物件上的方法: 讓我們來看看如何使用 ::用來建構函數。首先,讓我們定義一個具有不同建構函式的範例: 接下來,我們定義用於建立新 人員物件的 PersonFactory工廠介面: Converter converter = Integer::valueOf; Integer converted = converter.convert("123"); System.out.println(converted); // 123 class Something { String startsWith(String s) { return String.valueOf(s.charAt(0)); } } Something something = new Something(); Converter converter = something::startsWith; String converted = converter.convert("Java"); System.out.println(converted); // "J" class Person { String firstName; String lastName; Person() {} Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } } interface PersonFactory

{ P create(String firstName, String lastName); } 我們沒有手動實作工廠,而是使用建構函式參考將所有內容綁定在一起: 我們透過Person::new建立對Person類別 的建構函式的參考。Java 編譯器會透過比較建構函式的簽章與PersonFactory.create方法的簽章來自動呼叫適當的建構子。 PersonFactory personFactory = Person::new; Person person = personFactory.create("Peter", "Parker");

拉姆達區

從 lambda 表達式組織對外部作用域變數的存取類似於從匿名物件進行存取。 您可以從本機範圍存取最終變量,以及實例欄位和聚合變數。
訪問局部變數
我們可以從 lambda 表達式的作用域中 讀取帶有 final修飾符的局部變數: 但與匿名物件不同的是,變數不需要宣告為 final就可以從lambda表達式存取。此程式碼也是正確的: 但是, num變數必須保持不可變,即 對於程式碼編譯來說是隱式的 final。以下程式碼將無法編譯: 也不允許在 lambda 表達式中 更改 num 。 final int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); num = 3;
訪問實例字段和統計變量
與局部變數不同,我們可以讀取和修改 lambda 表達式內的實例欄位和統計變數。我們從匿名物件中知道這種行為。 class Lambda4 { static int outerStaticNum; int outerNum; void testScopes() { Converter stringConverter1 = (from) -> { outerNum = 23; return String.valueOf(from); }; Converter stringConverter2 = (from) -> { outerStaticNum = 72; return String.valueOf(from); }; } }
存取介面的預設方法
還記得第一部分有 公式實例 的範例嗎? Formula介面定義了一個預設的 sqrt方法,可以從 Formula的每個實例(包括匿名物件)存取該方法。這不適用於 lambda 表達式。無法在 lambda 表達式內存取預設方法。以下程式碼無法編譯: Formula formula = (a) -> sqrt( a * 100);

內建功能介面

JDK 1.8 API 包含許多內建的函數介面。其中一些在以前的 Java 版本中是眾所周知的。例如 ComparatorRunnable。這些介面經過擴展以包含使用 @FunctionalInterface註釋的 lambda 支援。但 Java 8 API 也充滿了新的功能接口,這將使您的生活更輕鬆。其中一些介面在 Google 的 Guava庫中是眾所周知的。即使您熟悉這個庫,您也應該仔細看看這些介面是如何擴展的,以及一些有用的擴充方法。
謂詞
謂詞是具有一個參數的布林函數。此介麵包含使用謂詞建立複雜邏輯表達式(and、or、negate)的各種預設方法 Predicate predicate = (s) -> s.length() > 0; predicate.test("foo"); // true predicate.negate().test("foo"); // false Predicate nonNull = Objects::nonNull; Predicate isNull = Objects::isNull; Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = isEmpty.negate();
功能
函數接受一個參數並產生一個結果。預設方法可用於將多個函數組合成一個鏈(compose、andThen)。 Function toInteger = Integer::valueOf; Function backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123"
供應商
供應商傳回一種或另一種類型的結果(實例)。與函數不同,提供者不接受參數。 Supplier personSupplier = Person::new; personSupplier.get(); // new Person
消費者
消費者用單一參數表示介面方法。 Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker"));
比較器
我們從 Java 的早期版本就知道比較器了。Java 8 可讓您為介面新增各種預設方法。 Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0
選項
Optionals 介面沒有功能,但它是防止 NullPointerException的一個很好的實用程式。這是下一節的重點,因此讓我們快速了解該介面的工作原理。Optional 介面是一個簡單的容器,用於儲存可以為 null或非 null 的值。想像一下,一個方法可以回傳一個值,也可以不回傳任何內容。在 Java 8 中,您不傳回 null,而是傳回一個 Optional實例。 Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0

溪流

java.util.Stream是對其執行一個或多個操作的元素序列。每個 Stream 操作要么是中間操作,要么是最終操作。終端操作傳回特定類型的結果,而中間操作會傳回流物件本身,從而允許建立方法呼叫鏈。Stream 是一個接口,類似於列表和集合的 java.util.Collection(不支援映射)。每個 Stream 操作可以順序執行,也可以並行執行。讓我們看看串流是如何運作的。首先,我們將以字串清單的形式建立範例程式碼: Java 8 中的集合得到了增強,因此您可以透過呼叫 Collection.stream()Collection.parallelStream()非常簡單地建立流。下一節將說明最重要、最簡單的流操作。 List stringCollection = new ArrayList<>(); stringCollection.add("ddd2"); stringCollection.add("aaa2"); stringCollection.add("bbb1"); stringCollection.add("aaa1"); stringCollection.add("bbb3"); stringCollection.add("ccc"); stringCollection.add("bbb2"); stringCollection.add("ddd1");
篩選
過濾器接受謂詞來過濾流的所有元素。此操作是中間操作,它允許我們對產生的(過濾後的)結果呼叫其他流操作(例如 forEach)。ForEach 接受將對已過濾流的每個元素執行的操作。ForEach 是一個終端操作。此外,呼叫其他操作也是不可能的。 stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1"
已排序
Sorted 是一個中間操作,它傳回流的排序表示。 除非您指定Comparator,否則元素會按正確的順序排序。 stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa1", "aaa2" 請記住,sorted 建立流的排序表示,而不影響集合本身。 stringCollection元素的順序保持不變: System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
地圖
中間映射操作使用結果函數將每個元素轉換為另一個物件。以下範例將每個字串轉換為大寫字串。但您也可以使用映射將每個物件轉換為不同的類型。產生的流物件的類型取決於傳遞給映射的函數類型。 stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
匹配
可以使用各種匹配操作來測試流關係中特定謂詞的真實性。所有匹配操作都是終端操作並返回布林結果。 boolean anyStartsWithA = stringCollection .stream() .anyMatch((s) -> s.startsWith("a")); System.out.println(anyStartsWithA); // true boolean allStartsWithA = stringCollection .stream() .allMatch((s) -> s.startsWith("a")); System.out.println(allStartsWithA); // false boolean noneStartsWithZ = stringCollection .stream() .noneMatch((s) -> s.startsWith("z")); System.out.println(noneStartsWithZ); // true
數數
Count 是一個終端操作,它以long 形式 傳回流的元素數量。 long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3
減少
這是一個終端操作,它使用傳遞的函數來縮短流的元素。結果將是 一個包含縮短值的可選值。 Optional reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

平行流

如上所述,流可以是順序的或並行的。順序流操作在串行執行緒上執行,而平行流操作在多個並行執行緒上執行。以下範例示範如何使用並行流輕鬆提高效能。首先,讓我們建立一個包含唯一元素的大型清單: 現在我們將確定對該集合的流進行排序所花費的時間。 int max = 1000000; List values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); }
序列流
long t0 = System.nanoTime(); long count = values.stream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("sequential sort took: %d ms", millis)); // sequential sort took: 899 ms
平行流
long t0 = System.nanoTime(); long count = values.parallelStream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("parallel sort took: %d ms", millis)); // parallel sort took: 472 ms 正如您所看到的,兩個片段幾乎相同,但並行排序快了 50%。您所需要做的就是將 stream()更改為 parallelStream()

地圖

正如已經提到的,地圖不支援流。相反,地圖開始支援解決常見問題的新且有用的方法。 上面的程式碼應該要直觀: putIfAbsent警告我們不要寫額外的空檢查。 forEach接受一個為每個映射值執行的函數。這個範例展示瞭如何使用函數對映射值執行操作: 接下來,我們將學習如何刪除給定鍵的條目(僅當它映射到給定值時): 另一種好方法: 合併映射條目非常簡單: 合併如果給定鍵沒有條目,則將把鍵/值插入到映射中,或呼叫合併函數,這將更改現有條目的值。 Map map = new HashMap<>(); for (int i = 0; i < 10; i++) { map.putIfAbsent(i, "val" + i); } map.forEach((id, val) -> System.out.println(val)); map.computeIfPresent(3, (num, val) -> val + num); map.get(3); // val33 map.computeIfPresent(9, (num, val) -> null); map.containsKey(9); // false map.computeIfAbsent(23, num -> "val" + num); map.containsKey(23); // true map.computeIfAbsent(3, num -> "bam"); map.get(3); // val33 map.remove(3, "val3"); map.get(3); // val33 map.remove(3, "val33"); map.get(3); // null map.getOrDefault(42, "not found"); // not found map.merge(9, "val9", (value, newValue) -> value.concat(newValue)); map.get(9); // val9 map.merge(9, "concat", (value, newValue) -> value.concat(newValue)); map.get(9); // val9concat
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION