欢迎光临
我们一直在努力

深入理解Scala可变参数函数:从Java数组语法到优雅集合传递

Java程序员写过public static void main(String… args)吧?这个String…就是Java的可变参数。

很多人第一次看到Scala的可变参数,第一反应是:“这不是和Java一样吗?不就是个数组吗?”

对,也不对。

说它对——Scala的可变参数本质上确实是一个序列。说它不对——Scala的可变参数比Java灵活太多了。Java的可变参数你只能传离散的值,Scala的可变参数你可以直接传一个集合——用:_*语法。

// Java:你只能传离散的值
public static void print(String... args) {
for (String s : args) System.out.println(s);
}

print("a", "b", "c"); // 可以

String[] arr = {"a", "b", "c"};
print(arr); // 也可以——但这是隐式的

List<String> list = Arrays.asList("a", "b", "c");
print(list); // 编译报错!List不是String[]

// Scala:你可以传离散值,也可以传一个集合
def print(args: String*): Unit = {
args.foreach(println)
}

print("a", "b", "c") // 离散值,没问题
val arr = Array("a", "b", "c")
print(arr: _*) // 传一个Array,用:_*展开
val list = List("a", "b", "c")
print(list: _*) // 传一个List,也用:_*展开

看出区别了吗?Java的可变参数只认数组,Scala的可变参数认任何序列——只要你用:_*告诉编译器"把这个集合展开成可变参数"。

这篇文章不讲"可变参数是什么"这种入门废话,直接说干货:Scala的可变参数底层是什么?:_*到底在做什么?和Java的可变参数比有什么区别?有什么坑?

Scala可变参数函数知识体系


先从一个痛点说起

你写一个函数,要接收任意个数的参数。Java里你会怎么做?

// Java:用可变参数
public static int sum(int... nums) {
int total = 0;
for (int n : nums) total += n;
return total;
}

sum(1, 2, 3); // 6
sum(1, 2, 3, 4, 5); // 15

看起来挺好的。但问题来了——如果你已经有了一个数组或List怎么办?

int[] arr = {1, 2, 3};
sum(arr); // 可以——因为int…本质就是int[]

List<Integer> list = Arrays.asList(1, 2, 3);
sum(list); // 编译报错!List不是int[]

Java的可变参数有一个隐藏的限制:**它本质上就是一个数组。**你传List进去,它不认——因为List不是数组。

那你怎么办?把List转成数组:

sum(list.toArray(new Integer[0])); // 强转一圈,麻烦

Scala呢?

def sum(nums: Int*): Int = {
nums.sum
}

sum(1, 2, 3) // 6
val arr = Array(1, 2, 3)
sum(arr: _*) // 6——直接传Array
val list = List(1, 2, 3)
sum(list: _*) // 6——直接传List
val seq = Seq(1, 2, 3)
sum(seq: _*) // 6——直接传Seq

不用转数组,不用强转——只要加个:_*就行。

这就是Scala可变参数和Java最大的区别:Java的可变参数是"数组的语法糖",Scala的可变参数是"序列的语法糖"。


可变参数的本质:它是一个WrappedArray

Scala里你写:

def sum(nums: Int*): Int = {
nums.sum
}

这个nums: Int*在底层是什么?——是一个Seq[Int]。更准确地说,是一个scala.collection.mutable.WrappedArray。

def whatAmI(args: Int*): Unit = {
println(args.getClass.getSimpleName)
}

whatAmI(1, 2, 3) // 输出:WrappedArray$ofInt

看到了吗?可变参数不是数组——它是一个包装过的序列。这就是为什么你可以在上面调用.sum、.map、.filter这些Seq的方法——因为它本身就是个Seq。

def process(args: String*): Unit = {
// 直接调用Seq的方法
args.foreach(println)
args.map(_.toUpperCase).foreach(println)
args.filter(_.startsWith("a")).foreach(println)
}

Java的可变参数你能干这个吗?Java的int…就是int[]——你只能用数组的方法。想调用.stream()?要自己转。想.map()?要自己写for循环。

Scala的可变参数天生就是集合——它自带所有Seq的方法。


:_*到底在做什么?

这是Scala可变参数最神奇的语法。很多人第一次看到:_*的时候,完全不知道这是什么意思。

简单说::_*就是告诉编译器——“把这个集合展开成可变参数”。

def sum(nums: Int*): Int = nums.sum

val list = List(1, 2, 3)

// 不带:_*——编译器报错
sum(list) // 类型不匹配:期望Int*,实际List[Int]

// 带:_*——编译器把list展开成三个参数
sum(list: _*) // 6

为什么要这个语法?因为Scala的可变参数接受的是Int*——一个特殊的类型标记。你传一个List[Int]进去,编译器不知道你是想传一个List,还是想把List里的元素展开成三个参数。

:_*就是你明确告诉编译器:“我要展开。”

**这个语法设计得非常好——它是显式的,不是隐式的。**你想把集合展开成可变参数,你必须明确说出来。编译器不会猜你的意图。


