java介面與lambda表示式
java介面與lambda表示式
介面
藉口不是類,而是對類的需求(功能)的描述,凡是實現介面的類,都需要實現介面中定義的需求(方法)。例如Comparable介面,描述的功能就是比較,一個類是否可以比較大小就看它是否實現了Comparable介面。
介面中宣告方法時,預設為public,因此可以不用加public關鍵字;但是實現的時候必須要加關鍵字,否則會預設protected,接著編譯器會發出警告。
介面中只能描述功能(方法),不能描述概念(屬性),因此介面中只有一系列public方法,沒有屬性,但是可以定義常量(在介面中定義的域均預設為final static、必須在宣告時賦值)。
Java SE8之前不能在介面中實現方法,但是Java SE8及其之後可以在介面中提供方法的預設實現。
介面特性
-
介面可以用來定義指標,但是不能用來例項化(new)。
-
檢測一個物件是否實現了某個介面可用instanceOf。
-
介面可以被擴充套件(繼承)。
-
介面只有public方法和public static final域。
-
介面沒有例項域。
-
Java SE8之前,介面沒有靜態方法;Java SE8及其之後,介面可以提供靜態方法。詳細描述在後面。
-
一個類只能繼承一個類,然而可以實現多個介面。例如一個類可以同時實現Comparable介面、Cloneable介面。
-
Java SE8之前,介面不能實現方法;Java SE8及其之後,介面可以提供方法的預設實現,需要用default
介面與抽象類
介面與抽象類最大的區別是介面可以實現多個,而類只能繼承一個,這樣非常不靈活(但是C++中由於支援多繼承,所以C++沒有介面,而是採用抽象類的方式)。
介面靜態方法
Java SE8之前,介面沒有靜態方法;Java SE8及其之後,介面可以提供靜態方法。
在標準庫中常常簡單這樣的例子Collection/Collections、Path/Paths、陣列/Arrays等這樣的介面/伴隨類的搭配,後者僅僅是提供一些操作前者的靜態方法。
在介面支援靜態方法之後,就可以將後者的靜態方法統一搬到前者介面中去,儘管這不太符合將介面作為抽象功能規範的初衷。
Java標準庫中的介面並沒有採用這種特性,這是因為重構整個Java庫的代價太大了。但是使用者卻可以這麼做。
預設方法
Java SE8之前,介面不能實現方法;Java SE8及其之後,介面可以提供方法的預設實現,需要用default修飾。
在某些情況下,使用者只需要介面中定義的部分功能(方法),但是將介面拆分開又顯得過於繁瑣;比如滑鼠監聽器包含了左鍵、右鍵、雙擊等回撥,然而我們很可能只需要左鍵單擊這一個功能,按照Java SE8之前的做法,我們需要實現所有的方法(哪怕是個空的什麼也不做的實現)。
有了預設方法之後,我們可以在介面中將這幾個回撥新增預設方法體(什麼也不做),使用者實現介面時就可以有選擇地選擇功能。
另外有些方法實現很簡單,但是不可或缺,這樣的方法就可以使用預設實現,而不需要讓使用者每次都重新實現。比如:
這樣實現了Collection的使用者就不需要關心isEmpty,只需要關心size方法就行了。
超類和介面預設方法的衝突
按照Java SE8之前的做法,並不會出現這種衝突,因為介面並沒有提供方法體。
Java SE8及其之後,如果超類和子類實現的介面有同名方法,或者實現的多個介面中有同名(包括方法名和引數)方法,則會發生衝突,Java中對衝突的處理如下:
-
超類和藉口衝突:超類優先。如果子類重寫了該方法,自然沒有爭議,如果沒有重寫,那麼超類優先。
由於這條規則的存在,我們不應該在介面中用預設方法重新定義Object的方法,因為就算你定義了,由於超類優先,在使用的時候仍然用的是Object提供的。
-
介面之間衝突:若多個介面描述了相同的方法,並且有介面(哪怕只有一個)提供了預設實現,實現類都必須自己實現該方法,否則編譯器會報錯。
Comparable介面
Comparable介面是一個常用的介面,他描述的功能是“比較”,實現它的類可以進行比較,進而可以進行基於比較的排序等操作。
Comparable
Comparable返回int值,在Comparable內部,當兩個數進行比較的時候,儘量使用Integer.compare()、Double.compare()等方法,而不是x-y這樣的方式,除非你很明確x-y這樣的形式不會造成溢位。
Comparable介面同equals方法一樣,在繼承時可能會有兩種情況:
若比較概念由子類定義(子類繼承改變了超類的語義),則子類定義comparableTo方法,並且在比較時新增Class物件比較的步驟(不同類丟擲異常)。
若比較概念由超類提供(子類繼承只改變了實現方法、沒有改變語義),則超類提供compareTo方法並新增final關鍵字,不允許子類改變比較的語義。
假如不按照上面的方式,可能出現這種情況:A繼承於B,然後A.compareTo(B)返回一個 int值,因為B引用可以轉換為A,但是B.compareTo(A)可能會丟擲異常,因為A引用不一定是B(可能是A的其他子類)。這不符合比較的反對稱原則。
Comparator介面
對於Comparable來說,當你實現了Comparable也就意味著你的comparableTo方法已經寫死了,排序時只能按照這一種規則。
對於有不同排序需求的物件來說,Comparator是一種解決方法。Comparator是一個介面,描述了一個比較器,可以為同一個物件定義多個比較器,排序時使用對應的比較器即可,Arrays.sort方法支援傳入比較器。
Cloneable介面
Cloneable介面描述了克隆功能clone方法,返回一個副本。
clone方法是Object類中的protected方法,因此一般來說,不能通過物件指標呼叫,但是可以在子類中用super指標訪問。
Object類中的clone方法預設將物件的所有域拷貝一份,是淺拷貝。因為不是所有的物件都可以克隆,所以Object預設的clone方法不會進行深度拷貝,這也是為什麼Object類的clone方法設為protected的原因(如果設為public,就意味著所有的物件都可以通過物件指標呼叫clone方法,這是不符合某些物件的語義的)。
Cloneable介面和Object類中都有clone方法,但是Cloneable中沒有預設實現,所以預設採用Object類的實現。
當想要為一個類向外提供public clone方法時,需要重寫clone方法,改為public方法,並且實現Cloneable介面,如果不實現Cloneable介面的話,Object.clone方法會丟擲異常(儘管類確實實現了clone方法)。
即使預設的clone方法(按域拷貝)可以滿足使用需求,但仍需要重寫clone為public方法,並且呼叫super.clone()。
lambda表示式
lambda表示式的意思是“帶引數的表示式(程式碼塊)”,本質上是一個匿名函式(方法),函式不正是帶引數的表示式?
在Java中lambda表示式表達的實體是函式式介面。詳細描述見後面。
很多時候我們創造一個物件,其實只是想用他的某一個方法,並非是整個物件,例如Comparator,當我們例項化一個實現了Comparator介面的物件並傳入Arrays.sort中,Arrays.soft只是簡單地通過comparator.compare()來呼叫compare方法;由於Java是面向物件的,所以必須構造一個類進行包裝,略顯複雜。
lambda表示式是一個可選的解決方案。
lambda語法
lambda表示式的語法:
一般例子:(String first, String second)->first.length()-second.length();
-
上述是一個一般的lambda表示式。
-
如果->後的表示式太長,無法用一條語句完成,可以像方法一樣,用{}框住,並顯示包含return語句。
-
當引數為空,仍然需要一個空括號,不能不寫:()->...。
-
如果lambda表示式的引數型別可以根據上下文推匯出來,那麼引數型別可以省略。比如:
Comparator
comp=(first,second)->first.length()-second.length(); -
如果引數只有一個,並且型別可以被推匯出來,那麼括號和引數型別可以同時省略。
-
lambda表示式的返回型別不需要指定,會根據上下文進行推導。、
-
如果lambda表示式內部分支語句可能造成返回值型別不同,將無法進行編譯(編譯報錯)。
函式式介面
對於只有一個抽象方法(不需要abstract關鍵字,只要不提供預設實現即可)的介面,當需要這種介面的物件的時候,可以通過lambda表示式生成,這樣的介面叫做函式式介面。Comparator介面就是一個函式式介面。
- 函式式介面有且僅有一個抽象方法(非dufault)。
- 函式式介面可以有多個default方法。
- 函式式介面常在宣告時加上註解@functional interface,但不是必須的。
lambda表示式可以即可理解為函式式介面的實現的簡略版本,lambda表示式可以根據賦值的函式式介面型別自動推導生成相應的物件。
java.util.function
在java.util.function包中,定義了很多非常通用的函式式介面。
例如BiFunction<U,T,R>介面,描述了一個引數為U、T,返回值為R的函式。例如:
BiFunction<String, String, Integer> comp
= (first, second) -> frst.length()-second.length();
方法引用,雙冒號語法
如果lambda表示式的程式碼塊已經存在於某個方法中,那麼可以通過方法引用進行引用,進而推匯出lambda表示式。
一般有以下幾種引用:
-
類名::靜態方法
當通過函式式介面呼叫方法時,實際上是ClassName.staticMethod()這樣呼叫的。
-
類名::例項方法
對於這種情況,比較特殊,返回來的方法引用引數會增加一個(第一個)。增加的引數是this指標,需要認為指定相應的this(呼叫者)。
-
物件::例項方法
實際呼叫是obj.method()。
-
類名::new
類似於類名::靜態。引數為構造器對應的引數(會自動尋找合適的構造器)。
-
型別[]::new
同上。不過引數為int。
使用如下:
package com.ame;
import java.util.Arrays;
interface InterfaceA {
void fnuc();
}
interface InterfaceB {
void func(ClassA classA);
}
interface InterfaceC {
ClassA func();
}
interface InterfaceD {
int[] func(int t);
}
class ClassA {
private int i = 0;
public static void g() {
System.out.println("g");
}
public void f() {
System.out.println("f:" + i++);
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
int i = 1;
System.out.println("test:" + i++);
test1();
System.out.println("test:" + i++);
test2();
System.out.println("test:" + i++);
test3();
System.out.println("test:" + i++);
test4();
System.out.println("test:" + i++);
test5();
}
//類名::靜態方法
public static void test1() {
ClassA classA = new ClassA();
InterfaceA interfaceA = null;
interfaceA = ClassA::g;
interfaceA.fnuc();
}
//類名::例項方法
public static void test2() {
ClassA classA = new ClassA();
InterfaceB interfaceB = null;
interfaceB = ClassA::f;
interfaceB.func(classA);
interfaceB.func(classA);
interfaceB.func(classA);
}
//物件::例項方法
public static void test3() {
ClassA classA = new ClassA();
InterfaceA interfaceA = null;
interfaceA = classA::f;
interfaceA.fnuc();
interfaceA.fnuc();
interfaceA.fnuc();
}
//類名::new
public static void test4() {
ClassA classA = new ClassA();
InterfaceC interfaceC = null;
interfaceC = ClassA::new;
classA = interfaceC.func();
classA.f();
classA.f();
classA.f();
}
//型別[]:new
public static void test5() {
InterfaceD interfaceD = null;
interfaceD = int[]::new;
int[] arr = interfaceD.func(3);
System.out.println(Arrays.toString(arr));
}
}
執行結果:
test:1
g
test:2
f:0
f:1
f:2
test:3
f:0
f:1
f:2
test:4
f:0
f:1
f:2
test:5
[0, 0, 0]
變數作用域
有時候希望lambda表示式訪問,自身表示式以外的變數。
我們知道lambda表示式最後會被封裝成一個物件(實現了對應的函式式介面),如果引用了自由變數(非lambda表示式自身定義),那麼封裝lambda表示式的時候會把這個變數也封裝(複製)過去。
在lambda表示式中,只允許引用值不變的自由變數,這裡的值不變有兩重含義,一是被定義為final,二是沒有被定義為final,但是從定義到銷燬,引用值沒有發生過改變(即effective final)。假如引用了值可能會發生改變的變數,當併發執行程式的時候,語義不明確。
- 可以引用final自由變數。
- 可以引用effective final自由變數。
注意1:被引用的變數在外部不能發生改變,在內部也不能。
注意2:由於this指標的值在一個方法體內是不會變的,因此lambda可以引用this指標。
例如:
package com.ame;
interface InterfaceE {
void func();
}
public class Test {
public static void main(String[] args) {
final int y = 1;
int x = 0;
InterfaceE interfaceE = null;
interfaceE = () -> {
System.out.println("hello world:" + x + "." + y);
// x++;不能在內部改變自由變數值
};
//x++;不能在外部改變自由變數值
interfaceE.func();
}
}
執行結果:
hello world:0.1
閉包
將在返回函式(即lambda表示式)中引用自由變數(外部定義)的程式結構稱之為閉包。
lambda和閉包是兩個東西。
在java中,lambda就是閉包。
閉包的作用是在捕獲自由變數,在這裡lambda表示式可以捕獲外部的final(或effective final)變數,因此是閉包。
執行lambda表示式
lambda表示式最大的特點就是延遲執行,在執行了生成lambda的程式碼之後,並不能確定lambda的內容何時執行。
常用的函式式介面
要想接受一個lambda表示式,需要一個函式式介面,有時甚至需要提供,下面列出了Java中提供的最重要的函式式介面:
Runnable:代表僅執行。無引數、無返回。run
Supplier:代表提供。無引數、有返回。get
Consumer:代表處理。有引數、無返回。accept
Function:代表普通函式。有引數、有返回。apply
UnaryOperator:一元操作。有引數、有返回。apply
BinaryOperator:二元操作。有引數、有返回。apply
Predicate:二值函式(布林)。有引數、有返回。
字首Bi代表Binary。
常用基本型別的函式式介面
另外,在利用上述函式式介面進行處理lambda表示式時,由於泛型機制,只能處理物件,而不能處理基本型別。儘管可以通過Integer、Boolean等物件使用,但是 由於頻繁裝箱拆箱,會帶來額外的開銷。
為了解決上述問題,Java提供了常用的操作基本資料型別的函式式介面:
當需要定義lambda表示式時,最好使用上述介面。
函式式介面中的default方法
為了便於使用者實現函式式介面中的抽象方法,很多函式式介面的開發者都提供了一系列default方法,輔助使用者實現抽象方法;或者是靜態方法,供使用者引用。