1. 程式人生 > >Java8 Lambda表示式與Stream API (一):Lambda表示式

Java8 Lambda表示式與Stream API (一):Lambda表示式

你要知道的Java8 匿名內部類、函式式介面、lambda表示式與Stream API都在這裡

本文主要講解Java8 Stream API,但是要講解這一部分需要匿名內部類、lambda表示式以及函式式介面的相關知識,本文將分為兩篇文章來講解上述內容,讀者可以按需查閱。

本文是該系列博文的第一篇Java 匿名內部類、lambda表示式與函式式介面,主要講解Java匿名內部類lambda表示式以及函式式介面,第二篇文章Java Stream API主要講解Java Stream API

匿名內部類

匿名內部類適用於那些只需要使用一次的類,比如設計模式下的命令模式,往往通過定義一系列介面進行呼叫,有時有的命令只會執行一次就不再執行,這個時候如果單獨定義一個類就顯得過於複雜並且編譯會生成這個類的.class

檔案,不利於管理,這種情況下使用匿名內部類就能夠很好的簡化程式設計並且不會編譯生成單獨的.class檔案。

接下來看一個例子:

interface Programmer
{
    void listLanguages();
    void introduceMyself();
}

class MyProgrammer implements Programmer
{
    public void listLanguages() {
        System.out.println("Objective-C Swift Python Go Java");
    }

    public
void introduceMyself() { System.out.println("My Name is Jiaming Chen"); } } public class HelloWorld { static void info(Programmer programmer) { programmer.listLanguages(); programmer.introduceMyself(); } public static void main(String[] args) { info(new
MyProgrammer()); } }

上面這個例子為了執行info函式定義了一個實現了Programmer介面的類MyProgrammer,如果它只執行一次這樣就顯得過於複雜,如果採用匿名內部類就會在很大程度上簡化程式設計,首先介紹一下匿名內部類的基礎語法:

new 需要實現的介面() | 父類構造器()
{
    //需要實現的方法或過載父類的方法
}

匿名內部類的語法很簡單,必須要實現一個介面或者繼承一個類,可以看到使用了new關鍵詞,因此在建立匿名內部類的同時會建立一個該類的例項,並且只能建立一個例項,建立完成後這個匿名內部類就不能再使用,因此,匿名內部類不能是抽象類,由於匿名內部類沒有類名所以也不能定義建構函式,但是可以在定義匿名內部類的時候呼叫父類的有參構造器也可以定義初始化塊用於初始化父類的成員變數。下面這個栗子是將上述程式碼修改為匿名內部類的實現方式:

class MyProgrammer implements Programmer
{
    public void listLanguages() {
        System.out.println("Objective-C Swift Python Go Java");
    }

    public void introduceMyself() {
        System.out.println("My Name is Jiaming Chen");
    }
}

public class HelloWorld
{   
    static void info(Programmer programmer)
    {
        programmer.listLanguages();
        programmer.introduceMyself();
    }

    public static void main(String[] args)
    {   
        info(new Programmer(){
            public void listLanguages() {
                System.out.println("Objective-C Swift Python Go Java");
            }

            public void introduceMyself() {
                System.out.println("My Name is Jiaming Chen");
            }
        });
    }
}

通過對比發現,使用匿名內部類比重新定義一個新類更加簡潔,在建立匿名內部類的時候可以呼叫父類的有參建構函式,栗子如下:

abstract class Programmer
{
    protected String name;

    public Programmer(String name)
    {
        this.name = name;
    }

    public abstract void listLanguages();
    public abstract void introduceMyself();
}

public class HelloWorld
{   
    static void info(Programmer programmer)
    {
        programmer.listLanguages();
        programmer.introduceMyself();
    }

    public static void main(String[] args)
    {   
        int age = 22;
        Programmer p = new Programmer("Jiaming Chen"){
            public void listLanguages()
            {
                System.out.println("Objective-C Swift Python Go Java");
            }
            public void introduceMyself()
            {
                System.out.println("My Name is " + this.name + " and I'm " + age + " years old.");
                //age = 2;
            }
        };
        info(p);
        //age = 23;
    }
}

