零基础起步:第一次写 Scala

6 个关卡,每一步都写清「做什么 / 敲什么 / 你应该看到 / 报错怎么办」。全程约 50 分钟。

6 关20 个微步骤16 条报错速查

第 0 关 · 装 JDK 11,看见 java 和 javac 的版本号(8 分钟)

目标知道 JDK 是编译和运行 Scala/Java 程序的底座,装好 JDK 11 后能在命令行同时看到 java 和 javac 的版本号。
前提:一台 Windows 10/11 电脑,能联网下载约 200 MB 的 JDK;不需要提前装过 Java,也不要求会编程。

1① 先检查电脑里有没有 JDK

🎯 按 Win+R,输入 cmd 后回车;在弹出的黑窗口里逐行输入下面两条命令。如果两条都能打印版本号,就直接跳到第 ③ 步;提示“不是内部或外部命令”就继续第 ② 步安装。

SCALA
java -version
javac -version
👀 你应该看到
# 已经装过 JDK 时,会看到类似输出:
openjdk version "11.0.22" 2024-01-16
OpenJDK Runtime Environment Temurin-11.0.22+7 (build 11.0.22+7)
OpenJDK 64-Bit Server VM Temurin-11.0.22+7 (build 11.0.22+7, mixed mode)
javac 11.0.22

# 没有装过 JDK 时,会看到:
'java' 不是内部或外部命令,也不是可运行的程序
或批处理文件。

重点看 java 和 javac 是否都有版本号。两条都有就跳到第 ③ 步;缺哪条都说明需要装 JDK,装完要关掉旧窗口再开新的。

⚠️ 如果报错
  • java 有版本,javac 提示找不到 → 只装了 JRE 运行环境,没有装 JDK 编译器;重新安装 JDK(不是 JRE),装完关闭并重开 cmd,再执行 javac -version
  • java 不是内部或外部命令 → 电脑没装 JDK,或 JDK 的 bin 目录不在 PATH 里;按第 ② 步安装 JDK 11;安装完成后必须关闭旧 cmd,再打开一个新的

2② 安装 JDK 11(Temurin 发行版)

🎯 优先用 winget 安装;如果电脑没有 winget,就打开 adoptium.net,选择 Temurin 11、Windows x64、.msi,下载后双击安装,确认勾选 Add to PATH,其余一路默认。装完关闭旧终端。

SCALA
winget install --id EclipseAdoptium.Temurin.11.JDK -e --accept-source-agreements --accept-package-agreements
👀 你应该看到
Found Eclipse Adoptium Temurin 11 [EclipseAdoptium.Temurin.11.JDK]
Downloading https://...
Successfully verified installer hash
Starting package install...
Successfully installed

看到最后一行 Successfully installed 就表示安装结束。下载大小、版本号和安装路径会随时间变化;装完必须重开命令行,旧窗口不会自动刷新环境变量。

⚠️ 如果报错
  • winget 不是内部或外部命令 → Windows 没有应用安装程序,或应用安装程序版本太旧;改用 adoptium.net 官网的 .msi 安装包,图形界面安装;或先在 Microsoft Store 更新“应用安装程序”
  • 安装到一半提示需要管理员权限 → 安装到 Program Files 需要提升权限;右键开始菜单,打开“终端(管理员)”,再执行 winget;或直接双击官网 msi,在弹窗里点“是”
  • 安装完 java -version 还是旧版本 → PATH 里旧 JDK 的顺序更靠前,且当前窗口没有刷新;关闭所有 cmd 后重新打开;仍旧就调整 Path 顺序,或卸载不用的旧 Java

3③ 重新开窗口,确认 java 与 javac 都能用

🎯 关闭安装前的 cmd,重新打开一个;执行下面三条命令。三条都要成功,缺一不可。

SCALA
java -version
javac -version
where java
👀 你应该看到
openjdk version "11.0.22" 2024-01-16
OpenJDK Runtime Environment Temurin-11.0.22+7 (build 11.0.22+7)
OpenJDK 64-Bit Server VM Temurin-11.0.22+7 (build 11.0.22+7, mixed mode)
javac 11.0.22
C:\Program Files\Eclipse Adoptium\jdk-11.0.22.7-hotspot\bin\java.exe

你的版本号、日期和路径可能不同;只要 java 与 javac 都能打印版本,且 where java 指向同一个 JDK 的 bin 目录,这一关就算过。本课示例用 JDK 11,版本号不同不影响后续,关键是别只装 JRE。

