JDK1.8的新特性


JAVA8新特性
接口改善

現在接口里已經完全可以定義靜態方法了. 舉一個比較普遍的例子就是在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的非默認實現。因此,一個在接口上這些的默認版本都是沒用的,它也不會被編譯。

 

函數式接口

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) -> ...);

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作為輸入,執行某種動作,沒有返回值

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM