从Java到Scala
元组和多重赋值
Scala的元组,与多重赋值(multiple assignment)结合,可以将返回多个值变成小菜一碟。
元组是一个不可变的对象序列,创建时使用逗号分隔。
Code:
@Testdef tuple: Unit = {var (firstName, lastName, emailAddress) = getPersonInfoprintln(s"person info: $firstName, $lastName, $emailAddress")var info = getPersonInfoprintln(s"person info: ${info._1}, ${info._2}, ${info._3}")}private def getPersonInfo = {("firstName", "lastName", "emailAddress")}
Output:
person info: firstName, lastName, emailAddress
person info: firstName, lastName, emailAddress
灵活的参数和参数值
我们可以设计接受变长参数值的方法。但是,如果我们有多个参数,那么只有最后一个参数可以接受变长参数值。我们可以在最后一个参数类型后面加上星号,以表明该参数(parameter)可以接受可变长度的参数值(argument)。
如果我们有一组值,那么我们更希望直接传递数组。我们可以使用数组展开标记(array explode notation)。
Code:
@Test
def variableLengthParameter: Unit = {
printInts("ps1", 1, 2, 3)
// :_* 可以展开List,匹配变长参数
val list = List(1, 2, 3, 4, 5)
printInts("ps2", list:_*)
}
private def printInts(name : String, ps : Int*): Unit = {
println(s"$name: $ps")
}
Output:
ps1: WrappedArray(1, 2, 3)
ps2: List(1, 2, 3, 4, 5)
隐式参数
函数的定义者首先需要把参数标记为implicit。针对这种场景,Scala要求我们把隐式参数放在一个单独的参数列表而非常规的参数列表中。如果一个参数被定义为implicit,那么就像有默认值的参数,该参数的值传递是可选的。然而,如果没有传值,Scala会在调用的作用域中寻找一个隐式变量。这个隐式变量必须和相应的隐式参数具有相同的类型,因此,在一个作用域中每一种类型都最多只能有一个隐式变量。
Code:
@Test
def implicitParameter: Unit = {
implicit var wifi = "home"
connectToNetwork("Jane")
wifi = "cafe"
connectToNetwork("Frank")
}
private def connectToNetwork(user : String)(implicit wifi : String): Unit = {
println(s"user:$user connect to WIFI $wifi")
}
Output:
user:Jane connect to WIFI home
user:Frank connect to WIFI cafe
字符串插值
在双引号前面的s的意思是s插值器(s-interpolator),它会找到字符串中的表达式,并将其替换成对应的值。
操作符重载
技术上说,Scala没有操作符,所以操作符重载的意思就是重载诸如+、-等符号。在Scala中,这些都是方法名。操作符利用了Scala宽松的方法调用语法——Scala不强制在对象引用和方法名中间使用点号(.)。
这两个特性结合在一起就给人一种操作符重载的幻觉。因此,当调用ref1 + ref2,实际上写的是ref1.+(ref2),是在ref1上面调用+()方法。
Code:
@Test
def operator: Unit = {
Assertions.assertEquals(1 + 1, 1.+(1))
}
处理对象
创建并使用类
Scala将类定义浓缩在了主构造器(primary constructor)上,并提供了一种简明的方式来定义字段和相应的方法。
Code:
@Test
def construct: Unit = {
var car = new Car(2021)
car.drive(100)
println(s"car year:${car.year} miles:${car.miles}")
}
// 不需要单独定义构造函数,声明成员变量year
private class Car(val year : Int) {
private var milesDriven : Int = 0
def miles = milesDriven
def drive(dis : Int): Unit = {
milesDriven += Math.abs(dis)
}
}
Output:
car year:2021 miles:100
单例对象和伴生对象
创建一个单例要使用关键字object而不是class。因为不能实例化一个单例对象,所以不能传递参数给它的构造器。
可以选择将一个单例关联到一个类。这样的单例,其名字和对应类的名字一致,因此被称为伴生对象(companion object)。相应的类被称为伴生类。
类与其伴生对象间没有边界——它们可以相互访问私有字段和方法。
Code:
@Test
def singleObject: Unit = {
// 实际调用了单例对象Car的apply方法,apply方法可以省略。类似构造List,List(1, 2, 3)也是调用了List.apply(1, 2, 3)
var car = Car(2020)
car.drive(200)
println(s"car year:${car.year} miles:${car.miles}")
}
// 单例对象
object Car {
// 创建伴生对象
def apply(year : Int): Car = new Car(year)
}
// 不需要单独定义构造函数,声明成员变量year
class Car(val year : Int) {
private var milesDriven : Int = 0
def miles = milesDriven
def drive(dis : Int): Unit = {
milesDriven += Math.abs(dis)
}
}
Output:
car year:2022 miles:200
包对象
包对象没什么特别的,就是一个单例,只不过它有特殊的名字和语法。它使用相应的包名作为名字,并用单词package标记。
Code:
@Test
def packageObject: Unit = {
var s = "1"
// 单元测试位于com.example包下,可以直接调用包对象com.example中定义的方法
var i = converter(s)
println(s"s class:${s.getClass}, i class:${i.getClass}")
}
Code2:
package com
package object example {
def converter(s : String): Int = {
s.toInt
}
}
Output:
s class:class java.lang.String, i class:int
善用类型
类型推断
类型推断发生在编译时。可以确定的是,在编译代码时类型检查就已经生效了,不会有任何运行时的开销。
基础类型
在Scala中,Any是所有类型的基础类型,Nothing是所有类型的子类型。
Any类型的直接后裔是AnyVal和AnyRef类型。AnyVal是Scala中所有值类型(如Int、Double等)的基础类型,并映射到了Java中的原始类型,而AnyRef是所有引用类型的基础类型。尽管AnyVal没有什么额外的方法[插图],但是AnyRef包含了Java的Object的方法,如notify()、wait()、finalize()等。
Nothing类型在Scala的类型验证机制的支持上意义重大。throw语句的结果类型不能被推断为函数正常返回的类型(比如Int)而被任意处理,因为在任何地方都可能会引发异常。Nothing类型这时候就派上用场了——通过作为所有类型的子类型,它可以替代任何东西。