⚠️ 如果报错
  • javac 还是不是内部或外部命令 → 装的是 JRE,或新 JDK 的 bin 没有进入 Path;确认安装包名称里带 JDK;把 JDK 根目录下的 bin 加入 Path,重开 cmd 后再验证
  • where java 第一条指向 Windows 系统目录或旧 Java → PATH 里还有别的 Java 启动器,且顺序更靠前;把新 JDK 的 bin 移到 Path 最前面;或卸载旧 Java 后重开终端
  • java -version 显示 1.8,但我想用 11 → 电脑上存在两个 JDK,PATH 先找到 1.8;Scala 2.13 和 sbt 用 JDK 8 也能运行;要统一版本就调整 Path 顺序或卸载旧版
✅ 过关标准新开一个 cmd 窗口后,java -version 和 javac -version 都能打印出版本号;where java 指向你刚安装的 JDK 目录,而不是系统自带的旧 Java。

第 1 关 · 装 sbt,第一次进 Scala REPL(9 分钟)

目标装好 sbt,会用 sbt --version 检查安装,会用 sbt console 进入 Scala 交互环境并安全退出。
前提:第 0 关的 JDK 已经可用;电脑能访问 Maven 中央仓库;第一次启动 sbt 需要下载依赖,慢一点是正常的。

1① 安装 sbt

🎯 优先用 winget 安装并固定为 1.10.7,避免装到 2.x 后和本课示例不一致;如果电脑没有 winget,就打开 sbt 官网下载页,找 1.10.7 的 .msi 安装包,默认下一步安装。装完关闭所有旧终端。

SCALA
winget install --id sbt.sbt --version 1.10.7 -e --accept-source-agreements --accept-package-agreements
👀 你应该看到
Found sbt [sbt.sbt]
Downloading https://...
Successfully installed

看到 Successfully installed 表示安装完成。旧命令行里的 PATH 不会自动更新,必须重新开一个 cmd 再验证。

⚠️ 如果报错
  • sbt 安装成功但 sbt --version 找不到命令 → PATH 没有刷新,或安装器没有把 sbt 的 bin 目录写进 PATH;关掉所有 cmd 后重新打开;仍不行就用 where sbt 查找,或手动把 sbt 的 bin 加入 Path
  • winget 源更新失败,无法安装 → winget 源或网络异常;先执行 winget source update;仍失败就改用 sbt 官网的 msi 安装包

2② 用 sbt --version 验证安装

🎯 新开一个 cmd,执行 sbt --version。第一次会下载 sbt 启动器和依赖,可能等 1 到 3 分钟;看到两行版本号再继续。

SCALA
sbt --version
👀 你应该看到
sbt version in this project: 1.10.7
sbt script version: 1.10.7

两行版本号不必与示例完全一样,只要都能打印出来即可。第一次运行下载慢是正常现象,不要反复强制关闭窗口。

⚠️ 如果报错
  • sbt 卡在 downloading 或 resolving → 第一次要访问 Maven Central 下载依赖,网络慢或访问受阻;耐心等待几分钟;换网络或配置代理;长期慢可在 sbt 配置里改用国内镜像
  • sbt: JAVA_HOME is not set → sbt 启动脚本找不到 JDK 根目录;把 JAVA_HOME 设置成 JDK 的根目录(不是 bin 目录),然后重开 cmd
  • sbt 报 UnsupportedClassVersionError → 当前 java 版本太低,不满足 sbt 的要求;用 java -version 确认版本,至少换成 JDK 8,推荐 JDK 11;调整 Path 后再试

3③ 用 build.sbt 固定 2.13,进入 REPL

🎯 新建一个只有 build.sbt 的目录,让 sbt 使用 Scala 2.13.14,再执行 sbt console。等出现 scala> 后,依次输入三条 Scala 语句;注意 :quit 前面有一个英文冒号。

SCALA
mkdir C:\scala-course\repl-demo
cd /d C:\scala-course\repl-demo
notepad build.sbt

# build.sbt 只写一行:
scalaVersion := "2.13.14"

# 保存并关闭记事本后执行:
sbt console
1 + 2
println("hi")
:quit
👀 你应该看到
[info] Starting scala interpreter...
Welcome to Scala 2.13.14 (OpenJDK 64-Bit Server VM, Java 11.0.22).
Type in expressions for evaluation. Or try :help.

scala> 1 + 2
res0: Int = 3

scala> println("hi")
hi

scala> :quit

res0 是 REPL 自动起的变量名,Int 是结果类型,3 才是值;println 只打印 hi,不会产生 res 行。build.sbt 里那一行保证 sbt 用 2.13.14,而不是它自带的 Scala 2.12。

⚠️ 如果报错
  • error: not found: value sbt → 在 REPL 里又输入了 sbt;scala> 后面只能写 Scala 表达式;输入 :quit 回到系统命令行,再执行 sbt console 或 sbt run
  • Welcome to Scala 2.12.x → 当前目录没有 build.sbt,sbt 退回了自己运行时的 Scala 2.12;在 repl-demo 目录里创建 build.sbt,写入 scalaVersion := "2.13.14",再启动 sbt console
  • value + is not a member of ... → 误用了中文加号、全角空格或别的符号;切换英文输入法,重新输入英文的 1 + 2
  • console 一直停在 downloading → 首次下载 Scala 2.13 编译器,网络较慢;等待几分钟;超过 5 分钟换网络或配置镜像;已下载过的依赖会缓存
✅ 过关标准sbt --version 能打印版本号;sbt console 出现 scala> 提示符;在 REPL 里能算出 1 + 2 = 3,能打印 hi,并用 :quit 退出。

第 2 关 · 写第一个 Scala 程序 Hello.scala(9 分钟)

目标能写一个 object Hello extends App 程序,用 scalac 编译、scala 运行,屏幕上出现 Hello, Scala!。
前提:第 0 关的 JDK 正常,第 1 关的 sbt 已可用;能新建文件夹并用记事本保存 UTF-8 文本文件。

1① 装 Scala 2.13 命令行,验证 scalac 与 scala

🎯 sbt 不会把 scalac/scala 直接装进 PATH,所以再装一个 Scala 2.13.14 发行包:官网下载 zip,解压到 C:\scala。在开始菜单搜索“编辑系统环境变量”,在用户变量 Path 中新建一行 C:\scala\scala-2.13.14\bin;再在同一窗口新建或修改 JAVA_HOME,值填 JDK 根目录(where java 的结果去掉 \bin\java.exe)。关闭所有 cmd 后重新打开,执行下面两条命令。

SCALA
scalac -version
scala -version
👀 你应该看到
Scala compiler version 2.13.14 -- Copyright 2002-2024, LAMP/EPFL and Lightbend, Inc.
Scala code runner version 2.13.14 -- Copyright 2002-2024, LAMP/EPFL and Lightbend, Inc.

两个命令都出现 2.13.14 就对了。单独装 Scala 发行包是为了直接练 scalac/scala;第 3 关的工程仍然交给 sbt 管理。

⚠️ 如果报错
  • scala 或 scalac 不是内部或外部命令 → Scala 的 bin 目录没有加入 Path,或加完没有重新开 cmd;检查用户 Path 里是否有 Scala 安装目录下的 bin;关闭旧终端再试
  • 系统找不到指定的路径 → 解压后的目录名与 Path 里写的路径不完全一致;在资源管理器进入解压目录,从地址栏复制真实路径,再修正 Path
  • JAVA_HOME is not set 或 Error: could not find java → Scala 启动脚本依赖 JAVA_HOME 找 JDK;把 JAVA_HOME 指向 JDK 根目录并重开 cmd;根目录不是 JDK 的 bin 子目录

2② 新建 Hello.scala,写入第一个程序

🎯 在 cmd 里创建练习目录(如果前面已建过,mkdir 提示“已经存在”可以忽略),打开记事本;把下面代码原样粘进去,另存为 Hello.scala,编码选择 UTF-8。

SCALA
mkdir C:\scala-course
cd /d C:\scala-course
notepad Hello.scala

# 在记事本中写入:
object Hello extends App {
  println("Hello, Scala!")
}
👀 你应该看到
记事本打开一个空白文件;保存后,C:\scala-course 目录里出现 Hello.scala。

文件名必须正好是 Hello.scala;记事本默认会补 .txt,保存时把“保存类型”改成“所有文件”,并确认编码是 UTF-8。

⚠️ 如果报错
  • 文件实际上变成了 Hello.scala.txt → 记事本自动补了 .txt 扩展名;另存为时文件名写 "Hello.scala"(带英文引号),保存类型选“所有文件”
  • 文件里有中文就乱码 → 保存编码不是 UTF-8;保存对话框的编码选 UTF-8;本程序全是英文,但以后写中文一定要统一
  • 代码被挤成一行 → 在记事本里按的是自动换行显示,没有真的按回车;每一行末尾按一次回车;花括号要各占一行

3③ 编译并运行 Hello

🎯 确认当前目录还是 C:\scala-course,先编译再运行。scalac 没有输出通常就是成功;scala 才会真正执行程序。

SCALA
scalac Hello.scala
scala Hello
👀 你应该看到
Hello, Scala!

编译会生成 Hello.class 等文件;只有第二行 scala Hello 会打印结果。类名 Hello 必须和 object 名 Hello 完全一致。

