本章重点介绍Scala基础之运算符与操作流程。
与Java不同,Scala的运算符可以进行重载。而选择,循环分支在Scala这里都做出了非常大的改变,比如说:Scala 中并移除了古老的switch分支;for循环可以用来收集元素……等等。
从本章开始,Scala的灵活性和复杂性就开始体现出来了。可能正因为Scala过于灵活,导致它的上手门槛要比 Java 更加困难,愿意拥抱这门语言的开发人员并不多。比如,目前国内绝大部分程序员仍然选择使用Java开发Spark项目。
这印证了”世界上没有免费的午餐”定理:一个灵活的语言必然带来了上手难度的提高。
Scala 运算符
无论是何种编程语言,运算符无非分为三类:算数运算符, 赋值运算符和关系运算符,So does Scala。
% 与 /
笔者有时会搞混这两个符号的区别……%
符号是取余数,/
则是取商(向下取整)。
/
又称之为div操作,%
又称之为mod操作。
println(10 / 3) // == 3
println(10 % 3) // == 1
Scala中没有++和–
Scala中取消了++
和--
运算符,具体原因不明,但至少不用再去思考++i
还是i++
的问题了。
若要实现自增操作,则只能使用复古的+=1
去解决问题:
var i = 3
// wrong : i++
i += 1
注意:虽然Scala中没有++
与--
操作,但是我们可以人为地对这些运算符进行重载操作。有关于重载部分,我们在后续部分会给出。
Scala移除了三目运算符
Scala同样取消了原先的:?
三目运算符,而是改用更加易懂的if-else
分支来表述:
var a = 3
//b = (a > 2) ? 2 : 3
val b = if(a > 2) 2 else 3
换句话说,Scala就是使用if-else
选择分支取代了原本的三目运算符,究其原因是Scala中的if-else
是具备返回值的。因此即使写一个更加复杂的if-else
分支来决定b的赋值完全可行!此时,传统的:?
就不能再达到同样的效果了。
val b = if(a > 2) 2 else if(a ==2) 3 else 4
简单了解Scala的输入语句
如何实现如同C语言的scanf
功能呢?只需要导入scala.io.StdIn
,而后调用StdIn
的readXXX
功能即可。
import scala.io.StdIn
val msg = StdIn.readLine()
//$符号形式打印字符串。
println(s"get message + $msg")
[重要]Scala的控制流程
Scala的选择分支
前文已经介绍过,Scala的if-else
内的每个分支都具备返回值。
当然,这并不意味着我们一定会使用到这个返回值。当你的判断分支不需要用到返回值的时候,则可以忽略掉它。则此时,每个默认分支返回的是一个Unit
。(Unit
在Scala中的作用和void
相当)
当程序运行时发现没有任何一个分支满足条件时,这个if-else
分支本身同样也会返回一个Unit
。
if (a > 3) {
println("a > 3")
} else if (a < 3) {
println("a < 3")
} else {
println("a == 3")
}
if-else
会默认选择每个分支中最后一个具有返回值的语句,也可以使用return
显示声明(但并没有必要)。比如下面的代码块,b
会根据a
与3的比较结果而被赋予不同的值。
由于每个分支内返回的都是Int
类型的值,因此Scala的编译器能自动推断出来b
的类型同样属于Int
。
val b = if (a > 3) {
println("a > 3")
//return 4
4
} else if (a < 3) {
println("a < 3")
//return 2
2
} else {
println("a == 3")
//return 3
3
}
//b可以推断出是int类型。
println(b.getClass)
换句话说,如果每个分支返回的类型不一样,则编译器就无能为力了。它只能认为b是一个Any
类型的某个上转型对象。
val b : Any = if (a > 3) {
println("a > 3")
//return a Int value
4
} else if (a < 3) {
println("a < 3")
//return a String
"2"
} else {
println("a == 3")
//return a Double value
3.00
}
//b可能是属于Any类的Double,Int,或者是String。
println(b.getClass)
Scala取消了“古老的”switch分支
它被Scala被淘汰的原因就和:?
一样:Scala本身有非常健壮与复杂的模式匹配来完美替换了switch
的功能。模式匹配涉及到了相当多的内容,现在介绍它为时尚早。因此笔者选择在靠后的篇章中介绍它。
Scala 循环分支—for循环
总体来说,Scala的for循环在原来的Java for循环基础上做了相当,相当多的改动和包装。本小节会逐一介绍细节。
Scala的for循环直接融合了Java中for-each
增强for循环的思想:你不止可以使用下标去遍历,也可以用元素去遍历。
//我们在后续的篇幅中介绍数组和集合。
val arr: Array[Int] = Array[Int](1,2,3,4,5)
//相当于Java中的for-each循环。
for(i <- arr) println(i)
仅需寥寥一行,就可以打印出这个arr
数组内的所有元素了。其中,i <- arr
代表着:arr
数组内的每一个元素i
。
1. to 与 until
我们可以利用to
和until
直观地表达出from...to(until)...
的涵义,而不再需要i=0;i<(=)...;i...
这种三段式的写法了。
to
和until
的区别就在于:是否包括最后一个元素。放到之前的for循环结构来说,就是判断条件中:<
(until)和<=
(to)的区别。
for(i <- 1 to 10) println(i) //输出 1 到 10,等价于i=1;i<=10;i++
for(i <- 1 until 10) println(i) //输出 1 到9,等价于i=1;i<10;1++
编译器为什么没有高亮显示to和until呢?因为实际上to和until是Scala已经替你实现的方法,它们属于一个RichInt的final class。
2. 根据数组下标迭代元素
在某些情况下,我们希望在遍历的时候顺便知道这个元素处于数组的第几个下标位。在老版本的for循环中,其实我们并不担心这个问题,因为大部分的for循环我们直接就是使用下标去遍历的:
//我们在Java中的做法
int[] arr = {1,2,3,4,5,6};
for(int i = 0 ; i< arr.length; i++)
{
System.out.println(arr[i]);
}
在Scala中,数组提供一个indices
方法来返回一个下标:
//arr.indices获取的是数组的下标,而非元素。
for(index <- arr.indices) println(s"第${index}个位置:${arr(index)}")
indices
本质上是一个对0 until length
的一个包装,其中length
是每个数组的长度。
3. Scala的for循环没有continue
Scala中没有continue,取而代之是循环守卫的概念:即在执行for循环之前,插入if
判断。只有if
后面的表达式全部为真,才执行for循环的结构体,反之不执行。
比如输出0-10之内的偶数:
//输出0-10以内的偶数。
for(i <- 0 to 10 if i % 2 ==0) { println(i)}
循环守卫的引入,使得我们可以使用最精简的语句就可以实现判断 + 循环的功能。
4. Scala的嵌套for循环可以折叠
我们偶尔会使用到嵌套的for循环来解决问题,以冒泡排序为例。
//一个混乱的int数组
int[] arr = {3,7,6,8,1,4,2};
//冒泡排序进行小->大排列
for(int i = 0 ; i< arr.length; i++)
{
for (int j = 0;j<arr.length-i-1; j++)
{
if (arr[j] > arr[j+1]){
int temp = arr[j];
arr[j] =arr[j+1];
arr[j+1] = temp;
}
}
}
//输出
for (int i : arr){ System.out.println(i); }
而实际上只有在最内部的嵌套循环中执行了逻辑。因此在Scala中,我们可以做如下简化:
for (i <- arr.indices; j <- 0 until arr.length - i -1 if arr(j)>arr(j+1)) {
val temp = arr(j)
arr(j) = arr(j+1)
arr(j+1) = temp
}
另外,当for循环的条件语句较多时,全部拥挤在一个()
内可能不利于代码阅读。因此上述的代码还可以改写成如下格式:
for {
i <- arr.indices
j <- 0 until arr.length - i -1
if arr(j)>arr(j+1)
}{
val temp = arr(j)
arr(j) = arr(j+1)
arr(j+1) = temp
}
5. 使用Range类进行跨步长迭代
Scala中没法使用i=0;i<10;i+2
这样的写法进行跨步迭代;因此要使用一个Range类进行辅助构造:
Range的构造函数如下,构造的是一个[start,end)
区间。
Range(start:Int,end:Int,step:Int)
//即通过起始下标,终止下标,步长来生成一个等差为step的数列。注意,生成的序列中不包含end,但是包含start。
尝试生成1到11以内的奇数:
//1,3,5,7,9,但不包含11。
for(i <- Range(1,10,2)){
println(i)
}
Range本质上是属于scala.collection.immutable下的一个不可变集合。
6. 利用yield收集元素
同if-else
分支一样,for
循环分支同样拥有返回值。我们可以使用yield
将这些元素收集起来。
//将每一个元素i装载到list内。
val ints: immutable.IndexedSeq[Int] = for(i <- Range(1,10,3)) yield i
yield
后面实际上会承接一个语句块。同样的,yield
会按照最后一个具有返回值的语句收集元素,并整体返回一个IndexSeq
类型集合。
7. Scala利用抛出异常实现break
Scala不仅抛弃了continue
关键字,break
也被舍弃了。如果要实现中断功能,则需要通过主动抛出异常的方式进行。
在此之前,我们首先需要引入import scala.util.control.Breaks._
。_
表示引入Breaks类的所有内容。
随后,我们在需要可能使用中断的语句块中使用breakable(()=>{...})
包裹起来。然后再需要中断的地方调用break()
语句。其作用相当于break
关键字。
//breakable是一个控制抽象。内部执行一个()=>Unit的函数。
breakable(() => {
var i = 0
while (i < 10) {
if (i == 5) break()
i += 1
}
}
)
这个breakable本质上是一个控制抽象,它接收一个()=>Unit类型的函数,并将这个函数体内的语句分配到一个线程当中去执行。笔者会在后续的 ”Scala之函数高级部分“ 中介绍高阶函数,控制抽象相关的知识点。
Scala保留了while/do-while循环
Scala中有while
/do-while
循环与Java无异,即无法用步长作为终止条件去控制循环分支时,会选择使用while
循环。
但是,Scala的设计者马丁·奥德斯基并不推崇使用while
,而是尽可能地使用for
做循环。原因是:if
分支和for
循环都可以有返回值(for
循环需要yield),而while
循环并没有。因此如果要保存变量,或者判断条件,则不可避免地要用到外部的变量。而马丁认为:内部的代码不应该对外部的内容进行更改。
最明显的例子是:Java 8 中,尝试在一个lambda表达式内去更改外部的值,则会报错。