Java 8之前你写回调,得这么写:
// Java 8之前:匿名内部类
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("clicked");
}
});
四行代码,就为了传一个回调函数。这就是Java早期的"匿名函数"——匿名内部类。
Java 8之后呢?
// Java 8:Lambda
button.setOnClickListener(v -> System.out.println("clicked"));
一行搞定。
Scala呢?Scala从第一天就有匿名函数——而且它的匿名函数比Java的Lambda更彻底、更灵活。
// Scala:匿名函数
val add = (a: Int, b: Int) => a + b
add(1, 2) // 3
// 用在集合操作里
List(1, 2, 3).map(x => x * 2) // List(2, 4, 6)
List(1, 2, 3).filter(x => x > 1) // List(2, 3)
这篇文章不讲"匿名函数是什么"这种入门废话,直接说干货:Scala的匿名函数底层是什么?它和Java的Lambda比好在哪?闭包到底是怎么捕获变量的?有什么坑?

先从一个痛点说起
Java 8之前,你想把函数当参数传怎么办?答案是:传一个接口的实现。
// Java:用接口实现"函数当参数"
interface Calculator {
int calculate(int a, int b);
}
public int compute(int a, int b, Calculator calc) {
return calc.calculate(a, b);
}
// 调用:传一个匿名内部类
compute(1, 2, new Calculator() {
@Override
public int calculate(int a, int b) {
return a + b;
}
});
这就是Java早期的"函数式编程"——你得先定义一个接口,再写一个匿名内部类,才能把函数当参数传。啰嗦,繁琐,但没办法——因为Java是面向对象语言,一切皆对象,函数不是一等公民。
Scala呢?Scala从第一天就把函数当一等公民——你可以把函数赋值给变量、当参数传、当返回值返回。
// Scala:函数是一等公民
val add = (a: Int, b: Int) => a + b // 函数赋值给变量
val result = compute(1, 2, add) // 函数当参数传
def compute(a: Int, b: Int, f: (Int, Int) => Int): Int = {
f(a, b)
}
没有接口,没有匿名内部类——一行=>就是一个函数。
这就是Scala匿名函数和Java匿名内部类的本质区别:Java的匿名函数是"对象的语法糖",Scala的匿名函数是"函数的语法糖"。
匿名函数的本质:它是一个FunctionN对象
很多人以为Scala的匿名函数就是个语法糖——你写(a: Int) => a * 2,编译器帮你生成一个类。
对,但不完整。
val double = (a: Int) => a * 2
这行代码在底层生成了什么?——一个Function1[Int, Int]的实例。
// 编译器帮你生成的代码,大概长这样
val double = new Function1[Int, Int] {
override def apply(a: Int): Int = a * 2
}
double(5) // 10——因为有apply方法,所以可以像函数一样调用
看到了吗?Scala的匿名函数本质上就是一个实现了FunctionN接口的对象。你有几个参数,它就是Function1、Function2、Function3……一直到Function22。
// 一个参数 → Function1
val f1 = (x: Int) => x + 1
// 两个参数 → Function2
val f2 = (x: Int, y: Int) => x + y
// 三个参数 → Function3
val f3 = (x: Int, y: Int, z: Int) => x + y + z
Scala最多支持22个参数的函数——因为标准库只定义了Function1到Function22。
这就是为什么Scala的匿名函数可以像函数一样调用——因为它是一个对象,但它有apply方法,所以你可以obj(args)这样调用。
闭包:匿名函数最强大的地方
匿名函数最神奇的能力是什么?——它可以捕获外部变量。
var multiplier = 2
val multiply = (x: Int) => x * multiplier
multiply(5) // 10
multiplier = 3
multiply(5) // 15——注意!这里变了!
看到了吗?multiply这个函数,捕获了外部变量multiplier。当multiplier变了,函数的行为也变了。
这就是闭包。
闭包 = 函数 + 它捕获的环境。
Java的匿名内部类也能捕获变量——但它有一个限制:你只能捕获final或等效final的变量。你不能在匿名内部类里修改外部变量。
// Java:匿名内部类只能捕获final变量
int multiplier = 2;
Runnable r = () -> {
System.out.println(multiplier * 5);
// multiplier = 3; // 编译报错!不能修改外部变量
};
Scala呢?Scala的闭包可以捕获变量,甚至可以修改变量。
var count = 0
val increment = () => {
count += 1
println(s"count = $count")
}
increment() // count = 1
increment() // count = 2
increment() // count = 3
为什么Scala能做到?因为Scala的闭包捕获的不是变量的值——而是变量本身。它捕获的是一个引用,通过这个引用可以读写外部变量。
Java的闭包是"值捕获"——你捕获的是变量的值,不能改。Scala的闭包是"变量捕获"——你捕获的是变量本身,可以读写。
这就是函数式编程和命令式编程的区别——函数式语言把函数当一等公民,所以它的闭包也是完整的闭包。
几个常见的简写
Scala的匿名函数有很多简写语法,写起来非常爽。
1. 下划线占位符
List(1, 2, 3).map(_ * 2) // List(2, 4, 6)
List(1, 2, 3).filter(_ > 1) // List(2, 3)
List(1, 2, 3).reduce(_ + _) // 6
_就是"这个参数"的占位符。如果函数只有一个参数,你可以用_代替参数名。
2. 类型推断
// 不用写参数类型——编译器帮你推断
List(1, 2, 3).map(x => x * 2)
// 用下划线更简洁
List(1, 2, 3).map(_ * 2)
Scala的类型推断会根据上下文推断参数类型——List[Int].map的参数肯定是Int,所以你不用写。
3. 多参数函数
// 两个参数的函数
val add = (_: Int) + (_: Int)
add(1, 2) // 3
每个_代表一个参数——第一个_是第一个参数,第二个_是第二个参数。
几个常见的坑
坑1:闭包捕获的是变量,不是值
var total = 0
val addToTotal = (x: Int) => total += x
addToTotal(10)
addToTotal(20)
println(total) // 30
很多人第一次写闭包的时候,以为闭包捕获的是变量的值。但实际上,闭包捕获的是变量本身——它是一个引用,指向那个变量。
所以当你修改变量的时候,所有闭包看到的值都变了。
这个特性有时候会导致意外——比如你在循环里创建闭包:
// 这个会出问题
val functions = (1 to 3).map { i =>
() => i
}
functions.foreach(f => println(f()))
// 你以为输出 1 2 3?
// 实际上输出什么?
这个问题在Java和JavaScript里都有——因为闭包捕获的是变量本身,不是变量的值。Scala也一样。
坑2:序列化问题
这个是大数据开发里最常见的坑——Flink/Spark里用Scala匿名函数,有时候会报序列化错误。
为什么?因为Scala的匿名函数是一个对象——它实现了FunctionN接口。如果你用的是普通的类,它没有实现Serializable,序列化的时候就会报错。
// 这个在Flink/Spark里会报序列化错误
val data = List(1, 2, 3)
data.map(x => x * 2) // 匿名函数没有实现Serializable
怎么解决?把匿名函数提取成一个独立的类,或者用lambda的方式。
这就是为什么Flink官方文档里说:JdbcSink的第二个参数不能用Scala匿名函数写,否则会报序列化错误。
坑3:函数的arity限制
Scala最多支持22个参数的函数——因为标准库只定义了Function1到Function22。
如果你需要23个参数的函数怎么办?——把多个参数打包成一个元组,或者用柯里化。
什么时候用匿名函数?
匿名函数最适合的场景就是:函数只在一个地方用,不需要复用。
比如:
// 集合操作——匿名函数用一次就扔
list.map(_ * 2)
list.filter(_ > 10)
list.reduce(_ + _)
// 回调——传一个一次性的回调
button.onClick(() => println("clicked"))
什么时候不用?——如果这个函数你要在多个地方复用,那就给它起个名字,定义成命名函数。
// 复用的函数——用命名函数
def double(x: Int): Int = x * 2
list.map(double)
list.filter(double(_) > 4)
简单说:一次性的操作用匿名函数,复用的操作用命名函数。
写在最后
我写这篇文章的时候,特意没罗列匿名函数有多少种写法——因为写法很简单,谁都能写。
真正值得花时间理解的是:匿名函数为什么是函数式编程的基石?
因为函数式编程的核心思想就是:把函数当参数传。
你不能把函数当参数传,就没有map、filter、reduce这些集合操作。没有这些集合操作,就没有声明式编程。没有声明式编程,就没有函数式编程。
Java花了很长时间才意识到这一点——Java 8才加了Lambda。但Java的Lambda是不完整的——它只能捕获final变量,不能修改变量。它是一个"阉割版"的闭包。
Scala呢?Scala从第一天就有完整的匿名函数和闭包——它可以捕获变量,可以修改变量,可以把函数当一等公民。
匿名函数不是一个"语法糖"——它是函数式编程的基石。没有匿名函数,就没有函数式编程。
你写的不是一个"没有名字的函数"——你写的是一个可以被传递、被组合、被组合的计算单元。
这就是函数式编程的威力:把计算当数据传递。