Scala学习笔记(三)

一、模式匹配

scala中有一个非常强大的模式匹配机制,可以应用在很多场景:

  • match语句
  • 类型查询
  • 使用模式匹配快速获取数据

1. 简单模式匹配

在java中,有switch关键字,可以简化if条件判断语句。在scala中,可以使用match表达式替代。

  • 语法格式

    变量 match {
    case "常量1" => 表达式1
    case "常量2" => 表达式2
    case "常量3" => 表达式3
    case _ => 表达式4 // 默认配
    }

  • 示例
    1. 从控制台输入一个单词(使用StdIn.readLine方法)
    2. 判断该单词是否能够匹配以下单词,如果能匹配,返回对应的话
    3. 打印这句话
单词返回
hadoop大数据分布式存储和计算框架
zookeeper大数据分布式协调服务框架
spark大数据分布式内存计算框架
未匹配未匹配
object MatchDemo {
  	def main(args: Array[String]): Unit = {
	    println("请输出一个词:")
	    // StdIn.readLine表示从控制台读取一行文本
	    val name = StdIn.readLine()
	
	    val result = name match {
	      case "hadoop" => "大数据分布式存储和计算框架"
	      case "zookeeper" => "大数据分布式协调服务框架"
	      case "spark" => "大数据分布式内存计算框架"
	      case _ => "未匹配"
	    }
	    println(result)
  	}
}

2. 匹配类型

除了像Java中的switch匹配数据之外,match表达式还可以进行类型匹配。如果我们要根据不同的数据类型,来执行不同的逻辑,也可以使用match表达式来实现。

  • 语法格式

    变量 match {
    case 类型1变量名: 类型1 => 表达式1
    case 类型2变量名: 类型2 => 表达式2
    case 类型3变量名: 类型3 => 表达式3
    ...
    case _ => 表达式4
    }

  • 示例
    • 定义一个变量为Any类型,然后分别给其赋值为"hadoop"、1、1.0
    • 定义模式匹配,然后分别打印类型的名称
    object MatchDemo {
      	def main(args: Array[String]): Unit = {
    	    val a:Any = "hadoop"
    	
    	    val result = a match {
    	      case _:String => "String"
    	      case _:Int => "Int"
    	      case _:Double => "Double"
    	    }
    	    println(result)
      	}
    }
    

tips:

  • 如果case表达式中无需使用到匹配到的变量,可以使用下划线代代替

3. 守卫

在java中,只能简单地添加多个case标签,例如:要匹配0-7,就需要写出来8个case语句。例如:

int a = 0;
switch(a) {
    case 0: a += 1;
    case 1: a += 1;
    case 2: a += 1;
    case 3: a += 1;
    case 4: a += 2;
    case 5: a += 2;
    case 6: a += 2;
    case 7: a += 2;
    default: a = 0;
}

在scala中,可以使用守卫来简化上述代码——也就是在case语句中添加if条件判断

  • 示例
    • 从控制台读入一个数字a(使用StdIn.readInt)
    • 如果 a >= 0 而且 a <= 3,打印[0-3]
    • 如果 a >= 4 而且 a <= 8,打印[4,8]
    • 否则,打印未匹配
    val a = StdIn.readInt()
    
    a match {
        case _ if a >= 0 && a <= 3 => println("[0-3]")
        case _ if a >= 4 && a <= 8 => println("[4-8]")
        case _ => println("未匹配")
    }
    

4. 匹配样例类

scala可以使用模式匹配来匹配样例类,从而可以快速获取样例类中的成员数据。后续,我们在开发Akka案例时,还会用到。

  • 示例

    • 创建两个样例类Customer、Order
      • Customer包含姓名、年龄字段
      • Order包含id字段
    • 分别定义两个案例类的对象,并指定为Any类型
    • 使用模式匹配这两个对象,并分别打印它们的成员变量值
    // 1. 创建两个样例类
    case class Person(name:String, age:Int)
    case class Order(id:String)
    
    def main(args: Array[String]): Unit = {
        // 2. 创建样例类对象,并赋值为Any类型
        val zhangsan:Any = Person("张三", 20)
        val order1:Any = Order("001")
    
        // 3. 使用match...case表达式来进行模式匹配
        // 获取样例类中成员变量
        order1 match {
            case Person(name, age) => println(s"姓名:${name} 年龄:${age}")
            case Order(id1) => println(s"ID为:${id1}")
            case _ => println("未匹配")
        }
    }
    

5. 匹配数组

  • 示例

    • 依次修改代码定义以下三个数组
      Array(1,x,y)   // 以1开头,后续的两个元素不固定
      Array(0)	   // 只匹配一个0元素的元素
      Array(0, ...)  // 可以任意数量,但是以0开头
    
    • 使用模式匹配上述数组
    val arr = Array(1, 3, 5)
    arr match {
        case Array(1, x, y) => println(x + " " + y)
        case Array(0) => println("only 0")
        case Array(0, _*) => println("0 ...")
        case _ => println("something else")
    }
    

6. 匹配列表

  • 示例

    • 依次修改代码定义以下三个列表

      List(0)				// 只保存0一个元素的列表
      List(0,...)   		// 以0开头的列表,数量不固定
      List(x,y)	   		// 只包含两个元素的列表  
      
    • 使用模式匹配上述列表

    val list = List(0, 1, 2)
    
    list match {
        case 0 :: Nil => println("只有0的列表")
        case 0 :: tail => println("0开头的列表")
        case x :: y :: Nil => println(s"只有另两个元素${x}, ${y}的列表")
        case _ => println("未匹配")
    }
    

7. 匹配元组

  • 示例

    • 依次修改代码定义以下两个元组
      (1, x, y)		// 以1开头的、一共三个元素的元组
      (x, y, 5)   // 一共有三个元素,最后一个元素为5的元组
      
    • 使用模式匹配上述元素
    val tuple = (2, 2, 5)
    
    tuple match {
        case (1, x, y) => println(s"三个元素,1开头的元组:1, ${x}, ${y}")
        case (x, y, 5) => println(s"三个元素,5结尾的元组:${x}, ${y}, 5")
        case _ => println("未匹配")
    }
    

