Generic 范型 - type parameter

范型简介

从JDK 5.0开始,范型作为一种新的扩展被引入到了java语言中。

有了范型,我们可以对类型(type=class+interface)进行抽象。最常见的例子是容器类型。

List myIntList = new LinkedList(); // 1
myIntList.add(new Integer(0)); // 2
Integer x = (Integer) myIntList.iterator().next(); // 3      

可以用范型对以上代码进行优化:

List<Integer> 
    myIntList = new LinkedList<Integer>(); // 1'
myIntList.add(new Integer(0)); // 2'
Integer x = myIntList.iterator().next(); // 3'

优化带来两点改进:

  1. 省去了造型(cast)的麻烦。
  2. 除了代码上的整洁,范型还在compile-time保证了代码的类型正确。如果没有范型,无法保证放入list的对象是Integer型。

定义简单的范型

从package java.util中摘录下接口List和Iterator的定义:

public interface List <E> {
    void add(E x);
    Iterator<E> iterator();
}

public interface Iterator<E> {
    E next();
    boolean hasNext();
}

这里声明了type parameter:E。Type parameters在范型的全部声明中都可以用,就像使用其他普通的类型一样。

调用范型的时候,需要为type parameter E 指定一个真实的类型变量【1】(又称为parameterized type),例如:

List<Integer> myIntList = new LinkedList<Integer>();

可以想象List<Integer>是List的一个版本,在这个版本里面,所有的 type parameter (E)都被Integer替换了:

public interface IntegerList {
    void add(Integer x);
    Iterator<Integer> iterator();
}

这种想象很有帮助,因为parameterized type的List<Integer> 确实包含了类似的方法;但是也容易带来误导,因为每次调用范型并不会生成代码的一个拷贝,通过编译,一个范型类型的声明只会编译一次,生成一个class文件;每次调用范型,类似于给一个方法传入了一个argument,只是这里传入的是一个普通的类型。

【1】这里用的是argument,即传给方法的值;区别parameter,parameter是作为方法签名的一部分,用于定义方法。

范型和子类型

假设Foo是Bar的子类型(class或者interface),G是一个范型类型声明,G<Foo> 不是G<Bar>的子类型,这点有些反直觉。

wildcards 通配符

接着上一节的讨论,假设Foo是Bar的子类型(class或者interface),G是一个范型类型声明,G<Foo> 不是G<Bar>的子类型。可是,如果我们确实需要在G<Foo> 和G<Bar>之间建立父子关系呢?具体来说,假设有以下一段代码:

void printCollection(Collection c) {
    Iterator i = c.iterator();
    for (k = 0; k < c.size(); k++) {
        System.out.println(i.next());
    }
}

用范型对其进行优化,这里是一种错误的方式:

void printCollection(Collection<Object> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

这样写本身没有错误,但是他对Collection中元素的类型进行了限制,只能是Object!那么,所有collection的超类是神马呢?就是Collection<?>(读作"collection of unknown"),这个Collection的元素类型可以任意匹配,被称作wildcard type

void printCollection(Collection<?> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

嗯,不错,现在我们可以从c中读取出任意类型的元素。可以,这样一来,又出现了新的问题:什么样的元素可以放到c里面去呢?答案是:任何类型的元素都无法放到c里面去!因为无法知道c中的type parameter(也许写作E)是什么类型。

Bounded Wildcards 有界通配符

可能是考虑到?过于宽泛,java引入了Bounded Wildcards,有界通配符。假设有以下代码:

public abstract class Shape {
    public abstract void draw(Canvas c);
}

public class Circle extends Shape {
    private int x, y, radius;
    public void draw(Canvas c) {
        ...
    }
}

public class Rectangle extends Shape {
    private int x, y, width, height;
    public void draw(Canvas c) {
        ...
    }
}

// These classes can be drawn on a canvas:
public class Canvas {
    public void draw(Shape s) {
        s.draw(this);
   }
}
// Assuming that they are represented as a list, 
// it would be convenient to have a method in Canvas that draws them all:
public void drawAll(List<Shape> shapes) {
    for (Shape s: shapes) {
        s.draw(this);
   }
}

看上去不错,但是问题又来了,类型方法drawAll的签名参数中的ShapeCircle的超类,尽管CircleShape的子类,但是List<Circle>不是List<Shape>的子类。所以要想drawAll可以处理List<Circle>,可以将其定义为:

public void drawAll(List<? extends Shape> shapes) {
    ...
}

Bounded Wildcards 也面临着?面临的问题,那就是他们都过于宽泛,因此无法
确定什么样的元素可以放到集合里面:

public void addRectangle(List<? extends Shape> shapes) {
    // Compile-time error!
    shapes.add(0, new Rectangle());
}

范型方法

前面讨论了范型type的声明,其实,同样可以声明范型方法:

static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
    for (T o : a) {
        c.add(o); // Correct
    }
}

所谓范型方法,就是在方法签名内的修饰符和方法返回类型之间,加入了type parameter,例如<T>。在调用方法的时候,并不需要传入type argument,编译器会根据actual argument的类型推断(infer)出type argument。

较之于范型类型,范型方法的声明要稍微复杂一些。具体来说,范型方法包含返回值和若干parameter,而他们之间可能会存在着类型的依赖关系。而这种依赖关系就带来一个问题,什么时候应该使用通配符,什么时候应该使用范型方法呢?
比如,查看JDK文档:

interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

为什么不写成:

interface Collection<E> {
    public <T> boolean containsAll(Collection<T> c);
    public <T extends E> boolean addAll(Collection<T> c);
    // Hey, type variables can have bounds too!
}

在containsAll 和 addAll中,type parameter T 仅使用了1次。返回值和其他parameter并不依赖于它,这种情况下,应该使用通配符。只有当返回值和parameter之间存在依赖的情况下,才应该使用范型方法。例如:

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

范型是如何实现的

范型是通过编译器对代码的erasure转换实现的。可以把这一过程想象成source-to-source的翻译。例如:

public String loophole(Integer x) {
    List<String> ys = new LinkedList<String>();
    List xs = ys;
    xs.add(x); // Compile-time unchecked warning
    return ys.iterator().next();
}

将被翻译成:

public String loophole(Integer x) {
    List ys = new LinkedList;
    List xs = ys;
    xs.add(x); 
    return(String) ys.iterator().next(); // run time error
}

在第二段代码中,我们从list中取出一个元素,并试图通过将其cast(造型)把它当成String处理,这里会得到一个ClassCastException。

因为在编译阶段,编译器对代码进行了erasure,<>内的一切都被删除了,所以所有对范型类型的调用(Invocations,或者说实例)共享同一个run-time class,随之而来的,static变量和方法也被这些实例共享,所以在static方法中,也无法引用type parameter;同时,Cast 和InstanceOf操作也就都失去了意义。

Collection cs = new ArrayList<String>();
// Illegal.
if (cs instanceof Collection<String>) { ... }

// Unchecked warning,
Collection<String> cstr = (Collection<String>) cs;
//gives an unchecked warning, since this isn't something the runtime system is //going to check for you.

同理,对于方法来说,type variables(<T>在方法中叫type variables,在类型声明中叫parameterized type)也不存在于run-time:

// Unchecked warning. 
<T> T badCast(T t, Object o) {
    return (T) o;
}

如何定义范型数组

private E[] elements = (E[]) new Object[10];
  • 数组和范型对类型的检查是不同的。

对于数组来说,下面的语句是合法的:

Object[] arr = new String[10];

Object[] 是 String[]的超类,因为Object是String的超类。然而,对于范型来说,就没有这样的继承关系,因此,以下声明无法通过编译:

List<Object> list = new ArrayList<String>(); // Will not compile. generics are invariant.

java中引入范型,是为了在编译阶段强化类型检查。同时,因为type erasure,范型也没有runtime的任何信息。所以,List<String> 只有静态类型的 List<String>,和一个动态类型 List

但是,数组携带了runtime的类型信息。在runtime,数组用Array Store Check来检查将要插入的元素是否和真实的数组类型兼容。因此,以下代码能很好的编译,但是由于Array Store Check,会在runtime失败:

Object[] arr = new String[10];
arr[0] = new Integer(10);

回到范型,编译器会提供编译阶段的检查,避免这种以这种方式创建索引,防止runtime的异常出现。

  • 那么,创建范型数组有什么问题呢?
    创建元素的类型是type parameter, parameterized type 或者bounded wildcard parameterized type的数组是type-unsafe的。考虑如下代码:
public <T> T[] getArray(int size) {
    T[] arr = new T[size];  // Suppose this was allowed for the time being.
    return arr;
}

在rumtime,T的类型未知,实际上创建的数组是Object[],因此在runtime,上面的方法像是:

public Object[] getArray(int size) {
    Object[] arr = new Object[size];
    return arr;
}

假设,有以下调用:

Integer[] arr = getArray(10);

这就是问题,这里将Object[] 指派给了一个Integer[]类型的索引,这段代码编译没有问题,但是在runtime会失败。因此,创建范型数组是不合法的。

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

推荐阅读更多精彩内容