scala demo
scala特性
- 纯面向对象
- 函数式编程,函数为一等公民
- 静态类型,通过编译时检查保证代码安全性,例如泛型,复合引用,协变与逆变等
- 扩展性,scala语言的名称来源于
Scalable
可扩展的,可扩展语义可以通过implicit
实现 - 并发性,scala使用Actor作为并发模型,类似于JAVA中的Thread,使用Akka作为默认Actor的实现
scala & java
scala优点
- 函数是一等公民,所有的函数都是一个输入参数输出结果的过程,没有任何副作用与状态无关,由于这种特性在分布式中有很多好处
- 更加的灵活,例如在扩展第三方类库的时候不需要继承,通过implicit实现,声明一个延迟加载的单例不需要重复检查,直接使用lazy object修饰就可以
- Actor的并发模型,并发编程写起来更简单
- 更多的语法糖代码简洁
scala缺点
- 生态小
- 语法多导致可读性差
语法上的差异
- scala不需要返回值
- scala不需要分号结尾
- scala类不需要大写,类上可以直接传参指定默认值
- scala可以使用maven管理项目
- scala有spark-shell来交互式的测试代码
数据类型
除了和java对应的数据类型外,scala还有Unit,Null,Nothing,Any,AnyRef类型,详情。
数据类型 | 描述 |
---|---|
Unit | 表示无值,和其他语言中void等同。用作不返回任何结果的方法的结果类型。Unit只有一个实例值,写成()。 |
Null | Null是所有AnyRef的子类,不能赋值给值类型(值类型父类是AnyVal,和AnyRef平级) |
Any | Any是所有其他类的超类 |
AnyRef | AnyRef类是Scala里所有引用类(reference class)的基类,AnyRef是Any的子类 |
AnyVal | Any子类,对应java值类型的所有类型都是AnyVal的子类 |
Nothing | Nothing类型在Scala的类层级的最低端;它是任何其他类型的子类型。Nothing没有对象,但是可以用来定义类型。e.g.,如果一个方法抛出异常,则异常的返回值类型就是Nothing |
变量
- val静态变量,var动态变量
- 变量不一定非要指定类型,scala可以通过遍历或者常量的初始值来推断变量类型。
- 变量必须初始化值,或者赋值为null
- 同时声明多个变量:
val xmax, ymax = 100
,xmax, ymax都声明为100。
修饰符
- scala修饰符比java的更严格,默认为public,但是可以通过作用域保护机制放宽限制,eg:
private[x]
- private:类的内部对象和内部类可见,子类不可见
- protected:子类可见,同一个包不可见
- public:均可见
函数
- scala提供了函数参数调用和函数名调用
- 可变参数:通过例如
foo(args:String*)
的形式代表可以传入多个String类型的参数 - 函数默认值:可通过在方法定义时,指定参数初始值,
foo(a:Int=1,b:Int=2)
- 匿名函数:
val inc=(x:Int)=>x+1
,调用var x=inc(1)+2
- 偏应用函数:函数
foo(x:Int,y:Int)
,偏函数def p_foo=foo(1,_x:Int)
,调用p_foo(2)
等价于调用foo(1,2)
,类似于java中对函数重写,然后指定了某个参数的默认值。 - 高阶函数:操作其他函数的函数,即使用函数作为入参或者出参。
- 函数柯里化:柯里化(Currying)指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数为参数的函数。
- 闭包:一个函数,一个可以访问另一个函数里面的局部变量的函数
高阶函数
高阶函数(Higher-Order Function)就是操作其他函数的函数。
Scala 中允许使用高阶函数, 高阶函数可以使用其他函数作为参数,或者使用函数作为输出结果。
以下实例中,apply() 函数使用了另外一个函数 f 和 值 v 作为参数,而函数 f 又调用了参数 v:
object Test {
def main(args: Array[String]) {
println( apply( layout, 10) )
}
// 函数 f 和 值 v 作为参数,而函数 f 又调用了参数 v
def apply(f: Int => String, v: Int) = f(v)
def layout[A](x: A) = "[" + x.toString() + "]"
}
执行以上代码,输出结果为:
$ scalac Test.scala
$ scala Test
[10]
柯里化
柯里化(Currying)指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数为参数的函数。
首先我们定义一个函数:
def add(x:Int,y:Int)=x+y
那么我们应用的时候,应该是这样用:add(1,2)
现在我们把这个函数变一下形:
def add(x:Int)(y:Int) = x + y
那么我们应用的时候,应该是这样用:add(1)(2),最后结果都一样是3,这种方式(过程)就叫柯里化。
实现过程
add(1)(2) 实际上是依次调用两个普通函数(非柯里化函数),第一次调用使用一个参数 x,返回一个函数类型的值,第二次使用参数y调用这个函数类型的值。
实质上最先演变成这样一个方法:
def add(x:Int)=(y:Int)=>x+y
那么这个函数是什么意思呢? 接收一个x为参数,返回一个匿名函数,该匿名函数的定义是:接收一个Int型参数y,函数体为x+y。现在我们来对这个方法进行调用。
val result = add(1)
返回一个result,那result的值应该是一个匿名函数:(y:Int)=>1+y
所以为了得到结果,我们继续调用result。
val sum = result(2)
最后打印出来的结果就是3。详情
闭包
闭包是一个函数,一个包含了外部变量的函数,普通的函数体可以看做是一个闭合语,如果函数体引用了外部变量,在执行时就需要去捕获外部变量进行绑定,将整个过程称为闭包。
在Scala中,函数引入传入的参数是再正常不过的事情了,比如 (x: Int) => x > 0
中,唯一在函数体x > 0
中用到的变量是x,即这个函数的唯一参数。
除此之外,Scala还支持引用其他地方定义的变量: (x: Int) => x + more
,这个函数将more
也作为入参,不过这个参数是哪里来的?从这个函数的角度来看,more是一个自由变量,因为函数字面量本身并没有给more赋予任何含义。相反,x是一个绑定变量,因为它在该函数的上下文里有明确的定义:它被定义为该函数的唯一参数。如果单独使用这个函数字面量,而没有在任何处于作用域内的地方定义more,编译器将报错:
scala> (x: Int) => x + more
<console>:12: error: not found: value more
(x: Int) => x + more
另一方面,只要能找到名为more的变量,同样的函数字面量就能正常工作:
scala> var more = 1
more: Int = 1
scala> val addMore = (x: Int) => x + more
addMore: Int => Int = $$Lambda$1104/583744857@33e4b9c4
scala> addMore(10)
res0: Int = 11
运行时从这个函数字面量创建出来的函数值(对象)被称为闭包。该名称源于“捕获”其自由变量从而“闭合”该函数字面量的动作。没有自由变量的函数字面量,比如(x: Int) => x + 1
,称为闭合语(这里的语指的是一段源代码)。因此,运行时从这个函数字面量创建出来的函数值严格来说并不是一个闭包,因为(x: Int) => x + 1
按照目前这个写法已经是闭合的了。而运行时从任何带有自由变量的函数字面量,比如(x: Int) => x + more
创建的函数,按照定义,要求捕获到它的自由变量more的绑定。相应的函数值结果(包含指向被捕获的more变量的引用)就被称为闭包,因为函数值是通过闭合这个开放语的动作产生的,详情。
与 Java 中使用内部类实现闭包相比,Scala 中为函数创建了一个对象 Function1 来保存变量的状态,然后具体执行的时候调用对应实例的 apply 方法,实现了函数作用域外也可以访问函数内部的变量,spark中闭包应用。
Option
Scala Option(选项)类型用来表示一个值是可选的(有值或无值),返回的是一个None或者Some对象。
Option[T] 是一个类型为 T 的可选值的容器: 如果值存在, Option[T] 就是一个 Some[T] ,如果不存在, Option[T] 就是对象 None 。
None和Some是Object的子类型
类
- 类名称可以小写
- Scala中的类不声明为public,一个Scala源文件中可以有多个类
- 主构造器:参数会直接跟在类名后面,最后会分别编译成字段
- 主构造器在执行时会执行类中所有语句
- 附属构造器的名称为this
- 每个附属构造器,必须先调用已经存在的父构造器
- 类构造器参数不带val或者var声明时相当于private[this],只能在内部使用
- 子类引用父类属性时不需要加val,
class Sdudent(val major:String)extends Person(name,age)
- 子类重写非抽象父类属性和方法时必须加
override
- 使用了case关键字的类定义就是就是样例类(case classes),样例类是种特殊的类,经过优化以用于模式匹配。
继承
- 重写一个非抽象方法必须使用override修饰符
- 重写抽象方法不需要使用override修饰符
- 只有主构造器才可以往基类的构造函数里面写参数
单例对象 & 伴生对象
scala里面没有static,如果需要实现单例模式可使用Object关键字,object对象不能带参数,但是可以访问同名类里面的参数,这个对象被称为伴生对象,类被称为这个单例的伴生类,类和其他伴生对象可互相访问其私有成员详情
- Object类比于java中的static关键字,注意:scala中static不是关键字
- 一个Object对象只实例化一次,且是被引用的时候进行加载
- 可以用Object实现工厂方法
StringBuilder的伴生对象生成了一个builder对象
object StringBuilder {
def newBuilder = new StringBuilder
}
特征
Scala Trait(特征) 相当于 Java 的接口,实际上它比接口还功能强大。与接口不同的是,它还可以定义属性和方法的实现,更加类似于java中的抽象类概念。
- 可以有具体的方法实现
- 类似于java中的接口,定义方法
- 既可以使用extends也可以使用with继承trait
- 可以在初始化对象时加trait,例如:
new TraitC with ObjectTrait
- 可以将一个特征通过this指针汇合到另一个特征里面去,但是在使用是必须同时继承两个trait
trait Users{
def username:String
}
trait Tweeter{
this:Users =>
def tweet(tweeText:String) = println(s"$username:$tweeText")
}
class VerifiedTweeter(val username_ :String) extends Tweeter with Users {
def username =s"real $username_"
}
特征构造顺序
先父类,再父特征,特征从左到右构造,特征不会重复构造
构造器的执行顺序:
- 调用超类的构造器;
- 特征构造器在超类构造器之后、类构造器之前执行;
- 特征由左到右被构造;
- 每个特征当中,父特征先被构造;
- 如果多个特征共有一个父特征,父特征不会被重复构造
- 所有特征被构造完毕,子类被构造。
trait和abstract class
- 一个类只能集成一个抽象类,但是可以通过with关键字继承多个特质;
- 抽象类有带参数的构造函数,特质不行(如 trait t(i:Int){} ,这种声明是错误的)
模式匹配
- case类最少包括: 关键词(case class)、类名、一组属性(可以为空)
- case class 不需要使用new进行初始化,因为case类有apply方法默认进行初始化
- case类的属性是 公有的,静态的,如果执行
caseClass.prop="xxx"
去改变值时编译报错 - case类是值比较(构成),不是引用
- 在case+mach中可以使用表达式:
case pattern if boolean => rst
- case class
样本类是一种不可变且可分解类的语法糖,这个语法糖的意思大概是在构建时,自动实现一些功能,在定一个case类C时,会生成一个伴生对象,这个伴生对象实现了apply和unapply方法。样本类具有以下特性:
- 自动添加与类名一致的构造函数(这个就是前面提到的伴生对象,通过apply方法实现),即构造对象时,不需要new;
- 样本类中的参数默认添加val关键字,即参数不能修改;
- 默认实现了toString,equals,hashcode,copy等方法;
- 样本类可以通过==比较两个对象,并且不在构造方法中定义的属性不会用在比较上。
//声明一个样本类
case class MyCaseClass(number: Int, text: String, others: List[Int]){
println(number)
}
//不需要new关键字,创建一个对象
val dto = MyCaseClass(3, "text", List.empty) //打印结果3
//利用样本类默认实现的copy方法
dto.copy(number = 5) //打印结果5
val dto2 = MyCaseClass(3, "text", List.empty)
pringln(dto == dto2) // 返回true,两个相同的引用对象
class MyClass(number: Int, text: String, others: List[Int]) {}
val c1 = new MyClass(1, "txt", List.empty)
val c2 = new MyClass(1, "txt", List.empty)
println(c1 == c2 )// 返回false,两个不同的引用对象
提取器
apply & unpply
- 当对象(伴生对象)以函数的方式进行调用时,scala 会隐式地将调用改为在该对象上调用apply方法。
- 当方法调用apply方法时就是在调用其本身
- unapply方法是apply方法的反向操作,apply方法接受构造参数变成对象,而unapply方法接受一个对象,从中提取值。
class Currency(val value: Double, val unit: String) {}
object Currency{
// 伴生对象的apply方法实现了构造器
def apply(value: Double, unit: String): Currency = new Currency(value, unit)
// 从对象中提取具体属性值
def unapply(currency: Currency): Option[(Double, String)] = {
if (currency == null){
None
}
else{
Some(currency.value, currency.unit)
}
}
}
// 在构建对象的时候就可以直接使用,这种方式,不用使用new。
val currency = Currency(30.2, "EUR")
// unpply方法常用语模式匹配,case函数默认调用了unpply方法
currency match {
case Currency(amount, "USD") => println("$" + amount)
case _ => println("No match.")
}
集合
- 可变集合与不可变集合的区别是在对集合对进行增删改操作时是否产生一个新集合
- 集合与元组的区别是元组的元素类型不需要一致
添加元素 | 集合操作 | |
---|---|---|
List/Queue | :+/+: | ::/::: |
Map/Set/BitSet/TreeSet/ListSet | +/- | ++ |
特殊情况
- 将单个元素作为List对象,使用::拼接成单个list,
"a"::"b"::List()
,返回List(a, b)
- Map创建:
var map:Map[String,String]=Map("key1"->"val1","key2"->"val2")
- Queue操作:
queue.enqueue("e");queue.dequeue
- Set:交集
set1&set2
;差集set1&~set2
代表Set1中仅有元素,差集set2&~set1
代表Set2中仅有元素
object SetTest extends App{
val set1=Set("a","b","c")
val set2=Set("a","b","d")
println(set1 & set2) // Set(a, b)
println(set1 &~ set2) // Set(c)
println(set2 &~ set1) // Set(d)
println(set2("c")) // false
}
SortedSet & TreeSet
- SortedSet和TreeSet默认为升序排序
- 可继承Ordering类实现compare方法自定义比较器
自定义比较器
object SortedSetTest extends App{
val sortedSet1=SortedSet("2","1","3")
println(sortedSet1) // TreeSet(1, 2, 3) // 默认升序
val treeSet1=TreeSet("2","1","3")
println(treeSet1) // TreeSet(1, 2, 3)
// 继承Ordering实现conpare方法自定义比较器
object CustomOrderOfSortedSet extends Ordering[Int]{
def compare(element1:Int,element2:Int)= {if (element1>element2) -1 else 1}
}
// 使用自定义比较器
val sortedSet2=SortedSet(2,1,3)(CustomOrderOfSortedSet)
println(sortedSet2) // TreeSet(3, 2, 1)
}
Array:定长数组
- 将array转字符串:
array.mkString("-")
- 定义二维数组:
Array.ofDim(2,2)
- 定义三维数组:
Array.ofDim(2,2,2)
- 数组元素比较:
arrayA.sameElements(arrayB)
- 数组合并:
arrayA.concat(arrayB)
- 数组复制:
Array.copy(src,srcPos,dest,destPos,Len)
- 创建区间数组:
Array.range(start,end,step)
- 创建指定长度数组,初始值为零,自定义每个元素的计算方法:
Array.tabulate(3)(_+3)
ArrayBuffer:变长数组
var c=new ArrayBuffer[Int]()
- 添加一个元素:c+=2
- 添加一组:c+=(3,4,5)
- 添加集合:c++=Array(6,7,8)
- 指定位置添加:c.insert(3,9),在下标3之前插入9
- 移除尾部N个元素:c.trimEnd(n)
- 移除尾部某个位置元素:c.trimStart(n)
- 移除中间一部分元素:c.remove(3,2)从下标
List
Nil是一个空的List,定义为List[Nothing],根据List的定义List[+A],所有Nil是所有List[T]的子类。
val L1 = 1 :: 2 :: 3 :: Nil
//output: List(1, 2, 3)
上面的L1定义如果没有Nil,Scala类型系统无法猜出L1的类型。那么 成员方法 '::' (把元素添加到List方法)就会出现找不到方法的编译错误。
不需要new的场景
Object,伴生对象,case class
关于 _
- 在package中代表通配符:
import scala.math._
- 在参数类型中代表可变长参数
- 在模式匹配中代表什么都可以匹配
- 在成员变量中代表初始化对象值为空类似于java中null,eg:
var s:String=_
- 在函数中作为占位符
Implicit
使用implicit可以不通过继承的方式,将一个类增强为另一个类,实现对类的方法扩展等成为隐式转换,其中包含了隐式类和隐式函数。
class A{}
// 隐式类用例:
// 1.1 在自定义类RichA中,将类A以参数形式传入
class RichA(a:A){
def rich(): Unit ={
println("隐式转换")
}
}
object ImplicitDemo extends App {
// 1.2 用implicit关键词初始化RichA——implicit关键字将类A隐式转换为类RichA
implicit def a2RichA(a: A) = new RichA(a)
val a = new A
// 1.3 在类A中可以直接调用RichA中定义的方法
a.rich()
// 2.1 在定义类的时候直接使用implicit关键字
implicit class Calculator(x: Int) {
def add(a: Int): Int = a + 1
}
// 2.2 类Calculator类实现了add方法,int类型的变量可直接使用
println(1.add(1))
// -----------------------* 限制Restrictions start *------------------------
// 3.1 implicit 类必须定义在 trait/class/object的里面
// object Helpers {
// implicit class RichInt(x: Int) // OK!
// }
//implicit class RichDouble(x: Double) // BAD!
// 3.2 implicit类只允许有一个非隐式参数
//implicit class RichDate(date: java.util.Date) // OK!
//implicit class Indexer[T](collecton: Seq[T], index: Int) // BAD!
//implicit class Indexer[T](collecton: Seq[T])(implicit index: Index) // OK!
// -----------------------* 限制Restrictions end *--------------------------
// --------------------* 隐式参数 start *--------------------
def testParam(implicit name: String): Unit = {
println(name)
}
implicit val name = "隐式参数"
testParam("显式参数")
testParam
// ---------------------* 隐式参数 end *---------------------
}
在Scala中implicit的功能很强大。当编译器寻找implicits时,如果不注意隐式参数的优先权,可能会引起意外的错误。因此编译器会按顺序查找隐式关键字。顺序如下:
- 当前类声明的implicits ;
- 导入包中的 implicits;
- 外部域(声明在外部域的implicts);
- inheritance
- package object
- implicit scope like companion objects
yield
yield用于循环迭代中生成新值,yield是comprehensions的一部分,是多个操作(foreach, map, flatMap, filter or withFilter)的composition语法糖。
comprehension
vaule class
语法
1 to 10
包含10,for(a <- 1 until 10)
,range(1,10)
不包含10- StringBuilder添加一个字符
stringBuilder+='c'
,添加字符串sb++="str"
- 模式匹配:
x match {case pattern if boolen-> rst
def showNotification(notification: Notification):String={
notification match {
case Email(sender,_,msg) if(!sender.equals("scala@qq.com"))=> s"email"
case Sms(sender,msg) => s"you got an sms from $sender,msg is :$msg"
case Tel(phone,msg) if 111==phone => s"only $phone"
case _ => ""
}
}
方法与函数
Scala 有方法与函数,二者在语义上的区别很小。Scala 方法是类的一部分,而函数是一个对象可以赋值给一个变量。换句话来说在类中定义的函数即是方法。详情
- Scala 中的方法跟 Java 的类似,方法是组成类的一部分。
- Scala 中的函数则是一个完整的对象,Scala 中的函数其实就是继承了 Trait 的类的对象。
- Scala 中使用 val 语句可以定义函数,def 语句定义方法。
区别
- 函数可作为一个参数传入方法中,而方法不行
- 在Scala中无法直接操作方法,如果要操作方法,必须先将其转换成函数,转换方法:
// 方法一:在方法名称后面跟上一个下划线
val f1=m _
// 方法二:显式的告诉编译器需要将方法转换为函数
val f1:(Int)=>Int = m
- 函数必须有参数列表,而方法可以没有参数列表
def m1=100
def m2()=100
val f1=()=>100
val f2== => 1000// 报错
call-by-value和call-by-name
-
f(x:Int)
:call by value,在调用f时先计算x的值,再执行方法体,现象为如果方法体里面多次调用了x,x仅仅计算一次 -
f(x:=>Int)
:call by name,在方法体需要x时才计算x的值,即如果入参是一个方法,则在方法体里面需要这个参数的地方都需要调用一次计算x值的方法,详情 -
lazy val
:lazy val 也是延迟加载,但是只初始化一次
理解函数是一等公民
狭义地说,函数式编程没有可变的变量、循环等这些命令式编程方式中的元素,像数学里的函数一样,对于给定的输入,不管你调用该函数多少次,永远返回同样的结果。而在我们常用的命令式编程方式中(例如java里面定义的对象变量等),变量用来描述事物的状态,整个程序,不过是根据不断变化的条件来维护这些变量。
广义地说,函数式编程重点在函数,函数是这个世界里的一等公民,函数和其他值一样,可以到处被定义,可以作为参数传入另一个函数,也可以作为函数的返回值,返回给调用者。利用这些特性,可以灵活组合已有函数形成新的函数,可以在更高层次上对问题进行抽象。
函数式编程优点
首先,函数式编程天然有并发的优势。由于工艺限制,摩尔定律已经失效,芯片厂商只能采取多核策略。程序要利用多核运算,必须采取并发,而并发最头疼的问题就是共享数据,狭义的函数式编程没有可变的变量,数据只读不写,并发的问题迎刃而解。这也是前面两篇文章中,一直建议大家在定义变量时,使用 val 而不是 var 的原因。爱立信公司发明的 Erlang 语言就是为解决并发的问题而生,在电信行业取得了不俗的成绩。
其次,函数式编程有迹可寻。由于不依赖外部变量,给定输入函数的返回结果永远不变,对于复杂的程序,我们可以用值替换的方式(substitution model)化繁为简,轻松得出一段程序的计算结果。为这样的程序写单元测试也很方便,因为不用担心环境的影响。
最后,函数式编程高屋建瓴。写程序最重要的就是抽象,不同风格的编程语言为我们提供了不同的抽象层次,抽象层次越高,表达问题越简洁,越优雅。读者从下面的例子中可以看到,使用函数式编程,有一种高屋建瓴的感觉。详情