Java程序员第一次写Scala函数的时候,一定会困惑一件事:为什么Scala的函数写起来这么奇怪?
scala
// Java写法
public static int add(int a, int b) {
return a + b;
}
// Scala写法
def add(a: Int, b: Int): Int = a + b
看起来差不多------参数、返回值、函数体。但真正用下去,你会发现越来越多奇怪的地方:为什么函数可以当参数传?为什么函数可以当返回值?为什么_ + _就是一个函数?为什么map方法接收的不是一个函数名,而是一个函数?
很多Java程序员学Scala函数的时候,记住了语法,但没理解本质。结果就是:写出来的Scala代码还是Java风格------全是def方法,没有函数式味道。
这篇文章不讲"函数怎么定义"这种入门废话,直接说干货:Scala里方法和函数到底差在哪?为什么函数是一等公民?高阶函数到底是什么?

先从一个困惑说起
Java程序员都知道方法(method)------它是类的一部分,你调用它,它干活。
但Scala里多了一个概念叫"函数"(function)。函数和方法到底有什么区别?
scala
// 这是一个方法(method)
def add(a: Int, b: Int): Int = a + b
// 这是一个函数(function value)
val addFunc = (a: Int, b: Int) => a + b
看起来都是"两个Int进去,一个Int出来"。但区别在哪?
区别在于:方法是属于类的,函数是一个值。
方法是类的成员------它绑定在某个类上,你通过对象调用它。函数是一个对象------它本身就是一个值,可以赋值给变量、可以作为参数传递、可以作为返回值。
scala
// 方法:属于类,不能直接赋值给变量
// val f = add // 这样写会报错吗?
// 函数:是值,可以随便传
val f = (a: Int, b: Int) => a + b
val g = f // 函数可以赋值
List(1, 2, 3).map(f) // 函数可以作为参数
这个区别看起来很小,但它是函数式编程的基石。如果函数不能作为值传递,那map、flatMap、filter这些高阶函数就根本不存在。
方法和函数的关系:它们是"可转换"的
Java程序员最困惑的点就在这:Scala里到底什么时候写方法,什么时候写函数?
答案是:日常开发里,你写def定义的是方法,但在需要函数的地方,Scala会自动把方法"提升"成函数。
scala
// 定义一个方法
def add(a: Int, b: Int): Int = a + b
// 这里需要一个函数:(Int, Int) => Int
// Scala自动把add方法转成了函数
val list = List(1, 2, 3)
list.map(x => add(x, 1)) // 这里add是方法,但被当成函数用了
这个过程叫"eta展开"------Scala编译器自动把方法转换成函数值。所以你日常写代码的时候,不需要刻意区分"这是方法还是函数"------编译器会帮你处理。
但理解这个区别很重要:当你需要把函数存起来、传出去的时候,它就必须是函数值,不能是方法。
scala
// 方法可以直接调用
add(1, 2) // 3
// 但方法不能直接赋值给变量
// val f = add // 这样写Scala会报错,因为add是方法不是函数
// 要把方法转成函数值,加个下划线
val f = add _ // eta展开:方法变成函数值
这个下划线_就是"把方法转成函数"的意思。很多人觉得这个语法奇怪------但它背后的设计意图是:明确告诉编译器,我要的是这个方法的函数值,不是调用这个方法。
函数的本质:它是一个接口
很多人以为函数是个很玄的东西------"函数式编程嘛,函数是一等公民"。但如果你从JVM的角度看,函数一点都不玄。
Scala的函数,本质上就是一个接口。
你定义一个函数(Int, Int) => Int,编译器背后给你生成的是:
scala
// 你写的
val addFunc = (a: Int, b: Int) => a + b
// 编译器背后做的
val addFunc = new Function2[Int, Int, Int] {
def apply(a: Int, b: Int): Int = a + b
}
看到了吗?所谓"函数值",其实就是一个实现了Function2接口的匿名类对象。
这就是为什么函数可以当参数传------因为它是一个对象,一个实现了FunctionN接口的对象。
scala
// 你写的
List(1, 2, 3).map(x => x + 1)
// 编译器背后
List(1, 2, 3).map(new Function1[Int, Int] {
def apply(x: Int): Int = x + 1
})
从这个角度看,Scala的函数和Java的匿名内部类,本质上是同一个东西------只不过Scala给了你一个更简洁的语法糖。
这就是Scala函数的真相:它不是什么魔法,它就是一个实现了FunctionN接口的对象。
函数定义的几种方式
1. 方法定义:def
最常用的方式。定义一个方法,属于当前类/对象。
scala
def add(a: Int, b: Int): Int = a + b
def greet(name: String): Unit = {
println(s"Hello, $name")
}
特点:有名字、有参数类型、有返回类型(可以省略,编译器推断)、函数体用=连接。
2. 函数值定义:val + lambda
定义一个函数值,它是一个对象,可以到处传。
scala
val addFunc = (a: Int, b: Int) => a + b
val greetFunc = (name: String) => println(s"Hello, $name")
特点:用val定义(因为它是值),参数和返回类型写在括号里,=>后面是函数体。
3. 匿名函数:_ 简写
当函数参数只有一个,而且只用一次的时候,可以用下划线简写。
scala
List(1, 2, 3).map(_ + 1) // 等价于 map(x => x + 1)
List("a", "b", "c").map(_.toUpperCase) // 等价于 map(s => s.toUpperCase)
这个写法简洁,但要注意:**下划线代表"参数",而且只能用一次。**如果函数体里同一个参数要用两次,就不能用下划线简写了。
scala
// 不能这样写:_用了两次,编译器不知道你指的是哪个
// List(1, 2, 3).reduce(_ + _) // 这个可以,两个不同的_
// 这个也可以:_出现两次,但reduce需要两个参数
List(1, 2, 3).reduce(_ + _) // 等价于 (a, b) => a + b
4. 带隐式参数的函数
这个是Scala特有的,在大数据开发里经常用到。
scala
def greet(name: String)(implicit prefix: String): Unit = {
println(s"$prefix, $name")
}
// 调用的时候,第二个参数列表自动找implicit值
implicit val prefix: String = "你好"
greet("Alice") // 自动传入prefix
这个语法看起来奇怪,但它是Scala做依赖注入的方式------Spark里的SparkSession就是靠implicit传的。
高阶函数:函数当参数传
理解了"函数是一个对象"之后,高阶函数就很好理解了。
高阶函数,就是接收一个函数作为参数,或者返回一个函数的函数。
scala
// map就是一个高阶函数
// 它接收一个函数:T => U,然后对每个元素应用这个函数
def map[U](f: T => U): List[U] = ...
// 你用的时候
List(1, 2, 3).map(x => x * 2)
// ^^^^^^^^^^^^
// 这就是传进去的函数
Java 8也有Lambda,也有Stream.map------那Scala的高阶函数和Java的有什么区别?
区别在于:Scala的函数是从一开始就设计成一等公民的,Java的Lambda是后来加的。
具体来说:
- Java的Lambda只能是函数式接口的实例 ------你传个Lambda,背后必须有个
@FunctionalInterface接口 - Scala的函数就是
FunctionN接口 ------它是统一的,不管几个参数,都是Function1、Function2... - Scala支持函数作为返回值------这个Java也有,但Scala用得更自然
scala
// 函数作为返回值:柯里化
def multiplyBy(factor: Int): Int => Int = {
(x: Int) => x * factor
}
val double = multiplyBy(2) // 返回一个函数:Int => Int
double(5) // 10
val triple = multiplyBy(3)
triple(5) // 15
这个叫"函数工厂"------你给它一个参数,它给你返回一个函数。这种写法在Java里也能写,但Scala的语法让它自然得多。
写在最后
我写这篇文章的时候,特意没罗列函数有多少种写法------因为那些语法东西,查文档就有。
真正值得花时间理解的是:Scala为什么要有函数这个概念?
不是因为"函数式编程嘛,函数是一等公民"------这句话说了等于没说。而是因为:当你需要对一组数据做变换的时候,你需要一个"可以传进去的操作"。
Java里你怎么做?你定义一个接口,然后实现它,然后传进去------这就是匿名内部类。Scala给了你一个更简洁的方式:lambda表达式。但本质是一样的------都是传一个"操作"进去。
函数的本质,就是一个可以传递的操作。
理解了这个,你就理解了Scala函数的设计哲学。你不会再纠结"为什么Scala的函数写起来这么奇怪"------因为你知道,那不是奇怪,那是在JVM之上,给函数式编程搭的一层抽象。
Java 8加了Lambda,加了Stream------但Java的设计思路是"给对象加函数能力",Scala的设计思路是"函数本身就是对象"。出发点不一样,写出来的代码感觉也不一样。
这就是为什么Spark用Scala写起来那么优雅------因为你可以把一个函数直接传给RDD,不需要定义接口,不需要实现匿名类,一行lambda就搞定了。
函数不是魔法,它就是一个实现了FunctionN接口的对象。理解了这一点,你就理解了Scala函数的全部。