8. 变量声明中的模式匹配

在定义变量的时候,可以使用模式匹配快速获取数据

8.1 获取数组中的元素

  • 示例
    • 生成包含0-10数字的数组,使用模式匹配分别获取第二个、第三个、第四个元素
    val list = (1 to 10).toList
    val head :: x :: y :: tail = list
    println(x, y)
    

8.2 获取List中的数据

  • 示例
    • 生成包含0-10数字的列表,使用模式匹配分别获取第一个、第二个元素
    val list = (1 to 10).toList
    val x :: y :: tail = list
    println(x, y)
    

二、样例类

样例类是一种特殊类,它可以用来快速定义一个用于保存数据的类(类似于Java POJO类),在后续要学习并发编程和spark、flink这些框架也都会经常使用它。

1. 定义样例类

  • 语法格式

    case class 样例类名([var/val] 成员变量名1:类型1, 成员变量名2:类型2, 成员变量名3:类型3)

  • 默认为val,可以省略
  • 如果要实现某个成员变量可以被修改,可以添加var

2. 定义一个样例类

  • 示例

    • 定义一个Person样例类,包含姓名和年龄成员变量
    • 创建样例类的对象实例(“Aiden”、23),并打印它
    object Demo{
      case class Person(name:String, age:Int)
    
      def main(args: Array[String]): Unit = {
        val zhangsan = Person("Aiden", 23)
        println(zhangsan)
      }
    }
    

3. 可变成员变量

  • 示例

    • 定义一个Person样例类,包含姓名和年龄成员变量
    • 创建样例类的对象实例(“Aiden”、23)
    • 修改张三的年龄为23岁,并打印
    object Demo{
      case class Person(var name:String, var age:Int)
    
      def main(args: Array[String]): Unit = {
        val zhangsan = Person("张三", 20)
        zhangsan.age = 23
        println(zhangsan)
      }
    }
    

4. 样例类的方法

当我们定义一个样例类,编译器自动帮助我们实现了以下几个有用的方法:

  • apply方法
  • toString方法
  • equals方法
  • hashCode方法
  • copy方法

4.1 apply方法

在样例类中,编译器自动封装了apply方法,可以完成在创建对象实例的时候,不用new对象,直接调用。

  • 示例

    • 用辅助构造器定义一个Person样例类
    • 创建main方法,通过编译器自动封装的apply方法,直接获取对象的实例
    case class Person(name:String,var age:Int)
    
    object CaseClassDemo {
    	def main(args:Array[String]):Unit = {
    		val person = Person("Aiden"23)
    	}
    }
    

4.1 toString方法

toString方法返回样例类名称(成员变量1, 成员变量2, 成员变量3....),我们可以更方面查看样例类的成员

  • 示例

    • 用辅助构造器定义一个Person样例类
    • 创建main方法,通过编译器自动封装的toString方法,直接打印对象信息
    case class Person(name:String, age:Int)
    
    object CaseClassDemo {
      	def main(args: Array[String]): Unit = {
    	    val person= CasePerson("Aiden", 23)
    	    println(person)// 输出:Person(Aiden,23)
      	}
    }
    

4.2 equals方法

样例类自动实现了equals方法,可以直接使用==比较两个样例类是否相等,即所有的成员变量是否相等

  • 示例

    • 创建一个样例类Person,包含姓名、年龄
    • 创建名字年龄分别为"Aiden", 23的两个对象
    • 比较它们是否相等
    case class Person(name:String, age:Int)
    
    object Demo {
      	def main(args: Array[String]): Unit = {
    	    val person1 = Person("Aiden", 23)
    	    val person2 = Person("Aiden", 23)
    	    println(person1 == person2 )// 输出:true
      	}
    }
    

4.3 hashCode方法

样例类自动实现了hashCode方法,如果所有成员变量的值相同,则hash值相同,只要有一个不一样,则hash值不一样。

  • 示例

    • 创建一个样例类Person,包含姓名、年龄
    • 创建一个名字年龄分别为"Aiden1", 23的对象
    • 再创建一个名字年龄分别为"Aiden2", 23的对象
    • 分别打印这两个对象的哈希值
    case class Person(name:String, age:Int)
    
    object Demo {
      	def main(args: Array[String]): Unit = {
    	    val person1 = Person("Aiden1", 23)
    	    val person2 = Person("Aiden2", 23)
    	    println(person1.hashCode())// 输出:-784293932
    	    println(person2.hashCode())// 输出:158930752
      	}
    }
    

4.4 copy方法

样例类实现了copy方法,可以快速创建一个相同的实例对象,可以使用带名参数指定给成员进行重新赋值

  • 示例

    • 创建名字年龄分别为"李四", 21的对象
    • 通过copy拷贝,名字为"王五"的对象
    • 创建一个样例类Person,包含姓名、年龄
    • 创建一个名字年龄分别为"Aiden", 23的对象person1
    • 通过copy方法,创建一个对象person2,打印
    • 通过copy方法,创建一个对象person3,并修改name属性,打印
    case class Person(name:String, age:Int)
    
    object Demo {
      	def main(args: Array[String]): Unit = {
    	    val person1 = Person("Aiden", 23)
    	    val person2 = person1.copy()
    	    println(person2)// 输出:Person(Aiden,23)
    	    val person3 = person1.copy(name = "AidenBrett")
    	    println(person3)// 输出:Person(AidenBrett,23)
      	}
    }
    

三、样例对象

使用case object可以创建样例对象。样例对象是单例的,而且它没有主构造器

  • 语法格式

    case object 样例对象名