⚠️ 如果报错
  • scalac: 找不到文件: Hello.scala → 当前目录不对,或文件名不是 Hello.scala;执行 cd /d C:\scala-course,再用 dir 确认文件;注意扩展名不能是 .txt
  • java.lang.ClassNotFoundException: Hello → 运行目录里没有 Hello.class,或编译其实失败了;先看 scalac 是否有报错;成功后再在同一目录执行 scala Hello
  • object Hello is already defined → 同一个目录里有重复的 Hello 定义,或文件里写了两遍;删掉重复定义或多余的 .scala 文件,只保留一个 object Hello
  • 打印的是 Hello, World! 不是 Hello, Scala! → 代码没有按示例改完整;逐字核对 println 里的字符串,注意英文逗号和英文感叹号
✅ 过关标准能说清 scalac 负责编译、scala 负责运行;Hello.scala 能编译出 class 文件;运行后只打印 Hello, Scala!。

第 3 关 · 用 sbt 建一个真正的工程(10 分钟)

目标会建 sbt 标准目录结构,知道 build.sbt 管构建配置、src/main/scala 放源码,并用 sbt run 运行和增量编译。
前提:sbt 已经在 PATH 中;会用记事本新建文件;第一次 sbt run 可能下载 Scala 编译器,请留出几分钟。

1① 建标准 sbt 工程目录

🎯 在 cmd 里一次创建工程根目录和源码目录,再进入工程根目录。sbt 只自动识别 src/main/scala 这个标准结构。

SCALA
mkdir C:\scala-course\sbt-demo\src\main\scala
cd /d C:\scala-course\sbt-demo
👀 你应该看到
没有输出,提示符变成 C:\scala-course\sbt-demo>;执行 dir 能看到 src 文件夹。

mkdir 成功时不会显示 OK。如果提示“子目录或文件已经存在”,说明之前建过,可以忽略,继续下一步。

⚠️ 如果报错
  • 拒绝访问 → 在系统目录或没有写权限的位置建工程;换到用户目录或 D 盘的普通文件夹,例如 D:\scala-course
  • 系统找不到指定的路径 → 命令中的盘符或目录拼写错误;改成你电脑真实存在的盘符;一条一条执行,先 mkdir 成功再 cd
  • cd 之后提示符没变 → 只写了 cd C:\...,没有加 /d 切换盘符;用 cd /d 加完整路径;或者先切到目标盘符再 cd

2② 写 build.sbt,只放构建配置

🎯 在工程根目录打开记事本,新建 build.sbt,写入 Scala 版本。不要把 build.sbt 放进 src 里。

SCALA
notepad build.sbt

# build.sbt 的内容只有一行:
scalaVersion := "2.13.14"
👀 你应该看到
记事本标题显示 build.sbt;保存后工程根目录有 build.sbt,和 src 文件夹并排。

sbt 用 := 赋值,版本号必须带英文双引号;这是构建配置,不是 Scala 程序,不要在里面写 object。

⚠️ 如果报错
  • build.sbt 报 expected eof 或语法错误 → 把 Scala 代码或中文说明写进了 build.sbt;build.sbt 只保留 scalaVersion := "2.13.14";注释用 //
  • 文件名变成 build.sbt.txt → 记事本自动补了 .txt;另存为时文件名加英文双引号,保存类型选“所有文件”
  • sbt 还是使用了别的 Scala 版本 → build.sbt 不在 sbt 启动目录,或文件名拼错;先 cd 到有 build.sbt 的目录再启动 sbt;确认文件名没有多余前后缀

3③ 把 Hello.scala 放到 src/main/scala

🎯 确认当前在 sbt-demo 根目录,打开源码文件,粘贴程序并保存。object 名和文件名都叫 Hello。

SCALA
notepad src\main\scala\Hello.scala

# 在记事本中写入:
object Hello extends App {
  println("Hello from sbt!")
}
👀 你应该看到
notepad 打开 src/main/scala 下的 Hello.scala;保存后完整路径是 sbt-demo/src/main/scala/Hello.scala。

sbt 默认只编译 src/main/scala 下的 .scala 文件。目录层级少一层或多一层,sbt run 都可能找不到程序。

⚠️ 如果报错
  • 文件放好了但 sbt 看不到 → 目录名拼错,例如 source、main/scala 大小写不一致,或放进了 project 子目录;逐级确认路径是 src/main/scala;Scala 和 sbt 的目录名区分大小写,按示例写
  • object 名和文件名不一致 → 编译或运行时找不到 Hello 类;文件名写 Hello.scala,object 名写 Hello,两者大小写保持一致
  • 中文注释保存后乱码 → 记事本保存成了 ANSI/GBK;另存为时编码选择 UTF-8;sbt 默认按 UTF-8 读源码

