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;
多個參數
函數接口也可以接收多個參數,這些參數可以為泛型而不是具體類型,實際上使用泛型的函數接口更為常見
以下定義了一個接收兩個參數 F1
和 F2
,返回 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
小節