它主要用在两个地方:

  1. 定义枚举
  2. 作为没有任何参数的消息传递(在《Scala学习笔记(四)》的Akka编程会讲到)

四、Option类型

scala中,Option类型来表示可选值。使用Option类型,可以用来有效避免空引用(null)异常。也就是说,将来我们返回某些数据时,可以返回一个Option类型来替代。这种类型的数据有两种形式:

  • Some(x):表示实际的值
  • None:表示没有值
    • 使用getOrElse方法,当值为None是可以指定一个默认值
  • 示例一

    • 创建一个Map对象,添加(name,Aiden)键值对
    • 打印value的值
    object Demo {
    	def main(args: Array[String]): Unit = {
    	    val m = Map("name" -> "Aiden")
    	    val result = m.get("name")
    	    println(result)// 输出:Some(Aiden)
    	    val result1 = m.get("name1")
    	    println(result1)// 输出:None
    	}
    }
    
  • 示例二

    • 定义一个两个数相除的方法,使用Option类型来封装结果
    • 然后使用模式匹配来打印结果
    • 不是除零,打印结果
    • 除零打印异常错误
    object Demo {
      	def dvi(a:Double, b:Double):Option[Double] = {
        	if(b != 0) {
          		Some(a / b)
        	}
        	else {
          		None
        	}
      	}
    
     	def main(args: Array[String]): Unit = {
        	val result1 = dvi(1.0, 5)
    
        	result1 match {
          		case Some(x) => println(x)
          		case None => println("除零异常")
        	}
    	}
    }
    

五、偏函数

偏函数可以提供了简洁的语法,可以简化函数的定义。配合集合的函数式编程,可以让代码更加优雅。

  • 偏函数被包在花括号内没有match的一组case语句是一个偏函数

  • 偏函数是PartialFunction[A, B]的一个实例

    • A代表输入参数类型
    • B代表返回结果类型
  • 示例一

    定义一个偏函数,根据以下方式返回

输入返回值
1
2
3
其他其他
object Demo {
	def main(args: Array[String]): Unit = {
		// func1是一个输入参数为Int类型,返回值为String类型的偏函数
		val func1: PartialFunction[Int, String] = {
			case 1 => "一"
			case 2 => "二"
			case 3 => "三"
			case _ => "其他"
		}
		println(func1(2))
	}
}
  • 示例二
    • 定义一个列表,包含1-10的数字
    • 请将1-3的数字都转换为[1-3]
    • 请将4-8的数字都转换为[4-8]
    • 将其他的数字转换为(8-*]
    object Demo {
    	def main(args: Array[String]): Unit = {
    		val list = (1 to 10).toList
    		val list2 = list.map{
    		    case x if x >= 1 && x <= 3 => "[1-3]"
    		    case x if x >= 4 && x <= 8 => "[4-8]"
    		    case x if x > 8 => "(8-*]"
    		}
    		println(list2)
    	}
    }
    

六、正则表达式(了解)

在scala中,可以很方便地使用正则表达式来匹配数据。

Regex类

  • scala中提供了Regex类来定义正则表达式
  • 要构造一个RegEx对象,直接使用String类的r方法即可
  • 建议使用三个双引号来表示正则表达式,不然就得对正则中的反斜杠来进行转义

    val regEx = """正则表达式""".r

findAllMatchIn方法

  • 使用findAllMatchIn方法可以获取到所有正则匹配到的字符串
  • 示例一

    • 定义一个正则表达式,来匹配邮箱是否合法
    • 合法邮箱测试:qq12344@163.com
    • 不合法邮箱测试:qq12344@.com
    val r = """.+@.+\..+""".r
    
    val eml1 = "qq12344@163.com"
    val eml2 = "qq12344@.com"
    
    if(r.findAllMatchIn(eml1).size > 0) {
        println(eml1 + "邮箱合法")
    }
    else {
        println(eml1 + "邮箱不合法")
    }
    
    if(r.findAllMatchIn(eml2).size > 0) {
        println(eml2 + "邮箱合法")
    }
    else {
        println(eml2 + "邮箱不合法")
    }
    
  • 示例二

    找出以下列表中的所有不合法的邮箱
    “38123845@qq.com”, “a1da88123f@gmail.com”, “zhansan@163.com”, “123afadff.com”

    val emlList =
    List("38123845@qq.com", "a1da88123f@gmail.com", "zhansan@163.com", "123afadff.com")
    
    val regex = """.+@.+\..+""".r
    
    val invalidEmlList = emlList.filter {
        x =>
        if (regex.findAllMatchIn(x).size < 1) true else false
    }
    
    println(invalidEmlList)
    
  • 示例三

  • 有以下邮箱列表

    "38123845@qq.com", "a1da88123f@gmail.com", "zhansan@163.com", "123afadff.com"
    
  • 使用正则表达式进行模式匹配,匹配出来邮箱运营商的名字。例如:邮箱zhansan@163.com,需要将163匹配出来

    • 使用括号来匹配分组
  • 打印匹配到的邮箱以及运营商

    // 使用括号表示一个分组
    val regex = """.+@(.+)\..+""".r
    
    val emlList =
    List("38123845@qq.com", "a1da88123f@gmail.com", "zhansan@163.com", "123afadff.com")
    
    val emlCmpList = emlList.map {
        case x@regex(company) => s"${x} => ${company}"
        case x => x + "=>未知"
    }
    
    println(emlCmpList)
    

七、异常处理

在java中,面对异常处理,通常采用捕获异常(try…catch…)或者抛出异常(throw)。在scala中也同样使用这两种方法。

1. 捕获异常

  • 语法格式

    try {
    // 代码
    }
    catch {
    case ex:异常类型1 => // 代码
    case ex:异常类型2 => // 代码
    }
    finally {
    // 代码
    }

    • try中的代码是我们编写的业务处理代码
    • 在catch中表示当出现某个异常时,需要执行的代码
    • 在finally中,是不管是否出现异常都会执行的代码
  • 示例

    • 使用try…catch…来捕获除零异常
    try {
        val i = 10 / 0
        println("你好!")
    } catch {
        case ex: Exception => println(ex.getMessage)
    } 
    

