《JAVA8開發指南》為什麼你需要關注 JAVA8
本章包含
- 代碼的可讀性
- 多核
- JAVA8特性的快速指南
JAVA8:為什麼你需要關注?
JAVA已經更新了!在 2014 年 3 月,JAVA發布了新版本-JAVA8,JAVA8 引入的一些新特性可能會改變你日常中基本的編碼方式。但不用擔心,這本簡潔的指南會帶著你掌握一些要領,現在你就可以開始閱讀。
第一章列舉了 JAVA8 中增加的主要功能概況。接下來的兩章則關注 JAVA8 的主要特性: lambda 表達式 和streams(流)。
驅動 JAVA8 改進的兩大動機:
- 代碼可讀性
- 更加簡化的多核支持
代碼可讀性
JAVA 是比較繁瑣的,這導致了可讀性的降低。換句話說,它需要很多代碼才能表達一個簡單的概念。
舉個例子:給一個票據列表按數值遞減排序。在 JAVA8 之前,你需要寫這樣寫代碼:
Collections.sort(invoices,new Comparator<Invoice>(){
public int compare(Invoice inv1,Invoice inv2){
return Double.compare(inv2.getAmount(),inv1.getAmount());
}
});
像這種編碼方式,你需要關注很多關於如何排序的小細節。換句話說,它很難對上麵陳述的問題(票據排序問題)用一個簡單的解決方案來描述。你需要創建一個 Comparator(比較器) 對象來定義如何對兩個票據進行比較。為了實現比較,你需要提供一個 compare() 方法的實現。在閱讀這段代碼的時候,你必須花費較多而的時間來理解實現的細節而不是理解實際的問題描述。
在 JAVA8 中,你可以用下麵這段代碼來進行重構:
invoices.sort(comparingDouble(Invoice::getAmount).reversed());
現在,問題描述得清晰明了。(不要擔心新的語法,我將會進行一個簡短的介紹)。這就是你為什麼應該關注 JAVA8 的原因,它帶來了新的語言特色和API的更新,能讓你寫出更加簡潔可讀的代碼。
此外,JAVA8 引入了一種新的API,叫做 Streams API。它可以讓你寫出可讀性強的代碼來處理數據。Streams API 支持幾種內建的操作,以更簡單的方式來處理數據。例如,在商業運營環境中,你可能希望產生一份日結報告用來過濾和聚合多個部門的票據信息。幸運的是,通過 Streams API,你可以不用去擔心如何實現這種查詢。這種方法和你使用SQL相似,事實上,在SQL中你可以製定一個查詢而不用去關心它內部的實現。例如,假設你想要找出所有票據中數據大於1000的票據單號:
SELECT id FROM invoices WHERE amount > 1000
通常把這種查詢的書寫風格稱作為聲明式編程。這就是你將用 Streams API 解決這個問題的方式:
List<Integer> ids = invoices.stream()
.filter(inv->inv.getAmount > 1000 )
.map(Invoice::getId)
.collect(Collections.toList());
現在不要關注這些代碼的細節,在第 3 章你會更深入地了解 Streams API。現在,把 Streams API 看作是一種新的抽象概念,以更好的可讀性來處理數據的查詢。
多核
JAVA8 中第二個大的改動就是多核處理時所不可缺少的。在過去,你的電腦隻有一個處理單元。要想更快地運行一個應用程序通常意味著提升處理單元的性能。不幸的是,處理單元的處理速度已經不再提升。今天,絕大多數的電腦和移動設備都有多個處理單元(簡稱核)在並行工作。應用程序應該利用不同的處理單元來提升性能。JAVA 應用程序中經典的實現方式就是利用線程。不幸的是使用線程往往是困難和容易出錯的,一般是為專家準備的。JAVA8 中的 Streams API 可以讓你很簡單地對數據查詢進行並發處理。例如,你僅僅需要用 parallelStream() 替代 stream() 即可實現先前代碼的並發運行:
List<Integer> ids = invoices
.parallelStream()
.filter(inv->inv.getAmount > 1000 )
.map(Invoice::getId)
.collect(Collections.toList());
在第 3 章,我會探討使用 parallel streams 的細節及其最佳實現。
JAVA8特性的快速指南
這部分會提供 JAVA8 一些主要的新特性的概述,並附帶一些代碼例子,向你展示一些可用的概念。接下來的兩章會重點描述 JAVA8 的兩大重要特性:lambda 表達式 和 streams。
lambda 表達式
lambda 表達式讓你用一種簡潔的方式去避免一大塊的代碼。例如,你需要一個線程來執行一個任務。你可以創建一個 Runnable 對象,然後做為參數傳遞給 Thread:
Runnable runnable =new Runnable(){
@Override
public void run(){
System.out.println(“Hi”);
}
}
new Thread(runnable).start();
另一種辦法,使用 lambda 表達式,你可以用一種更加易讀的方式去重構先前的代碼:
new Thread(()->System.out.println(“Hi”)).start();
在第 2 章,你將會學習關於 lambda 表達式的更多重要的細節。
方法引用
方法引用聯合 lambda 表達式組成了一個新的特性。它可以讓你快速的選擇定義在類中的已經存在的方法。例如:你需要忽略大小寫去比較一個字符串列表。一般地,你將會像這樣寫代碼:
List<String> strs = Arrays.asList(“C”,”a”,”A”,”b”);
Collections.sort(strs,new Comparator<String>(){
@Override
public int compare(String s1,String s2){
return s1.compareToIgnoreCase(s2);
}
});
上麵展示的這段代碼可謂是極度詳細。畢竟你需要的隻是 compareToIgnoreCase 方法。利用方法引用,就可以明確地表明應該用 String 類中定義的 compareToIgnoreCase 方法來進行比較操作:
Collections.sort(strs,String::compareToIgnoreCase);
String::compareToIgnoreCase 這部分代碼就是一個方法引用。它使用了一種特殊語法 :: (關於方法引用的更多細節會在接下來的章節中描述)。
Streams
幾乎每一個 JAVA 應用程序都會創建和處理集合。它們是許多編程任務中的基石,可以讓你聚合及處理數據。然而,處理集合過於繁瑣而且難於並發。接下來的這段代碼會說明處理集合會是多麼的繁瑣。從一個票據列表中找到訓練相關的票據ID並按票據的數值排序:
List<Invoice> trainingInvoices = new ArraysList<>();
for(Invoice inv:invoices){
if(inv.getTitle().contains(“Training”)){
trainingInvoices.add(inv);
}
}
Collections.sort(trainingInvoices,new Comparator<Invoice>(){
public int compare(Invoice inv1,Invoice inv2){
return inv2.getAmount().compareTo(inv1.getAmount());
}
});
List<Integer> invoiceIds = new Arrays<>();
for(Invoice inv : trainingInvoices){
invoiceIds.add(inv.getId());
}
JAVA8 引進了一種新的抽象概念叫做 Stream ,可以讓你以一種聲明式的方式進行數據的處理。在 JAVA8 中你可以使用 streams 去重構之前的代碼,就像這樣:
List<Integer> invoiceIds = invoices.stream()
.filter(inv -> inv.getTitle().contains(“Training”))
.sort(comparingDouble(Invoice::getAmount).reversed())
.map(Invoice::getId)
.collect(Collections.toList());
另外,你可以通過使用集合中 parallelStream 方法取代 stream 方式來明確的並發執行一個 stream(現在不要關注這段代碼的實現細節,你將會在第3 章中學到更多關於 Streams API 的知識)。
增強接口
JAVA8 中對接口進行了兩大改造,使其可以在接口中聲明具體的方法。
第一、JAVA8 引入了默認方法,它可以讓你在接口聲明的方法中增加實現體,作為一種將 JAVA API 演變為向後兼容的機製。例如,你會看到在 JAVA8 的 List 接口中現在支持一種排序方法,像下麵這麼定義的:
default void sort(Comparator<? super E> c){
Collections.sort(this,c);
}
默認方法也可以當做一種多重繼承的機製來提供服務。事實上,在 JAVA8 之前,類已經可以實現多接口。現在,你可以從多個不同的接口中繼承其默認方法。注意,為了防止出現類似 C++ 中的繼承問題(例如鑽石問題),JAVA8 定義了明確的規則。
第二、接口現在也可以擁有靜態方法。它和定義一個接口,同時用一個內部類定義一個靜態方法去進行接口的實例化是同一種機製。例如,JAVA 中有 Collection 接口和 定義了通用靜態方法的 Collections 類,現在這些通用的靜態也可以放在接口中。例如,JAVA8 中的 stream 接口是這樣定義靜態方法的:
public static <T> Stream<T> of (T…values){
return Arrays.stream(values);
}
新的日期時間 API
JAVA8 引入了一套新的日期時間 API ,修複了之前舊的 Date 和 Calendar 類的許多問題。這套新的日期時間 API 包含兩大主要原則:
領域驅動設計
新的日期時間 API 采用新的類來精確地表達多種日期和時間的概念。例如,你可以用 Period 類去表達一個類似於 “2個月零3天(63天)”,用 ZonedDateTime 去表達一個帶有時間區域的時間。每一個類提供特定領域的方法且采用流式風格。因此,你可以通過方法鏈寫出可讀性更強的代碼。例如,接下來的這段代碼會展示如何創建一個 LocalDateTime 對象而且增加 2小時30分:
LocalDateTime coffeeBreak = LocalDateTime.now()
.plusHours(2)
.plusMinutes(30);
不變性
Date(日期) 和 Calendar(日曆)的其中一個問題就是他們是非線程安全的。此外,開發者使用 Dates (日期) 作為他們的API的一部分時,Dates(日期)的值可能會被意外的改變。為了避免這種潛在的BUG,在新的日期時間 API 中的所有類都是不可變的。
也就是說,在新的日期時間 API 中,你不能改變對象的狀態,取而代之的是,你調用一個方法會返回一個帶有更新的值的新對象。下麵的這段代碼列舉了多種在新的日期時間 API 中可用的方法:
ZoneId london = ZoneId.of(“Europe/London”);
LocalDate july4 = LocalDate.of(2014,Month.JULY,4);
LocalTime early = LocalTime.parse(“08:05″);
ZonedDateTime flightDeparture = ZonedDateTime.if(july4,early,london);
System.out.println(flightDeparture);
LocalTime from = LocalTime.from(flightDeparture);
System.out.println(from)
ZonedDateTime touchDown = ZonedDateTime.of(july4,
LocalTime.of(11,35),
ZoneId.of(“Europe/Stockholm”));
Duration flightLength = Duration.between(flightDeparture,touchDown);
System.out.println(flightLength);
ZonedDateTime now = ZonedDateTime.now();
Duration timeHere = Duration.between(touchDown,now);
System.out.println(timeHere);
這段代碼會產生一份類似於這樣的輸出:
2015-07-04T08+01:00[Europe/London]
08:45
PT1H50M
PT269H46M55.736S
CompletableFuture
JAVA8 中引入了一種新的方式來進行程序的異步操作,即使用一個新類 CompletableFuture 。它是舊的 Future 類的改進版,這種靈感來自於類似 Streams API 所選擇的設計(也就是聲明式的風格和流式方法鏈)。換句話說,你可以使用聲明式的操作來組裝多種異步任務。下麵這個例子需要同時並發地查詢兩個獨立(封閉)的任務。一個價格搜索服務與一個頻率計算交織在一起。一但這兩個服務返回了可用的結果,你就可以把他們的結果組合在一起,計算並輸入在GBP中的價格:
findBestPrice(“iPhone6″)
.thenCombine(lookupExchangeRate(Currency.GBP),this::exchange)
.thenAccept(localAmount -> System.out.printf(“It will cost you %f GBP \n”,localAmount));
private CompletableFuture<Price> findBestPrice(String productName){
return CompletableFuture.supplyAsync(() -> priceFinder.findBestPrice(productName));
}
private CompletableFuture<Double> lookupExchangeRate(Currency localCurrency){
return CompletableFuture.supplyAsync(() -> exchangeService.lookupExchangeRate(Currency.USD,localCurrency));
}
Optional
JAVA8 中引入了一個新的類叫做 Optional。靈感來自於函數式編程語言,它的引入是為了當值為空或缺省時你的代碼庫能容許更好的模型。
把它當作是一種單值容器,這種情況下如果沒有值則為空。Optional 已經在可供選擇的集合框架(比如 Guava)中可用,但現在它作為 JAVA API 的一部分,可用於JAVA中。Optional 的另一個好處是它可以幫助你避免空指針異常。事實上,Optional 定義了方法強製你去明確地檢查值存在還是缺省。下麵這段代碼就是一個例子:
getEventWithId(10).getLocation().getCity();
如果 getEventWithId(10) 返回 NULL,那麼代碼就會拋出 NullPointerException(空指針異常)。如果 getLocation() 返回 NULL,它也會拋出 NullPointerException(空指針異常)。換句話說,如果任何一個方法返回 NULL,就會拋出 NullPointerException(空指針異常)。你可以采用防禦性的檢查來避免這種異常,就像下麵這樣:
public String getCityForEvent(int id ){
Event event = getEventWithId(id);
if( event != null ){
Location location = event.getLocation();
if(location != null ){
return location.getCity();
}
}
return “ABC”;
}
在這段代碼中,一個事件可能會有一個與之關聯的地點。然而,一個地點總是會與一個與之關聯的城市。不幸的是,它通常容易忘記去檢查值是否為 NULL 。此外,這段代碼太詳細而且難於跟蹤。使用 Optional 你可以用更加簡潔清晰的方式去重構這段代碼,就像這樣:
public String getCityForEvent(int id){
Optional.ofNullable(getEventWithId(id))
.flatMap(this::getLocation)
.map(this::getCity)
.ofElse(“TBC”);
}
在任何時候,如果方法返回一個空的 Optional 對象,你就會得到默認值 TBC。
最後更新:2017-05-19 14:33:37