快学Scala读书笔记

Scala与Java的关系

Scala与Java的关系是非常紧密的!!

因为Scala是基于Java虚拟机,也就是JVM的一门编程语言。所有Scala的代码,都需要经过编译为字节码,然后交由Java虚拟机来运行。

所以Scala和Java是可以无缝互操作的。Scala可以任意调用Java的代码。所以Scala与Java的关系是非常非常紧密的。

安装Scala

·从Scala官方网站下载,http://www.scala-lang.org/download/,windows版本的安装包是scala-2.11.7.msi。

·使用下载下来的安装包安装Scala。

·在PATH环境变量中,配置$SCALA_HOME/bin目录。

·在windows命令行内即可直接键入scala,打开scala命令行,进行scala编程。

Scala解释器的使用

·REPLRead(取值)-> Evaluation(求值)-> Print(打印)-> Loop(循环)。scala解释器也被称为REPL,会快速编译scala代码为字节码,然后交给JVM来执行。

·计算表达式:在scala>命令行内,键入scala代码,解释器会直接返回结果给你。如果你没有指定变量来存放这个值,那么值默认的名称为res,而且会显示结果的数据类型,比如Int、Double、String等等。

·例如,输入1 + 1,会看到res0: Int = 2

·内置变量:在后面可以继续使用res这个变量,以及它存放的值。

·例如,2.0 * res0,返回res1: Double = 4.0

·例如,"Hi, " + res0,返回res2: String = Hi, 2

·自动补全:在scala>命令行内,可以使用Tab键进行自动补全。

·例如,输入res2.to,敲击Tab键,解释器会显示出以下选项,toCharArray,toLowerCase,toString,toUpperCase。因为此时无法判定你需要补全的是哪一个,因此会提供给你所有的选项。

·例如,输入res2.toU,敲击Tab键,直接会给你补全为res2.toUpperCase。

声明变量

·声明val变量:可以声明val变量来存放表达式的计算结果。

·例如,val result = 1 + 1

·后续这些常量是可以继续使用的,例如,2 * result

·但是常量声明后,是无法改变它的值的,例如,result = 1,会返回error: reassignment to val的错误信息。

·声明var变量:如果要声明值可以改变的引用,可以使用var变量。

·例如,val myresult = 1,myresult = 2

·但是在scala程序中,通常建议使用val,也就是常量,因此比如类似于spark的大型复杂系统中,需要大量的网络传输数据,如果使用var,可能会担心值被错误的更改。

·在Java的大型复杂系统的设计和开发中,也使用了类似的特性,我们通常会将传递给其他模块 / 组件 / 服务的对象,设计成不可变类(Immutable Class)。在里面也会使用java的常量定义,比如final,阻止变量的值被改变。从而提高系统的健壮性(robust,鲁棒性),和安全性。

·指定类型:无论声明val变量,还是声明var变量,都可以手动指定其类型,如果不指定的话,scala会自动根据值,进行类型的推断。

·例如,val name: String = null

·例如,val name: Any = "leo"

·声明多个变量:可以将多个变量放在一起进行声明。

·例如,val name1, name2:String = null

·例如,val num1, num2 = 100

数据类型与操作符

·基本数据类型:Byte、Char、Short、Int、Long、Float、Double、Boolean。

·乍一看与Java的基本数据类型的包装类型相同,但是scala没有基本数据类型与包装类型的概念,统一都是类。scala自己会负责基本数据类型和引用类型的转换操作。

·使用以上类型,直接就可以调用大量的函数,例如,1.toString(),1.to(10)。

·类型的加强版类型: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.

函数调用与apply()函数

·函数调用方式:在scala中,函数调用也很简单。

·例如,import scala.math._,sqrt(2),pow(2, 4),min(3, Pi)。

·不同的一点是,如果调用函数时,不需要传递参数,则scala允许调用函数时省略括号的,例如,"Hello World".distinct

·apply函数

    ·Scala中的apply函数是非常特殊的一种函数,在Scala的object中,可以声明apply函数。而使用“类名()”(严格来讲应该是“对象名()”)的形式,其实就是“类名.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类的实例,也就是一个数组。


if表达式

·if表达式的定义:在Scala中,if表达式是有值的,就是if或者else中最后一行语句返回的值。

    ·例如,val age = 30; if (age > 18) 1 else 0

    ·可以将if表达式赋予一个变量,例如,val isAdult = if (age > 18) 1 else 0

    ·另外一种写法,var isAdult = -1; if(age > 18) isAdult = 1 else isAdult = 0,但是通常使用上一种写法

·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 ()。

·将if语句放在多行中:默认情况下,REPL只能解释一行语句,但是if表达式通常需要放在多行。

    ·可以使用{}的方式,比如以下方式,或者使用:paste和ctrl+D的方式。

if(age > 18) { "adult"

} else if(age > 12) "teenager" else "children"

语句终结符、块表达式

·默认情况下,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 }

输入和输出

·print和printlnprint打印时不会加换行符,而println打印时会加一个换行符。

    ·例如,print("Hello World"); println("Hello World")

·printfprintf可以用于进行格式化

    ·例如,printf("Hi, my name is %s, I'm %d years old.\n", "Leo", 30)

·readLine: readLine允许我们从控制台读取用户输入的数据,类似于java中的System.in和Scanner的作用。

·综合案例:游戏厅门禁

val name = readLine("Welcome to Game House. Please tell me your name: ")

print("Thanks. Then please tell me your age: ")

val age = readInt()

if(age > 18) {

  printf("Hi, %s, you are %d years old, so you are legel to come here!", name, age)

} else {

  printf("Sorry, boy, %s, you are only %d years old. you are illegal to come here!", name, age)

}

循环

·while do循环:Scala有while do循环,基本语义与Java相同。

var n = 10

while(n > 0) {

  println(n)

  n -= 1

}

·Scala没有for循环,只能使用while替代for循环,或者使用简易版的for语句

    ·简易版for语句:var n = 10; for(i <- 1 to n) println(i)

    ·或者使用until,表式不达到上限:for(i <- 1 until n) println(i)

 ·也可以对字符串进行遍历,类似于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) {

    println(i * j)

  } else {

    print(i * j + " ")

  }

}

·if守卫:取偶数

for(i <- 1 to 100 if i % 2 == 0) println(i)

·for推导式:构造集合

for(i <- 1 to 10) yield i


函数的定义与调用

在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("leo", 30)

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

}

递归函数与返回类型

如果在函数体内递归调用函数自身,则必须手动给出函数的返回类型。

例如,实现经典的斐波那契数列:

9 + 8; 8 + 7 + 7 + 6; 7 + 6 + 6 + 5 + 6 + 5 + 5 + 4; ....

def fab(n: Int): Int = {

  if(n <= 1) 1

  else fab(n - 1) + fab(n - 2)

}

默认参数

在Scala中,有时我们调用某些函数时,不希望给出参数的具体值,而希望使用参数自身默认的值,此时就定义在定义函数时使用默认参数。

def sayHello(firstName: String, middleName: String = "William", lastName: String = "Croft") = firstName + " " + middleName + " " + lastName

如果给出的参数不够,则会从左往右依次应用默认参数。

def sayHello(name: String, age: Int = 20) {

  print("Hello, " + name + ", your age is " + age)

}

sayHello("leo")

函数调用时带名参数

在调用函数时,也可以不按照函数定义的参数顺序来传递参数,而是使用带名参数的方式来传递。

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

}

sum(1, 2, 3, 4, 5)

序列作为变长参数

在如果想要将一个已有的序列直接调用变长参数函数,是不对的。比如val s = sum(1 to 5)。此时需要使用Scala特殊的语法将参数定义为序列,让Scala解释器能够识别。这种语法非常有用!一定要好好主意,在spark的源码中大量地使用到了。

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("C://Users//Administrator//Desktop//test.txt").mkString

即使文件不存在,也不会报错,只有第一个使用变量时会报错,证明了表达式计算的lazy特性。


val lines = fromFile("C://Users//Administrator//Desktop//test.txt").mkString

lazy val lines = fromFile("C://Users//Administrator//Desktop//test.txt").mkString

相当于定义了一个方法,只有在调用该方法时才会去执行方法体:

def lines = fromFile("C://Users//Administrator//Desktop//test.txt").mkString

异常

在Scala中,异常处理和捕获机制与Java是非常相似的。

try {

  throw new IllegalArgumentException("x should not be negative")

} catch {

  case _: IllegalArgumentException => println("Illegal Argument!")

} finally {

  print("release resources!")

}


Import java.io._

