很多其他语言都有泛型支持,Scala也不例外,我们今天就从头开始看一下我们为什么需要泛型,以及Scala是如何支持泛型的。

我们为什么需要泛型?

你为项目组实现了一个类,它可也放入一个Int值,并自动记录放入的时间。可以根据时间来得到之前放入的Int值。(不考虑同一时间放入多个值的情况。)

class IntRecord{
val map=scala.collection.mutable.HashMap[Date,Int]()
def add(e:Int) ={
map += (new Date() -> e)
}
def get(date:Date):Int =map(date)
}

大家觉得你实现的这个类太棒了,但是又想让你实现一个可以存放String类型并同时可以记录时间的类。你想想这也不难,再加一个类:

class StringRecord{
val map=scala.collection.mutable.HashMap[Date,String]()
def add(e:String) ={
map += (new Date() -> e)
}
def get(date:Date):String=map(date)
}

很好,又满足需求了,这时又有很多人提出他们要Boolean,Float,Double类型的。还有人提出要支持他们自定义的所有类。这时你可能想到这么一种解决办法:

class AnyRecord{
val map=scala.collection.mutable.HashMap[Date,Any]()
def add(e:Any) ={
map += (new Date() -> e)
}
def get(date:Date):Any=map(date)
}

好了,我这个类可以满足你们所有需求!它能放进去所有东西!可是过了一会,你就得到了很多抱怨:
因为他们发现他们之前这样的语句现在不能通过编译了:

val record = new AnyRecord //原本是:new IntRecord
record add 3
record add "abc" //这样的语句也能通过编译。
val value:Int = record.get(new Date()) //原本能编译过,现在不能了。因为返回值已经改成了Any,并不是Int。

项目组为了使用你最新设计的通用代码不得不加上了令人生厌的类型检查语句:

val value:Int = record.get(new Date()) match{
case i:Int => i
case _ => sys.error("Not Int") //这里返回的类型是Nothing,可以赋值给所有类型。
}

有没有一种既能支持各种类型,又是类型安全(不用做类型转换,类型检查)的方法呢?那就是泛型。它的优点就是你可以编写针对通用类型,但又是类型安全的代码。我们来改一下代码:

class AnyRecord[T]{ //[T] 定义了类型参数T,是类型的变量名。
val map=scala.collection.mutable.HashMap[Date,T]() // 这里可以直接用类型变量T来定义Map。
def add(e:T) ={
map += (new Date() -> e)
}
def show()=map.foreach( println(_) )
def get(date:Date):T =map(date) //返回值类型也是T
}
val record = new AnyRecord[Int] //我们必须通过具体类型来实例化对象,一旦实例化,这个对象内部所有的T,都会被替换为Int。
record add 3
record add "abc" //这样的语句不能通过编译。
val value:Int = record.get(new Date())//因为T被替换成了Int,所以这里get函数返回的就是Int。

通过定义泛型类,我们成功的解决了类型通用并同时类型安全的问题。

泛型类的局限

因为在我们定义泛型类的时候,并没有具体类型,如果不知道操作的变量是什么类型,那么对它能进行的操作就非常有限了。只能把它当做一个集合成员进行管理,操作基本都是在集合上,而不是元素上。这是因为我们不知道元素是什么类型,所以无法进行操作。除了Any类型里定义的那些操作。

我要修一个澡堂,别人说最少要有两个房间,因为要一个男澡堂,一个女澡堂。我表示理解并开始定义我的类。

trait Customer{ //定义Cutomer trait
  val sex:String
}
class Man extends Customer{
  override val sex="Man" //重写sex属性为Man
}
class Woman extends Customer{
  override val sex="Woman"  //重写sex属性为Woman
}

class BathRoom{
  val customers = ListBuffer[Customer]()
  def in(c:Customer): List={
    println("One "+c.sex+" come.") //进来一个人,打印他/她的性别
    customers+=c
    customers.toList  //返回不可变的所有客人的List
  }
}

我们定义了一个customer的trait,里边定义了一个属性 sex。对于澡堂来说,区别客人性别是非常重要的。然后我们分别实现了男客户和女客户。他们都混入了Customer这个trait,分别给自己的sex属性赋值。最后我们实现了一个澡堂类BathRoom。它设计的时候是基于Cutomer设计的,这样男澡堂可以用,女澡堂也可以用。
接着我们实例化一个男澡堂,开始营业了。

val menBathRoom = new BathRoom
menBathRoom.in(new Man) // One Man come
menBathRoom.in(new Woman) //One Woman come

非常不幸,一个女生进入了男澡堂,编译器竟然没有发现类型错误。编译器说你是用Customer定义的,女客人也是客人啊。当然可以进。可见类型安全我们没有做好。那么这里可以用泛型来帮我们吗?我们来试一下:

class BathRoom[T]{
  val customers = ListBuffer[T]()
  def in(c:T): List[T] ={
    println("One "+c.sex+" come.") //这里出错,因为T可以是任意类型,不是所有类型都有sex属性。
    customers+=c
    customers.toList
  }
}

我们把BathRoom加上类型参数T,我们都是基于T来构造,可是问题出现了。我们无法调用类型T的sex属性,因为T是个通用类型,sex属性可不是所有类型都有的属性。这时候我们来看一下上界

上界

class BathRoom[T<:Customer]{
  val customers = ListBuffer[T]()
  def in(c:T): List[T] ={
    println("One "+c.sex+" come.")
    customers+=c
    customers.toList
  }
}

我们只改动了一处,就是把BathRoom的类型参数从T 改成了T<:Customer.意思是说T只能被Customer以及它的子类来实例化。同时我们在BathRoom的定义里可以访问sex属性了。因为Customer以及它的子类都是有sex属性的。然后我们再来看看上次进错澡堂的问题能解决吗:

