Java & Groovy & Scala & Kotlin - 27.泛型

Overview

泛型使类型参数化变得可能。在声明类或接口时,可以使用自定义的占位符来表示类型,在运行时由传入的具体类型进行替换。泛型的引入让集合变得更加好用,使很多错误在编译时就能被发现,也省去了一些强制转换的麻烦。

Java 篇

泛型是 Java 1.5才引进的特性。没有泛型的时候使用一个持有特定类型的值的类的时候是非常麻烦的

例:

public class ObjectCapture {
    private Object object;
    public ObjectCapture(Object o) {
        this.object = o;
    }
    public void set(Object object) {
        this.object = object;
    }
    public Object get() {
        return object;
    }
}

使用以上类

ObjectCapture integerObjectCapture = new ObjectCapture(10);
assert 10 == (Integer) integerObjectCapture.get();

没有泛型的时候在取数据时必须进行强制转换,但是此时根本无法保证 之前使用的 ObjectCapture 保存的是 Integer 类型的值,如果是其它类型的话,程序就会直接挂掉,而且这种错误只有运行时才能发现。

创建泛型

类型参数使用 <类型参数名> 作为类型的占位符。

public class Capture {
    private T t;
    public Capture(T t) {
        this.t = t;
    }
    public void set(T t) {
        this.t = t;
    }
    public T get() {
        return t;
    }
}

Java 中最常用的占位符为通用的 "T",表示 Key 的 "K", 表示 Value 的 "V" 和表示异常的 "E"。

使用泛型

Capture integerCapture = new Capture<>(10);
assert 10 == integerCapture.get();
Capture stringCapture = new Capture<>("Hi");
assert "Hi".equals(stringCapture.get());

以上分别用 IntegerString 作为传入的类型参数,如果向这两个对象传入不符合的类型时编译器就会理解报错,此外取数据时也不用进行强制转换,比起没有泛型时要方便很多。

类型擦除

Java 的泛型是在编译器层次实现的,所以运行时有关泛型的信息都会被丢失,这被称作类型擦除。也就是说上节例子中的 CaptureCapture 在运行时都是 Capture 类型,没有任何区别。

协变与逆变

如果 Capture 被看做是 Capture 的子类型,则称这种特性为协变。相反情况则称为逆变。

协变

在 Java 中,协变是默认支持,所以可以写出以下例子:

Integer[] integers = new Integer[2];
Object[] objects = integers;

但是这样会造成以下的问题

Date[] dates = new Date[2];
Object[] objects2 = dates;
objects2[0] = "str";

这种代码在编写时完全没有问题,但是运行时会抛出异常。所以引进泛型时就不支持协变,所以以下代码在编译时就会报错。

List dateList = new ArrayList<>();
List objectList = dateList;
 
 

逆变

Java 不支持逆变。

类型通配符

由于泛型不支持协变,所以在使用泛型作为参数传递时会非常麻烦。

private static void foo(List list) {}
 
 

以上例子中是无法将 dateList 传入 foo() 方法中的。解决方法是使用通配符 ?

private static void foo(List list) {}

以上例子中就能正常传入 dateList 了。

注意:List 和 List 并不是一个概念

List 是原生类型,表示不对 List 的类型进行限制,可以进行各种操作,错误使用在运行时才能发现。

List 表示 List 中存放的是某种类型的数据,只是类型本身并不确定。所以无法建立 List 的实例,也无法向 List 中追加任何数据。

类型参数边界

上节说过无法向 List 中追加任何数据,这一做法会让程序变得非常麻烦,解决方法就是使用类型参数边界。类型参数边界分为上边界和下边界。

上边界用于限定类型参数一定是某个类的子类,使用关键字 extends 指定。下边界用于限定类型参数一定是某个类的超类,使用关键字 super 指定。

上边界无法确定容器中保存的真实类型,所以无法向其中追加数据,但是可以获得边界类型的数据

private static void foo3(List list) {
    //        list.add(new Num(4));
    Num num = list.get(0);
}

下边界可以追加边界类型的数据,但是获得数据都只能是 Object 类型

private static void foo4(List list) {
    list.add(new Num(4));
    Object object = list.get(0);
}

上下边界在这里实际是起到了协变和逆变的作用,具体可以对比 Kotlin 的例子。

Groovy 篇

Groovy 中使用的就是 Java 的泛型,所以参考 Java 就行了。但是要注意的是由于 Groovy 的动态特性,所以有些Java 会报的编译错误在 Groovy 中只有运行时才会发现。

例如以下代码在 Java 中是非法的,在 Groovy 中虽然编译通过,但运行时会报错

List dateList = new ArrayList<>()
dateList.add(1)
dateList.add(new Date())

Scala 篇

创建泛型

类型参数使用 [类型参数名] 作为类型的占位符,而 Java 用的是 <>

class Capture[A](val a: A) {
}

Scala 中最常用的占位符为 "A"。

使用泛型

val integerCapture = new Capture[Int](10)
val nint10:Int = integerCapture.a
val stringCapture = new Capture[String]("Hi")
val strHi:String = stringCapture.a
println(strHi)

