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

推荐阅读更多精彩内容

  • 一. 背景 Kotlin 已然成为Android 开发的首推语言,我们以前在开发gradle 插件时,通常会使用g...
    wind_sky阅读 335评论 0 0
  • 1. 预解析的相关概念 JavaScript 代码是由浏览器中的 JavaScript 解析器来执行的。JavaS...
    itlu阅读 46评论 0 3
  • 锁的分类 悲观锁和乐观锁 在Java里使用的各种锁,几乎全都是悲观锁。synchronized从偏向锁、轻量级锁到...
    风月寒阅读 37评论 0 0
  • 1.var 定义的是变量,没有块作用域的概念,可以跨块作用域访问,不能跨函数访问 块级作用域由 { } 包括,if...
    onresize阅读 848评论 0 1
  • 为什么使用 ES6 ? 每一次标准的诞生都意味着语言的完善,功能的加强。JavaScript语言本身也有一些令人不...
    肖青荣阅读 130评论 0 3