方法与函数

方法与函数 #

共通 #

方法,函数与过程 #

这三种名词在编程中非常常见,其概念非常相近。一般来说函数 (Function) 是可重复调用带有有输出和输入的代码块。方法 (Method) 是定义在类中,作为类的成员的函数。过程(Subroutine)即没有返回值的函数。也就是说函数是基础,其它两种只是函数的特例。

由于这些名词容易混淆,在 Java 中统一都使用方法这个名词。而在 Kotlin 中使用关键字 fun 即表示 Kotlin 中使用的是函数这个名词。为了方便起见,本系列都使用方法这个名字进行描述,只有在牵涉到函数式编程时才使用函数这个名词。

Lambda 表达式 #

所谓的 Lambda 表达式其实就是一个匿名函数。

SAM 类型 #

SAM (Single Abstract Method)是有且仅有一个抽象方法的类型,该类型可以是抽象类也可以是接口。

闭包 #

闭包是一种带有自由变量的代码块,其最显著的特性就是能够扩大局部变量的生命周期。

闭包与 Lambda 表达式 #

闭包与 Lambda 表达式的概念非常容易让人混淆,但两者确实是不同的东西。Lambda 表达式是匿名函数,而闭包则是实现函数是第一类对象的一种手段。两者的直接关系是 Lambda 表达式可以作为闭包的一种表现形式,但闭包除了 Lambda 表达式也可以表现为多种形式,这也就是在各种语言中闭包的写法会有很多差别的原因。

闭包与方法 #

闭包和方法的最大区别是方法执行完毕后其内部的变量便会被释放,而闭包不会。闭包可以进行嵌套,而方法不行。

Java #

方法 #

定义方法 #

完整的 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 中由于不能有独立于类的函数存在,所以匿名函数一直都是通过定义一个包含抽象方法的匿名内部类来实现的。而 Java 1.8 后引入的 Lambda 表达式其实只是原来实现方式的一种语法糖。

Java 1.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"));

SAM 类型与函数接口 #

SAM (Single Abstract Method)是有且仅有一个抽象方法的类型,该类型可以是抽象类也可以是接口。

函数接口是 Java 1.8 中引入的概念,其实就是一个普通的接口,但是该接口中有且仅有一个抽象方法。所以函数接口就是一种 SAM 类型。

Java 中的 Lambda 表达式就是通过函数接口来实现的,所以其与 1.8 以前使用匿名内部类的最大区别就是匿名内部类中可以定义多个抽象方法,而要使用 Lambda 表达式则只能定义一个抽象方法。

定义一个函数接口

@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;

闭包 #

概念 #

闭包是一种带有自由变量的代码块,其最显著的特性就是能够扩大局部变量的生命周期。

闭包与 Lambda 表达式 #

闭包与 Lambda 表达式的概念非常容易让人混淆,但两者确实是不同的东西。Lambda 表达式是匿名函数,而闭包则是实现函数是第一类对象的一种手段。两者的直接关系是 Lambda 表达式可以作为闭包的一种表现形式,但闭包除了 Lambda 表达式也可以表现为多种形式,这也就是在各种语言中闭包的写法会有很多差别的原因。

闭包与方法 #

闭包和方法的最大区别是方法执行完毕后其内部的变量便会被释放,而闭包不会。闭包可以进行嵌套,而方法不行。

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
沪ICP备17055033号-2