简介:一份面向大数据开发者的Scala升级版学习笔记,围绕Spark 2.0以后对Scala 2.11的依赖展开,适合想入门Spark或系统补齐Scala语法基础的读者。资源仅含1个docx文档,压缩包整体2.04MB,轻量实用,可直接阅读。目前已有2060人学习浏览,内容覆盖Scala安装配置、JDK与开发工具选择、基础语法、类与对象、伴生对象等核心模块。文档从官网六大特征切入,逐一说明Java与Scala混编、类型推测、并发分布式、特质、模式匹配和高阶函数,并给出Windows/Linux安装scala及IntelliJ IDEA配置SDK的具体步骤;在语法部分,通过var与val对比、if表达式、apply方法、Person类和Lesson_Class对象等示例,演示变量声明、类的实例化与main方法调用。整体结构由环境搭建到代码实践层层推进,能帮助读者快速理解Scala精简语法并衔接Spark开发;文档结构清晰、示例具体,适合自学也适合教学参考。
1. Scala 不是升级版 Java:先搞明白它解决什么问题
Scala 是那种看着像「升级版 Java」、一上手却被 val/var 和伴生对象绊倒的语言。它跑在 JVM 上,却把面向对象和函数式编程组合在一起,类型推测、表达式赋值、模式匹配、高阶函数这些特性,让习惯了逐行写命令的开发者一开始很容易翻车。可只要你想碰大数据,Scala 几乎是绕不开的门票:Spark 1.6 依赖 Scala 2.10,Spark 2.0 以上依赖 Scala 2.11,想读 Spark 源码或者写算子,基础语法必须先落地。这份升级版笔记覆盖 Windows/Linux 安装、IDEA 配置、变量声明、控制流、类与伴生对象、方法与函数的完整链路,适合刚装好 JDK 却不知道下一步做什么的入门者,也适合想快速确认边界和易错点的从业者。
2. 开发环境与类型系统:JDK、IDEA 插件与 val/var 的边界
很多人学 Scala 第一个翻车点不是语法,是环境装完跑不起来。Scala 官网列过六个核心特征——Java/Scala 混编、类型推测、并发与分布式、特质、模式匹配、高阶函数,其中一半要先有能跑的环境才能体验。我建议装环境就按「JDK 1.8 + Scala 2.11.8 + IDEA Scala 插件」这套组合走,理由有两个:一是和 Spark 2.x 完全对齐,二是社区教程、现成代码包大多在这个组合上验证过,出问题能搜到答案。下面把 Windows 和 Linux 两条路径分别说清楚。
2.1 Windows/Linux 安装与环境变量:SCALA_HOME 别指错层级
Windows 安装最简单的方式是直接装 scala-2.11.8.msi,装完自动写注册表,但环境变量不一定全;更高频的坑是下载了 tgz 自己解压,结果 SCALA_HOME 指到了 bin 目录内部。正确做法是新建 SCALA_HOME 指向解压根目录,再把 %SCALA_HOME%\bin 和 %SCALA_HOME%\jre\bin 追加到 Path。
# Windows 系统属性里新建系统变量 SCALA_HOME=C:\Program Files\scala-2.11.8 # 编辑 Path,在末尾追加(注意前面的分号) Path=%Path%;%SCALA_HOME%\bin;%SCALA_HOME%\jre\bin # 配置完成后,新开一个 CMD 窗口验证 scala -version这里有个血泪经验:配置完环境变量后必须新开终端,不要用已经打开的 CMD 窗口,否则 session 里读不到新的 Path 值,报「不是内部或外部命令」会让你误以为装坏了。验证时如果看到类似 Scala version 2.11.8 的输出,说明解释器已经可用;如果报找不到 main 类,多半是 SCALA_HOME 指向了包含 bin 的再上一层目录,导致 jre 路径失效。
Linux 下更简单,解压即用,不加进 Path 也可以直接调用完整路径,但做开发还是建议写入用户环境变量:
tar -zxvf scala-2.11.8.tgz -C /opt export SCALA_HOME=/opt/scala-2.11.8 export PATH=$PATH:$SCALA_HOME/bin scala -version注意 /opt/scala-2.11.8 这个路径要能看到 bin 和 lib 两个目录,如果解压后多了一层嵌套目录,比如 /opt/scala-2.11.8/scala-2.11.8/bin,SCALA_HOME 也要指到内层那个根目录,否则 scala 命令会找不到。这一步很多人栽在「解压目录名带版本号、解压后又套了一层」上,echo 一下路径就能看出来。
2.2 IDEA 插件与 Scala SDK:创建项目时的关键一步
我见过不少人卡在 IDEA 里:插件装了,代码文件也建了,但写完发现没有语法高亮,或者 New Project 里根本没有 Scala 选项。这两个问题的根因不同,插件没装好是前者,SDK 没配置是后者。推荐的做法是打开 IDEA 后先关闭项目,进入 Configure → Plugins,搜索 Scala 并安装,安装完成后重启。
重启后新项目就有 Scala 类型可选。此时最关键的是给项目指定 Scala SDK:在 Project Structure 里先设置 JDK 路径(建议选 1.8),然后在 Global Libraries 里新增 Scala SDK,点 Browse 选择本地 Scala 安装根目录。完成这一步后,新建的 Scala 项目会自动带上 scala-library。为了验证链路通不通,我一般会先写一个最小程序:
object Hello { def main(args: Array[String]): Unit = { println("hello scala") } }这段代码有三个约定要先记住:object Hello 定义的是单例对象,不是类,所以不需要 new;def main 是入口方法,args 参数类型固定为 Array[String];Unit 表示方法没有返回值,类似 Java 的 void 但更严格。如果这一步能打印 hello scala,说明插件、JDK、SDK 三条链路已经打通。Eclipse 想装 Scala 插件也可以,但插件版本必须和 Eclipse 版本严格对应,装错直接不加载,不推荐新手花时间在这种环境调试上。
2.3 val/var、类型推测与表达式赋值:极简原则的三个边界
Scala 里声明变量只有两个关键字,val 和 var。val 定义的是不可变常量,相当于 Java 的 final;var 定义的是可变变量。很多初级代码翻车都出在把业务上不该变的值用 var 写,结果后面被别处修改,调试时找不到源头。
var name = "zhangsan" println(name) name = "lisi" // var 可以重新赋值 val gender = "m" // gender = "f" // 编译报错:不能给 val 再赋值 val a: Int = 10 // 可以显式写明类型,但 Scala 鼓励省略 val x = 1 // 编译器自动推测为 Int val y = if (1 > 0) 1 else -1 // 整个表达式赋值给 val val mix = if (x > 0) 1 else "jay" // Int 和 String 的公共父类是 Any val p = if (x > 5) 1 // 没有 else 分支,自动补 Unit这里的核心是「极简原则」:能省的类型标注全省掉,类型推测会帮编译器拿到正确类型。但省略类型不意味着没有类型,mix 的类型被推成 Any,因为 Int 和 String 共同的父类只有 Any;p 这种缺 else 的表达式,实际类型是 AnyVal,因为缺失分支被补成了 Unit。写业务代码时,如果发现变量类型变成 Any,通常说明两个分支的类型差异太大,不是设计问题就是写错了。
懒加载是另一个容易忽视的 val 细节:
lazy val lazyVal = { println("I am too lazy") 1 } println("before access") println(lazyVal) // 只有第一次访问才执行初始化块lazy 只能修饰 val,作用是把初始化延迟到第一次真正访问时,适合那些构建成本高、又不一定用得到的对象。注意 lazy 不是线程安全的「延迟初始化」,并发场景下多个线程同时第一次访问时,初始化块可能被执行多次,这是初学阶段最容易踩的并发语义坑。
3. 控制流与集合操作:Range、for 推导式与 yield 的返回边界
Scala 的控制流和 Java 最大的区别是:for 不只是循环,它更像一个表达式生成器。理解 to/until 的端点规则、for 的守卫条件、yield 的返回机制,之后看 Spark 里的算子代码会顺很多。这章我会按 Range 构造、for 多种写法、yield 与 while 三条线走,代码都来自课程里可以直接手敲的例子。
3.1 to 与 until:端点、步长和 Range 底层
to、until 和 Range 三个概念是同一件事的不同写法。to 是闭区间,左右都包含;until 是前闭后开,不包含右端点;Range 是底层的整数范围类型,支持指定步长和递减。
println(1 to 10) // Range(1, 2, ..., 10),包含 10 println(1 until 10) // Range(1, 2, ..., 9),不包含 10 println(1.to(10)) // 与 1 to 10 等价 println(1 to (10, 2)) // 1, 3, 5, 7, 9,步长为 2 println(1 until (10, 3)) // 1, 4, 7,不包含 10 val r1 = Range(1, 10) // 等价 1 until 10 val r2 = Range(1, 10, 2) // 初始值 1,条件值 10,步长 2 val r3 = Range(10, 1, -2) // 10, 8, 6, 4, 2,步长可以为负注意步长不能为 0,写 0 会直接抛 IllegalArgumentException。Range 构造的第二个参数是「条件值」,不是严格意义的结束值,递减时要写成 Range(10, 1, -2),而不是 Range(10, 0, -2)。这个细节很多人第一次写倒序循环时都会翻车。另外中缀写法 1 to 10 和显式方法调用 1.to(10) 完全等价,Scala 里带单个参数的方法都可以这么写,但括号版本在步长不为 1 时必须用1 to (10, 2)这种带括号的形式,省略括号会变成传两个参数,编译直接报错。
3.2 for 推导式:多层生成器与守卫条件
Scala 的 for 可以跟多个生成器,中间用分号分隔,构成嵌套循环。而且 for 里不能写 count++、count--,只能写 count += 1,这是 Java 转 Scala 最常见的语法报错之一。
for (i <- 1 to 10) { println(i) } for (i <- 1 to 10; j <- 1 until 10) { println("i=" + i + ", j=" + j) } // 守卫条件:把过滤逻辑写在 if 里 for (i <- 1 to 10; if (i % 2 == 0); if (i == 4)) { println(i) // 只有 4 会输出 }守卫条件的位置可以紧跟生成器,也可以隔开,多个 if 之间用分号分隔,作用是把不满足条件的元素直接过滤掉,相当于 Java 循环体里套 if 然后 continue。课程里有个九九表的例子,正是多层生成器加守卫条件的典型组合:
for (i <- 1 until 10; j <- 1 until 10) { if (i >= j) { print(i + " * " + j + " = " + i * j + " ") } if (i == j) { println() } }这段逻辑里,外层循环是行,内层循环是列,i >= j 的条件把乘法表的下三角部分保留,i == j 时换行。执行到 9 那一行时,内层循环会完整跑一遍 1 到 9,但由于 i >= j 的限制,每行输出数量刚好等于当前行号。看着简单,实际是把多层循环、守卫条件、字符串拼接三个点一起练了。
3.3 yield 与 while:把循环结果收成集合
for 推导式加 yield,会把每次满足条件的元素收集起来,最终返回一个集合对象。这是 Scala 里把循环变成表达式的重要手段,也是理解后续集合算子 chain 的基础。
val result = for (i <- 1 to 100 if (i > 50) if (i % 2 == 0)) yield i println(result) // Vector(52, 54, 56, ... , 100) val list = for (i <- 1 to 10; if (i > 5)) yield i for (w <- list) { println(w) } // 课程加深练习:取出数组中的所有偶数 val a1 = Array(1, 2, 3, 4, 5, 6, 7, 8, 9) val evens = for (i <- a1 if (i % 2 == 0)) yield iyield 的结果类型跟源集合类型相关:源是 Array,返回 Array;源是 Range,返回 Vector。写的时候要注意 yield 关键字必须放在表达式最后,并且整个 for 表达式的返回值不要当成 Java 的「循环里 break」,它是一次性收集,不是逐元素消费。while 和 do-while 在 Scala 里依然存在,但函数式风格下用得越来越少:
var index = 0 while (index < 100) { println("第" + index + "次 while 循环") index += 1 } index = 0 do { index += 1 println("第" + index + "次 do while 循环") } while (index < 100)强烈建议刚入门时把 while 写法都改成 for + yield 试试。while 里操作的是外部可变变量,出了循环就看不到结果,而 for + yield 会返回一个干净的集合,调试时直接打印 result 就能看到中间值,省去一堆 println 埋点。
4. 类与对象模型:默认构造器、apply 与伴生对象的联动
Scala 的类体系初看和 Java 相似,实际坑很多:class 默认就能带参数,那参数就是主构造器;object 是单例且不能传参,传参就走 apply 方法;同名 class 和 object 互称伴生,能互相访问私有成员。这三个机制单独看都不难,合在一起写业务代码时,容易搞混该 new 还是不该 new。
4.1 class 与 object:一个要 new,一个是单例
class 是普通的类模板,必须 new 才有实例;object 是单例对象,相当于 Java 里的静态工具类容器,直接拿名字调用。项目里习惯把 main 方法放在 object 里,作为入口。
class Person { val name = "zhangsan" val age = 18 def sayName() = { "my name is " + name } } object Lesson_Class { def main(args: Array[String]): Unit = { val person = new Person() println(person.age) println(person.sayName()) } }new Person 时,类里字段初始化(name、age 赋值)会执行,但 sayName 的方法体不会执行,只有调用才执行。这个「new 时哪些代码会跑」的边界很多人一开始搞不清楚:字段初始化表达式会跑,方法体不跑,构造器里的逻辑跑。另外 class 的属性自带 getter 和 setter,外部直接 person.age 就能读,不需要手写 JavaBean 那一套。
4.2 apply:object 传参背后的自动路由
object 不能像 class 那样定义构造参数,但如果你在 object 里定义了 apply 方法,调用时直接 object 名加参数,编译器就会自动按参数个数寻找对应的 apply。
object Lesson_ObjectWithParam { def apply(s: String) = { println("name is " + s) } def apply(s: String, age: Int) = { println("name is " + s + ", age = " + age) } def main(args: Array[String]): Unit = { Lesson_ObjectWithParam("zhangsan") // 匹配 1 个参数的 apply Lesson_ObjectWithParam("lisi", 18) // 匹配 2 个参数的 apply } }这里 Lesson_ObjectWithParam("zhangsan") 看起来像在新建对象,实际是调用 Lesson_ObjectWithParam.apply("zhangsan")。参数个数不同,走不同的 apply 重载。这个机制在集合类里用得极多,比如 List(1,2,3) 就是 apply 的语法糖。自己写 object 时,如果调用处报「缺少参数」,优先检查是不是没定义 apply,或者 apply 的参数个数和调用处不一致。
4.3 伴生类、辅助构造器与私有访问
同一个源文件里,同名的 class 和 object 会形成伴生关系。class 是伴生类,object 是伴生对象,两者可以互相访问对方的私有成员。这个特性让工具方法和类本身可以放在一起,又保持静态与实例的边界。
class Person(xname: String, xage: Int) { var name = Person.name // 访问伴生对象的成员 val age = xage var gender = "m" def this(name: String, age: Int, g: String) { this(name, age) // 辅助构造器第一行必须调用主构造器 gender = g } def sayName() = { "my name is " + name } } object Person { val name = "zhangsanfeng" def main(args: Array[String]): Unit = { val person = new Person("wangwu", 10, "f") println(person.age) println(person.sayName()) println(person.gender) } }这里有两个重点。一是 class 的默认参数列表就是主构造器,new Person("wangwu", 10) 走的是主构造;如果还想提供三参数的构造方式,定义 def this(...) 时必须先调用 this(name, age),也就是必须先经过主构造器,这是硬性规则。二是伴生对象里的 name 是 val,伴生类里也有一个 name 字段,两者互不干扰,但伴生类可以直接引用 Person.name,看起来像访问静态成员。命名上建议类名首字母大写、方法首字母小写、整体驼峰;Trait 特征和 object 一样不能传参数,这个后面写特质时会遇到。
5. 常见问题排查:Scala 入门的五个高频坑
这些坑是我带人学 Scala 时反复看到的,每一个都能用场景复现,也都有一句话能说清的原因。下面按「现象、原因、解决」拆开写,卡住时直接对照。环境问题排在最前面,是因为 Scala 本身是 JVM 语言,任何「装好了跑不起来」的现象,九成是路径问题而不是代码问题,先排除环境再查语法,效率最高。
5.1 环境与路径:命令不识别、SDK 选不到
坑 1:安装完成后,CMD 里输入 scala -version,提示「不是内部或外部命令」。现象:环境变量配了,也点过确定,但新终端依然找不到 scala 命令。原因:SCALA_HOME 指向的不是包含 bin 目录的根目录,而是 bin 目录本身,或者 Path 里写成了 %SCALA_HOME%\bin 但 SCALA_HOME 本身有误,导致 jre 相对路径失效。解决:先 echo %SCALA_HOME%,确认输出能看到 bin 和 lib 同级;再看 Path 末尾是否真的追加了 %SCALA_HOME%\bin;最后关掉当前 CMD,重开一个新窗口验证。不要用已经有历史 session 的终端,这是最容易被忽略的原因。
坑 2:IDEA 里创建 Scala 项目时,Scala SDK 下拉框是空的,或者项目建完没有语法高亮。现象:插件装了,但 New Project 里 Scala 选项不可选,SDK 下拉列表中没有任何条目。原因:Scala 插件只负责编辑功能,SDK 需要你在 Project Structure 里手动添加本地 Scala 根目录;没有 SDK 的项目本质上还是 Java 工程,Scala 文件只会被当成文本。解决:进入 Project Structure → Global Libraries,新增 Scala SDK,Browse 指向安装目录,确认列表里出现 scala-library-2.11.8.jar 后,再到项目里把 SDK 挂上。顺序不能反,先建项目再回头配 SDK 往往会遇到缓存不刷新的问题,最简单是重启 IDEA。
5.2 语法语义:val 赋值报错、apply 找不到
语法语义类的坑有一个共同特点:编译报错信息很短,但指向很明确。val 赋值和 apply 调用是入门阶段最高发的两个编译错误,一个属于「变量语义没分清」,一个属于「对象调用的语法糖没理解」。
坑 3:用 val 声明变量后,后面又给它重新赋值,直接编译报错。现象:val gender = "m",再写 gender = "f",编译器报 error: reassignment to val。原因:val 是不可变引用,等价于 Java 的 final。如果你把它理解成「普通变量」,就会在业务代码里到处踩这个错。解决:需要被复用的状态用 var,但优先思考这个变量是否真的需要变化;如果只是中途要改一次,说明代码结构可能需要拆成两个 val,而不是把一个 var 改来改去。写完代码回头看这种 warning,基本能发现一半坏味道。
坑 4:object 后面跟参数调用,报找不到 apply 方法。现象:Lesson_ObjectWithParam("zhangsan") 编译失败,提示 Application does not take parameters。原因:object 本身没有构造器,不能直接接受参数;编译器会尝试按名字找 apply,找不到就报错。另外 object 名称和调用处不一致,或者 apply 参数个数不匹配,也会同样报错。解决:在 object 内部定义 def apply(s: String) 和 def apply(s: String, age: Int),然后重编译。不确定时把调用改成 Lesson_ObjectWithParam.apply("zhangsan") 显式写,如果显式调能过、简写不过,就是 apply 签名问题,而不是调用方式问题。
5.3 版本匹配:Spark 与 Scala 的兼容性
坑 5:用 Scala 2.10 写的代码拿到 Spark 2.0 环境跑,出现 NoSuchMethodError 或 ClassNotFoundError。现象:代码能编译,提交到集群后运行时直接抛 NoSuchMethodError,日志指向 scala.collection 相关类。原因:Spark 1.6 依赖 Scala 2.10,Spark 2.0 以上依赖 Scala 2.11;Scala 小版本之间二进制不兼容,2.10 编译的 class 放进 2.11 运行时就会找不到方法。解决:学习阶段统一装 Scala 2.11.8,和 Spark 2.x 对齐。别装最新版 Scala 然后抱怨生态不支持你,先弄清楚目标框架锁定的 Scala 版本,再决定本地 SDK 装什么。
| 检查项 | 命令或位置 | 预期结果 |
|---|---|---|
| SCALA_HOME | echo %SCALA_HOME% | 输出 scala-2.11.8 根目录,bin 与 lib 同级 |
| 解释器版本 | scala -version | 输出 Scala version 2.11.8 |
| IDEA SDK | Project Structure → Global Libraries | 存在 scala-library-2.11.8.jar |
| 最小程序 | 运行 Hello 对象 | 控制台输出 hello scala |
| 编译检查 | scalac Hello.scala | 生成 .class 且无 error 输出 |
自检按从上到下顺序执行,哪一步不过就停在哪一步,不要跳到后面查代码。这张表做不了全部排查,但能覆盖八成入门期的环境问题。剩下语法类的问题,记住一个原则:报错信息里出现 reassignment 就是 val 语义问题,出现 does not take parameters 就是 apply 缺失或签名不匹配。
6. 用一个综合练习验收:偶数过滤到高阶函数串联
学完语法不等于会用。我的习惯是把课程里分散的点压成三个小题,每个小题只验证一个能力,跑通了才继续看下一章。第一个题是把数组中的偶数取出来,同时检验 for 守卫和 yield 的边界;第二个题是写一个带默认值的偏应用函数,检验参数传递规则;第三个题是把 apply 和高阶函数揉在一起,检验「函数当参数」的直观理解。
// 练习 1:验证 for 守卫与 yield val a1 = Array(1, 2, 3, 4, 5, 6, 7, 8, 9) val evens = for (i <- a1 if (i % 2 == 0)) yield i println(evens.mkString(",")) // 期望输出 2,4,6,8// 练习 2:验证默认参数与偏应用 def log(date: String, msg: String) = println(date + " : " + msg) val logToday = log("2024-01-01", _: String) logToday("first") // 期望输出 2024-01-01 : first logToday("second") // 期望输出 2024-01-01 : second// 练习 3:验证 apply 与高阶函数的配合 object Calc { def apply(x: Int, y: Int, f: (Int, Int) => Int) = f(x, y) } println(Calc(3, 4, (a, b) => a + b)) // 期望输出 7 println(Calc(3, 4, (a, b) => a * b)) // 期望输出 12练习 1 如果输出成了空集合,先查守卫条件是不是写成了赋值号;练习 2 如果报错缺参数,说明偏应用的占位符 _: String 少了类型标注;练习 3 如果编译不过,优先查 apply 的最后一个参数类型是不是 (Int, Int) => Int。三个题都跑通,再去看伴生对象里互相访问私有成员、辅助构造器第一行必须调用主构造器这些细枝末节,心里就踏实得多。我那会儿就是跳过了这套验收直接去看 Spark,结果在闭包和序列化上被坑了一整周。从那以后我每学一个语法点,都强制自己写一个最小示例跑一遍,再把示例精简到不能再删。希望帮到你。
本文还有配套的精品资源,点击获取