Java & Groovy & Scala & Kotlin - 16.方法,Lambda 與閉包

Overview

本章主要介紹各語言中的方法定義以及與方法有很大聯系的 Lambda 表達式,閉包等概念。

基本概念

為了更好的進行闡述,序章部分會簡單介紹一下本章會涉及到的各種名詞,在接下來各語言中會再進一步解說。

方法,函數與過程

這三種名詞在編程中非常常見,其概念非常相近。一般來說函數 (Function) 是可重復調用帶有有輸出和輸入的代碼塊。方法 (Method) 是定義在類中,作為類的成員的函數。過程(Subroutine)即沒有返回值的函數。也就是說函數是基礎形式,方法與過程只是函數的特例。

由于這些名詞容易混淆,在 Java 中一般都統一使用方法這個名詞。而在 Kotlin 中使用關鍵字 fun 即表示 Kotlin 中使用的其實是函數這個名詞。不過為了方便起見,本系列主要都使用方法這個不一定精確的名字。

Lambda 表達式

Java 8 中引入了 Lambda 表達式,實際接觸時發現有不少同學把這和函數式算子混到了一起理解,覺得 Lambda 表達式遍歷效率不行,這是一個非常大的誤解。實際上 Lambda 表達式不是什么新東西,就是一個匿名函數的語法糖,簡單理解就是繁體字=匿名函數,簡體字=Lambda,Java 8 無非就是在原來只能用繁字體的地方也允許使用簡體字罷了。

函數式接口

只包含一個抽象方法的接口,是 Java 8 中用于實現 Lambda 表達式的根本機制,函數接口就是一種 SAM 類型。

SAM 類型

SAM (Single Abstract Method)是有且僅有一個抽象方法的類型,該類型可以是抽象類也可以是接口。

閉包

閉包是一種帶有自由變量的代碼塊,其最顯著的特性就是能夠擴大局部變量的生命周期。

閉包與方法

閉包和方法的最大區別是方法執行完畢后其內部的變量便會被釋放,而閉包不會。閉包可以進行嵌套,而方法不行。

Java 篇

方法

定義方法

語法為

[訪問控制符] [static] [返回值類型] 方法名(參數列表)

Java 中方法必須聲明在類的內部,且被分為成員方法和靜態方法。

成員方法表示類的對象的一種行為,聲明時沒有關鍵字 static

public int add(int x, int y) {
    return x + y;
}

靜態方法使用關鍵字 static 聲明,屬于類的行為,或稱作類對象的行為,因此調用時無需創建任何對象。main() 方法就是最常見的靜態方法。

public static void main(String[] args) {
}

Varargs

Varargs 即參數長度不確定,簡稱變長參數。Java 使用符號 ... 表示變參,但是變參只能出現在參數列表的最后一個,即 sum(int x, int y, int...n) 是合法的,但 sum(int x, int...n, int y)sum(int...n, int x, int y) 都是非法的。

聲明一個變參方法

例:

class Calculator {
    public void sum(int... n) {
        int result = 0;
        for (int i : n) {
            result += i;
        }
        System.out.println(result);
    }
}

調用該方法

Calculator calculator = new Calculator();
calculator.sum(1, 2, 3);

參數默認值

Java 不支持參數默認值,所以調用時必須為每一個參數賦值

例:

private static void say(String name, String word) {
    if (word == null) {
        System.out.println(word + " " + name);
    }
}

say("Peter", null);

返回值

Java 中方法除非返回值類型聲明為 void 表示沒有返回值,否則必須在方法中調用 return 語句返回到調用處。

例:

public int add(int x, int y) {
    return x + y;
}

Lambda 表達式

序章已經說過了,Lambda 只是匿名方法的語法糖

例:

Java 8 以前實現匿名內部類的方式

  button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(final ActionEvent e) {
        System.out.println("Perform Click");
    }
});

Java 1.8 使用 Lambda 表達式簡化原來的調用方式

button.addActionListener(e -> System.out.println("Perform Click"));

函數接口

定義一個函數接口

@FunctionalInterface
interface Excite {
    String accept(String from);
}

以上使用了注解 @FunctionalInterface,在 Java 1.8 的初期版本這個注解用于標示一個接口為函數接口,但在現在的版本這個注解已經僅僅是個標識符了,可以進行省略。