2. 抛出异常

同样的,也可以在一个方法中抛出异常。语法格式和java类似,使用throw new Exception...

  • 示例

    • 在main方法中抛出一个异常
      def main(args: Array[String]): Unit = {
        throw new Exception("这是一个异常")
      }
    
    Exception in thread "main" java.lang.Exception: 这是一个异常
    	at ForDemo$.main(ForDemo.scala:3)
    	at ForDemo.main(ForDemo.scala)
    
  • scala不需要在方法上声明要抛出的异常,它已经解决了再java中被认为是设计失败的检查型异常。

下面是Java代码

public static void main(String[] args) throws Exception {
    throw new Exception("这是一个异常");
}

八、提取器(Extractor)(了解)

我们之前已经使用过scala中非常强大的模式匹配功能了,通过模式匹配,我们可以快速匹配样例类中的成员变量。例如:

// 1. 创建两个样例类
case class Person(name:String, age:Int)
case class Order(id:String)

def main(args: Array[String]): Unit = {
    // 2. 创建样例类对象,并赋值为Any类型
    val zhangsan:Any = Person("张三", 20)
    val order1:Any = Order("001")

    // 3. 使用match...case表达式来进行模式匹配
    // 获取样例类中成员变量
    order1 match {
        case Person(name, age) => println(s"姓名:${name} 年龄:${age}")
        case Order(id1) => println(s"ID为:${id1}")
        case _ => println("未匹配")
    }
}

那是不是所有的类都可以进行这样的模式匹配呢?答案是:

不可以的。要支持模式匹配,必须要实现一个提取器

[!NOTE]

样例类自动实现了apply、unapply方法

1. 定义提取器

之前我们学习过了,实现一个类的伴生对象中的apply方法,可以用类名来快速构建一个对象。伴生对象中,还有一个unapply方法。与apply相反,unapply是将该类的对象,拆解为一个个的元素。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dYwz9sP0-1573179725189)(assets/1552639637165.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HAw76kF8-1573179725190)(assets/1552639674932.png)]

要实现一个类的提取器,只需要在该类的伴生对象中实现一个unapply方法即可。

  • 语法格式

    def unapply(stu:Student):Option[(类型1, 类型2, 类型3...)] = {
    if(stu != null) {
    Some((变量1, 变量2, 变量3...))
    }
    else {
    None
    }
    }

  • 示例

    • 创建一个Student类,包含姓名年龄两个字段
    • 实现一个类的解构器,并使用match表达式进行模式匹配,提取类中的字段。
    class Student(var name:String, var age:Int)
    
    object Student {
        def apply(name:String, age:Int) = {
            new Student(name, age)
        }
    
        def unapply(student:Student) = {
            val tuple = (student.name, student.age)
    
            Some(tuple)
        }
    }
    
    def main(args: Array[String]): Unit = {
        val zhangsan = Student("张三", 20)
    
        zhangsan match {
            case Student(name, age) => println(s"${name} => ${age}")
        }
    }
    

九、泛型(了解)

scala和Java一样,类和特质、方法都可以支持泛型。我们在学习集合的时候,一般都会涉及到泛型。

scala> val list1:List[String] = List("1", "2", "3")
list1: List[String] = List(1, 2, 3)

那如何自己定义泛型呢?

1. 定义一个泛型方法

在scala中,使用方括号来定义类型参数。

  • 语法格式

def 方法名[泛型名称](..) = {
//...
}

  • 示例

    • 用一个方法来获取任意类型数组的中间的元素
    • 不考虑泛型直接实现(基于Array[Int]实现)
    • 加入泛型支持

    不考虑泛型的实现

      def getMiddle(arr:Array[Int]) = arr(arr.length / 2)
    
      def main(args: Array[String]): Unit = {
        val arr1 = Array(1,2,3,4,5)
    
        println(getMiddle(arr1))
      }
    

    加入泛型支持

    def getMiddleElement[T](array:Array[T]) =
    array(array.length / 2)
    
    def main(args: Array[String]): Unit = {
        println(getMiddleElement(Array(1, 2, 3, 4, 5)))
        println(getMiddleElement(Array("a", "b", "c", "d", "e")))
    }
    

2. 泛型类

scala的类也可以定义泛型。接下来,我们来学习如何定义scala的泛型类

  • 语法格式

    class 类[T](val 变量名: T)

  • 定义一个泛型类,直接在类名后面加上方括号,指定要使用的泛型参数
  • 指定类对应的泛型参数后,就使用这些类型参数来定义变量了
  • 示例
    • 实现一个Pair泛型类
    • Pair类包含两个字段,而且两个字段的类型不固定
    • 创建不同类型泛型类对象,并打印
    case class Pair[T](var a:T, var b:T)
    
    def main(args: Array[String]): Unit = {
        val pairList = List(
            Pair("Hadoop", "Storm"),
            Pair("Hadoop", 2008),
            Pair(1.0, 2.0),
            Pair("Hadoop", Some(1.9))
        )
    
        println(pairList)
    }
    

3. 上下界

需求:

我们在定义方法/类的泛型时,限定必须从哪个类继承、或者必须是哪个类的父类。此时,就需要使用到上下界。

3.1 上界定义

使用<: 类型名表示给类型添加一个上界,表示泛型参数必须要从该类(或本身)继承

  • 语法格式

    [T <: 类型]

  • 示例

    • 定义一个Person类
    • 定义一个Student类,继承Person类
    • 定义一个demo泛型方法,该方法接收一个Array参数,
    • 限定demo方法的Array元素类型只能是Person或者Person的子类
    • 测试调用demo,传入不同元素类型的Array
    class Person
    class Student extends Person
    
    def demo[T <: Person](a:Array[T]) = println(a)
    
    def main(args: Array[String]): Unit = {
        demo(Array(new Person))
        demo(Array(new Student))
        // 编译出错,必须是Person的子类
        // demo(Array("hadoop"))
    }
    

