Java 8導入了一個新型態的語法──Lambda。然而這個Lambda語法並不是新的語法,在Script Languages和Functional Languages中都可以常常見到,為什麼Java 8要特地導入Lambda呢?難道先前的那些語法沒辦法完成什麼特別的功能嗎?這問題待文章之後解答,我們先來看看什麼是Lambda。



什麼是Lambda?

Lambda通用定義

在不同領域,對於Lambda的定義可能不太相同,但概念都是Lambda為一個函數,可以根據輸入的值,決定輸出的值。但Lambda與一般函數不同的是,Lambda並不需要替函數命名(如F(X) = X + 2G(X) = F(X) + 3中的FG便是函數的名稱),所以我們常常把Lambda形容為「匿名的」(Anonymous)。

匿名類別(Anonymous class)

在Java中,匿名類別是非常常見的物件實體化方式,相信學過Java的人都有用過類似以下的方式去實體化一個物件出來:

class ClassA {

}

public class MainClass {

    public static void main(String[] args) {
        ClassA a = new ClassA() { //實體化匿名類別
            public void function1(int x, int y) {
                System.out.println(x + y);
            }

            public int function2(int x) {
                return x + 1;
            }
        };
    }
}
以上是將匿名類別實體化出來的一段程式。為何說程式碼內的ClassA是匿名的呢?其實匿名類別指的不是ClassA(ClassA已有ClassA這個名稱,自然不會是一個匿名類別),而是在new的同時,被重新修改過的ClassA(在new statment之後加上大括號{},就判定為重新修改)。這樣說可能還是很模糊,如果對照以下非匿名類別的撰寫方式,就可以清楚匿名和非匿名的差異了。

class ClassA {

}

class ClassB extends ClassA { //在另外的地方宣告class,並且繼承或是實作ClassA

    public void function1(int x, int y) {

    }

    public int function2(int x) {
        return x + 1;
    }
}

public class MainClass {

    public static void main(String[] args) {
        ClassA a = new ClassB();
    }
}

將原先使用的匿名類別,另外宣告成一個ClassB出來,調用時直接new ClassB,此處的類別便不是匿名類別,而相比每次只能夠在同一個程式碼位置new出實體的匿名類別,ClassB則可以在任何Scope所及的地方new出實體。

我們都知道,Java會把每個類別在編譯時期,都創建出相對應的.class檔案。如果是一般的類別,則會直接以Class的名稱作為.class檔案的檔名。如果是內部(Inner)的非匿名類別,則會以$為分隔字元,表示類別在哪層類別之下,作為.class檔案的檔名。舉例,編譯出以下的程式,會得到A.classA$B.class兩個檔案。

class A {

    class B {

    }
}

但如果有內部的匿名類別,那.class會如何產生呢?無論匿名類別是從何種類別重新修改而來,Java都會按照匿名類別是在哪層底下,給予流水的數字編號。舉例,編譯出以下的程式,會得到C.classA.classA$B.classA$1.classA$2.class五個檔案。

class C {

}

class A {

    class B {

    }

    public void f2() {
        C c = new C() {

        };
    }

    public void f1() {
        B c = new B() {

        };
    }
}

由此可知,匿名類別實際上也擁有一個.class檔案,在概念上,它其實就是將一個類別或是介面在編譯new statement時由編譯器重新產生出另一個類別。

Java的Lambda

在Java 8之後,導入了Lambda語法,或者可以稱為Lambda表示式。如果說Lambda語法是用來表示一個匿名類別的話,那就不太正確了。實際上Lambda語法只能用來表示一個「只擁有一個方法的介面」所實作出來的匿名類別,但這樣講也是不太正確,在文章之後會進行觀念的改正,現在姑且先將其認知成這樣。

「只擁有一個方法的介面」在Java中很常使用到,例如執行緒的Runnable介面只擁有一個run方法,或是AWT的ActionListener只擁有一個actionPerformed方法。這類介面,都可以直接使用Lambda,將它們擁有的那單個方法快速實作出來。在Java 8之前,我們將這類的介面稱為Single Abstract Method type(SAM);在Java 8之後,因為這類的介面變得十分重要,所以將其另稱為Functional Interface。

Lambda語法結構

input -> body

其中,inputbody都各有多種撰寫方式,以下分別舉例。

input

不輸入

()

單個輸入

x

多個輸入(不省略型態)

(int x,int y)

多個輸入(省略型態)

(x,y)
body

什麼都不做

{}

單行不回傳值

System.out.println("NO");

多行不回傳值

{
    System.out.println("NO");
    System.out.println("NO2");
}

單行回傳值

x+y

多行回傳值

{
    x++;
    y-=x;
    return x+y;
}

以上幾種方式,可以自己練習組合看看。另外還有一種特殊語法結構,可以省略掉input->,因為會使程式碼較不易閱讀,不建議使用,將在文章最後提到。

Lambda實際應用與效能比較

取代Functional Interface產出的匿名類別

在Java中,許多只有一個方法的介面,如果要使用這些介面,往往需要使用到至少4行程式碼才有辦法達成,舉例:

Runnable runnbale = new Runnable() {
    public void run() {
        System.out.println("run me!");
    }
};

這樣的使用方式,往往會讓Java程式拉的很長,變得十分複雜。因此Lambda最重要的功能,就是取代Functional Interface所產出的匿名類別,簡化程式碼,甚至還可以提升效能(稍候提到)。舉例,同樣的Runnable使用方式,可以減化成:

Runnable runnbale = () -> System.out.println("run me!");

使用Lambda來取代以往Functional Interface的使用方式,可以大大的縮短程式碼,在編譯的過程中,也可以避免掉產生新的.class檔案出來,執行的時候,也不會再重新new出一個物件實體,而是直接將Lambda的body程式碼存放在記憶體,直接以類似call function的方式去執行,大大的提升程式的執行效能。

如果有興趣知道Lambda是如何運作,可以查看編譯出來.class檔案的bytecode,查看命令如下:

javap -c Class路徑

舉例:

javap -c org.magiclen.A

比較原先不使用Lambda,與使用Lambda的方式:

不使用Lambda

0: new #2
3: dup
4: invokespecial #3

使用Lambda

0: invokedynamic #2, 0

可以發現使用Lambda就連bytecode,都可以從三行命令,縮減成一行命令。invokedynamic就是動態調用記憶體內的某段程式碼。

此外,因為少了new這個指令,使用Lambda實作出來的方法,並不會另外實體化出物件,因此會有以下這個現象發生:

Runnable r1 = () -> System.out.println("r1: " + this.getClass());
	
Runnable r2 = new Runnable(){
    public void run(){
        System.out.println("r2: " + this.getClass());
    }
};

new Thread(r1).start();
new Thread(r2).start();

雖然兩個執行緒都是呼叫this.getClass(),但print出來的結果卻不一樣。而且可以知道使用Lambda的r1this所指的物件就是此行Lambda語法所在的物件,並不會像沒使用Lambda的r2一樣變成一個匿名類別的物件。

Lambda與Collection的關係

Lambda除了用來取代以往使用Functional Interface的方式外,還可以用在Collection的走訪、過濾和一些簡單的運算上。而且同樣地,使用Lambda可以增加不少的效能。

走訪

過去我們使用For each走訪一個List可能要寫成這樣:

for (String s : list) {
    System.out.print(s);
}

如果要走訪一個Map更麻煩,可能要寫成這樣:

Set<String> keySet = map.keySet();
for (String s : keySet) {
	System.out.print(s + ":" + map.get(s));
}

若改以Lambda來走訪這些Collection,可以簡化成這樣:

list.forEach(s -> System.out.print(s));
map.forEach((k, v) -> System.out.print(k + ":" + v));

程式碼精簡許多,因為少了迴圈,就連bytecode也能將原先十幾行指令省略到剩下三行。

過濾和基本運算

在新的Java 8中,Collection提供了stream()方法,可以對集合做一些過濾和基本運算,而且這個當然也是有經過效能優化過的。除了stream()方法外還有parallelStream()方法,可以讓Collection各別針對它的entry另開出一個Thread,進行stream提供的運算,讓多核心的CPU資源更能有效的被利用。以下將舉幾個過濾和基本運算的例子:

List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("3");
list.add("5");
list.add("4");
list.stream().filter(s -> Integer.valueOf(s) < 3).forEach(s -> System.out.print(s));

以上會輸出12

List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("3");
list.add("5");
list.add("4");
List<String> list2 = list.stream().filter(s -> Integer.valueOf(s) < 3).collect(Collectors.toList());
list2.add("7");
list2.forEach(s -> System.out.print(s));

以上會輸出127

List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("3");
list.add("5");
list.add("4");
System.out.println(list.stream().mapToInt(s->Integer.valueOf(s)).sum());
System.out.println(list.stream().filter(s -> Integer.valueOf(s) < 3).mapToInt(s->Integer.valueOf(s)).average().getAsDouble());

以上會輸出

15
1.5

Lambda特殊精簡語法結構

除了前面介紹到的input -> body結構,Lambda在某些特定的場合下,還能夠寫出更短的語法,結構如下:

方法名稱

什麼!?只要寫方法名稱就好了嗎?甚至連參數也不用傳!是的,Lambda允許這種類型的語法存在,但必須要明確指定方法名稱是在哪個類別或是哪個物件之下,而且最後一個.要改成::。這樣說好像很模糊,舉幾個例子好了:

interface B {

    public void doStringWork(String s);
}

interface C {

    public double doComputeWork(float x, float y);
}

public class A {

    public A() {
        B b = this::printOnce;
        b.doStringWork("哈囉");
    }

    public static void main(String[] args) {
        B b = A::printTwice;
        b.doStringWork("嗨");
        new A();

        C c = Math::pow;
        b.doStringWork(String.valueOf(c.doComputeWork(2.5f, 2)));
    }

    public static void printTwice(String s) {
        System.out.print(s);
        System.out.println(s);
    }

    public void printOnce(String s) {
        System.out.println(s);
    }
}

以上輸出結果為:

嗨嗨
哈囉
6.256.25

這樣的結構雖然可以使得程式變得非常精簡,卻也不易閱讀。這種撰寫方式,實際上常與forEach搭配,如下:

List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("3");
list.add("5");
list.add("4");
list.forEach(System.out::print);

以上程式會輸出12354

總結

Java 8依靠Lambda新語法,簡化程式,改善效能,在結構上淺顯易懂、容易使用,實屬不錯的新功能!