為什麼您已經需要升級到 Java 8
已發表: 2022-03-11Java 平台的最新版本 Java 8 於一年多前發布。 許多公司和開發人員仍在使用以前的版本,這是可以理解的,因為從一個平台版本遷移到另一個版本存在很多問題。 即便如此,許多開發人員仍在使用舊版本的 Java 開發新應用程序。 這樣做的充分理由很少,因為 Java 8 為該語言帶來了一些重要的改進。
Java 8 中有許多新特性。我將向您展示一些最有用和最有趣的特性:
- Lambda 表達式
- 用於處理集合的流 API
- 使用
CompletableFuture
進行異步任務鏈 - 全新的時間 API
Lambda 表達式
lambda是一個代碼塊,它可以被引用並傳遞給另一段代碼以供將來執行一次或多次。 例如,其他語言中的匿名函數是 lambda。 與函數一樣,lambda 可以在執行時傳遞參數,從而修改其結果。 Java 8 引入了lambda 表達式,它提供了一種簡單的語法來創建和使用 lambda。
讓我們看一個如何改進我們的代碼的示例。 這裡我們有一個簡單的比較器,它通過模 2 比較兩個Integer
數值:
class BinaryComparator implements Comparator<Integer>{ @Override public int compare(Integer i1, Integer i2) { return i1 % 2 - i2 % 2; } }
將來可能會在需要此比較器的代碼中調用此類的實例,如下所示:
... List<Integer> list = ...; Comparator<Integer> comparator = new BinaryComparator(); Collections.sort(list, comparator); ...
新的 lambda 語法讓我們可以更簡單地做到這一點。 這是一個簡單的 lambda 表達式,它與BinaryComparator
的compare
方法做同樣的事情:
(Integer i1, Integer i2) -> i1 % 2 - i2 % 2;
該結構與函數有許多相似之處。 在括號中,我們設置了一個參數列表。 語法->
表明這是一個 lambda。 在這個表達式的右側,我們設置了 lambda 的行為。
現在我們可以改進前面的例子:
... List<Integer> list = ...; Collections.sort(list, (Integer i1, Integer i2) -> i1 % 2 - i2 % 2); ...
我們可以用這個對象定義一個變量。 讓我們看看它的外觀:
Comparator<Integer> comparator = (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;
現在我們可以重用這個功能,像這樣:
... List<Integer> list1 = ...; List<Integer> list2 = ...; Collections.sort(list1, comparator); Collections.sort(list2, comparator); ...
請注意,在這些示例中,lambda 以與前面示例中傳遞BinaryComparator
實例相同的方式傳遞給sort()
方法。 JVM 如何知道正確解釋 lambda?
為了允許函數將 lambda 作為參數,Java 8 引入了一個新概念:函數式接口。 函數式接口是只有一個抽象方法的接口。 事實上,Java 8 將 lambda 表達式視為函數式接口的特殊實現。 這意味著,為了接收一個 lambda 作為方法參數,該參數的聲明類型只需要是一個函數式接口。
當我們聲明一個函數式接口時,我們可以添加@FunctionalInterface
符號來向開發人員展示它是什麼:
@FunctionalInterface private interface DTOSender { void send(String accountId, DTO dto); } void sendDTO(BisnessModel object, DTOSender dtoSender) { //some logic for sending... ... dtoSender.send(id, dto); ... }
現在,我們可以調用方法sendDTO
,傳入不同的 lambdas 來實現不同的行為,如下所示:
sendDTO(object, ((accountId, dto) -> sendToAndroid(accountId, dto))); sendDTO(object, ((accountId, dto) -> sendToIos(accountId, dto)));
方法參考
Lambda 參數允許我們修改函數或方法的行為。 正如我們在最後一個示例中看到的,有時 lambda 僅用於調用另一個方法( sendToAndroid
或sendToIos
)。 對於這種特殊情況,Java 8 引入了一種方便的簡寫方式:方法引用。 這種縮寫語法表示調用方法的 lambda,並具有objectName::methodName
形式。 這使我們可以使前面的示例更加簡潔易讀:
sendDTO(object, this::sendToAndroid); sendDTO(object, this::sendToIos);
在這種情況下,方法sendToAndroid
和sendToIos
在this
中實現。 我們也可以引用另一個對像或類的方法。
流 API
Java 8 以全新的 Stream API 的形式帶來了使用Collections
的新功能。 這個新功能由java.util.stream
包提供,旨在為使用集合進行編程提供更實用的方法。 正如我們將看到的,這在很大程度上要歸功於我們剛剛討論的新的 lambda 語法。
Stream API 提供了對集合的簡單過濾、計數和映射,以及從中獲取信息切片和子集的不同方法。 由於函數式語法,Stream API 允許使用更短、更優雅的代碼來處理集合。
讓我們從一個簡短的例子開始。 我們將在所有示例中使用此數據模型:
class Author { String name; int countOfBooks; } class Book { String name; int year; Author author; }
假設我們需要打印books
收藏中的所有作者,這些作者在 2005 年之後寫了一本書。在 Java 7 中我們將如何做到這一點?
for (Book book : books) { if (book.author != null && book.year > 2005){ System.out.println(book.author.name); } }
我們將如何在 Java 8 中做到這一點?
books.stream() .filter(book -> book.year > 2005) // filter out books published in or before 2005 .map(Book::getAuthor) // get the list of authors for the remaining books .filter(Objects::nonNull) // remove null authors from the list .map(Author::getName) // get the list of names for the remaining authors .forEach(System.out::println); // print the value of each remaining element
這只是一種表達方式! 在任何Collection
上調用方法stream()
會返回一個Stream
對象,該對象封裝了該集合的所有元素。 這可以使用 Stream API 中的不同修飾符進行操作,例如filter()
和map()
。 每個修改器都返回一個帶有修改結果的新Stream
對象,可以進一步對其進行操作。 .forEach()
方法允許我們對結果流的每個實例執行一些操作。
這個例子還展示了函數式編程和 lambda 表達式之間的密切關係。 請注意,傳遞給流中每個方法的參數要么是自定義 lambda,要么是方法引用。 從技術上講,每個修飾符都可以接收任何功能接口,如上一節所述。
Stream API 幫助開發人員從一個新的角度看待 Java 集合。 現在想像一下,我們需要獲取每個國家/地區可用語言的Map
。 這將如何在 Java 7 中實現?
Map<String, Set<String>> countryToSetOfLanguages = new HashMap<>(); for (Locale locale : Locale.getAvailableLocales()){ String country = locale.getDisplayCountry(); if (!countryToSetOfLanguages.containsKey(country)){ countryToSetOfLanguages.put(country, new HashSet<>()); } countryToSetOfLanguages.get(country).add(locale.getDisplayLanguage()); }
在 Java 8 中,事情變得更加簡潔:
import java.util.stream.*; import static java.util.stream.Collectors.*; ... Map<String, Set<String>> countryToSetOfLanguages = Stream.of(Locale.getAvailableLocales()) .collect(groupingBy(Locale::getDisplayCountry, mapping(Locale::getDisplayLanguage, toSet())));
collect()
方法允許我們以不同的方式收集流的結果。 在這裡,我們可以看到它首先按國家分組,然後按語言映射每個組。 ( groupingBy()
和toSet()
都是Collectors
類的靜態方法。)

Stream API 還有很多其他功能。 完整的文檔可以在這裡找到。 我建議進一步閱讀以更深入地了解該軟件包必須提供的所有強大工具。
帶有CompletableFuture
的異步任務鏈
在 Java 7 的java.util.concurrent
包中,有一個接口Future<T>
,它允許我們獲取未來某個異步任務的狀態或結果。 要使用此功能,我們必須:
- 創建一個
ExecutorService
,它管理異步任務的執行,並且可以生成Future
對象來跟踪它們的進度。 - 創建一個
Runnable
可運行任務。 - 在
ExecutorService
中運行任務,這將提供一個Future
來訪問狀態或結果。
為了利用異步任務的結果,有必要使用Future
接口的方法從外部監視其進度,當它準備好時,顯式檢索結果並使用它們執行進一步的操作。 這在沒有錯誤的情況下實現起來可能相當複雜,尤其是在具有大量並發任務的應用程序中。
然而,在 Java 8 中, Future
概念得到了進一步的發展,使用CompletableFuture<T>
接口,它允許創建和執行異步任務鏈。 它是在 Java 8 中創建異步應用程序的強大機制,因為它允許我們在完成後自動處理每個任務的結果。
讓我們看一個例子:
import java.util.concurrent.CompletableFuture; ... CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(() -> blockingReadPage()) .thenApply(this::getLinks) .thenAccept(System.out::println);
CompletableFuture.supplyAsync
方法在默認的Executor
(通常是ForkJoinPool
)中創建一個新的異步任務。 任務完成後,其結果將自動作為參數提供給函數this::getLinks
,該函數也在新的異步任務中運行。 最後,第二階段的結果會自動打印到System.out
。 thenApply()
和thenAccept()
只是幾個有用的方法中的兩個,它們可以幫助您在不手動使用Executors
的情況下構建並發任務。
CompletableFuture
使管理複雜異步操作的順序變得容易。 假設我們需要創建一個包含三個任務的多步數學運算。 任務 1和任務 2使用不同的算法來尋找第一步的結果,我們知道其中只有一個會工作,而另一個會失敗。 然而,哪一個有效取決於輸入數據,我們事先並不知道。 這些任務的結果必須與任務 3的結果相加。 因此,我們需要找到任務 1或任務 2的結果,以及任務 3的結果。 為了實現這一點,我們可以這樣寫:
import static java.util.concurrent.CompletableFuture.*; ... Supplier<Integer> task1 = (...) -> { ... // some complex calculation return 1; // example result }; Supplier<Integer> task2 = (...) -> { ... // some complex calculation throw new RuntimeException(); // example exception }; Supplier<Integer> task3 = (...) -> { ... // some complex calculation return 3; // example result }; supplyAsync(task1) // run task1 .applyToEither( // use whichever result is ready first, result of task1 or supplyAsync(task2), // result of task2 (Integer i) -> i) // return result as-is .thenCombine( // combine result supplyAsync(task3), // with result of task3 Integer::sum) // using summation .thenAccept(System.out::println); // print final result after execution
如果我們檢查 Java 8 如何處理這個問題,我們將看到所有三個任務將同時異步運行。 儘管任務 2因異常而失敗,但最終結果將被成功計算和打印。
CompletableFuture
使構建具有多個階段的異步任務變得更加容易,並為我們提供了一個簡單的界面,用於準確定義在每個階段完成時應採取的操作。
Java 日期和時間 API
正如Java自己承認的那樣:
在 Java SE 8 版本之前,Java 日期和時間機制由
java.util.Date
、java.util.Calendar
和java.util.TimeZone
類以及它們的子類(例如java.util.GregorianCalendar
。 這些類有幾個缺點,包括
- Calendar 類不是類型安全的。
- 因為這些類是可變的,所以它們不能在多線程應用程序中使用。
- 由於不尋常的月份數和缺乏類型安全性,應用程序代碼中的錯誤很常見。”
Java 8 終於通過新的java.time
包解決了這些長期存在的問題,其中包含用於處理日期和時間的類。 它們都是不可變的,並且具有類似於流行的框架 Joda-Time 的 API,幾乎所有 Java 開發人員在他們的應用程序中都使用它,而不是原生的Date
、 Calendar
和TimeZone
。
下面是這個包中一些有用的類:
-
Clock
- 顯示當前時間的時鐘,包括當前時刻、日期和帶時區的時間。 -
Duration
和Period
- 時間量。Duration
使用基於時間的值,例如“76.8 秒”,而Period
使用基於日期的值,例如“4 年 6 個月和 12 天”。 -
Instant
- 瞬時時間點,有多種格式。 -
LocalDate
,LocalDateTime
,LocalTime
,Year
,YearMonth
- 日期、時間、年份、月份或它們的某種組合,在 ISO-8601 日曆系統中沒有時區。 -
OffsetDateTime
,OffsetTime
- 在 ISO-8601 日曆系統中與 UTC/格林威治有偏移的日期時間,例如“2015-08-29T14:15:30+01:00”。 -
ZonedDateTime
- 在 ISO-8601 日曆系統中具有關聯時區的日期時間,例如“1986-08-29T10:15:30+01:00 Europe/Paris”。
有時,我們需要找到一些相對日期,例如“每月的第一個星期二”。 對於這些情況java.time
提供了一個特殊的類TemporalAdjuster
。 TemporalAdjuster
類包含一組標準的調整器,可作為靜態方法使用。 這些使我們能夠:
- 查找該月的第一天或最後一天。
- 查找下個月或上個月的第一天或最後一天。
- 查找一年中的第一天或最後一天。
- 查找下一年或上一年的第一天或最後一天。
- 查找一個月內的第一天或最後一天,例如“六月的第一個星期三”。
- 查找一周中的下一天或上一天,例如“下週四”。
這是一個簡短的示例,如何獲取每月的第一個星期二:
LocalDate getFirstTuesday(int year, int month) { return LocalDate.of(year, month, 1) .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY)); }
Java 8 總結
正如我們所見,Java 8 是 Java 平台的劃時代版本。 語言發生了很多變化,尤其是引入了 lambda,這代表了將更多函數式編程能力帶入 Java 的舉措。 Stream API 是一個很好的例子,lambdas 如何改變我們使用我們已經習慣的標準 Java 工具的方式。
此外,Java 8 還帶來了一些用於處理異步編程的新特性,並對其日期和時間工具進行了急需的檢修。
這些變化共同代表了 Java 語言向前邁出的一大步,使 Java 開發更有趣、更高效。