Java 8 的新特性和改進總覽

oschina發表於2013-05-02

  這篇文章是對Java 8中即將到來的改進做一個面向開發者的綜合性的總結,JDK的這一特性將會在2013年9月份釋出。

  在寫這篇文章的時候,Java 8的開發工作仍然在緊張有序的進行中,語言特新和API仍然有可能改變,我會盡我最大的努力保持這份文件跟得到Java 8的改動。

  Java 8的預覽版,也就是 “Project Lambda”,現在可以從java.net下載到。

  我使用了IntelliJ的預覽版做我的IDE,在我看來他是目前支援java 8特性最好的一個IDE,你可以從這裡下載到.

  由於我沒有找到Oracle釋出的Java 8的官方文件,所以目前Java 8的文件還只有本地版本,等Oracle公開文件的時候,我將會重新連結到官方文件。

  介面改善

  現在介面裡已經完全可以定義靜態方法了. 舉一個比較普遍的例子就是在java類庫中, 對於一些介面如Foo, 都會有一個有靜態方法的工具類Foos 來生成或者配合Foo物件例項來使用. 既然靜態方法可以存在於介面當中, 那麼大多數情況下 Foos工具類完全可以使用介面中的公共方法來代理 (或者將Foos置成package-private).

  除此之外更重要的就是, Java 8中介面可以定義預設的方法了.舉個例子,一個for-each迴圈的方法就可以加入到java.lang.Iterable中:

public default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action); for (T t : this) {
        action.accept(t);
    }
}

  在過去,java類庫的介面中新增方法基本上是不可能的. 在介面中新增方法意味著破壞了實現了這個介面的程式碼. 但是現在, 只要能夠提供一個正確明智的預設的方法的實現, java類庫的維護者就可以在介面中新增方法.

  Java 8中, 大量的預設方法已經被新增到核心的JDK介面中了. 稍候我會詳細介紹它們.

  為什麼不能用預設方法來過載equals,hashCode和toString?

  介面不能提供對Object類的任何方法的預設實現。特別是,這意味著從介面裡不能提供對equals,hashCode或toString的預設實現。

  這剛看起來挺奇怪的,但考慮到一些介面實際上是在文件裡定義他們的equals行為的。List介面就是一個例子了。因此,為什麼不允許這樣呢?

  Brian Goetz在這個問題上的冗長的回覆裡給出了4個原因。我這裡只說其中一個,因為那個已經足夠說服我了:

  它會變得更困難來推導什麼時候該呼叫預設的方法。現在它變得很簡單了:如果一個類實現了一個方法,那總是優先於預設的實現的。一旦所有介面的例項都是Object的子類,所有介面例項都已經有對equals/hashCode/toString的非預設實現。因此,一個在介面上這些的預設版本都是沒用的,它也不會被編譯。

  要看更多的話,看下由Brian Goetz寫的解釋: 對“允許預設方法來過載Object的方法”的回覆

  函式式介面

  Java 8 引入的一個核心概念是函式式介面。如果一個介面定義個唯一一個抽象方法,那麼這個介面就成為函式式介面。比如,java.lang.Runnable就是一個函式式介面,因為它只頂一個一個抽象方法:

public abstract void run();

  留意到“abstract”修飾詞在這裡是隱含的,因為這個方法缺少方法體。為了表示一個函式式介面,並非想這段程式碼一樣一定需要“abstract”關鍵字。

  預設方法不是abstract的,所以一個函式式介面裡可以定義任意多的預設方法,這取決於你。

  同時,引入了一個新的Annotation:@FunctionalInterface。可以把他它放在一個介面前,表示這個介面是一個函式式介面。加上它的介面不會被編譯,除非你設法把它變成一個函式式介面。它有點像@Override,都是宣告瞭一種使用意圖,避免你把它用錯。

  Lambdas

  一個函式式介面非常有價值的屬性就是他們能夠用lambdas來例項化。這裡有一些lambdas的例子:

  左邊是指定型別的逗號分割的輸入列表,右邊是帶有return的程式碼塊:

(int x, int y) -> { return x + y; }

  左邊是推導型別的逗號分割的輸入列表,右邊是返回值:

(x, y) -> x + y

  左邊是推導型別的單一引數,右邊是一個返回值:

x -> x * x

  左邊沒有輸入 (官方名稱: “burger arrow”),在右邊返回一個值:

() -> x

  左邊是推導型別的單一引數,右邊是沒返回值的程式碼塊(返回void):

x -> { System.out.println(x); }

  靜態方法引用:

String::valueOf

  非靜態方法引用:

Object::toString

  繼承的函式引用:

x::toString

  建構函式引用:

ArrayList::new

  你可以想出一些函式引用格式作為其他lambda格式的簡寫。

方法引用  等價的lambda表示式  
String::valueOf   x -> String.valueOf(x)
Object::toString   x -> x.toString()
x::toString   () -> x.toString()
ArrayList::new   () -> new ArrayList<>()

  當然,在Java裡方法能被過載。類可以有多個同名但不同引數的方法。這同樣對構造方法有效。ArrayList::new能夠指向它的3個構造方法中任何一個。決定使用哪個方法是根據在使用的函式式介面。

  一個lambda和給定的函式式介面在“外型”匹配的時候相容。通過“外型”,我指向輸入、輸出的型別和宣告檢查異常。

  給出兩個具體有效的例子:

Comparator<String> c = (a, b) -> Integer.compare(a.length(),
                                                 b.length());

  一個Comparator<String>的compare方法需要輸入兩個闡述,然後返回一個int。這和lambda右側的一致,因此這個任務是有效的。

Runnable r = () -> { System.out.println("Running!"); }

  一個Runnable的run方法不需要引數也不會返回值。這和lambda右側一致,所以任務有效。

  在抽象方法的簽名裡的受檢查異常(如果存在)也很重要。如果函式式介面在它的簽名裡宣告瞭異常,lambda只能丟擲受檢查異常。

  捕獲和非捕獲的Lambda表示式

  當Lambda表示式訪問一個定義在Lambda表示式體外的非靜態變數或者物件時,這個Lambda表示式稱為“捕獲的”。比如,下面這個lambda表示式捕捉了變數x:

int x = 5; return y -> x + y;

  為了保證這個lambda表示式宣告是正確的,被它捕獲的變數必須是“有效final”的。所以要麼它們需要用final修飾符號標記,要麼保證它們在賦值後不能被改變。

  Lambda表示式是否是捕獲的和效能悄然相關。一個非不捕獲的lambda通常比捕獲的更高效,雖然這一點沒有書面的規範說明(據我所知),而且也不能為了程式的正確性指望它做什麼,非捕獲的lambda只需要計算一次. 然後每次使用到它都會返回一個唯一的例項。而捕獲的lambda表示式每次使用時都需要重新計算一次,而且從目前實現來看,它很像例項化一個匿名內部類的例項。

  lambdas不做的事

  你應該記住,有一些lambdas不提供的特性。為了Java 8它們被考慮到了,但是沒有被包括進去,由於簡化以及時間限制的原因。

  Non-final* 變數捕獲 - 如果一個變數被賦予新的數值,它將不能被用於lambda之中。”final”關鍵字不是必需的,但變數必須是“有效final”的(前面討論過)。這個程式碼不會被編譯:

