Scala中的trait特质是一种特殊的概念。
首先可以将trait作为接口来使用,此时的trait就与Java中的接口非常类似。
在trait中可以定义抽象方法,与抽象类中的抽象方法一样,只要不给出方法的具体实现即可。
类可以使用extends关键字继承trait。
注意:在Scala中没有implement的概念,无论继承类还是trait特质,统一都是extends。
类继承trait特质后,必须实现其中的抽象方法,实现时可以省略override关键字。
Scala不支持对类进行多继承,但是支持多重继承trait特质,使用with关键字即可。
package lagou.cn.part06 trait HelloTrait{ def sayHello } trait MakeFriendTrait{ def makFriend } //如果一个类继承了多个Trait特质,第一个Trait用extends,其他Trait用with关键字 class Person(name:String)extends HelloTrait with MakeFriendTrait{ override def sayHello: Unit = { println(s"hello,my name is $name") } override def makFriend: Unit = { println(s"hello,$name") } } object TraitDemo { def main(args: Array[String]): Unit = { val person=new Person("zhangfei") person.sayHello person.makFriend } }
具体方法
Scala中的trait特质不仅仅可以定义抽象方法,还可以定义具体实现的方法,这时的trait更像是包含了通
用工具方法的类。比如,trait中可以包含一些很多类都通用的功能方法,比如打印日志等等,Spark中
就使用了trait来定义通用的日志打印方法。
具体字段
Scala trait特质中的字段可以是抽象的,也可以是具体的。
package lagou.cn.part06 trait People{ val name:String val age=30 def eat:Unit={ println("Eeating ......") } } trait Worker{ val age=25 def work:Unit={ println("Working......") } } class Student extends Worker with People{ //重写name抽象字段,此处override可以省略 override val name: String = "lisi" //由于Worker和People中都有age字段,所以当Student类继承这两个特质时,需要重写age字段 //并且要使用override关键字,否则就会报错。 //此时的override关键字不能省略。 override val age = 20 } object TraitDemoTwo { def main(args: Array[String]): Unit = { val student=new Student student.eat student.work println(s"姓名:${student.name} ,年龄:${student.age}") } }
注意:特质Person和Worker中都有age字段,当Student继承这两个特质时,需要重写age字段,并且
要用override关键字,否则就会报错。
在Scala中,trait特质也是有构造器的,也就是trait中的不包含在任何方法中的代码。
构造器以如下顺序执行:
1、执行父类的构造器;
2、执行trait的构造器,多个trait从左到右依次执行;
3、构造trait时会先构造父trait,如果多个trait继承同一个父trait,则父trait只会构造一次;
4、所有trait构造完毕之后,子类的构造器才执行
package lagou.cn.part06 class Person2 { println("Person's Constructor!") } trait Logger { println("Logger's Constructor!") } trait MyLogger extends Logger { println("MyLogger's Constructor!") } trait TimeLogger extends Logger { println("TimeLogger's Constructor!") } //如果一个类既继承了父类,也继承特质,那么要先写父类,再写特质 //extends 父类 with 特质1 with 特质2..... class Student2 extends Person2 with MyLogger with TimeLogger { println("Student's Constructor!") } object TraitDemoThree { def main(args: Array[String]): Unit = { val stu = new Student2 } }
在Scala中,trait特质也可以继承class类,此时这个class类就会成为所有继承此trait的类的父类。
package lagou.cn.part06 class MyUtil { def printMessage(msg: String): Unit = { println(msg) } } trait Log extends MyUtil { def log(msg: String): Unit = { println(msg) } } //Person3继承了Log特质,Log特质继承了MyUtil类,那么MyUtil类就成为Person3的父类 class Person3(name: String) extends Log { def sayHello: Unit = { log("hello, " + name) printMessage("hi, " + name) } } object TraitDemoFour { def main(args: Array[String]): Unit = { val person = new Person3("jacky") person.sayHello } }
在Java中对象的比较有两个接口,分别是Comparable和Comparator。它们之间的区别在于:
实现Comparable接口的类,重写compareTo()方法后,其对象自身就具有了可比较性;
实现Comparator接口的类,重写了compare()方法后,则提供一个第三方比较器,用于比较两个对象。
在Scala中也引入了以上两种比较方法(Scala.math包下):
Ordered特质混入Java的Comparable接口,它定义了相同类型间的比较方式,但这种内部比较方式是
单一的;
trait Ordered[A] extends Any with java.lang.Comparable[A]{......}
Ordering特质混入Comparator接口,它是提供第三方比较器,可以自定义多种比较方式,在实际开发
中也是使用比较多的,灵活解耦合。
trait Ordering[T] extends Comparator[T] with PartialOrdering[T] with Serializable {......}
使用Ordered特质进行排序操作
case class Project(tag: String, score: Int) extends Ordered[Project] { def compare(pro: Project) = tag.compareTo(pro.tag) } object OrderedDemo { def main(args: Array[String]): Unit = { val list = List(Project("hadoop", 60), Project("flink", 90), Project("hive", 70), Project("spark", 80)) println (list.sorted) } }
使用Ordering特质进行排序操作
object OrderingDemo { def main(args: Array[String]): Unit = { val pairs = Array(("a", 7, 2), ("c", 9, 1), ("b", 8, 3)) // Ordering.by[(Int,Int,Double),Int](_._2)表示从Tuple3转到Int型 // 并按此Tuple3中第二个元素进行排序 Sorting.quickSort(pairs)(Ordering.by[(String, Int, Int), Int](_._2)) println(pairs.toBuffer) } }
Scala没有Java中的switch case,它有一个更加强大的模式匹配机制,可以应用到很多场合。
Scala的模式匹配可以匹配各种情况,比如变量的类型、集合的元素、有值或无值。
模式匹配的基本语法结构:变量 match { case 值 => 代码 }
模式匹配match case中,只要有一个case分支满足并处理了,就不会继续判断下一个case分支了,不
需要使用break语句。这点与Java不同,Java的switch case需要用break阻止。如果值为下划线,则代
表不满足以上所有情况的时候如何处理。
模式匹配match case最基本的应用,就是对变量的值进行模式匹配。match是表达式,与if表达式一
样,是有返回值的。
除此之外,Scala还提供了样例类,对模式匹配进行了优化,可以快速进行匹配。
import scala.util.Random object MatchDemo{ def main(args: Array[String]): Unit = { val charStr = '+' charStr match { case '+' => println("匹配上了加号") case '-' => println("匹配上了减号") case '*' => println("匹配上了乘号") case '/' => println("匹配上了除号") //注意:不满足以上所有情况,就执行下面的代码 case _ => println("都没有匹配上,我是默认值") } println("=========================================") val arr = Array("hadoop", "zookeeper", "spark") val name = arr(Random.nextInt(arr.length)) name match { case "hadoop" => println("大数据分布式存储和计算框架...") case "zookeeper" => println("大数据分布式协调服务框架...") case "spark" => println("大数据分布式内存计算框架...") case _ => println("我不认识你...") } } }
object MatchDemo { def main(args: Array[String]): Unit = { //守卫式匹配,添加if判断 val char = '/' val num = char match { case '+' => 1 case '-' => 2 case _ if char.equals('*') => 3 case _ => 4 } println(num) typeMatch(Array(1,2)) } //匹配类型,语法:case 变量:类型 => 代码 def typeMatch(x: Any) = { x match { case x: String => println("这是一个字符串类型") case x: Int => println("是一个整型") case x: Boolean if (x == false) => println("是一个false的布尔类型") case x: Array[Int] => println("是一个整型数组") case _ => println("不知是啥类型!") } } }
Scala的模式匹配还有一个强大的功能,它可以直接匹配类型,而不是值。这一点是Java的switch case
做不到的。
匹配类型的语法:case 变量 : 类型 => 代码,而不是匹配值的“case 值 => 代码”这种语法。
object FSD { def main(args: Array[String]): Unit = { val a = 3 val obj = if(a == 1) 1 else if(a == 2) "2" else if(a == 3) BigInt(3) else if(a == 4) Map("aa" -> 1) else if(a == 5) Map(1 -> "aa") else if(a == 6) Array(1, 2, 3) else if(a == 7) Array("aa", 1) else if(a == 8) Array("aa") val r1 = obj match { case x: Int => x case s: String => s.toInt // case BigInt => -1 //不能这么匹配 case _: BigInt => Int.MaxValue case m: Map[String, Int] => "Map[String, Int]类型的Map集合" case m: Map[_, _] => "Map集合" case a: Array[Int] => "It's an Array[Int]" case a: Array[String] => "It's an Array[String]" case a: Array[_] => "It's an array of something other than Int" case _ => 0 } println(r1 + ", " + r1.getClass.getName) } }
package lagou.cn.part07 object MatchCollection { def main(args: Array[String]): Unit = { val array = Array(3, 5, 7) //对Array数组进行模式匹配,分别匹配: //带有指定个数元素的数组、带有指定元素的数组、以某元素开头的数组 array match { case Array(1, x, y) => println(x + " " + y) case Array(1) => println("only 1...") case Array(1, _*) => println("以1开头的数组") case _ => println("something else ...") } val list = List(3, 4) //对List列表进行模式匹配,分别匹配: //带有指定个数元素的列表、带有指定元素的列表、以某元素开头的列表 //Nil表示一个空列表,::起到一个连接的作用,表示将元素添加到某列表中 //tail表示返回除第一个元素之外的其他元素的列表 list match { case x :: y :: Nil => println(s"$x $y") case 2 :: Nil => println("only 2...") case 1 :: tail => println("以1开头的列表") case _ => println("something else...") } val tuple = (3, 4, 5) tuple match { //以1开头,包含三个元素的元组 case (1, x, y) => println(s"$x $y") //以5结尾,包含三个元素的元组 case (_, z, 5) => println(z) case _ => println("else") } } }
case class样例类是Scala中特殊的类。当声明样例类时,以下事情会自动发生:
case class是多例的,后面要跟构造参数,case object是单例的。
此外,case class样例类中可以添加方法和字段,并且可用于模式匹配。
package lagou.cn.part07 class Amount //样例类中主构造器的参数默认用val修饰 //样例类中自动生成apply方法 case class Dollar(value: Double) extends Amount case class Currency(value: Double, unit: String) extends Amount case object Nothing extends Amount object CaseClassDemo { def main(args: Array[String]): Unit = { judgeIdentity(Dollar(10.0)) judgeIdentity(Currency(20.0, "100")) judgeIdentity(Nothing) } def judgeIdentity(amt: Amount): Unit = { amt match { case Dollar(value) => println(s"$value") case Currency(value, unit) => println(s"$value $unit") case Nothing => println("nothing") } } }
Scala Option选项类型用来表示一个值是可选的,有值或无值。
Option[T] 是一个类型为 T 的可选值的容器,可以通过get()函数获取Option的值。如果值存在,
Option[T] 就是一个 Some。如果不存在,Option[T] 就是对象 None 。
Option通常与模式匹配结合使用,用于判断某个变量是有值还是无值。
package lagou.cn.part07 object OptionDemo { val grades = Map("jacky" -> 90, "tom" -> 80, "jarry" -> 60) def getGrade(name: String): Unit = { val grade: Option[Int] = grades.get(name) grade match { case Some(grade) => println("成绩是:" + grade) case None => println("没有此人成绩!") } } def main(args: Array[String]): Unit = { getGrade("jacky") getGrade("lisi") } }
Scala中函数为头等公民,不仅可以定义一个函数然后调用它,还可以写一个未命名的函数字面量,然
后可以把它当成一个值传递到其它函数或是赋值给其它变量。
函数字面量体现了函数式编程的核心理念。字面量包括整数字面量、浮点数字面量、布尔型字面量、字
符字面量、字符串字面量、符号字面量、函数字面量等。什么是函数字面量呢?
在函数式编程中,函数是“头等公民”,可以像任何其他数据类型一样被传递和操作。函数的使用方式和
其他数据类型的使用方式完全一致,可以像定义变量那样去定义一个函数,函数也会和其他变量一样,
有类型有值;
就像变量的“类型”和“值”是分开的两个概念一样,函数的“类型”和“值”也成为两个分开的概念;
函数的“值”,就是“函数字面量”。
scala> def add1(x: Int): Int = { x + 1 } add1: (x: Int)Int // 函数的类型为: (Int) => Int // 输入参数列表只有一个括号,可以简写为: Int => Int scala> def add2(x: Int, y: Int): Int = { x + y } add2: (x: Int, y: Int)Int // 函数的类型为: (Int, Int) => Int scala> def add3(x: Int, y: Int, z: Int): Int = { x + y + z } add3: (x: Int, y: Int, z: Int)Int // 函数的类型为: (Int, Int, Int) => Int scala> def add4(x: Int, y: Int, z: Int): (Int, Int) = { (x + y, y + z) } add4: (x: Int, y: Int, z: Int)(Int, Int) // 函数的类型为: (Int, Int, Int) => (Int, Int)
函数类型:(输入参数类型列表) => (输出参数类型列表)
只有一个参数时,小括号可省略;函数体中只有1行语句时,大括号可以省略;
把函数定义中的类型声明部分去除,剩下的就是函数的“值”,即函数字面量:
在Scala中我们这样定义变量: val 变量名: 类型 = 值 ;
我们可以用完全相同的方式定义函数: val 函数名: 函数类型 = 函数字面量
val add1 = (x: Int) => x + 1 val add2 = (x: Int, y: Int) => x + y val add3 = (x: Int, y: Int, z: Int) => x + y + z val add4 = (x: Int, y: Int, z: Int) => (x + y, y + z)
备注:要让编译器进行自动类型推断,要告诉编译器足够的信息,所以添加了 x 的类型信息。
函数的定义:
val 函数名: (参数类型1,参数类型2) => (返回类型) = 函数字面量 val 函数名 = 函数字面量 函数字面量:(参数1:类型1,参数2:类型2) => 函数体 val 函数名 = (参数1:类型1,参数2:类型2) => 函数体
scala> def addm(x: Int, y: Int): Int = x + y addm: (x: Int, y: Int)Int scala> val addf = (x: Int, y: Int) => x + y addf: (Int, Int) => Int = <function2>
严格的说:使用 val 定义的是函数(function),使用 def 定义的是方法(method)。二者在语义上的区别
很小,在绝大多数情况下都可以不去理会它们之间的区别,但是有时候有必要了解它们之间的不同。
Scala中的方法与函数有以下区别:
// 下面用三种方式定义了函数,其中第二种方式最常见 val adder1: (Int, Int) => Int = (x, y) => x+y val adder2 = (x: Int, y: Int) => x+y // Function2是特质,不能直接new // new Function2[Int,Int,Int]{ ... } 其实是定义并实例化一个实现了 Function2 特质的类的对象 val adder3 = new Function2[Int, Int, Int]{ def apply(x: Int, y: Int): Int = { x + y } }
// 方法不能作为单独的表达式而存在,而函数可以 scala> def addm(x: Int, y: Int): Int = x + y addm: (x: Int, y: Int)Int scala> val addf = (x: Int, y: Int) => x + y addf: (Int, Int) => Int = <function2> scala> addm <console>:13: error: missing argument list for method addm scala> addf res8: (Int, Int) => Int = <function2> // 函数必须要有参数列表,而方法可以没有参数列表 scala> def m1 = "This is lagou edu" m1: String // 函数必须有参数列表 scala> val f1 = () => "This is lagou edu" f1: () => String = <function0> // 方法名是方法调用 scala> m1 res16: String = This is lagou edu // 函数名代表函数对象 scala> f1 res17: () => String = <function0> // 这才代表函数调用 scala> f1() res18: String = This is lagou edu // 需要函数的地方,可以传递一个方法 scala> val list = (1 to 10).toList lst: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) scala> def double(x: Int) = x*x double: (x: Int)Int scala> list.map(double(_)) res20: List[Int] = List(1, 4, 9, 16, 25, 36, 49, 64, 81, 100)
将方法转换为函数:
scala> def f1 = double _ //注意:方法名与下划线之间有一个空格 f1: Int => Int scala> f1 res21: Int => Int = <function1>
写程序的时候是定义方法、还是定义函数?
一般情况下,不对二者做区分,认为都是函数,更多的时候使用def定义函数。
函数没有名字就是匿名函数;
匿名函数,又被称为 Lambda 表达式。 Lambda表达式的形式如下:
(参数名1: 类型1, 参数名2: 类型2, … …) => 函数体
// 定义匿名函数 scala> (x: Int) => x + 1 res0: Int => Int = <function1> // 函数没有名字,在集成开发环境中是无法被调用的 scala> res0(10) res1: Int = 11 scala> val list = (1 to 10).toList lst: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) // 将匿名函数作为参数传递给另一个函数 scala> list.map((x: Int) => x + 1) res2: List[Int] = List(2, 3, 4, 5, 6, 7, 8, 9, 10, 11) // x一定是Int类型,这里可以省略 scala> list.map((x) => x + 1) res3: List[Int] = List(2, 3, 4, 5, 6, 7, 8, 9, 10, 11) // 只有一个参数,小括号可以省略 scala> list.map(x => x + 1) res4: List[Int] = List(2, 3, 4, 5, 6, 7, 8, 9, 10, 11) // 使用占位符简化函数字面量 scala> list.map(_ + 1) res5: List[Int] = List(2, 3, 4, 5, 6, 7, 8, 9, 10, 11) // 实现将List中的每个元素*2 + 1,但是出错了 scala> list.map(_ + _ + 1) <console>:13: error: missing parameter type for expanded function ((x$1, x$2) => x$1.$plus(x$2).$plus(1)) // 这样是可行的 scala> list.map(2 * _ + 1) // 通过reduce这个高阶函数,将list列表中的元素相加求和 scala> list.reduce((x,y) => x + y) res0: Int = 55 // 使用占位符简化函数字面量 // 第一个下划线代表第一个参数,第二个下划线代表第二个参数 scala> list.reduce(_ + _) res1: Int = 55
多个下划线指代多个参数,而不是单个参数的重复运用
高阶函数:接收一个或多个函数作为输入 或 输出一个函数。
函数的参数可以是变量,而函数又可以赋值给变量,由于函数和变量地位一样,所以函数参数也可以是
函数;
常用的高阶函数:map、reduce、flatMap、foreach、filter、count … … (接收函数作为参数)
package lagou.cn.part08 object HighFunction { def main(args: Array[String]): Unit = { //接收一个或多个函数作为输入的高阶函数 println("------------map高阶函数----------") val func = (n) => "*" * n (1 to 5).map(func(_)).foreach(println) //输出一个函数的高阶函数 println("------------输出一个函数的高阶函数----------") val URLBuilder = (ssl: Boolean, domainName: String) => { val schema = if (ssl) "https://" else "http://" //输出一个匿名函数 (endpoint: String, query: String) => s"$schema$domainName/$endpoint?$query" } val domainName = "www.lagou.com" def getURl = URLBuilder(true, domainName) val endpoint = "show" val query = "id=1" val url = getURl(endpoint, query) println(url) } }
闭包是一种函数,一种比较特殊的函数,它和普通的函数有很大区别:
// 普通的函数 val addMore1 = (x: Int) => x + 10 // 外部变量,也称为自由变量 var more = 10 // 闭包 val addMore2 = (x: Int) => x + more // 调用addMore1函数 println(addMore1(5)) // 每次addMore2函数被调用时,都会去捕获外部的自由变量 println(addMore2(10)) more = 100 println(addMore2(10)) more = 1000 println(addMore2(10))
闭包是在其上下文中引用了自由变量的函数;
闭包引用到函数外面定义的变量,定义这个函数的过程就是将这个自由变量捕获而构成的一个封闭的函
数,也可理解为”把函数外部的一个自由变量关闭进来“。
何为闭包?需满足下面三个条件:
1、闭包是一个函数
2、函数必须要有返回值
3、返回值依赖声明在函数外部的一个或多个变量,用Java的话说,就是返回值和定义的全局变量有关
函数编程中,接收多个参数的函数都可以转化为接收单个参数的函数,这个转化过程就叫柯里化
(Currying)。
Scala中,柯里化函数的定义形式和普通函数类似,区别在于柯里化函数拥有多组参数列表,每组参数
用小括号括起来。
Scala API中很多函数都是柯里化的形式。
// 使用普通的方式 def add1(x: Int, y: Int) = x + y // 使用闭包的方式,将其中一个函数作为返回值 def add2(x: Int) = (y:Int) => x + y // 使用柯里化的方式 def add(x: Int)(y: Int) = x + y //调用柯里化函数add scala> add(1)(2) res1: Int = 3 //add(1)(2)实际上第一次调用使用参数x,返回一个函数类型的值,第二次使用参数y调用这个函数类型的 值。 //实际上最先演变成这样的函数:def add(x: Int) = (y:Int) => x + y //在这个函数中,接收一个x为参数,返回一个匿名函数,这个匿名函数的定义是:接收一个Int型参数y,函 数体是x+y。 //调用过程如下: scala> val result=add(1) result: Int => Int = <function1> scala> val sum=result(2) sum: Int = 3 scala> sum res0: Int = 3
部分应用函数(Partial Applied Function)也叫偏应用函数,与偏函数从名称上看非常接近,但二者之
间却有天壤之别。
部分应用函数是指缺少部分(甚至全部)参数的函数。
如果一个函数有n个参数, 而为其提供少于n个参数, 那就得到了一个部分应用函数。
// 定义一个函数 def add(x:Int, y:Int, z:Int) = x+y+z // Int不能省略 def addX = add(1, _:Int, _:Int) addX(2,3) addX(3,4) def addXAndY = add(10, 100, _:Int) addXAndY(1) def addZ = add(_:Int, _:Int, 10) addZ(1,2) // 省略了全部的参数,下面两个等价。第二个更常用 def add1 = add(_: Int, _: Int, _: Int) def add2 = add _
偏函数(Partial Function)之所以“偏”,原因在于它们并不处理所有可能的输入,而只处理那些能与至
少一个 case 语句匹配的输入;
在偏函数中只能使用 case 语句,整个函数必须用大括号包围。这与普通的函数字面量不同,普通的函
数字面量可以使用大括号,也可以用小括号;
被包裹在大括号中的一组case语句是一个偏函数,是一个并非对所有输入值都有定义的函数;
Scala中的Partial Function是一个trait,其类型为PartialFunction[A,B],表示:接收一个类型为A的参
数,返回一个类型为B的结果。
// 1、2、3有对应的输出值,其它输入打印 Other val pf: PartialFunction[Int, String] = { case 1 => "One" case 2 => "Two" case 3 => "Three" case _=> "Other" } pf(1) // 返回: One pf(2) // 返回: Two pf(5) // 返回: Other
需求:过滤List中的String类型的元素,并将Int类型的元素加1。
通过偏函数实现上述需求。
package lagou.cn.part08 object PartialFunctionDemo { def main(args: Array[String]): Unit = { //[Any,Int],偏函数接收的数据类型是Any,返回的数据类型是Int val partialFunction = new PartialFunction[Any, Int] { //如果返回true,那么就调用apply方法;如果返回的是false,就过滤掉 override def isDefinedAt(x: Any): Boolean = { println("===========isDefinedAt输出==============") println(x.toString) x.isInstanceOf[Int] } //对传入的整数值+1,并将其返回 override def apply(v1: Any): Int = { println("************apply输出,只会输出整数**************") println(v1.toString) v1.asInstanceOf[Int] + 1 } } val list=List(10,"hadoop",20,"spark",30,"flink") list.collect(partialFunction).foreach(println(_)) } }
主要内容: 1、Scala中的可变和不可变集合 2、集合的三大类:Seq、Set、Map 3、集合的常用算子 4、Scala与Java之间的集合转换
根据容器中元素的组织方式和操作方式,可以分为有序和无序、可变和不可变等不同的容器类别;
不可变集合是指集合内的元素一旦初始化完成就不可再进行更改,任何对集合的改变都将生成一个新的
集合;
可变集合提供了改变集合内元素的方法;
Scala同时支持可变集合和不可变集合,主要下面两个包:
对于几乎所有的集合类,Scala都同时提供了可变和不可变的版本。
Scala优先采用不可变集合,不可变集合元素不可更改,可以安全的并发访问。
Scala集合有三大类:Seq(序列)、Set(集)、Map(映射);
所有的集合都扩展自Iterable特质。
immutable不可变集合:
mutable可变集合:
小结:
String属于IndexedSeq
Queue队列和Stack堆这两个经典的数据结构属于LinearSeq
Map体系下有一个SortedMap,说明Scala中的Map是可以支持排序的
mutable可变集合中Seq中的Buffer下有ListBuffer,它相当于可变的List列表;
List列表属于Seq中的LinearSeq
Seq代表按照一定顺序排列的元素序列;
该序列是一种特别的可迭代集合,包含可重复的元素;
元素的顺序是确定的,每个元素对应一个索引值;
Seq提供了两个重要的子特质:
List代表元素顺序固定的不可变的链表,它是Seq的子类,在Scala编程中经常使用。
List是函数式编程语言中典型的数据结构,与数组类似,可索引、存放类型相同的元素。
List一旦被定义,其值就不能改变。
List列表有头部和尾部的概念,可以分别使用head和tail方法来获取:
这体现出列表具有递归的链表结构。
Scala定义了一个空列表对象Nil,定义为List[Nothing]
借助 Nil 可将多个元素用操作符 :: 添加到列表头部,常用来初始化列表;
操作符 ::: 用于拼接两个列表;
object ListDemo { def main(args: Array[String]): Unit = { //Nil表示一个空的列表 //::操作符表示向集合中添加元素,它是从右往左进行运算的,所以集合对象一定要放在最右边 val list1 = 1 :: 2 :: 3 :: 4 :: Nil val list2 = 5 :: 6 :: 7 :: 8 :: Nil //使用:::操作符进行了拼接,不要使用::进行列表的拼接,因为这样拼接的结果不是我们想要的 val list3=list1:::list2 println(list3.head) //返回第一个元素 println(list3.tail) //返回除第一个元系之外的其他元素构成的新列表 println(list3.init) //返回除最后一个元素之外的其他元素构成的新列表 println(list3.last) //返回最后一个元素 } }
列表递归的结构,便于编写递归的算法:
object ListDemo { def main(args: Array[String]): Unit = { val list4=List(4,2,6,1,7,9) println(quickSort(list4)) } def quickSort(list:List[Int]):List[Int]={ list match { case Nil=>Nil case head::tail=> //通过partition将tail分为两部分 //小于head的元素放入less列表中,大于head的元素放入greater列表中 val (less,greater) =tail.partition(_<head) quickSort(less):::head::quickSort(greater) } } }
队列Queue是一个先进先出的结构。
队列是一个有序列表,在底层可以用数组或链表来实现。
先进先出的原则,就是先存入的数据,要先取出,后存入的数据后取出。
在Scala中,有scala.collection.mutable.Queue和scala.collection.immutable.Queue,一般来说,我
们使用的是scala.collection.mutable.Queue
package lagou.cn.part09 import scala.collection.mutable object QueueDemo { def main(args: Array[String]): Unit = { //创建一个可变的队列 val queue1=new mutable.Queue[Int]() println(queue1) //队列当中添加元素 queue1 +=1 //队列当中添加List列表 queue1 ++=List(2,3,4) println(queue1) //按照进入队列的顺序,删除队列当中的元素 //返回队列中的第一个元素,并从队列中删除这个元素 val dequeue=queue1.dequeue() println(dequeue) println(queue1) //再向队列中添加元素 queue1.enqueue(5,6,7) println(queue1) //获取第一个、最后一个元素 println(queue1.head) println(queue1.last) } }
Set(集合)是没有重复元素的对象集合,Set中的元素是唯一的;
Set分为可变的和不可变的集合;
默认情况下,使用的是不可变集合(引用 scala.collection.immutable.Set);
使用可变集合,需要引用 scala.collection.mutable.Set 包;
package lagou.cn.part09 object SetDemo { def main(args: Array[String]): Unit = { //创建一个Set集合 val set = Set(1, 2, 3, 4, 5, 6) set.drop(1) println(set) //创建一个可变的Set import scala.collection.mutable.Set val mutableSet = Set(3, 4, 5) //对可变的Set进行增加元素、删除元素的操作 mutableSet.add(7) println(mutableSet) mutableSet.remove(7) println(mutableSet) //通过使用+= -=进行增加、删除元素的操作 mutableSet += 8 mutableSet -= 3 println(mutableSet) //对Set集合进行交集的操作(& intersect) println(Set(1, 2, 3) & Set(2, 3, 4)) println(Set(1, 2, 3).intersect(Set(2, 3, 4))) //对Set集合进行并集的操作(++ | union) println(Set(1,2,3) ++ Set(2,3,4)) println(Set(1,2,3) | Set(2,3,4)) println(Set(1,2,3).union(Set(2,3,4))) //对Set集合进行差集的操作(-- &~ diff) //差集是将第一个集合中与第二个集合相同的元素去除,剩余的就是差集 println(Set(1,2,3) -- Set(2,3,4)) println(Set(1,2,3) &~ Set(2,3,4)) println(Set(1,2,3).diff(Set(2,3,4)) ) } }
Map(映射)是一系列键值对的容器;Scala 提供了可变的和不可变的两种版本的Map,
分别定义在包 scala.collection.mutable 和 scala.collection.immutable 里;
默认情况下,Scala中使用不可变的 Map;
如果要使用可变Map,必须导入scala.collection.mutable.Map; 在Map中,键的值是唯一的,可以根据键来对值进行快速的检索。
package lagou.cn.part09 import scala.collection.mutable object MapDemo { def main(args: Array[String]): Unit = { //使用两种方式定义Map val map1 = Map("a" -> 1, "b" -> 2) val map2 = Map(("a", 1), ("b", 2)) map1.keys.foreach(println(_)) map1.values.foreach(println(_)) //如果访问不存在的Key值时,会抛出异常 // println(map1("c")) //也可以使用get方法,来获取与Key值相对应的Value值。 //get方法会返回一个Option对象,要么要是Some(有值),要么是None(无值) val num: Option[Int] = map1.get("c") num match { case None => println("None") case Some(x) => println(x) } //获取Key值所对应的Value值,如果键Key不存在,那么就返回指定的默认值 val num2: Int = map1.getOrElse("d", 0) println(num2) //创建一个可变的Map val map3 = scala.collection.mutable.Map("a" -> 1, "b" -> 2) println(map3) map3("a") = 10 println(map3) //增加一个元素 map3("c") = 3 println(map3) //通过+=添加元素,-=删除元素 map3 += ("d" -> 4, "f" -> 5) println(map3) map3 -= "d" println(map3) //将Key与Value的值互换 val kv: mutable.Map[Int, String] = for ((k, v) <- map3) yield (v, k) println(kv) //推荐使用下面的方式将Key与value的值互换 map3.map(x=>(x._2,x._1)).foreach(println(_)) //通过拉链操作创建Map val a=Array(1,2,3) val b=Array("a","b","c") val c: Array[(Int, String)] = a.zip(b) val d: Map[Int, String] = a.zip(b).toMap println(d) } }
集合对象都有 foreach、map 算子。
两个算子的共同点在于:都是用于遍历集合对象,并对每一项执行指定的方法;
两个算子的差异点在于:
foreach无返回值(准确说返回void),用于遍历集合
map返回集合对象,用于将一个集合转换成另一个集合
// 使用 foreach 打印集合元素 val numlist = (1 to 10).toList numlist.foreach(elem=>print(elem+" ")) numlist.foreach(print _) numlist.foreach(print) // 使用 map 对集合进行转换 numlist.map(_ > 2) numlist.map(_ * 2)
操作 Map集合时,mapValues用于遍历value,是map操作的一种简化形式;
// Range(20, 0, -2)用给定的步长值设定一个范围,从开始到结束(不包含)。 //Map(20 -> 0,18 -> 1,16 -> 2,14 -> 3,12 -> 4,10 -> 5,8 -> 6,6 -> 7,4 -> 8,2 ->9) val map = Range(20, 0, -2).zipWithIndex.toMap // 将map集合中的value值+100 map.map(elem => (elem._1, elem._2 + 100)) map.map{case (k,v) => (k, v+100)} // mapValues的表达最简洁 map.mapValues(_+100)
flatten的作用是把嵌套的结构展开,把结果放到一个集合中;
在 flatMap 中传入一个函数,该函数对每个输入都返回一个集合(而不是一个元素),最后把生成的多
个集合“拍扁”成为一个集合;
scala> val lst1 = List(List(1,2), List(3,4)) lst1: List[List[Int]] = List(List(1, 2), List(3, 4)) scala> lst1.flatten res5: List[Int] = List(1, 2, 3, 4) // flatten 把一个字符串的集合展开为一个字符集合,因为字符串本身就是字符的集合 scala> val lst4 = List("Java", "hadoop") lst4: List[String] = List(Java, hadoop) scala> lst4.flatten res8: List[Char] = List(J, a, v, a, h, a, d, o, o, p) // flatten 有效的处理 Some 和 None 组成的集合。它可以展开Some元素形成一个新的集合,同时去掉 None元素 scala> val x = Array(Some(1), None, Some(3), None) x: Array[Option[Int]] = Array(Some(1), None, Some(3), None) // 方法很多,flatten最简单 scala> x.flatten res9: Array[Int] = Array(1, 3) scala> x.collect{case Some(i) => i} res10: Array[Int] = Array(1, 3) scala> x.filter(!_.isEmpty).map(_.get) res11: Array[Int] = Array(1, 3) // 下面两条语句等价 val lst = List(List(1,2,5,6),List(3,4)) // 将 lst 中每个元素乘2,最后作为一个集合返回 // 此时 flatMap = flatten + map //List(1,2,5,6,3,4) lst.flatten.map(_*2) lst.flatMap((x: List[Int]) => x.map(_*2)) lst.flatMap(_.map(_*2)) // 将字符串数组按空格切分,转换为单词数组 val lines = Array("Apache Spark has an advanced DAG execution engine","Spark offers over 80 high-level operators") // 下面两条语句效果等价 //map算子产生的结果:Array(Array(Apache, Spark, has, an, advanced, DAG, execution,engine), Array(Spark, offers, over, 80, high-level, operators)) // flatten算子产生的结果:Array(Apache, Spark, has, an, advanced, DAG, execution,engine, Spark, offers, over, 80, high-level, operators) lines.map(_.split(" ")).flatten // 此时 flatMap = map + flatten lines.flatMap(_.split(" "))
备注:flatMap = flatten + map 或 flatMap = map + flatten
collect通过执行一个并行计算(偏函数),得到一个新的数组对象
object CollectDemo { //通过下面的偏函数,把chars数组的小写a转换为大写的A val fun: PartialFunction[Char, Char] = { case 'a' => 'A' case x => x } def main(args: Array[String]): Unit = { val chars = Array('a', 'b', 'c') val newchars = chars.collect(fun) println("newchars:" + newchars.mkString(",")) } }
reduce可以对集合当中的元素进行归约操作;
还有 reduceLeft 和 reduceRight ,reduceLeft 从左向右归约,reduceRight 从右向左归约;
val lst1 = (1 to 10).toList lst1.reduce(_+_) // 为什么这里能出现两个占位符? lst1.reduce(_+_) // 我们说过一个占位符代表一个参数,那么两个占位符就代表两个参数。根据这个思路改写等价的语句 // x类似于buffer,缓存每次操作的数据;y每次操作传递新的集合元素 lst1.reduce((x, y) => x + y) // 利用reduce操作,查找 lst1 中的最大值 lst1.reduce((x,y) => if (x>y) x else y) // reduceLeft、reduceRight lst1.reduceLeft((x,y) => if (x>y) x else y) lst1.reduceRight((x,y) => if (x>y) x else y)
Scala中对于集合的排序有三种方法:sorted、sortBy、sortWith
object SortDemo { def main(args: Array[String]): Unit = { val list = List(1, 9, 3, 8, 5, 6) //sorted方法对一个集合进行自然排序 //sorted源码:def sorted[B >: A](implicit ord: Ordering[B]): Repr //源码中有两点值得注意的地方: // 1.sorted方法中有个隐式参数ord: Ordering。 // 2.sorted方法真正排序的逻辑是调用的java.util.Arrays.sort val numSort: List[Int] = list.sorted println(numSort) //sortBy源码:def sortBy[B](f: A => B)(implicit ord: Ordering[B]): Repr =sorted(ord on f) //sortBy最后调用的sorted方法 println(list.sortBy(x => x).reverse) //sortWith源码:def sortWith(lt: (A, A) => Boolean): Repr = sorted(OrderingfromLessThan lt) print(list.sortWith(_ > _)) } }
使用 scala.collection.JavaConverters 与Java集合交互。它有一系列的隐式转换,添加了asJava和
asScala的转换方法。
import scala.collection.JavaConverters._ val list: Java.util.List[Int] = List(1,2,3,4).asJava val buffer: scala.collection.mutable.Buffer[Int] = list.asScala
隐式转换和隐式参数是Scala中两个非常强大的功能,利用隐式转换和隐式参数,可以提供类库,对类
库的使用者隐匿掉具体的细节。
Scala会根据隐式转换函数的签名,在程序中使用到隐式转换函数接收的参数类型定义的对象时,会自
动将其传入隐式转换函数,转换为另外一种类型的对象并返回,这就是“隐式转换”。
隐式转换需要使用implicit关键字。
使用Scala的隐式转换有一定的限制:
Spark源码中有大量的隐式转换和隐式参数,因此必须掌握隐式机制。
Scala的隐式转换最核心的就是定义隐式转换函数,即implicit conversion function。
定义的隐式转换函数,只要在编写的程序内引入,就会被Scala自动使用。
隐式转换函数由Scala自动调用,通常建议将隐式转换函数的名称命名为“one2one”的形式。
示例1:下面代码中定义了一个隐式函数
class Num {} class RichNum(num: Num) { def rich(): Unit = { println("Hello Implicit!") } } object ImplicitDemo { // 定义一个名称为num2RichNum的隐式函数 implicit def num2RichNum(num: Num): RichNum = { new RichNum(num) } def main(args: Array[String]): Unit = { val num = new Num // num对象并没有rich方法,编译器会查找当前范围内是否有可转换的函数 // 如果没有则编译失败,如果有则会调用。 num.rich() } }
示例2:导入隐式函数
package test.implicitdemo object Int2String { implicit def int2String(num: Int):String = num.toString }
下面代码中调用了String类型的length方法,Int类型本身没有length方法,但是在可用范围内定义了可
以把Int转换为String的隐式函数int2String,因此函数编译通过并运行出正确的结果。
此示例中隐式函数的定义必须定义在使用之前,否则编译报错。
import test.implicitdemo.Int2String._ object ImplicitTest { def main(args: Array[String]): Unit = { println(20.length) } }
通过import test.implicitdemo.Int2String._,将Int2StringTest内部的成员导入到相应的作用域内,否
则无法调用隐式函数。
要实现隐式转换,只要在程序可见的范围内定义隐式转换函数即可,Scala会自动使用隐式转换函数。
隐式转换函数与普通函数的语法区别就是,要以implicit开头,而且最好要定义函数返回类型。
隐式转换案例:特殊售票窗口(只接受特殊人群买票,比如学生、老人等),其他人不能在特殊售票窗
口买票。
package lagou.cn.part10 class SpecialPerson(var name: String) class Older(var name: String) class Student(var name: String) class Worker(var name: String) object ImplicitDemoTwo { def buySpecialTickWindow(person: SpecialPerson): Unit = { if (person != null) { println(person.name + "购买了一张特殊票!") } else { println("你不是特殊人群,不能在此购票!") } } //定义一个隐式转换函数 implicit def any2SpecialPerson(any: Any): SpecialPerson = { any match { case any: Older => new SpecialPerson(any.asInstanceOf[Older].name) case any: Student => new SpecialPerson(any.asInstanceOf[Student].name) case _ => null } } def main(args: Array[String]): Unit = { val stu = new Student("jacky") val older = new Older("old man") val worker = new Worker("lisi") buySpecialTickWindow(stu) buySpecialTickWindow(older) buySpecialTickWindow(worker) } }
在函数定义的时候,支持在最后一组参数中使用 implicit ,表明这是一组隐式参数。在调用该函数
的时候,可以不用传递隐式参数,而编译器会自动寻找一个 implicit 标记过的合适的值作为参数。
Scala编译器会在两个范围内查找:
package lagou.cn.part10 object DoublyDemo { def print(num:Double)(implicit fmt:String): Unit ={ println(fmt format(num)) } def main(args: Array[String]): Unit = { print(3.245)("%.1f") //定义一个隐式变量 implicit val printFmt="%.3f" print(3.24) } }
主要内容:
1、类型参数
泛型类、泛型函数、协变和逆变
2、Akka
Scala的类型参数与Java的泛型是一样的,可以在集合、类、函数中定义类型参数,从而保证程序更好的
健壮性。
泛型类,顾名思义,其实就是在类的声明中定义一些泛型类型,然后在类内部的字段或者方法,就可以
使用这些泛型类型。
使用泛型类,通常是需要对类中的某些成员,比如某些字段和方法中的参数或变量进行统一的类型限
制,这样可以保证程序更好的健壮性和稳定性。
如果不使用泛型进行统一的类型限制,那么在后期程序运行过程中难免会出现问题,比如传入了不希望
的类型导致程序出问题。
在使用泛型类的时候,比如创建泛型类的对象,只需将类型参数替换为实际的类型即可。
Scala自动推断泛型类型特性:直接给使用泛型类型的字段赋值时,Scala会自动进行类型推断。
泛型类的定义如下:
//定义一个泛型类 class Stack[T1, T2, T3](name: T1) { var age: T2 = _ var address: T3 = _ def getInfo: Unit = { println(s"$name,$age,$address") } }
使用上述的泛型类,只需要使用具体的类型代替类型参数即可。
object GenericityDemo { def main(args: Array[String]): Unit = { //创建泛型类对象 val stack = new Stack[String, Int, String]("lisi") stack.age = 20 stack.address = "北京" stack.getInfo } }
泛型函数,与泛型类类似,可以给某个函数在声明时指定泛型类型,然后在函数体内,多个变量或者返
回值之间,就可以使用泛型类型进行声明,从而对某个特殊的变量,或者多个变量,进行强制性的类型
限制。
与泛型类一样,你可以通过给使用了泛型类型的变量传递值来让Scala自动推断泛型的实际类型,也可
以在调用函数时,手动指定泛型类型。
案例:卡片售卖机,可以指定卡片的内容,内容可以是String类型或Int类型
object GenericityFunction { def getCard[T](content: T) = { content match { case content: Int => s"card:$content is Int " case content: String => s"card:$content is String" case _ => s"card:$content" } } def main(args: Array[String]): Unit = { println(getCard[String]("hello")) println (getCard(1001)) } }
Scala的协变和逆变是非常有特色的,完全解决了Java中的泛型的一大缺憾!
举例来说,Java中,如果有Professional是Master的子类,那么Card[Professionnal]是不是
Card[Master]的子类?答案是:不是。因此对于开发程序造成了很多的麻烦。
而Scala中,只要灵活使用协变和逆变,就可以解决Java泛型的问题。
协变定义形式如:trait List[+T] {}
当类型S是类型A的子类型时,则List[S]也可以认为是List[A}的子类型,即List[S]可以泛化为List[A],也
就是被参数化,类型的泛化方向与参数类型的方向是一致的,所以称为协变(covariance)。
逆变定义形式如:trait List[-T] {}
当类型S是类型A的子类型,则Queue[A]反过来可以认为是Queue[S}的子类型,也就是被参数化类型的
泛化方向与参数类型的方向是相反的,所以称为逆变(contravariance)。
小结:
如果A是B的子类,那么在协变中,List[A]就是List[B]的子类; 在逆变中,List[A]就是List[B] 的父类。
协变案例:只有大师以及大师级别以下的名片都可以进入会场
package lagou.cn.part11 //大师 class Master //专家 class Professor extends Master //讲师 class Teacher //定义协变 //class Card[+T] //定义逆变 class Card[-T] object ConvarianceDemo { def enterMeet(card: Card[Professor]): Unit = { //只有Card[Master]和它的子类才能进入会场 println("欢迎进入会场!") } def main(args: Array[String]): Unit = { val masterCard = new Card[Master] val professorCard = new Card[Professor] val teacherCard = new Card[Teacher] enterMeet(masterCard) enterMeet(professorCard) // enterMeet(teacherCard) } }
Akka是Java虚拟机平台上构建高并发、分布式和容错应用的工具包和运行时。
Akka用Scala语言编写,同时提供了Scala和Java的开发接口。
Akka处理并发的方法基于Actor模型,Actor之间通信的唯一机制就是消息传递。
Actor
Scala的Actor类似于Java中的多线程编程。
但是不同的是,Scala的Actor提供的模型与多线程有所不同。Scala的Actor尽可能地避免锁和共享状
态,从而避免多线程并发时出现资源争用的情况,进而提升多线程编程的性能。
Actor可以看作是一个个独立的实体,Actor之间可以通过交换消息的方式进行通信,每个Actor都有自
己的收件箱(Mailbox)。
一个Actor收到其他Actor的信息后,根据需要作出各种相应。消息的类型可以是任意的,消息的内容也
可以是任意的。
ActorSystem
在Akka中,ActorSystem是一个重量级的结构。
它需要分配多个线程,所以在实际应用中,ActorSystem通常是一个单例对象,我们可以使用这个
ActorSystem创建很多Actor。
Akka案例:
创建一个maven项目,在项目的pom文件中增加如下依赖:
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>ScalaMavenPro</artifactId> <version>1.0-SNAPSHOT</version> <!-- 定义一下常量--> <properties> <encoding>UTF-8</encoding> <scala.version>2.12.3</scala.version> <scala.compat.version>2.11</scala.compat.version> <akka.version>2.4.17</akka.version> </properties> <dependencies> <!-- 添加akka的actor依赖 --> <dependency> <groupId>org.scala-lang</groupId> <artifactId>scala-actors</artifactId> <version>2.11.8</version> </dependency> <!-- https://mvnrepository.com/artifact/com.typesafe.akka/akka-actor --> <dependency> <groupId>com.typesafe.akka</groupId> <artifactId>akka-actor_2.11</artifactId> <version>2.3.16</version> </dependency> <!-- 添加akka的actor依赖 --> <dependency> <groupId>com.typesafe.akka</groupId> <artifactId>akka-actor_${scala.compat.version}</artifactId> <version>${akka.version}</version> </dependency> <!-- 多进程之间的Actor通信 --> <!-- https://mvnrepository.com/artifact/com.typesafe.akka/akka-remote --> <dependency> <groupId>com.typesafe.akka</groupId> <artifactId>akka-remote_${scala.compat.version}</artifactId> <version>${akka.version}</version> </dependency> </dependencies> </project>
程序
import akka.actor.{Actor, ActorRef, ActorSystem, Props} import scala.io.StdIn //伴生类 class HelloActor extends Actor { //接收消息并进行处理 override def receive: Receive = { case "吃了吗" => println("吃过了") case "吃的啥" => println("北京烤鸭") case "拜拜" => { //关闭自己 context.stop(self) //关闭ActorSystem context.system.terminate() } } } object HelloActor { //通过ActorSystem创建线程池对象myFactory private val myFactory = ActorSystem("myFactory") //通过myFactory.actorOf来创建一个Actor private val helloActorRef: ActorRef = myFactory.actorOf(Props[HelloActor], "helloActor") def main(args: Array[String]): Unit = { var flag = true while (flag) { print("请输入发送的消息:") val consoleLine: String = StdIn.readLine() //!发送消息 helloActorRef ! consoleLine if (consoleLine.equals("拜拜")) { flag = false println("程序即将结束!") } //让程序休眠100毫秒 Thread.sleep(100) } } }
第一部分 Scala基础 第二部分 控制结构和函数 第三部分 数组和元组 第四部分 类与对象 第五部分 继承 第六部分 特质 第七部分 模式匹配和样例类 第八部分 函数及抽象化 第九部分 集合 第十部分 隐式机制 第十一部分 扩展部分