2018年第42周-scala入门-基本语法

让事情变得更加简单方便, 注意是简单方便, 而事情内在的复杂性并没有降低.

变量定义

变量是一种使用方便的占位符,用于引用计算机内存地址。
Scala有两种变量,valvar。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有的概念,值范围也一样。

数据类型描述
Byte8位有符号补码整数。数值区间为 -128 到 127
Short16位有符号补码整数。数值区间为 -32768 到 32767
Int32位有符号补码整数。数值区间为 -2147483648 到 2147483647
Long4位有符号补码整数。数值区间为 -9223372036854775808 到 9223372036854775807
Float32位IEEE754单精度浮点数
Double64位IEEE754单精度浮点数
Char16位无符号Unicode字符, 区间值为 U+0000 到 U+FFFF
String字符串
Boolean布尔类型
Unit表示无值,和其他语言中void等同。用作不返回任何结果的方法的结果类型。Unit只有一个实例值,写成()。
Nullnull或空引用
NothingNothing类型在Scala的类层级的最低端;它是任何其他类型的子类型。
AnyAny是所有其他类的超类
AnyRefAnyRef类是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的代码,

  1. 从代码上对比, 代码量少很多.
  2. 调用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的思路, 它是一门靠经验积累的语言, 直白的说就是语法少了很多条条框框, 让程序员更自由.

相关推荐