4④ sbt run,并改一句再跑一次

🎯 在工程根目录执行 sbt run。第一次会下载 Scala 编译器;看到 Hello from sbt! 后,把字符串改成 你好,sbt!并保存,再执行一次 sbt run。

SCALA
sbt run
👀 你应该看到
[info] welcome to sbt 1.10.7 (Eclipse Adoptium Java 11.0.22)
[info] loading settings for project sbt-demo from build.sbt...
[info] compiling 1 Scala source to C:\scala-course\sbt-demo\target\scala-2.13\classes ...
[info] running Hello
Hello from sbt!
[success] Total time: 4 s, completed ...

版本、路径和耗时会不同。关键是有 running Hello、Hello from sbt! 和 success;改完再跑会先重新 compile,然后打印中文。

⚠️ 如果报错
  • sbt 一直 downloading → 首次下载依赖,或仓库网络超时;等待并重试;换网络、配置代理或国内镜像;已下载的依赖会缓存
  • [error] (Compile / run) No main class detected. → sbt 没在 src/main/scala 找到继承 App 的 object,或 object 名与文件名不一致;确认 src/main/scala/Hello.scala 里有 object Hello extends App;执行 sbt clean compile,先看清编译错误
  • sbt run 没有任何输出就结束 → 程序没有执行,或 object 既没有继承 App 也没有 main 方法;确认 object Hello extends App 且包含 println;保存后重新执行 sbt run
  • 中文变成问号 → 源文件不是 UTF-8,或控制台代码页不对;源码另存为 UTF-8;在 cmd 执行 chcp 65001 后重开 sbt run
✅ 过关标准工程根目录有 build.sbt,源码在 src/main/scala/Hello.scala;sbt run 打印 Hello from sbt!;改一行字符串后再跑一次,能看到重新编译和新结果。

第 4 关 · 在 REPL 里练 val、插值与 List(7 分钟)

目标能用 REPL 做小实验:区分 val 与 var,用 s"" 和 f"" 拼字符串,用 map/filter 处理 List。
前提:第 2 关的 scala 命令可用;知道怎么在命令行里输入多行表达式并按回车。

1① 进 REPL,分清 val 和 var

🎯 在 cmd 里输入 scala 进入 REPL。逐行输入下面内容;注意所有标点都是英文符号。

SCALA
scala
val course = "Scala 程序设计"
var week = 1
week = 2
week += 1
:quit
👀 你应该看到
Welcome to Scala 2.13.14 (OpenJDK 64-Bit Server VM, Java 11.0.22).
Type in expressions for evaluation. Or try :help.

scala> val course = "Scala 程序设计"
course: String = Scala 程序设计

scala> var week = 1
week: Int = 1

scala> week = 2
week: Int = 2

scala> week += 1
week: Int = 3

scala> :quit

val 是只读绑定,赋值一次后不能改;var 可以重新赋值,也能用 += 自增。Scala 的习惯是优先 val,确实要变才用 var。

⚠️ 如果报错
  • reassignment to val → 对 val 又赋了一次值;需要变化就改成 var;如果只是算新值,就用新的 val,不要改旧变量
  • not found: value course → 变量名拼错、大小写不一致,或已经退出当前 REPL;Scala 区分大小写;保持同一个 REPL 会话,先定义再使用
  • value += is not a member of Int → += 只能用在你声明为 var 的变量上,而且它本质是先读后写;把 var week 声明好;如果是 val,改成 var 或写 val next = week + 1

2② 字符串插值 s"" 与 f""

🎯 重新进入 scala(如果刚才已退出),定义姓名和分数,然后依次测试 s 插值、表达式插值和 f 格式化。

SCALA
scala
val name = "小明"
val score = 92.5
s"$name 的成绩是 $score 分"
s"1 + 2 = ${1 + 2}"
f"$score%.1f"
:quit
👀 你应该看到
Welcome to Scala 2.13.14 (OpenJDK 64-Bit Server VM, Java 11.0.22).
Type in expressions for evaluation. Or try :help.

scala> val name = "小明"
name: String = 小明

scala> val score = 92.5
score: Double = 92.5

scala> s"$name 的成绩是 $score 分"
res0: String = 小明 的成绩是 92.5 分

scala> s"1 + 2 = ${1 + 2}"
res1: String = 1 + 2 = 3

scala> f"$score%.1f"
res2: String = 92.5

scala> :quit

s 前缀是最常用的插值;${} 里可以放表达式;f 前缀在变量后面加格式符,%.1f 表示保留 1 位小数。

