零基础起步:第一次写 Scala
6 个关卡,每一步都写清「做什么 / 敲什么 / 你应该看到 / 报错怎么办」。全程约 50 分钟。
关卡目录
第 0 关 · 装 JDK 11,看见 java 和 javac 的版本号(8 分钟)
前提:一台 Windows 10/11 电脑,能联网下载约 200 MB 的 JDK;不需要提前装过 Java,也不要求会编程。
1① 先检查电脑里有没有 JDK
🎯 按 Win+R,输入 cmd 后回车;在弹出的黑窗口里逐行输入下面两条命令。如果两条都能打印版本号,就直接跳到第 ③ 步;提示“不是内部或外部命令”就继续第 ② 步安装。
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 -versionjava 不是内部或外部命令→ 电脑没装 JDK,或 JDK 的 bin 目录不在 PATH 里;按第 ② 步安装 JDK 11;安装完成后必须关闭旧 cmd,再打开一个新的
2② 安装 JDK 11(Temurin 发行版)
🎯 优先用 winget 安装;如果电脑没有 winget,就打开 adoptium.net,选择 Temurin 11、Windows x64、.msi,下载后双击安装,确认勾选 Add to PATH,其余一路默认。装完关闭旧终端。
winget install --id EclipseAdoptium.Temurin.11.JDK -e --accept-source-agreements --accept-package-agreementsFound 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,重新打开一个;执行下面三条命令。三条都要成功,缺一不可。
java -version
javac -version
where javaopenjdk 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 顺序或卸载旧版
第 1 关 · 装 sbt,第一次进 Scala REPL(9 分钟)
前提:第 0 关的 JDK 已经可用;电脑能访问 Maven 中央仓库;第一次启动 sbt 需要下载依赖,慢一点是正常的。
1① 安装 sbt
🎯 优先用 winget 安装并固定为 1.10.7,避免装到 2.x 后和本课示例不一致;如果电脑没有 winget,就打开 sbt 官网下载页,找 1.10.7 的 .msi 安装包,默认下一步安装。装完关闭所有旧终端。
winget install --id sbt.sbt --version 1.10.7 -e --accept-source-agreements --accept-package-agreementsFound sbt [sbt.sbt] Downloading https://... Successfully installed
看到 Successfully installed 表示安装完成。旧命令行里的 PATH 不会自动更新,必须重新开一个 cmd 再验证。
sbt 安装成功但 sbt --version 找不到命令→ PATH 没有刷新,或安装器没有把 sbt 的 bin 目录写进 PATH;关掉所有 cmd 后重新打开;仍不行就用 where sbt 查找,或手动把 sbt 的 bin 加入 Pathwinget 源更新失败,无法安装→ winget 源或网络异常;先执行 winget source update;仍失败就改用 sbt 官网的 msi 安装包
2② 用 sbt --version 验证安装
🎯 新开一个 cmd,执行 sbt --version。第一次会下载 sbt 启动器和依赖,可能等 1 到 3 分钟;看到两行版本号再继续。
sbt --versionsbt 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 目录),然后重开 cmdsbt 报 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 前面有一个英文冒号。
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> :quitres0 是 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 runWelcome to Scala 2.12.x→ 当前目录没有 build.sbt,sbt 退回了自己运行时的 Scala 2.12;在 repl-demo 目录里创建 build.sbt,写入 scalaVersion := "2.13.14",再启动 sbt consolevalue + is not a member of ...→ 误用了中文加号、全角空格或别的符号;切换英文输入法,重新输入英文的 1 + 2console 一直停在 downloading→ 首次下载 Scala 2.13 编译器,网络较慢;等待几分钟;超过 5 分钟换网络或配置镜像;已下载过的依赖会缓存
第 2 关 · 写第一个 Scala 程序 Hello.scala(9 分钟)
前提:第 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 后重新打开,执行下面两条命令。
scalac -version
scala -versionScala 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 里写的路径不完全一致;在资源管理器进入解压目录,从地址栏复制真实路径,再修正 PathJAVA_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。
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 才会真正执行程序。
scalac Hello.scala
scala HelloHello, Scala!
编译会生成 Hello.class 等文件;只有第二行 scala Hello 会打印结果。类名 Hello 必须和 object 名 Hello 完全一致。
scalac: 找不到文件: Hello.scala→ 当前目录不对,或文件名不是 Hello.scala;执行 cd /d C:\scala-course,再用 dir 确认文件;注意扩展名不能是 .txtjava.lang.ClassNotFoundException: Hello→ 运行目录里没有 Hello.class,或编译其实失败了;先看 scalac 是否有报错;成功后再在同一目录执行 scala Helloobject Hello is already defined→ 同一个目录里有重复的 Hello 定义,或文件里写了两遍;删掉重复定义或多余的 .scala 文件,只保留一个 object Hello打印的是 Hello, World! 不是 Hello, Scala!→ 代码没有按示例改完整;逐字核对 println 里的字符串,注意英文逗号和英文感叹号
第 3 关 · 用 sbt 建一个真正的工程(10 分钟)
前提:sbt 已经在 PATH 中;会用记事本新建文件;第一次 sbt run 可能下载 Scala 编译器,请留出几分钟。
1① 建标准 sbt 工程目录
🎯 在 cmd 里一次创建工程根目录和源码目录,再进入工程根目录。sbt 只自动识别 src/main/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 成功再 cdcd 之后提示符没变→ 只写了 cd C:\...,没有加 /d 切换盘符;用 cd /d 加完整路径;或者先切到目标盘符再 cd
2② 写 build.sbt,只放构建配置
🎯 在工程根目录打开记事本,新建 build.sbt,写入 Scala 版本。不要把 build.sbt 放进 src 里。
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。
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。
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
第 4 关 · 在 REPL 里练 val、插值与 List(7 分钟)
前提:第 2 关的 scala 命令可用;知道怎么在命令行里输入多行表达式并按回车。
1① 进 REPL,分清 val 和 var
🎯 在 cmd 里输入 scala 进入 REPL。逐行输入下面内容;注意所有标点都是英文符号。
scala
val course = "Scala 程序设计"
var week = 1
week = 2
week += 1
:quitWelcome 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
val name = "小明"
val score = 92.5
s"$name 的成绩是 $score 分"
s"1 + 2 = ${1 + 2}"
f"$score%.1f"
:quitWelcome 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> :quits 前缀是最常用的插值;${} 里可以放表达式;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
val nums = List(1, 2, 3, 4, 5, 6)
nums.map(_ * 2)
nums.filter(_ % 2 == 0)
nums.map(x => x * x).filter(_ > 10)
:quitWelcome 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 = _ * 2value % is not a member of ...→ List 元素不是数字,或括号写错导致 _ 指错对象;先用 nums 检查类型;筛选用 nums.filter(n => n % 2 == 0),把条件写完整type mismatch; found: List[Int], required: ...→ map/filter 的返回类型与下一步操作不匹配;在 REPL 一行一行执行,看每行 res 的类型;不要一次粘贴多行后猜测错误位置
第 5 关 · 读 CSV,按班级分组统计(8 分钟)
前提:第 2 关的 scalac/scala 可用;会用记事本保存 UTF-8 文件;准备与本关示例一致的 scores.csv。
1① 准备 scores.csv
🎯 在 cmd 里进入 C:\scala-course,用记事本新建 scores.csv,写入表头和四行数据,保存编码选 UTF-8。
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,粘贴下面完整代码并保存。先不用改,后面会运行它。
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 都在当前目录;先编译,再运行。
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 改成新文件名并保存,然后重新编译运行,验证程序不是写死在一份文件上。
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
常见报错速查 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 Int | Scala 的 Int 没有 ++ 自增运算符;++ 是集合拼接方法,不是数字自增 | 用 i += 1 或 i = i + 1;拼接两个集合才用 a ++ b。 |
| reassignment to val | 用 val 声明的变量在绑定时就固定了,不能再赋值 | 需要重新赋值就声明成 var;如果只是要新值,就新建一个 val,例如 val next = old + 1。 |
| type mismatch; found: Unit, required: Int | if 少了 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 SDK | IDEA 没安装或没启用 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 或异常类型,再到报错速查里找到原因和改法。