数据开发日常使用 spark 过程中,总免不了在 scala 和 java 之间进行类型转换,今天尝试一篇文章讲清楚,再次遇到不再懵逼
一、根本原因
在 Spark Scala 开发中,Scala 与 Java 集合的互操作是一个经常出现的痛点,经常写着写着发现:咦,怎么没有 XX 方法了?其实这是因为类型不一样导致的,而更深层的原因是由于 Spark 底层大量使用 Java 实现(如 Hadoop API、JDBC 连接器等),而业务层普遍采用 Scala API 开发,作为开发者,经常需要在两种集合类型间转换。而不熟悉相关细节,就总会在开发过程中频频出错,IDE 满眼飘红。例如,以下场景会导致方法调用失败:
scala
// 错误示例:尝试调用Scala集合方法
val javaList = new java.util.ArrayList[String]()
javaList.foreach(println) // 编译错误:Java集合没有foreach方法
Spark开发中频繁涉及Scala与Java集合转换,其核心原因源于两种语言在设计理念和生态定位上的差异,一般说来主要体现在以下三个层面:
1. 接口设计差异
一方面,Scala 与 Java 的集合 API 遵循了不同的编程范式,导致其支持的操作有较大的区别:
- Scala集合 :基于函数式编程设计,提供
map
、filter
、reduce
等高阶函数,支持链式调用和惰性求值(如view
)。 - Java集合 :则是以面向对象为基础,主要依赖迭代器(
Iterator
)和for
循环操作,函数式操作需通过Stream API(Java 8+)实现。
一个典型案例:
scala
// Scala开发者期望的操作方式
val processed = list.map(_ * 2).filter(_ > 10)
// 未转换的Java集合无法直接调用
val javaList = new ArrayList[Int](List(1,2,3).asJava)
javaList.map(_ * 2) // 编译错误:Java List无map方法
2. 类型系统与实现隔离
另一方面,Scala与Java集合在类型层面是完全独立的:
- 类型继承隔离 :Scala的
List
(scala.collection.immutable.List
)与Java的ArrayList
(java.util.ArrayList
)无任何继承关系。 - 泛型差异 :Scala支持协变/逆变(如
List[+A]
),而Java泛型通过通配符(? extends T
)实现类似功能,两者无法自动兼容。
又是一个典型的案例:
scala
// 试图将Scala List赋值给Java接口
def processJavaList(list: java.util.List[String]): Unit = {...}
val scalaList = List("a", "b", "c")
processJavaList(scalaList) // 类型不匹配,需显式转换
processJavaList(scalaList.asJava) // 正确写法
3. 跨语言生态兼容需求
最后是因为,Spark作为多语言兼容框架,底层依赖Java生态组件,而业务层常用Scala API,导致数据类型"双重身份":
- Java生态依赖 :Hadoop(HDFS)、JDBC连接器、Kafka客户端等均基于Java实现,返回
java.util.List
/Map
等类型。 - Scala业务逻辑:开发者更倾向使用Scala集合的函数式操作处理数据(因为链式调用行云流水,用起来更爽)。
典型场景:
scala
// 从HDFS读取数据返回Java对象
val hadoopRDD: RDD[(Text, IntWritable)] = ...
// 转换为Scala类型以便处理
hadoopRDD.map { case (text, intWritable) =>
(text.toString, intWritable.get) // 显式转换基础类型
}.collect().toList // 最终转为Scala List
二、转换机制
了解了问题产生的原因,接下来让我们看下如何通过转换机制来正确处理项目中的类型转换。
目前笔者常常见的业务中,常用的 spark 2.4 基于 Scala 2.11 ,类型转换需使用 JavaConverters
实现集合互操作。核心原理如下:
scala
// 必须显式导入隐式转换包
import scala.collection.JavaConverters._
// 转换示例(Java集合 ↔ Scala集合)
val javaList = new java.util.ArrayList[Int]()
val scalaBuffer = javaList.asScala // 转换为mutable.Buffer视图
val reconvertJavaList = scalaBuffer.asJava // 转回Java List视图
其方法和类型总结如下
方法 | 输入类型 | 输出类型 |
---|---|---|
.asScala |
java.util.Collection |
Scala集合视图 |
.asJava |
scala.collection |
Java集合视图 |
三、Spark 下的典型场景
场景 1:配置参数处理
读取 Java 实现的配置文件时返回 java.util.Properties
,需要转换为 Scala 集合来进行操作
scala
// 从Java配置工具获取参数
val props: java.util.Properties = ConfigLoader.load()
// 错误写法:直接操作Java集合
props.keySet().forEach(k => println(k)) // 需使用Java 8+语法
// 正确转换:转换为Scala集合
props.asScala.foreach { case (k, v) =>
println(s"配置项:$k = ${v.toString}")
}
场景 2:UDF 中的集合处理
处理 Java API 返回的集合数据时(例如需要大数据 ETL 需要和 web 系统交互时),则需要使用 Scala 集合方法
scala
// 定义UDF处理Java集合
spark.udf.register("array_sum", (arr: java.util.List[Integer]) => {
arr.asScala.map(_.toInt).sum // 必须转换才能使用sum方法
})
// SQL调用示例
spark.sql("SELECT array_sum(array(1,2,3))").show()
场景 3:DataFrame 行级操作
操作 Row 对象中的 Java 集合字段时需转换类型
scala
// 收集数据到本地
val rows: Array[Row] = df.collect()
// 关键处理逻辑
val processed = rows.map { row =>
val javaList = row.getAs[java.util.List[String]]("tags_field")
val cleaned = javaList.asScala.filter(_.nonEmpty).asJava // 转换核心
Row(row.get(0), cleaned) // 构建新Row
}
四、一些经验
1. 统一代码规范
• 入口/出口:在数据读取和写入阶段集中转换,这样可以避免频繁类型转换带来的性能损耗
scala
def readHBaseData(): scala.collection.Map[String, String] = {
val javaData = hbaseClient.getData().asScala
javaData.asScala.mapValues(_.toString)
}
• 中间处理:保持使用 Scala 集合进行业务逻辑操作,这样代码逻辑更加流畅
2. 转换的经验法则
最后,送上一个根据经验整理的转换的表格,大家在开发过程中可以根据实际场景来判断转换的方向和必要性。
场景类型 | 转换方向 | 典型代码模式 | 必要性说明 |
---|---|---|---|
Scala链式处理 | Java → Scala | javaData.asScala.map(...).filter(...).groupBy(...) |
必须转换,否则无法使用Scala集合的map/filter/reduce 等函数式方法 |
调用Java库/API | Scala → Java | scalaData.asJava.forEach(...) |
Java组件(如Kafka/JDBC)通常只接受java.util.List/Map 类型参数 |