目录
问题拆解:
解题步骤:
1. 拆分所有字符串为单词
2. 遍历所有单词并统计
3. 打印结果
基础版代码实现:
代码解释:
输出结果:
为什么这样设计?
继承的APP是个啥?
使用高阶函数式编程
第一步:理解基础操作
目标:
第二步:拆分字符串
第三步:展平列表(flatMap)
第四步:分组统计(groupBy)
第五步:统计每组数量
第六步:排序并打印
完整代码(针对原题)
关键概念解释
1. flatMap
2. groupBy
3. mapValues
执行结果
新手常见疑问
Q1:为什么要用 split(" +") 而不是 split(" ")?
Q2:identity 是什么?
Q3:sortBy(_._1) 中的 _._1 是什么意思?
调试技巧
问题拆解:
- 目标:统计每个单词出现的次数。
- 输入:一个包含多个字符串的列表,每个字符串中有多个单词。
- 输出:每个单词及其出现次数,例如
Hadoop: 2
。
解题步骤:
1. 拆分所有字符串为单词
- 每个字符串(如
"Hadoop Spark"
)需要拆分成独立的单词("Hadoop"
和"Spark"
)。 - 使用
split("\\s+")
方法按空格分割字符串(\\s+
表示匹配一个或多个空格)。
2. 遍历所有单词并统计
- 创建一个可变的
Map
来存储单词和对应的出现次数。 - 遍历每个单词:
- 如果单词已经在 Map 中,将次数 +1。
- 如果不在 Map 中,添加该单词并将次数设为 1。
3. 打印结果
- 遍历 Map 中的所有键值对,按格式输出。
基础版代码实现:
object WordCountBasic extends App {
val list1 = List(
"Hadoop Spark",
"Spark Scala",
"Scala Java",
"Scala Akka",
"Hadoop Java Scala"
)
// 步骤1:创建一个可变的Map来存储结果
val wordCount = scala.collection.mutable.HashMap[String, Int]()
// 步骤2:遍历每个字符串
for (sentence <- list1) {
// 拆分当前字符串为单词数组
val words = sentence.split("\\s+")
// 遍历每个单词
for (word <- words) {
// 如果Map中已有该单词,次数+1;否则添加单词,次数设为1
if (wordCount.contains(word)) {
wordCount(word) += 1
} else {
wordCount(word) = 1
}
}
}
// 步骤3:打印结果
for ((word, count) <- wordCount) {
println(s"$word: $count")
}
}
代码解释:
- 可变Map:用
mutable.HashMap
存储单词计数,方便修改值。 - 双层循环:
- 外层循环
for (sentence <- list1)
:遍历列表中的每个字符串。 - 内层循环
for (word <- words)
:遍历每个字符串拆分后的单词。
- 外层循环
- 统计逻辑:
if (wordCount.contains(word))
:检查单词是否已存在。wordCount(word) += 1
:存在则次数+1。wordCount(word) = 1
:不存在则初始化为1。
- 最终输出:遍历 Map 并打印每个键值对。
输出结果:
复制代码
Hadoop: 2
Spark: 2
Scala: 4
Java: 2
Akka: 1
为什么这样设计?
- 新手友好:用基础的
for
循环和if-else
代替高阶函数,逻辑更直观。 - 可变性:使用可变 Map 可以逐步更新状态,符合新手对“变量”的直觉。
- 分步拆解:明确的三步走(拆分、统计、打印),降低理解难度。
object WordCountBasic extends App
继承的APP是个啥?
在Scala中,App
是一个特质(trait)。
- 简化主方法
- 当一个Scala类扩展了
App
特质时,它就不需要显式地定义main
方法了。例如在你给出的WordCountBasic
类扩展了App
,就可以直接在类体中编写可执行的代码,就好像这些代码是写在main
方法内部一样。
- 当一个Scala类扩展了
- 执行入口
- 当运行这个类时,Scala运行时会查找这个类中的可执行代码(类似于查找传统的
main
方法)并执行它。这是一种更简洁的方式来编写Scala程序的入口点,相比于定义一个包含main
方法的类。
- 当运行这个类时,Scala运行时会查找这个类中的可执行代码(类似于查找传统的
当一个类扩展了 App
特质时,可以省略 def main(args: Array[String]): Unit = {}
这种传统的定义主方法的形式。
- 原理
- 当类扩展
App
时,Scala编译器会在背后生成一个合适的main
方法。这个生成的main
方法会执行类体中的代码,就好像这些代码是写在传统的main
方法内部一样。
- 当类扩展
- 示例对比
- 传统方式:
- 如果不使用
App
,你需要像这样定义一个类:
object WordCountTraditional { def main(args: Array[String]): Unit = { // 这里编写主逻辑,比如 println("Hello, world!") } }
- 如果不使用
- 使用
App
方式:object WordCountApp extends App { println("Hello, world!") }
- 在这两个示例中,
WordCountApp
以更简洁的方式实现了与WordCountTraditional
类似的功能,不需要显式地定义main
方法。
- 传统方式:
使用高阶函数式编程
object WordCount {
def main(args: Array[String]): Unit = {
val list1 = List(
"Hadoop Spark",
"Spark Scala",
"Scala Java",
"Scala Akka",
"Hadoop Java Scala"
)
val wordCounts = list1
.flatMap(_.split(" +")) // 步骤1+2:拆分并展平
.groupBy(identity) // 步骤3:按单词分组
.mapValues(_.size) // 步骤3:统计每组的数量
.toList // 转换为列表
.sortBy(_._1) // 步骤4:按单词排序
wordCounts.foreach { case (word, count) =>
println(s"$word: $count") // 步骤4:打印结果
}
}
}
第一步:理解基础操作
假设我们有一个简单的列表:
val simpleList = List("Hello World", "Hello Scala")
目标:
统计每个单词出现的次数,结果应该是:
复制代码
Hello → 2
World → 1
Scala → 1
第二步:拆分字符串
每个句子需要拆分成单词:
// 拆分第一个元素 "Hello World" → Array("Hello", "World")
val split1 = simpleList(0).split(" ") // 按空格拆分
println(split1.mkString(", ")) // 输出: Hello, World
问题:如果直接对整个列表用 map
,会得到嵌套结构:
val splitAll = simpleList.map(_.split(" "))
// 结果:List(Array("Hello", "World"), Array("Hello", "Scala"))
第三步:展平列表(flatMap
)
用 flatMap
把嵌套的数组变成“平”的列表:
val allWords = simpleList.flatMap(_.split(" "))
// 结果:List("Hello", "World", "Hello", "Scala")
为什么用 flatMap
?
map
的结果:List(Array(...), Array(...))
(两层结构)flatMap
的结果:List("Hello", "World", "Hello", "Scala")
(一层结构)
第四步:分组统计(groupBy
)
把相同的单词分到同一组:
val groups = allWords.groupBy(word => word)
// 结果:
// Map(
// "Hello" -> List("Hello", "Hello"),
// "World" -> List("World"),
// "Scala" -> List("Scala")
// )
解释:
groupBy(word => word)
表示按单词本身分组word => word
可以简写为identity
(等价函数)
第五步:统计每组数量
对每个分组计算元素个数:
val counts = groups.map { case (word, list) =>
(word, list.size)
}
// 结果:Map("Hello" -> 2, "World" -> 1, "Scala" -> 1)
第六步:排序并打印
将结果按字母顺序排序:
val sorted = counts.toList.sortBy(_._1) // 按单词排序
sorted.foreach { case (word, count) =>
println(s"$word: $count")
}
完整代码(针对原题)
现在将上述步骤应用到原题的数据:
scala复制代码
object WordCount {
def main(args: Array[String]): Unit = {
val list1 = List(
"Hadoop Spark",
"Spark Scala",
"Scala Java",
"Scala Akka",
"Hadoop Java Scala"
)
// 步骤1+2:拆分并展平所有单词
val allWords = list1.flatMap(_.split(" +")) // " +" 表示1个或多个空格
// 步骤3:分组
val groups = allWords.groupBy(identity) // 按单词分组
// 步骤4:统计数量
val counts = groups.mapValues(_.size) // 计算每组的长度
// 步骤5:排序并打印
counts.toList // 转换为List
.sortBy(_._1) // 按单词排序
.foreach { case (word, count) =>
println(s"$word: $count")
}
}
}
关键概念解释
1. flatMap
- 作用:先做
map
(转换),再flatten
(展平) - 示例:
List("a b", "c").flatMap(_.split(" ")) → List("a", "b", "c")
2. groupBy
- 作用:按规则分组,返回
Map[Key, List[Value]]
- 示例:
List("a", "b", "a").groupBy(identity) → Map("a" -> List("a", "a"), "b" -> List("b"))
3. mapValues
- 作用:对
Map
中的值做转换,保留键不变 - 示例:
Map("a" -> List(1,2)).mapValues(_.size) → Map("a" -> 2)
执行结果
运行代码后输出:
Akka: 1
Hadoop: 2
Java: 2
Scala: 4
Spark: 2
新手常见疑问
Q1:为什么要用 split(" +")
而不是 split(" ")
?
split(" ")
会把连续空格拆分成空字符串,例如"a b"
→Array("a", "", "b")
split(" +")
中的+
表示匹配一个或多个空格,能正确处理连续空格
Q2:identity
是什么?
identity
是一个预定义的函数,等价于x => x
- 例如:
groupBy(identity)
和groupBy(x => x)
完全一样
Q3:sortBy(_._1)
中的 _._1
是什么意思?
_
表示元组,例如("Scala", 4)
_._1
表示取元组的第一个元素(即单词),_._2
是第二个元素(即计数)
调试技巧
如果中间步骤不理解,可以插入 println
查看数据:
val allWords = list1.flatMap(_.split(" +"))
println(s"拆分后的单词:$allWords") // 查看展平后的结果