大数据学习笔记:Scala
Scala
Scala是一门多范式的编程语言,设计初衷是要集成面向对象编程和函数式编程的各种特性
特点
- 运行在JVM上,可与现存程序同时运行
- 可直接使用Java类库
- 同Java一样静态类型
- 语法和Java类似,比Java更加简洁(简洁而并不是简单),表达性更强
- 同时支持面向对象、函数式编程
- 比Java更面向对象
1. 环境配置
-
Windows中下载安装配置环境变量:
- 类似于java配置
SCALA_HOME
为安装目录。 - 添加
%SCALA_HOME%\bin
到path环境变量。
- 类似于java配置
-
IntelliJ IDEA环境配置:
- 创建Maven项目,J选择JDK版本。
- 安装插件:Scala。
- Maven项目默认用Java写,在
src/main/
目录下新建目录scala/
,然后将目录标记为Source Root。 - 这样甚至可以在同一个项目中混用Scala和Java源文件,并互相调用。
- 需要能够添加Scala源文件,右键项目,添加框架支持,配置Scala SDK,选择,然后就可以右键添加Scala源文件了。
- 添加包,添加Scala类,选择对象,编辑源码。
-
Scala中类和伴生对象:
object
关键字创建的伴生对象,可以理解为替代Java的static
关键字的方式,将静态方法用单例对象的实例方法做了替代,更纯粹的面向对象。- 等价的类定义,区别Scala和Java
public class Student { private String name; private Integer age; private static String school = "HDU"; public Student(String name, Integer age) { this.name = name; this.age = age; } public void printInfo() { System.out.println(this.name + " " + this.age + " " + Student.school); } // psvm public static void main(String[] args) { Student flash7k = new Student("flash7k", 20); tch.printInfo(); }
class Student(name: String, age: Int) { def printInfo(): Unit = { println(name + " " + age + " " + Student.school) } } // 引入伴生对象,名称一致,同一个文件 object Student { val school: String = "HDU" def main(args: Array[String]): Unit = { val flash7k = new Student("flash7k", 20) tch.printInfo() }
2. 变量与数据类型
代码规范快捷键
- tab向右缩进,shift+tab向左缩进
- ctrl + alt + L 代码格式化
变量和常量
var name [:VariableType] = value // variable
val name [:ConstantType] = value // constant
能用常量就不用变量
- 声明变量时,类型可以省略,编译器自动推导(类型推导)
- 类型确定后,不能修改(Scala是强数据类型语言)
- 变量声明时,必须有初始值
- 定义变量时,可用
var
或val
修饰,var
修饰的变量可改变- 引用类型常量,不能改变常量指向的对象,但可以改变对象的字段
标识符命名规范
-
以字母下划线开头,后跟字母数字下划线,和C/C++/Java一样。
-
操作符开头,且只包含(+-*/#!等)
好处:灵活的运算符也是方法,更加面向对象
-
用反引号包括的任意字符串,即使是同39个Scala关键字同名也可以
var _abc:String = "hello"
val -+/%# = 10
val `if` = 10
println(_abc)
println(-+/%#)
println(`if`)
关键字
package import class obejct trait extends with type for
private protected abstract sealed final implicit lazy override
try catch finlly throw
if else match case do while for return yield
def var val
this super
new
true false null
- 其中Java没有的关键字:
object trait with implicit match yield def val var
字符串
- 类型:
String
+
号连接*
字符串乘法,复制一个字符串多次printf
格式化输出- 字符串插值:
s"xxx${varname}"
前缀s
模板字符串,前缀f
格式化模板字符串,通过$
获取变量值,%
后跟格式化字符串。 - 原始字符串:
raw"rawstringcontents${var}"
,不会考虑后跟的格式化字符串。 - 多行字符串:
""" """
。 - 输出:
print printf println ...
val name: String = "hello" + " " + "flash7k"
val age = 17
println((name + " ") * 3)
printf("%s : dead in %d\n", name, age)
print(s"$name : dead in ${age}")
val power = 98.9072
println(f" : power ${power}%.2f.")
var sql = s"""
|Select *
|from
| Student
|Where
| name = ${name}
|and
| age >= ${age}
""".stripMargin // strip | and whitespaces before |
println(sql)
键盘输入
StdIn.readLine()
StdIn.readShort() StdIn.readDouble
import scala.io.StdIn
println("input name:")
val name: String = StdIn.readLine()
println("input age:")
val age:Int = StdIn.readInt()
println(name + " : " + age)
读写文件
import scala.io.Source
import java.io.PrintWriter
import java.io.File
object FileIO {
def main(args: Array[String]): Unit ={
// read from file
Source.fromFile("FileIO.txt").foreach(print)
// write to file
// call java API to write
val writer = new PrintWriter(new File("WFile.txt"))
writer.write("flash7k IO!")
writer.close()
}
}
数据类型
- Java
- 基本类型
char byte short int long float double boolean
。 - 对应包装类:
Charater Byte Short Integer Long Float Double Boolean
。 - 不是纯粹的面向对象。
- 基本类型
- Scala
- 所有数据都是对象,都是
Any
的子类。 Any
有两个子类:AnyVal
值类型 、AnyRef
引用类型。- 数值类型都是
AnyVal
子类,和Java数值包装类型都一样,只有整数在Scala中是Int
、字符是Char
有点区别。 StringOps
是java中String
类增强,AnyVal
子类。Unit
对应java中的void
,AnyVal
子类。用于方法返回值的位置,表示方法无返回值,Unit
是一个类型,只有一个单例的对象,转成字符串打印出来为()
。Void
不是数据类型,只是一个关键字。Null
是一个类型,只有一个单例对象null
就是空引用,所有引用类型AnyRef
的子类,这个类型主要用途是与其他JVM语言互操作,几乎不在Scala代码中使用。Nothing
所有类型的子类型,也称为底部类型。它常见的用途是发出终止信号,例如抛出异常、程序退出或无限循环。
- 所有数据都是对象,都是
整数类型:都是有符号整数,标准补码表示。
Byte
1字节Short
2字节Int
4字节Long
8字节- 整数赋初值超出表示范围报错。
- 自动类型推断,整数字面值默认类型
Int
,长整型字面值必须加L
后缀表示。 - 直接向下转换会失败,需要使用强制类型转换,
(a + 10).toByte
。
浮点类型
Float
IEEE 754 32位浮点数Double
IEEE 754 64位浮点数- 字面值默认
Double
字符类型
- 同Java的
Character
,2字节,UTF-16编码的字符 - 字符常量:
''
- 类型
Char
- 转义:
\t \n \r \\ \" \'
布尔类型:true false
空类型
Unit
无值,只有一个实例,用于函数返回值。Null
只有一个实例null
,空引用。Nothing
确定没有正常的返回值,可以用Nothing来指定返回值类型。(大概只有)抛异常时返回Nothing
强制类型转换
toByte toInt toChar toXXXX
'a'.toInt
2.7.toInt
- 数值与String的转换:
"" + n
"100".toInt
"12.3".toFloat
12.3".toDouble.toInt
- 整数强转是二进制截取,整数高精度转低精度可能会溢出,比如
128.toByte
(-128)
Scala标准库
Int
Double
这些数据类型对应于Java中的原始数据类型,在底层的运行时不是一个对象,但Scala提供了从这些类型到scala.runtime.RichInt/RichDouble/...
的(低优先级)隐式类型转换(在Perdef
中定义),从而提供了非原始类型具有的对象操作。- 基本类型都是默认导入的,不需要显式导入,位于包
scala
中。还有scala.Predef
对象也是自动导入。 - 其他需要导入的包:
scala.collection
集合。scala.collection.immutable
不可变数据结构,比如数组、列表、范围、哈希表、哈希集合。scala.collection.mutable
可变数据结构,数组缓冲、字符串构建器、哈希表、哈希集合。scala.collection.concurrent
可变并发数据结构,比如字典树。
scala.concurrent
原始的并发编程。scala.io
输入输出。scala.math
基本数学操作。scala.sys
操作系统交互。scala.util.matching
正则。- 标准库中的其他部分被放在独立的分开的库中。可能需要单独安装,包括:
scala.swing
java的GUI框架Swing的封装。- 定义了一些别名给常用的类,比如
List
是scala.collection.immutable.List
的别名,也可以理解为默认导入? - 其他别名可能是底层平台JVM提供的,比如
String
是java.lang.String
的别名。
3. 运算符
运算符
- 和Java基本相同。
- 算术运算:
+ - * / %
,+
可以用于一元正号,二元加号,还可以用作字符串加法,取模也可用于浮点数。没有自增和自减语法++ --
。 - 关系运算:
== != < > <= >=
- 逻辑运算:
&& || !
,&& ||
所有语言都支持短路求值,Scala也不例外。 - 赋值运算:
= += -= *= /= %=
- 按位运算:
& | ^ ~
- 移位运算:
<< >> >>>
,其中<< >>
是有符号左移和右移,>>>
无符号右移。 - Scala中所有运算符本质都是对象的方法调用,拥有比C++更灵活的运算符重载。
自定义运算符
- Scala中运算符即是方法,任何具有单个参数的方法都可以用作中缀运算符,写作中缀表达式的写法。
10.+(1)
即是10 + 1
。 - 定义时将合法的运算符(只有特殊符号构成的标识符)作为函数名称即可定义。
4. 控制流
if-else
if (condition) {
xxx
} else if (condition) {
xxx
} else {
xxx
}
- Scala中特殊一点,
if-else
语句也有返回值,也就是说也可以作为表达式,定义为执行的最后一个语句的返回值。 - 可以强制要求返回
Unit
类型,此时忽略最后一个表达式的值,得到()
。 - 多种返回类型的话,赋值的目标变量类型需要指定为具体公共父类,也可以自动推断。
- scala中没有三元条件运算符,可以用
if (a) b else c
替代a ? b : c
。 - 嵌套条件同理。
for
- 范围遍历:
for(i <- 1 to 10) {}
,其中1 to 10
是Int
一个方法调用,返回一个Range
。 - 范围
1 to 10
1 until 10
是包含右边界和不包含右边界的范围,也可以直接用Range
类。 - 范围步长
1 to 10 by 2
。 - 范围也是一个集合,也可以遍历普通集合:
for(i <- collection) {}
- 循环守卫:即循环保护式,或者叫条件判断式,循环守卫为
true
则进入循环体内部,为fasle
则跳过,类似于continue
。
for(i <- collection if condition) {}//等价于if (i <- collection) { if (condition) { }}
- 嵌套循环同理:
//标准写法for (i <- 1 to 4) { for (j <- 1 to 5) { println("i = " + i + ", j = " + j) } }//等价写法for (i <- 1 to 4; j <- 1 to 5) { println("i = " + i + ", j = " + j) }// 乘法表for (i <- 1 to 9; j <- 1 to i) { print(s"$j * $i = ${i * j} \t") if (j == i) println()}
- 循环同样有返回值,返回值都是空,也就是
Unit
实例()
。 - 循环中同样可以用
yield
返回,外面可以接住用来操作,循环暂停,执行完后再继续循环
val v = for (i <- 1 to 10) yield i * i // default implementation is Vector, Vector(1, 4, 9, 16, 25, 36, 49, 64, 81, 100)
while do-while
- 为了兼容java,不推荐使用,结果类型是
Unit
。 - 不可避免需要声明变量在循环外部,等同于循环内部对外部变量造成了影响,所以不推荐使用。
循环中断:Breaks.break()
- Scala内置控制结构去掉了
break continue
关键字,为了更好适应函数式编程,推荐使用函数式风格解决。 - 使用
breakable
结构来实现break continue
功能。 - 循环守卫可以一定程度上替代
continue
。 - 可以用抛出异常捕获的方式退出循环,替代
break
。
try { for (i <- 0 to 10) { if (i == 3) throw new RuntimeException println(i) }} catch { case e: Exception => // do nothing}
- 可以使用Scala中的
Breaks
类中的break
方法(只是封装了异常捕获),实现异常抛出和捕获。
import scala.util.control.BreaksBreaks.breakable( for (i <- 0 to 10) { if (i == 3) Breaks.break() println(i) })
5. 函数式编程
不同范式对比
- 面向过程:按照步骤解决问题。
- 面向对象:分解对象、行为、属性,通过对象关系以及行为调用解决问题。耦合低,复用性高,可维护性强。
- 函数式编程:面向对象和面向过程都是命令式编程,但是函数式编程不关心具体运行过程,而是关心数据之间的映射。纯粹的函数式编程语言中没有变量,所有量都是常量,计算过程就是不停的表达式求值的过程,每一段程序都有返回值。不关心底层实现,对人来说更好理解,相对地编译器处理就比较复杂。
- 优点:编程效率高,函数式编程的不可变性,对于函数特定输入输出是特定的,与环境上下文等无关。函数式编程无副作用,利于并行处理,所以Scala特别利于应用于大数据处理,比如Spark,Kafka框架。
函数定义
def func(arg1: TypeOfArg1, arg2: ...): RetType = { ...}
- 函数式编程语言中,函数是一等公民(可以像对象一样赋值、作为参数返回值),可以在任何代码块中定义函数。
- 一般将定义在类或对象中(最外层)的函数称为方法,而定义在方法中(内层)的称为函数。广义上都是函数。
- 返回值用
return
返回,不写的话会使用最后一行代码作为返回值。 - 无返回值
Unit
时可以用return
可以用return ()
可以不返回。 - 其他时候只需要返回值是返回值类型的子类对象就行。
函数参数
-
可变参数,类似于Java,使用数组包装。
def f1(str:String*): Unit = {}
。- 如果除了可变参数还有其他参数,需要将可变参数放在末尾。
- 可变参数当做数组来使用。
-
参数默认值:
def f2(name: String = "alice"): Unit = {}
- 和C++一样,默认参数可以不传,默认参数必须全部放在末尾。
-
带名称传参:
- 调用时带名称。
def f3(name: String, age: Int = 20, loc: String = "BeiJing"): Unit = { println(s"name ${name}, age ${age}, location ${loc}")}f3("Bob")f3("Alice", loc = "Xi'An")f3("Michael", 30)
- 不给名称的就是按顺序赋值。
- 调用时带名参数必须位于实参列表末尾。
- 和默认参数一起使用会很方便,比如有多个默认参数,但只想覆盖其中一个。
函数至简原则
-
能省则省。
-
最后一行代码会作为返回值,可以省略
return
。 -
函数体只有一行代码的话,可以省略花括号。
-
如果返回值类型能够自动推断那么可以省略。
-
如果函数体中用
return
做返回,那么返回值类型必须指定。 -
如果声明返回
Unit
,那么函数体中使用return
返回的值也不起作用。 -
如果期望是无返回值类型,那么可以省略
=
。这时候没有返回值,函数也可以叫做过程。【2.13.0已废弃,能编过不过会提示。】 -
无参函数如果声明时没有加
()
,调用时可以省略()
。【如果声明时有()
调用也可以省略,不过2.13.3废弃了。】 -
不关心函数名称时,函数名称和
def
也可以省略,去掉返回值类型,将=
修改为=>
定义为匿名函数。
val fun = (name: String) => { println("name") }
匿名函数
-
没有名称的函数,可以被赋值给一个量。也叫lambda表达式
-
val fun = (name: String) => { println("name") }
-
匿名函数定义时不能有函数的返回值类型。
-
简化原则:
- 参数的类型可以省略,如果可以根据高阶函数形参自动推导。
- 类型省略之后如果只有一个参数,那么可以省略参数列表的
()
,name => println(name)
。 - 匿名函数函数体只要一行,那么
{}
可以省略。 - 如果参数只出现一次,则参数可以省略,后面出现的参数用
_
代替,println(_)
也是一个lambda,表示name => {println(name)}
。 - 如果可以推断出当前传入的
println
是一个函数体,而不是函数调用语句,那么可以省略下划线。也就是省略了转调,直接将函数名称作为参数传递。
def f(func: String => Unit): Unit = { func("alice")}f((name: String) => { println(name) })f((name) => println(name))f(println(_))f(println)
- 样例:极致省略
def dualOp(func: (Int, Int) => Int): Int = { func(1, 2)}println(dualOp( (a: Int, b: Int) => a + b) )println(dualOp( (a: Int, b: Int) => a - b) )println(dualOp( (a, b) => a - b) )println(dualOp( _ + _ )) // a + bprintln(dualOp( -_ + _ )) // -a + b
高阶函数
- 三种形式:函数作为值传递、函数作为参数、函数作为返回值。
- 作为值传递:经过赋值之后在底层变成一个lambda对象。
// define functiondef foo(n: Int): Int = { println("call foo") n + 1}// function assign to value, also a objectval func = foo _ // represent the function foo, not function callval func1: Int => Int = foo // specify the type of func1println(func) // Main$$$Lambda$674/0x000000080103c588@770beef5println(func == func1) // false, not a same object
- 函数作为参数:可以传匿名函数、函数名称、lambda对象。
// function as argumentsdef dualEval(op: (Int, Int) => Int, a: Int, b: Int) = { op(a, b)}def add(a: Int, b: Int): Int = a + bprintln(dualEval(add, 10, 100))val mul:(Int, Int) => Int = _ * _println(dualEval(mul, 10, 100))println(dualEval((a, b) => a + b, 1000, 24))
- 函数作为返回值:
// function as return valuedef outerFunc(): Int => Unit = { def inner(a: Int): Unit = { println(s"call inner with argument ${a}") } inner // return a function}println(outerFunc()(10)) // inner return ()
高阶函数举例
- 使用特定操作处理数组元素,得到新数组。也就是集合处理的map(映射)操作。
// deal with an array, get a new array// map operation of arraydef arrayOp(arr: Array[Int], op: Int => Int): Array[Int] = { for (elem <- arr) yield op(elem) // the whole for expression get a new array}val arr = Array(1, 2, 3, 4)def addOne(elem: Int): Int = elem + 1println(arrayOp(arr, addOne _).mkString(", ")) // pass addOne also workprintln(arrayOp(arr, elem => elem * 2).mkString(", "))println(arrayOp(arr, _ * 3).mkString(", "))
- 套娃
def func(a: Int): String => (Char => Boolean) = { def f1(s: String): Char => Boolean = { def f2(c: Char): Boolean = { if (a == 0 && s == "" && c == '0') false else true } f2 } f1}println(func(0)("")('0')) // falseprintln(func(1)("hello")('c')) // true
- 极致简写:内层函数可以使用外层函数的参数
// simplify to anonymous functiondef func1(a: Int): String => (Char => Boolean) = { s => c => !(a == 0 && s == "" && c == '0')}println(func1(0)("")('0')) // falseprintln(func1(1)("hello")('c')) // true
- 柯里化
// Currying def func2(a: Int)(s: String)(c: Char): Boolean = !(a == 0 && s == "" && c == '0')println(func2(0)("")('0')) // falseprintln(func2(1)("hello")('c')) // true
柯里化和闭包
闭包:如果一个函数,访问到了它的外部(局部)变量的值,那么这个函数和他所处的环境,称为闭包。
- 闭包的定义:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如C++)。
- 因为外层调用结束返回内层函数后,经过堆栈调整(比如在C中主调或者被调清理),外层函数的参数已经被释放了,所以内层是获取不到外层的函数参数的。为了能够将环境(函数中用到的并非该函数参数的变量和他们的值)保存下来(需要考虑释放问题,可以通过GC可以通过对象生命周期控制,GC是一个常见选择),这时会将执行的环境打一个包保存到堆里面。
柯里化:将一个参数列表的多个参数,变成多个参数列表的过程。也就是将普通多参数函数变成高阶函数的过程。
- 定义:
在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。在直觉上,柯里化声称“如果你固定某些参数,你将得到接受余下参数的一个函数”。柯里化是一种处理函数中附有多个参数的方法,并在只允许单一参数的框架中使用这些函数。
- Scala中的柯里化函数定义:
// Currying def add(a: Int)(b: Int): Int = a + bprintln(add(4)(3))val addFour = add(4) _// val addFour: Int => int = add(4)println(addFour(3))
递归
- 方法调用自身。
- 递归要有结束逻辑。
- 调用自身时,传递参数要有规律。
- scala中递归定义函数必须声明返回值类型,因为无法通过推导获得。
- 纯函数式语言比如Haskell,连循环都没有,很多操作都需要通过递归来做,性能比较依赖尾递归优化。
- 尾递归优势:保存结果值,每次递归之前把原有栈清空,再把上一次的计算结果和操作数压入栈
- Scala中的尾递归优化例子:
def factorial(n: Int) : Int = { if (n < 0) return -1 if(n == 0) return 1 factorial(n-1) * n}// tail recusion implementation of factorialdef tailFact(n: Int): Int = { if (n < 0) return -1 @annotation.tailrec def loop(n: Int, curRes: Int): Int = { if (n == 0) return curRes loop(n - 1, curRes * n) } loop(n, 1)}
抽象控制
- 值调用:按值传递参数,计算值后再传递。多数语言中一般函数调用都是这个方式
- 名调用:按名称传递参数,直接用实参替换函数中使用形参的地方。并不是函数调用,预处理时直接替换。
- 例子:
// pass by valuedef f0(a: Int): Unit = { println("a: " + a) println("a: " + a)}f0(10)// pass by name, argument can be a code block that return to Intdef f1(a: => Int): Unit = { println("a: " + a) println("a: " + a)}def f2(): Int = { println("call f2()") 10}f1(10)f1(f2()) // pass by name, just replace a with f2(), then will call f2() twice//每次出现f2都会调用f2f1({ println("code block") // print twice 30})
- 应用:使用传名参数实现一个函数相当于while的功能。
// built-in whilevar n = 10while (n >= 1) { print(s"$n ") n -= 1}println()// application: self-defined while, implement a function just like while keyworddef myWhile(condition: => Boolean): (=> Unit) => Unit = { def doLoop(op: => Unit): Unit = { if (condition) { op myWhile(condition)(op) } } doLoop _}n = 10myWhile (n >= 1) { print(s"$n ") n -= 1}println()// simplfydef myWhile2(condition: => Boolean): (=> Unit) => Unit = { op => { if (condition) { op myWhile2(condition)(op) } }}n = 10myWhile (n >= 1) { print(s"$n ") n -= 1}println()// use curryingdef myWhile3(condition: => Boolean)(op: => Unit): Unit = { if (condition) { op myWhile3(condition)(op) }}n = 10myWhile3 (n >= 1) { print(s"$n ") n -= 1}println()
惰性加载
- 当函数返回值被声明为
lazy
时,函数的执行将会被推迟,知道我们首次对此取值,该函数才会被执行。这种函数成为惰性函数。
def main(args: Array[String]): Unit = { // just like pass by name lazy val result: Int = sum(13, 47) println("before lazy load") println(s"result = ${result}") // first call sum(13, 47) println(s"result = ${result}") // result has been evaluated}def sum(a: Int, b: Int): Int = { println("call sum") a + b}
- 有点像传名参数,但懒加载只是推迟求值到第一次使用时,而不是单纯替换。
6. 包管理
包
-
package name
-
作用:
- 区分相同名字类,避免名称冲突。
- 类很多时,分模块管理。
- 访问权限控制。
-
命名:包名称只能是常规的标识符(字母数字下划线,数字不能开头)。同样
.
作为不同层级分割符,整体作为包名。 -
命名规范:一般情况下按照如下规则命名
com.company.projectname.modulename
,视项目规定而定,只是一个名称而已。 -
Scala中的两种包管理方式:
- 第一种,Java风格,每个源文件声明一个包,写在源文件最上方。但源文件位置不需要和包名目录层级一致,只代表逻辑层级关系,不像Java一样源文件也必须按照包名目录层级关系放置。当然惯例是和Java一样按照包名目录层级来放置。
- 第二种,用
{}
嵌套风格定义包:
package com { // code in com package object Outer { var name = "Outer" } package inner { // code in com.inner package package scala { // code in com.innner.scala package object Inner { def main(args: Array[String]):Unit = { println(Outer.name) Outer.name = "Inner" println(Outer.name) } } } }}
- 嵌套风格好处:
- 一个源文件可以声明多个并列的最顶层的包。
- 子包中的类可以访问父包中的内容,无需导入。但外层是不能直接访问内层的,需要导入。
包对象
- 为Scala包定义一个同名的单例包对象,定义在包对象中的成员,作为其对应包下的所有类和对象的共享变量,可以被直接访问,无需导入。
- 关键字
package object
,需要和包在同一层级下。比如为com.inner
包定义包对象的话,必须在com
包中,定义形式package obejct inner { ... }
。
包的导入
import users._ // 导入包 users 中的所有成员import users.User // 导入类 Userimport users.{User, UserPreferences} // 仅导入选择的成员import users.{UserPreferences => UPrefs} // 导入类并且设置别名import users.{User => _, _} // 导入出User类以外的所有users包中的内容
- 可以在任意位置导入(作用于代码块),可以设置别名,可以选择性导入想要导入的内容,可以屏蔽某个类。
- 所有Scala源文件默认导入:
import java.lang._import scala._import scala.Predef._
7. 面向对象
类
- Java:如果是
public
向外公开的,那么必须和文件名一致,也只能有一个。不写访问修饰符则可以定义多个,包访问权限。 - Scala:没有
public
关键字,默认就是公有,不能加public
,一个文件可以写多个类,不要求和文件名一致。
[descriptor] class classname { // body: fields & methods [descriptor] var/val name: Type = _ [descriptor] method(args: ArgsType): RetType = { // method body }}
- 访问修饰符可以是:
private
protected
private [pacakgeName]
,默认就是公有,不需要加。 - 成员如果需要Java Bean规范的getter和setter的话可以加
@scala.beans.BeanProperty
相当于自动创建,不需要显式写出。 - 成员给初值
_
会赋默认值,Scala中定义变量必须赋值,可以这样做。值类型的值0,引用则是null
。定义常量的话不能用_
,因为只能初始化一次,编译器会提示。
封装
- Java:私有化,提供getter和setter。
- Scala:公有属性,底层实际为
private
,并通过get方法obj.field()
和set方法obj.field_=(value)
对其进行操作。所以Scala不推荐设置为private
。如果需要和其他框架互操作,必须提供Java Bean规范的getter和setter的话可以加@scala.beans.BeanProperty
注解。
访问权限
- Java中
private protected public
和默认包访问权限。 - Scala
- 属性和方法默认公有,并且不提供
public
关键字。 private
私有,类内部和伴生对象内可用。protected
保护权限,Scala中比Java中严格,只有同类、子类可访问,同包无法访问。(Java定义很奇怪)private [pacakgeName]
增加包访问权限,在包内可以访问。
- 属性和方法默认公有,并且不提供
构造器
- 主构造器和辅助构造器
class ClassName [descriptor] [([descriptor][val/var] arg1: Arg1Type, [descriptor][val/var] arg2: ...)] { // main constructor, only one, like record in java // assist constructor def this(argsList1) { this(args) // call main constructor } def this(argsList2) { // overload constrcutor this(argsList1) // can call main constructor or other constructor that call main constructor directly or indirectly }}
- 特点:
- 主构造器写在类定义上,一定是构造时最先被调用的构造器,方法体就是类定义,可以在类中方法定义的同级编写逻辑,都是主构造器一部分,按顺序执行。
- 辅助构造器用
this
定义。- 辅助构造器必须直接或者间接调用主构造器,调用其他构造必须位于第一行。
- 主构造器和辅助构造器是重载的方法,所以参数列表不能一致。
- 可以定义和类名同名方法,就是一个普通方法。
- 主构造器中形参三种形式:不使用任何修饰,
var
修饰,val
修饰。
- 不使用任何修饰那就是一个形参,但此时在类内都可以访问到这个变量。
- 使用
var val
修饰那就是定义为类成员,分别是变量和常量,不需要也不能在类内再定义一个同名字段。调用时传入参数就直接给到该成员,不需要再显式赋值。- 主构造器中的
var val
成员也可以添加访问修饰符。- 不加参数列表相当于为空,
()
可以省略。- 主构造器的访问修饰符添加到参数列表
()
前。- 实践指南:
- 推荐使用Scala风格的主构造器
var val
修饰参数的编写方法,而不要被Java毒害!- 如果需要多种重载的构造器那么就添加新的的辅助构造器。
class Person(private var name: String) { var age: Int = _ println("call main construtor") def this(name: String, age: Int) = { this(name) this.age = age println("call assist constructor 2") println(s"Person: $name $age") } // just a common method, not constructor def Person(): Unit = { println("call Person.Person() method") }}
继承
class ChildClassName[(argList1)] extends BaseClassName[(args)] { body }
- 子类继承父类属性和方法。
- 可以调用父类构造器,但感觉好像很局限,子类中只可能调用到主构造或者辅助构造中的其中一个构造器。那如果父类有多种构造方式,子类想继承也没有办法?只能是其中一种。
多态
- Java中属性静态绑定,根据变量的引用类型确定,方法是动态绑定。
- 但Scala中属性和方法都是动态绑定。就属性而言,其实也不应该在子类和父类中定义同名字段。
- 同Java一样,所有实例方法都是虚方法,都可以被子类覆写。
override
关键字覆写。- Scala中属性(字段)也可以被重写,加
override
关键字。
抽象类
abstract calss ClassName
- 抽象属性:
val/var name: Type
,不给定初始值。 - 抽象方法:
def methodName(): RetType
,只声明不实现。 - 子类如果没有覆写全部父类未定义的属性和方法,那么就必须定义为抽象类。
- 重写非抽象方法属性必须加
override
,重写抽象方法则可以不加override
。 - 子类调用父类中方法使用
super
关键字。 - 子类重写父类抽象属性,父类抽象属性可以用
var
修饰,val var
都可以。因为父类没有实现,需要到子类中来实现。 - 如果是重写非抽象属性,则父类非抽象属性只支持
val
,不支持var
。因为var
修饰为可变量,子类继承后可以直接使用修改,没有必要重写。val
不可变才有必要重写。 - 实践建议是重写就加
override
,都是很自然的东西,理解就好,不必纠结于每一个细节。
匿名子类
- 和Java类似,重写所有抽象字段和方法。
val/var p: baseClass = new baseClass { override ...}
伴生对象(Companion Object)
- 取代
static
语义。 - 编译后其实会生成两个类,伴生对象是伴生类(类名为对应类后加
$
符号)的单例对象。 obejct
,名称和类一致,必须放同一个文件。- 常见用法:构造器私有化,用伴生对象中的工厂方法。和静态工厂方法使用起来也没有什么区别。
- 伴生对象实现
apply
方法后调用时可以省略.apply
,直接使用className(args)
。库中很多这种用法创建实例,是一个语法糖。 - 测试伴生对象时就在该对象内定义
main
函数编译时会出现的奇怪的访问权限问题。可能对包含入口的伴生对象做了特殊处理,具体细节尚不清楚。最好将main
定义在单独的伴生对象内。
特质/特征(Trait)
- 替代java接口的概念。但比接口更为灵活,一种实现多继承的手段。
- 多个类具有相同的特征时,就可以将这个特征提取出来,用继承的方式来复用。
- 用关键字
trait
声明。
trait traitName { ...}
- 引入/混入(mixin)特征:
- 有父类
class extends baseClass with trait1 with trait2 ... {}
- 没有父类
class extends trait1 with trait2 ... {}
- 有父类
- 其中可以定义抽象和非抽象的属性和方法。
- 匿名子类也可以引入特征。
- 特征和基类或者多个特征中重名的属性或方法需要在子类中覆写以解决冲突,最后因为动态绑定,所有使用的地方都是子类的字段或方法。属性的话需要类型一致,不然提示不兼容。方法的话参数列表不一致会视为重载而不是冲突。
- 如果基类和特征中的属性或方法一个是抽象的,一个非抽象,且兼容,那么可以不覆写。很直观,不能冲突不能二义就行。
- 多个特征和基类定义了同名方法的,就需要在子类重写解决冲突。其中可以调用父类和特征的方法,此时
super.methodName
指代按照顺序最后一个拥有该方法定义的特征或基类。也可以用super[baseClassOrTraitName].methodName
直接指代某个基类的方法,注意需要是直接基类,间接基类则不行。 - 基类和特征基本是同等地位。
- 例子:
class Person { val name: String = "Person" var age: Int = 18 def sayHi(): Unit = { println(s"hello from : $name") }}trait Young { // abstract and non-abstract attribute var age: Int val name: String = "young" // method def play(): Unit = { println(s"young people $name is playing") } def dating(): Unit}trait Knowledge { var amount: Int = 0 def increase(): Unit = { amount += 1 }}trait Talent { def increase(): Unit = { println("increase talent") }}class Student extends Person with Young with Knowledge with Talent{ override val name: String = "alice" def dating(): Unit = { println(s"Sutdent $name $age is dating") } def study(): Unit = println(s"Student $name is studying") override def sayHi(): Unit = { super.sayHi() println(s"hello from : student $name") } override def increase(): Unit = { super.increase() // call Talent.increase(), just the last println(s"studnet $name knowledge increase: $amount") }}object Trait { def main(args: Array[String]): Unit = { val s = new Student() s.sayHi() s.increase() s.study() s.increase() s.play() s.increase() s.dating() s.increase() }}
- 特征的继承:
trait childTrait extends baseTrait
- 特征的菱形继承解决方式:转换为线性的继承链条,在前面的成为基类,后面的成为子类。
- 例子:
trait Ball { def describe(): String = "ball"}trait ColorBall extends Ball { var color: String = "red" override def describe(): String = color + "_" + super.describe()}trait CategoryBall extends Ball { var category: String = "foot" override def describe(): String = category + "_" + super.describe()}// equals to MyFootBall -> ColorBall -> CategoryBall -> Ballclass MyFootBall extends CategoryBall with ColorBall { override def describe(): String = super.describe()}object TraitInheritance { def main(args: Array[String]): Unit = { val b = new MyFootBall() println(b.describe()) // red_foot_ball }}
- Scala的单继承多实现,实现体现在特征上。基类主要用于一个对象比较核心比较本质的部分上。
- 继承特征与类的区别:特征构造时不能给参数。其他都是同样的,都可以实现多态。
自身类型
- 可实现依赖注入的功能。
- 一个类或者特征指定了自身类型的话,它的对象和子类对象就会拥有这个自身类型中的所有属性和方法。
- 将一个类或者特征插入到另一个类或者特征中,属性和方法都就像直接复制插入过来一样,能直接使用。但不是继承,不能用多态。
- 语法:在类或特征中:
_: SelfType =>
,其中_
的位置是别名定义,也可以是其他,_
指代this
。插入后就可以用this.xxx
来访问自身类型中的属性和方法了。 - 注入进来的目的是让你能够使用,可见,提前使用应该拥有的属性和方法。最终只要自身类型和注入目标类型同时被继承就能够得到定义了。
- 例子:
class User(val name: String, val password: String)// user database access objecttrait UserDao { // dependency injection from external _: User => // self type // simulate insert data to databse def insert(): Unit = { println(s"insert into db: $name $password") }}// register userclass RegisterUser(name: String, password: String) extends User(name, password) with UserDaoobject SelfType { def main(args: Array[String]): Unit = { val u = new RegisterUser("catholly", "nephren") u.insert() }}
枚举类
- 继承
Enumeration
。 - 用
Value
类型定义枚举值。
object WorkDay extends Enumeration { val MONDAY = Value(1, "Monday") val TUESDAY = Value(2, "Tuesday") val THURSDAy = Value(3, "Thrusday")}object EnumClass { def main(args: Array[String]): Unit = { println(WorkDay.MONDAY) println(WorkDay.TUESDAY) }}
应用类
- 继承
App
,包装了main
方法,就不需要显式定义main
方法了,可以直接执行。
object TestApp extends App { println("hello,world!")}
定义类型别名:
type SelfDefineType = TargetType
。
密封类
sealed
,子类只能定义在同一个文件内。
8. 集合
Scala集合介绍
Java集合:
- 三大类型:列表
List
、集合Set
、映射Map
,有多种不同实现。
Scala集合三大类型:
- 序列
Seq
,集合Set
,映射Map
,所有集合都扩展自Iterable
。 - 对于几乎所有集合类,都同时提供可变和不可变版本。
- 不可变集合:
scala.collection.immutable
- 可变集合:
scala.collection.mutable
- 两个包中可能有同名的类型,需要注意区分是用的可变还是不可变版本,避免冲突和混淆。
- 不可变集合:
- 对于不可变集合,指该集合长度数量不可修改,每次修改(比如增删元素)都会返回一个新的对象,而不会修改源对象。
- 可变集合可以对源对象任意修改,一般也提供不可变集合相同的返回新对象的方法,但也可以用其他方法修改源对象。
- 建议:操作集合时,不可变用操作符,可变用方法。操作符也不一定就会返回新对象,但大多是这样的,还是要具体看。
- Scala中集合类的定义比java要清晰不少。
不可变集合:
scala.collection.immutable
包中不可变集合关系一览:- 不可变集合没有太多好说的,集合和映射的哈希表和二叉树实现是肯定都有的,序列中分为随机访问序列(数组实现)和线性序列(链表实现),基本数据结构都有。
Range
是范围,常用来遍历,有语法糖支持1 to 10 by 2
10 until 1 by -1
其实就是隐式转换加上方法调用。- Scala中的
String
就是java.lang.String
,和集合无直接关系,所以是虚箭头,是通过Perdef
中的低优先级隐式转换来做到的。经过隐式转换为一个包装类型后就可以当做集合了。 Array
和String
类似,在图中漏掉了。- 此类包装为了兼容Java在Scala中非常常见,Scala中很多类型就是对java类型的包装或者仅仅是别名。
- Scala推荐更多地使用不可变集合。能用不可变就用不可变。
可变集合:
scala.collection.mutable
包中可变集合关系一览:- 序列中多了
Buffer
,整体结构差不多。
不可变和可变:
- 不可变指的是对象大小不可变,但是可以修改元素的值(不能修改那创建了也没有用),需要注意这一点。而如果用了
val
不变量存储,那么指向对象的地址也不可变。 - 不可变集合在原集合上个插入删除数据是做不到的,只能返回新的集合。
泛型
- 集合类型大多都是支持泛型,使用泛型的语法是
[Type]
,不同于Java的<Type>
。
数组 Array
不可变数组:
- 访问元素使用
()
运算符,通过apply/update
方法实现,源码中的实现只是抛出错误作为存根方法(stab method),具体逻辑由编译器填充。
// 1. newval arr = new Array[Int](5)// 2. factory method in companion obejctval arr1 = Array[Int](5)val arr2 = Array(0, 1, 3, 4)// 3. traverse, range forfor (i <- 0 until arr.length) arr(i) = ifor (i <- arr.indices) print(s"${arr(i)} ")println()// 4. tarverse, foreachfor (elem <- arr) print(s"$elem ") // elem is a valprintln()// 5. tarverse, use iteratorval iter = arr.iteratorwhile (iter.hasNext) print(s"${iter.next()} ")println()// 6. traverse, use foreach method, pass a functionarr.foreach((elem: Int) => print(s"$elem "))println()println(arr2.mkString(", ")) // to string directly// 7. add element, return a new array, : should toward to objectval newArr = arr :+ 10 // arr.:+(10) add to endprintln(newArr.mkString(", "))val newArr2 = 20 +: 10 +: arr :+ 30 // arr.+:(10).+:(20).:+(30)println(newArr2.mkString(", "))
- 可以看到自定义运算符可以非常灵活,规定如果运算符首尾有
:
那么:
一定要指向对象。- 下标越界当然会抛出异常,使用前应该检查。
- 通过
Predef
中的隐式转换为一个混入了集合相关特征的包装类型从而得以使用Scala的集合相关特征,Array
类型中并没有相关混入。
可变数组:
- 类型
ArrayBuffer
。
// 1. createval arr: ArrayBuffer[Int] = new ArrayBuffer[Int]()val arr1: ArrayBuffer[Int] = ArrayBuffer(10, 20, 30)println(arr.mkString(", "))println(arr1) // call toString ArrayBuffer(10, 20, 30)// 2. visitarr1(2) = 10// 3. add element to tailvar newArr = arr :+ 15 :+ 20 // do not change arrprintln(newArr)newArr = arr += 15 // modify arr itself, add to tail return itself, do not recommand assign to other varprintln(arr)println(newArr == arr) // true// 4. add to head77 +=: arr println(arr)// 5. insert to middlearr.insert(1, 10)println(arr)// 6. remove elementarr.remove(0, 1) // startIndex, countprintln(arr)arr -= 15 // remove specific elementprintln(arr)// 7. convert to Arrayval newImmuArr: Array[Int] = arr.toArrayprintln(newImmuArr.mkString(", "))// 8. Array to ArryBufferval buffer: scala.collection.mutable.Buffer[Int] = newImmuArr.toBufferprintln(buffer)
- 推荐:不可变集合用运算符,可变集合直接调用对应方法。运算符容易迷惑。
- 更多方法查看文档和源码用到去找就行。
- 可变数组和不可变数组可以调用方法互相转换。
二维数组:
- 就是数组的数组。
- 使用
Array.ofDim[Type](firstDim, secondDim, ...)
方法。
// create 2d arrayval arr: Array[Array[Int]] = Array.ofDim[Int](2, 3)arr(0)(1) = 10arr(1)(0) = 100 // traversearr.foreach(v => println(v.mkString(",")))
列表 List
不可变列表:
List
,抽象类,不能直接new
,使用伴生对象apply
传入元素创建。List
本身也有apply
能随机访问(做了优化),但是不能update
更改。foreach
方法遍历。- 支持
+: :+
首尾添加元素。 Nil
空列表,::
添加元素到表头。- 常用
Nil.::(elem)
创建列表,换一种写法就是10 :: 20 :: 30 :: Nil
得到结果List(10, 20, 30)
,糖是真滴多! - 合并两个列表:
list1 ::: list2
或者list1 ++ list2
。
可变列表:
- 可变列表
ListBuffer
,和ArrayBuffer
很像。 final
的,可以直接new
,也可以伴生对象apply
传入元素创建(总体来说scala中更推荐这种方式)。- 方法:
append prepend insert remove
- 添加元素到头或尾:
+=: +=
- 合并:
++
得到新的列表,++=
合并到源上。 - 删除元素也可以用
-=
运算符。 - 具体操作很多,使用时阅读文档即可。
集合 Set
不可变集合:
- 数据无序,不可重复。
- 可变和不可变都叫
Set
,需要做区分。默认Set
定义为immutable.Set
别名。 - 创建时重复数据会被去除,可用来去重。
- 添加元素:
set + elem
- 合并:
set1 ++ set2
- 移除元素:
set - elem
- 不改变源集合。
可变集合:
- 操作基于源集合做更改。
- 为了与不可变集合区分,
import scala.collection.mutable
并用mutable.Set
。 - 不可变集合有的都有。
- 添加元素到源上:
set += elem
add
- 删除元素:
set -= elem
remove
- 合并:
set1 ++= set2
- 都很简单很好理解,多看文档和源码就行。
映射 Map
不可变映射:
Map
默认就是immutable.Map
别名。- 两个泛型类型。
- 基本元素是一个二元组。
// create Mapval map: Map[String, Int] = Map("a" -> 13, "b" -> 20)println(map)// traversemap.foreach((kv: (String, Int)) => println(kv))map.foreach(kv => println(s"${kv._1} : ${kv._2}"))// get keys and valuesfor (key <- map.keys) { println(s"${key} : ${map.get(key)}")}// get value of given keyprintln(map.get("a").get)println(map.getOrElse("c", -1)) // avoid excptionprintln(map("a")) // if no such key will throw exception// mergeval map2 = map ++ Map("e" -> 1024)println(map2)
可变映射:
mutable.Map
- 不可变的都支持。
// create mutable Mapval map: mutable.Map[String, Int] = mutable.Map("a" -> 10, "b" -> 20)// add elementmap.put("c", 30)map += (("d", 40)) // two () represent tuple to avoid ambiguityprintln(map)// remove elementmap.remove("a")map -= "b" // just need keyprintln(map)// modify elementmap.put("c", 100) // call update, add/modifyprintln(map)// merge Mapmap ++= Map("a" -> 10, "b" -> 20, "c" -> 30) // add and will overrideprintln(map)
元组 (x,y)
- 创建元组:
(elem1, elem2, ...)
类型可以不同。 - 最多只能22个元素,从
Tuple1
定义到了Tuple22
。 - 使用
_1 _2 _3 ...
访问。 - 也可以使用
productElement(index)
访问,下标从0开始。 ->
创建二元组。- 遍历:
for(elem <- tuple.productIterator)
- 可以嵌套,元组的元素也可以是元组。
集合通用属性和方法
- 线性序列才有长度
length
、所有集合类型都有大小size
。 - 遍历
for (elem <- collection)
、迭代器for (elem <- collection.iterator)
。 - 生成字符串
toString
mkString
,像Array
这种是隐式转换为Scala集合的,toString
是继承自java.lang.Object
的,需要自行处理。 - 是否包含元素
contains
。
衍生集合的方式
- 获取集合的头元素
head
(元素)和剩下的尾tail
(集合)。 - 集合最后一个元素
last
(元素)和除去最后一个元素的初始数据init
(集合)。 - 反转
reverse
。 - 取前后n个元素
take(n) takeRight(n)
- 去掉前后n个元素
drop(n) dropRight(n)
- 交集
intersect
- 并集
union
,线性序列的话已废弃用concat
连接。 - 差集
diff
,得到属于自己、不属于传入参数的部分。 - 拉链
zip
,得到两个集合对应位置元素组合起来构成二元组的集合,大小不匹配会丢掉其中一个集合不匹配的多余部分。 - 滑窗
sliding(n, step = 1)
,框住特定个数元素,方便移动和操作。得到迭代器,可以用来遍历,每个迭代的元素都是一个n个元素集合。步长大于1的话最后一个窗口元素数量可能个数会少一些。
集合的简单计算操作
- 求和
sum
求乘积product
最小值min
最大值max
maxBy(func)
支持传入一个函数获取元素并返回比较依据的值,比如元组默认就只会判断第一个元素,要根据第二个元素判断就返回第二个元素就行xxx.maxBy(_._2)
。- 排序
sorted
,默认从小到大排序。从大到小排序sorted(Ordering[Int].reverse)
。 - 按元素排序
sortBy(func)
,指定要用来做排序的字段。也可以再传一个隐式参数逆序sortBy(func)(Ordering[Int].reverse)
- 自定义比较器
sortWith(cmp)
,比如按元素升序排列sortWith((a, b) => a < b)
或者sortWith(_ < _)
,按元组元素第二个元素升序sortWith(_._2 > _._2)
。 - 例子:
object Calculations { def main(args: Array[String]): Unit = { // calculations of collections val list = List(1, 4, 5, 10) // sum var sum = 0 for (elem <- list) sum += elem println(sum) println(list.sum) println(list.product) println(list.min) println(list.max) val list2 = List(('a', 1), ('b', 2), ('d', -3)) println(list2.maxBy((tuple: (Char, Int)) => tuple._2)) println(list2.minBy(_._2)) // sort, default is ascending val sortedList = list.sorted println(sortedList) // descending println(list.sorted(Ordering[Int].reverse)) // sortBy println(list2.sortBy(_._2)) // sortWith println(list.sortWith((a, b) => a < b)) println(list2.sortWith(_._2 > _._2)) }}
集合高级计算函数
- 大数据的处理核心就是映射(map)和规约(reduce)。
- 映射操作(广义上的map):
- 过滤:自定义过滤条件,
filter(Elem => Boolean)
- 转化/映射(狭义上的map):自定义映射函数,
map(Elem => NewElem)
- 扁平化(flatten):将集合中集合元素拆开,去掉里层集合,放到外层中来。
flatten
- 扁平化+映射:先映射,再扁平化,
flatMap(Elem => NewElem)
- 分组(group):指定分组规则,
groupBy(Elem => Key)
得到一个Map,key根据传入的函数运用于集合元素得到,value是对应元素的序列。
- 过滤:自定义过滤条件,
- 规约操作(广义的reduce):
- 简化/规约(狭义的reduce):对所有数据做一个处理,规约得到一个结果(比如连加连乘操作)。
reduce((CurRes, NextElem) => NextRes)
,传入函数有两个参数,第一个参数是第一个元素(第一次运算)和上一轮结果(后面的计算),第二个是当前元素,得到本轮结果,最后一轮的结果就是最终结果。reduce
调用reduceLeft
从左往右,也可以reduceRight
从右往左(实际上是递归调用,和一般意义上的从右往左有区别,看下面例子)。 - 折叠(fold):
fold(InitialVal)((CurRes, Elem) => NextRes)
相对于reduce
来说其实就是fold
自己给初值,从第一个开始计算,reduce
用第一个做初值,从第二个元素开始算。fold
调用foldLeft
,从右往左则用foldRight
(翻转之后再foldLeft
)。具体逻辑还得还源码。从右往左都有点绕和难以理解,如果要使用需要特别注意。
- 简化/规约(狭义的reduce):对所有数据做一个处理,规约得到一个结果(比如连加连乘操作)。
- 例子:
object HighLevelCalculations { def main(args: Array[String]): Unit = { val list = List(1, 10, 100, 3, 5, 111) // 1. map functions // filter val evenList = list.filter(_ % 2 == 0) println(evenList) // map println(list.map(_ * 2)) println(list.map(x => x * x)) // flatten val nestedList: List[List[Int]] = List(List(1, 2, 3), List(3, 4, 5), List(10, 100)) val flatList = nestedList(0) ::: nestedList(1) ::: nestedList(2) println(flatList) val flatList2 = nestedList.flatten println(flatList2) // equals to flatList // map and flatten // example: change a string list into a word list val strings: List[String] = List("hello world", "hello scala", "yes no") val splitList: List[Array[String]] = strings.map(_.split(" ")) // divide string to words val flattenList = splitList.flatten println(flattenList) // merge two steps above into one // first map then flatten val flatMapList = strings.flatMap(_.split(" ")) println(flatMapList) // divide elements into groups val groupMap = list.groupBy(_ % 2) // keys: 0 & 1 val groupMap2 = list.groupBy(data => if (data % 2 == 0) "even" else "odd") // keys : "even" & "odd" println(groupMap) println(groupMap2) val worldList = List("China", "America", "Alice", "Curry", "Bob", "Japan") println(worldList.groupBy(_.charAt(0))) // 2. reduce functions // narrowly reduce println(List(1, 2, 3, 4).reduce(_ + _)) // 1+2+3+4 = 10 println(List(1, 2, 3, 4).reduceLeft(_ - _)) // 1-2-3-4 = -8 println(List(1, 2, 3, 4).reduceRight(_ - _)) // 1-(2-(3-4)) = -2, a little confusing // fold println(List(1, 2, 3, 4).fold(0)(_ + _)) // 0+1+2+3+4 = 10 println(List(1, 2, 3, 4).fold(10)(_ + _)) // 10+1+2+3+4 = 20 println(List(1, 2, 3, 4).foldRight(10)(_ - _)) // 1-(2-(3-(4-10))) = 8, a little confusing }}
集合应用案例
- Map的默认合并操作是用后面的同key元素覆盖前面的,如果要定制为累加他们的值可以用
fold
。
// merging two Map will override the value of the same key// custom the merging process instead of just overrideval map1 = Map("a" -> 1, "b" -> 3, "c" -> 4)val map2 = mutable.Map("a" -> 6, "b" -> 2, "c" -> 5, "d" -> 10)val map3 = map1.foldLeft(map2)( (mergedMap, kv) => { mergedMap(kv._1) = mergedMap.getOrElse(kv._1, 0) + kv._2 mergedMap })println(map3) // HashMap(a -> 7, b -> 5, c -> 9, d -> 10)
- 经典案例:WordCount:分词,计数,取排名前三结果。
// count words in string list, and get 3 highest frequency wordsdef wordCount(): Unit = { val stringList: List[String] = List( "hello", "hello world", "hello scala", "hello spark from scala", "hello flink from scala" ) // 1. split val wordList: List[String] = stringList.flatMap(_.split(" ")) println(wordList) // 2. group same words val groupMap: Map[String, List[String]] = wordList.groupBy(word => word) println(groupMap) // 3. get length of the every word, to (word, length) val countMap: Map[String, Int] = groupMap.map(kv => (kv._1, kv._2.length)) // 4. convert map to list, sort and take first 3 val countList: List[(String, Int)] = countMap.toList .sortWith(_._2 > _._2) .take(3) println(countList) // result}
- 单词计数案例扩展,每个字符串都可能出现多次并且已经统计好出现次数,解决方式,先按次数合并之后再按照上述例子处理。
// strings has their frequencydef wordCountAdvanced(): Unit = { val tupleList: List[(String, Int)] = List( ("hello", 1), ("hello world", 2), ("hello scala", 3), ("hello spark from scala", 1), ("hello flink from scala", 2) ) val newStringList: List[String] = tupleList.map( kv => (kv._1.trim + " ") * kv._2 ) // just like wordCount val wordCountList: List[(String, Int)] = newStringList .flatMap(_.split(" ")) .groupBy(word => word) .map(kv => (kv._1, kv._2.length)) .toList .sortWith(_._2 > _._2) .take(3) println(wordCountList) // result}
- 当然这并不高效,更好的方式是利用上已经统计的频率信息。
def wordCountAdvanced2(): Unit = { val tupleList: List[(String, Int)] = List( ("hello", 1), ("hello world", 2), ("hello scala", 3), ("hello spark from scala", 1), ("hello flink from scala", 2) ) // first split based on the input frequency val preCountList: List[(String, Int)] = tupleList.flatMap( tuple => { val strings: Array[String] = tuple._1.split(" ") strings.map(word => (word, tuple._2)) // Array[(String, Int)] } ) // group as words val groupedMap: Map[String, List[(String, Int)]] = preCountList.groupBy(_._1) println(groupedMap) // count frequency of all words val countMap: Map[String, Int] = groupedMap.map( kv => (kv._1, kv._2.map(_._2).sum) ) println(countMap) // to list, sort and take first 3 words val countList = countMap.toList.sortWith(_._2 > _._2).take(3) println(countList)}
队列
- 可变队列
mutable.Queue
- 入队
enqueue(Elem*)
出队Elem = dequeue()
- 不可变队列
immutable.Queue
,使用伴生对象创建,出队入队返回新队列。
并行集合(Parllel Collection)
- 使用并行集合执行时会调用多个线程加速执行。
- 使用集合类前加一个
.par
方法。 - 具体细节待补。
- 依赖
scala.collection.parallel.immutable/mutable
,2.13版本后不再在标准库中提供,需要单独下载,暂未找到编好的jar的下载地址,从源码构造需要sbt,TODO。
9. 模式匹配
match-case
中的模式匹配:
- 用于替代传统C/C++/Java的
switch-case
结构,但补充了更多功能,拥有更强的能力。 - 语法:(Java中现在也支持
=>
的写法了)
value match { case caseVal1 => returnVal1 case caseVal2 => returnVal2 ... case _ => defaultVal}
- 有一个case条件成立才返回,否则继续往下走。
case
匹配中可以添加模式守卫,用条件判断来代替精确匹配。
def abs(num: Int): Int= { num match { case i if i >= 0 => i case i if i < 0 => -i }}
- 模式匹配支持类型:所有类型字面量,包括字符串、字符、数字、布尔值、甚至数组列表等。
- 你甚至可以传入
Any
类型变量,匹配不同类型常量。 - 需要注意默认情况处理,
case _
也需要返回值,如果没有但是又没有匹配到,就抛出运行时错误。默认情况case _
不强制要求通配符(只是在不需要变量的值建议这么做),也可以用case abc
一个变量来接住,可以什么都不做,可以使用它的值。 - 通过指定匹配变量的类型(用特定类型变量接住),可以匹配类型而不匹配值,也可以混用。
- 需要注意类型匹配时由于泛型擦除,可能并不能严格匹配泛型的类型参数,编译器也会报警告。但
Array
是基本数据类型,对应于java的原生数组类型,能够匹配泛型类型参数。
// match typedef describeType(x: Any) = x match { case i: Int => "Int " + i case s: String => "String " + s case list: List[String] => "List " + list case array: Array[Int] => "Array[Int] " + array case a => "Something else " + a }println(describeType(20)) // matchprintln(describeType("hello")) // matchprintln(describeType(List("hi", "hello"))) // matchprintln(describeType(List(20, 30))) // matchprintln(describeType(Array(10, 20))) // matchprintln(describeType(Array("hello", "yes"))) // not matchprintln(describeType((10, 20))) // not match
- 对于数组可以定义多种匹配形式,可以定义模糊的元素类型匹配、元素数量匹配或者精确的某个数组元素值匹配,非常强大。
for (arr <- List( Array(0), Array(1, 0), Array(1, 1, 0), Array(10, 2, 7, 5), Array("hello", 20, 50))) { val result = arr match { case Array(0) => "0" case Array(1, 0) => "Array(1, 0)" case Array(x: Int, y: Int) => s"Array($x, $y)" // Array of two elements case Array(0, _*) => s"an array begin with 0" case Array(x, 1, z) => s"an array with three elements, no.2 is 1" case Array(x:String, _*) => s"array that first element is a string" case _ => "somthing else" } println(result)
List
匹配和Array
差不多,也很灵活。还可用用集合类灵活的运算符来匹配。- 比如使用
::
运算符匹配first :: second :: rest
,将一个列表拆成三份,第一个第二个元素和剩余元素构成的列表。
- 比如使用
- 注意模式匹配不仅可以通过返回值当做表达式来用,也可以仅执行语句类似于传统
switch-case
语句不关心返回值,也可以既执行语句同时也返回。 - 元组匹配:
- 可以匹配n元组、匹配元素类型、匹配元素值。如果只关心某个元素,其他就可以用通配符或变量。
- 元组大小固定,所以不能用
_*
。
变量声明匹配:
- 变量声明也可以是一个模式匹配的过程。
- 元组常用于批量赋值。
val (x, y) = (10, "hello")
val List(first, second, _*) = List(1, 3, 4, 5)
val List(first :: second :: rest) = List(1, 2, 3, 4)
for
推导式中也可以进行模式匹配:
- 元组中取元素时,必须用
_1 _2 ...
,可以用元组赋值将元素赋给变量,更清晰一些。 for ((first, second) <- tupleList)
for ((first, _) <- tupleList)
- 指定特定元素的值,可以实现类似于循环守卫的功能,相当于加一层筛选。比如
for ((10, second) <- tupleList)
- 其他匹配也同样可以用,可以关注数量、值、类型等,相当于做了筛选。
- 元组列表匹配、赋值匹配、
for
循环中匹配非常灵活,灵活运用可以提高代码可读性。
匹配对象:
- 对象内容匹配。
- 直接
match-case
中匹配对应引用变量的话语法是有问题的。编译报错信息提示:不是样例类也没有一个合法的unapply/unapplySeq
成员实现。 - 要匹配对象,需要实现伴生对象
unapply
方法,用来对对象属性进行拆解以做匹配。
样例类:
- 第二种实现对象匹配的方式是样例类。
case class className
定义样例类,会直接将打包apply
和拆包unapply
的方法直接定义好。- 样例类定义中主构造参数列表中的
val
甚至都可以省略,如果是var
的话则不能省略,最好加上的感觉,奇奇怪怪的各种边角简化。
对象匹配和样例类例子:
object MatchObject { def main(args: Array[String]): Unit = { val person = new Person("Alice", 18) val result: String = person match { case Person("Alice", 18) => "Person: Alice, 18" case _ => "something else" } println(result) val s = Student("Alice", 18) val result2: String = s match { case Student("Alice", 18) => "Student: Alice, 18" case _ => "something else" } println(result2) }}class Person(val name: String, val age: Int)object Person { def apply(name: String, age: Int) = new Person(name, age) def unapply(person: Person): Option[(String, Int)] = { if (person == null) { // avoid null reference None } else { Some((person.name, person.age)) } }}case class Student(name: String, age: Int) // name and age are vals
10. 异常处理
Scala异常处理整体上的语法和底层处理细节和java非常类似。
Java异常处理:
- 用
try
语句包围要捕获异常的块,多个不同catch
块用于捕获不同的异常,finally
块中是捕获异常与否都会执行的逻辑。
try { int a = 0; int b = 0; int c = a / b;} catch (ArithmeticException e) { e.printStackTrace();} catch (Exception e) { e.printStackTrace();} finally { System.out.println("finally");}
Scala异常处理:
try
包围要捕获异常的内容,catch
仅仅是关键字,将捕获异常的所有逻辑包围在catch
块中。finally
块和java一样都会执行,一般用于对象的清理工作。- scala中没有编译期异常,所有异常都是运行时处理。
- scala中也是用
throw
关键字抛出异常,所有异常都是Throwable
的子类,throw
表达式是有类型的,就是Nothing
。Nothing
主要用在一个函数总是不能正常工作,总是抛出异常的时候用作返回值类型。 - java中用了
throws
关键字声明此方法可能引发的异常信息,在scala中对应地使用@throws[ExceptionList]
注解来声明,用法差不多。
object Exceptionstest { def main(args: Array[String]): Unit = { // test of exceptions try { val n = 10 / 0 } catch { case e: ArithmeticException => { println(s"ArithmeticException raised.") } case e: Exception => { println("Normal Exceptions raised.") } } finally { println("finally") } }}
11. 隐式转换
编译器做隐式转换的时机:
- 编译器第一次编译失败时,会在当前环境中查找能让代码编译通过的方法,将类型隐式转换,尝试二次编译。
隐式函数
- 函数定义前加上
implicit
声明为隐式转换函数。 - 当编译错误时,编译器会尝试在当前作用域范围查找能调用对应功能的转换规则,这个过程由编译器完成,称之为隐式转换或者自动转换。
// convert Int to MyRichIntimplicit def convert(arg: Int): MyRichInt = { new MyRickInt(arg)}
- 在当前作用域定义时需要在使用前定义才能找到。
object ImplicitConversion { def main(args: Array[String]): Unit = { implicit def convert(num: Int): MyRichInt = new MyRichInt(num) println(12.myMax(15)) // will call convert implicitly }}class MyRichInt(val self: Int) { // self define compare method def myMax(n: Int): Int = if (n < self) self else n def myMin(n: Int): Int = if (n > self) self else n}
隐式参数
- 普通方法或者函数中的参数可以通过
implicit
关键字声明为隐式参数,调用方法时,如果传入了,那么以传入参数为准。如果没有传入,编译器会在当前作用域寻找复合条件的隐式值。例子:集合排序方法的排序规则就是隐式参数。 - 隐式值:
- 同一个作用域,相同类型隐式值只能有一个。
- 编译器按照隐式参数的类型去寻找对应隐式值,与隐式值名称无关。
- 隐式参数优先于默认参数。(也就是说隐式参数和默认参数可以同时存在,加上默认参数之后其实就相当于两个不同优先级的默认参数)
- 隐式参数有一个很淦的点:
- 如果参数列表中只有一个隐式参数,无论这个隐式参数是否提供默认参数,那么如果要用这个隐式参数就应该将调用隐式参数的参数列表连同括号一起省略掉。如果调用时又想加括号可以在函数定义的隐式参数列表前加一个空参数列表
()
,那么()
将使用隐式参数,()()
将使用默认参数(如果有,没有肯定编不过),()(arg)
使用传入参数。 - 也就是说一个隐式参数时通过是否加括号可以区分隐式参数、默认参数、传入参数三种情况。
- 如果参数列表中只有一个隐式参数,无论这个隐式参数是否提供默认参数,那么如果要用这个隐式参数就应该将调用隐式参数的参数列表连同括号一起省略掉。如果调用时又想加括号可以在函数定义的隐式参数列表前加一个空参数列表
- 可以进一步简写隐式参数,在参数列表中直接去掉,在函数中直接使用
implicity[Type]
(Predef
中定义的)。但这时就不能传参数了。
object ImplicitArgments { def main(args: Array[String]): Unit = { implicit val str: String = "Alice from implicit argument" def sayHello()(implicit name: String = "Alice from default argument"): Unit = { println(s"hello $name") } sayHello() // implicit sayHello()() // default sayHello()("Alice from normal argument") // normal def sayHi(implicit name: String = "Alice from default argument"): Unit = { println(s"hi $name") } sayHi // implicit sayHi() // default sayHi("Alice from normal argument") // normal def sayBye() = { println(s"bye ${implicitly[String]}") } sayBye() }}
隐式类:
- scala2.10之后提供了隐式类,使用
implicit
声明为隐式类。将类的构造方法声明为隐式转换函数。 - 也就是说如果编译通不过,就可能将数据直接传给构造转换为对应的类。
- 隐式函数的一个扩展。
- 说明:
- 所带构造参数有且只能有一个。
- 隐式类必须被定义在类或者伴生对象或者包对象中,隐式类不能是顶层的。
- 同一个作用域定义隐式转换函数和隐式类会冲突,定义一个就行。
隐式解析机制的作用域
- 首先在当前代码作用域下查找隐式实体(隐式方法、隐式类、隐式对象)。
- 如果第一条规查找隐式对象失败,会继续在隐式参数的类型的作用域中查找。
- 类型的作用域是指该类型相关联的全部伴生对象以及该类型所在包的包对象。
作用:
- 隐式函数和隐式类可以用于扩充类的功能,常用语比如内建类
Int Double String
这种。 - 隐式参数相当于就是一种更高优先级的默认参数。用于多个函数需要同一个默认参数时,就不用每个函数定义时都写一次默认值了。为了简洁无所不用其极。
12. 泛型
泛型:
[TypeList]
,定义和使用都是。- 常用于集合类型中用于支持不同元素类型。
- 和java一样通过类型擦除/擦拭法来实现。
- 定义时可以用
+-
表示协变和逆变,不加则是不变。
class MyList[+T] {} // 协变class MyList[-T] {} // 逆变class MyList[T] {} // 不变
协变和逆变:
- 比如
Son
和Father
是父子关系,Son
是子类。- 协变(Covariance):
MyList[Son]
是MyList[Father]
的子类,协同变化。 - 逆变(Contravariance):
MyList[Son]
是MyList[Father]
的父类,逆向变化。 - 不变(Invariant):
MyList[Father] MyList[Son]
没有父子关系。
- 协变(Covariance):
- 还需要深入了解。
泛型上下限:
- 泛型上限:
class MyList[T <: Type]
,可以传入Type
自身或者子类。 - 泛型下限:
class MyList[T >: Type]
,可以传入Type
自身或者父类。 - 对传入的泛型进行限定。
上下文限定:
def f[A : B](a: A) = println(a)
等同于def f[A](a: A)(implicit arg: B[A])
- 是将泛型和隐式转换结合的产物,使用上下文限定(前者)后,方法内无法使用隐式参数名调用隐式参数,需要通过
implicitly[Ordering[A]]
获取隐式变量。 - 了解即可,可能基本不会用到。
总结
- 看起来是一门静态类型语言,提供了很其强大的类型推导,可以一定程度上实现隐式静态类型,但写起来如果高度依赖类型推导的话会和动态类型一样简洁,仅需提供少量必须的类型,只是有点牺牲可读性。
- 函数式编程很有趣。
- 语法糖太太太多了,虽然看起来更简洁了,但是读起来不一定更简单,学起来心智负担也更大。
- 运算符非常灵活,目前遇到过的运算符最灵活的语言。
- 并发编程还没有学,TODO。
- Scala语法确实有点太强大了,当然软件工程的东西都是tradeoff,写起来爽用起来复杂学起来难。
Scala是我目前学过的最舒服的语言,很多特点简直太棒了,如果此生只能选一门语言的话,那我可能真会选这门刚学了几天的语言。吸引我的点:
- 函数式编程,和集合的映射推导结合起来很有用。
- 类型推导,像动态语言用起来的感觉,但也有编译期类型检查,再加上隐式类型转换,真我全都要。
- 各种能简则简的语法糖,初看可能很诧异,习惯之后只能说去**的java,简洁而不简单。
- 运算符重载,容易被滥用,但用得好会使代码进一步简化,当然各式各样的运算符会进一步增加读代码的难度。
- 更加纯粹的面向对象,万物皆是对象。