上述栗子首先定義了一個抽象父類,並且該抽象父類只有一個建構函式,因此在建立匿名內部類的時候需要顯示呼叫該建構函式,這樣就可以在匿名內部類內部使用父類定義的成員變量了,匿名內部類也可以使用外部變數,在Java8中上述栗子中的age會自動宣告為final型別,這稱為effectively final,只要匿名內部類訪問了一個區域性變數,這個區域性變數無論是否被final修飾它都會自動被宣告為final型別,不允許任何地方進行修改,Java與其他語言相比在閉包內訪問外部變數的侷限更大,因為只能是final型別,比如OCblock內部也可以捕獲外部變數,swift也有一套閉包值捕獲機制,都可以對捕獲的值進行修改或許可權的設定,Java的侷限性太大。

lambda表示式

Java8引入了lambda表示式,在其他語言中,比如pythonswift都支援lambda表示式,這個特性用起來也非常方便和簡潔。接下來舉一個常見的對一個列表進行排序的例子:

class MyComparator implements Comparator<String>
{
    public int compare(String o1, String o2)
    {
        return o1.compareTo(o2);
    }
}

public class HelloWorld
{   
    public static void main(String[] args)
    {   
        ArrayList<String> list = new ArrayList<>();
        list.add("Objective-C");
        list.add("Swift");
        list.add("Python");
        list.add("Golang");
        list.add("Java");
        list.sort(new MyComparator());
        list.forEach(System.out::println);
        /*
        輸出:(下面的程式碼的輸出結果是一致的,不再贅述)
        Golang
        Java
        Objective-C
        Python
        Swift
        */
    }
}

在開發中經常會對一個集合型別進行排序操作,呼叫集合的sort方法時需要傳入一個實現了Comparator介面的引數,因此上述栗子就定義了一個類MyComparator並且這個類實現了Comparator介面,因此可以建立一個MyComparator的物件用於排序操作,不難發現這樣做非常複雜需要重新定義一個全新的類,經過前文的介紹這裡完全可以用匿名內部類來代替,關於最後一行程式碼list.forEach(System.out::println);在後文會介紹,這裡先賣個關子,它的作用就是遍歷整個集合並輸出。接下來看一下使用匿名內部類實現的方式:

public class HelloWorld
{   
    public static void main(String[] args)
    {   
        ArrayList<String> list = new ArrayList<>();
        list.add("Objective-C");
        list.add("Swift");
        list.add("Python");
        list.add("Golang");
        list.add("Java");
        list.sort(new Comparator<String>(){
            public int compare(String o1, String o2) {
                return o1.compareTo(o2);
            }           
        });
        list.forEach(System.out::println);
    }
}

結果同上,顯而易見,採用了匿名內部類更加的方便了,程式碼簡潔明瞭,那有沒有再簡介一點的辦法呢?答案當然是肯定的,那就是使用lambda表示式,栗子如下:

public class HelloWorld
{   
    public static void main(String[] args)
    {   
        ArrayList<String> list = new ArrayList<>();
        list.add("Objective-C");
        list.add("Swift");
        list.add("Python");
        list.add("Golang");
        list.add("Java");
        list.sort((String o1, String o2)->{
            return o1.compareTo(o2);
        });
        list.forEach(System.out::println);
    }
}

通過上述程式碼發現,sort函式傳入的引數是一個lambda表示式,整個程式碼非常類似前文中我們實現的compare函式,但是我們不需要再寫new Comparator<String()這樣繁瑣的程式碼,也不需要寫函式名,因此lambda表示式可以很好的替代匿名內部類,不難發現lambda表示式由三部分組成:

  • 首先是引數列表,這個引數列表與要實現的方法的引數列表一致,因為系統已經知道了你需要實現哪一個方法因此可以省略形參型別,當只有一個引數時也可以省略圓括號,但是當沒有形參時圓括號不可以省略
  • 接下來是一個->符號,該符號用於分隔形參列表與函式體,該符號不允許省略。
  • 最後就是程式碼體了,如果程式碼體只有一行程式碼就可以省略掉花括號,並且如果方法需要有返回值連return關鍵詞都可以省略,系統會自動將這一行程式碼的結果返回。

通過上面的講解,我們就可以寫一個更加簡潔lambda表示式了,栗子如下:

public class HelloWorld
{   
    public static void main(String[] args)
    {   
        ArrayList<String> list = new ArrayList<>();
        list.add("Objective-C");
        list.add("Swift");
        list.add("Python");
        list.add("Golang");
        list.add("Java");
        list.sort((s1, s2)->s1.compareTo(s2));
        list.forEach(System.out::println);
    }
}

