1. 程式人生 > 實用技巧 >java介面與lambda表示式

java介面與lambda表示式

java介面與lambda表示式

介面

藉口不是類,而是對類的需求(功能)的描述,凡是實現介面的類,都需要實現介面中定義的需求(方法)。例如Comparable介面,描述的功能就是比較,一個類是否可以比較大小就看它是否實現了Comparable介面。

介面中宣告方法時,預設為public,因此可以不用加public關鍵字;但是實現的時候必須要加關鍵字,否則會預設protected,接著編譯器會發出警告。

介面中只能描述功能(方法),不能描述概念(屬性),因此介面中只有一系列public方法,沒有屬性,但是可以定義常量(在介面中定義的域均預設為final static、必須在宣告時賦值)。

Java SE8之前不能在介面中實現方法,但是Java SE8及其之後可以在介面中提供方法的預設實現。

介面特性

  1. 介面可以用來定義指標,但是不能用來例項化(new)。

  2. 檢測一個物件是否實現了某個介面可用instanceOf。

  3. 介面可以被擴充套件(繼承)。

  4. 介面只有public方法和public static final域。

  5. 介面沒有例項域。

  6. Java SE8之前,介面沒有靜態方法;Java SE8及其之後,介面可以提供靜態方法。詳細描述在後面。

  7. 一個類只能繼承一個類,然而可以實現多個介面。例如一個類可以同時實現Comparable介面、Cloneable介面。

  8. 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中對衝突的處理如下:

  1. 超類和藉口衝突:超類優先。如果子類重寫了該方法,自然沒有爭議,如果沒有重寫,那麼超類優先。

    由於這條規則的存在,我們不應該在介面中用預設方法重新定義Object的方法,因為就算你定義了,由於超類優先,在使用的時候仍然用的是Object提供的。

  2. 介面之間衝突:若多個介面描述了相同的方法,並且有介面(哪怕只有一個)提供了預設實現,實現類都必須自己實現該方法,否則編譯器會報錯。

Comparable介面

Comparable介面是一個常用的介面,他描述的功能是“比較”,實現它的類可以進行比較,進而可以進行基於比較的排序等操作。

Comparable介面在實現事可以是通過指定T來指定型別。

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

  1. 上述是一個一般的lambda表示式。

  2. 如果->後的表示式太長,無法用一條語句完成,可以像方法一樣,用{}框住,並顯示包含return語句

  3. 當引數為空,仍然需要一個空括號,不能不寫:()->...。

  4. 如果lambda表示式的引數型別可以根據上下文推匯出來,那麼引數型別可以省略。比如:

    Comparator comp=(first,second)->first.length()-second.length();

  5. 如果引數只有一個,並且型別可以被推匯出來,那麼括號和引數型別可以同時省略。

  6. lambda表示式的返回型別不需要指定,會根據上下文進行推導。、

  7. 如果lambda表示式內部分支語句可能造成返回值型別不同,將無法進行編譯(編譯報錯)。

函式式介面

對於只有一個抽象方法(不需要abstract關鍵字,只要不提供預設實現即可)的介面,當需要這種介面的物件的時候,可以通過lambda表示式生成,這樣的介面叫做函式式介面。Comparator介面就是一個函式式介面。

  1. 函式式介面有且僅有一個抽象方法(非dufault)。
  2. 函式式介面可以有多個default方法。
  3. 函式式介面常在宣告時加上註解@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表示式。

一般有以下幾種引用:

  1. 類名::靜態方法

    當通過函式式介面呼叫方法時,實際上是ClassName.staticMethod()這樣呼叫的。

  2. 類名::例項方法

    對於這種情況,比較特殊,返回來的方法引用引數會增加一個(第一個)。增加的引數是this指標,需要認為指定相應的this(呼叫者)。

  3. 物件::例項方法

    實際呼叫是obj.method()。

  4. 類名::new

    類似於類名::靜態。引數為構造器對應的引數(會自動尋找合適的構造器)。

  5. 型別[]::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)。假如引用了值可能會發生改變的變數,當併發執行程式的時候,語義不明確。

  1. 可以引用final自由變數。
  2. 可以引用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方法,輔助使用者實現抽象方法;或者是靜態方法,供使用者引用。