3.2 下界

上界是要求必须是某个类的子类,或者必须从某个类继承,而下界是必须是某个类的父类(或本身)

  • 语法格式

    [T >: 类型]

[!NOTE]

如果类既有上界、又有下界。下界写在前面,上界写在后面

  • 示例

    • 定义一个Person类
    • 定义一个Policeman类,继承Person类
    • 定义一个Superman类,继承Policeman类
    • 定义一个demo泛型方法,该方法接收一个Array参数,
    • 限定demo方法的Array元素类型只能是Person、Policeman
    • 测试调用demo,传入不同元素类型的Array
    class Person
    class Policeman extends Person
    class Superman extends Policeman
    
    def demo[T >: Policeman](array:Array[T]) = println(array)
    
    def main(args: Array[String]): Unit = {
        demo(Array(new Person))
        demo(Array(new Policeman))
        // 编译出错:Superman是Policeman的子类
        // demo(Array(new Superman))
    }
    

4. 协变、逆变、非变

spark的源代码中大量使用到了协变、逆变、非变,学习该知识点对我们将来阅读spark源代码很有帮助。

来看一个类型转换的问题:

class Pair[T]

object Pair {
  def main(args: Array[String]): Unit = {
    val p1 = Pair("hello")
    // 编译报错,无法将p1转换为p2
    val p2:Pair[AnyRef] = p1

    println(p2)
  }
}

如何让带有泛型的类支持类型转换呢?

4.1 非变

  • 语法格式

    class Pair[T]{}

  • 默认泛型类是非变的
  • 类型B是A的子类型,Pair[A]和Pair[B]没有任何从属关系
  • Java是一样的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FA0vunEi-1573179725190)(assets/1558064807949.png)]

4.2 协变

  • 语法格式

    class Pair[+T]

  • 类型B是A的子类型,Pair[B]可以认为是Pair[A]的子类型
  • 参数化类型的方向和类型的方向是一致的。

4.3 逆变

  • 语法格式

    class Pair[-T]

  • 类型B是A的子类型,Pair[A]反过来可以认为是Pair[B]的子类型
  • 参数化类型的方向和类型的方向是相反的
  • 示例

    • 定义一个Super类、以及一个Sub类继承自Super类
    • 使用协变、逆变、非变分别定义三个泛型类
    • 分别创建泛型类来演示协变、逆变、非变
    class Super
    class Sub extends Super
    
    class Temp1[T]
    class Temp2[+T]
    class Temp3[-T]
    
    def main(args: Array[String]): Unit = {
        val a:Temp1[Sub] = new Temp1[Sub]
        // 编译报错
        // 非变
        //val b:Temp1[Super] = a
    
        // 协变
        val c: Temp2[Sub] = new Temp2[Sub]
        val d: Temp2[Super] = c
    
        // 逆变
        val e: Temp3[Super] = new Temp3[Super]
        val f: Temp3[Sub] = e
    }
    

十、Actor

1. Actor介绍

scala的Actor并发编程模型可以用来开发比java线程效率更高的并发程序。

1.1 Java并发编程的问题

在java并发编程中,每个对象都有一个逻辑监视器(monitor),可以用来控制对象的多线程访问。我们添加sychronized关键字来标记,需要进行同步加锁访问。这样,通过加锁的机制来确保同一时间只有一个线程访问共享数据。但这种方式存在资源争夺、以及死锁问题,程序越大问题越麻烦。
在这里插入图片描述
线程死锁
在这里插入图片描述

1.2 Actor并发编程模型

Actor并发编程模型,是scala提供给程序员的一种与java并发编程完全不一样的并发编程模型,是一种基于事件模型的并发机制。Actor并发编程模型是一种不共享数据,依赖消息传递的一种并发编程模式,有效避免资源争夺、死锁等情况。
在这里插入图片描述

1.3 Java并发编程对比Actor并发编程

Java内置线程模型scala Actor模型
"共享数据-锁"模型 (share data and lock)share nothing
每个object有一个monitor,监视线程对共享数据的访问不共享数据,Actor之间通过Message通讯
加锁代码使用synchronized标识
死锁问题
每个线程内部是顺序执行的每个Actor内部是顺序执行的

tips:
scala在2.11.x版本中加入了Akka并发编程框架,老版本已经废弃。Actor的编程模型和Akka很像。

2. 创建Actor

创建Actor的方式和Java中创建线程很类似,也是通过继承来创建。

2.1 使用方式

  1. 定义class或object继承Actor特质
  2. 重写act方法
  3. 调用Actor的start方法执行Actor