⚠️ 如果报错
  • 输出的是 $name,不是 小明 → 字符串前少了 s 前缀,$name 被当成了普通字符;改成 s"$name 的成绩是 $score 分";插值前缀要紧贴左引号,中间不能有空格
  • unknown format conversion → f 插值的格式符写错,或变量类型与格式不匹配;Double 用 %.2f,整数用 %d;先去掉格式符,用 s 插值确认值是否正确
  • not found: value score → 退出 REPL 后变量就没了,或拼写不一致;REPL 变量只在当前会话有效;重新进入后要再定义一次

3③ List 的 map 与 filter

🎯 再进一次 REPL,先定义一个整数 List,再分别测试 map、filter 和链式调用。每输入一行就按一次回车。

SCALA
scala
val nums = List(1, 2, 3, 4, 5, 6)
nums.map(_ * 2)
nums.filter(_ % 2 == 0)
nums.map(x => x * x).filter(_ > 10)
:quit
👀 你应该看到
Welcome to Scala 2.13.14 (OpenJDK 64-Bit Server VM, Java 11.0.22).
Type in expressions for evaluation. Or try :help.

scala> val nums = List(1, 2, 3, 4, 5, 6)
nums: List[Int] = List(1, 2, 3, 4, 5, 6)

scala> nums.map(_ * 2)
res0: List[Int] = List(2, 4, 6, 8, 10, 12)

scala> nums.filter(_ % 2 == 0)
res1: List[Int] = List(2, 4, 6)

scala> nums.map(x => x * x).filter(_ > 10)
res2: List[Int] = List(16, 25, 36)

scala> :quit

map 对每个元素做变换并保持个数,filter 只保留满足条件的元素;下划线是“每个元素”的简写,遇到推不出类型时就写完整参数。

⚠️ 如果报错
  • missing parameter type → 匿名函数的下划线没有上下文,或外层表达式太复杂;写成 nums.map((x: Int) => x * 2),或先声明 val f: Int => Int = _ * 2
  • value % is not a member of ... → List 元素不是数字,或括号写错导致 _ 指错对象;先用 nums 检查类型;筛选用 nums.filter(n => n % 2 == 0),把条件写完整
  • type mismatch; found: List[Int], required: ... → map/filter 的返回类型与下一步操作不匹配;在 REPL 一行一行执行,看每行 res 的类型;不要一次粘贴多行后猜测错误位置
✅ 过关标准能解释 val 和 var 的区别;能写出一个 s 插值和一个 f 插值;能用 map 与 filter 得到预期列表。

第 5 关 · 读 CSV,按班级分组统计(8 分钟)

目标能用 scala.io.Source 读入 CSV 文本,跳过表头、按列分组,算人数与平均分并打印;改路径后能再跑一遍。
前提:第 2 关的 scalac/scala 可用;会用记事本保存 UTF-8 文件;准备与本关示例一致的 scores.csv。

1① 准备 scores.csv

🎯 在 cmd 里进入 C:\scala-course,用记事本新建 scores.csv,写入表头和四行数据,保存编码选 UTF-8。

SCALA
cd /d C:\scala-course
notepad scores.csv

# 在记事本中写入:
name,cls,score
小明,一班,92
小红,一班,85
小刚,二班,78
小美,二班,96
👀 你应该看到
记事本打开 scores.csv;保存后,文件出现在 C:\scala-course 目录,用记事本重新打开能看到完整的 5 行数据。

第一行是表头,程序会跳过;后面四行每行三列。逗号必须是英文逗号,文件必须是 UTF-8,中文才不容易乱码。

⚠️ 如果报错
  • 文件名变成 scores.csv.txt → 记事本自动补了 .txt 后缀;另存为时文件名写 "scores.csv",保存类型选“所有文件”;代码里的文件名也要和它一致
  • 中文显示成问号 → 文件保存成了 ANSI/GBK,而程序按 UTF-8 读取;在记事本另存为时选择 UTF-8;或把程序里的编码改成与文件实际编码一致
  • 数据行只写了一列 → 用了中文逗号,或漏了逗号;每行写成 姓名,班级,分数 三部分,分隔符用英文逗号

2② 写 ScoreReport.scala

🎯 在同一个目录新建 ScoreReport.scala,粘贴下面完整代码并保存。先不用改,后面会运行它。

SCALA
notepad ScoreReport.scala

# 将下面代码完整粘入并保存:
import scala.io.Source

object ScoreReport extends App {
  val path = "scores.csv"
  val src = Source.fromFile(path, "UTF-8")
  val lines = src.getLines().toList
  src.close()

  val rows = lines.drop(1).filter(_.trim.nonEmpty).map { line =>
    val parts = line.split(",").map(_.trim)
    (parts(0), parts(1), parts(2).toInt)
  }

  val byClass = rows.groupBy(_._2)
  byClass.toSeq.sortBy(_._1).foreach { case (cls, items) =>
    val scores = items.map(_._3)
    val avg = scores.sum.toDouble / scores.size
    println(f"$cls:${items.size} 人,平均分 $avg%.1f,最高 ${scores.max} 分")
  }
}
👀 你应该看到
notepad 打开 ScoreReport.scala;保存后,文件与 scores.csv 位于同一个目录。

代码用 Source.fromFile 的第二个参数指定 UTF-8;getLines().toList 读完立即 close。drop(1) 跳过表头,groupBy 按第二列班级分组。

⚠️ 如果报错
  • not found: value Source → 忘了第一行 import scala.io.Source;把 import 放在文件第一行,和 object 的缩进无关
  • not found: value line / parts → 花括号不配对,map 的代码块没有包住;在编辑器里检查括号;map 后面用 { line => ... } 包住三行逻辑
  • 非法字符或 unexpected character → 用了中文引号、中文逗号,或从网页复制了不可见字符;重新手敲关键符号;字符串和参数分隔都用英文符号

3③ 编译并运行 ScoreReport

🎯 确认命令行在 C:\scala-course,且 scores.csv 和 ScoreReport.scala 都在当前目录;先编译,再运行。

SCALA
scalac ScoreReport.scala
scala ScoreReport
👀 你应该看到
一班:2 人,平均分 88.5,最高 92 分
二班:2 人,平均分 87.0,最高 96 分

两个班级各 2 人;一班平均 (92+85)/2=88.5,二班平均 (78+96)/2=87.0。只要数值和格式一致,就说明分组统计跑通了。

⚠️ 如果报错
  • java.io.FileNotFoundException: scores.csv → 当前目录没有 scores.csv,或文件名拼写不一致;在 cmd 里执行 dir 确认;先 cd 到 CSV 和源码所在目录,或把 path 改成绝对路径
  • MalformedInputException 或中文乱码 → CSV 的编码与 Source 指定的 UTF-8 不一致;把 CSV 另存为 UTF-8;确认没有混入其他编码的字符
  • NumberFormatException → 分数列有空格、空行、表头没跳过或列数不对;保留 drop(1) 跳过表头;检查每行 3 列;先打印原始行定位问题
  • ArrayIndexOutOfBoundsException → 某行列数不足,parts(2) 越界;打印原始行并检查逗号;过滤空行,确认每行都有三列

4④ 改路径,再跑第二遍

🎯 复制一份数据文件,把代码里的 path 改成新文件名并保存,然后重新编译运行,验证程序不是写死在一份文件上。

SCALA
copy scores.csv scores2.csv
notepad ScoreReport.scala
# 把 val path = "scores.csv" 改成:
# val path = "scores2.csv"
# 保存后再执行:
scalac ScoreReport.scala
scala ScoreReport
👀 你应该看到
一班:2 人,平均分 88.5,最高 92 分
二班:2 人,平均分 87.0,最高 96 分

输出应该和第二遍之前完全一样。改路径后仍能跑,说明你理解了“相对路径是相对当前命令行目录”的这一层。

⚠️ 如果报错
  • 复制后运行还是找不到文件 → scores2.csv 没复制成功,或 path 仍然写着 scores.csv;执行 dir 看文件名;把 path 改成带引号的 scores2.csv,保存后再编译
  • 改了文件但运行结果没变 → 改完没有重新 scalac 编译,运行的是旧 class;每次改源码后都重新执行 scalac ScoreReport.scala,再执行 scala ScoreReport
✅ 过关标准scores.csv 能被程序读取;输出两个班级的人数与平均分;把文件名改成 scores2.csv 并同步改代码路径后,程序仍能正确输出。

常见报错速查 Ctrl+F 搜关键字