隐式类型转换
隐式函数
Code:
@Test
def implicitFunction: Unit = {
// 定义隐式函数
implicit def convertInt2DateHelper(offset : Int) : DateHelper = new DateHelper(offset)
val ago = "ago"
val from_now = "from_now"
// 2通过隐式函数转换成了DateHelper类型,等价于new DateHelper(2).days(ago)
2 days ago
2 days from_now
}
class DateHelper(offset : Int) {
def days(when : String) : LocalDate = {
val today = LocalDate.now
when match {
case "ago" => today.minusDays(offset)
case "from_now" => today.plusDays(offset)
case _ => today
}
}
}
隐式类
可以将一个类标记为implicit类。当使用隐式类的时候,Scala设置了一些限制。其中最值得注意的是,它不能是一个独立的类,它必须要在一个单例对象、类或者特质中。
Code:
@Test
def implicitClass: Unit = {
import DateHelperClass._
// 2通过隐式类转换成了DateHelper类型,等价于new DateHelper(2).days(ago)
2 days ago
2 days from_now
}
object DateHelperClass {
val ago = "ago"
val from_now = "from_now"
implicit class DateHelper(offset : Int) {
import java.time.LocalDate
def days(when : String) : LocalDate = {
val today = LocalDate.now
when match {
case "ago" => today.minusDays(offset)
case "from_now" => today.plusDays(offset)
case _ => today
}
}
}
}
值类
隐式转换越多,所创建的短生命周期的垃圾对象也就越多。
Scala的值对象直接解决了这个问题。这些小的垃圾对象将会被消除,编译器将会使用没有中间对象的函数组合来直接编译这些流式方法调用。要创建一个值对象,只需要从AnyVal扩展你的类即可。让我们将DateHelper修改成为一个值类。
一个值类可以给你世界上最好的两样东西:更好的设计以及更富表现力的代码,并且不需要使用显式的对象。
Code:
// 改变隐式类型转换中定义的DateHelper,重写为值类
implicit class DateHelper(offset : Int) extends AnyVal
函数值和闭包
在函数式编程中,函数是一等公民。函数可以作为参数值传入其他函数中,函数的返回值可以是函数,函数甚至可以嵌套函数。这些高阶函数在Scala中被称为函数值(function value)。闭包(closure)是函数值的特殊形式,会捕获或者绑定到在另一个作用域或上下文中定义的变量。
柯里化(currying)
Scala中的柯里化(currying)会把接收多个参数的函数转化为接收多个参数列表的函数。如果你会用同样的一组参数多次调用一个函数,你就能用柯里化去除噪声并使代码更加有趣。
我们来看一下Scala对柯里化做了怎样的支持。编写一个带有多个参数列表,每个参数列表只有一个参数的方法,而不要编写一个带有一个参数列表,含有多个参数的方法;在每个参数列表中,也可以接受多个参数。也就是说,要写成这样deffoo(a: Int)(b: Int)(c:Int) {},而不是def foo(a: Int, b: Int, c: Int) = {}。你可以这样调用,如foo(1)(2)(3)、foo(1){2}{3},甚至可以是foo{1}{2}{3}。
Code:
@Test
def currying: Unit = {
val list = List(1, 2, 3, 4, 5)
var sum = 0
list.foreach(i => {sum += i})
//foldLeft方法的作用就是遍历集合,执行指定函数的逻辑,指定函数是foldLeft方法的一个参数列表,可以写在()中,也可以写在{}中
val sum2 = list.foldLeft(0)((a, b) => a + b)
val sum3 = list.foldLeft(0){(a, b) => a + b}
println(s"sum: $sum, sum2: $sum2, sum3: $sum3")
}
参数的占位符
Scala用下划线(_)这个记号来表示一个函数值的参数。一开始下划线或许会让你觉得很隐晦,你一旦习惯了,就会发现这种写法能让代码变得简洁且容易修改。你可以用这个符号表示一个参数,但只有在你打算在这个函数值中只引用这个参数一次时可以这样做。你可以在一个函数值中多次使用下划线,但每个下划线都表示相继的不同参数。
Code:
@Test
def placeholderForParameters: Unit = {
val list = List(1, 2, 3, 4, 5)
val sum = list.foldLeft(0){_ + _}
println(s"sum: $sum")
}
部分应用函数
调用一个函数,实际上是在一些参数上应用这个函数。如果传递了所有期望的参数,就是对这个函数的完整应用,就能得到这次应用或者调用的结果。然而,如果传递的参数比所要求的参数少,就会得到另外一个函数。这个函数被称为部分应用函数。部分应用函数使绑定部分参数并将剩下的参数留到以后填写变得很方便。
Code:
@Test
def partiallyAppliedFunction: Unit = {
var aFunc = func(1, _, _)
var abFunc = aFunc(2, _)
println(s"aFunc:$aFunc, abFunc:$abFunc")
var abc = abFunc(3)
println(s"abc:$abc")
}
private def func(a : Int, b : Int, c : Int): Unit = {
println(s"a:$a, b:$b, c:$c")
}
Output:
aFunc:com.example.FunctionTest$$Lambda$222/1236107653@5f1247b, abFunc:com.example.FunctionTest$$Lambda$223/1541860271@7c870e27
a:1, b:2, c:3
abc:()
闭包(closure)
代码块绑定或者捕获作用域和参数列表之外的变量,这样的代码块被称之为闭包(closure)。
特质
特质类似于带有部分实现的接口,提供了一种介于单继承和多继承的中间能力,因为可以将它们混入或包含到其他类中。通过这种能力,可以使用横切特性增强类或者实例。
理解特质
如果一个类没有扩展任何其他类,则使用extends关键字来混入特质。
我们可以混入任意数量的特质。如果要混入额外的特质,要使用with关键字。
选择性混入
特质不仅可以混入类中,也可以选择性混入实例中。在创建实例时,只需要使用with关键字对其进行标记即可。
Scala的特质给了开发人员很大的灵活性,可以将某个类的所有实例都看作是某个特质,也可以仅将特定的实例视为某个特质。如果想要将特质应用于已经存在的类、第三方提供的类,甚至是Java类,那么后者会非常有用。
@Test
def selectiveMixing: Unit = {
val list = new util.ArrayList[Int]() with Car
list.drive
}
trait Car {
def drive: Unit = {
println("drive car")
}
}
特质中的方法延迟绑定
Scala在调用super.xxx()方法时做了两件事情。首先,它对该调用执行了延迟绑定。其次,它要求混入了这些特质的类提供一个该方法的具体实现。
Scala巧妙地避免了方法冲突的问题,并将方法调用按照从右到左的顺序进行链接,最后一个混入的特质具有拦截方法调用的最高优先级。
集合
常见的Scala集合
Scala有3种主要的集合类型:
• List——有序的对象集合;
• Set——无序的集合;
• Map——键值对字典。
我们没有使用new关键字来创建Set的实例。类似于X(…)这样的语句,其中X是一个类的名称或者一个实例的引用,将会被看作是X.apply(…)。如果对应的方法存在,Scala会自动调用这个类的伴生对象上的apply()方法。这种隐式调用apply()方法的能力也可以在Map和List上找到。
不可变列表
通过使用head方法,Scala使访问一个列表的第一个元素更加简单快速。使用tail方法,可以访问除第一个元素之外的所有元素。
方法名的第一个字母决定了优先级。最后一个字母也有一个效果——它决定了方法调用的目标。
如果方法名以冒号(:)结尾,那么调用的目标是该操作符后面的实例。
Code:
@Test
def list: Unit = {
var list = List(1, 2, 3)
// 将0前插到list
var list2 = 0 :: list
// 将List(-1, -2, -3)前插到list
var list3 = List(-1, -2, -3) ::: list
println(s"list:$list")
println(s"list2:$list2")
println(s"list3:$list3")
}
Output:
list:List(1, 2, 3)
list2:List(0, 1, 2, 3)
list3:List(-1, -2, -3, 1, 2, 3)
for表达式
for([pattern <- generator; definition]+; filter)
[yield] expression
for表达式接受一个或者多个生成器作为参数,并带有0个或者多个定义以及0个或者多个过滤器。这些都是由分号分隔的。yield关键字是可选的,如果存在,则告诉表达式返回一个值列表而不是一个Unit。
Code:
@Test
def _for: Unit = {
val result = for(i <- 1 to 10) yield i * 2
println(s"result:$result")
val result2 = for(i <- 1 to 10) i * 2
println(s"result2:$result2")
}
Output:
result:Vector(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
result2:()
模式匹配和正则表达式
使用case类进行模式匹配
Code:
@Test
def _match: Unit = {
handleMessage(new Register("hj"))
handleMessage(new Login("hj"))
handleMessage(new Logout("hj"))
handleMessage(new Register("admin"))
handleMessage(new Login("admin"))
handleMessage(new Logout("admin"))
}
def handleMessage(message : Message): Unit = {
message match {
case Register(msg) => println(s"register id:$msg")
case Login("admin") => println(s"admin login")
case Login(msg) => println(s"login id:$msg")
case Logout("admin") => println(s"admin logout")
case Logout(msg) => println(s"Logout id:$msg")
}
}
Output:
register id:hj
login id:hj
Logout id:hj
register id:admin
admin login
admin logout
正则表达式作为提取器
Code:
@Test
def regexpMatch: Unit = {
handleMessageByRegexp("hj hello")
handleMessageByRegexp("admin hello")
}
def handleMessageByRegexp(input : String): Unit = {
val Pattern = """(\w+) (\w+)""".r
input match {
case Pattern("hj", msg) => println(s"hj: $msg")
case Pattern(name, msg) => println(s"name:$name, msg:$msg")
}
}
Output:
hj: hello
name:admin, msg:hello
无处不在的下划线字符
- 作为包引入的通配符。例如,在Scala中import java.util._等同于Java中的import java.util.*。
- 作为元组索引的前缀。对于给定的一个元组val names = (“Tom”, “Jerry”),可以使用names._1和names._2来分别索引这两个值。
- 作为函数值的隐式参数。代码片段list.map { 2 }和list.map { e => e 2 }是等价的。同样,代码片段list.reduce { + _ }和list.reduce { (a, b) => a +b }也是等价的。
- 用于用默认值初始化变量。例如,var min : Int = 将使用0初始化变量min,而var msg : String = 将使用null初始化变量msg。
- 用于在函数名中混合操作符。你应该还记得,在Scala中,操作符被定义为方法。例如,用于将元素前插到一个列表中的::()方法。Scala不允许直接使用字母和数字字符的操作符。例如,foo:是不允许的,但是可以通过使用下划线来绕过这个限制,如foo_:。
- 在进行模式匹配时作为通配符。case 将会匹配任意给定的值,而case :Int将匹配任何整数。此外,case {_*}将会匹配名为people的XML元素,其具有0个或者多个子元素。
- 在处理异常时,在catch代码块中和case联用。
- 作为分解操作的一部分。例如,max(arg: _*)在将数组或者列表参数传递给接受可变长度参数的函数前,将其分解为离散的值。
- 用于部分应用一个函数。例如,在代码片段val square = Math.pow(_: Int,2)中,我们部分应用了pow()方法来创建了一个square()函数。
_符号的目的是为了使代码更加简洁和富有表现力。开发人员应该根据自己的判断来决定何时使用该符号。只在代码真的变得更加简洁的时候才使用它。
惰性求值和并行集合
即时响应性是一项决定任何应用程序成败的关键因素。其他因素,如商业价值、易用性、可用性、成本以及回弹性,也很重要,但是即时响应性是最重要的。
释放变量的惰性
Code:
@Test
def lazyVar: Unit = {
lazy val i = retInt()
println("do something")
// 使用变量i时才计算retInt()
println(s"i: $i")
}
def retInt(): Int = {
println("retInt")
10
}
Output:
do something
retInt
i: 10
释放集合的惰性
Code:
@Test
def lazyCollection: Unit = {
val person = List(("Frank", 20), ("Jane", 22), ("John", 24), ("Will", 26))
println(person.filter{isOlderThan17}.filter{isNameStartsWithJ}.head);
println("========================")
// view()返回集合的惰性视图,减少不必要的计算
println(person.view.filter{isOlderThan17}.filter{isNameStartsWithJ}.head);
}
def isOlderThan17(person : (String , Int)) = {
println(s"isOlderThan17 called for $person")
person._2 > 17
}
def isNameStartsWithJ(person : (String, Int)) = {
println(s"isNameStartsWithJ called for $person")
person._1 startsWith("J")
}
Output:
isOlderThan17 called for (Frank,20)
isOlderThan17 called for (Jane,22)
isOlderThan17 called for (John,24)
isOlderThan17 called for (Will,26)
isNameStartsWithJ called for (Frank,20)
isNameStartsWithJ called for (Jane,22)
isNameStartsWithJ called for (John,24)
isNameStartsWithJ called for (Will,26)
(Jane,22)
========================
isOlderThan17 called for (Frank,20)
isNameStartsWithJ called for (Frank,20)
isOlderThan17 called for (Jane,22)
isNameStartsWithJ called for (Jane,22)
(Jane,22)
并行集合
Code:
@Test
def parallelCollection: Unit = {
val urls = List("https://www.baidu.com", "https://www.google.com", "https://bing.com", "https://spring.io", "https://github.com")
var t1 = System.nanoTime();
var size = urls.map(contentSize).foldLeft(0){_ + _}
var t2 = System.nanoTime()
println(f"[seq]totalSize: $size, time:${(t2 - t1) / 1E9}%.2fs")
// par()将集合转成并行版本
t1 = System.nanoTime();
size = urls.par.map(contentSize).foldLeft(0){_ + _}
t2 = System.nanoTime()
println(f"[par]totalSize: $size, time:${(t2 - t1) / 1E9}%.2fs")
}
def contentSize(url : String) : Int = {
val size = Source.fromURL(url)("UTF-8").mkString.size
println(s"url: $url, size: $size, thread:${Thread.currentThread()}")
size
}
Output:
url: https://www.baidu.com, size: 2349, thread:Thread[Test worker,5,main]
url: https://www.google.com, size: 15923, thread:Thread[Test worker,5,main]
url: https://bing.com, size: 65768, thread:Thread[Test worker,5,main]
url: https://spring.io, size: 51829, thread:Thread[Test worker,5,main]
url: https://github.com, size: 236799, thread:Thread[Test worker,5,main]
[seq]totalSize: 372668, time:1.59s
url: https://www.baidu.com, size: 2349, thread:Thread[scala-execution-context-global-25,5,main]
url: https://www.google.com, size: 15868, thread:Thread[scala-execution-context-global-27,5,main]
url: https://bing.com, size: 65702, thread:Thread[scala-execution-context-global-26,5,main]
url: https://github.com, size: 236799, thread:Thread[scala-execution-context-global-29,5,main]
url: https://spring.io, size: 51829, thread:Thread[scala-execution-context-global-28,5,main]
[par]totalSize: 372547, time:13.99s