上面的程式碼我們省略了形參的型別,由於只有一行我們同時省略了花括號和return語句,整個程式碼相比使用匿名內部類更加簡潔了。到這裡有同學可能會問了,lambda表示式是怎麼知道我們實現的是介面的哪一個方法?

lambda表示式的型別也被稱為目標型別 target type,該型別必須是函式式介面 Functional Interface函式式介面代表有且只有一個抽象方法,但是可以包含多個預設方法或類方法的介面,因此使用lambda表示式系統一定知道我們實現的介面的哪一個方法,因為實現的介面有且只有一個抽象方法供我們實現。

函式式介面可以使用註釋@FunctionalInterface來要求編譯器在編譯時進行檢查,是否只包含一個抽象方法。Java提供了大量的函式式介面這樣就能使用lambda表示式簡化程式設計。lambda表示式的目標型別必須是函式式介面lambda表示式也只能為函式式介面建立物件因為lambda表示式只能實現一個抽象方法。

前文介紹了在使用lambda表示式時,如果程式碼體只有一行程式碼可以省略花括號,如果有返回值也可以省略return關鍵詞,不僅如此,lambda表示式在只有一條程式碼時還可以引用其他方法或構造器並自動呼叫,可以省略引數傳遞,程式碼更加簡潔,引用方法的語法需要使用::符號。lambda表示式提供了四種引用方法和構造器的方式:

  • 引用物件的方法 類::例項方法
  • 引用類方法 類::類方法
  • 引用特定物件的方法 特定物件::例項方法
  • 引用類的構造器 類::new

舉個栗子:

public class HelloWorld
{   
    public static void main(String[] args)
    {   
        ArrayList<String> list = new ArrayList<>();
        list.add("Objective-C");
        list.add("Swift");
        list.add("Python");
        list.add("Golang");
        list.add("Java");
        //list.sort((s1, s2)->s1.compareTo(s2));
        list.sort(String::compareTo);
        list.forEach(System.out::println);
    }
}

對比上述兩行程式碼,第一個sort函式傳入了一個lambda表示式用於實現Comparator介面的compare函式,由於該實現只有一條程式碼,因此可以省略花括號以及return關鍵字。第二個sort方法則直接引用了物件的例項方法,語法規則為類::例項方法,系統會自動將函式式介面實現的方法的所有引數中的第一個引數作為呼叫者,接下來的引數依次傳入引用的方法中即自動進行s1.compareTo(s2)的方法呼叫,明顯第二個sort函式呼叫更加簡潔明瞭。

最後一行程式碼list.forEach(System.out::println);則引用了類方法,集合類的例項方法forEach接收一個Consumer介面物件,該介面是一個函式式介面,只有一個抽象方法void accept(T t);,因此可以使用lambda表示式進行呼叫,這裡引用System.out的類方法println,引用語法類::類方法,系統會自動將實現的函式式介面方法中的所有引數都傳入該類方法並進行自動呼叫。

再舉一個栗子:

@FunctionalInterface
interface Index
{
    int index(String subString);
}

@FunctionalInterface
interface Generator
{
    String generate();
}

public class HelloWorld
{   
    static int getIndex(Index t, String subString)
    {
        return t.index(subString);
    }

    static String generateString(Generator g)
    {
        return g.generate();
    }

    public static void main(String[] args)
    {   
        String str = "Hello World";
        System.out.println(getIndex(str::indexOf, "e"));
        System.out.println(generateString(String::new).length());
    }
}

這個栗子似乎沒有任何實際意義,就是為了演示引用特定物件的例項方法和引用類的構造器。介面IndexGenerator都是函式式介面,因此可以使用lambda表示式。對於getIndex方法需要傳入一個實現Index介面的物件和一個子串,在呼叫時首先定義了一個字串Hello World,然後引用了這個物件的例項方法indexOf,這個時候系統會自動將這個特定物件作為呼叫者然後將所有的引數因此傳入該實力方法。
引用構造器的方法也很簡單類::new,不再贅述。

總結

本文主要講解匿名內部類lambda表示式函式式介面以及lambda表示式方法引用構造器引用,通過幾個例子逐步深入,逐步簡化程式碼的編寫,可以發現Java8提供的lambda表示式是如此的強大。接下來的一篇文章會對Java8新增的Stream API進行講解,Stream的流式API支援並行,對傳統程式設計方式進行了改進,可以編寫出更簡潔明瞭的高效能程式碼。有興趣的讀者可以閱讀Java Stream API

備註

由於作者水平有限,難免出現紕漏,如有問題還請不吝賜教。