scala 系列
前言
上一篇博客已经给大家介绍了 scala 函数和方法, 有了 scala 函数和方法的基础再去学习 OOP,会变得无往而不利。
本篇博客将为大家带来 scala OOP的介绍。scala OOP 和 java OOP有很多相似之处,因此以下会经常拿 java OOP 作为对比,学习 scala OOP。
思维导图
类和对象
之前说过 scala 是面向函数式编程的语言,除此之外,scala 和 java 一样,是支持面向对象的编程语言,也有类和对象的概念。
创建类和对象
类通过class
关键字定义,类通过new
关键字创建实例对象
创建一个Person类,并创建它的对象如下:
class OOP01 {
// 创建类
class Person{}
def main(args: Array[String]): Unit = {
// 创建对象
val p = new Person()
}
}
注意:
- 如果类是空的,没有任何成员,可以省略
{}
- 如果构造器的参数为空,可以省略
()
上述代码可以改造如下:
object OOP01 {
// 创建类,省略{}
class Person
def main(args: Array[String]): Unit = {
// 创建对象,省略()
val p = new Person
}
}
类拥有成员变量和方法
像 java 一样,类可以有自己的属性和行为,scala中可以通过定义和声明变量来定义类的属性,定义成员方法来定义类的行为。
- 在类中使用 var/val 来定义成员变量
- 对象直接使用成员变量名称来访问成员变量
- 使用def来定义成员方法
例如:
object OOP01 {
// 创建类
class Person{
// 定义成员属性
var name = ""
// 使用下划线进行初始化成员属性
var age:Int = _
// 定义成员方法
def show(): Unit ={
println(s"我叫${name},今年${age}岁")
}
}
def main(args: Array[String]): Unit = {
// 创建对象
val p = new Person
p.name = "hubert"
p.age = 22
// 获取变量值
println(s"${p.name} ${p.age}")
// 调用成员方法
p.show()
}
}
注意:
- 定义成员属性时必须初始化属性
- 使用 var +下划线 初始化成员变量时,必须显示的指定成员变量的类型,下划线代表成员变量类型默认值
val
类型的成员变量,必须要自己手动初始化- 成员属性可以定义在构造器中(下面会说)
- 类中无法定义静态成员变量和静态方法,静态变量和静态方法定义在object 单例对象中
访问修饰符
和Java一样,scala也可以通过访问修饰符,来控制成员变量和成员方法是否可以被访问。
java 访问修饰符
Modifier | Class | Package | Subclass | World |
---|---|---|---|---|
public | Y | Y | Y | Y |
protected | Y | Y | Y | N |
default | Y | Y | N | N |
private | Y | N | N | N |
scala 访问修饰符
Modifier | Class | Companion | Subclass | Package | World |
---|---|---|---|---|---|
default | Y | Y | Y | Y | Y |
protected | Y | Y | Y | N | N |
private | Y | Y | N | N | N |
Java中的访问控制,同样适用于scala,可以在成员前面添加 private/protected 关键字来控制成员的可见性。但在scala中,没有public关键字,任何没有被标为 private 或 protected 的成员都是公共的。
构造器
scala 中的构造器分为主构造器和辅助构造器,当创建对象的时候,会自动调用类的主构造器。而 java 中直接是构造方法的重载操作。
主构造器
类无需明确定义构造方法,通过主构造器的构造参数列表声明为类的一部分。
语法:
class 类名(var/val 参数名:类型 = 默认值, ..., var/val 参数名:类型 = 默认值){
构造代码块
}
注意:
- 主构造器的参数列表是直接定义在类名后面,添加了 val/var 表示直接通过主构造器定义成员变量
- 构造器参数列表可以指定默认值
- 创建实例,调用构造器可以指定字段进行初始化
- 整个class中除了字段定义和方法定义的代码都是构造代码
辅助构造器
在scala中,除了定义主构造器外,还可以根据需要来定义辅助构造器。例如:允许通过多种方式,来创建对象,这时候就可以定义其他更多的构造器。我们把除了主构造器之外的构造器称为辅助构造器。
语法:
def this(参数名:类型, ..., 参数名:类型){
//第一行必须调用主构造器或者其他构造器
//构造器代码
}
注意:
- 定义辅助构造器与定义方法一样,也使用 def 关键字来定义
- 这个方法的名字为 this
- 辅助构造器的第一行代码,必须要调用主构造器或者其他辅助构造器
之前Person类的构造器使用如下:
object OOP01 {
// 创建类 主构造器
class Person(var name:String,var age:Int){
// 辅助构造器
def this(arr: Array[Any]){
this(arr(0).asInstanceOf[String],arr(1).asInstanceOf[Int])
println("辅助构造器")
}
// 定义成员方法
def show(): Unit ={
println(s"我叫${name},今年${age}岁")
}
}
def main(args: Array[String]): Unit = {
// 创建对象 主构造器
val p = new Person("hubert",22)
// 创建对象 主构造器
val p1 = new Person(Array("hubert",22))
// 调用成员方法
p.show()
p1.show()
}
}
// 结果
辅助构造器
我叫hubert,今年22岁
我叫hubert,今年22岁
继承
定义
- scala和Java一样,使用“extends”关键字实现继承。
- 可以在子类中定义父类中没有的字段和方法,或者重写父类的方法,子类重写父类方法必须使用“override”关键字
- 类和单例对象(后面会讲)都可以从某个父类继承
例如:
object OOP01 {
class Person{
var name:String = _
var age:Int = _
def show(): Unit ={
println(s"我叫${name},今年${age}岁")
}
}
// 类继承
class Student extends Person
// 单例对象继承
object Student extends Person
def main(args: Array[String]): Unit = {
val s = new Student
s.name = "hubert"
s.age = 22
s.show() // 我叫hubert,今年22岁
// 单例对象优先加载,类似java 静态变量,故而输出null
println(Student.name) // null
}
}
override 和 super
类似于Java语言,我们在子类中使用override需要来重写父类的成员,可以使用super来引用父类。
注意:
- 子类要覆盖父类中的一个方法,必须要使用override关键字
- 使用override来重写一个val字段
- 使用super关键字来访问父类的成员方法
例如:
object OOP01 {
class Person{
val name:String = "jack"
def getName = name
}
class Student extends Person{
override val name = "hubert"
override def getName = {
super.getName + name
}
}
def main(args: Array[String]): Unit = {
val s = new Student
println(s.getName)
}
}
类型判断
isInstanceOf 和 asInstanceOf
在Java中,我们可以使用 instanceof 关键字来判断类型、以及 (类型)object 来进行类型转换,在 scala 中如何实现呢?scala 中对象提供 isInstanceOf 和 asInstanceOf 方法。
语法如下:
// isInstanceOf 判断对象是否为指定类的对象
val trueOrfalse:Boolean = 对象.isInstanceOf[类型]
// asInstanceOf 将对象转换为指定类型
val 变量 = 对象.asInstanceOf[类型]
例如:
def main(args: Array[String]): Unit = {
val a = "hubert"
val b = 22
println(a.isInstanceOf[Int])
println(b.isInstanceOf[String]) //报错 a value of type Int cannot also be a String (the underlying of String)
}
isInstanceOf 并不是所有的类型都可以使用,anyVal 不能 isInstanceOf【anyRef】中,否则会报错。
getClass 和 classOf
isInstanceOf 只能判断对象是否为指定类以及其子类的对象,而不能精确的判断出,对象就是指定类的对象。如果要求精确地判断出对象就是指定类的对象,那么就只能使用 getClass 和 classOf。
注意:
对象.getClass
可以精确获取对象的类型classOf[类名]
可以精确获取类型- 使用
==
操作符可以直接比较类型
例如:
object OOP01 {
class Person
class Student extends Person
def main(args: Array[String]): Unit = {
val s:Person = new Student
println(s.isInstanceOf[Person]) // ture
println(s.getClass == classOf[Person]) // false
println(s.getClass == classOf[Student]) // ture
}
}
单例对象
Scala 的类中无法定义静态成员,即无 static 关键字。如何像 Java 一样表达类的静态成员变量、成员方法与静态代码块?scala 采用了单例对象——object 来解决这一问题。
Scala 单例对象的特点:
- 使用“object”关键字声明,可包含静态变量、静态方法与静态代码块定义
- 单例对象中的成员变量、成员方法通过单例对象名直接调用
- 单例对象第一次被访问时初始化,并执行全部代码块,java 静态是本地的,虚拟机直接装载 class 信息的
- 单例对象不能new,且无构造参数
- 程序入口main()方法必须定义在单例对象中
- 单例对象与同名类定义在同一文件中时形成绑定关系,即伴生对象(下面会讲)
例如:
object Person {
// 静态变量
val address:String = "江苏省南京市"
// 静态方法
def show(): Unit ={
println(s"我的地址是${address}")
}
def main(args: Array[String]): Unit = {
println(Person.address)
Person.show()
}
}
在 object 中定义的成员变量类似于 Java 的静态变量,在 object 中定义的成员方法类似于 Java 的静态方法,单例对象表示全局仅有一个对象,类似于 java 的 static 概念,事实上 scala 把 java 的 class 拆成了 class 和 object 两个而已。
scala和Java一样,如果要运行一个程序,必须有一个main方法。而在Java中main方法是静态的,而在scala中没有静态方法。在scala中,这个main方法必须放在一个单例对象中。此外也可以通过 实现App Trait 来定义入口,具体如下:
// 实现App Trait 来定义 main 入口
object Person extends App {
// 静态变量
val address:String = "江苏省南京市"
// 静态方法
def show(): Unit ={
println(s"我的地址是${address}")
}
println(Person.address)
Person.show()
}
伴生对象
在 Java 中,经常会有一些类,同时有实例成员又有静态成员; scala 中要实现类似的效果,可以使用伴生对象来实现。
定义伴生对象
单例对象与同名类定义在同一文件中时形成绑定关系,就是伴生对象。伴生对象有以下特点:
- 同名类称为单例对象的伴生类(class)
- 单例对象称为同名类伴生对象(object)
- 伴生类与伴生对象可相互访问各自private成员
- 伴生对象可为伴生类增加静态成员
private [this] 访问权限
当然某个成员的权限设置为private[this]
,表示只能在当前类中访问,伴生对象也不可以访问。
下述代码会编译报错:
class Person(private[this]var name:String)
object Person{
def show(person:Person): Unit ={
println(person.name)
}
def main(args: Array[String]): Unit = {
val p = new Person("hubert")
Person.show(p)
}
}
apply
当scala中类或者对象有一个主要用途的时候,apply方法就是一个很好地语法糖。单例对象用于持有一个类的唯一实例。通常用于工厂模式。
使用伴生对象的apply()方法省掉new关键字。例如:Person.apply()等价于Person()
语法定义:
object A{
def apply(field:fieldType, ... ,field:fieldType): A = new A(field, ... , field)
}
// 调用
A(field, ... , field)
unapply
伴生对象中,还有一个unapply方法。与apply相反,unapply 是将该类的对象,拆解为一个个的元素。
例如:
class Ba(name:String,age:Int) {
var username = name
var userage = age
}
object Ba{
// 构造 属性 =》 对象
def apply(name: String, age: Int): Ba = new Ba(name, age)
// 解构 对象=》 Option
def unapply(arg: Ba): Option[(String, Int)] = {
if (null ==arg){
None
}else{
Some(arg.username,arg.userage)
}
}
def main(args: Array[String]): Unit = {
var b = Ba("ZS",20)
// 样例类的模式匹配
b match {
case Ba(name,age) => println(name,age)
case _=> println("未匹配到")
}
}
}
scala 的 object 和 class 有什么差异?
- 在 scala 中没有静态方法和静态字段,所以在 scala 中可以用 object 来实现这些功能,直接用对象名调用的方法都是采用这种实现方式,例如Array.toString。对象的构造器在第一次使用的时候会被调用,如果一个对象从未被使用,那么他的构造器也不会被执行;对象本质上拥有类(scala中)的所有特性,除此之外,object 还可以一扩展类以及一个或者多个特质
- object 不能提供构造器参数
- 所有的 main 方法都必须在 object 中被调用,来提供程序的主入口
- 类和伴生对象可以相互访问他们的私有属性,但是他们必须在同一个源文件内。
抽象类
和 java 一样,scala 也可以定义抽象类,scala 和 java 抽象几乎一致。抽象类的特点如下:
- 抽象类可包含未实现的方法,即抽象方法;可以包含未初始化的变量,即抽象属性
- 抽象类无法实例化
- 抽象类使用“abstract”关键字修饰
- 子类重写父类抽象方法时,“override”关键字可选,子类重写父类非抽象方法,“override”关键字必写
例如:
abstract class Person{
// 抽象属性
val address:String
def show(): Unit ={
println(s"我的地址是${address}")
}
// 抽象方法
def learn():Unit
}
class Student extends Person{
// 重写抽象属性
override val address: String = "江苏省南京市"
// 重写抽象方法,override 可选
def learn(): Unit = {
println("好好学习")
}
override def show(): Unit = {
println(s"我的地址是${address}")
}
}
匿名内部类
一个类可以作为另一个类的成员,称为内部类。Java 内部类是外部类的成员,而 Scala 内部类是绑定到外部类的对象实例。
而内部类最常用的就是匿名内部类。 匿名内部类是没有名称的子类,直接用来创建实例对象。Spark 的源代码中有大量使用到匿名内部类。 scala中的匿名内部类使用与Java一致。
例如:
abstract class Person{
def show():Unit
}
object PersonTest{
def main(args: Array[String]): Unit = {
// 匿名内部类的创建
val p = new Person {
override def show(): Unit = println("我是人")
}
p.show()
}
}
样例类
样例类是一种特殊的类,它可以用来快速定义一个用于保存数据的类(类似于Java POJO类),在后续的学习并发编程和spark,flink这些框架也都会经常使用它。样例类常用于描述不可变的值对象(Value Object)。
样例类定义
语法:case class 样例类名([val/var] 成员变量名1:类型1 , ... , [val/var] 成员变量名n:类型n )
例如:
//定义样例类
case class Teacher(name:String,age:Int)
object TeacherText{
def main(args: Array[String]): Unit = {
//创建样例类的实例,无需new关键字
val tea = Teacher("Jason",19)
//访问对象属性
println(tea.name)
}
}
样例类特点:
- 样例类构造参数默认声明为“val”,自动实现类构造参数的getter
- 样例类构造参数声明为“var”时,自动实现类构造参数的setter和getter
- 样例类自动创建伴生对象
- 样例类自动实现的其他方法:toString()、equals()、copy()、hashCode()和伴生对象中的apply()、unapply()
样例类方法
样例类自动实现的其他方法包括 toString()、equals()、copy()、hashCode()和伴生对象中的apply()、unapply()
apply 和 unapply 方法
apply 方法可以让我们快速地使用类名来创建对象。unapply 方法是将该类的对象,拆解为一个个的元素。之前已经说过,这里不再细说
toString 方法
toString返回样例类名称(成员变量1,成员变量2,成员变量3…)
,我们可以更方便查看样例类的成员。
case class Teacher(name:String,age:Int)
object TeacherText{
def main(args: Array[String]): Unit = {
val tea = Teacher("Jason",19)
println(tea.toString)
// 输出:Teacher(Jason,19)
}
}
equals 方法
样例类自动实现了equals方法,可以直接使用==
比较两个样例类是否相等,即所有的成员变量是否相等。
case class Teacher(name:String,age:Int)
object TeacherText{
def main(args: Array[String]): Unit = {
val tea1 = Teacher("Jason",19)
val tea2 = Teacher("Jason",19)
println(tea1.equals(tea2))
// 输出:true
}
}
hashCode 方法
样例类自动实现了hashCode方法,如果所有的成员变量的值相同,则 hash 值相同,只要有一个不一样,则hash值不一样。
case class Teacher(name:String,age:Int)
object TeacherText{
def main(args: Array[String]): Unit = {
val tea1 = Teacher("Jason",19)
val tea2 = Teacher("Jason",19)
println(tea1.hashCode()) // 输出:1835026438
println(tea2.hashCode()) // 输出:1835026438
}
}
copy方法
样例类实现了copy方法,可以快速创建一个相同的实例对象,可以使用带名参数指定给成员进行重新赋值。
case class Teacher(name:String,age:Int)
object TeacherText{
def main(args: Array[String]): Unit = {
val tea1 = Teacher("Jason",19)
val tea2 = tea1.copy("hubert")
println(tea2.toString) // 输出: Teacher(hubert,19)
}
}
样例类对象
使用case object 可以创建样例对象。样例对象是单例的,而且它没有构造器。
它主要用在两个地方:定义枚举和作为没有任何参数的消息传递。
object Weekday extends Enumeration {
//枚举值从0开始计数
val Mon,Tue,Wed,Thu,Fri,Sat,Sun=Value
}
//枚举的使用
Weekday.Sun
Weekday.Sun.id //获取枚举值的计数值
Weekday.values.foreach(println)
样例类与枚举区别
- 枚举更简单,代码更少
- 样例类的字段比枚举的值更强大
- 样例类可扩展
样例类与普通类的区别
- 样例类通常用于描述不可变的数据,数据完全依赖构造参数
- 样例类默认不可变,通过模式匹配可分解
- 两个样例类“==”操作时,通过按值比较而不是按引用
- 样例类操作更简单
- 如果一个对象在内部执行有状态计算,或者表现出其他类型的复杂行为,那么它应该是一个普通类
特质
scala 中没有Java 中的接口(interface),替代的概念是——特质。特质可以作为接口使用,可以定义具体的属性和方法;常用于模板模式、动态混入 trait和调用链模式。
作为接口使用
trait 作为接口使用,与 java 的接口使用方法一样。
abstract class A {
def eat():Unit
println("A的构造器")
}
class B extends A{
println("B的构造器")
override def eat(): Unit = {
println("B吃饭")
}
}
trait C extends A{
println("C的构造器")
def sleep() = {
println("C睡觉")
}
override def eat(): Unit = {
println("C吃饭")
}
}
trait E extends F{
println("E的构造器")
}
trait F{
println("F的构造器")
}
class D extends B with C with E{
println("D的构造器")
}
object K {
def main(args: Array[String]): Unit = {
val d = new D()
d.sleep()
d.eat()
/**
* 输出:
* A的构造器
* B的构造器
* C的构造器
* F的构造器
* E的构造器
* D的构造器
* C睡觉
* C吃饭
*/
}
}
从上面可以看出,如果一个类实现了多个 trait ,那这些 trait 是如何构造的呢?
一个类继承另一个类、以及多个trait,当创建该类的实例时,它的构造顺序如下:
- 执行父类的构造器
- 从左到右依次执行trait的构造器
- 如果 trait 有父 trait ,先构造父 trait ,如果多个 trait 有同样的父 trait,则只初始化一次
- 执行子类构造器
定义具体的属性和方法
和类一样,trait 还可以定义具体的方法和属性,继承 trait 的子类自动拥有 trait 中定义的属性和方法。
例如:
trait Logger{
// 具体属性
val sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
// 抽象属性
val types:String
// 抽象方法
def log(msg:String)
}
class ConsoleLogger extends Logger{
override val types: String = "控制台打印"
override def log(msg: String): Unit = println(s"${types}:${sdf.format(new Date())}:${msg}")
}
object LoggerTest{
def main(args: Array[String]): Unit = {
val logger = new ConsoleLogger
logger.log("我爱scala")
}
}
模板模式
在一个特质中,具体方法依赖于抽象方法,而抽象方法可以放到继承tarit的子类中实现,这种设计方法也称为模板模式。
例如:
trait Logger{
def log(msg:String)
//具体方法定义,调用抽象方法
def info(msg:String) = log("提示:"+msg)
def warn(msg:String) = log("警告:"+msg)
def error(msg:String) = log("错误:"+msg)
}
class ConsoleLogger extends Logger{
override def log(msg: String): Unit = println(msg)
}
object LoggerTest{
def main(args: Array[String]): Unit = {
val logger = new ConsoleLogger
//创建类对象,调用不同级别的日志进行输出
logger.info("我爱scala")
logger.warn("val i =2.3")
logger.error("val a:String = 1")
}
}
java 中的log4j
就是这样实现的。
动态混入 trait
当某个特质被用于组合类时,被称为混入,一个类只能有一个父类但是可以有多个混入(分别使用关键字extends和with)。
scala 中可以将 trait 混入到对象中,就是将 trait 中定义的方法、字段添加到一个对象中。
例如:
class MyTrait {
self:Eat=>
def show() ={
myeat()
}
}
trait Eat{
def myeat():Unit
}
trait Person extends Eat{
override def myeat(): Unit = {
println("人吃饭")
}
}
trait Pig extends Eat{
override def myeat(): Unit = {
println("猪吃饭")
}
}
object MyTrait{
def main(args: Array[String]): Unit = {
(new MyTrait() with Person).myeat()
}
}
self:Type=> 自身类型,表示该类实例化时必须混入相应特质或子特质,self是this的别名。
此外,需要注意的是动态混入,不能和伴生共用。
trait 实现调用链模式
实现一个模拟支付过程的调用链如下:
// 定义 HandlerTrait
trait HandlerTrait{
def handler(data:String)={
println("处理支付数据...")
}
}
// 定义数据校验
trait DataValidateTrait extends HandlerTrait{
override def handler(data: String): Unit = {
println("数据校验...")
super.handler(data)
}
}
// 定义签名校验
trait SignatureValidateTrait extends HandlerTrait{
override def handler(data: String): Unit = {
println("签名校验")
super.handler(data)
}
}
// 定义支付服务
class PayService extends DataValidateTrait with SignatureValidateTrait{
override def handler(data: String): Unit = {
println("准备支付")
super.handler(data)
}
}
//创建支付对象,调用支付方法
object PayText{
def main(args: Array[String]): Unit = {
val service = new PayService
service.handler("支付数据")
}
}
结果:
准备支付
签名校验
数据校验...
处理支付数据...
当类继承了多个 trait 后,可以依次调用多个 trait 中的同一个方法,只要让多个trait中的同一个方法在最后都依次执行super关键字即可。类中调用多个tait中都有这个方法时,首先会从最右边的trait方法开始执行,然后依次往左执行,形成一个调用链条。
scala 的Trait 和 class 的区别
特质是 scala 中代码复用的基础单元,它可以将方法和字段定义封装起来,然后添加到类中,与类继承不一样的是,类继承要求每个类都只能继承一个超类,而一个类可以添加任意数量的特质。
使用 extends 来继承 trait(scala 不论是类还是特质,都是使用 extends 关键字);如果要继承多个 trait,则使用 with 关键字。
scala 的Trait 和 java 的 interface 的区别
- 接口不会有构造器,特质可以有构造器,并且在实现类继承特质的时候,先要调用特质的构造器。
- Scala 的特质能够提供具体方法的实现,而 java 的接口只有方法的定义,这一点很像 java 的抽象类,1.8之后有 default 方法和静态方法
- 接口中不能有未初始化的属性,且属性的修饰都是public static final, 特质中可以有未初始化的属性变量,即抽象字段;未初始化的属性可以有提前定义的特性
- Scala 的特质能在对象生成时临时加入,java 则没有这个特质,scala 中为实例提供 混入 trait (只有某个对象有 trait 的性质,其他对象没有),Java 中没有
- 类实现 implements 多个接口,且用逗号“,”隔开,类继承 extends 多个 trait 且用 with 隔开。
- Java 中接口不能继承普通类,但是 scala 中特质可以extends class,并且该类成为所有继承 trait 的类的父类
特质与抽象类的选择
- 优先使用特质:抽象类只能继承一次,特质可混入多个
- 需要使用带参构造方法时,使用抽象类
- 与Java互操作性:抽象类与Java完全可互操作,特质只有在不包含任何实现代码时才可互操作。
泛型类
scala 和Java 一样,类和特质、方法和参数类型都可以支持泛型。泛型类指可以接受类型参数的类,泛型类在集合类中被广泛使用,与Java不同,定义泛型类使用“[]”。
泛型种类
泛型参数
当不确定存放元素的类型,或者说确定存放元素的具体类型时,均可采用泛型去约束集合存放元素的类型,泛型参数常用于指定集合存放的元素类型泛型,例如:
scala> val list:List[String] = List("hubert","rose","jack")
list: List[String] = List(hubert, rose, jack)
泛型方法
当不确定一个方法的具体的传入形参类型,但确定出入形参类型在一个范围内(父类)时,常常通过泛型方法指定的不同类型来控制形参具体限制的类型。
语法如下:
def 方法名[泛型名称]():{
...
}
例如:
object Test {
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("1","2","3","4","5")))
}
}
泛型类
scala的类也可以定义泛型。
语法:class 类名[泛型名称] (...){...}
例如:
class AAA[T]{
println("kkk")
}
类型边界
在Scala中,我们在定义方法/类的泛型时,限定必须从哪个类继承、或者必须是哪个类的父类。就是说类型参数可以有一个类型边界约束,又称上下界。具体如下:
- 类型上界:将类型限制为另一种类型的子类
- T<:A 表示类型变量T应该是类型A的子类
- A是具体类型,T是泛型
- 类型下界:将类型声明为另一种类型的超类
- T>:A 表示类型变量T应该是类型A的超类
- A是具体类型,T是泛型
- 如果类既有上界、又有下界。下界写在前面,上界写在后面
例如:
class MyTt {
def abc[T<:Father](t:T)={
val list:ListBuffer[String] = ListBuffer.empty[String]
list.append()
}
def aaa[T>:Father](t:T)={
val list:ListBuffer[String] = ListBuffer.empty[String]
list.append()
}
}
object A{
def main(args: Array[String]): Unit = {
new MyTt().abc(new Father())
new MyTt().abc(new Son())
new MyTt().aaa(new String())
}
}
class Father {
println("father")
}
class Son extends Father{
println("son")
}
结果:
father
father
son
型变
spark的源代码中大量使用到了协变、逆变、非变,学习该知识点对我们将来阅读spark源代码很有帮助。
协变
class Foo[+T] // 协变类
对于两种类型 A 和 B,如果 A 是 B 的子类型,那么 Foo[A] 就是 Foo[B] 的子类型
逆变
class Bar[-T] // 逆变类
对于两种类型 A 和 B,如果 A 是 B 的子类型,那么 Bar[B] 就是 Bar[A] 的子类型
不变
class Baz[T] // 不变类
默认情况下,Scala中的泛型类是不变的
用一张图表示如下:
代码如下:
class Super
class Sub extends Super
class Temp1[T]
class Temp2[+T]
class Temp3[-T]
object Test {
val a:Temp1[Sub] = new Temp1[Sub]
// 编译报错
// 非变 val b:Temp1[Super] = new Temp1[Sub]
// 协变
val c:Temp2[Sub] = new Temp2[Sub]
val d:Temp2[Super] = c
// 逆变
val e:Temp3[Super] = new Temp3[Super]
val f:Temp3[Sub] = e
}
协变,相当于向上转型,逆变,相当于向下转型。
包与包对象
Scala包:package 包名
- 只能包含数字、字母、下划线、圆点
- 不能用数字开头, 不能使用关键字
- 可以在同一个.scala文件中,声明多个并列的package
例如:
package com.kgc{
package scala1 {... }
package scala2 {... }
}
package scala3 {... }
Scala包对象
包可以包含类、对象和特质,但不能包含变量或方法的定义,应使用包对象解决这个问题
例如:
package com.kgc{
package object scala { //对应包com.kgc.scala,每个包都可以有一个包对象
val name="Wow"
}...
package scala{...}//与包对象同名的包可直接使用包对象中定义的变量和方法
}
包引用
-
import让包和包对象的成员可以直接通过名称访问
常用:
//易于访问Fruit import cn.kgc.Fruit //易于访问cn.kgc的所有成员 import cn.kgc._ //易于访问cn.kgc.Fruits的所有成员 import cn.kgc.Fruits._ //只引用Apple与Orange,并且Apple重命名为McIntosh import cn.kgc.Fruits.{Apple=>McIntosh,Orange}
-
import灵活引用
- 可以出现在任何地方
- 可以是对象和包
- 可以重命名或隐藏一些被引用的成员