使用 Lambda 表達式

Lambda 表達式的基本語法為

(參數列表) -> {執行語句}

如果執行語句只有一句的話可以省略包裹其的花括號

例:

Excite excite = (word) -> word + "!!";

然后我們可以很方便的調用這個接口

excite.accept("Java")

如果 Lambda 語句只有一個語句且只有一個參數,且該語句調用的是一個靜態方法,則可以使用符號 :: 進一步縮減代碼

Excite hello = (w) -> String.valueOf(w);

以上等同于

Excite hello = String::valueOf;

如果 Lambda 語句只有一個語句,且該語句為使用類的無參構造方法創建類的實例,則也可以使用符號 :: 進一步縮減代碼

Excite hello = (w) -> new Word();

以上等同于

Excite hello = Word::new;

多個參數

函數接口也可以接收多個參數,這些參數可以為泛型而不是具體類型,實際上使用泛型的函數接口更為常見

以下定義了一個接收兩個參數 F1F2,返回 T 類型的接口

interface Convert<F1, F2, T> {
    T convert(F1 from1, F2 from2);
}

使用該接口

Convert<Integer, Integer, String> convert = (x, y) -> {
    int result = x + y;
    return x + " plus " + y + " is " + result;
};
System.out.println(convert.convert(1, 2));  //  1 plus 2 is 3

變參

在 Lambda 表達式中也一樣可以使用變參

例:

定義一個含有變參的接口

interface Contact<F, T> {
    T accept(F... from);
}

使用該接口

Contact<String, String> contact = (args) -> String.join(",", args);
contact.accept("Java", "Groovy", "Scala", "Kotlin");

內置函數接口

通過以上例子我們可以看到要想使用 Lambda 表達式我們必須先定義一個函數接口,這樣用法太過麻煩。所以 Java 提供了一些內置的函數接口供我們調用.

Predicate

Predicate 接口用于接收一個參數并返回 Boolean 值,主要用于處理邏輯動詞。該接口還有一個默認方法 negate() 用于進行邏輯取反。

Predicate<String> predicate = (s) -> s.length() > 0;
assert predicate.test("foo");
assert !predicate.negate().test("foo");
Function

Function 接口接收一個參數并返回單一結果,主要用于進行類型轉換等功能。該接口也提供了一個 andThen() 方法用于執行鏈式操作。

Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
assert toInteger.apply("123") == 123;
assert backToString.apply("123").equals("123");
Supplier

Supplier 接口沒有參數,但是會返回單一結果,可以用于實現工廠方法。

Supplier<Calculator> calculatorSupplier = Calculator::new;
assert calculatorSupplier.get().add(1, 2) == 3;
Consumer

Consumer 接口接收一個參數,沒有返回值,用于對傳入的參數進行某些處理。該接口也提供了 andThen() 方法。

Consumer<Person> calculatorConsumer = (p) ->
        System.out.println("The name is " + p.getName());
calculatorConsumer.accept(new Person("Peter"));
Comparator

Comparator 接口接收兩個參數,返回 int 值,用于進行排序操作。該接口提供了 reversed() 方法進行反序排列。

Comparator<Person> comparator = (p1, p2) ->
        p1.getAge().compareTo(p2.getAge());
Person john = new Person("John", 20);
Person alice = new Person("Alice", 18);

assert comparator.compare(john, alice) > 0;
assert comparator.reversed().compare(john, alice) < 0;

函數接口作為參數

函數接口也可以作為參數來使用

private static int max(int[] arr, Function<int[], Integer> integerFunction) {
    return integerFunction.apply(arr);
}

使用該接口

int maxValue = max(new int[]{3, 10, 2, 40}, (s) -> {
    int max = -1;
    for (int n : s) {
        if (max < n) max = n;
    }
    return max;
});
assert maxValue == 40;

閉包

Java 中的閉包

Java 中和閉包相近的概念就是匿名類以及本章所說的 Lambda 表達式。但是這兩種都不是真正意義上的閉包。

先看一個例子:

interface Excite {
    String accept(String from);
}
private static Excite excite(String s) {
  Excite e = from -> {
    from = "from=" + from;
    return from + "," + s;
  };
  return e;
}
Excite excite = excite("hello");
System.out.println(excite.accept("world")); //  from=world,hello

以上例子中 e 為 Lambda 表達式,其定義在 excite() 方法中并且訪問了該方法的參數列表。按照生命周期,excite() 的參數 s 應該在方法被調用后就自動釋放,即 Excite excite = excite("hello") 調用后就不存在參數 hello 了,但實際打印語句還是打印出來了。

這一表現形式非常像閉包,因為參數明顯在其生命周期外還存活。但是如果我們在 Lambda 表達式內試圖修改參數 s 的值后編譯器會報 s 必須為 final ,這就是說該變量實際并不是自由變量,所以并不是真正的閉包。

如果查看 Groovy 的閉包形式你可以發現 Groovy 實際也是通過實現繼承自 Closure 類的匿名內部類來實現閉包形式的,這一點與 Java 一致。所以理論上 Java 也能實現真正的閉包,至于 1.8 為什么沒有這么做就不得而知了。

Groovy 篇

方法

定義方法

完整的 Groovy 方法定義語法為

[訪問控制符] [static] def 方法名(參數列表)

Groovy 也和 Java 一樣有成員方法和靜態方法之分。

成員方法表示類的對象的一種行為,聲明時沒有關鍵字 static

def add(x, y) {
    x + y
}

靜態方法使用關鍵字 static 聲明,屬于類的行為,或稱作類對象的行為,因此調用時無需創建任何對象。main() 方法就是最常見的靜態方法。

static def main(String[] args) {
}

Varargs

Groovy 表示變參的方式與 Java 一樣,且變參也只能出現在參數列表的最后一個。

聲明一個變參方法

class Calculator {
    def sum(int ... n) {
        print(n.sum())
    }
}

調用該方法

def calculator = new Calculator()
calculator.sum(1, 2, 3)

參數默認值

Groovy 支持參數默認值,但是一旦使用參數默認值時,參數列表的最后一個或最后幾個參數都必須有默認值,即 def foo(x, y, z ="bar")def foo(x, y = "bar", z = "bar") 都是合法的,但是 def foo(x, y = "bar", z) 則是非法的。

例:

static def say(name, word = "Hello") {
    println("$word $name")
}

say("Peter")

返回值

Groovy 中由動態類型的存在,所以可以不聲明返回值類型。并且在 Groovy 中方法的最后一個語句的執行結果總是回被返回(也適用于無返回值的時候),所以也無需 return 語句。

例:

def add(x, y) {
    x + y
}

Lambda 表達式

Groovy 目前還不支持 Java 1.8 的特性,所以 Java 中的 Lambda 表達式和對應的函數式接口無法在 Groovy 中直接使用。但是 Groovy 本身支持閉包,且閉包就是以 Lambda 表達式的形式存在的,所以閉包和 Lambda 合在一節講。

閉包

概念

閉包是一種帶有自由變量的代碼塊,其最顯著的特性就是能夠擴大局部變量的生命周期。與 Java 不同,Groovy 支持真正的閉包。

創建一個閉包

由于閉包是個代碼塊,所以一般意義上最簡單的閉包形式如下

{ println("foo") }

不過由于 Java 的普通代碼塊也是這樣的形式,所以為了避免混淆,以上閉包必須寫成如下形式

{ -> println("foo") }

綜上所述,閉包的語法為

{ 參數列表 -> 執行語句 }

例:

{ x, y ->
    println "$x plus $y is ${x + y}"
}

Groovy 中定義閉包實際是定義了一個繼承自 Closure 類的匿名內部類,執行閉包實際是執行該類的實例的方法。這一點與 Java 非常相似。

字面量

閉包可以存儲在一個變量中,這一點是實現函數是一等公民的重要手段。

例:

def excite = { word ->
    "$word!!"
}

調用閉包

excite("Java")

excite.call("Groovy")

多參數

閉包的參數可以和方法的參數一樣擁有多個參數及默認值

例:

def plus = { int x, int y = 1 ->
    println "$x plus $y is ${x + y}"
}

it

it 是個隱式參數,當閉包只有一個參數時,使用 it 可以直接指代該參數而不用預先聲明參數列表。

例:

def greeting = { "Hello, $it!" }
println(greeting("Peter"))

Varargs

閉包也支持變參

例:

def contact = { String... args -> args.join(',') }
println(contact("Java", "Groovy", "Scala", "Kotlin"))

閉包作為參數

由于閉包本質是 Closure 的子類,所以可以使用 Closure 作為參數的類型接收一個閉包

static def max(numbers, Closure<Integer> closure) {}

進一步簡化后

static def max(numbers, cls) {
    cls(numbers)
}

傳入閉包

def maxValue = max([3, 10, 2, 1, 40]) {
    def list = it as List<Integer>
    list.max()
}
assert maxValue == 40

自執行閉包

自執行閉包即定義閉包的同時直接執行閉包,一般用于初始化上下文環境,Javascript 中常使用這種方法來初始化文檔。

定義一個自執行的閉包

{ int x, int y ->
    println "$x plus $y is ${x + y}"
}(1, 3) //  1 plus 3 is 4

Scala 篇

方法

定義方法

完整的 Scala 方法定義語法為

[訪問控制符] def 方法名(參數列表) [:返回值類型] [=] {}

Scala 可以省略變量定義的類型聲明和返回值類型,但是在定義參數列表時則必須明確指定類型。

例:

def add(x: Int, y: Int): Int = {
  x + y
}

Scala 只有成員方法,沒有靜態方法,但是可以通過單例來實現靜態方法的功能,具體內容見 Object 章節。

參數列表

Scala 中參數列表必須明確指定參數類型。如果一個方法沒有參數列表時,可以省略小括號,但是調用時也不能加上小括號。

例:

//  沒有小括號
def info(): Unit = {
  println("This is a class called Calculator.")
}
println(info())

//  有小括號
def info2: Unit = {
  println("This is a class called Calculator.")
}
println(info)

Varargs

Scala 使用 參數類型* 表示變參。

聲明一個變參方法

class Calculator {
  def sum(n: Int*) {
    println(n.sum)
  }
}

調用該方法

val calculator = new Calculator
calculator.sum(1, 2, 3)

_*

如果希望將一個 Sequence 作為參數傳入上一節的 sum() 方法的話編輯器會報參數不匹配。此時可以使用 _* 操作符,_* 可以將一個 Sequence 展開為多個參數進行傳遞。

例:

calculator.sum(1 to 3: _*)

參數默認值

Scala 同 Groovy 一樣支持參數默認值,但是一旦使用參數默認值時,參數列表的最后一個或最后幾個參數都必須有默認值。

def say(name: String, word: String = "Hello"): Unit = {
  println(s"$word $name")
}

say("Peter")

返回值

Scala 中總是會返回方法內部的最后一個語句的執行結果,所以無需 return 語句。如果沒有返回值的話需要聲明返回值類型為 Unit,并此時可以省略 :Unit=。如果方法沒有遞歸的話返回值類型也可以省略,但是必須使用 =

默認返回最后一行的執行結果

def add(x: Int, y: Int): Int = {
  x + y
}

無返回值的情況

def echo(): Unit = {}

無返回值時可以簡寫為以下形式

def echo() = {}

方法嵌套

Scala 支持方法嵌套,即一個方法可以定義在另一個方法中,且內層方法可以訪問外層方法的成員。

例:

def testMethod(): Unit = {
  var x = 1
  def add(y: Int): Int = {
    x + y
  }
  println(add(100))
}

Lambda 表達式

同 Groovy 一樣,閉包和 Lambda 也合在一節講。

閉包

同 Groovy 一樣,Scala 也支持閉包,但是寫法有些不同。

創建一個閉包

由于閉包是個代碼塊,所以最簡單的閉包形式如下

例:

() => println("foo")

字面量

閉包可以存儲在一個變量中,這一點是實現函數是一等公民的重要手段。

例:

val excite = (word: String) =>
  s"$word!!"

調用閉包

excite("Java")

excite.apply("Scala")

多參數

閉包的參數可以和方法的參數一樣擁有多個參數,但是同 Groovy 不一樣,Scala 中閉包的參數不能有默認值,且參數列表為多個時必須將參數包裹在小括號內。

例:

val plus =  (x: Int, y: Int) =>
  println(s"$x plus $y is ${x + y}")

_

_ 是個占位符,當閉包只有一個參數時,使用 _ 可以直接指代該參數而不用預先聲明參數列表。

例:

val greeting = "Hello,  " + _
println(greeting("Peter"))

Varargs

Scala 中閉包不支持變參

閉包作為參數

def max(numbers: Array[Int], s: (Array[Int]) => Int): Unit = {
  s.apply(numbers)
}

傳入閉包

val maxValue = max(Array(3, 10, 2, 1, 40), (numbers) => {
  numbers.max
})

也可以使用如下方式進行簡化

def max2(numbers: Array[Int])(s: (Array[Int]) => Int): Unit = {
  s.apply(numbers)
}

maxValue = max2(Array(3, 10, 2, 1, 40)) { numbers =>
  numbers.max
}

自執行閉包

自執行閉包即定義閉包的同時直接執行閉包,一般用于初始化上下文環境,Javascript 中常使用這種方法來初始化文檔。

定義一個自執行的閉包

例:

((x: Int, y: Int) => {
  println(s"$x plus $y is ${x + y}")
})(1, 3)    //  1 plus 3 is 4

Kotlin 篇

方法

定義方法

完整的 Kotlin 方法定義語法為

[訪問控制符] fun 方法名(參數列表) [:返回值類型] {}

Kotlin 可以省略變量定義的類型聲明,但是在定義參數列表和定義返回值類型時則必須明確指定類型。

例:

fun add(x: Int, y: Int): Int {
    return x + y
}

Kotlin 只有成員方法,沒有靜態方法,但是可以通過單例來實現靜態方法的功能,具體內容見 Object 章節。

Varargs

Kotlin 使用 vararg 修飾參數來表示變參。

聲明一個變參方法

class Calculator {
    fun sum(vararg n: Int) {
        println(n.sum())
    }
}

調用該方法

val calculator = Calculator()
calculator.sum(1, 2, 3)

參數默認值

Kotlin 同 Scala 一樣支持參數默認值,但是一旦使用參數默認值時,參數列表的最后一個或最后幾個參數都必須有默認值。

fun say(name: String, word: String = "Hello") {
    println("$word $name")
}

say("Peter")

返回值

Kotlin 同 Java 一樣不會必須使用 return 語句來返回執行結果。

例:

fun add(x: Int, y: Int): Int {
    return x + y
}

方法嵌套

Kotlin 支持方法嵌套,即一個方法可以定義在另一個方法中,且內層方法可以訪問外層方法的成員。

例:

fun testMethod() {
    var x = 1
    fun add(y: Int): Int {
        return x + y
    }
    println(add(100))
}

Lambda 表達式

同 Scala 一樣,閉包和 Lambda 也合在一節講。

閉包

同 Scala 一樣,Kotlin 也支持閉包,但是寫法有些不同。

創建一個閉包

由于閉包是個代碼塊,所以最簡單的閉包形式如下

例:

{ -> println("foo") }

字面量

閉包可以存儲在一個變量中,這一點是實現函數是一等公民的重要手段。

例:

val excite = { word: String ->
    "$word!!"
}

調用閉包

excite("Java")

excite.invoke("Kotlin")

多參數

同 Scala 一樣,Kotlin 中閉包的參數不能有默認值。

例:

val plus = { x: Int, y: Int ->
    println("$x plus $y is ${x + y}")
}

it

同 Groovy 一樣閉包只有一個參數時可以使用 it 直接指代該參數而不用預先聲明參數列表。但是不像 Groovy 那么方便,Kotlin 中這一特性僅能用作傳遞作為參數的閉包中而不能用在定義閉包時。

以下閉包作為參數傳遞給方法 filter

val ints = arrayOf(1, 2, 3)
ints.filter {
    it > 3
}

以下定義閉包時指定 it 是非法的

val greeting = { -> println(it) }

Varargs

Kotlin 中閉包不支持變參

閉包作為參數

fun max(numbers: Array<Int>, s: (Array<Int>) -> Int): Int {
    return s.invoke(numbers)
}

傳入閉包

val maxValue = max(arrayOf(3, 10, 2, 1, 40)) {
    it.max()!!
}

自執行閉包

自執行閉包即定義閉包的同時直接執行閉包,一般用于初始化上下文環境,Javascript 中常使用這種方法來初始化文檔。

定義一個自執行的閉包

{ x: Int, y: Int ->
    println("$x plus $y is ${x + y}")
}(1, 3)    //  1 plus 3 is 4

Summary

  • ?除 Java 外,其它語言都支持參數默認值

文章源碼見 https://github.com/SidneyXu/JGSK 倉庫的 _16_method 小節

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容