函數編程很是關鍵的幾個特性以下:java
函數編程支持函數做爲第一類對象,有時稱爲 閉包或者 仿函數(functor)對象。實質上,閉包是起函數的做用並能夠像對象同樣操做的對象。
與此相似,FP 語言支持 高階函數。高階函數能夠用另外一個函數(間接地,用一個表達式) 做爲其輸入參數,在某些狀況下,它甚至返回一個函數做爲其輸出參數。這兩種結構結合在一塊兒使得能夠用優雅的方式進行模塊化編程,這是使用 FP 的最大好處。程序員
在惰性計算中,表達式不是在綁定到變量時當即計算,而是在求值程序須要產生表達式的值時進行計算。延遲的計算使您能夠編寫可能潛在地生成無窮輸出的函數。由於不會計算多於程序的其他部分所須要的值,因此不須要擔憂由無窮計算所致使的 out-of-memory 錯誤。express
所謂"反作用"(side effect),指的是函數內部與外部互動(最典型的狀況,就是修改全局變量的值),產生運算之外的其餘結果。函數式編程強調沒有"反作用",意味着函數要保持獨立,全部功能就是返回一個新的值,沒有其餘行爲,尤爲是不得修改外部變量的值。
綜上所述,函數式編程能夠簡言之是: 使用不可變值和函數, 函數對一個值進行處理, 映射成另外一個值。這個值在面嚮對象語言中能夠理解爲對象,另外這個值還能夠做爲函數的輸入。編程
完整的Lambda表達式由三部分組成:參數列表、箭頭、聲明語句;數組
(Type1 param1, Type2 param2, ..., TypeN paramN) -> { statment1; statment2; //............. return statmentM;}
1. 絕大多數狀況,編譯器均可以從上下文環境中推斷出lambda表達式的參數類型,因此參數能夠省略:安全
(param1,param2, ..., paramN) -> { statment1; statment2; //............. return statmentM;}
二、 當lambda表達式的參數個數只有一個,能夠省略小括號:數據結構
param1 -> { statment1; statment2; //............. return statmentM;}
三、 當lambda表達式只包含一條語句時,能夠省略大括號、return和語句結尾的分號:閉包
param1 -> statment
Java 是一門面向對象編程語言。面向對象編程語言和函數式編程語言中的基本元素(Basic Values)均可以動態封裝程序行爲:面向對象編程語言使用帶有方法的對象封裝行爲,函數式編程語言使用函數封裝行爲。但這個相同點並不明顯,由於Java 對象每每比較「重量級」:實例化一個類型每每會涉及不一樣的類,並須要初始化類裏的字段和方法。oracle
不過有些 Java 對象只是對單個函數的封裝。例以下面這個典型用例:Java API 中定義了一個接口(通常被稱爲回調接口),用戶經過提供這個接口的實例來傳入指定行爲,例如:app
1
2
3
|
public interface ActionListener {
void actionPerformed(ActionEvent e);
}
|
這裏並不須要專門定義一個類來實現 ActionListener
,由於它只會在調用處被使用一次。用戶通常會使用匿名類型把行爲內聯(inline):
1
2
3
4
5
|
button.addActionListener(
new ActionListener() {
public void actionPerformed(ActionEvent e) {
ui.dazzle(e.getModifiers());
}
});
|
不少庫都依賴於上面的模式。對於並行 API 更是如此,由於咱們須要把待執行的代碼提供給並行 API,並行編程是一個很是值得研究的領域,由於在這裏摩爾定律獲得了重生:儘管咱們沒有更快的 CPU 核心(core),可是咱們有更多的 CPU 核心。而串行 API 就只能使用有限的計算能力。
隨着回調模式和函數式編程風格的日益流行,咱們須要在Java中提供一種儘量輕量級的將代碼封裝爲數據(Model code as data)的方法。匿名內部類並非一個好的 選擇,由於:
this
和變量名容易令人產生誤解final
的局部變量上面的多數問題均在Java SE 8中得以解決:
不過,Java SE 8 的目標並不是解決全部上述問題。所以捕獲可變變量(問題 4)和非局部控制流(問題 5)並不在 Java SE 8的範疇以內。(儘管咱們可能會在將來提供對這些特性的支持)
儘管匿名內部類有着種種限制和問題,可是它有一個良好的特性,它和Java類型系統結合的十分緊密:每個函數對象都對應一個接口類型。之因此說這個特性是良好的,是由於:
上面提到的 ActionListener
接口只有一個方法,大多數回調接口都擁有這個特徵:好比 Runnable
接口和 Comparator
接口。咱們把這些只擁有一個方法的接口稱爲 函數式接口。(以前它們被稱爲 SAM類型,即 單抽象方法類型(Single Abstract Method))
咱們並不須要額外的工做來聲明一個接口是函數式接口:編譯器會根據接口的結構自行判斷(判斷過程並不是簡單的對接口方法計數:一個接口可能冗餘的定義了一個 Object
已經提供的方法,好比 toString()
,或者定義了靜態方法或默認方法,這些都不屬於函數式接口方法的範疇)。不過API做者們能夠經過 @FunctionalInterface
註解來顯式指定一個接口是函數式接口(以免無心聲明瞭一個符合函數式標準的接口),加上這個註解以後,編譯器就會驗證該接口是否知足函數式接口的要求。
實現函數式類型的另外一種方式是引入一個全新的 結構化 函數類型,咱們也稱其爲「箭頭」類型。例如,一個接收 String
和 Object
並返回 int
的函數類型能夠被表示爲 (String, Object) -> int
。咱們仔細考慮了這個方式,但出於下面的緣由,最終將其否認:
m(T->U)
和 m(X->Y)
進行重載(Overload)因此咱們選擇了「使用已知類型」這條路——由於現有的類庫大量使用了函數式接口,經過沿用這種模式,咱們使得現有類庫可以直接使用 lambda 表達式。例以下面是 Java SE 7 中已經存在的函數式接口:
除此以外,Java SE 8中增長了一個新的包:java.util.function
,它裏面包含了經常使用的函數式接口,例如:
Predicate<T>
——接收 T
並返回 boolean
Consumer<T>
——接收 T
,不返回值Function<T, R>
——接收 T
,返回 R
Supplier<T>
——提供 T
對象(例如工廠),不接收值UnaryOperator<T>
——接收 T
對象,返回 T
BinaryOperator<T>
——接收兩個 T
,返回 T
除了上面的這些基本的函數式接口,咱們還提供了一些針對原始類型(Primitive type)的特化(Specialization)函數式接口,例如 IntSupplier
和 LongBinaryOperator
。(咱們只爲 int
、long
和 double
提供了特化函數式接口,若是須要使用其它原始類型則須要進行類型轉換)一樣的咱們也提供了一些針對多個參數的函數式接口,例如 BiFunction<T, U, R>
,它接收 T
對象和 U
對象,返回 R
對象。
匿名類型最大的問題就在於其冗餘的語法。有人戲稱匿名類型致使了「高度問題」(height problem):好比前面 ActionListener
的例子裏的五行代碼中僅有一行在作實際工做。
lambda表達式是匿名方法,它提供了輕量級的語法,從而解決了匿名內部類帶來的「高度問題」。
下面是一些lambda表達式:
1
2
3
|
(
int x, int y) -> x + y
() ->
42
(String s) -> { System.out.println(s); }
|
第一個 lambda 表達式接收 x
和 y
這兩個整形參數並返回它們的和;第二個 lambda 表達式不接收參數,返回整數 ‘42’;第三個 lambda 表達式接收一個字符串並把它打印到控制檯,不返回值。
lambda 表達式的語法由參數列表、箭頭符號 ->
和函數體組成。函數體既能夠是一個表達式,也能夠是一個語句塊:
return
語句會把控制權交給匿名方法的調用者break
和 continue
只能在循環中使用表達式函數體適合小型 lambda 表達式,它消除了 return
關鍵字,使得語法更加簡潔。
lambda 表達式也會常常出如今嵌套環境中,好比說做爲方法的參數。爲了使 lambda 表達式在這些場景下儘量簡潔,咱們去除了沒必要要的分隔符。不過在某些狀況下咱們也能夠把它分爲多行,而後用括號包起來,就像其它普通表達式同樣。
下面是一些出如今語句中的 lambda 表達式:
1
2
3
4
5
6
7
8
|
FileFilter java = (File f) -> f.getName().endsWith(
"*.java");
String user = doPrivileged(() -> System.getProperty(
"user.name"));
new Thread(() -> {
connectToService();
sendNotification();
}).start();
|
須要注意的是,函數式接口的名稱並非 lambda 表達式的一部分。那麼問題來了,對於給定的 lambda 表達式,它的類型是什麼?答案是:它的類型是由其上下文推導而來。例如,下面代碼中的 lambda 表達式類型是 ActionListener
:
1
|
ActionListener l = (ActionEvent e) -> ui.dazzle(e.getModifiers());
|
這就意味着一樣的 lambda 表達式在不一樣上下文裏能夠擁有不一樣的類型:
1
2
3
|
Callable<String> c = () ->
"done";
PrivilegedAction<String> a = () ->
"done";
|
第一個 lambda 表達式 () -> "done"
是 Callable
的實例,而第二個 lambda 表達式則是 PrivilegedAction
的實例。
編譯器負責推導 lambda 表達式類型。它利用 lambda 表達式所在上下文 所期待的類型 進行推導,這個 被期待的類型 被稱爲 目標類型。lambda 表達式只能出如今目標類型爲函數式接口的上下文中。
固然,lambda 表達式對目標類型也是有要求的。編譯器會檢查 lambda 表達式的類型和目標類型的方法簽名(method signature)是否一致。當且僅當下面全部條件均知足時,lambda 表達式才能夠被賦給目標類型 T
:
T
是一個函數式接口T
的方法參數在數量和類型上一一對應T
的方法返回值相兼容(Compatible)T
的方法 throws
類型相兼容因爲目標類型(函數式接口)已經「知道」 lambda 表達式的形式參數(Formal parameter)類型,因此咱們沒有必要把已知類型再重複一遍。也就是說,lambda 表達式的參數類型能夠從目標類型中得出:
1
|
Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);
|
在上面的例子裏,編譯器能夠推導出 s1
和 s2
的類型是 String
。此外,當 lambda 的參數只有一個並且它的類型能夠被推導得知時,該參數列表外面的括號能夠被省略:
1
2
3
|
FileFilter java = f -> f.getName().endsWith(
".java");
button.addActionListener(e -> ui.dazzle(e.getModifiers()));
|
這些改進進一步展現了咱們的設計目標:「不要把高度問題轉化成寬度問題。」咱們但願語法元素可以儘量的少,以便代碼的讀者可以直達 lambda 表達式的核心部分。
lambda 表達式並非第一個擁有上下文相關類型的 Java 表達式:泛型方法調用和「菱形」構造器調用也經過目標類型來進行類型推導:
1
2
3
4
5
|
List<String> ls = Collections.emptyList();
List<Integer> li = Collections.emptyList();
Map<String, Integer> m1 =
new HashMap<>();
Map<Integer, String> m2 =
new HashMap<>();
|
以前咱們提到 lambda 表達式智能出如今擁有目標類型的上下文中。下面給出了這些帶有目標類型的上下文:
? :
)在前三個上下文(變量聲明、賦值和返回語句)裏,目標類型便是被賦值或被返回的類型:
1
2
3
4
5
6
7
8
|
Comparator<String> c;
c = (String s1, String s2) -> s1.compareToIgnoreCase(s2);
public Runnable toDoLater() {
return () -> {
System.out.println(
"later");
}
}
|
數組初始化器和賦值相似,只是這裏的「變量」變成了數組元素,而類型是從數組類型中推導得知:
1
2
3
4
|
filterFiles(
new FileFilter[] {
f -> f.exists(), f -> f.canRead(), f -> f.getName().startsWith(
"q")
});
|
方法參數的類型推導要相對複雜些:目標類型的確認會涉及到其它兩個語言特性:重載解析(Overload resolution)和參數類型推導(Type argument inference)。
重載解析會爲一個給定的方法調用(method invocation)尋找最合適的方法聲明(method declaration)。因爲不一樣的聲明具備不一樣的簽名,當 lambda 表達式做爲方法參數時,重載解析就會影響到 lambda 表達式的目標類型。編譯器會經過它所得之的信息來作出決定。若是 lambda 表達式具備 顯式類型(參數類型被顯式指定),編譯器就能夠直接 使用lambda 表達式的返回類型;若是lambda表達式具備 隱式類型(參數類型被推導而知),重載解析則會忽略 lambda 表達式函數體而只依賴 lambda 表達式參數的數量。
若是在解析方法聲明時存在二義性(ambiguous),咱們就須要利用轉型(cast)或顯式 lambda 表達式來提供更多的類型信息。若是 lambda 表達式的返回類型依賴於其參數的類型,那麼 lambda 表達式函數體有可能能夠給編譯器提供額外的信息,以便其推導參數類型。
1
2
|
List<Person> ps = ...
Stream<String> names = ps.stream().map(p -> p.getName());
|
在上面的代碼中,ps
的類型是 List<Person>
,因此 ps.stream()
的返回類型是 Stream<Person>
。map()
方法接收一個類型爲 Function<T, R>
的函數式接口,這裏 T
的類型便是 Stream
元素的類型,也就是 Person
,而 R
的類型未知。因爲在重載解析以後 lambda 表達式的目標類型仍然未知,咱們就須要推導 R
的類型:經過對 lambda 表達式函數體進行類型檢查,咱們發現函數體返回 String
,所以 R
的類型是 String
,於是 map()
返回 Stream<String>
。絕大多數狀況下編譯器都能解析出正確的類型,但若是碰到沒法解析的狀況,咱們則須要:
p
提供顯式類型)以提供額外的類型信息Function<Person, String>
R
提供一個實際類型。(.<String>map(p -> p.getName())
)lambda 表達式自己也能夠爲它本身的函數體提供目標類型,也就是說 lambda 表達式能夠經過外部目標類型推導出其內部的返回類型,這意味着咱們能夠方便的編寫一個返回函數的函數:
1
|
Supplier<Runnable> c = () -> () -> { System.out.println(
"hi"); };
|
相似的,條件表達式能夠把目標類型「分發」給其子表達式:
1
|
Callable<Integer> c = flag ? (() ->
23) : (() -> 42);
|
最後,轉型表達式(Cast expression)能夠顯式提供 lambda 表達式的類型,這個特性在沒法確認目標類型時很是有用:
1
2
|
// Object o = () -> { System.out.println("hi"); }; 這段代碼是非法的
Object o = (Runnable) () -> { System.out.println(
"hi"); };
|
除此以外,當重載的方法都擁有函數式接口時,轉型能夠幫助解決重載解析時出現的二義性。
目標類型這個概念不只僅適用於 lambda 表達式,泛型方法調用和「菱形」構造方法調用也能夠從目標類型中受益,下面的代碼在 Java SE 7 是非法的,但在 Java SE 8 中是合法的:
1
2
3
|
List<String> ls = Collections.checkedList(
new ArrayList<>(), String.class);
Set<Integer> si = flag ? Collections.singleton(
23) : Collections.emptySet();
|
在內部類中使用變量名(以及 this
)很是容易出錯。內部類中經過繼承獲得的成員(包括來自 Object
的方法)可能會把外部類的成員掩蓋(shadow),此外未限定(unqualified)的 this
引用會指向內部類本身而非外部類。
相對於內部類,lambda 表達式的語義就十分簡單:它不會從超類(supertype)中繼承任何變量名,也不會引入一個新的做用域。lambda 表達式基於詞法做用域,也就是說 lambda 表達式函數體裏面的變量和它外部環境的變量具備相同的語義(也包括 lambda 表達式的形式參數)。此外,’this’ 關鍵字及其引用在 lambda 表達式內部和外部也擁有相同的語義。
爲了進一步說明詞法做用域的優勢,請參考下面的代碼,它會把 "Hello, world!"
打印兩遍:
1
2
3
4
5
6
7
8
9
10
11
|
public class Hello {
Runnable r1 = () -> { System.out.println(
this); }
Runnable r2 = () -> { System.out.println(toString()); }
public String toString() { return "Hello, world"; }
public static void main(String... args) {
new Hello().r1.run();
new Hello().r2.run();
}
}
|
與之相相似的內部類實現則會打印出相似 Hello$1@5b89a773
和 Hello$2@537a7706
之類的字符串,這每每會使開發者大吃一驚。
基於詞法做用域的理念,lambda 表達式不能夠掩蓋任何其所在上下文中的局部變量,它的行爲和那些擁有參數的控制流結構(例如 for
循環和 catch
從句)一致。
我的補充:這個說法很拗口,因此我在這裏加一個例子以演示詞法做用域:
1
2
3
4
5
|
int i = 0;
int sum = 0;
for (int i = 1; i < 10; i += 1) { //這裏會出現編譯錯誤,由於i已經在for循環外部聲明過了
sum += i;
}
|
在 Java SE 7 中,編譯器對內部類中引用的外部變量(即捕獲的變量)要求很是嚴格:若是捕獲的變量沒有被聲明爲 final
就會產生一個編譯錯誤。咱們如今放寬了這個限制——對於 lambda 表達式和內部類,咱們容許在其中捕獲那些符合 有效只讀(Effectively final)的局部變量。
簡單的說,若是一個局部變量在初始化後從未被修改過,那麼它就符合有效只讀的要求,換句話說,加上 final
後也不會致使編譯錯誤的局部變量就是有效只讀變量。
1
2
3
4
|
Callable<String> helloCallable(String name) {
String hello =
"Hello";
return () -> (hello + ", " + name);
}
|
對 this
的引用,以及經過 this
對未限定字段的引用和未限定方法的調用在本質上都屬於使用 final
局部變量。包含此類引用的 lambda 表達式至關於捕獲了 this
實例。在其它狀況下,lambda 對象不會保留任何對 this
的引用。
這個特性對內存管理是一件好事:內部類實例會一直保留一個對其外部類實例的強引用,而那些沒有捕獲外部類成員的 lambda 表達式則不會保留對外部類實例的引用。要知道內部類的這個特性每每會形成內存泄露。
儘管咱們放寬了對捕獲變量的語法限制,但試圖修改捕獲變量的行爲仍然會被禁止,好比下面這個例子就是非法的:
1
2
|
int sum = 0;
list.forEach(e -> { sum += e.size(); });
|
爲何要禁止這種行爲呢?由於這樣的 lambda 表達式很容易引發 race condition。除非咱們可以強制(最好是在編譯時)這樣的函數不能離開其當前線程,但若是這麼作了可能會致使更多的問題。簡而言之,lambda 表達式對 值 封閉,對 變量 開放。
我的補充:lambda 表達式對 值 封閉,對 變量 開放的原文是:lambda expressions close over values, not variables,我在這裏增長一個例子以說明這個特性:
1
2
3
4
5
|
int sum = 0;
list.forEach(e -> { sum += e.size(); });
// Illegal, close over values
List<Integer> aList =
new List<>();
list.forEach(e -> { aList.add(e); });
// Legal, open over variables
|
lambda 表達式不支持修改捕獲變量的另外一個緣由是咱們可使用更好的方式來實現一樣的效果:使用規約(reduction)。java.util.stream
包提供了各類通用的和專用的規約操做(例如 sum
、min
和 max
),就上面的例子而言,咱們可使用規約操做(在串行和並行下都是安全的)來代替 forEach
:
1
2
3
4
|
int sum =
list.stream()
.mapToInt(e -> e.size())
.sum();
|
sum()
等價於下面的規約操做:
1
2
3
4
|
int sum =
list.stream()
.mapToInt(e -> e.size())
.reduce(
0 , (x, y) -> x + y);
|
規約須要一個初始值(以防輸入爲空)和一個操做符(在這裏是加號),而後用下面的表達式計算結果:
1
|
0 + list[0] + list[1] + list[2] + ...
|
規約也能夠完成其它操做,好比求最小值、最大值和乘積等等。若是操做符具備可結合性(associative),那麼規約操做就能夠容易的被並行化。因此,與其支持一個本質上是並行並且容易致使 race condition 的操做,咱們選擇在庫中提供一個更加並行友好且不容易出錯的方式來進行累積(accumulation)。
lambda 表達式容許咱們定義一個匿名方法,並容許咱們以函數式接口的方式使用它。咱們也但願可以在 已有的 方法上實現一樣的特性。
方法引用和 lambda 表達式擁有相同的特性(例如,它們都須要一個目標類型,並須要被轉化爲函數式接口的實例),不過咱們並不須要爲方法引用提供方法體,咱們能夠直接經過方法名稱引用已有方法。
如下面的代碼爲例,假設咱們要按照 name
或 age
爲 Person
數組進行排序:
1
2
3
4
5
6
7
8
9
10
11
12
|
class Person {
private final String name;
private final int age;
public int getAge() { return age; }
public String getName() {return name; }
...
}
Person[] people = ...
Comparator<Person> byName = Comparator.comparing(p -> p.getName());
Arrays.sort(people, byName);
|
在這裏咱們能夠用方法引用代替lambda表達式:
1
|
Comparator<Person> byName = Comparator.comparing(Person::getName);
|
這裏的 Person::getName
能夠被看做爲 lambda 表達式的簡寫形式。儘管方法引用不必定(好比在這個例子裏)會把語法變的更緊湊,但它擁有更明確的語義——若是咱們想要調用的方法擁有一個名字,咱們就能夠經過它的名字直接調用它。
由於函數式接口的方法參數對應於隱式方法調用時的參數,因此被引用方法簽名能夠經過放寬類型,裝箱以及組織到參數數組中的方式對其參數進行操做,就像在調用實際方法同樣:
1
2
3
4
|
Consumer<Integer> b1 = System::exit;
// void exit(int status)
Consumer<String[]> b2 = Arrays:sort;
// void sort(Object[] a)
Consumer<String> b3 = MyProgram::main;
// void main(String... args)
Runnable r = Myprogram::mapToInt
// void main(String... args)
|
方法引用有不少種,它們的語法以下:
ClassName::methodName
instanceReference::methodName
super::methodName
ClassName::methodName
Class::new
TypeName[]::new
對於靜態方法引用,咱們須要在類名和方法名之間加入 ::
分隔符,例如 Integer::sum
對於具體對象上的實例方法引用,咱們則須要在對象名和方法名之間加入分隔符:
1
2
|
Set<String> knownNames = ...
Predicate<String> isKnown = knownNames::contains;
|
這裏的隱式 lambda 表達式(也就是實例方法引用)會從 knownNames
中捕獲 String
對象,而它的方法體則會經過Set.contains
使用該 String
對象。
有了實例方法引用,在不一樣函數式接口之間進行類型轉換就變的很方便:
1
2
|
Callable<Path> c = ...
Privileged<Path> a = c::call;
|
引用任意對象的實例方法則須要在實例方法名稱和其所屬類型名稱間加上分隔符:
1
|
Function<String, String> upperfier = String::toUpperCase;
|
這裏的隱式 lambda 表達式(即 String::toUpperCase
實例方法引用)有一個 String
參數,這個參數會被 toUpperCase
方法使用。
若是類型的實例方法是泛型的,那麼咱們就須要在 ::
分隔符前提供類型參數,或者(多數狀況下)利用目標類型推導出其類型。
須要注意的是,靜態方法引用和類型上的實例方法引用擁有同樣的語法。編譯器會根據實際狀況作出決定。
通常咱們不須要指定方法引用中的參數類型,由於編譯器每每能夠推導出結果,但若是須要咱們也能夠顯式在 ::
分隔符以前提供參數類型信息。
和靜態方法引用相似,構造方法也能夠經過 new
關鍵字被直接引用:
1
|
SocketImplFactory factory = MySocketImpl::
new;
|
若是類型擁有多個構造方法,那麼咱們就會經過目標類型的方法參數來選擇最佳匹配,這裏的選擇過程和調用構造方法時的選擇過程是同樣的。
若是待實例化的類型是泛型的,那麼咱們能夠在類型名稱以後提供類型參數,不然編譯器則會依照」菱形」構造方法調用時的方式進行推導。
數組的構造方法引用的語法則比較特殊,爲了便於理解,你能夠假想存在一個接收 int
參數的數組構造方法。參考下面的代碼:
1
2
|
IntFunction<
int[]> arrayMaker = int[]::new;
int[] array = arrayMaker.apply(10) // 建立數組 int[10]
|
lambda 表達式和方法引用大大提高了 Java 的表達能力(expressiveness),不過爲了使把 代碼即數據 (code-as-data)變的更加容易,咱們須要把這些特性融入到已有的庫之中,以便開發者使用。
Java SE 7 時代爲一個已有的類庫增長功能是很是困難的。具體的說,接口在發佈以後就已經被定型,除非咱們可以一次性更新全部該接口的實現,不然向接口添加方法就會破壞現有的接口實現。默認方法(以前被稱爲 虛擬擴展方法 或 守護方法)的目標便是解決這個問題,使得接口在發佈以後仍能被逐步演化。
這裏給出一個例子,咱們須要在標準集合 API 中增長針對 lambda 的方法。例如 removeAll
方法應該被泛化爲接收一個函數式接口 Predicate
,但這個新的方法應該被放在哪裏呢?咱們沒法直接在 Collection
接口上新增方法——否則就會破壞現有的 Collection
實現。咱們卻是能夠在 Collections
工具類中增長對應的靜態方法,但這樣就會把這個方法置於「二等公民」的境地。
默認方法 利用面向對象的方式向接口增長新的行爲。它是一種新的方法:接口方法能夠是 抽象的 或是 默認的。默認方法擁有其默認實現,實現接口的類型經過繼承獲得該默認實現(若是類型沒有覆蓋該默認實現)。此外,默認方法不是抽象方法,因此咱們能夠放心的向函數式接口裏增長默認方法,而不用擔憂函數式接口的單抽象方法限制。
下面的例子展現瞭如何向 Iterator
接口增長默認方法 skip
:
1
2
3
4
5
6
7
8
9
|
interface Iterator<E> {
boolean hasNext();
E next();
void remove();
default void skip(int i) {
for ( ; i > 0 && hasNext(); i -= 1) next();
}
}
|
根據上面的 Iterator
定義,全部實現 Iterator
的類型都會自動繼承 skip
方法。在使用者的眼裏,skip
不過是接口新增的一個虛擬方法。在沒有覆蓋 skip
方法的 Iterator
子類實例上調用 skip
會執行 skip
的默認實現:調用 hasNext
和 next
若干次。子類能夠經過覆蓋 skip
來提供更好的實現——好比直接移動遊標(cursor),或是提供爲操做提供原子性(Atomicity)等。
當接口繼承其它接口時,咱們既能夠爲它所繼承而來的抽象方法提供一個默認實現,也能夠爲它繼承而來的默認方法提供一個新的實現,還能夠把它繼承而來的默認方法從新抽象化。
除了默認方法,Java SE 8 還在容許在接口中定義 靜態 方法。這使得咱們能夠從接口直接調用和它相關的輔助方法(Helper method),而不是從其它的類中調用(以前這樣的類每每以對應接口的複數命名,例如 Collections
)。好比,咱們通常須要使用靜態輔助方法生成實現 Comparator
的比較器,在Java SE 8中咱們能夠直接把該靜態方法定義在 Comparator
接口中:
1
2
3
4
|
public static <T, U extends Comparable<? super U>>
Comparator<T> comparing(Function<T, U> keyExtractor) {
return (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
|
和其它方法同樣,默認方法也能夠被繼承,大多數狀況下這種繼承行爲和咱們所期待的一致。不過,當類型或者接口的超類擁有多個具備相同簽名的方法時,咱們就須要一套規則來解決這個衝突:
爲了演示第二條規則,咱們假設 Collection
和 List
接口均提供了 removeAll
的默認實現,而後 Queue
繼承並覆蓋了 Collection
中的默認方法。在下面的 implement
從句中,List
中的方法聲明會優先於 Queue
中的方法聲明:
1
|
class LinkedList<E> implements List<E>, Queue<E> { ... }
|
當兩個獨立的默認方法相沖突或是默認方法和抽象方法相沖突時會產生編譯錯誤。這時程序員須要顯式覆蓋超類方法。通常來講咱們會定義一個默認方法,而後在其中顯式選擇超類方法:
1
2
3
|
interface Robot implements Artist, Gun {
default void draw() { Artist.super.draw(); }
}
|
super
前面的類型必須是有定義或繼承默認方法的類型。這種方法調用並不僅限於消除命名衝突——咱們也能夠在其它場景中使用它。
最後,接口在 inherits
和 extends
從句中的聲明順序和它們被實現的順序無關。
咱們在設計lambda時的一個重要目標就是新增的語言特性和庫特性可以無縫結合(designed to work together)。接下來,咱們經過一個實際例子(按照姓對名字列表進行排序)來演示這一點:
好比說下面的代碼:
1
2
3
4
5
6
|
List<Person> people = ...
Collections.sort(people,
new Comparator<Person>() {
public int compare(Person x, Person y) {
return x.getLastName().compareTo(y.getLastName());
}
})
|
冗餘代碼實在太多了!
有了lambda表達式,咱們能夠去掉冗餘的匿名類:
1
2
|
Collections.sort(
people, (Person x, Person y) -> x.getLastName().compareTo(y.getLastName()));
|
儘管代碼簡潔了不少,但它的抽象程度依然不好:開發者仍然須要進行實際的比較操做(並且若是比較的值是原始類型那麼狀況會更糟),因此咱們要藉助 Comparator
裏的 comparing
方法實現比較操做:
1
|
Collections.sort(people, Comparator.comparing((Person p) -> p.getLastName()));
|
在類型推導和靜態導入的幫助下,咱們能夠進一步簡化上面的代碼:
1
|
Collections.sort(people, comparing(p -> p.getLastName()));
|
咱們注意到這裏的 lambda 表達式其實是 getLastName
的代理(forwarder),因而咱們能夠用方法引用代替它:
1
|
Collections.sort(people, comparing(Person::getLastName));
|
最後,使用 Collections.sort
這樣的輔助方法並非一個好主意:它不但使代碼變的冗餘,也沒法爲實現 List
接口的數據結構提供特定(specialized)的高效實現,並且因爲 Collections.sort
方法不屬於 List
接口,用戶在閱讀 List
接口的文檔時不會察覺在另外的 Collections
類中還有一個針對 List
接口的排序(sort()
)方法。
默認方法能夠有效的解決這個問題,咱們爲 List
增長默認方法 sort()
,而後就能夠這樣調用:
1
|
people.sort(comparing(Person::getLastName));;
|
此外,若是咱們爲 Comparator
接口增長一個默認方法 reversed()
(產生一個逆序比較器),咱們就能夠很是容易的在前面代碼的基礎上實現降序排序。
1
|
people.sort(comparing(Person::getLastName).reversed());;
|
Java SE 8 提供的新語言特性並不算多——lambda 表達式,方法引用,默認方法和靜態接口方法,以及範圍更廣的類型推導。可是把它們結合在一塊兒以後,開發者能夠編寫出更加清晰簡潔的代碼,類庫編寫者能夠編寫更增強大易用的並行類庫。
1.8以前JDK自帶的日期處理類很是不方便,咱們處理的時候常常是使用的第三方工具包,好比commons-lang包等。不過1.8出現以後這個改觀了不少,好比日期時間的建立、比較、調整、格式化、時間間隔等。
這些類都在java.time包下。比原來實用了不少。
1 /* 2 JDK8新特性 3 */ 4 public class JDK8_features { 5 6 public List<Integer> list = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10); 7 8 /** 9 * 1.Lambda表達式 10 */ 11 @Test 12 public void testLambda(){ 13 list.forEach(System.out::println); 14 list.forEach(e -> System.out.println("方式二:"+e)); 15 } 16 17 /** 18 * 2.Stream函數式操做流元素集合 19 */ 20 @Test 21 public void testStream(){ 22 List<Integer> nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10); 23 System.out.println("求和:"+nums 24 .stream()//轉成Stream 25 .filter(team -> team!=null)//過濾 26 .distinct()//去重 27 .mapToInt(num->num*2)//map操做 28 .skip(2)//跳過前2個元素 29 .limit(4)//限制取前4個元素 30 .peek(System.out::println)//流式處理對象函數 31 .sum());// 32 } 33 34 /** 35 * 3.接口新增:默認方法與靜態方法 36 * default 接口默認實現方法是爲了讓集合類默認實現這些函數式處理,而不用修改現有代碼 37 * (List繼承於Iterable<T>,接口默認方法沒必要須實現default forEach方法) 38 */ 39 @Test 40 public void testDefaultFunctionInterface(){ 41 //能夠直接使用接口名.靜態方法來訪問接口中的靜態方法 42 JDK8Interface1.staticMethod(); 43 //接口中的默認方法必須經過它的實現類來調用 44 new JDK8InterfaceImpl1().defaultMethod(); 45 //多實現類,默認方法重名時必須複寫 46 new JDK8InterfaceImpl2().defaultMethod(); 47 } 48 49 public class JDK8InterfaceImpl1 implements JDK8Interface1 { 50 //實現接口後,由於默認方法不是抽象方法,重寫/不重寫都成! 51 // @Override 52 // public void defaultMethod(){ 53 // System.out.println("接口中的默認方法"); 54 // } 55 } 56 57 public class JDK8InterfaceImpl2 implements JDK8Interface1,JDK8Interface2 { 58 //實現接口後,默認方法名相同,必須複寫默認方法 59 @Override 60 public void defaultMethod() { 61 //接口的 62 JDK8Interface1.super.defaultMethod(); 63 System.out.println("實現類複寫重名默認方法!!!!"); 64 } 65 } 66 67 /** 68 * 4.方法引用,與Lambda表達式聯合使用 69 */ 70 @Test 71 public void testMethodReference(){ 72 //構造器引用。語法是Class::new,或者更通常的Class< T >::new,要求構造器方法是沒有參數; 73 final Car car = Car.create( Car::new ); 74 final List< Car > cars = Arrays.asList( car ); 75 //靜態方法引用。語法是Class::static_method,要求接受一個Class類型的參數; 76 cars.forEach( Car::collide ); 77 //任意對象的方法引用。它的語法是Class::method。無參,全部元素調用; 78 cars.forEach( Car::repair ); 79 //特定對象的方法引用,它的語法是instance::method。有參,在某個對象上調用方法,將列表元素做爲參數傳入; 80 final Car police = Car.create( Car::new ); 81 cars.forEach( police::follow ); 82 } 83 84 public static class Car { 85 public static Car create( final Supplier< Car > supplier ) { 86 return supplier.get(); 87 } 88 89 public static void collide( final Car car ) { 90 System.out.println( "靜態方法引用 " + car.toString() ); 91 } 92 93 public void repair() { 94 System.out.println( "任意對象的方法引用 " + this.toString() ); 95 } 96 97 public void follow( final Car car ) { 98 System.out.println( "特定對象的方法引用 " + car.toString() ); 99 } 100 } 101 102 /** 103 * 5.引入重複註解 104 * 1.@Repeatable 105 * 2.能夠不用之前的「註解容器」寫法,直接寫2次相同註解便可 106 * 107 * Java 8在編譯器層作了優化,相同註解會以集合的方式保存,所以底層的原理並無變化。 108 */ 109 @Test 110 public void RepeatingAnnotations(){ 111 RepeatingAnnotations.main(null); 112 } 113 114 /** 115 * 6.類型註解 116 * 新增類型註解:ElementType.TYPE_USE 和ElementType.TYPE_PARAMETER(在Target上) 117 * 118 */ 119 @Test 120 public void ElementType(){ 121 Annotations.main(null); 122 } 123 124 /** 125 * 7.最新的Date/Time API (JSR 310) 126 */ 127 @Test 128 public void DateTime(){ 129 //1.Clock 130 final Clock clock = Clock.systemUTC(); 131 System.out.println( clock.instant() ); 132 System.out.println( clock.millis() ); 133 134 //2. ISO-8601格式且無時區信息的日期部分 135 final LocalDate date = LocalDate.now(); 136 final LocalDate dateFromClock = LocalDate.now( clock ); 137 138 System.out.println( date ); 139 System.out.println( dateFromClock ); 140 141 // ISO-8601格式且無時區信息的時間部分 142 final LocalTime time = LocalTime.now(); 143 final LocalTime timeFromClock = LocalTime.now( clock ); 144 145 System.out.println( time ); 146 System.out.println( timeFromClock ); 147 148 // 3.ISO-8601格式無時區信息的日期與時間 149 final LocalDateTime datetime = LocalDateTime.now(); 150 final LocalDateTime datetimeFromClock = LocalDateTime.now( clock ); 151 152 System.out.println( datetime ); 153 System.out.println( datetimeFromClock ); 154 155 // 4.特定時區的日期/時間, 156 final ZonedDateTime zonedDatetime = ZonedDateTime.now(); 157 final ZonedDateTime zonedDatetimeFromClock = ZonedDateTime.now( clock ); 158 final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of( "America/Los_Angeles" ) ); 159 160 System.out.println( zonedDatetime ); 161 System.out.println( zonedDatetimeFromClock ); 162 System.out.println( zonedDatetimeFromZone ); 163 164 //5.在秒與納秒級別上的一段時間 165 final LocalDateTime from = LocalDateTime.of( 2018, Month.APRIL, 16, 0, 0, 0 ); 166 final LocalDateTime to = LocalDateTime.of( 2019, Month.APRIL, 16, 23, 59, 59 ); 167 168 final Duration duration = Duration.between( from, to ); 169 System.out.println( "Duration in days: " + duration.toDays() ); 170 System.out.println( "Duration in hours: " + duration.toHours() ); 171 } 172 173 /** 174 * 8.新增base64加解密API 175 */ 176 @Test 177 public void testBase64(){ 178 final String text = "就是要測試加解密!!abjdkhdkuasu!!@@@@"; 179 String encoded = Base64.getEncoder() 180 .encodeToString( text.getBytes( StandardCharsets.UTF_8 ) ); 181 System.out.println("加密後="+ encoded ); 182 183 final String decoded = new String( 184 Base64.getDecoder().decode( encoded ), 185 StandardCharsets.UTF_8 ); 186 System.out.println( "解密後="+decoded ); 187 } 188 189 /** 190 * 9.數組並行(parallel)操做 191 */ 192 @Test 193 public void testParallel(){ 194 long[] arrayOfLong = new long [ 20000 ]; 195 //1.給數組隨機賦值 196 Arrays.parallelSetAll( arrayOfLong, 197 index -> ThreadLocalRandom.current().nextInt( 1000000 ) ); 198 //2.打印出前10個元素 199 Arrays.stream( arrayOfLong ).limit( 10 ).forEach( 200 i -> System.out.print( i + " " ) ); 201 System.out.println(); 202 //3.數組排序 203 Arrays.parallelSort( arrayOfLong ); 204 //4.打印排序後的前10個元素 205 Arrays.stream( arrayOfLong ).limit( 10 ).forEach( 206 i -> System.out.print( i + " " ) ); 207 System.out.println(); 208 } 209 210 /** 211 * 10.JVM的PermGen空間被移除:取代它的是Metaspace(JEP 122)元空間 212 */ 213 @Test 214 public void testMetaspace(){ 215 //-XX:MetaspaceSize初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整 216 //-XX:MaxMetaspaceSize最大空間,默認是沒有限制 217 //-XX:MinMetaspaceFreeRatio在GC以後,最小的Metaspace剩餘空間容量的百分比,減小爲分配空間所致使的垃圾收集 218 //-XX:MaxMetaspaceFreeRatio在GC以後,最大的Metaspace剩餘空間容量的百分比,減小爲釋放空間所致使的垃圾收集 219 } 220 221 } 222 引用到的相關類: 223 public interface JDK8Interface1 { 224 225 //1.接口中能夠定義靜態方法了 226 public static void staticMethod(){ 227 System.out.println("接口中的靜態方法"); 228 } 229 230 //2.使用default以後就能夠定義普通方法的方法體了 231 public default void defaultMethod(){ 232 System.out.println("接口中的默認方法"); 233 } 234 } 235 236 public interface JDK8Interface2 { 237 238 //接口中能夠定義靜態方法了 239 public static void staticMethod(){ 240 System.out.println("接口中的靜態方法"); 241 } 242 //使用default以後就能夠定義普通方法的方法體了 243 public default void defaultMethod(){ 244 System.out.println("接口中的默認方法"); 245 } 246 } 247 248 /* 249 JDK8新特性 250 */ 251 public class RepeatingAnnotations { 252 @Target( ElementType.TYPE ) 253 @Retention( RetentionPolicy.RUNTIME ) 254 public @interface Filters { 255 Filter[] value(); 256 } 257 258 @Target( ElementType.TYPE ) 259 @Retention( RetentionPolicy.RUNTIME ) 260 @Repeatable( Filters.class ) 261 public @interface Filter { 262 String value(); 263 String value2(); 264 }; 265 266 @Filter( value="filter1",value2="111" ) 267 @Filter( value="filter2", value2="222") 268 //@Filters({@Filter( value="filter1",value2="111" ),@Filter( value="filter2", value2="222")}).注意:JDK8以前:1.沒有@Repeatable2.採用本行「註解容器」寫法 269 public interface Filterable { 270 } 271 272 public static void main(String[] args) { 273 //獲取註解後遍歷打印值 274 for( Filter filter: Filterable.class.getAnnotationsByType( Filter.class ) ) { 275 System.out.println( filter.value() +filter.value2()); 276 } 277 } 278 } 279 280 /* 281 @Description:新增類型註解:ElementType.TYPE_USE 和ElementType.TYPE_PARAMETER(在Target上) 282 */ 283 public class Annotations { 284 @Retention( RetentionPolicy.RUNTIME ) 285 @Target( { ElementType.TYPE_USE, ElementType.TYPE_PARAMETER } ) 286 public @interface NonEmpty { 287 } 288 289 public static class Holder< @NonEmpty T > extends @NonEmpty Object { 290 public void method() throws @NonEmpty Exception { 291 } 292 } 293 294 public static void main(String[] args) { 295 final Holder< String > holder = new @NonEmpty Holder< String >(); 296 @NonEmpty Collection< @NonEmpty String > strings = new ArrayList<>(); 297 } 298 }