int count = 0;
List<String> strings = Arrays.asList("a", "b", "c");
strings.forEach(s -> {
    count++; // error: can't modify the value of count });

  例外的透明度 - 如果一個已檢測的例外可能從lambda內部丟擲,功能性的介面也必須宣告已檢測例外可以被丟擲。這種例外不會散佈到其包含的方法。這個程式碼不會被編譯:

void appendAll(Iterable<String> values, Appendable out) throws IOException { // doesn't help with the error values.forEach(s -> {
        out.append(s); // error: can't throw IOException here // Consumer.accept(T) doesn't allow it });
}

  有繞過這個的辦法,你能定義自己的功能性介面,擴充套件Consumer的同時通過像RuntimeException之類丟擲 IOException。我試圖用程式碼寫出來,但發現它令人困惑是否值得。

  控制流程 (break, early return) -在上面的 forEach例子中,傳統的繼續方式有可能通過在lambda之內放置 ”return;”來實現。但是,沒有辦法中斷迴圈或者從lambda中通過包含方法的結果返回一個數值。例如:

final String secret = "foo"; boolean containsSecret(Iterable<String> values) {
    values.forEach(s -> { if (secret.equals(s)) {
            ??? // want to end the loop and return true, but can't }
    });
}

  進一步閱讀關於這些問題的資料,看看這篇Brian Goetz寫的說明:在 Block<T>中響應“已驗證例外”

  為什麼抽象類不能通過利用lambda例項化

  抽象類,哪怕只宣告瞭一個抽象方法,也不能使用lambda來例項化。

  下面有兩個類 Ordering 和 CacheLoader的例子,都帶有一個抽象方法,摘自於Guava 庫。那豈不是很高興能夠宣告它們的例項,像這樣使用lambda表示式?

Ordering<String> order = (a, b) -> ...;

CacheLoader<String, String> loader = (key) -> ...;

  這樣做引發的最常見的爭論就是會增加閱讀lambda的難度。以這種方式例項化一段抽象類將導致隱藏程式碼的執行:抽象類的構造方法。

  另一個原因是,它丟擲了lambda表示式可能的優化。在未來,它可能是這種情況,lambda表示式都不會計算到物件例項。放任使用者用lambda來宣告抽象類將妨礙像這樣的優化。

  此外,有一個簡單地解決方法。事實上,上述兩個摘自Guava 庫的例項類已經證明了這種方法。增加工廠方法將lambda轉換成例項。

Ordering<String> order = Ordering.from((a, b) -> ...);
CacheLoader<String, String> loader = CacheLoader.from((key) -> ...);

  要深入閱讀,請參看由 Brian Goetz所做的說明: response to “Allow lambdas to implement abstract classes”

  java.util.function

  包概要:java.util.function

  作為Comparator 和Runnable早期的證明,在JDK中已經定義的介面恰巧作為函式介面而與lambdas表示式相容。同樣方式可以在你自己的程式碼中定義任何函式介面或第三方庫。

  但有特定形式的函式介面,且廣泛的,通用的,在之前的JD卡中並不存在。大量的介面被新增到新的java.util.function 包中。下面是其中的一些:

  • Function<T, R> -T作為輸入,返回的R作為輸出
  • Predicate<T> -T作為輸入,返回的boolean值作為輸出
  • Consumer<T> - T作為輸入,執行某種動作但沒有返回值
  • Supplier<T> - 沒有任何輸入,返回T
  • BinaryOperator<T> -兩個T作為輸入,返回一個T作為輸出,對於“reduce”操作很有用

  這些最原始的特徵同樣存在。他們以int,long和double的方式提供。例如:

  • IntConsumer -以int作為輸入,執行某種動作,沒有返回值

  這裡存在效能上的一些原因,主要釋在輸入或輸出的時候避免裝箱和拆箱操作。

  java.util.stream

  包彙總: java.util.stream

  新的java.util.stream包提供了“支援在流上的函式式風格的值操作”(引用javadoc)的工具。可能活動一個流的最常見方法是從一個collection獲取:

Stream<T> stream = collection.stream();

  一個流就像一個地帶器。這些值“流過”(模擬水流)然後他們離開。一個流可以只被遍歷一次,然後被丟棄。流也可以無限使用。

  流能夠是 序列的 或者 並行的。 它們可以使用其中一種方式開始,然後切換到另外的一種方式,使用stream.sequential()或stream.parallel()來達到這種切換。序列流在一個執行緒上連續操作。而並行流就可能一次出現在多個執行緒上。

  所以,你想用一個流來幹什麼?這裡是在javadoc包裡給出的例子:

int sumOfWeights = blocks.stream().filter(b -> b.getColor() == RED)
                                  .mapToInt(b -> b.getWeight())
                                  .sum();

  注意:上面的程式碼使用了一個原始的流,以及一個只能用在原始流上的sum()方法。下面馬上就會有更多關於原始流的細節。

  流提供了流暢的API,可以進行資料轉換和對結果執行某些操作。流操作既可以是“中間的”也可以是“末端的”。

  •  -中間的操作保持流開啟狀態,並允許後續的操作。上面例子中的filter和map方法就是中間的操作。這些操作的返回資料型別是流;它們返回當前的流以便串聯更多的操作。
  • 末端的 - 末端的操作必須是對流的最終操作。當一個末端操作被呼叫,流被“消耗”並且不再可用。上面例子中的sum方法就是一個末端的操作。

  通常,處理一個流涉及了這些步驟:

  1. 從某個源頭獲得一個流。
  2. 執行一個或更多的中間的操作。
  3. 執行一個末端的操作。

  可能你想在一個方法中執行所有那些步驟。那樣的話,你就要知道源頭和流的屬性,而且要可以保證它被正確的使用。你可能不想接受任意的Stream<T>例項作為你的方法的輸入,因為它們可能具有你難以處理的特性,比如並行的或無限的。

  有幾個更普通的關於流操作的特性需要考慮:

  • 有狀態的 - 有狀態的操作給流增加了一些新的屬性,比如元素的唯一性,或者元素的最大數量,或者保證元素以排序的方式被處理。這些典型的要比無狀態的中間操作代價大。
  • 短路 - 短路操作潛在的允許對流的操作儘早停止,而不去檢查所有的元素。這是對無限流的一個特殊設計的屬性;如果對流的操作沒有短路,那麼程式碼可能永遠也不會終止。

  對每個Sttream方法這裡有一些簡短的,一般的描述。查閱javadoc獲取更詳盡的解釋。下面給出了每個操作的過載形式的連結。

  中間的操作:

  • filter 1 - 排除所有與斷言不匹配的元素。
  • map 1 2 3 4 - 通過Function對元素執行一對一的轉換。
  • flatMap 1 2 3 4 5 - 通過FlatMapper將每個元素轉變為無或更多的元素。
  • peek 1 - 對每個遇到的元素執行一些操作。主要對除錯很有用。
  • distinct 1 - 根據.equals行為排除所有重複的元素。這是一個有狀態的操作。
  • sorted 1 2 - 確保流中的元素在後續的操作中,按照比較器(Comparator)決定的順序訪問。這是一個有狀態的操作。
  • limit 1 - 保證後續的操作所能看到的最大數量的元素。這是一個有狀態的短路的操作。
  • substream 1 2 - 確保後續的操作只能看到一個範圍的(根據index)元素。像不能用於流的String.substring一樣。也有兩種形式,一種有一個開始索引,一種有一個結束索引。二者都是有狀態的操作,有一個結束索引的形式也是一個短路的操作。

  末端的操作:

  • forEach 1 - 對流中的每個元素執行一些操作。
  • toArray 1 2 - 將流中的元素傾倒入一個陣列。
  • reduce 1 2 3 - 通過一個二進位制操作將流中的元素合併到一起。
  • collect 1 2 - 將流中的元素傾倒入某些容器,例如一個Collection或Map.
  • min 1 - 根據一個比較器找到流中元素的最小值。
  • max 1 -根據一個比較器找到流中元素的最大值。
  • count 1 - 計算流中元素的數量。
  • anyMatch 1 - 判斷流中是否至少有一個元素匹配斷言。這是一個短路的操作。
  • allMatch 1 - 判斷流中是否每一個元素都匹配斷言。這是一個短路的操作。
  • noneMatch 1 - 判斷流中是否沒有一個元素匹配斷言。這是一個短路的操作。
  • findFirst 1 - 查詢流中的第一個元素。這是一個短路的操作。
  • findAny 1 - 查詢流中的任意元素,可能對某些流要比findFirst代價低。這是一個短路的操作。

  如 javadocs中提到的 , 中間的操作是延遲的(lazy)。只有末端的操作會立即開始流中元素的處理。在那個時刻,不管包含了多少中間的操作,元素會在一個傳遞中處理(通常,但並不總是)。(有狀態的操作如sorted() 和distinct()可能需要對元素的二次傳送。)

  流試圖儘可能做很少的工作。有一些細微優化,如當可以判定元素已經有序的時候,省略一個sorted()操作。在包含limit(x) 或 substream(x,y)的操作中,有些時候對一些不會決定結果的元素,流可以避免執行中間的map操作。在這裡我不準備實現公平判斷;它通過許多細微的但卻很重要的方法表現得很聰明,而且它仍在進步。

  回到並行流的概念,重要的是要意識到並行不是毫無代價的。從效能的立場它不是無代價的,你不能簡單的將順序流替換為並行流,且不做進一步思考就期望得到相同的結果。在你能(或者應該)並行化一個流以前,需要考慮很多特性,關於流、它的操作以及資料的目標方面。例如:訪問順序確實對我有影響嗎?我的函式是無狀態的嗎?我的流有足夠大,並且我的操作有足夠複雜,這些能使得並行化是值得的嗎?

  有針對int,long和double的專業原始的Stream版本:

  可以在眾多函式中,通過專業原始的map和flatMap函式,在一個stream物件與一個原始stream物件之間來回轉換。給幾個虛設例子:

List<String> strings = Arrays.asList("a", "b", "c");
strings.stream() // 
Stream<String> .mapToInt(String::length) // IntStream .longs() // 
LongStream .mapToDouble(x -> x / 10.0) // DoubleStream .boxed() // 
Stream<Double> .mapToLong(x -> 1L) // LongStream .mapToObj(x -> "") // 
Stream<String> ...

  原始的stream也為獲得關於stream的基礎資料統計提供方法,那些stream是指作為資料結構的。你可以發現count, sum, min, max, 以及元素平均值全部是來自於一個終端的操作。

  原始型別的剩餘部分沒有原始版本,因為這需要一個不可接受的JDK數量的膨脹。IntStream, LongStream, 和 DoubleStream被認為非常有用應當被包含進去,其他的數字型原始stream可以由這三個通過擴充套件的原始轉換來表示。

  在flatMap操作中使用的 FlatMapper 介面是具有一個抽象方法的功能性介面:

void flattenInto(T element, Consumer<U> sink);

  在一個flatMap操作的上下文中,stream為你提供element和 sink,然後你定義該用element 和 sink做什麼。element是指在stream中的當前元素,而sink代表當flatMap操作結束之後在stream中應該顯示些什麼。例如:

Set<Color> colors = ...;
List<Person> people = ...;
Stream<Color> stream = people.stream().flatMap(
    (Person person, Consumer<Color> sink) -> { // Map each person to the colors they like. for (Color color : colors) { if (person.likesColor(color)) {
                sink.accept(color);
            }
        }
    });

  注意上面lambda中的引數型別是指定的。在大多數其它上下文中,你可以不需要指定型別,但這裡由於FlatMapper的自然特性,編譯器需要你幫助判定型別。如果你在使用flatMap又迷惑於它為什麼不編譯,可能是因為你沒有指定型別。

  最令人感到困惑,複雜而且有用的終端stream操作之一是collect。它引入了一個稱為Collector的新的非功能性介面。這個介面有些難理解,但幸運的是有一個Collectors工具類可用來產生所有型別的有用的Collectors。例如:

List<String> strings = values.stream()
                             .filter(...)
                             .map(...)
                             .collect(Collectors.toList());

  如果你想將你的stream元素放進一個Collection,Map或String,那麼Collectors可能具有你需要的。在javadoc中瀏覽那個類絕對是值得的。

  泛型介面改進

  建議摘要:JEP 101: 通用化目標-Type 介面

  這是一個以前不能做到的,對編譯器判定泛型能力的努力改進。在以前版本的Java中有許多情形編譯器不能給某個方法計算出泛型,當方法處於巢狀的或串聯方法呼叫這樣的上下文的時候,即使有時候對程式設計師來說它看起來“很明顯”。那些情況需要程式設計師明確的指定一個“型別見證”(type witness)。它是一種通用的特性,但吃驚的是很少有Java程式設計師知道(我這麼說是基於私下的交流並且閱讀了一些StackOverflow的問題)。它看起來像這樣:

// In Java 7: foo(Utility.<Type>bar());
Utility.<Type>foo().bar();

  如果沒有型別見證,編譯器可能會將<Object>替代為泛型,而且如果需要的是一個更具體的型別,程式碼將編譯失敗。

  Java 8 極大的改進了這個狀況。在更多的案例中,它可以基於上下文計算出更多的特定的泛型型別。

// In Java 8: foo(Utility.bar());
Utility.foo().bar();

  這項工作仍在發展中,所以我不確定建議中列出的例子有多少能真正包含進Java 8。希望是它們全部。

  java.time

  包概要: java.time

  在Java8中新的 date/timeAPI存在於 java.time包中。如果你熟悉Joda Time,它將很容易掌握。事實上,我認為如此好的設計,以至於從未聽說過 Joda Time的人也能很容易的掌握。

  幾乎在API中的任何東西都是永恆的,包括值型別和格式化 。對於Date域或者處理或處理本地執行緒日期格式化不必太過擔心。

  與傳統的date/timeAPI的交叉是最小的。有一個清晰的分段:

  新API對於像月份和每週的天數,喜歡列舉型別更勝過整形常量。

  那麼,那是什麼呢?包級別的javadocs 對額外型別的做出了非常好的闡述。我將對一些值得注意的部分做一些簡短的綱要。

  非常有用的值型別:

  其他有用的型別:

  • DateTimeFormatter - 將日期型別轉換成字串型別
  • ChronoUnit - 計算出兩點之間的時間量,例如ChronoUnit.DAYS.between(t1, t2)
  • TemporalAdjuster - 例如date.with(TemporalAdjuster.firstDayOfMonth())

  大多數情況下,新的值型別由JDBC提供支援。有一小部分異常,如ZonedDateTime在SQL中沒有對應的(型別)。

  集合API附件

  實際上介面能夠定義預設方法允許了JDK作者加入大量的附件到集合API介面中。預設實現在核心介面裡提供,而其他更有效或更好的過載實現被加入到可適用的具體類中。

  這裡是新方法的列表:

  同樣, Iterator.remove() 現在有一個預設的, 會丟擲異常的實現,使得它稍微容易地去定義不可修改的迭代器。

  Collection.stream()和Collection.parallelStream()是流API的主要門戶。有其他方法去生成流,但這些在以後會更為長用。

  List.sort(Comparator)的附件有點怪異。以前排序一個ArrayList的方法是:

Collections.sort(list, comparator);

  這程式碼是你在Java7裡唯一可選的,非常低效。它會複製list到一個陣列裡,排序這個陣列,然後使用ListIterator來把陣列插入到新list的新位置上。

  List.sort(比較器)的預設實現仍然會做這個,但是具體的實現類可以自由的優化。例如,ArrayList.sort在ArrayList內部陣列上呼叫了Arrays.sort。CopyOnWriteArrayList做了同樣的事情。

  從這些新方法中獲得的不僅僅是效能。它們也具有更多的令人滿意的語義。例如, 對Collections.synchronizedList()排序是一個使用了list.sort的原子操作。你可以使用list.forEach對它的所有元素進行迭代,使之成為原子操作。

  Map.computeIfAbsent使得操作類似多重對映的結構變得容易了:

// Index strings by length: 
Map<Integer, List<String>> map = new HashMap<>(); for (String s : strings) {
    map.computeIfAbsent(s.length(),
                        key -> new ArrayList<String>())
       .add(s);
} // Although in this case the stream API may be a better choice:
 Map<Integer, List<String>> map = strings.stream()
    .collect(Collectors.groupingBy(String::length));

  增加併發API

  有太多的連結可以點選,因此參看ConcurrentHashMap javadocs文件以獲得更多資訊。

  • ConcurrentHashMap.reduce…
  • ConcurrentHashMap.search…
  • ConcurrentHashMap.forEach…

  ForkJoinPool.commonPool()是處理所有並行流操作的結構。當你 需要的時候,它是一個好而簡單的方式去獲得一個ForkJoinPool/ExecutorService/Executor物件。ConcurrentHashMap<K, V>完全重寫。內部看起來它一點不像是Java7版本。從外部來看幾乎相同,除了它有大量批量操作方法:多種形式的減少搜尋和forEach。
ConcurrentHashMap.newKeySet()提供了一個併發的java.util.Set實現。它基本上是Collections.newSetFromMap(new ConcurrentHashMap<T, Boolean>())的另一種方式的重寫。

  StampedLock是一種新型鎖的實現,很可能在大多數場景都可以替代ReentrantReadWriteLock。當作為一個簡單的讀寫鎖的時候,它比RRWL的效能要好。它也為“讀優化”提供了API,通過它你獲得了一個功能有點弱,但代價很小的讀操作鎖的版本,執行讀操作,然後檢查鎖是否被一個寫操作設定為無效。在Heinz Kabutz彙總的一系列幻燈片中,有更多關於這個類及其效能的細節(在這個系列幻燈片大約一半的地方開始的):“移相器和StampedLock演示

  CompletableFuture<T>是Future介面的一個非常棒的實現,它提供了無數執行(和串接)非同步任務的方法。它特別依賴功能性的介面;lambdas是值得增加這個類的一個重要原因。如果你正在使用Guava的 Future工具,例如FuturesListenableFuture, 和 SettableFuture,那麼你可能會希望校驗CompletableFuture能否作為潛在的替代選擇。

  IO/NIO API的新增內容

  簡單的說,這些API用於從檔案和InputStreams獲取java.util.stream.Stream物件。不過它們與直接從常規的collection得到的流有些不同,它們引入了兩個概念:

  • UncheckedIOException - 當有IO錯誤時丟擲這個異常,不過由於Iterator/Stream的簽名中不允許有IOException,所以它只能藉助於unchecked異常。
  • CloseableStream - 可以(並且應該)定義在 try-with-resources 語句裡面的流。

  反射和annotation的改動

  Annotation允許在更多的地方被使用,例如List<@Nullable String>。受此影響最大的可能是那些靜態分析工具,如Sonar和FindBugs。

  JSR 308的網站解釋了增加這些改動的動機,介紹的不錯: “型別Annotation (JSR 308) 和 Checker框架”

  Nashorn JavaScript 引擎

  提案的摘要: JEP 174: Nashorn JavaScript 引擎

  我對Nashorn沒什麼經驗,因而我對上面提案所描述的所知甚少。簡單的說,它是 Rhino 的接替者。Rhino 顯得有些老了,並且有點慢,開發者決定最好還是從頭做一個。

  其他新增,涉及java.lang,java.util,和java.sql

  這裡可以介紹的太多了,只能挑一些最需要注意的專案。

  ThreadLocal.withInitial(Supplier<T>) 可以在定義thread-local變數時更好的進行初始化。之前你初始化變數時是這樣的:

ThreadLocal<List<String>> strings = new ThreadLocal<List<String>>() { @Override protected List<String> initialValue() { return new ArrayList<>();
        }
    };

  現在則是這樣:

ThreadLocal<List<String>> strings =
    ThreadLocal.withInital(ArrayList::new);

  stream的API的返回值有一個可選的<T>,就像min/max, findFirst/Any, 以及reduce的某些形式。這樣做是因為stream中可能沒有任何元素,但是它要提供一個一致的API,既可以處理“一些結果”,也可以處理“沒有結果”。你可以提供預設值,拋異常,或者只在有結果的時候執行一些動作。

  它與Guava’s Optional類非常非常的相似。它一點都不像是在Scala裡的操作,也不會試圖成為之一,有相似的地方純屬巧合。

  旁白:Java 8′s Optional和Guava’s Optional最終如此的相似是很有趣的事,儘管荒謬的辯論發生在這兩個庫。

“FYI…. Optional was the cause of possibly the single greatest conflagration on the internal Java libraries discussion lists ever.”

  Kevin Bourrillion在 response to “Some new Guava classes targeted for release 10″如實寫到:

“On a purely practical note, the discussions surrounding Optional have exceeded its design budget by several orders of magnitude.”

  Brian Goetz 在  response to “Optional require(s) NonNull”寫到。

  StringJoinerandString.join(…)來得太晚了。他們來得如此之晚以至於大多數Java開發者已經為字串聯合編寫或發現了有用的工具,但這對JDK本身來說很每秒,因為最終自己實現這一點。每個人都會遇到要求字串連線的情形,我們現在能夠通過每個Java開發者(事實上的)即將知道的標準的API來闡明,這也算是一件好事。
ComparatorsandComparator.thenComparing(…)提供了非常優秀的工具,基於鏈的比較和基於域的比較。像這樣:

people.sort(Comparators.comparing(Person::getLastName)
                       .thenComparing(Person::getFirstName));

  這些新增功能提供了良好的,複雜的各種可讀的簡寫。大多數用例由JDK裡增加的 ComparisonChain和Ordering工具類來提供服務。對於什麼是有價值的,我認為JDK版本比在Guava-ese裡功能等效的版本的可讀性好了很多。

  更多?

  其實,還有很多的小問題的修正和一些效能的改進在這篇文章中沒有提及,但是那些也是非常的重要的。

  寫這篇文章主要是想全面的介紹到Java 8的每個語言層面和API層面的改進,如果有遺漏,那就是個錯誤。如果你發現了這樣的錯誤請告訴我,你可以通過e-mail聯絡我或者是在黑客諮詢上釋出評論。

  英文原文:Everything about Java 8

相關文章