报错原因解决
javac: 找不到或无法加载主类 Hello把编译命令和运行命令混用了:javac 只负责编译,java/ scala 才负责运行;也可能是类名、object 名与文件名不一致,或当前目录不对Scala 程序用 scalac Hello.scala 编译、scala Hello 运行;Java 程序用 javac Hello.java、java Hello。先 cd 到文件所在目录,并让 object/class 名与文件名一致。
not found: value sbt在 Scala 代码或 REPL 里把 sbt 当成了 Scala 变量/命令;sbt 是终端命令,不是 Scala 库里的值输入 :quit 回到系统命令行,再执行 sbt run 或 sbt console;不要在 scala> 提示符后面敲 sbt。
value ++ is not a member of IntScala 的 Int 没有 ++ 自增运算符;++ 是集合拼接方法,不是数字自增用 i += 1 或 i = i + 1;拼接两个集合才用 a ++ b。
reassignment to val用 val 声明的变量在绑定时就固定了,不能再赋值需要重新赋值就声明成 var;如果只是要新值,就新建一个 val,例如 val next = old + 1。
type mismatch; found: Unit, required: Intif 少了 else 分支、方法最后一句是 println,或循环/赋值返回 Unit,却被当成了 Int给 if 补全 else,让每个分支都返回 Int;方法的最后一行写表达式,而不是 println。
missing parameter type匿名函数用了下划线或没有标注参数,但编译器没有上下文可以推断参数类型写完整参数,例如 (x: Int) => x * 2;或先声明函数类型 val f: Int => Int = _ * 2。
scala.MatchError: 5 (of class java.lang.Integer)match 没有覆盖所有可能取值,也没有 case _ 兜底,运行到没写的值时崩溃加上 case _ => ... 兜底;更好的做法是用 sealed trait 建模,让编译器检查是否穷尽。
java.util.NoSuchElementException: None.get对 None 调用了 get;Option 为空时直接取会抛异常用 getOrElse、map、fold 或 match 处理 None,不要在对外的业务代码里直接写 opt.get。
error: not found: type X类或类型 X 没有定义、名字大小写拼错、缺少 import,或定义在被遮挡的作用域里先定义 class/trait/case class;检查拼写和 import;把类型放到可见的作用域或正确的包中。
[error] download error: Connection timed out(sbt 首次下载依赖超时)sbt 第一次运行要下载 sbt 启动器、Scala 编译器和依赖,访问 Maven 仓库超时重试并换网络;必要时配置代理或国内镜像;在用户目录的 .sbt/repositories 里配置仓库。
object Hello is already defined同一个包、同一个文件或同一个 REPL 会话里重复定义了 Hello删除重复定义或多余文件;REPL 里重新定义请先 :reset 或重开;一个文件只保留一个 object Hello。
中文乱码(显示锟斤拷、问号或方块)源文件、CSV 或控制台的编码不一致;Windows 默认代码页不一定是 UTF-8源文件另存为 UTF-8;编译时用 scalac -encoding UTF-8;读文件用 Source.fromFile(path, "UTF-8") 或 import scala.io.Codec 后 implicit val codec = Codec.UTF8;控制台可先执行 chcp 65001。
IDEA 报 Cannot resolve symbol Scala / 找不到 Scala SDKIDEA 没安装或没启用 Scala 插件,或者项目没有关联 Scala SDK安装并启用 Scala 插件后重启;Project Structure 里设置 JDK 与 Scala SDK;sbt 项目直接选择重新导入 sbt 通常最省事。
implicit not found / could not find implicit value for parameter ord: Ordering[Student]泛型方法需要 Ordering 或其他隐式参数,但作用域里没有对应的隐式实例为自定义类型提供 implicit val ord: Ordering[Student] = Ordering.by(_.score);或导入内置类型对应的隐式。Scala 3 用 given/using 表达同样的机制。
java.lang.UnsupportedClassVersionError编译时用的 JDK 和运行时的 JDK 版本不一致,class 文件版本比当前 JVM 新保证 java -version 与 javac -version 指向同一个 JDK;sbt 工程清理 target 后重新编译;本课统一用 JDK 11 最稳。
java.io.FileNotFoundException: scores.csv (系统找不到指定的文件。)相对路径是相对启动程序时的当前目录,不是相对源码文件所在目录;也可能文件名或扩展名写错先 cd 到数据文件目录,或把路径改成绝对路径;用 dir 确认文件名,并让代码里的扩展名与实际文件一致。

完成自检清单

  • ☐ 我能打开 cmd,运行 java -version 和 javac -version,并说出它们各自负责什么。
  • ☐ 我能用 sbt --version 看到版本号,不会再被“不是内部或外部命令”卡住。
  • ☐ 我能用 sbt console 进入 scala>,算出 1 + 2,并用 :quit 退出来。
  • ☐ 我能自己写 object Hello extends App,用 scalac 编译、scala 运行,看到 Hello, Scala!。
  • ☐ 我能建出 build.sbt 和 src/main/scala 目录,用 sbt run 运行,并在改一行后重新跑通。
  • ☐ 我能解释 val 与 var 的区别,并用 s"" 和 f"" 完成字符串拼接与小数格式化。
  • ☐ 我能用 List 的 map、filter 以及链式调用,把一组数变换、筛选成新列表。
  • ☐ 我能用 scala.io.Source 读 CSV,按某列 groupBy 统计,并换一个文件名再跑一次。
  • ☐ 遇到报错时,我能先读第一行 error 或异常类型,再到报错速查里找到原因和改法。