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
小节