tips:
类似于java线程,这里的每个Actor是并行执行的

  • 示例

    • 创建两个Actor,一个Actor打印1-10,另一个Actor打印11-20
      • 使用class继承Actor创建(如果需要在程序中创建多个相同的Actor)
      • 使用object继承Actor创建(如果在程序中只创建一个Actor)
    • 使用class继承Actor创建
    • 使用class继承Actor创建
    object Demo {
      class Actor1 extends Actor {
        override def act(): Unit = (1 to 10).foreach(println(_))
      }
    
      class Actor2 extends Actor {
        override def act(): Unit = (11 to 20).foreach(println(_))
      }
    
      def main(args: Array[String]): Unit = {
        new Actor1().start()
        new Actor2().start()
      }
    }
    
    • 使用object继承Actor创建
    object Demo {
      object Actor1 extends Actor {
        override def act(): Unit =
          for(i <- 1 to 10) {
            println(i)
          }
      }
    
      object Actor2 extends Actor {
        override def act(): Unit =
          for(i <- 11 to 20) {
            println(i)
          }
      }
    
      def main(args: Array[String]): Unit = {
        Actor1.start()
        Actor2.start()
      }
    

2.1 Actor程序运行流程

  1. 调用start()方法启动Actor
  2. 自动执行act()方法
  3. 向Actor发送消息
  4. act()方法执行完成后,程序会调用exit()方法

3. 发送消息/接收消息

3.1 使用方式

  • 发送消息

我们可以使用三种方式来发送消息:

发送异步消息,没有返回值
!?发送同步消息,等待返回值
!!发送异步消息,返回值是Future[Any]

接收消息

Actor中使用receive方法来接收消息,需要给receive方法传入一个偏函数

{
case 变量名1:消息类型1 => 业务处理1,
case 变量名2:消息类型2 => 业务处理2,
...
}

tips:
receive方法只接收一次消息,接收完后继续执行act方法

  • 示例
    • 创建两个Actor(ActorSender、ActorReceiver)
    • ActorSender发送一个异步字符串消息给ActorReceiver
    • ActorReceive接收到该消息后,打印出来
      在这里插入图片描述
    object Demo{
    	object ActorSender extends Actor {
    	 	override def act(): Unit = {
    		    // 发送消息
    		    while(true) {
    		      	ActorReceiver ! "hello!"
    		      	TimeUnit.SECONDS.sleep(3)
    		    }
    	  	}
    	}
    	
    	object ActorReceiver extends Actor {
    	  	override def act(): Unit = {
    		    // 持续接收消息
    		    while(true) {
    		      	receive {
    		        	case msg:String => println("接收到消息:" + msg)
    		      	}
    		    }
    	  	}
    	}
    	
    	def main(args: Array[String]): Unit = {
    	  	ActorReceiver.start()
    	  	ActorSender.start()
    	}
    }
    

4. 持续接收消息

通过上一个示例,ActorReceiver调用receive来接收消息,但接收一次后,Actor就退出了。

object Demo{
	object ActorSender extends Actor {
	  	override def act(): Unit = {
	    	// 发送字符串消息给Actor2
	    	val msg =  "你好,ActorSender"
	    	println(s"ActorSender: 发送消息$msg")
	    
	    	ActorReceiver ! msg
	    	// 再次发送一条消息,ActorReceiver无法接收到
	    	ActorReceiver ! "你叫什么名字?"
	 	}
	}

object ActorReceiver extends Actor {
  	override def act(): Unit =
    	receive {
      		case msg: String => println(s"接收Actor: 接收到$msg")
    	}
}

	object ActorMsgDemo {
	  	def main(args: Array[String]): Unit = {
	    	ActorSender.start()
	    	ActorReceiver.start()
	  	}
	}
}

上述代码,ActorReceiver无法接收到ActorSender发送的第二条消息。

我们希望ActorReceiver能够一直接收消息,怎么实现呢?

——我们只需要使用一个while(true)循环,不停地调用receive来接收消息。

  • 示例
  • 在上一个示例的基础上,让ActorReceiver能够一直接收消息

    object Demo{
        object ActorSender extends Actor {
            override def act(): Unit = {
                // 发送消息
                ActorReceiver ! "hello!"
                ActorReceiver ! "bye!"
                ActorReceiver ! "stop"
            }
        }
    
        object ActorReceiver extends Actor {
            override def act(): Unit = {
                // 持续接收消息
                while(true) {
                    receive {
                        case "stop" => System.exit(0)
                        case msg:String => println("接收到消息:" + msg)
                    }
                }
            }
        }
    
        def main(args: Array[String]): Unit = {
            ActorReceiver.start()
            ActorSender.start()
        }
    }
    

5. 使用loop和react优化接收消息

上述代码,使用while循环来不断接收消息。

  • 如果当前Actor没有接收到消息,线程就会处于阻塞状态
  • 如果有很多的Actor,就有可能会导致很多线程都是处于阻塞状态
  • 每次有新的消息来时,重新创建线程来处理
  • 频繁的线程创建、销毁和切换,会影响运行效率

在scala中,可以使用loop + react来复用线程。比while + receive更高效

  • 示例
    object Demo{
        object ActorSender extends Actor {
            override def act(): Unit = {
                // 发送消息
                ActorReceiver ! "hello!"
                ActorReceiver ! "bye!"
                ActorReceiver ! "stop"
            }
        }
    
        object ActorReceiver extends Actor {
            override def act(): Unit = {
                // 持续接收消息
                loop{
                    react{
                        case "stop" => System.exit(0)
                        case msg:String => println("接收到消息:" + msg)
                    }
                }
            }
        }
    
        def main(args: Array[String]): Unit = {
            ActorReceiver.start()
            ActorSender.start()
        }
    }
    

6. 发送和接收自定义消息

我们前面发送的消息是字符串类型,Actor中也支持发送自定义消息,常见的如:使用样例类封装消息,然后进行发送处理。

  • 示例一
    • 创建一个MsgActor,并向它发送一个同步消息,该消息包含两个字段(id、message)
    • MsgActor回复一个消息,该消息包含两个字段(message、name)
    • 打印回复消息

[!TIP]

  • 使用!?来发送同步消息
  • 在Actor的act方法中,可以使用sender获取发送者的Actor引用
  case class Message(id:Int, msg:String)
  case class ReplyMessage(msg:String, name:String)

  object MsgActor extends Actor {
    override def act(): Unit = {
      loop {
        react {
          case Message(id, msg) => {
            println(s"接收到消息:${id}/${msg}")
            sender ! ReplyMessage("不太好", "Tom")
          }
        }
      }
    }
  }

  def main(args: Array[String]): Unit = {
    MsgActor.start()

    val replyMessage: Any = MsgActor !? Message(1, "你好")
    println("回复消息:" + replyMessage.asInstanceOf[ReplyMessage])
  }
  • 示例二
  • 创建一个MsgActor,并向它发送一个异步无返回消息,该消息包含两个字段(message, company)

[!TIP]

使用!发送异步无返回消息

case class Mesasge(message:String, company:String)

object MsgActor extends Actor {
    override def act(): Unit = {
        loop {
            react {
                case Mesasge(message, company) =>
                println(s"MsgActor接收到消息:${message}/${company}")
            }
        }
    }
}

def main(args: Array[String]): Unit = {
    MsgActor.start()

    MsgActor ! Mesasge("中国联通", "大爷,快交话费!")
}
  • 示例三
    • 创建一个MsgActor,并向它发送一个异步有返回消息,该消息包含两个字段(id、message)
    • MsgActor回复一个消息,该消息包含两个字段(message、name)
    • 打印回复消息

[!TIP]

  • 使用!!发送异步有返回消息
  • 发送后,返回类型为Future[Any]的对象
  • Future表示异步返回数据的封装,虽获取到Future的返回值,但不一定有值,可能在将来某一时刻才会返回消息
  • Future的isSet()可检查是否已经收到返回消息,apply()方法可获取返回数据
case class Message(id:Int, message:String)
case class ReplyMessage(message:String, name:String)

object MsgActor extends Actor {
    override def act(): Unit = {
        loop {
            react {
                case Message(id, message) =>
                println(s"MsgActor接收到消息:${id}/${message}")
                sender ! ReplyMessage("收到消息!", "JIm")
            }
        }
    }
}

def main(args: Array[String]): Unit = {
    MsgActor.start()

    val future: Future[Any] = MsgActor !! Message(1, "你好!")

    while(!future.isSet) {}

    val replyMessage = future.apply().asInstanceOf[ReplyMessage]
    println(replyMessage)
}

7. WordCount案例

使用Actor并发编程模型实现多文件的单词统计。

  • 案例介绍

给定几个文本文件(文本文件都是以空格分隔的),使用Actor并发编程来统计单词的数量

  • 实现思路分析
    1. MainActor获取要进行单词统计的文件
    2. 根据文件数量创建对应的WordCountActor
    3. 将文件名封装为消息发送给WordCountActor
    4. WordCountActor接收消息,并统计单个文件的单词计数
    5. 将单词计数结果发送给MainActor
    6. MainActor等待所有的WordCountActor都已经成功返回消息,然后进行结果合并
      在这里插入图片描述

7.1 获取文件列表

  • 实现思路

在main方法中读取指定目录(${project_root_dir}/data/)下的所有文件,并打印所有的文件名

实现步骤

  1. 创建用于测试的数据文件
  2. 加载工程根目录,获取到所有文件
  3. 将每一个文件名,添加目录路径
  4. 打印所有文件名

参考代码

// 1. MainActor获取要进行单词统计的文件
val DIR_PATH = "./data/"
val dataDir = new File(DIR_PATH)

// 读取所有data目录下的所有文件
println("对以下文件进行单词统计:")
// 构建文件列表
val fileList = dataDir.list().toList.map(DIR_PATH + _)
println(fileList)

7.2 创建WordCountActor

实现思路

根据文件数量创建WordCountActor,为了方便后续发送消息给Actor,将每个Actor与文件名关联在一起

实现步骤

  1. 创建WordCountActor
  2. 将文件列表转换为WordCountActor
  3. 为了后续方便发送消息给Actor,将Actor列表和文件列表拉链到一起
  4. 打印测试

参考代码

MainActor.scala

// 2. 根据文件数量创建对应的WordCountActor
val actorList = fileList.map {
    x => new WordCountActor
}

// 将Actor和文件名列表建立为元组
val actorWithFileList: List[(WordCountActor, String)] = actorList.zip(fileList)

WordCountActor.scala

class WordCountActor extends Actor{
  override def act(): Unit = {
  }
}

7.3 启动Actor/发送/接收任务消息

实现思路

启动所有WordCountActor,并发送单词统计任务消息给每个WordCountActor

[!NOTE]

此处应发送异步有返回消息

实现步骤

  1. 创建一个WordCountTask样例类消息,封装要进行单词计数的文件名
  2. 启动所有WordCountTask,并发送异步有返回消息
  3. 获取到所有的WordCount中获取到的消息(封装到一个Future列表中)
  4. 在WordCountActor中接收并打印消息

参考代码

MainActor.scala

// 3. 将文件名封装为消息发送给WordCountActor,并获取到异步返回结果
val futureList = actorWithFileList.map {
    // tuple为Actor和文件名
    tuple =>
    // 启动actor
    tuple._1.start()
    // 发送任务消息
    tuple._1 !! WordCountTask(tuple._2)
}

MessagePackage.scala

/**
  * 单词统计任务消息
  * @param fileName 文件名
  */
case class WordCountTask(fileName:String)

WordCountActor.scala

loop {
    receive {
        // 接收单词统计任务消息
        case WordCountTask(fileName) => {
            println("接收到消息:" + fileName)
        }
    }
}

7.4 消息统计文件单词计数

实现思路

读取文件文本,并统计出来单词的数量。例如:

(hadoop, 3), (spark, 1)...

实现步骤

  1. 读取文件内容,并转换为列表
  2. 按照空格切割文本,并转换为一个一个的单词
  3. 为了方便进行计数,将单词转换为元组
  4. 按照单词进行分组,然后再进行聚合统计
  5. 打印聚合统计结果

参考代码

WordCountActor.scala

// 4. 统计单个文件的单词计数
val iter: Iterator[String] = Source.fromFile(fileName).getLines()
// [第一行] hadoop hadoop
// [第二行] hadoop spark
val lineList = iter.toList
// [单词列表] hadoop, hadoop, hadoop, spark
val wordList: List[String] = lineList.flatMap(_.split(" "))
// 将单词转换为元组
// [元组列表] (hadoop, 1), (hadoop, 1), (hadoop, 1), (spark, 1)
val tupleList = wordList.map(_ -> 1)
// 按照单词进行分组
// [单词分组] = {hadoop->List(hadoop->1, hadoop->1, hadoop->1), spark->List(spark ->1)}
val grouped: Map[String, List[(String, Int)]] = tupleList.groupBy(_._1)
// 将分组内的数据进行聚合
// [单词计数] = (hadoop, 3), (spark, 1)
val wordCount: Map[String, Int] = grouped.map {
    tuple =>
    // 单词
    val word = tuple._1
    // 进行计数
    // 获取到所有的单词数量,然后进行累加
    val total = tuple._2.map(_._2).sum
    word -> total
}
println(wordCount)

7.5 封装单词计数结果回复给MainActor

实现思路

  • 将单词计数的结果封装为一个样例类消息,并发送给MainActor
  • MainActor等待所有WordCount均已返回后获取到每个WordCountActor单词计算后的结果

实现步骤

  1. 定义一个样例类封装单词计数结果
  2. 将单词计数结果发送给MainActor
  3. MainActor中检测所有WordActor是否均已返回,如果均已返回,则获取并转换结果
  4. 打印结果

参考代码

MessagePackage.scala

/**
  * 单词统计结果
  * @param wordCount 单词计数
  */
case class WordCountResult(wordCount: Map[String, Int])

WordCountActor.scala

// 5. 将单词计数结果回复给MainActor
sender ! WordCountResult(wordCount)

MainActor.scala

// 等待所有Actor都已经返回
while(futureList.filter(_.isSet).size != fileList.size){}
// MainActor等待所有的WordCountActor都已经成功返回消息,然后进行结果合并
val resultList: List[Map[String, Int]] = futureList.map(_.apply.asInstanceOf[WordCountResult].wordCount)
println("接收到所有统计结果:" + resultList)

7.6 结果合并

实现思路

对接收到的所有单词计数进行合并。因为该部分已经在WordCountActor已经编写过,所以抽取这部分一样的代码到一个工具类中,再调用合并得到最终结果

实现步骤

  1. 创建一个用于单词合并的工具类
  2. 抽取重复代码为一个方法
  3. 在MainActor调用该合并方法,计算得到最终结果,并打印

参考代码

WordCountUtil.scala

  /**
    * 单词分组统计
    * @param wordCountList 单词计数列表
    * @return 分组聚合结果
    */
  def reduce(wordCountList:List[(String, Int)]) = {
    // 按照单词进行分组
    // [单词分组] = {hadoop->List(hadoop->1, hadoop->1, hadoop->1), spark->List(spark ->1)}
    val grouped: Map[String, List[(String, Int)]] = wordCountList.groupBy(_._1)
    // 将分组内的数据进行聚合
    // [单词计数] = (hadoop, 3), (spark, 1)
    val wordCount: Map[String, Int] = grouped.map {
      tuple =>
        // 单词
        val word = tuple._1
        // 进行计数
        // 获取到所有的单词数量,然后进行累加
        val total = tuple._2.map(_._2).sum
        word -> total
    }
    wordCount
  }

MainActor.scala

// 扁平化后再聚合计算
val result: Map[String, Int] = WordCountUtil.reduce(resultList.flatten)

println("最终结果:" + result)

7.7 完整版本

//创建样例类
case class SubmitTask(filePath: String)

//定义一个样例类,用于封装返回结果
case class ReplyMessage(result: Map[String, Int])

class Task extends Actor {
  override def act(): Unit = {
    loop {
      react {
        case SubmitTask(filePath) => {
          //使用scala Source进行文件的读取 把结束变成String
          val context: String = Source.fromFile(filePath).mkString
          //根据文件的换行进行切割
          val lines: Array[String] = context.split("\r\n")
          //根据单词间的分隔符进行切割,并且进行压平操作
          val words = lines.flatMap(_.split(" "))
          //把每个单词都标记成1-->(单词,1)
          val kv1 = words.map((_, 1))
          //根据kv1 元素(元组)中的第一个元素(单词)进行分组
          val wordGroup = kv1.groupBy(_._1)

          //todo 需要一个方法可以作用于map中value进行操作,不影响keys
          //todo mapvalue可以把函数作用于map中value,返回的结果和原来的key组成一个新的map
          val result = wordGroup.mapValues((_.length))
          //          println(result)
          //把处理的返回结果
          sender ! ReplyMessage(result)
        }
      }
    }
  }
}

object WordCount {
  def main(args: Array[String]): Unit = {
    //定义一个集合,用于保存返回future状态
    val futureSet = new mutable.HashSet[Future[Any]]()
    //定义一个集合,用于保存最终的result结果
    val resultList = new ListBuffer[ReplyMessage]

    //待处理文件的路径
    //val filePath = "C:\\Users\\59162\\Desktop\\1.txt"

    //待处理文件列表
    val files = Array("C:\\Users\\59162\\Desktop\\1.txt", "C:\\Users\\59162\\Desktop\\2.txt", "C:\\Users\\59162\\Desktop\\3.txt")

    //遍历文件列表,分别启动actor进行wc
    for (f <- files) {
      //创建actor的实例
      val task = new Task
      //启动actor
      task.start()
      //把待处理的文件传递给actor 异步需要返回值 todo 返回的是一个状态结果 future
      val future = task !! SubmitTask(f)
      //把返回的future保存至futureSet中
      futureSet += future
    }
    //遍历futureSet集合,找出真正处理完的,有最终的结果,把结果提取出来,进行最终的合并
    while (futureSet.size > 0) {
      //过滤出已经处理完的future
      val completeFuture = futureSet.filter(_.isSet)
      //遍历已经完成future提取结果
      for (c <- completeFuture) {
        val result = c.apply().asInstanceOf[ReplyMessage]
          resultList += result
          //把已经提取完结果的future从futureSet中剔除
          futureSet.remove(c)
      }
    }
      println(resultList.map(_.result).flatten.groupBy(_._1).mapValues(_.foldLeft(0)(_+_._2)))
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值