以上分别用 IntString 作为传入的类型参数,如果向这两个对象传入不符合的类型时编译器就会理解报错,此外取数据时也不用进行强制转换,比起没有泛型时要方便很多。

协变与逆变

如果 Capture 被看做是 Capture 的子类型,则称这种特性为协变。相反情况则称为逆变。
在 Scala 中,这两种特性都是默认不支持的。

注意,函数的参数是逆变的,函数的返回值是协变的。

使用协变

使用协变需要在类型前加上 +

定义一个支持协变的类,协变类型参数只能用作输出,所以可以作为返回值类型但是无法作为入参的类型

class CovariantHolder[+A](val a: A) {
  def foo(): A = {
    a
  }
}

使用该类

var strCo = new CovariantHolder[String]("a")
var intCo = new CovariantHolder[Int](3)
var anyCo = new CovariantHolder[AnyRef]("b")

//  Wrong!! Int 不是 AnyRef 的子类
// anyCo = intCo
anyCo = strCo

使用逆变

使用逆变需要在类型前加上 -

定义一个支持逆变的类,逆变类型参数只能用作输入,所以可以作为入参的类型但是无法作为返回值类型

class ContravarintHolder[-A]() {
  def foo(p: A): Unit = {
  }
}

使用该类

var strDCo = new ContravarintHolder[String]()
var intDCo = new ContravarintHolder[Int]()
var anyDCo = new ContravarintHolder[AnyRef]()

//  Wrong!! AnyRef 不是 Int 的超类
// strDCo = anyDCo
strDCo = anyDCo

类型通配符

概念与 Java 基本一致,只是 Scala 使用 _ 作为通配符。

def foo2(capture: Capture[_]): Unit = {
}

类型参数边界

上边界用于限定类型参数一定是某个类的子类,使用符号 <: 指定。下边界用于限定类型参数一定是某个类的超类,使用符号 >: 指定。

上边界无法确定容器中保存的真实类型,所以无法向其中追加数据,但是可以获得边界类型的数据

def foo3(list: collection.mutable.MutableList[_ <: Num]): Unit = {
    //    list += new Num(4)
    val num = list.head
    println(num.number)
}

下边界可以追加边界类型的数据,但是获得数据都只能是 Any 类型

def foo4(list: collection.mutable.MutableList[_ >: Num]): Unit = {
    list += new Num(4)
    val num = list.head
    println(num.asInstanceOf[Num].number)
}

最小类型

Scala 中在表示边界时可以使用 Nothing 表示最小类型,即该类为所有类型的子类,所有可以写出以下代码。

def foo5(capture: Capture[_ >: Nothing]): Unit = {
}

Kotlin 篇

创建泛型

同 Java。

class Capture(val t: T)

使用泛型

val integerCapture = Capture(10)
val nint10 = integerCapture.t
val stringCapture = Capture("Hi")
val str = stringCapture.t

协变与逆变

在 Kotlin 中,这两种特性都是默认不支持的。

注意,函数的参数是逆变的,函数的返回值是协变的。

使用协变

使用协变需要在类型前加上 out,相比较 Scala 使用的 + 可能更能让人理解。

定义一个支持协变的类,协变类型参数只能用作输出,所以可以作为返回值类型但是无法作为入参的类型

class CovariantHolder(val a: A) {
    fun foo(): A {
        return a
    }
}

使用该类

var strCo: CovariantHolder = CovariantHolder("a")
var anyCo: CovariantHolder = CovariantHolder("b")
anyCo = strCo

使用逆变

使用逆变需要在类型前加上 in

定义一个支持逆变的类,逆变类型参数只能用作输入,所以可以作为入参的类型但是无法作为返回值的类型

class ContravarintHolder(a: A) {
    fun foo(a: A) {
    }
}

使用该类

var strDCo = ContravarintHolder("a")
var anyDCo = ContravarintHolder("b")
strDCo = anyDCo

类型通配符

Kotlin 使用 * 作为通配符,而 Java 是 ,Scala 是 _

fun foo2(capture: Capture<*>) {
}

类型参数边界

Kotlin 并没有上下边界这种说法。但是可以通过在方法上使用协变和逆变来达到同样的效果。

使用协变参数达到上边界的作用,这里的 out 很形象地表示了协变参数只能用于输出

fun foo3(list: MutableList) {
    val num: Num = list.get(0)
    println(num)
}

使用逆变参数达到下边界的作用,这里的 in 很形象地表示了逆变参数只能用于输入

fun foo4(list: MutableList) {
    list.add(Num(4))
    val num: Any? = list.get(0)
    println(num)
}

Summary

  • Java 和 Groovy 的用法完全一致,都只支持逆变
  • Scala 和 Kotlin 支持逆变和协变,但是都需要显示指定
  • Java 使用 ? 作为通配符,Scala 使用 _,Kotlin 使用 *

文章源码见 https://github.com/SidneyXu/JGSK 仓库的 _27_generics 小节

你可能感兴趣的:(Java & Groovy & Scala & Kotlin - 27.泛型)