Scala学习
《快学Scala》心得体会
1 基础
1.1 声明和定义
Val无法改变它的内容,实际上就是一个常量
Var 声明其值可变的变量
在scala中,与java不同的是,变量或函数的类型总是写在变量或函数名称的后面。
1.2 常用类型
Scala有7种数值类型:Byte、Char、Short、Int、Long、Float、Double,以及一个Boolen类型。与java不同的是,在scala中不需要包装类型,编译器会自动完成基本类型和包装类型之间的转换。如 1.to(10) ;其实是Int值1首先被转换成RichInt,然后调用to方法。
1.3 Apply方法
在Scala中通常会使用类似函数调用的语法。
如”string”(4)其实是将()操作符进行了重载,”string”.apply(4)实际调用的apply方法,在StringOps类中。
2 控制结构和函数
2.1 一切都是对象
与java,c/c++不同的是,scala中Everything is an object,特别是函数也可以做参数。
在scala中,任何语句都有返回值,语句返回Unit
object TimerAnonymous {
def oncePerSecond(callback: () => Unit) {
while(true) { callback(); Thread sleep 1000 }
}
def main(args: Array[String]) {
oncePerSecond(() => println("time flies like an arrow..."))
}
}
2.2 懒值引进
当val被声明为lazy时,它的初始化会被延迟,直到我们首次对它取值。
应用到Non-Strict中,以及Stream,视图等待,同时spark中的RDD
优点:懒性求值,没用上的参数不会浪费计算资源
对于大数据量或者无限长的数据,有着良好的支持(增量计算)
缺点:重复计算(可通过缓存计算过的值改善)
使用不当,将会使逻辑难以推断(不知道参数到底用没用,或者在什么时候用)
3 数组,集合
3.1 可变与不可变
scala中一般都有可变与不可变的数组,集合等,分别在scala.collection.mutable和scala.collections.immutable中。
3.2 与java的互操作
由于scala是兼容java的,所以代码中会出现,scala代码中调用java中的变量,这是就需要一些转换操作,不然会出现编译错误。
如在写RncKpiJoinHourTaskSpec这个DT时:
import scala.collection.JavaConversions._ //不加这句就会报编译错误,应为usccpchBasics 是java文件中定义的
cellPmCounters.usccpchBasics = List(new UsccpchBasic(111, "D2"))
public List<UsccpchBasic> usccpchBasics;
public List<RrcestCause> rrcestCauseList;
3.3 常用变换
3.3.1 Map
map[B](f: (A) ⇒ B): List[B]
定义一个变换,把该变换应用到列表的每个元素中,原列表不变,返回一个新的列表数据
求平方例子
val nums = List(1,2,3)
val square = (x: Int) => x*x
val squareNums1 = nums.map(num => num*num) //List(1,4,9)
val squareNums2 = nums.map(math.pow(_,2)) //List(1,4,9)
val squareNums3 = nums.map(square) //List(1,4,9)
val text = List("Homeway,25,Male","XSDYM,23,Female")
val usersList = text.map(_.split(",")(0)) // List[String] = List(Homeway, XSDYM)
val usersWithAgeList = text.map(line => {
val fields = line.split(",")
val user = fields(0)
val age = fields(1).toInt
(user,age)
})// List[(String, Int)] = List((Homeway,25), (XSDYM,23))
3.3.2 flatMap&flatten
flatten: flatten[B]: List[B] 对列表的列表进行平坦化操作
flatMap: flatMap[B](f: (A) ⇒ GenTraversableOnce[B]): List[B] map之后对结果进行flatten
定义一个变换f, 把f应用列表的每个元素中,每个f返回一个列表,最终把所有列表连结起来。
val text = List("A,B,C","D,E,F")
val textMapped = text.map(_.split(",").toList) // List(List("A","B","C"),List("D","E","F"))
val textFlattened = textMapped.flatten // List("A","B","C","D","E","F")
val textFlatMapped = text.flatMap(_.split(",").toList) // List("A","B","C","D","E","F")
3.3.3 reduce& reduceLeft& reduceRight
reduce[A1 >: A](op: (A1, A1) ⇒ A1): A1
使用 reduce 我们可以处理列表的每个元素并返回一个值。通过使用 reduceLeft 和 reduceRight 我们可以强制处理元素的方向
定义一个变换二元操作,并应用的所有元素。
列表求和
val nums = List(1,2,3)
val sum1 = nums.reduce((a,b) => a+b) //6
val sum2 = nums.reduce(_+_) //6
val sum3 = nums.sum //6
reduceLeft: reduceLeft[B >: A](f: (B, A) ⇒ B): B
reduceRight: reduceRight[B >: A](op: (A, B) ⇒ B): B
reduceLeft从列表的左边往右边应用reduce函数,reduceRight从列表的右边往左边应用reduce函数
val nums = List(2.0,2.0,3.0)
val resultLeftReduce = nums.reduceLeft(math.pow) // = pow( pow(2.0,2.0) , 3.0) = 64.0
val resultRightReduce = nums.reduceRight(math.pow) // = pow(2.0, pow(2.0,3.0)) = 256.0
3.3.4 fold,foldLeft,foldRight
fold: fold[A1 >: A](z: A1)(op: (A1, A1) ⇒ A1): A1 带有初始值的reduce,从一个初始值开始,从左向右将两个元素合并成一个,最终把列表合并成单一元素。
foldLeft: foldLeft[B](z: B)(f: (B, A) ⇒ B): B 带有初始值的reduceLeft
foldRight: foldRight[B](z: B)(op: (A, B) ⇒ B): B 带有初始值的reduceRight
val nums = List(2,3,4)
val sum = nums.fold(1)(_+_) // = 1+2+3+4 = 9
val nums = List(2.0,3.0)
val result1 = nums.foldLeft(4.0)(math.pow) // = pow(pow(4.0,2.0),3.0) = 4096
val result2 = nums.foldRight(1.0)(math.pow) // = pow(1.0,pow(2.0,3.0)) = 8.0
3.3.5 filter, filterNot
filter: filter(p: (A) ⇒ Boolean): List[A]
filterNot: filterNot(p: (A) ⇒ Boolean): List[A]
filter 保留列表中符合条件p的列表元素 , filterNot,保留列表中不符合条件p的列表元素
val nums = List(1,2,3,4)
val odd = nums.filter( _ % 2 != 0) // List(1,3)
val even = nums.filterNot( _ % 2 != 0) // List(2,4)
4 类,对象,特质
4.1 与java中的区别
Trait有点类似于java中的interface,但也有不同之处。
①java的interface只定义方法名称和参数列表,不能定义方法体。而trait则可以定义方法体。
如
trait Friendly {
def greet() = "Hi"
}
②在java中实现接口用implement,而在scala中,实现trait用extends。
Scala中不再有implement这个关键词。但类似的,scala中的class可以继承0至多个traits。
class Dog extends Friendly {
override def greet() = "Woof"
}
此处,需要注意的一点是,与java不同,在scala中重写一个方法是需要指定override关键词的。如果重写一个方法时,没有加上override关键词,那么scala编译会无法通过。
③ java的interface和scala的trait的最大区别是,scala可以在一个class实例化的时候混合进一个trait。
trait Friendly {
def greet() = "Hi"
}
class Dog extends Friendly {
override def greet() = "Woof"
}
class HungryDog extends Dog {
override def greet() = "I'd like to eat my own dog food"
}
trait ExclamatoryGreeter extends Friendly {
override def greet() = super.greet() + "!"
}
var pet: Friendly = new Dog
println(pet.greet())
pet = new HungryDog
println(pet.greet())
pet = new Dog with ExclamatoryGreeter
println(pet.greet())
pet = new HungryDog with ExclamatoryGreeter
println(pet.greet())
4.2 在写DT时出现的一些问题
由于Scala中没有静态方法一说,于是产生了一种新的特性,单例对象,object,里面存放的就相当于java里面的静态方法,字段。
为了得到既有实例方法和静态方法的类,可以通过创建与类同名的对象来达到目的。
在公司的以前的一些业务代码中,由于没有写DT,发现现在在写时,有些单例类型Oject的方法无法写测试
object A ===>
object A extends A
class A{
}
然后在测试用例中,新建A类,并将不关心的方法进行overwirte为空,这样就可以在不影响其他业务的情况下也能完成DT
应为对外界其实也有个单例类型object A。
但是在这样处理时,要注意,Oject里面的样例类case class应该继续放到oject里面,其他内容放到class A里;应为如果不这样处理,那么后面得到的样例类实例化对象就会不同,达不到我们想要的结果。
假如定义了一个类Outer,在里面定义了样本类Inner,并持有一个Int值
scala> class Outer {
| case class Inner(value:Int)
| }
defined class Outer
scala> val outer1 = new Outer; val outer2 = new Outer
outer1: Outer = Outer@560245
outer2: Outer = Outer@af0d85
scala> val inner1 = new outer1.Inner(1); val inner2 = new outer2.Inner(1)
inner1: outer1.Inner = Inner(1) // 内部类赋值一样
inner2: outer2.Inner = Inner(1) // 但是,注意他们的类型是不一样的,一个是outer1.Inner,另一个是outer2.Inner,它是依赖于具体对象的一个类型
scala> inner1 == inner2 // 在比较的时候返回false,虽然它们持有的值都是1
res5: Boolean = false
这样就在处理一些合并时,本来以为Key是相同的记录不会合并在一起。
5 协变与逆变
5.1 Java中的泛型
已知String
是Object
的子类,从直觉上我们可能认为List<String>
应该是List<Object>
的子类型,但是看下面例子:
private static void printList(List<Object> list) {
for (Object obj : list) {
System.out.println(obj.toString());
}
}
publicstatic
void
main(String[]
args)
{
List<String>
names
=
new
ArrayList<String>();
names.add("aaa");
names.add("bbb");
printList(names);
// compile error!!!
}
在调用printList时,传入的是一个List<String>,需要的是一个List<Object>,如果List<String>是List<Object>的子类,这个地方应该可以编译通过,结果不是,说明他们之间没什么关系。但是为了满足一些客观现实情况,我们在scala中映入了协变和逆变的概率。
5.2 Scala中的协变和逆变
1.不变: class Box[T]
2.协变: class Box[+T]
如果A是B的子类,那么Box [A]也是Box [B]的子类型
一般情况下,在定义协变类型时,主要用于返回,不要用于方法的参数,否则可能出现下面的结果:
classBox[+T](v:
T)
{
def
get:
T
=
???
def
set(v:
T)
=
???
// 编译错误
}
valstringBox
=
new
Box[String]("abc")
valanyBox:
Box[Any]
=
stringBox
anyBox.set(123)
如果编译器不做这样的限制,我们可能把一个
int
型的数据插入到需要
String
的
Box
中。
3.逆变: class Box[-T]
如果A是B的超类,那么Box [A]是Box [B]的子类型 逆变
classBox[-T](v:
T)
{
def
get:
T
=
???
// compile error
def
set(v:
T)
=
???
}
--
valanyBox
=
new
Box[Any](123)
valstringBox:
Box[String]
=
anyBox
stringBox.get
如果不做返回值的限制,那么我们可能就会从
string
的盒子拿出一个
int
值,有违常理。
破坏了我们的类型系统。所以逆变的类型参数一般放在方法的参数,而不是返回值。
下面我们看一个协变的应用:
定义一个泛型类型Friend[T],表示希望与类型T的人成为朋友的人。
Trait Friend[T]{
def befriend(someone:T)
}
有这样一个函数
def makeFriendWith(s: Student,f: Friend[Student]){f.befriend(s)}
其中,
class Person extends Friend[Person]
class Student extends Person
val susan = new Student
val fred = new Person
客观上,fred可以和任何Person做朋友,那么就应该可以喝任何student做朋友,所以,我们要求函数调用makeFriendWith(susan,fred)是可以的,即Friend[Person]应该是Friend[Student]的子类型,于是我们将Friend定义为逆变的就可以达到要求:
Trait Friend[-T]{
def befriend(someone:T)
}
5.3 类型变量界定
协变逆变与上下界的不同,上下界表明传入的参数类型是满足么个类型的超类或者子类,那么方法中就应该具备一些方法。
比如需要对一个Pair类型中的两个组件进行比较时:
class Pair[T](val first:T,val second: T){
def smaller = if(first.compareTo(second<0)first else second
}
但是这样会报错,因为T是个泛型,不知道他是否有compareTo这个方法。这个时候我们就可以采用上界的方法来解决这个问题:
class Pair[T<:Comparable[T]](val first:T,val second: T){
def smaller = if(first.compareTo(second<0)first else second
}
这样就限制了T必须是Comparable[T]的子类型,肯定具备compareTo方法。类似的下界T<:Comparable[T]要求T必须是Comparable[T]的超类型。