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<T> {
    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<Integer> integerCapture = new Capture<>(10);
assert 10 == integerCapture.get();
Capture<String> stringCapture = new Capture<>("Hi");
assert "Hi".equals(stringCapture.get());

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

类型擦除

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

协变与逆变

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

协变

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

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

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

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

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

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

逆变

Java 不支持逆变。

类型通配符

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

private static void foo(List<Object> 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<? extends Num> list) {
    //        list.add(new Num(4));
    Num num = list.get(0);
}

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

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

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

Groovy 篇

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

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

List<Date> 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<String> 被看做是 Capture<Object> 的子类型,则称这种特性为协变。相反情况则称为逆变。
在 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<T>(val t: T)

使用泛型

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

协变与逆变

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

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

使用协变

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

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

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

使用该类

var strCo: CovariantHolder<String> = CovariantHolder("a")
var anyCo: CovariantHolder<Any> = CovariantHolder<Any>("b")
anyCo = strCo

使用逆变

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

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

class ContravarintHolder<in A>(a: A) {
    fun foo(a: A) {
    }
}

使用该类

var strDCo = ContravarintHolder("a")
var anyDCo = ContravarintHolder<Any>("b")
strDCo = anyDCo

类型通配符

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

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

类型参数边界

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

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

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

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

fun foo4(list: MutableList<in Num>) {
    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 小节

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

推荐阅读更多精彩内容

  • 前言 人生苦多,快来 Kotlin ,快速学习Kotlin! 什么是Kotlin? Kotlin 是种静态类型编程...
    任半生嚣狂阅读 26,062评论 9 118
  • 前言 泛型(Generics)的型变是Java中比较难以理解和使用的部分,“神秘”的通配符,让我看了几遍《Java...
    珞泽珈群阅读 7,572评论 12 51
  • object 变量可指向任何类的实例,这让你能够创建可对任何数据类型进程处理的类。然而,这种方法存在几个严重的问题...
    CarlDonitz阅读 884评论 0 5
  • 本文大量参考Thinking in java(解析,填充)。 定义:多态算是一种泛化机制,解决了一部分可以应用于多...
    谷歌清洁工阅读 438评论 0 2
  • 一个女人在打电话 她的太阳镜下藏了什么? 伤心的眼泪? 失望的泪水? 凶狠的目光? 她的包里又藏了什么? 手枪? ...
    上官血樱阅读 202评论 0 1