和Java可变参数的对比

很多人会问:Scala的可变参数和Java的有什么区别?

对比维度Java可变参数Scala可变参数
底层类型 数组 T[] 序列 Seq[T](WrappedArray)
传集合 不支持,要转数组 支持,用:_*展开
可调用方法 只有数组方法 所有Seq方法(map/filter/sum等)
调用方式 f(1,2,3) 或 f(arr) f(1,2,3) 或 f(seq: _*)
类型安全 编译时擦除,有坑 编译时类型检查,安全

最关键的区别就是:Java的可变参数是数组的语法糖,Scala的可变参数是Seq的语法糖。

这意味着Scala的可变参数天生就是函数式的——你可以在上面做map、flatMap、filter、fold这些操作,不用转来转去。

Java的可变参数呢?它就是个数组——你想用集合操作?自己转Stream吧。


几个常见的坑

坑1::_*只能用在可变参数函数上

def sum(nums: Int*): Int = nums.sum

val list = List(1, 2, 3)
sum(list: _*) // 可以——因为sum是可变参数函数

def add(a: Int, b: Int): Int = a + b
add(list: _*) // 编译报错——add不是可变参数函数

:_*这个语法只能用在可变参数函数上——它的作用就是"把集合展开成可变参数"。普通函数你用这个语法,编译器直接报错。

坑2:可变参数和类型擦除

// 这个会出问题
def print(args: String*): Unit = args.foreach(println)
def print(args: Int*): Unit = args.foreach(println)

// 编译报错——两个方法签名在擦除后一样

Java的可变参数也有这个问题——因为String*和Int*擦除后都是Object[],所以它们不能重载。

Scala也一样。这不是Scala的问题,是JVM类型擦除的问题。

坑3:可变参数方法的重写

class Base {
def print(args: String*): Unit = println("Base")
}

class Derived extends Base {
// 重写的时候,参数类型必须一致
override def print(args: String*): Unit = println("Derived")
}

重写可变参数方法的时候,参数类型必须完全一致——你不能在子类里改成Seq[String],也不能改成List[String]。必须和父类一模一样。


什么时候用可变参数?

不是所有函数都该用可变参数。判断标准很简单:

这个函数的参数个数是不是"不确定的"?

比如:

  • sum(nums: Int*) —— 求和,参数个数不确定 → 用
  • print(args: String*) —— 打印任意行,参数个数不确定 → 用
  • query(sql: String, params: Any*) —— SQL查询,参数个数随SQL变化 → 用

什么时候不用?

  • 参数个数固定的 → 不用
  • 参数类型不同的 → 不用(可变参数只能是同一种类型)
  • 参数需要命名的 → 不用(可变参数没有参数名)

简单说:当你需要"任意多个同类型参数"的时候,用可变参数。其他时候不用。


实战:SQL查询里的可变参数

这是大数据开发里最常见的场景——写一个SQL查询函数,参数个数随SQL变化:

def query(sql: String, params: Any*): List[Row] = {
// 用?占位,params填充
val pstmt = connection.prepareStatement(sql)
params.zipWithIndex.foreach { case (param, i) =>
pstmt.setObject(i + 1, param)
}
// 执行查询…
}

// 调用
query("SELECT * FROM users WHERE id = ?", 1)
query("SELECT * FROM users WHERE name = ? AND age > ?", "zhangsan", 20)
query("SELECT * FROM orders WHERE status = ? AND amount > ? AND created_at > ?", "PAID", 100, "2024-01-01")

SQL里有几个?,你就传几个参数——可变参数完美匹配这个场景。

Java里你怎么写?用数组?用List?都别扭——因为参数个数是随SQL变化的,你提前不知道要传几个。

Scala的可变参数就是为这种场景设计的。


写在最后

我写这篇文章的时候,特意没罗列可变参数有多少种用法——因为用法很简单,谁都能写。

真正值得花时间理解的是:Scala的可变参数为什么比Java好用?

不是因为它有:_*这个语法——而是因为它的底层是Seq,不是数组。

Java的可变参数是数组的语法糖——你传List进去,它不认,因为List不是数组。你想传?自己转。

Scala的可变参数是Seq的语法糖——你传任何序列进去,只要用:_*展开就行。不用转,不用强转,一行代码搞定。

**这就是函数式语言的设计哲学:可变参数不是"数组的便捷写法",它是"序列的便捷写法"。**你写的是可变参数,背后是整个集合库——你可以在上面做map、filter、fold,不用转来转去。

Java的可变参数是命令式思维的产物——它就是个数组,你想用集合操作?自己转Stream吧。

Scala的可变参数是函数式思维的产物——它就是个Seq,天生就是集合,天生就是函数式的。

可变参数不只是"传任意个参数"——它是函数式编程里"把集合当参数传"的优雅方式。

赞(0)
未经允许不得转载:171主机测评 » 深入理解Scala可变参数函数:从Java数组语法到优雅集合传递
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址