1. 程式人生 > >《JAVA8開發指南》為什麼你需要關注 JAVA8

《JAVA8開發指南》為什麼你需要關注 JAVA8

本文翻譯自《JAVA開發指南》第一章  

作者:Raoul-Gabriel Urma   譯者:二進位制的蛇

本章包含

  • 程式碼的可讀性
  • 多核
  • 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。

 轉載自併發程式設計網 – ifeve.com本文連結地址: 《JAVA8開發指南》為什麼你需要關注 JAVA8