try {

  throw new IOException(“io exception!!!")

} catch {

  case _: IllegalArgumentException => println("illegal argument")

}


try {

  throw new IOException("user defined exception")

} catch {

  case e1: IllegalArgumentException => println("illegal argument")

  case e2: IOException => println("io exception")

}

Array

在Scala中,Array代表的含义与Java中类似,也是长度不可改变的数组。此外,由于Scala与Java都是运行在JVM中,双方可以互相调用,因此Scala数组的底层实际上是Java数组。例如字符串数组在底层就是Java的String[],整数数组在底层就是Java的Int[]。

// 数组初始化后,长度就固定下来了,而且元素全部根据其类型初始化

val a = new Array[Int](10)

a(0)

a(0) = 1

val a = new Array[String](10)

// 可以直接使用Array()创建数组,元素类型自动推断

val a = Array("hello", "world")

a(0) = "hi"

val a = Array("leo", 30)

ArrayBuffer

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

// 如果不想每次都使用全限定名,则可以预先导入ArrayBuffer类

import scala.collection.mutable.ArrayBuffer

// 使用ArrayBuffer()的方式可以创建一个空的ArrayBuffer

val b = ArrayBuffer[Int]()

// 使用+=操作符,可以添加一个元素,或者多个元素

// 这个语法必须要谨记在心!因为spark源码里大量使用了这种集合操作语法!

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.quickSort(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 <- 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 foundFirstNegative = false

var arrayLength = a.length

var index = 0

while (index < arrayLength) {

  if (a(index) >= 0) {

    index += 1

  } else {

    if (!foundFirstNegative) { foundFirstNegative = true; index += 1 }

    else { a.remove(index); arrayLength -= 1 }

  }

}

算法案例:移除第一个负数之后的所有负数(改良版)

// 重新构建数组

val a = ArrayBuffer[Int]()

a += (1, 2, 3, 4, 5, -1, -3, -5, -9)

// 每记录所有不需要移除的元素的索引,稍后一次性移除所有需要移除的元素

// 性能较高,数组内的元素迁移只要执行一次即可

var foundFirstNegative = false

val keepIndexes = for (i <- 0 until a.length if !foundFirstNegative || a(i) >= 0) yield {

  if (a(i) < 0) foundFirstNegative = true

  i

}

for (i <- 0 until keepIndexes.length) { a(i) = a(keepIndexes(i)) }

a.trimEnd(a.length - keepIndexes.length)

创建Map

// 创建一个不可变的Map

val ages = Map("Leo" -> 30, "Jen" -> 25, "Jack" -> 23)

ages("Leo") = 31

// 创建一个可变的Map

val ages = scala.collection.mutable.Map("Leo" -> 30, "Jen" -> 25, "Jack" -> 23)

ages("Leo") = 31

// 使用另外一种方式定义Map元素

val ages = Map(("Leo", 30), ("Jen", 25), ("Jack", 23))

// 创建一个空的HashMap

val ages = new scala.collection.mutable.HashMap[String, Int]

访问Map的元素

// 获取指定key对应的value,如果key不存在,会报错

val leoAge = ages("Leo")

val leoAge = ages("leo")

// 使用contains函数检查key是否存在

val leoAge = if (ages.contains("leo")) ages("leo") else 0

// getOrElse函数

val leoAge = ages.getOrElse("leo", 0)

修改Map的元素

// 更新Map的元素

ages("Leo") = 31

// 增加多个元素

ages += ("Mike" -> 35, "Tom" -> 40)

// 移除元素

ages -= "Mike"

// 更新不可变的map

val ages2 = ages + ("Mike" -> 36, "Tom" -> 40)

// 移除不可变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("leo" -> 30, "alice" -> 15, "jen" -> 25)

// LinkedHashMap可以记住插入entry的顺序

val ages = new scala.collection.mutable.LinkedHashMap[String, Int]

ages("leo") = 30

ages("alice") = 15

ages("jen") = 25

Map的元素类型—Tuple

// 简单Tuple

val t = ("leo", 30)

// 访问Tuple

t._1

// zip操作

val names = Array("leo", "jack", "mike")

val ages = Array(30, 24, 26)

val nameAges = names.zip(ages)

for ((name, age) <- nameAges) println(name + ": " + age)

定义一个简单的类

// 定义类,包含field以及方法

class HelloWorld {

  private var name = "leo"

  def sayHello() { print("Hello, " + name) } 

  def getName = name

}

// 创建类的对象,并调用其方法

val helloWorld = new HelloWorld

helloWorld.sayHello()

print(helloWorld.getName) // 也可以不加括号,如果定义方法时不带括号,则调用方法时也不能带括号

getter与setter

1、定义不带private的 var field,此时scala生成class时,会自动生成一个private[this]的成员字段(名称与field不同),并还生成一对getter和setter方法,分别叫做field和 field_=,并且getter和setter方法的访问修饰符与field定义相同

2、而如果使用private修饰field,则只生成的getter和setter,且访问修饰也是private的

3、如果定义val field,则只会生成getter方法

4、 如果不希望生成setter和getter方法,则将field声明为private[this]

class Student {

  var name = "leo"

}

// 调用getter和setter方法,分别叫做name和name_=

val leo = new Student

print(leo.name)

leo.name = "leo1" //实际上会调用 leo.name_=("leo1")方法

自定义getter与setter

// 如果只是希望拥有简单的getter和setter方法,那么就按照scala提供的语法规则,根据需求为field选择合适的修饰符就好:var、val、private、private[this]

// 但是如果希望能够自己对getter与setter进行控制,则可以自定义getter与setter方法

// 自定义setter方法的时候一定要注意scala的语法限制,签名、=、参数间不能有空格

class Student {

  private var myName = "leo" //默认会生成一对private getter(myName)、setter(myName _=)方法

  def name = "your name is " + myName //自定义myName 成员变量getter方法

  def name_=(newValue: String)  {//自定义myName 成员变量的setter方法

    print("you cannot edit your name!!!")

  }

}

val leo = new Student

print(leo.name)

leo.name = "leo1" //会去调用 name_+ 自定义setter 方法

仅暴露field的getter方法

// 如果你不希望field有setter方法,则可以定义为val,但是此时就再也不能更改field的值了

// 但是如果希望能够仅仅暴露出一个getter方法,并且还能通过某些方法更改field的值,那么需要综合使用private以及自定义getter方法。此时,由于field是private的,所以setter和getter都是private,对外界没有暴露;自己可以实现修改field值的方法;自己可以覆盖getter方法

class Student {

  private var myName = "leo"

  def updateName(newName: String) { //更改field的其他方法(命名约束的不满足setter方法)

    if(newName == "leo1") myName = newName

    else print("not accept this new name!!!")

  }

  def name = "your name is" + myName  //覆盖自动生成的私有getter方法

}

private[this]的使用

// 如果将field使用private来修饰,那么代表这个field是类私有的,在类的方法中,可以直接访问类的其他对象的private field

// 这种情况下,如果不希望field被其他对象访问到,那么可以使用private[this],意味着对象私有的field,只有本对象内可以访问到(子类对象中也是不可以访问的,因为私有的是不能被继承的)

class Student {

  private var myAge = 0 //试着修改成private[this]

  def age_=(newValue: Int) {

    if (newValue > 0) myAge = newValue

    else print("illegal age!")

  }

  def age = myAge

  def older(s: Student) = {

    myAge > s.myAge //修改成private[this]后,就会报错

  }

}

private[this]还可以用为修改方法

Java风格的getter和setter方法

// Scala的getter和setter方法的命名与java是不同的,是field和field_=的方式

// 如果要让scala自动生成java风格的getter和setter方法,只要给field添加@BeanProperty注解即可

// 此时会生成4个方法,name: String、name_=(newValue: String): Unit、getName(): String、setName(newValue: String): Unit

import scala.reflect.BeanProperty

class Student {

  @BeanProperty var name: String = _

}

class Student(@BeanProperty var name: String)

val s = new Student

s.setName("leo")

s.getName()

辅助constructor

// Scala中,可以给类定义多个辅助constructor,类似于java中的构造函数重载

// 辅助constructor之间可以互相调用,而且必须第一行调用主constructor或其他辅构造器

class Student {

  private var name = ""

  private var age = 0

  def this(name: String) {

    this()

    this.name = name

  }

  def this(name: String, age: Int) {

    this(name)

    this.age = age

  }

}

主constructor

// Scala中,主constructor是与类名放在一起的,有且只有一个,与java不同

// 而且类中,没有定义在任何方法中的代码(包括成员字段),都属于主constructor的代码,且执行的顺序与代码书写的顺序一致。这其实与Java是一样的,在Java中方法之外的代码(成员以及代码块)会在构造器调用之前最先执行,姑且将这些代码看作也是放到了一个主构造器中进行执行的,只不过这种主构造器不能带构造参数

//主构造器与类定义是在一起的,如果有参数,则在类名后面跟括号即可:

class Student(val name: String, val age: Int) {

  println("your name is " + name + ", your age is " + age)

}

当然没有参数的主构造器也可以带括号:

class Student() {}


// 主constructor中还可以通过使用默认参数,来给参数默认的值

class Student(val name: String = "leo", val age: Int = 30) {

  println("your name is " + name + ", your age is " + age)

}

// 如果主constrcutor传入的参数什么修饰都没有,比如name: String,那么如果类内部除主constrcutor方法外其它方法也使用到了,则会自动将该参数修饰为private[this] name以便其它方法使用:

class Student(name: String) {

         def f(){print(name)}

         def f(s:Student){print(s.name)}//编译出错

}

class Student(val name: String) {

         def f(){print(name)}

         def f(s:Student){print(s.name)}//编译正确,证明没有使用var或val修饰时,且在除主构造器中使用外,则使用private[this]来修饰

}

类中没有定义的在任何方法中的代码都属于主构造器,并且执行顺序与书写顺序一致:

内部类

// Scala中,同样可以在类中定义内部类;但是与java不同的是,每个外部类的对象的内部类,都属于不同的类

import scala.collection.mutable.ArrayBuffer

class Class {

  class Student(val name: String) {}

  val students = new ArrayBuffer[Student]

  def getStudent(name: String) =  {

    new Student(name)

  }

}

val c1 = new Class

val s1 = c1.getStudent("leo")

c1.students += s1

val c2 = new Class

val s2 = c2.getStudent("leo")

c1.students += s2   //异常

object

1、object,相当于class的单个实例(但与从class实例化出来的对象的内容决不一样),通常在里面放一些class层面上共享的内容,如Java中的静态field或者method即在定义在object中(注:Scala中没有Java的静态概念,所以延伸出了object这个东东)

2、你可以将object看作是一个类class,只是这个类在内存中只有一个单例,且定义的object名就是实例名,不需我们自己实例化,运行时JVM已帮我们new出来了

3、第一次调用object的方法时,就会执行object的constructor,也就是object内部不在method中的代码;但是object不能定义接受参数的constructor

4、注意,object的constructor只会在其第一次被调用时执行一次,以后再次调用就不会再次执行constructor了

5、object通常用于作为单例模式的实现,或者放class的静态成员,比如工具方法

object Person {

  private var eyeNum = 2

  println("this Person object!")

  def getEyeNum = eyeNum

}

伴生对象

// 如果有一个class,还有一个与class同名的object,那么就称这个object是class的伴生对象,class是object的伴生类

// 伴生类和伴生对象必须存放在一个.scala文件之中

// 伴生类和伴生对象,最大的特点就在于,互相可以访问private field

object Person {

  private val eyeNum = 2

  def getEyeNum = eyeNum

}

class Person(val name: String, val age: Int) {

def sayHello = println("Hi, " + name + ", I guess you are " + age + " years old!" + ", and usually you must have " + Person.eyeNum + " eyes.")

}

让object继承抽象类

// object的功能其实和class类似,除了不能定义接受参数的constructor之外

// object也可以继承抽象类,并覆盖抽象类中的方法

abstract class Hello(var message: String) {

  def sayHello(name: String): Unit

}

object HelloImpl extends Hello("hello") {

  override def sayHello(name: String) = {

    println(message + ", " + name)

  }

}

apply方法

// object中非常重要的一个特殊方法,就是apply方法

// 通常在伴生对象中实现apply方法,并在其中实现构造伴生类的对象的功能,一般用作工厂方法

// 而创建伴生类的对象时,通常不会使用new Class的方式,而是使用Class()的方式,隐式地调用伴生对象得apply方法,这样会让对象创建更加简洁

// 比如,Array类的伴生对象的apply方法就实现了接收可变数量的参数,并创建一个Array对象的功能

val a = Array(1, 2, 3, 4, 5)

// 比如,定义自己的伴生类和伴生对象

class Person(val name: String)

object Person {

  def apply(name: String) = new Person(name)

}


另外,如果直接在一个对象后面接小括号,则会去调用这个对象所对应类中相应的apply方法:

class Person {

  def apply(name: String) = println(name)

}

scala> val p = new Person

scala> p("Persion")

Persion

main方法

// 就如同java中,如果要运行一个程序,必须编写一个包含main方法类一样;在scala中,如果要运行一个应用程序,那么必须有一个main方法,作为入口

// scala中的main方法定义为def main(args: Array[String]),而且必须定义在object中

object HelloWorld {

  def main(args: Array[String]) {

    println("Hello World!!!")

  }

}

// 除了自己实现main方法之外,还可以继承App Trait,然后将需要在main方法中运行的代码,直接作为object的constructor代码;而且用args可以接受传入的参数

object HelloWorld extends App {

  if (args.length > 0) println("hello, " + args(0))

  else println("Hello World!!!")

}


// 如果要运行上述代码,需要将其放入.scala文件,然后先使用scalac编译,再用scala执行

scalac HelloWorld.scala

scala -Dscala.time HelloWorld

// App Trait的工作原理为:App Trait继承自DelayedInit Trait,scalac命令进行编译时,会把继承App Trait的object的constructor代码都放到DelayedInit Trait的delayedInit方法中,然后由App Trait的main方法去调用执行

用object来实现枚举功能

// Scala没有直接提供类似于Java中的Enum这样的枚举特性,如果要实现枚举,则需要用object继承Enumeration类,并且调用Value方法来初始化枚举值

object Season extends Enumeration {

  val SPRING, SUMMER, AUTUMN, WINTER = Value

}

// 还可以通过Value传入枚举值的id和name,通过id和toString可以获取; 还可以通过id和name来查找枚举值

object Season extends Enumeration {

  val SPRING = Value(0, "spring")

  val SUMMER = Value(1, "summer")

  val AUTUMN = Value(2, "autumn")

  val WINTER = Value(3, "winter")

}

Season(0)  // spring

Season.withName("spring") // spring,根据名称找

// 使用枚举object.values可以遍历枚举值

for (ele <- Season.values) println(ele)

extends

// Scala中,让子类继承父类,与Java一样,也是使用extends关键字

// 继承就代表,子类可以从父类继承父类的field和method;然后子类可以在自己内部放入父类所没有,子类特有的field和method;使用继承可以有效复用代码

// 子类可以覆盖父类的field和method;但要注意的是final类是不能被继承的,而且final类型的field和method是无法被覆盖的

class Person {

  private var name = "leo"

  def getName = name

}

class Student extends Person {

  private var score = "A"

  def getScore = score

}

override和super

// Scala中,如果子类要覆盖一个父类中的非抽象方法,则必须使用override关键字;如果是抽象的方法,则可以省略

// override关键字可以帮助我们尽早地发现代码里的错误,比如:override修饰的父类方法的方法名我们拼写错了;比如要覆盖的父类方法的参数我们写错了;等等

// 此外,在子类覆盖父类方法之后,如果我们在子类中就是要调用父类的被覆盖的方法呢?那就可以使用super关键字,显式地指定要调用父类的方法

class Person {

  private var name = "leo"

  def getName = name

}

class Student extends Person {

  private var score = "A"

  def getScore = score

override def getName = "Hi, I'm " + super.getName

}

重写时需要override关键字,如果是实现则可以省略override关键字

override field

子类可以覆盖父类的同名的非private成员

// Scala中,子类可以覆盖父类的val field,而且子类的val field还可以覆盖父类的val field的getter方法;只要在子类中使用override关键字即可

class Person {

  /*private*/ val name: String = "Person"

}

class Student extends Person {

override val name: String = "leo" // 重写一定要带上override关键字

}


只有val变量才能被重写,var变量是不能被重写的:

class A{

         var f = "a"

}

class B extends A{

         override var f = "b" // error: overriding variable f in class A of type String;

                                             //variable f cannot override a mutable variable

}

下面也是不行的:

class A{

         var f:String = "a"

}

class B extends A{

         override def f:String = "b"

         override def f_=(x:String) = println(x) // error: overriding variable f in class A of type String;

                                                                            //method f_= cannot override a mutable variable

}


var变量只能被实现,如果将上面换成抽象的var字段,则是可以的:

abstract class A{

         var f:String

}

class B extends A{

         /* override */ def f:String = "b" //由于是实现,所以可以省略override

         override def f_=(x:String) = println(x) //也可以不省略override

}

或者:

abstract class A{

         var f:String

}

class B extends A{

         var f:String = ""

}


val变量只能被val实现,不能被def实现:

abstract class A{

         val f:String

}

class B extends A{

         def f:String = ""  // error: overriding value f in class A of type String;

                                             //method f needs to be a stable, immutable value

}

但可以这样:

abstract class A{

         val f:String

}

class B extends A{

         val f:String = ""

}

val、var override/实现 def

abstract class Person {

    def id: Int  

}

class Student extends Person{

    override var id = 9527  //Error: method id_= overrides nothing

}

在scala中定义了一个var变量,会自动生成getter和setter方法。由于父类中只定义了一个方法def id: Int,而子类中var变量会自动生成getter(id)与setter方法(id_),但是父类并没有这个setter方法,所以是无法重写的。如下修改即可:

abstract class Person {

    def id: Int 

    def id_=(value: Int) //父类必须有set方法

}

class Student extends Person{

    override var id = 9527 //为var变量自动生成get和set方法

}

或者子类定义成val变量:

abstract class Person {

    def id: Int 

}

class Student extends Person{

    override val id = 9527

}


上面是val或var来实现def,下面是val或var来重写def:

class Person {

    def id: Int = 1

}

class Student extends Person{

    override val id = 9527

}

class Person {

    def id: Int = 1

    def id_=(value: Int) =println(value)

}

class Student extends Person{

    override var id = 9527

}


但是不能使用def重写val或var:

class Person {

  val sex: String

}

class Student extends Person {

  override def sex:String = "" //error: overriding value sex in class Person of type String;

                                     //method sex needs to be a stable, immutable value

}


class Person {

  var sex: String = "X"

}

class Student extends Person {

  override def sex:String = ""

  override def sex_=(x:String) = println(x)

}

也不能使用def实现val:

abstract class Person {

  val sex: String

}

class Student extends Person {

  def sex:String = "" //error: overriding value sex in class Person of type String;

                              //method sex needs to be a stable, immutable value

}

但可以使用def实现var:

abstract class Person {

  var sex: String

}

class Student extends Person {

  def sex:String = ""

  def sex_=(x:String) = println(x)

}


成员变量与方法之间重写与实现结论:可以使用val或var来重写或实现def,也可以使用def实现var;但不能使用def重写val或var,也不能使用def实现val

isInstanceOf和asInstanceOf

// 如果我们创建了子类的对象,但是又将其赋予了父类类型的变量。则在后续的程序中,我们又需要将父类类型的变量转换为子类类型的变量,应该如何做?

// 首先,需要使用isInstanceOf判断对象是否是指定类的对象,如果是的话,则可以使用asInstanceOf将对象转换为指定类型

// 注意,如果对象是null,则isInstanceOf一定返回false,asInstanceOf一定返回null

// 注意,如果没有用isInstanceOf先判断对象是否为指定类的实例,就直接用asInstanceOf转换,则可能会抛出异常

class Person

class Student extends Person

val p: Person =  new Student

var s: Student = null

if (p.isInstanceOf[Student]) s = p.asInstanceOf[Student]


scala> p.isInstanceOf[Student]

res7: Boolean = true

scala> p.isInstanceOf[Person]

res8: Boolean = true

getClass和classOf

// isInstanceOf只能判断出对象是否是给定类或其子类的实例对象,而不能精确判断出对象就是给定类的实例对象

// 如果要求精确地判断对象就是指定类的对象,那么就只能使用getClass和classOf了

// 对象.getClass可以精确获取对象所属的类class,classOf[类]可以精确获取类,然后使用==操作符即可判断

class Person

class Student extends Person

val p: Person = new Student

p.isInstanceOf[Person]

p.getClass == classOf[Person]

p.getClass == classOf[Student]

使用模式匹配进行类型判断

// 但是在实际开发中,比如spark的源码中,大量的地方都是使用了模式匹配的方式来进行类型的判断,这种方式更加地简洁明了,而且代码得可维护性和可扩展性也非常的高

// 使用模式匹配,功能性上来说,与isInstanceOf一样,也是判断主要是该类以及该类的子类的对象即可,也不是精准判断的

class Person

class Student extends Person

val p: Person = new Student

p match {

  case per: Person => println("it's Person's object")

  case _  => println("unknown type")

}

protected

// 跟java一样,scala中同样可以使用protected关键字来修饰field和method,这样子类就可以继承这些成员或方法

// 还可以使用protected[this],则只能在当前子类对象中访问父类的使用protected[this]修饰的field和method,无法通过其他子类对象访问父类中的这些字段与方法

class Person {

  protected var name: String = "leo"

  protected[this] var hobby: String = "game"

}

class Student extends Person {

  def sayHello = println("Hello, " + name)

  def makeFriends(s: Student) {

println("my hobby is " + hobby + ", your hobby is " + s.hobby) //此处编译出错

  }

}

protected[this]修饰的字段只能在本对象或其子对象中使用,不能在其他对象中使用:

class Person {

  protected var name: String = "leo"

  protected[this] var hobby: String = "game"

  def makeFriends(s: Person ){

println("my hobby is " + hobby + ", your hobby is " + s.hobby) //此处编译还是出错

  }

}

与private[this]一样,protected[this]也可以修饰方法

调用父类的constructor

// Scala中,每个类可以有一个主constructor和任意多个辅助constructor,而每个辅助constructor的第一行都必须是调用其他辅助constructor或者是主constructor;因此子类的辅助constructor是一定不可能直接调用父类的constructor的

// 只能在子类的主constructor中调用父类的constructor,以下这种语法,就是通过子类的主构造函数来调用父类的构造函数(即在extends后面指定需要调用父类哪个构造器)

// 注意!如果是父类中接收的参数,比如name和age,子类中接收时,就不要用任何val或var来修饰了(或者带上修饰了,但将参数名命成不一样也可),否则会认为是子类要覆盖父类的field

class Person(val name: String, val age: Int)

class Student(name: String, age: Int, var score: Double) extends Person(name, age) /*调用父类的辅助构造器*/{

  def this(name: String) {

    this(name, 0, 0) //调用主构造器

  }

  def this(age: Int) {

    this("leo", age, 0) //调用主构造器

  }

}

调用父类的主构造器:

class Person(val name: String, val age: Int){

         def this(){

                   this("11",11)

         }

}

class Student(name: String, age: Int, var score: Double) extends Person/*或 Person()*/ {

  def this(name: String) {

    this(name, 0, 0) //调用主构造器

  }

  def this(age: Int) {

    this("leo", age, 0) //调用主构造器

  }

}

匿名内部类

// 在Scala中,匿名子类是非常常见,而且非常强大的。Spark的源码中也大量使用了这种匿名子类。

// 匿名子类,也就是说,可以定义一个类的没有名称的子类,并直接创建其对象,然后将对象的引用赋予一个变量。之后甚至可以将该匿名子类的对象传递给其他函数使用。

class Person(protected val name: String) {

  def sayHello = "Hello, I'm " + name

}

val p = new Person("leo") {

  override def sayHello = "Hi, I'm " + name

}

def greeting(p: Person { def sayHello: String }) {

  println(p.sayHello)

}

抽象类

// 如果在父类中,有某些方法无法立即实现,而需要依赖不同的子来来覆盖,重写实现自己不同的方法实现。此时可以将父类中的这些方法不给出具体的实现,只有方法签名,这种方法就是抽象方法。

// 而一个类中如果有一个抽象方法,那么类就必须用abstract来声明为抽象类,此时抽象类是不可以实例化的

// 在子类中覆盖抽象类的抽象方法时,不需要使用override关键字(也可带上),但如果是重写父类具体方法或成员,则不能省略override

abstract只能修饰类,不能修饰成员与方法,哪怕成员(没有初始化)与方法(没有方法体)是抽象的


abstract class Person(val name: String) {

  def sayHello: Unit

}

class Student(name: String) extends Person(name) {

  def sayHello: Unit = println("Hello, " + name)

}

抽象field

// 如果在父类中,定义了field,但是没有给出初始值,则此field为抽象field

// 抽象field意味着,scala会根据自己的规则,为var或val类型的field生成对应的getter和setter方法,但是父类中是没有该field的

// 子类必须覆盖field,以定义自己的具体field,并且覆盖抽象field,不需要使用override关键字

abstract class Person {

  val name: String

}

class Student extends Person {

  val name: String = "leo"

}



没有初始化的成员所在的类要是抽象类:

abstract class A{

         var a:String

}


/*class B extends A*/编译时报错:需要重写父类的抽象成员

class B extends A{

         /*override*/ var a:String = "a" //也可以省略override

}

除了通过上面直接覆盖父类的抽象成员外,还可以简接通过实现抽象成员所对应的getter与setter方法即可:

class B extends A{

         /*override*/ def a = "a" //由于是实现,所以可以省略override

         override def a_=(x:String){println(a)}

}

上面是通过实现父类抽象成员所对应的getter与setter方法来重写抽象成员,所以可以看出:没有被初始化的成员所对应的getter与setter方法实质上就是抽象的,所以类要定义是abstract,成员字段本身没有什么抽象不抽象的概念

将trait作为接口使用

// Scala中的Triat是一种特殊的概念

// 首先我们可以将Trait作为接口来使用,此时的Triat就与Java中的接口非常类似

// 在triat中可以定义抽象方法,就与抽象类中的抽象方法一样,只要不给出方法的具体实现即可

// 类可以使用extends关键字继承trait,注意,这里不是implement,而是extends,在scala中没有implement的概念,无论继承类还是trait,统一都是extends

// 类继承trait后,必须实现其中的抽象方法(如果是trait继承trait则不需要,这好比Java中的接口继承接口一样),实现时不需要使用override关键字

// scala不支持对类进行多继承,但是支持多重继承trait,使用with关键字即可

trait HelloTrait {

  def sayHello(name: String)

}

trait MakeFriendsTrait {

  def makeFriends(p: Person)

}

class Person(val name: String) extends HelloTrait with MakeFriendsTrait with Cloneable {

  def sayHello(name: String) = println("Hello, " + name)

  def makeFriends(p: Person) = {sayHello(name);println("Hello, my name is " + name + ", your name is " + p.name)}

}

val p1 = new Person("leo")

val p2 = new Person("lily")

p1.makeFriends(p2)

在Trait中定义具体方法

// Scala中的Triat可以不是只定义抽象方法,还可以定义具体方法,此时trait更像是包含了通用工具方法的东西

// 

有一个专有的名词来形容这种情况,就是说trait的功能混入了类

// 举例来说,trait中可以包含一些很多类都通用的功能方法,比如打印日志等等,spark中就使用了trait来定义了通用的日志打印方法

trait Logger {

  def log(message: String) = println(message)

}

class Person(val name: String) extends Logger {

  def makeFriends(p: Person) {

    println("Hi, I'm " + name + ", I'm glad to make friends with you, " + p.name)

    log("makeFriends methdo is invoked with parameter Person[name=" + p.name + "]")

  }

}

val p1 = new Person("leo")

val p2 = new Person("lily")

p1.makeFriends(p2)

在Trait中定义具体字段

// Scala中的Triat可以定义具体field,此时继承trait的类就自动获得了trait中定义的field

trait Person {

  val eyeNum: Int = 2

}

class Student(val name: String) extends Person {

  def sayHello = println("Hi, I'm " + name + ", I have " + eyeNum + " eyes.")

}

val s = new Student("leo")

s.sayHello

在Trait中定义抽象字段

// Scala中的Triat可以定义抽象field,而trait中的具体方法则可以基于抽象field来编写

// 但是继承trait的类,则必须覆盖抽象field,提供具体的值

trait SayHello {

  val msg: String //抽象字段

def sayHello(name: String) = println(msg + ", " + name) // 具体方法调用抽象字段(实质上是调用val抽象字段所对应的getter抽象方法),相当于Java中的模式方法,另参看这里

}

class Person(val name: String) extends SayHello {

  val msg: String = "hello"

  def makeFriends(p: Person) {

    sayHello(p.name)

    println("I'm " + name + ", I want to make friends with you!")

  }

}

val p1 = new Person("leo")

val p2 = new Person("lily")

p1.makeFriends(p2)

为实例混入trait

// 有时我们可以在创建类的对象时,指定该对象混入某个trait,这样,就只有这个对象混入该trait的方法,而类的其他对象则没有

trait Logged {

  def log(msg: String) {}

}

trait MyLogger extends Logged {

  override def log(msg: String) { println("log: " + msg) }

class Person(val name: String) extends Logged {

    def sayHello { println("Hi, I'm " + name); log("sayHello is invoked!") }

}

val p1 = new Person("leo")

p1.sayHello  // Hi, I'm leo

val p2 = new Person("jack") with MyLogger //实例化时混入

p2.sayHello  // Hi, I'm jack

//log: sayHello is invoked!

trait调用链

// Scala中支持让类继承多个trait后,依次调用多个trait中的同一个方法,只要让多个trait的同一个方法中,在方法最后都执行“super.方法”来调用父类方法即可

// 类中调用多个trait中都有的这个方法时,首先会从最右边的trait的方法开始执行,然后依次往左执行,形成一个调用链条

// 这种特性非常强大,其实就相当于设计模式中的责任链模式的一种具体实现

trait Handler {

  def handle(data: String) {}

}

trait DataValidHandler extends Handler {

  override def handle(data: String) {

    println("check data: " + data)

 super.handle(data)

  }

}

trait SignatureValidHandler extends Handler {

  override def handle(data: String) {

    println("check signature: " + data)

 super.handle(data)

  }

}

class Person(val name: String) extends SignatureValidHandler with DataValidHandler {

  def sayHello = { println("Hello, " + name); handle(name) }

}

val p = new Person("leo")

p.sayHello


Hello, leo

check data: leo

check signature: leo

在trait中覆盖抽象方法

// 在trait中,是可以覆盖父trait的抽象方法的

// 但是覆盖时,如果使用了“super.方法”形式调用了父类抽象方法,则无法通过编译。因为super.方法就会去掉用父trait的抽象方法,此时子trait的该方法还是会被认为是抽象的,所以在override的同时还需要加上abstract

// 此时如果要通过编译,就得给子trait的方法加上abstract override修饰

trait Logger {

  def log(msg: String)

}

trait MyLogger extends Logger {

 abstract override def log(msg: String) { println("MyLogger.log()");super.log(msg) }

}

class BasicLog extends Logger{

def log(msg: String) { println("BasicLog.log()"); println(msg) }

}

class Person(val name: String) extends BasicLog with MyLogger {

  def makeFriends(p: Person) {

    println("Hi, I'm " + name + ", I'm glad to make friends with you, " + p.name)

    log("makeFriends methdo is invoked with parameter Person[name=" + p.name + "]")

  }

}

val p1 = new Person("leo")

val p2 = new Person("lily")

p1.makeFriends(p2)


Hi, I'm leo, I'm glad to make friends with you, lily

MyLogger.log()

BasicLog.log()

makeFriends methdo is invoked with parameter Person[name=lily]

混合使用trait的具体方法和抽象方法

// 在trait中,可以混合使用具体方法和抽象方法

// 可以让具体方法依赖于抽象方法,而抽象方法则放到继承trait的类中去实现

// 这种trait其实就是设计模式中的模板设计模式的体现

trait Valid { 

  def getName: String  //抽象方法

  def valid: Boolean = { //具体方法中调用抽象方法,相当于Java中的模板方法

    getName == "leo"   

  }

}

class Person(val name: String) extends Valid {

  println(valid)

  def getName = name

}

val p = new Person("leo") //true

trait的构造机制

// 在Scala中,trait也是有构造代码的,也就是trait中的,不包含在任何方法中的代码

// 而继承了trait的类的构造机制如下:1、父类的构造函数执行;2、trait的构造代码执行,多个trait从左到右依次执行;3、构造trait时会先构造父trait,如果多个trait继承同一个父trait,则父trait只会构造一次;4、所有trait构造完毕之后,子类的构造函数执行

class Person { 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!") }

class Student extends Person with MyLogger with TimeLogger {

  println("Student's constructor!")

}

val s = new Student

trait field的初始化

// 在Scala中,trait的构造函数是不能接参数的(包括主构造器与辅助构造器),即trait不能定义辅助构造器,这是trait与class的唯一区别,但是如果需求就是要trait能够对field进行初始化,该怎么办呢?只能使用Scala中非常特殊的一种高级特性——提前定义

trait SayHello {

  val msg: String

  println("1、SayHello")

println(msg.toString) // 抛NullPointerException异常。由于在调用msg成员字段时,发现在msg是被重新实现(或重写,这里为实现),则会去调用子类中的实现的msg成员,但由于此时子类构造器还未执行,所以子类msg还没来得及初始化,所以返回null,最终导致空指针异常

}

class Person extends SayHello{

 println("2、Person")

 val msg:String = "init"

}

new Person // 抛NullPointerException异常,原因父trait构造代码会先于子类构造器执行,在执行msg.toString时子类中的msg还没有来得及初始化。但如果将上面的val都修改为def,则可以正常运行。因为初始化父类时,由于子类实现(或重写,这里为实现)了msg方法,所以msg.toString会去调用子类实现的msg方法而返回"init",即使此时子类还没有被初始化:

trait SayHello {

  def msg: String

  println("1、SayHello")

  println(msg.toString)

}

class Person extends SayHello{

 println("2、Person")

 def msg:String = "init"

}

new Person


即使父类提供了初始化,但还是抛NullPointerException,原因是子类重写了父类该字段msg,在执行父类构造器中的msg.toString时,msg使用的是子类中被重写过的,但此时子类构造器还未被执行,所以子类的msg此时还为null:

trait SayHello {

  val msg: String = "000"

  println("1、SayHello")

  println(msg.toString) // NullPointerException

}

class Person extends SayHello{

 println("2、Person")

 override val msg:String = "init"

}

new Person

而下面的则不会抛异常了,原因是子类没有重写msg字段,所以父类构造器在执行时,msg使用的还是父中的msg,且已经被初始化过了:

trait SayHello {

  val msg: String = "000"

  println("1、SayHello")

println(msg.toString) // 不会抛异常,注意:此名要放在上面msg初始化语句的后面,否则还是会抛空指针

}

class Person extends SayHello{

 println("2、Person")

}

new Person


下面根据前面的知识(字段与方法相互实现与重写),结合上面的经验,分析分析一下下面的情况:

以下也可以,原因也是通过方法的多态来初始化:

trait SayHello {

  var msg: String

  println("1、SayHello")

  println(msg.toString)//会去调用子类实现方法msg,顺利执行

}

class Person extends SayHello{

 println("2、Person")

 def msg:String = {println("person.msg");"init" }

 def msg_=(x:String) = println(x)

}

new Person


trait SayHello {

 var msg: String

  println("1、SayHello")

println(msg.toString) // 抛 NullPointerException,原因父类中的msg被子类实现过,但父类调用时,子类还未初始msg字段

}

class Person extends SayHello{

 println("2、Person")

var msg:String = "init"

}

new Person

上面除了通过调用子类实现(或重写)方法解决问题外,下面还可以通过提前定义方式来初始化:

trait SayHello {

  val msg: String

  println("3、SayHello")

  println(msg.toString)

}

class Person{println("2、Person")}

val p = new {

  val msg: String = {println("1、init");"init"}  // 实例化时提前初始化

} with Person with SayHello

1、init -> 2、Person -> 3、SayHello

注意上面new … with与class …extends…with的区别,new…with是动态混入,执行构造器是从new后面的类(或块,这里为块)开始从左到右依次执行;而class…extends…with则是静态混入,在定义class时就已确定,其构造器是从extends后面的类开始从左往右依次执行,执行完后最后执行class 后面指定的类的构造器。如下面的new … with形式构造顺序:

trait A{

 println("a")

}

class B extends A{

 println("b")

}

trait C extends A{

 println("c")

}

new B with C // a -> b -> c


class…extends…with构造顺序:

trait A{

 println("a")

}

trait B{

 println("b")

}

class C extends A with B{

 println("c")

}

new C // a -> b -> c


下面是另一种初始化方式(class …extends…with静态定义方式),此种方式比上面初始化方式好理解一点:

trait SayHello {

  val msg: String

  println("2、SayHello")

  println(msg.toString)

}

class Person extends {

  val msg: String = {println("1、init");"init"} // 类定义时提前初始化

} with SayHello {

  println("3、Person")

}

new Person


// 另外一种方式就是使用lazy value

trait SayHello {

lazy val msg: String = {println("SayHello");null} // 此句不会执行

println(msg.toString) // 此句会调用子类重写过的msg成员,由于子类msg定义成了lazy,而lazy变量有个特性就是在使用时会执行右边表达式,所以在这里调用msg.toString方法时,就会触发懒加载右边的计算表达式,所以lazy字段不是由类来初始化的,而是由调用时机来决定,所以子类中的lazy msg会先于子类其他成员被初始化

  println("2")

}

class Person extends SayHello {

  println("3")

  val m: String = {println("4");"m"}

  override lazy val msg: String = {println("1");"init"}

}

new Person


1

init

2

3

4

trait继承class

// 在Scala中,trait也可以继承自class,此时这个class就会成为所有继承该trait的类的父类

class MyUtil {

  def printMessage(msg: String) = println(msg)

}

trait Logger extends MyUtil {

  def log(msg: String) = printMessage("log: " + msg)

}

class Person(val name: String) extends Logger {

  def sayHello {

    log("Hi, I'm " + name)

    printMessage("Hi, I'm " + name)

  }

}

new Person("leo").sayHello


log: Hi, I'm leo

Hi, I'm leo

将函数赋值给变量

// Scala中的函数是一等公民,可以独立定义,独立存在,而且可以直接将函数作为值赋值给变量

// Scala的语法规定,将函数赋值给变量时,必须在函数后面加上空格和下划线

def sayHello(name: String) { println("Hello, " + name) }

val sayHelloFunc = sayHello _

sayHelloFunc("leo")

匿名函数

// Scala中,函数也可以不需要命名,此时函数被称为匿名函数。

// 可以直接定义函数之后,将函数赋值给某个变量;也可以将直接定义的匿名函数传入其他函数之中

// Scala定义匿名函数的语法规则就是,(参数名: 参数类型) => 函数体

// 这种匿名函数的语法必须深刻理解和掌握,在spark的中有大量这样的语法,如果没有掌握,是看不懂spark源码的

val sayHelloFunc = (name: String) => println("Hello, " + name)

sayHelloFunc("leo")


变量带返回类型:

val sayHelloFunc:String=>Unit = (name: String) => println("Hello, " + name)

高阶函数

// Scala中,由于函数是一等公民,因此可以直接将某个函数传入其他函数,作为参数。这个功能是极其强大的,也是Java这种面向对象的编程语言所不具备的。

// 接收其他函数作为参数的函数,也被称作高阶函数(higher-order function)

val sayHelloFunc = (name: String) => println("Hello, " + name)

def greeting(func: (String) => Unit, name: String) { func(name) }

greeting(sayHelloFunc, "leo")


Array(1, 2, 3, 4, 5).map((num: Int) => num * num)


// 高阶函数的另外一个功能是将函数作为返回值,即返回值就是一个函数,如下面根据不同的msg生成不同的函数

def getGreetingFunc(msg: String) = (name: String) => println(msg + ", " + name)

var greetingFunc = getGreetingFunc("hello")

greetingFunc("leo")

greetingFunc = getGreetingFunc("hi")

greetingFunc("leo")

高阶函数的类型推断

// 高阶函数可以自动推断出参数类型,而不需要写明类型;而且对于只有一个参数的函数,还可以省去其小括号;

def greeting(func: (String) => Unit, name: String) { func(name) }

greeting((name: String) => println("Hello, " + name), "leo")

greeting((name) => println("Hello, " + name), "leo")

greeting(name => println("Hello, " + name), "leo")


// 只要某个参数只在函数体里出现一次,则可以使用下划线 _ 来替换这个参数

def triple(func: (Int) => Int) = { func(3) }

triple(3 * _)


// 诸如3 * _的这种语法,必须掌握!!spark源码中大量使用了这种语法!


有多少个下划线,则就表示有多少个不同的参数。多个占位符时,第一个下划线表示第一个参数,第二个下划线表示第二个参数,以此类推;所以同一参数多处出现时是无法使用这种占位符来表示的。


使用占位符时,有时无法推导出类型,如:

scala> val f = _ + _

此时需明确写出类型:

scala> val f = (_: Int) + (_: Int)

f: (Int, Int) => Int =

Scala的常用高阶函数

// map: 对传入的每个元素都进行映射,返回一个处理后的元素

Array(1, 2, 3, 4, 5).map(2 * _)


// foreach: 对传入的每个元素都进行处理,但是没有返回值

(1 to 9).map("*" * _).foreach(println _)


// filter: 对传入的每个元素都进行条件判断,如果对元素返回true,则保留该元素,否则过滤掉该元素

(1 to 20).filter(_ % 2 == 0)


// reduceLeft: 从左侧元素开始,进行reduce操作,即先对元素1和元素2进行处理,然后将结果与元素3处理,再将结果与元素4处理,依次类推,即为reduce;reduce操作必须掌握!spark编程的重点!!!

// 下面这个操作就相当于1 * 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9

(1 to 9).reduceLeft( _ * _)


// sortWith: 对元素进行两两相比,进行排序

Array(3, 2, 5, 4, 10, 1).sortWith(_ < _)

闭包

// 闭包最简洁的解释:函数在变量不处于其有效作用域时,还能够对变量进行访问,即为闭包

def getGreetingFunc(msg: String) = (name: String) => println(msg + ", " + name)

val greetingFuncHello = getGreetingFunc("hello")

val greetingFuncHi = getGreetingFunc("hi")

// 两次调用getGreetingFunc函数,传入不同的msg,并创建不同的函数返回

// 然而,msg只是一个局部变量,却在getGreetingFunc执行完之后,还可以继续存在创建的函数之中;greetingFuncHello("leo"),调用时,值为"hello"的msg被保留在了函数体内部,可以反复的使用

// 这种变量超出了其作用域,还可以使用的情况,即为闭包

// Scala通过为每个函数创建对象来实现闭包,实际上对于getGreetingFunc函数创建的函数,msg是作为函数对象的变量存在的,因此每个函数才可以拥有不同的msg

SAM转换

如果Scala调用Java的某个方法传入的是一个SAM,则可以通过Scala提供的很方便的一种转换,将函数对象会传给Java方法


// 在Java中,由于不支持直接将函数传入一个方法作为参数,通常来说,唯一的办法就是定义一个实现了某个接口的类的实例对象,该对象只有一个方法,犹如这样接口只有单个的抽象方法,就叫single abstract method,简称为SAM


// 由于Scala是可以调用Java的代码的,因此当我们调用Java的某个方法时,可能就不得不创建SAM传递给方法,非常麻烦;但是Scala又是支持直接传递函数的。此时就可以使用Scala提供的,在调用Java方法时,使用Scala提供的SAM转换功能,即将SAM转换为Scala函数


// 要使用SAM转换,需要使用Scala提供的特性,隐式转换

import javax.swing._

import java.awt.event._

val button = new JButton("Click")

button.addActionListener(new ActionListener {// ActionListener接口只有一个抽象方法,这样的接口叫SAM

  override def actionPerformed(event: ActionEvent) {

    println("Click Me!!!")

  }

})


implicit def getActionListener(actionProcessFunc: (ActionEvent) => Unit) = new ActionListener {

  override def actionPerformed(event: ActionEvent) {

    actionProcessFunc(event)

  }

}

button.addActionListener((event: ActionEvent) => println("Click Me!!!"))

Currying函数

// Curring函数,指的是,将原来接收两个参数的一个函数,转换为两个函数,第一个函数接收原先的第一个参数,然后返回接收原先第二个参数的第二个函数。

// 在函数调用的过程中,就变为了两个函数连续调用的形式

// 在Spark的源码中,也有体现,所以对()()这种形式的Curring函数,必须掌握!

def sum(a: Int, b: Int) = a + b

sum(1, 1)


def sum2(a: Int) = (b: Int) => a + b

sum2(1)(1)


def sum3(a: Int)(b: Int) = a + b

sum2(1)(1)

return到外层函数

// Scala中,不需要使用return来返回函数的值,函数最后一行语句的值,就是函数的返回值。在Scala中,return用于在匿名函数中返回值给包含匿名函数的带名函数(即外层函数),并作为带名函数的返回值。

// 使用return的匿名函数,是必须给出返回类型的,否则无法通过编译

def greeting(name: String) = {

def sayHello(name: String):String = {

    return "Hello, " + name

  }

  sayHello(name)

}

greeting("leo")

Scala的集合体系结构

// Scala中的集合体系主要包括:Iterable、Seq、Set、Map。其中Iterable是所有集合trait的根trait。这个结构与Java的集合体系非常相似(最上层为public interface Collection extends Iterable)。

// Scala中的集合是分成可变和不可变两类集合的,其中可变集合就是说,集合的元素可以动态修改,而不可变集合的元素在初始化之后,就无法修改了。分别对应scala.collection.mutable和scala.collection.immutable两个包。

// Seq下包含了Range、ArrayBuffer、List等子trait。其中Range就代表了一个序列,通常可以使用“1 to 10”这种语法来产生一个Range。 ArrayBuffer就类似于Java中的ArrayList。

List

// List代表一个不可变的列表

// List的创建,val list = List(1, 2, 3, 4)

// List有head和tail,head代表List的第一个元素,tail代表第一个元素之后的所有元素,list.head,list.tail

// List有特殊的::操作符,可以用于将head和tail合并成一个List,0 :: list

// ::这种操作符要清楚,在spark源码中都是有体现的,一定要能够看懂!

// 如果一个List只有一个元素,那么它的head就是这个元素,它的tail是Nil

// 案例:用递归函数来给List中每个元素都加上指定前缀,并打印加上前缀的元素

def decorator(l: List[Int], prefix: String) {

  if (l != Nil) {

    println(prefix + l.head)

    decorator(l.tail, prefix)

  }

}

LinkedList

// LinkedList代表一个可变的列表,使用elem可以引用其头部,使用next可以引用其尾部

// val l = scala.collection.mutable.LinkedList(1, 2, 3, 4, 5); l.elem; l.next

// 案例:使用while循环将LinkedList中的每个元素都乘以2

val list = scala.collection.mutable.LinkedList(1, 2, 3, 4, 5)

var currentList = list

while (currentList != Nil) {

  currentList.elem = currentList.elem * 2

  currentList = currentList.next

}

// 案例:使用while循环将LinkedList中,从第一个元素开始,每隔一个元素,乘以2

val list = scala.collection.mutable.LinkedList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

var currentList = list

var first = true

while (currentList != Nil && currentList.next != Nil) {

  if (first) { currentList.elem = currentList.elem * 2; first = false }

  currentList  = currentList.next.next

  if (currentList != Nil) currentList.elem = currentList.elem * 2

}

Set

// Set代表一个没有重复元素的集合,Set为trait,分为可变与不可变两种trait

// 将重复元素加入Set是没有用的,比如val s = Set(1, 2, 3); s + 1; s + 4

// 而且Set是不保证插入顺序的,也就是说,Set中的元素是乱序的,val s = new scala.collection.mutable.HashSet[Int](); s += 1; s += 2; s += 5

// LinkedHashSet会用一个链表维护插入顺序,val s = new scala.collection.mutable.LinkedHashSet[Int](); i += 1; s += 2; s += 5

// SrotedSet会自动根据key来进行排序,val s = scala.collection.mutable.SortedSet("orange", "apple", "banana")

集合的函数式编程

// 集合的函数式编程非常非常非常之重要!!!

// 必须完全掌握和理解Scala的高阶函数是什么意思,Scala的集合类的map、flatMap、reduce、reduceLeft、foreach等这些函数,就是高阶函数,因为可以接收其他函数作为参数

// 高阶函数的使用,也是Scala与Java最大的一点不同!!!因为Java里面是没有函数式编程的,也肯定没有高阶函数,也肯定无法直接将函数传入一个方法,或者让一个方法返回一个函数

// 对Scala高阶函数的理解、掌握和使用,可以大大增强你的技术,而且也是Scala最有诱惑力、最有优势的一个功能!!!

// 此外,在Spark源码中,有大量的函数式编程,以及基于集合的高阶函数的使用!!!所以必须掌握,才能看懂spark源码


// map案例实战:为List中每个元素都添加一个前缀

List("Leo", "Jen", "Peter", "Jack").map("name is " + _)


// faltMap案例实战:将List中的多行句子拆分成单词

List("Hello World", "You Me").flatMap(_.split(" "))


// foreach案例实战:打印List中的每个单词

List("I", "have", "a", "beautiful", "house").foreach(println(_))


// zip案例实战:对学生姓名和学生成绩进行关联

List("Leo", "Jen", "Peter", "Jack").zip(List(100, 90, 75, 83))

函数式编程综合案例:统计多个文本内的单词总数

// 使用scala的io包将文本文件内的数据读取出来

val lines01 = scala.io.Source.fromFile("C://Users//Administrator//Desktop//test01.txt").mkString

val lines02 = scala.io.Source.fromFile("C://Users//Administrator//Desktop//test02.txt").mkString


// 使用List的伴生对象,将多个文件内的内容创建为一个List

val lines = List(lines01, lines02)


// 下面这一行才是我们的案例的核心和重点,因为有多个高阶函数的链式调用,以及大量下划线的使用,如果没有透彻掌握之前的课讲解的Scala函数式编程,那么下面这一行代码,完全可能会看不懂!!!

// 但是下面这行代码其实就是Scala编程的精髓所在,就是函数式编程,也是Scala相较于Java等编程语言最大的功能优势所在

// 而且,spark的源码中大量使用了这种复杂的链式调用的函数式编程

// 而且,spark本身提供的开发人员使用的编程api的风格,完全沿用了Scala的函数式编程,比如Spark自身的api中就提供了map、flatMap、reduce、foreach,以及更高级的reduceByKey、groupByKey等高阶函数

// 如果要使用Scala进行spark工程的开发,那么就必须掌握这种复杂的高阶函数的链式调用!!!


lines.flatMap(_.split(" ")).map((_, 1)).map(_._2).reduceLeft(_ + _)

模式匹配

// Scala是没有Java中的switch case语法的,相对应的,Scala提供了更加强大的match case语法,即模式匹配,类替代switch case,match case也被称为模式匹配


// Scala的match case与Java的switch case最大的不同点在于,Java的switch case仅能匹配变量的值,比1、2、3等;而Scala的match case可以匹配各种情况,比如变量的类型、集合的元素、有值或无值


// match case的语法如下:变量 match { case 值 => 代码 }。如果值为下划线,则代表了不满足以上所有情况下的默认情况如何处理。此外,match case中,只要一个case分支满足并处理了,就不会继续判断下一个case分支了。(与Java不同,java的switch case需要用break阻止)

// match case语法最基本的应用,就是对变量的值进行模式匹配

// 案例:成绩评价

def judgeGrade(grade: String) {

  grade match {

    case "A" => println("Excellent")

    case "B" => println("Good")

    case "C" => println("Just so so")

    case _ => println("you need work harder")

  }

}

在模式匹配中使用if守卫

// Scala的模式匹配语法,有一个特点在于,可以在case后的条件判断中,不仅仅只是提供一个值,而是可以在值后面再加一个if守卫,进行双重过滤

// 案例:成绩评价(升级版)

def judgeGrade(name: String, grade: String) {

  grade match {

    case "A" => println(name + ", you are excellent")

    case "B" => println(name + ", you are good")

    case "C" => println(name + ", you are just so so")

case _ if name == "leo" => println(name + ", you are a good boy, come on")

    case _ => println("you need to work harder")

  }

}

在模式匹配中进行变量赋值

// Scala的模式匹配语法,有一个特点在于,可以将模式匹配的默认情况,将下划线替换为一个变量名,此时模式匹配语法就会将要匹配的值赋值给这个变量,从而可以在后面的处理语句中使用要匹配的值


// 为什么有这种语法??思考一下。因为只要使用用case匹配到的值,是不是我们就知道这个只啦!!在这个case的处理语句中,是不是就直接可以使用写程序时就已知的值!


// 但是对于下划线_这种情况,所有不满足前面的case的值,都会进入_这种默认情况进行处理,此时如果我们在处理语句中需要拿到具体的值进行处理呢?那就需要使用这种在模式匹配中进行变量赋值的语法!!


// 案例:成绩评价(升级版)

def judgeGrade(name: String, grade: String) {

  grade match {

    case "A" => println(name + ", you are excellent")

    case "B" => println(name + ", you are good")

    case "C" => println(name + ", you are just so so")

case grade_ if name == "leo" => println(name + ", you are a good boy, come on, your grade is " + grade+  " : " + grade_)

case _ => println("you need to work harder, your grade is " + grade)

  }

}

对类型进行模式匹配

// Scala的模式匹配一个强大之处就在于,可以直接匹配类型,而不是值!!!这点是java的switch case绝对做不到的。


// 理论知识:对类型如何进行匹配?其他语法与匹配值其实是一样的,但是匹配类型的话,就是要用“case 变量: 类型 => 代码”这种语法,而不是匹配值的“case 值 => 代码”这种语法。


// 案例:异常处理

import java.io._

def processException(e: Exception) {

  e match {

    case e1: IllegalArgumentException => println("you have illegal arguments! exception is: " + e1)

    case e2: FileNotFoundException => println("cannot find the file you need read or write!, exception is: " + e2)

    case e3: IOException => println("you got an error while you were doing IO operation! exception is: " + e3)

    case _: Exception => println("cannot know which exception you have!" )

  }

}

processException(new IOException ("File not found"))

对Array和List进行模式匹配

// 对Array进行模式匹配,分别可以匹配带有指定元素的数组、带有指定个数元素的数组、以某元素打头的数组

// 对List进行模式匹配,与Array类似,但是需要使用List特有的::操作符

// 案例:对朋友打招呼

def greeting(arr: Array[String]) {

  arr match {

    case Array("Leo") => println("Hi, Leo!")

    case Array(girl1, girl2, girl3) => println("Hi, girls, nice to meet you. " + girl1 + " and " + girl2 + " and " + girl3)

    case Array("Leo", _*) => println("Hi, Leo, please introduce your friends to me.")

    case _ => println("hey, who are you?")

  }

}

greeting(Array("Leo","Jack"))


def greeting(list: List[String]) {

  list match {

    case "Leo" :: Nil => println("Hi, Leo!")

    case girl1 :: girl2 :: girl3 :: Nil => println("Hi, girls, nice to meet you. " + girl1 + " and " + girl2 + " and " + girl3)

    case "Leo" :: tail => println("Hi, Leo, please introduce your friends to me.")

    case _ => println("hey, who are you?")

  }

}

greeting(List("Leo","Jack"))

case class与模式匹配

// Scala中提供了一种特殊的类,用case class进行声明,中文也可以称作样例类。case class其实有点类似于Java中的JavaBean的概念。即只定义field,并且由Scala编译时自动提供getter和setter方法,但是没有method。


// case class的主构造函数接收的参数通常不需要使用var或val修饰,Scala自动就会使用val修饰(但是如果你自己使用var修饰,那么还是会按照var来)


//  Scala自动为case class定义了伴生对象,也就是object,并且定义了apply()方法,该方法接收主构造函数中相同的参数,并返回case class对象

// 案例:学校门禁

class Person

case class Teacher(name: String, subject: String) extends Person

case class Student(name: String, classroom: String) extends Person

def judgeIdentify(p: Person) {

  p match {

    case Teacher(name, subject) => println("Teacher, name is " + name + ", subject is " + subject)

    case Student(name, classroom) => println("Student, name is " + name + ", classroom is " + classroom)

    case _ => println("Illegal access, please go out of the school!")

  } 

}

judgeIdentify(Student("Leo","1"))

Option与模式匹配

// Scala有一种特殊的类型,叫做Option。Option有两种值,一种是Some,表示有值,一种是None,表示没有值。

// Option通常会用于模式匹配中,用于判断某个变量是有值还是没有值,这比null来的更加简洁明了

// Option的用法必须掌握,因为Spark源码中大量地使用了Option,比如Some(a)、None这种语法,因此必须看得懂Option模式匹配,才能够读懂spark源码。

// 案例:成绩查询

val grades = Map("Leo" -> "A", "Jack" -> "B", "Jen" -> "C")

def getGrade(name: String) {

  val grade = grades.get(name)

  grade match {

    case Some(grade1) => println("your grade is " + grade1)

    case None => println("Sorry, your grade information is not in the system")

  }

}

getGrade("Lily")

getGrade("Leo")


Scala集合类的某些标准操作会产生Option可选值,如Map的get方法,查到值时返回Some(value)对象,没查到时返回None对象(而Java中返回的为Null,这会容易导致程序运行错误)

类型参数

类型参数是什么?类型参数其实就类似于Java中的泛型。先说说Java中的泛型是什么,比如我们有List a = new ArrayList(),接着a.add(1),没问题,a.add("2"),然后我们a.get(1) == 2,对不对?肯定不对了,a.get(1)获取的其实是个String——"2",String——"2"怎么可能与一个Integer类型的2相等呢?


所以Java中提出了泛型的概念,其实也就是类型参数的概念,此时可以用泛型创建List,List a = new ArrayList[Integer](),那么,此时a.add(1)没问题,而a.add("2")呢?就不行了,因为泛型会限制,只能往集合中添加Integer类型,这样就避免了上述的问题。


那么Scala的类型参数是什么?其实意思与Java的泛型是一样的,也是定义一种类型参数,比如在集合,在类,在函数中,定义类型参数,然后就可以保证使用到该类型参数的地方,就肯定,也只能是这种类型。从而实现程序更好的健壮性。


此外,类型参数是Spark源码中非常常见的,因此同样必须掌握,才能看懂spark源码。

泛型类

// 泛型类(类声明时类名后面中括号中的即为类型参数),顾名思义,其实就是在类的声明中,定义一些泛型类型,然后在类内部,比如field或者method,就可以使用这些泛型类型。

// 使用泛型类,通常是需要对类中的某些成员,比如某些field和method中的参数或变量,进行统一的类型限制,这样可以保证程序更好的健壮性和稳定性。

// 如果不使用泛型进行统一的类型限制,那么在后期程序运行过程中,难免会出现问题,比如传入了不希望的类型,导致程序出问题。

// 在使用类的时候,比如创建类的对象,将类型参数替换为实际的类型,即可。

案例:新生报到,每个学生来自不同的地方,id可能是Int,可能是String

class Student[T](val localId: T) { // 在类参数中使用类型参数

def getSchoolId(hukouId: T) = "S-" + hukouId + "-" + localId // 在方法参数中使用类型参数

}

val leo = new Student[Int](111)


// Scala自动推断泛型类型特性:直接给使用了泛型类型的field赋值时,Scala会自动进行类型推断。

scala> val leo = new Student(111)

leo: Student[Int] = Student@f001896

scala> val leo = new Student("string")

leo: Student[String] = Student@488eb7f2

泛型函数

// 泛型函数(方法声明时方法名后面中括号中的即为类型参数),与泛型类类似,可以给某个函数在声明时指定泛型类型,然后在函数体内,多个变量或者返回值之间,就可以使用泛型类型进行声明,从而对某个特殊的变量,或者多个变量,进行强制性的类型限制。

案例:卡片售卖机,可以指定卡片的内容,内容可以是String类型或Int类型

def getCard[T](content: T) = {

  if(content.isInstanceOf[Int]) "card: 001, " + content

  else if(content.isInstanceOf[String]) "card: this is your card, " + content

  else "card: " + content

}

getCard[String]("hello world")

getCard[Double](0.01)


// 与泛型类一样,你可以通过给使用了泛型类型的变量传递值来让Scala自动推断泛型的实际类型,也可以在调用函数时,手动指定泛型类型,上面就是在调用时手动在中括号中指定的,下面靠传入值自动推断:

scala> getCard ("hello world")

res2: String = card: this is your card, hello world

scala> getCard (0.01)

res3: String = card: 0.01

上边界Bounds

// 在指定泛型类型的时候,有时,我们需要对泛型类型的范围进行界定,而不是可以是任意的类型。比如,我们可能要求某个泛型类型,它就必须是某个类的子类,这样在程序中就可以放心地调用泛型类型继承的父类的方法,程序才能正常的使用和运行。此时就可以使用上下边界Bounds的特性。如下面没有使用上边界时,是不能调用类型参数相关方法的:

class Person(val name: String) {

  def makeFriends(p: Person) {}

}

class Party[T](p1: T, p2: T) {

def play = p1.makeFriends(p2) // 编译会出错

}


// Scala的上下边界特性允许泛型类型必须是某个类的子类,或者必须是某个类的父类


案例:在派对上交朋友

class Person(val name: String) {

  def sayHello = println("Hello, I'm " + name)

  def makeFriends(p: Person) {

    sayHello

    p.sayHello

  }

}

class Student(name: String) extends Person(name)

class Party[T <: Person](p1: T, p2: T) { // <:要求T必须是Person或其子类,由于p1 类型为Person或其子类Student,所以可以调用父类中的方法

  def play = p1.makeFriends(p2)

}


val leo = new Student("Leo")

val lily = new Student("Lily")

new Party(leo,lily).play

下边界Bounds

// 除了指定泛型类型的上边界,还可以指定下边界,即指定泛型类型必须是某个类的父类


案例:领身份证(只能是自己或父亲代领)

class Father(val name: String)

class Child(name: String) extends Father(name)

def getIDCard[R >: Child](person: R) { // R必须是Child的父类

  if (person.getClass == classOf[Child]) println("please tell us your parents' names.")

  else if (person.getClass == classOf[Father]) println("sign your name for your child's id card.")

  else println("sorry, you are not allowed to get id card.")

}


val f = new Father("Father")

val c = new Child("Child")

scala> getIDCard[Father](f)

sign your name for your child's id card.

scala> getIDCard[Child](c)

please tell us your parents' names.


scala> getIDCard(f) // 类型自动推断

sign your name for your child's id card.

scala> getIDCard(c) // 类型自动推断

please tell us your parents' names.


val s = new Student("Student")

scala> getIDCard(s)

sorry, you are not allowed to get id card.


注:下边界与上边界是不同的,上边界是为了可以调用类型参数相应方法,而下边界是为了限制泛型类或泛型函数只适用于哪些类,而不是为了调用类型参数相应方法。

View Bounds

// 上下边界Bounds,虽然可以让一种泛型类型,支持有父子关系的多种类型。但是,在某个类与上下边界Bounds指定的父子类型范围内的类都没有任何关系,则默认是肯定不能接受的。


// 然而,View Bounds作为一种上下边界Bounds的加强版,支持可以对类型进行隐式转换,将指定的类型进行隐式转换后,再判断是否在边界指定的类型范围内


案例:跟小狗交朋友

class Person(val name: String) {

  def sayHello = println("Hello, I'm " + name)

  def makeFriends(p: Person) {

    sayHello

    p.sayHello

  }

}

class Student(name: String) extends Person(name)

class Dog(val name: String) { def sayHello = println("Wang, Wang, I'm " + name) }


implicit def dog2person(o: Object): Person =

       if(o.isInstanceOf[Dog]) {println("-D-");val _o = o.asInstanceOf[Dog]; new Person(_o.name){

              override def sayHello = _o.sayHello

} }// 如果是狗,隐式的转换为人

       //注:即使是Person或Student,也要将Object强转成Person后返回,而不能直接将o返回,否则可能引起循环调用隐式转换

else if(o.isInstanceOf[Person]) {println("-P-");val _o = o.asInstanceOf[Person];_o} // 如果是人,不用转换,只是强转型后返回

       else {println("-O-");error(o.toString)} //其他情况返回Nothing



class Party[T <% Person](p1: T){ // <%表示T可以是Person或其子类,或者是可以经过隐式转换后成为Person或其子类的类

   def play() = {println(p1.name);println("--------------------")}

}

class Party2[T <% Person](p1: T,p2: T){

  def play() = {p1.makeFriends(p2);println("--------------------")}

}


val leo = new Person("Leo")

val lily = new Student("Lily")

new Party(leo).play()

new Party(lily).play()

val dog = new Dog("Dog")

new Party(dog).play()//发生隐式转换

new Party2(lily,leo).play()

new Party2(dog,leo).play()//发生隐式转换

new Party2(lily,dog).play()//发生隐式转换

Context Bounds

// Context Bounds是一种特殊的Bounds,它会根据泛型类型的声明,比如“T: 类型”要求必须存在一个类型为“类型[T]”的隐式值(运行时Scala会帮我们自动注入这个已存在的隐式值)。其实个人认为,Context Bounds之所以叫Context,是因为它基于的是一种全局的上下文,需要使用到上下文中的隐式值以及注入。


案例:使用Scala内置的比较器比较大小

// Ordering[T]类似Java中的Comparator比较器

class Calculator[T: Ordering] (val number1: T, val number2: T) {

  //运行时,Scala会在上下文中去找类型为Ordering[T]的隐式值并注进来,所以调用该方法时不需要传递该参数值了

  def max(implicit order: Ordering[T]) = if(order.compare(number1, number2) > 0) number1 else number2

}

new Calculator(3,4).max

Manifest Context Bounds

// 在Scala中,如果要实例化一个泛型数组,就必须使用Manifest Context Bounds。也就是说,如果数组元素类型为T的话,需要为类或者函数定义[T: Manifest]泛型类型,这样才能实例化Array[T]这种泛型数组。


案例:打包饭菜(一种食品打成一包)

class Meat(val name: String)

class Vegetable(val name: String)


def packageFood[T: Manifest] (food: T*) = {

// 创建泛型数组Array[T]:即数组中的元素类型要在运行时才能确定,编译时无法确定,元素类型为动态

  val foodPackage = new Array[T](food.length)

  for(i <- 0 until food.length) foodPackage(i) = food(i)

  foodPackage

}

val gongbaojiding = new Meat("gongbaojiding")

val yuxiangrousi = new Meat("yuxiangrousi")

val shousiyangpai = new Meat("shousiyangpai")

val meatPackageFood = packageFood(gongbaojiding,yuxiangrousi,shousiyangpai)

meatPackageFood: Array[Meat] = Array(Meat@20b829d5, Meat@7c5f29c6, Meat@4baf997)


val qingcai = new Vegetable("qingcai")

val baicai = new Vegetable("baicai")

val huanggua = new Vegetable("huanggua")

val vegPackageFood = packageFood(qingcai,baicai,huanggua)

vegPackageFood: Array[Vegetable] = Array(Vegetable@583030bd, Vegetable@2ac3d530, Vegetable@2431050d)

协变和逆变

// Scala的协变和逆变是非常有特色的!完全解决了Java中的泛型的一大缺憾!

// 举例来说,Java中,如果有Professional是Master的子类,那么Card[Professionnal]是不是Card[Master]的子类?答案是:不是。因此对于开发程序造成了很多的麻烦。

// 而Scala中,只要灵活使用协变和逆变,就可以解决Java泛型的问题。


案例:进入会场

class Master // 大师

class Professional extends Master // 专家,按理来说,大师是一种专家,应该是Master为Professional子类才对


// 大师以及专家的名片都可以进入会场

class Card[+T] (val name: String) // 协变:当类型B是类型A的子类型,则可以认为T[B]是T[A]的子类

def enterMeet(card: Card[Master]) {

  println("welcome to have this meeting!")

}

// 如果去掉+加号,则Card[Professional]不能传入到enterMeet方法


//专家级别及以上大师级别的名片就可以进入会场

class Card[-T] (val name: String) // 逆变:当类型B是类型A的子类型,则反过来可以认为T[A]是T[B]的子类型

// 要想父类也可以传进来,则要让Card进行逆变,这样Card[Master]就反过来成为Card[Professional]的子类,所以就能传进来了

def enterMeet(card: Card[Professional]) {

  println("welcome to have this meeting!")

}

Existential Type

// 在Scala里,有一种特殊的类型参数,就是Existential Type,存在性类型。这种类型务必掌握是什么意思,因为在spark源码实在是太常见了!

Array[T] forSome { type T }

Array[_]



scala> def foo[T](x : Array[T]) = println(x.length)

foo: [T](x: Array[T])Unit

scala>  foo(Array[String]("foo", "bar", "baz"))

3


scala> def foo(x : Array[T] forSome { type T}) = println(x.length)

foo: (x: Array[_])Unit

scala> foo(Array[String]("foo", "bar", "baz"))

3


scala> def foo(x : Array[_]) = println(x.length)

foo: (x: Array[_])Unit

scala> foo(Array[String]("foo", "bar", "baz"))

3


scala> def foo(x : Array[T] forSome { type T <: CharSequence}) = x.foreach(y => println(y.length))

foo: (x: Array[_ <: CharSequence])Unit

scala> foo(Array[String]("foo", "bar", "baz"))

3

3

3

隐式转换

Scala提供的隐式转换和隐式参数功能,是非常有特色的功能。是Java等编程语言所没有的功能。它可以允许你手动指定,将某种类型的对象转换成其他类型的对象。通过这些功能,可以实现非常强大,而且特殊的功能。


Scala的隐式转换,其实最核心的就是定义隐式转换函数,即implicit conversion function。定义的隐式转换函数,只要在编写的程序内引入,就会被Scala自动使用。Scala会根据隐式转换函数的签名,在程序中使用到隐式转换函数接收的参数类型定义的对象时,会自动将其传入隐式转换函数,转换为另外一种类型的对象并返回。这就是“隐式转换”。


隐式转换函数叫什么名字是无所谓的,因为通常不会由用户手动调用,而是由Scala进行调用。但是如果要使用隐式转换,则需要对隐式转换函数进行导入。因此通常建议将隐式转换函数的名称命名为“one2one”的形式。


Spark源码中有大量的隐式转换和隐式参数,因此必须精通这种语法。


// 要实现隐式转换,只要程序可见的范围内定义隐式转换函数即可。Scala会自动使用隐式转换函数。隐式转换函数与普通函数唯一的语法区别就是,要以implicit开头,而且最好要定义函数返回类型。


// 案例:特殊售票窗口(只接受特殊人群,比如学生、老人等)

class SpecialPerson(val name: String)

class Student(val name: String)

class Older(val name: String)

implicit def object2SpecialPerson (obj: Object): SpecialPerson = {

  if (obj.getClass == classOf[Student]) { val stu = obj.asInstanceOf[Student]; new SpecialPerson(stu.name) }

  else if (obj.getClass == classOf[Older]) { val older = obj.asInstanceOf[Older]; new SpecialPerson(older.name) }

 else error(obj.toString)

}

var ticketNumber = 0

def buySpecialTicket(p: SpecialPerson) = {//只针对特殊人群卖票,所以如果是学生与老人,则要先转换为特殊人群

  ticketNumber += 1

  "T-" + ticketNumber

}


class Teacher(val name:String)

scala> val tom = new Teacher("tom")

scala> buySpecialTicket(tom)

java.lang.RuntimeException: Teacher@277f7dd3

  at scala.sys.package$.error(package.scala:27)

  at scala.Predef$.error(Predef.scala:144)

  at .object2SpecialPerson(:17)

  ... 32 elided


scala> val s = new Student("student")

scala> val o = new Older("older")

scala> buySpecialTicket(s)

res1: String = T-1

scala> buySpecialTicket(o)

res2: String = T-2

使用隐式转换加强现有类型

// 隐式转换非常强大的一个功能,就是可以在不知不觉中加强现有类型的功能(有点像Java中的装饰模式)。也就是说,可以为某个类定义一个加强版的类,并定义互相之间的隐式转换,从而让源类在使用加强版的方法时,由Scala自动进行隐式转换为加强类,然后再调用该方法(如内置的Int 类的加强版 RichInt)。


// 案例:超人变身

class Man(val name: String)

class Superman(val name: String) {

def emitLaser = println("emit a laster!") // 超人才有该方法

}

implicit def man2superman(man: Man): Superman = new Superman(man.name)

val leo = new Man("leo") // 普通人

leo.emitLaser // 调用不存在的方法时,会自动的先转换为超人

隐式转换函数作用域与导入

// Scala默认有两种找隐式转换的方式:首先是从源类型或者目标类型,这两类型的伴生对象中找隐式转换函数;然后在当前程序作用域内找隐式转换函数。


// 如果隐式转换函数不在上述两种情况下的话,那么就必须手动使用import语法引入某个包下的隐式转换函数,比如import test._。通常建议,仅仅在需要进行隐式转换的地方,比如某个函数或者方法内,用import导入隐式转换函数,这样可以缩小隐式转换函数的作用域,避免不需要的隐式转换。

隐式转换的发生时机

// 1、调用某个函数,但是给函数传入的参数的类型,与函数定义的接收参数类型不匹配(案例:特殊售票窗口

// 2、使用某个类型的对象,调用某个方法,而这个方法并不存在于该类型时(案例:超人变身

// 3、使用某个类型的对象,调用某个方法,虽然该类型有这个方法,但是传给方法的参数类型与方法定义的接收参数的类型不匹配(案例:特殊售票窗口加强版)

//4、将一种类型赋值给另一种类型时,如果类型不兼容时


// 案例:特殊售票窗口加强版

class TicketHouse {

  var ticketNumber = 0

  def buySpecialTicket(p: SpecialPerson) = {

    ticketNumber += 1

    "T-" + ticketNumber

  }

}

new TicketHouse().buySpecialTicket(new Student("student"))

隐式参数

// 所谓的隐式参数,指的是在函数或者方法中,定义一个用implicit修饰的参数,此时Scala会尝试找到一个指定类型的,用implicit修饰的对象,即隐式值,并自动注入到该隐式参数。


// Scala会在两个范围内查找:一种是当前作用域内定义val或var隐式变量;一种是从隐式参数类型的伴生对象内找隐式值


// 案例:考试签到

class SignPen {

  def write(content: String) = println(content)

}

implicit val signPen = new SignPen

def signForExam(name: String) (implicit signPen: SignPen) {

  signPen.write(name + " come to exam in time.")

}

Actor

Scala的Actor类似于Java中的多线程编程。但是不同的是,Scala的Actor提供的模型与多线程有所不同。Scala的Actor尽可能地避免锁和共享状态,从而避免多线程并发时出现资源争用的情况,进而提升多线程编程的性能。此外,Scala Actor的这种模型还可以避免死锁等一系列传统多线程编程的问题。


Spark中使用的分布式多线程框架,是Akka。Akka也实现了类似Scala Actor的模型,其核心概念同样也是Actor。因此只要掌握了Scala Actor,那么在Spark源码研究时,至少即可看明白Akka Actor相关的代码。但是,换一句话说,由于Spark内部有大量的Akka Actor的使用,因此对于Scala Actor也至少必须掌握,这样才能学习Spark源码。

Actor的创建、启动和消息收发

// Scala提供了Actor trait来让我们更方便地进行actor多线程编程,就Actor trait就类似于Java中的Thread和Runnable一样,是基础的多线程基类和接口。我们只要重写Actor trait的act方法,即可实现自己的线程执行体,与Java中重写run方法类似。

// 此外,使用start()方法启动actor;使用!符号,向actor发送消息;actor内部使用receive和模式匹配接收消息

// 案例:Actor Hello World

import scala.actors.Actor

class HelloActor extends Actor {

  def act() {

    while (true) {

      receive {

        case name: String => println("Hello, " + name)

      }

    }

  }

}

val helloActor = new HelloActor

helloActor.start()

helloActor ! "leo"

收发case class类型的消息

// Scala的Actor模型与Java的多线程模型之间,很大的一个区别就是,Scala Actor天然支持线程之间的精准通信;即一个actor可以给其他actor直接发送消息。这个功能是非常强大和方便的。

// 要给一个actor发送消息,需要使用“actor ! 消息”的语法。在scala中,通常建议使用样例类,即case class来作为消息进行发送。然后在actor接收消息之后,可以使用scala强大的模式匹配功能来进行不同消息的处理。

// 案例:用户注册登录后台接口

import scala.actors.Actor

case class Login(username: String, password: String)

case class Register(username: String, password: String)

class UserManageActor extends Actor {

  def act() {

    while (true) {

      receive {

        case Login(username, password) => println("login, username is " + username + ", password is " + password)

        case Register(username, password) => println("register, username is " + username + ", password is " + password)

      }

    }

  }

}

val userManageActor = new UserManageActor

userManageActor.start()

userManageActor ! Register("leo", "1234"); userManageActor ! Login("leo", "1234")

Actor之间互相收发消息

// 如果两个Actor之间要互相收发消息,那么scala的建议是,一个actor向另外一个actor发送消息时,同时带上自己的引用;其他actor收到自己的消息时,直接通过发送消息的actor的引用,即可以给它回复消息。

// 案例:打电话

import scala.actors.Actor

case class Message(content: String, sender: Actor)

class LeoTelephoneActor extends Actor {

  def act() {

    while (true) {

      receive {

        case Message(content, sender) => { println("leo telephone: " + content); sender ! "I'm leo, please call me after 10 minutes." }

      }

    }

  }

}

class JackTelephoneActor(val leoTelephoneActor: Actor) extends Actor {

  def act() {

    leoTelephoneActor ! Message("Hello, Leo, I'm Jack.", this)

    receive {

      case response: String => println("jack telephone: " + response)

    }

  }

}

val leoTel = new LeoTelephoneActor

val jackTel = new JackTelephoneActor(leoTel)

leoTel.start

jackTel.start

同步消息和Future

// 默认情况下,消息都是异步的;但是如果希望发送的消息是同步的,即对方接受后,一定要给自己返回结果,那么可以使用!?的方式发送消息。即val reply = actor !? message。


// 如果要异步发送一个消息,但是在后续要获得消息的返回值,那么可以使用Future。即!!语法。val future = actor !! message。val reply = future()。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345