val bathRoom = new BathRoom[Man]
bathRoom.in(new Man)
bathRoom.in(new Woman)  //编译器报错,有女客户试图闯入男澡堂!!!type mismatch found:Woman,required:Man

利用静态语言的类型安全特性,我们完美解决了问题。

下界

有上界,就要有下界。我们先定义一个新的类

class Boy extends Man

正如你所想,下界就是用符号>:表示的.那我们试着把BathRoom的类型参数改成T>:Man试试

class BathRoom[T>:Man]{
  val customers = ListBuffer[T]()
  def in(c:T): List[T]={
    //println("One "+c.sex+" come.") //这里报错,说sex不是T的属性
    customers+=c
    customers
  }
}

这个报错也合情合理,因为我们这个BathRoom可以用任意类型来实例化,只要它是Boy的父类,包括用AnyRef类型。AnyRef可没有sex属性。我们注释掉出错的这一句。
然后我们看看实例化BathRoom时,对类型参数有什么要求:

val menBathRoom = new BathRoom[Man] //没有问题
val boyBathRoom = new BathRoom[Boy] //出错,因为这里Boy不是Man的父类

目前下界只能防止你用某个类的子类去实例化一个类型参数,好像作用不大,下界和我们将要讲的协变在一起可以有更大的作用。

协变

我们再来定义一个类:Boy

class Boy extends Man

Boy继承自Man,所以下边这个语句是没有问题的,一个男孩就是一个男顾客。

val boy:Man=new Boy

那么BathRoom[Man] 和BathRoom[Boy] 之间有没有关系呢?一个男孩澡堂是不是一个男澡堂呢?

val boyRoom:BathRoom[Man]=new BathRoom[Boy] //type mismatch;found BathRoom[Boy],required BathRoom[Man]

不幸的是,在默认情况下这句话不能通过编译,男孩澡堂和男澡堂是两种不同的类型,并不能赋值。除非,你定义BathRoom的类型参数是协变的。协变的意思就是如果类型A是类型B的父类,那么通过类型A参数化的模板类也是通过类型B参数化的模板类的父类,定义方式就是在模板类的类型参数前加一个”+”.那我们试一下

class BathRoom[+T]{    //在类型T前边加了一个+,这样类型BathRoom[Man]就应该是BathRoom[Boy]的父类了。
  val customers = ListBuffer[T]() //编译错误:covariant type T occurs in invariant position
  def in(c:T): List[T] ={  //编译错误:covariant type T occurs in contravariant position in type T of value c
    customers+=c
    customers.toList
  }
}
val boyBathRoom:BathRoom[Man] = new BathRoom[Boy]

理论上,在类型T前边加了一个+,这样类型BathRoom[Man]就应该是BathRoom[Boy]的父类了。但是,实际情况并没有那么简单。编译器马上找出了两个可能的错误。这两个错误都是类似的,如果编译器没有报错,那么就可能出现下边的情况:

val boyBathRoom:BathRoom[Boy]=new BathRoom[Boy] //用Boy类型创建一个BathRoom[Boy]
val anyBathRoom:BathRoom[Any]=boyBathRoom //因为BathRoom是协变的,所以BathRoom[Boy]可以赋值给BathRoom[Any]
anyBathRoom.in(123)//现在anyBathRoom里的类型信息是Any,我们当然可以加入了一个Any的实例Int 123.

上边代码的第二行只是为了你理解,其实如果你声明了你的类是协变的,那么你的类内部就需要能处理任意类型。并返回正确的类型。比如我们声明了一个BathRoom[Boy],它的in(c:T)方法需要能接受Man,Customer,AnyRef,Any。既然支持到了Any,那就是可以放任意值进来。我们的BathRoom类如果要支持协变,有几个问题:

  • 首先我们定义的成员属性ListBuffer[T]不具备这种能力,一旦用具体的类型A,比如Man。实例化ListBuffer[T]后,这个list是不能再加入像123这样的数字的。
  • 第二我们的in方法在用具体类型A实例化后,不能输入A的父类的参数,以及返回A的父类的列表。

我们对代码进行如下的改动:

class BathRoom[+T](val customers:List[T]){  //定义协变,传入一个immutable的List
  def in[B>:T](c:B): List[B] ={ // 定义了类型B的下界是T,因此B类型可以是T的任意父类。返回类型是List[B]
     c::customers //List可以自动向上转型为List[B]
  }
}

通过上边的修改, 我们的BathRoom类可以处理任意类型了。我们试一下

val boyBathRoom:BathRoom[Boy] = new BathRoom[Boy](List[Boy]())//定义一个BathRoom[Boy]
boyBathRoom.in(new Boy)//返回一个List[Boy]
val manBathRoom:BathRoom[Man] = boyBathRoom//因为协变,BathRoom[Boy]是BathRoom[Man]的子类了。
manBathRoom.in(new Boy)//返回一个List[Man]
manBathRoom.in(new Woman)//返回一个List[Customer]
manBathRoom.in(123)//返回一个List[Any]

协变我们是想让BathRoom[Boy]是BathRoom[Man]的子类,从而可以让男孩澡堂赋值给男人澡堂。但是有时候我们可能需要的是逆变。

逆变

比如我们有一个冰箱,它有几个模式,水果模式,冻肉模式,如果调成了水果模式,那么我只放苹果肯定是可以的。逆变就是在模板类的类型前加上一个减号

class Fridge[-T] //逆变的Fridge
class Fruit
class Apple extends Fruit

val fruitFridge=new Fridge[Fruit] //定义一个水果类型的冰箱
val appFridge:Fridge[Apple]=fruitFridge//水果类型的冰箱可以赋值给一个苹果冰箱。因为Fridge是逆变的。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

%d 博主赞过: