工具篇-Scala基础

1. Scala开发为什么需要Java环境?

Scala需要Java虚拟机把程序编译成字节码运行。这里提一下Scala中集合和Java中集合相互转换的办法,即

import scala.collection.JavaConversions._,主要方法有:

2. Scala中所有类的超类及空返回值

Java中所有类的超类是Object,Scala中所有类的超类是Any;

Java中的空返回值为Void,Scala中的空返回值是Unit。另外Scala还有Null,null,Nil,Nothing,None,Unit几种空,具体讲解可以参见:https://www.cnblogs.com/moonandstar08/p/5759137.html

3. Scala中的to和util

相同点:Scala中to和util都返回Range类型

不同点:返回的数据范围不同,例如:

1 val to1 = 1 to 10
2 val to2 = 1 to 10 by 2
3 val until1 = 1 until 10
4 val until2 = 1 until 10 by 2
5 println(to1)
6 println(to2)
7 println(until1)
8 println(until2)

输出:

1 Range(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
2 Range(1, 3, 5, 7, 9)
3 Range(1, 2, 3, 4, 5, 6, 7, 8, 9)
4 Range(1, 3, 5, 7, 9)

4. Scala中的Val和Var声明的变量

Scala的类文件中不写set和get方法,val声明的变量为不可变变量,默认只有get方法,var声明的是可变变量,默认有set和get方法。

5. Scala中的方法和函数

Scala的方法跟Java中类似,它是类的一部分,比如Scala中的操作符(+,-等)都是方法,方法通常用def定义,定义格式如下:

1 def methodName ([参数1:参数类型1,参数2:参数类型2,...]) : [return type] = {
2    method body
3    return [expr]
4 }

其中,方法可以没有参数,返回值也可以是空Unit,类似于Java中的Void;

而函数是一个继承了Trait类的对象,函数必须有参数,函数通常用val定义,定义格式如下:

1 val functionName = (参数1:参数类型1,参数2:参数类型2,...) => functionBody

另外,方法可以转为函数:

1 f1 = m1 _ 

6. 匿名函数

什么是:一个函数只使用一次,并没有多次调用,即没有名字的话就是匿名函数。

应用场景:

  • 赋给变量;
  • 作为参数传给函数。

举例:

1 //匿名函数的使用场景1,作为参数,传入给高阶函数
2 Array(3,2,1,5).map{(x: Int) => x + 2}
3 
4 //匿名函数的使用场景2,直接赋值给变量
5 val kafkaName = (name: String) => println("--kafka-->" + name)

7. 高阶函数

使用函数值作为参数,或者返回值为函数值的“函数”和“方法”,均称之为“高阶函数”。例如Scala集合类(collections)的高阶函数map:

 1 (1 to 9).map("^" * _).foreach(println _)
 2 /**打印三角形
 3 ^
 4 ^^
 5 ^^^
 6 ^^^^
 7 ^^^^^
 8 ^^^^^^
 9 ^^^^^^^
10 ^^^^^^^^
11 ^^^^^^^^^
12 */

返回值为函数值的例子可以参考:高阶函数

8. 柯里化

官方定义:Currying指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数为参数的函数。这里的柯里化函数实际上也是一个高阶函数。

例如对于原始函数(不知道为啥叫函数)

1 def add(x:Int,y:Int)=x+y

有变形

1 def add(x:Int)(y:Int) = x + y

实际上给定一个参数 x,返回一个函数类型的值,第二次使用参数y调用这个函数类型的值。

1 def add(x:Int)=(y:Int)=>x+y

这样我们就可以定义多个函数,例如给定x=1

1 scala> val second=add(1)
2 second: Int => Int = <function1>
3 
4 scala> second(2)
5 res1: Int = 3

再比如给定x=2

1  scala> val third=add(2)
2  third: Int => Int = <function1>
3  
4  scala> third(2)
5  ress: Int = 4

9. Scala中的for循环和yield

for循环和yield使用的原理是:

  • 每一次for循环,yield 会产生一个值记录下来 (内部实现上,像是一个缓冲区),当循环结束后, 会返回所有yield值组成的集合;
  • 返回集合的类型与被遍历的集合类型是一致的。

例1:

1 scala> for (e <- a) yield e * 2
2 res6: Array[Int] = Array(2, 4, 6, 8, 10)

当然,也可以在for循环中加上'if'表达式,如例2:

1 scala> val a = Array(1, 2, 3, 4, 5)
2 a: Array[Int] = Array(1, 2, 3, 4, 5)
3  
4 scala> for (e <- a if e > 2) yield e
5 res1: Array[Int] = Array(3, 4, 5)

10. Scala中偏函数

偏函数只接受输入参数的子集,例如下面collect函数的例子,Int=>41 / i这个匿名函数只针对非零输入41才有定义,而对0没有定义。

1 scala> List(41, 0) collect { case i: Int ⇒ 41 / i }
2 res1: List[Int] = List(1)

其实偏函数可以显式地定义为:

1 val fraction = new PartialFunction[Int, Int] {
2     def apply(d: Int) = 42 / d
3     def isDefinedAt(d: Int) = d != 0
4 }

isDefinedAt方法会自动过滤传入参数,大体是这么个意思,具体可参考:https://www.cnblogs.com/daoyou/articles/3894182.html

11. 单例对象

如果某个方法或域与某个类的特定对象无关,而是只与类相关,把它归于单例对象singleton objects,即由object关键字声明的类型:

1 object X {
2     // 相当于Java中静态代码块
3     val n = 2;
4     // 相当于Java中静态方法
5     def f = n * 10
6 }    

因为它是单例对象,所以在使用的时候,不需要New,直接调X.f即可得到返回值20,对Java程序猿来说,这就是static的作用,在Scala中,所有原本Java中用static修饰的属性和方法或者代码块都应该移到单例对象里, 所以scala创建一个单例对象(包含main方法)来为程序的执行提供入口点, 如果单例对象继承App,App内部已经有一个main方法,因此不用main也可以执行:

1 object ObjectDemo extends App {
2     println("hello")
3 }

单例对象可以扩展类和接口。单例对象有些时候是作为伴随对象companion object出现的,即它是另一个同名类的伴随对象

12. 伴生对象

当一个单例对象与类具有相同的名称时,这个单例对象被称作是这个类的伴生对象,同样,这个类被称作是这个单例对象的伴生类
类和它的伴生对象可以互相访问私有特性,类和它的伴生对象必须定义在同一个源文件中,如果Scala中不用New直接调用,一般是调用了类的伴生对象,经典Demo如下:

 1 /**
 2   * Created by root
 3   * Description : 伴生对象和伴生类
 4   *
 5   * 伴生类ObjectAndClassTest的构造函数定义为private,虽然这不是必须的,却可以有效防止外部实例化ObjectAndClassTest类,使得ObjectAndClassTest类只能供对应伴生对象使用;
 6   * 每个类都可以有伴生对象,伴生类与伴生对象写在同一个文件中;
 7   * 在伴生类中,可以访问伴生对象的private字段ObjectAndClassTest.specialty
 8   * 而在伴生对象中,也可以访问伴生类的private方法:objectAndClassTest.getSpecialtyName()
 9   * 最后,在外部不用实例化,直接通过伴生对象访问方法:ObjectAndClassTest.printSpectalty()
10   *
11   */
12 class  ObjectAndClassTest private(val name:String) {
13 
14   private def getSpecialtyName() = "name = " + name + ";specialty = " + ObjectAndClassTest.specialty
15 }
16 
17 object ObjectAndClassTest{
18     private val specialty = "software development"
19     private val objectAndClassTest = new ObjectAndClassTest("xiaoming")
20     def printSpectalty() = println(objectAndClassTest.getSpecialtyName())
21 
22 }
23 
24 
25 object Test{
26   def main(args: Array[String]): Unit = {
27     ObjectAndClassTest.printSpectalty()
28   }
29 }

另外,当一个单例对象没有与类具有相同的名称,这个单例对象被称作是独立对象
独立对象一般作为Scala应用的入口点,或相关功能方法的工具类。

13. Apply方法

通常,在一个类的伴生对象中定义Apply方法,在生成这个类的对象时,不用New关键字默认调用Apply方法,比如Map、Array等集合类。Apply方法可以理解为注入,在构造类的时候调用,主要用来解决复杂对象的初始化问题,Scala的Apply有两种方式,一种是伴生类的Apply,另一种是伴生对象的Apply。示例参考Scala基础-对象:单例对象、伴生对象、apply方法,如下:

 1 class ApplyTest {
 2   def apply() = println("这是伴生类的apply方法")
 3 }
 4 object ApplyTest {
 5   def apply() = {
 6     println("这是伴生对象的apply方法")
 7      new ApplyTest
 8   }
 9 }
10 //  独立对象,作为程序入口
11 object ApplyInit{
12   def main(args: Array[String]): Unit = {
13     //  通过伴生对象的apply方法,进行实例化
14     val applyTest=ApplyTest()
15     //  这是伴生类的apply方法
16     applyTest()
17   }
18 }

结果:

1 这是伴生对象的apply方法
2 这是伴生类的apply方法

14. Unapply方法

unapply方法的入参对应你想要进行匹配的对象,出参则是解构后的元素。比如,求一个数的开方根

1 object Square {
2   def unapply(z: Double): Option[Double] = Some(math.sqrt(z))
3 }

我们不用显式的调用,在match case中使用时编辑器会帮我们调用

1 val number: Double = 36.0
2 number match {
3   case Square(n) => println(s"square root of $number is $n")
4   case _ => println("nothing matched")
5 }

上边的大致步骤是

1、调用unapply,传入number

2、接收返回值并判断返回值是None,还是Some

3、如果是Some,则将其解开,并将其中的值赋值给n(就是case Square(n)中的n)

当然,Unapplyseq和Unapply差不多,也减少了我们Java编程里必须写的代码,定义如下

1 object Names {
2   def unapplySeq(str: String): Option[Seq[String]] = {
3     if (str.contains(",")) Some(str.split(","))
4     else None
5   }
6 }

15. Lazy关键字

特性:一个变量声明为lazy,则只有第一次调用时才会进行初始化,之后再也不会被计算,因此lazy修饰的变量必须同时是val修饰的不可变变量;

使用场景:

  • 某些变量初始化比较耗时,则可以使用lazy;如网络IO,磁盘IO等,marathon源码与spark源码中大量使用了这种特性;
  • 构造顺序问题,比如:
1 trait Person{
2   val name: String
3   val nameLen = name.length
4 }
5 
6 class Student extends Person{
7   override val name: String = "Tom"
8 }

在main函数中new一个Student,会报空指针异常,这是因为父类的constructor先与子类执行,那么在执行val nameLen = name.length的时候name还没有被赋值,所以才会导致空指针。解决办法就是在nameLen前加lazy,延迟初始化。

1 lazy val nameLen = name.length

原理:Scala也是编译成字节码跑在Jvm上的,而Jvm的字节码指令并没有提供对lazy这种语义的支持,所以推断lazy只是一个语法糖,就是编译器在编译期将变量的初始化过程替换为Double Check Lock,类似Java中的懒汉式单例模式初始化。

16. case class

Scala中的case class和普通的class的区别是:

  • 初始化的时候可以不用new,普通类一定需要加new;
  • toString的实现更漂亮;
  • 默认实现了equals 和hashCode
  • 默认是可序列化,也就是实现了Serializable ;
  • case class构造函数的参数是public,我们可以直接访问类的成员;
  • 支持模式匹配;这是定义case class的唯一理由。

17. match case(模式匹配)

Scala的match case和Java中switch case有区别,Scala中模式匹配可以匹配值、集合、类型以及有值和没值,另外每个分支不需要break,自动退出。

  • 变量值的匹配,比如字符串匹配
1 变量 match{
2       case "aa" => println("0")
3       case "bb" => println("1")
4       case "cc" => println("2")
5       case _ => println("null")  //表示所有的都没有匹配到执行这里
6 }
  • 增加if守卫,进行双重过滤
1 变量1 match{
2        case "aa" => println("0")
3        case "bb" => println("1")
4        case "cc" => println("2")
5        case _ if 变量2=="dd" => println("3") 
6        case _ => println("null")  //表示所有的都没有匹配到执行这里
7  }
  • 匹配类型
1 变量 match{
2       case x:Int if(x>3) => println("Int")
3       case y:String  => println("String")
4       case z:Double => println("Double")
5       case flag:Boolean => println("Boolean")
6       case _ => println("null")  //表示所有的都没有匹配到执行这里
7 }

或者

1 try{
2       println(3/0)
3 }catch {
4      case e:IOException  => println("IOException")
5      case e:Exception  => println("Exception")
6 }finally{
7      println("程序结束!")
8 }
  • 变量赋值(对于_这种不满足前面分支的情况,可以对其进行赋值处理)
1 score match {
2         case "A"=>println("excellent")
3         case "B"=>println("good")
4         case "C"=>println("soso")
5         case _score =>println("you need work harder,your score only "+_score)  //变量赋值
6 }
  • 匹配数组和序列
 1 //匹配数组
 2 val arr3=Array(0,1,2,3,4,5)
 3 arr3 match{
 4       case Array(0,x,y) => println(x+"--"+y) //匹配以0开头,后面两个元素的数组
 5       case Array(0) => println("only 0")     //匹配只有一个元素的数组
 6       case Array(0,_*) => println("many") //匹配以0开头的数组,_表示任意元素,*表示一到多个元素
 7 }
 8 
 9 //匹配序列
10 val lst1=List(3,1,-1)
11 lst1 match{
12       case 0::Nil => println("only 0")    //匹配只有一个元素0的序列
13       case x::y::Nil =>println(x+"--"+y) //匹配有两个元素的序列
14       case 0::tail => println("0 is head ") //匹配以0开头的序列
15       case _ => println("nothing ")
16 }
  • case class
 1 变量 match {
 2       case SubmitTask(id,name) => println(id,name)  //这里能将样例类中的参数提取出来,是是因为有unapply方法
 3       case HeartBeat(time) => println(time)
 4       case CheckTimeOutTask => println("CheckTimeOutTask")
 5 }
 6 
 7 //多例样例类
 8 case class SubmitTask(id: String, name: String)
 9 //多例样例类
10 case class HeartBeat(time: Long)
11 //单例样例类
12 case object CheckTimeOutTask
  • Option(有值是Some,没值是None)
1 grade match {
2         case Some(grade) => println("your grade is " + grade)
3         case None => println("Sorry, your grade information is not in the system")
4 }

18. 隐式转换

  • 什么时候调用隐式转换

调用一个方法时会首先从全局中寻找,当寻找不到或者方法的参数类型不匹配时才使用隐式转换,隐式转换只能定义在Object中。

直接上例子:1 to 10可以写成1.to(10),我们知道1是Int类型的变量,所以按道理Int类中会有一个to的方法,但事实上我们在Int类型中根本就没有找到to方法,为啥还能正常运行呢?

在Scala交互命令行中执行命令:implicits -v,可以查看系统为我们自动引入的各种默认隐式转换,其中就有一个

1 implicit def intWrapper(x: Int): runtime.RichInt

这个隐式方法能够把Int类型的变量转换成runtime.RichInt变量,而RichInt 中确实存在 to 方法

可以看到隐式转换很神奇,但个人水平有限我还是觉得能少用就少用。

19. Scala中Array、ArrayBuffer

Scala中Array与Java中类似,也是长度不可改变的数组。此外,Scala数组的底层实际上是Java数组。例如字符串、整型数组在底层就是Java的String[]、Int[]。

1 // 数组初始化后,长度就固定下来了
2 val a = new Array[Int](10)
3 a(0) = 1
4 
5 // 可以直接使用Array()创建数组,元素类型自动推断
6 val a = Array("leo", 30)
7 a(0) = "hi"

Array中的一些操作如下:

 1 // 数组元素求和
 2 val a = Array(1, 2, 3, 4, 5)
 3 val sum = a.sum
 4 // 获取数组最大值
 5 val max = a.max
 6 // 对数组进行排序
 7 scala.util.Sorting.quickSort(a)
 8 // 获取数组中所有元素内容
 9 a.mkString
10 a.mkString(", ")
11 a.mkString("<", ",", ">")
12 // toString函数
13 a.toString
14 b.toString

在Scala中,如果需要类似于Java中的ArrayList这种长度可变的集合类,则可以使用ArrayBuffer

 1 // 如果不想每次都使用全限定名,则可以预先导入ArrayBuffer类
 2 import scala.collection.mutable.ArrayBuffer
 3 // 使用ArrayBuffer()的方式可以创建一个空的ArrayBuffer
 4 val b = ArrayBuffer[Int]()
 5 // 使用+=操作符,可以添加一个元素,或者多个元素
 6 // 这个语法必须要谨记在心!因为spark源码里大量使用了这种集合操作语法!
 7 b += 1
 8 b += (2, 3, 4, 5)
 9 // 使用++=操作符,可以添加其他集合中的所有元素
10 b ++= Array(6, 7, 8, 9, 10)
11 // 使用trimEnd()函数,可以从尾部截断指定个数的元素
12 b.trimEnd(5)
13 // 使用insert()函数可以在指定位置插入元素
14 // 但是这种操作效率很低,因为需要移动指定位置后的所有元素
15 b.insert(5, 6)
16 b.insert(6, 7, 8, 9, 10)
17 // 使用remove()函数可以移除指定位置的元素
18 b.remove(1)
19 b.remove(1, 3)
20 // Array与ArrayBuffer可以互相进行转换
21 b.toArray
22 a.toBuffer

Array和ArrayBuffer的遍历

 1 // 使用for循环和until遍历Array / ArrayBuffer
 2 // 使until是RichInt提供的函数
 3 for (i <- 0 until b.length)
 4   println(b(i))
 5 // 跳跃遍历Array / ArrayBuffer
 6 for(i <- 0 until (b.length, 2))
 7   println(b(i))
 8 // 从尾部遍历Array / ArrayBuffer
 9 for(i <- (0 until b.length).reverse)
10   println(b(i))
11 // 使用“增强for循环”遍历Array / ArrayBuffer
12 for (e <- b)
13   println(e)

这一部分具体可以参考:https://www.cnblogs.com/HeQiangJava/p/6706951.html  

20. 字符串插值

Scala为我们提供了三种字符串插值的方式,分别是 s, f 和  raw。它们都是定义在 StringContext 中的方法。

  • s字符串插值器

可以解析字符串中的变量,可以调用方法,还能进行计算。实际调用的是StringContext中的s方法。

 1 val name = "Unmi"
 2 println(s"Hello $name")   //Hello Unmi
 3 println(s"Hello ${name}qq) //Hello Unmiqq 界定变量用大括号{},s"Hello $nameqq" 会试图解析变量nameqq
 4 println(s"1 + 1 = ${1 + 1} //1 + 1 = 2,能计算值
 5  
 6 class Person(val name: String){
 7     def say(what: String) = s"say $what"
 8 }
 9 val person = new Person("Unmi")
10 println(s"Hello ${person.name}, ${person.say(person.name)}")   //Hello Unmi, say Unmi, 这个比较复杂

用大括号可以界定变量,{}里可以求值,还可以使用属性和调用方法。

  • f字符串插值器

它除s的功能外(不指定格式就和s一样),还能进行格式化输出,在变量后用%指定输出格式。实际调用的是StringContext中的f方法。

1 val height = 1.9d
2 val name = "James"
3 println(f"$name%s is $height%2.2f meters tall") // James is 1.90 meters tall
  • raw 字符串插值

raw 能让字符串原原本本的输出来,而不是产生控制效果,如对\n,\t 等的输出。实际调用的是StringContext中的raw方法。

1 scala> println("a\nb\tc")
2 a
3 b    c
4  
5 scala> println(raw"a\nb\tc")
6 a\nb\tc

 

Apply

 

21. 异常处理

  • 抛出异常
当抛出异常时,当前的运算被终止,Scala 没有“受检”异常--不需要声明函数或方法可能会抛出的某种异常。
创建一个异常后用 throw关键字抛出,例如:
throw new IllegalArgumentException
// 抛出的异常类型是 Nothing
def halt(n:Int): Unit ={
  if(n%2==0)
    println(n + " 是偶数")
  else
    throw new RuntimeException(n+ " 是奇数")
}

half(5)的运行结果:

Exception in thread "main" java.lang.RuntimeException: 5 是奇数
    at study.Throw$.halt(Throw.scala:13)
    at study.Throw$.main(Throw.scala:6)
    at study.Throw.main(Throw.scala)
  • 捕获异常

 scala使用catch来捕获异常,通过catch语句里的模式匹配语句匹配异常。依次尝试每个catch字句,如果没匹配到异常 try-catch 将终结,并把异常上升出去。

def catchTest(): Unit ={
  try {
    val file=new FileReader("test.txt")
  //使用并关闭文件
  }catch{
    case ex:FileNotFoundException => println("file not found")
    case ex:IOException =>
  }
}
  • finally子句

通常 finally 子句做一些关闭文件或连接的清理工作。 

def finallyTest(): Unit ={
  val file=new FileReader("test.txt")
  try {
   //使用文件
  }catch{
    case ex:FileNotFoundException => println("file not found")
    case ex:IOException =>
  }finally {
    //关闭文件
    file.close()
  }
}

 

posted @ 2019-05-07 08:53  akia开凯  阅读(635)  评论(0编辑  收藏  举报