2018年第42周-scala入门-基本语法
变量定义
变量是一种使用方便的占位符,用于引用计算机内存地址。
Scala有两种变量,val和var。val类似于java的final变量。var则为非final变量。
在scala程序中, 通常建议使用val, 也就是常量, 因为类似于spark的大型复杂系统中, 需要大量的网络传输数据, 如果使用var, 可能会担心值被错误的更改.
在Java的大型复杂系统的设计和研发中, 也使用了类似的特性, 我们通常会将传递给其他模块/组件/服务的对象, 设计成不可变类(Immutable Class). 在里面也会使用java的常量定义, 比如final, 阻止变量的值被改变. 从而提高系统的健壮性(robust, 鲁棒性), 和安全性.
简单的说, 就是让事情变得不可能发生, 那这错误就永远不会发生.
声明val变量
声明val变量来存放表达式的计算结果.
例如, val result = 1 + 1
后续这些常量是可以继续使用的, 例如, 2 * result
但是常量声明后, 是无法改变它的值的, 例如, result=1, 会返回error: reassignment to val 的错误信息.
声明var变量
如果要声明值可以改变的引用, 可以使用val变量.
例如, val myresult = 1, myresult =2
类型推断
无论声明val变量, 还是声明var变量. 都可以手动指定类型, 如果不指定的话, scala会自动根据值, 进行类型的推断, 这种称为类型推断(type inference)能力,它能让Scala自动理解你省略了的类型。.
例如, var some = 2.0
例如, val name: String = null
例如, val name: Any = "jc"
第一个会自动判断为浮点, 而第二三个, 变量类型可以定义为值的父类.
数据类型
数据类型,除了Unit、Nothing、Any、AnyRef,其他都是Java有的概念,值范围也一样。
数据类型 | 描述 |
---|---|
Byte | 8位有符号补码整数。数值区间为 -128 到 127 |
Short | 16位有符号补码整数。数值区间为 -32768 到 32767 |
Int | 32位有符号补码整数。数值区间为 -2147483648 到 2147483647 |
Long | 4位有符号补码整数。数值区间为 -9223372036854775808 到 9223372036854775807 |
Float | 32位IEEE754单精度浮点数 |
Double | 64位IEEE754单精度浮点数 |
Char | 16位无符号Unicode字符, 区间值为 U+0000 到 U+FFFF |
String | 字符串 |
Boolean | 布尔类型 |
Unit | 表示无值,和其他语言中void等同。用作不返回任何结果的方法的结果类型。Unit只有一个实例值,写成()。 |
Null | null或空引用 |
Nothing | Nothing类型在Scala的类层级的最低端;它是任何其他类型的子类型。 |
Any | Any是所有其他类的超类 |
AnyRef | AnyRef类是Scala里所有引用类(reference class)的基类 |
类型的加强版类型
scala使用很多加强类给数据类型增加了上百种增强的功能或函数.
例如, String类通过StringOps类型增强了大量的函数, "Hello".intersect("World")
例如, Scala还提供了RichInt, RichDouble, RichChar等类型, RichInt就提供了to函数, 1.to(10), 此处Int先隐式转换为RichInt, 然后再调用其to函数.
基本操作符
scala的算术操作符与Java的算术操作符也没有什么区别, 比如+, -, *, /, %等, 以及&, |, ^, >>, <<等.
但是, 在scala中, 这些操作符其实是数据类型的函数, 比如 1 + 1, 可以写做1.+(1)
例如, 1.to(10), 又可以写做 1 to 10
scala中没有提供++, --操作符,我们只能使用+=和-=, 比如counter=1, counter++ 是错误的, 必须写做counter +=1
控制流语句
if表达式
在scala中, if表达式是有值的, 就是if或者else中最后一行语句返回的值. 简单的理解就是scala不想参数传来传去, 约定由于配置, 所以就直接默认最后一句就是返回值, 在后面的函数也有所体现, 不用return, 直接最后一句就是返回值. 这样确实对比Java来说, 敲键盘的次数少了很多, 临时变量也不需要到处都是.
例如, val age = 30; if(age > 18) 1 else 0
可以将if表达式赋予一个变量, 例如, val isAdult = if(aget > 18) 1 else 0
另外一种写法, var isAdult=-1; if(age>18) isAdult=1 else isAdult = 0, 但是通常使用上一种写法.
还有一种多语句的写法:
if(age>18){ "adult" }else if(age > 12) "teenage" else "children"
if表达式的类型推断
由于if表达式是有值的, 而if和else子句的值类型可能不同, 此时if表达式的值是什么类型呢? scala会自动进行推断, 取两个类型的公共父类型.
例如, if(age > 18) 1 else 0, 表达式的类型是Int, 因为1和0都是Int
例如, if(age > 18) "adult" else 0, 此时if和else的值分别是String和Int, 则表达式的值是Any, Any是String和Int的公共父类型.
如果if后面没有跟else, 则默认else的值是Unit, 也用()表示, 类似于java中的void或者null. 例如, val age = 12; if(age > 18) "adult", 此时就相当于if(age > 18) "adult" else()
语句终结符, 块表达式
默认情况下, scala不需要语句终结符, 默认将每一行作为一个语句
一行放多条语句: 如果一行要放多条语句, 则必须使用语句终结符
例如, 使用分号作为语句终结符, var a,b,c=0; if(a < 10){b = b+1; c=c+1}
通常来说, 对于多行语句, 还是会使用花括号的方式
if(a<\10){ b = b + 1 c = c + 1 }
块表达式: 块表达式, 指的就是{}中的值, 其中可以包含多条语句, 最后一条语句的值就是块表达式的返回值.
例如, var d=if(a<10){b=b+1; c+1}
循环
while do循环
while do循环: scala有while do循环, 基本语义与java相同.
var n = 10 while(n>0){ println(n); n-=1 }
scala没有for循环
scala没有for循环, 只能使用while替代for循环, 或者使用简易版的for语句
简易版for语句(包括n):
var n=10; for(i <- 1 to n) println(i)
或者使用until, 表达式不达到上限, for(i <- 1 until n) println(i), 没执行一次pirntln(i), i会往上+1, 直至n停止(不包括n)
也可以对字符串进行变量, 类似于java的增强for循环, for(c <- "Hello World") print(c)
跳出循环语句
scala没有提供类似于java的break语句
但是可以使用boolean类型变量, return或者Breaks的break函数来替代使用.
import scala.util.control.Breaks._ breakable{ var n = 10 for(c <- "Hello World"){ if(n == 5) break; print(c) n -= 1 } }
高级for循环
多重for循环: 九九乘法
for(i <- 1 to 9; j <- 1 to 9){ if(j==9){ printf("%d * %d = %d", i,j,i*j) println() }else{ printf("%d * %d = %d\t", i,j,i*j) } }
if守卫: 取偶数
for(i <- 1 to 100 if i % 2 ==0 ) println(i)
for推导式: 构造集合
for(i <- 1 to 10) yield i
函数
函数调用与apply()函数
先不看函数是如何定义的, 先使用起函数, 先体会, 后面再去理解函数.
函数调用方式
在scala中, 函数调用也很简单, 例如使用数学的函数:
scala> import scala.math._ import scala.math._ scala> sqrt(2) res0: Double = 1.4142135623730951 scala> pow(2,4) res2: Double = 16.0 scala> min(3,Pi) res4: Double = 3.0
不同的一点是, 如果调用函数时, 不需要传递参数,则scala允许调用函数时省略括号, 例如, "Hello World".distinct
apply函数
scala中的apply函数是非常特殊的一种函数, 在scala的object中, 可以声明apply函数. 而使用"对象名()"的形式, 其实就是"对象名.apply()"的一种缩写. 通常使用这种方式来构造类的对象, 而不是使用"new 类名()"的方式(注意, 这里的对象名和类名我没搞错, 这个是伴生对象的特性, 后面会讲解).
例如, "Hello World"(6), 因为在StringOps类中有def apply(n: Int): Char的函数定义, 所以"Hello World"(6), 实际是"Hello World".apply(6)的缩写.
例如, Array(1,2,3,4), 实际上是用Array object的apply()函数来创建Array类的实例, 也就是一个数组.
定义函数
在scala中定义函数时, 需要定义函数的函数名, 参数, 函数体.
我们的第一个函数如下所示:
def sayHello(name:String, age:Int)={ if(age>=18) { printf("hi %s, you are a big boy\n",name) age } else { printf("hi %s, you are a little boy\n",name) age } } sayHello("jevoncode",29)
scala要求必须给出所有参数的类型,但是不一定给出函数返回值的类型, 只要右侧的函数体中不包含递归的语句, scala就可以自己根据右侧的表达式推断出返回类型.
单行函数
单行的函数: def sayHello(name: String)= print("Hello, "+name)
在代码块中定义函数体
如果函数体中有多行代码, 则可以使用代码块的方式包裹多行代码, 代码块中最后一行的方绘制就是整个函数的返回值. 与Java不同, 不是使用return返回值的.
比如下面的函数, 实现累加的功能:
def sum(n: Int)={ var sum=0; for(i <-1 to n) sum+=i sum }
递归函数
如果在函数体内递归调用函自身, 则必须手动给出函数的返回类型.
例如, 实现经典的斐波那契数列:
1 1 2 3 5 8 13
简单的说斐波那契数列就是一个数是前面两个数值之和的数列.
此函数求第n个(从0开始)斐波那契数列的值
def fab(n:Int): Int={ if(n<=1) 1 else fab(n-2)+fab(n-1) }
默认参数
在scala中, 有时我们调用某些函数时, 不希望给出参数的具体值, 而希望使用参数自身默认的值, 此时就在定义函数时使用默认参数.
def sayHello(firstName: String, middleName: String = "William", lastName: String = "Croft") = firstName + " " + middleName + " " + lastName \\调用方式 scala> sayHello("a") res1: String = a William Croft scala> sayHello("a","b") res2: String = a b Croft scala> sayHello("a","b","c") res3: String = a b c
如果给出的参数不够, 则会从左往右依次应用参数.
Java与scala实现默认参数的区别
public void sayHello(String firstName, String middleName, String lastName){ if(middleName == null) middleName = "William"; if(lastName == null) lastName = "Croft"; System.out.println(firstName + " " + middleName + " " + lastName); }
对比上面的scala的代码,
- 从代码上对比, 代码量少很多.
- 调用Java的sayHello函数需全部字段传入, 如sayHelle(a,null,null), 这就显得有点麻烦
虽然Java有这样的缺点, 但是Java提供了代理模式, 可以通过注解+proxy的方式动态的给参数注入值, 代理模式提供很大的灵活性. 但写代码的便利性还不如scala, 因为无聊使不使用代理, 传参时都得写全.
带名参数
在调用函数时, 也可以不按照函数定义的参数顺序来传递参数, 而是使用带名参数的方式来传递.
sayHello(firstName = "Mick", lastName="Nina", middleName="Jack")
还可以混合使用未命名参数和带名参数, 但是未命名参数必须排在带名参数的前面.
sayHello("Mick", lastName="Nina", middleName="Jack")
变长参数
在scala中, 有时我们需要将函数定义为参数个数可变的形式, 则可以使用变长参数来定义函数.
def sum(nums: Int*)={ var res = 0 for(num <-nums) res+=num res } \\调用方式 scala> sum(1,2,3,4,5,6) res6: Int = 21
使用序列调用变长参数
在如果想要将一个已有的序列直接调用变长参数函数, 则不对的. 比如val s=sum(1 to 5). 此时需要使用scala特殊的语法将参数定义为序列, 让scala解析器能够识别.
val s = sum(1 to 5:_*)
案例: 使用递归函数实现累加
def sum2(nums: Int*): Int={ if(nums.length == 0) 0 else nums.head + sum2(nums.tail:_*) }
过程
在scala中, 定义函数时, 如果函数体直接包含在花括号里面, 而没有使用=链接, 则函数返回值类型就是Unit. 这样的函数就被称为过程. 过程通常用于不需要返回值的函数.
过程还有一种写法, 就是将函数的返回值类型定义为Unit.
def sayHello(name: String) = "Hello, " + name //非过程 def sayHello(name: String) {print("Hello, "+ name); "Hello, " + name} //过程 def sayHello(name: String): Unit = "Hello, " + name //过程
就是概念的定义,暂时还没看到这概念带来思想的升华.
lazy值
在scala中, 提供lazy值的特性, 也就是说, 如果将一个变量声明为lazy, 则有在第一次调用该变量时, 变量对于的表达式才会发生计算.这种特性对于特别耗时的计算操作特别有用, 比如打开文件进行IO, 进行网络IO等.
import scala.io.Source._ lazy val lines = fromFile("/home/gucci/Desktop/helloworld.txt").mkString
即使文件不存在, 也不会报错, 只有第一个使用变量时会报错, 证明了表达式计算的lazy特性.
scala> val lines2 = fromFile("/home/gucci/Desktop/helloworld2.txt").mkString java.io.FileNotFoundException: /home/gucci/Desktop/helloworld2.txt (No such file or directory) at java.io.FileInputStream.open0(Native Method) at java.io.FileInputStream.open(FileInputStream.java:195) at java.io.FileInputStream.<init>(FileInputStream.java:138) at scala.io.Source$.fromFile(Source.scala:91) at scala.io.Source$.fromFile(Source.scala:76) at scala.io.Source$.fromFile(Source.scala:54) ... 36 elided scala> lazy val lines = fromFile("/home/gucci/Desktop/helloworld2.txt").mkString lines: String = <lazy> scala> lines java.io.FileNotFoundException: /home/gucci/Desktop/helloworld2.txt (No such file or directory) at java.io.FileInputStream.open0(Native Method) at java.io.FileInputStream.open(FileInputStream.java:195) at java.io.FileInputStream.<init>(FileInputStream.java:138) at scala.io.Source$.fromFile(Source.scala:91) at scala.io.Source$.fromFile(Source.scala:76) at scala.io.Source$.fromFile(Source.scala:54) at .lines$lzycompute(<console>:14) at .lines(<console>:14) ... 36 elided
异常
在scala中, 异常处理和捕获机制与Jav是非常相似的.
try{ throw new IllegalArgumentException("x should not be negative") }catch{ case _:IllegalArgumentException => println("Illegal Argument!") }finally{ print("release resource!") }
除了异常捕获, 这里还有模式匹配和匿名函数知识点, 后面高级语法会讲到.
数据结构
Array
在scala中, Array代表的含义与Java中类似, 也是长度不可改变的数据. 此外, 由于scala与Java都是运行在JVM中, 双方可以互相调用, 因此scala数组的底层实际上是Java数组. 例如字符串数组的底层就是Java的String[], 整数数组底层就是Java的Integer[]
数组初始化后, 长度就固定下来了, 而且元素全部根据其类型初始化. Int就是0, String就是null
val a = new Array[Int](10) val a = new Array[String](10)
可以直接使用Array()创建数组, 元素类型自动推断
val a = Array("hello", "world") a(0) = "hi"
ArrayBuffer
在Scala中, 如果需要类似于Java的ArrayList这种长度可变的集合类, 则可以使用ArrayBuffer
// 如果不想每次都是用全限定名, 则可以预先导入ArrayBuffer类 import scala.collection.mutable.ArrayBuffer //使用ArrayBuffer()的方式可以创建一个空的ArrayBuffer val b = ArrayBuffer[Int]() //使用+=操作符, 可以添加一个元素, 或者多个元素 b+=1 b+=(2,3,4,5) //使用++=操作符, 可以添加其他集合中的所有元素 b++=Array(6,7,8,9,10) //使用trimEnd()函数, 可以从尾部截断指定个数的元素 b.trimEnd(5) //使用insert()函数可以在指定位置插入元素 //但这种操作效率很低, 因为需要移动指定位置后的所有元素 b.insert(5,6) b.insert(6,7,8,9,10) //使用remove()函数可以移除指定位置的元素 b.remove(1) b.remove(1,3) //Array与ArrayBuffer可以互相进行转换 b.toArray a.toBuffer
遍历Array和ArrayBuffer
//使用for循环和until遍历Array/ArrayBuffer //使用until是RichInt提供的函数 for(i <- 0 until b.length) println(b(i)) //跳跃遍历Array/ArrayBuffer for(i <- 0 until (b.length,2)) println(b(i)) //从尾部遍历Array/ArrayBuffer for(i <-(0 until b.length).reverse) println(b(i)) //使用"增强for循环"遍历Array/ArrayBuffer for(e <- b) println(e)
数组常见操作
//元素求和 val a = Array(1,2,3,4,5) val sum = a.sum //获取数组最大值 val max = a.max //对数组进行排序 scala.util.Sorting.quckSort(a) //获取数组中所有元素内容 a.mkString a.mkString(",") a.mkString("<",",",">") //toString函数 a.toString b.toString
使用yield和函数式编程(初体验, 暂不需要理解)转换数组
//对Array进行转换, 获取的还是Array val a = Array(1,2,3,4,5) val a2 = for(ele <- a) yield ele * ele //对ArrayBuffer进行转换, 获取的还是ArrayBuffer val b = ArrayBuffer[Int]() b+=(1,2,3,4,5) val b2 = for(ele<-b) yield ele* ele //结合if守卫, 仅转换需要的元素 val a3 = for(ele <-b if ele % 2 == 0) yield ele * ele //使用函数式编程转换数组(通常使用是一种方式) a.filter(_%2==0).map(2*_) a.filter{_%2==0}map{2*_}
算法案例: 移除第一个负数之后的所有负数
// 构建数组 val a = ArrayBuffer[Int]() a += (1,2,3,4,5,-1,-3,-5,-9) //每发现一个负数(不包括第一个负数), 进行移除, 但这个性能比较差, 需多次移动数组 var isFoundFirstNegative = false var arrayLength = a.length var index = 0 while(index < arrayLength){ if(a(index)>0){ index+=1 }else{ if(!isFoundFirstNegative){isFoundFirstNegative = true; index+=1} else{ a.remove(index); arrayLength-=1} } }
算法案例: 移除第一个负数之后的所有负数(改良版)
// 构建数组 val a = ArrayBuffer[Int]() a += (1,2,3,4,5,-1,-3,-5,-9) //记录所有不需要移除的元素的所有, 稍后一次性移除所有需要移动的元素 //性能比较高, 数组内的元素迁移只需要执行一次即可 var isFoundFirstNegative = false val keepIndexes = for(i<-0 until a.length if !isFoundFirstNegative || a(i) >=0) yield{ if(a(i) < 0) isFoundFirstNegative = true i } for(i <-0 until keepIndexes.length) {a(i) = a(keepIndexes(i))} a.trimEnd(a.length - keepIndexes.length)
创建Map
//创建一个不可变的Map val ages = Map("Jevoncode"->29, "Jen"->25, "Jack"->23) ages("Jevoncode") = 30 //出错value update is not a member of scala.collection.immutable.Map[String,Int] //创建一个可变的Map val ages = scala.collection.mutable.Map("Jevoncode"->29, "Jen"->25, "Jack"->23) ages("Jevoncode") = 30 //使用另外一个种方式定义Map元素 val ages = Map(("Jevoncode",29),("Jen",25),("Jack",23)) //创建一个空的HashMap val ages = new scala.collection.mutable.HashMap[String,Int] //添加元素 scala> ages += "jevoncode" ->30 res6: ages.type = Map(jevoncode -> 30) scala> ages res7: scala.collection.mutable.HashMap[String,Int] = Map(jevoncode -> 30) scala> ages += "jevoncode2" ->29 res8: ages.type = Map(jevoncode2 -> 29, jevoncode -> 30)
访问Map的元素
//获取指定key对应的value, 如果key不存在, 会报错 scala> val jcAge = ages("jc") java.util.NoSuchElementException: key not found: jc at scala.collection.MapLike.default(MapLike.scala:232) at scala.collection.MapLike.default$(MapLike.scala:231) at scala.collection.AbstractMap.default(Map.scala:59) at scala.collection.mutable.HashMap.apply(HashMap.scala:65) ... 36 elided scala> val jcAge = ages("jevoncode") jcAge: Int = 30 //使用container函数检查key是否存在 val jcAge = if(ages.contains("jc")) ages("jc") else 0 //getOrElse函数 val jcAge = ages.getOrElse("jc",0)
修改Map的元素
//更新Map的元素 ages("jevoncode") = 29 //增加多一个元素 ages += ("Mike"->35, "Tom"->40) //移除元素 ages -="Mike" //更新不可变的Map val ages2 = ages + ("Mike"->36, "Tom"->41) //移除不可变Map的元素 val ages3 = ages-"Tom"
遍历Map
// 遍历map的entrySet for((key,value) <- ages) println(key + " " +value) // 遍历map的key for(key <- ages.keySet) println(key) // 遍历map的value for(value <- ages.values) println(value) //生成新map, 反转key和value for((key,value) <- ages) yield(value,key)
SortedMap和LinkedHashMap
//SortedMap可以自动对Map的key的排序 val ages = scala.collection.immutable.SortedMap("jevoncode"->29, "alice"->15, "jen"->25) //LinkedHashMap可以记住插入entry的顺序 val ages = new scala.collection.mutable.LinkedHashMap[String,Int] ages("jevoncode")=30 ages("alice")=15 ages("jen")=25
元组Tuple
//简单Tuple val t=("jevoncode",29) //访问Tuple t._1 //zip操作, zip有拉链的意思 val names = Array("jevoncode","jack","mike") val ages = Array(29,24,26) val nameAges = names.zip(ages) for((name,age) <- nameAges) println(name + ": "+age)
以上
点下最先开头的那句话:
让事情变得更加简单方便, 注意是简单方便, 而事情内在的复杂性并没有降低.我个人体会就是scala把java一些繁琐的东西给简化, 还有常用的功能也写进去. 如 1 to 10数列, 元组Tuple, 还有后续的可直接定义object, extends"接口"等等.
还有就是让语言更加语义化, 这或许对熟悉英语的人才更加有体会吧, 如:
var n=10; for(i <- 1 to n) println(i)
如多个"接口", 用with链接.
最后引用知乎上《Scala 是一门怎样的语言,具有哪些优缺点?》那几段话
Java的模块化,给企业、大公司带来了第一道曙光,模块化之后,这些公司不再给程序员一整个任务,而是一大块任务的一小块。接口一定义,虚拟类一定义,换谁上都可以,管你是保罗·格雷厄姆这样的明星程序员,还是一个新来的大学生,程序员不听话就直接开除,反正模块化之后,开除程序员的成本大大降低,这也是为什么谷歌、甲骨文(这货最后收购了Java)一类的公司大规模的推崇Java,还一度提出了模块化人事管理的理念(把人当模块化的积木一样随时移进移出)。过度企业化后,这延展出了Java的第二个特性,束缚手脚。保罗·格雷厄姆在《黑客与画家》中写道,Java属于B&D(捆绑与束缚)类型的语言。为何束缚手脚?因为要让新手和明星程序员写出类似质量的代码,尽可能的抹消人的才华对程序的影响。不同于C/C++,老手和新手写出的Java代码不会有上百倍的耗时差距。但同样也导致了Java的一个弱点——不容易优化。很多优化Java代码的程序员必须要对JVM(虚拟机)进行优化,实际上增大了很多任务难度。
Scala不把程序员当傻子。
在这就不评判这几段话观点是否政治正确, 因为不同的立场, 问题的答案就有不同. 但我想表达是, 这几段话给出了学习scala的思路, 它是一门靠经验积累的语言, 直白的说就是语法少了很多条条框框, 让程序员更自由.