「深入Java」Generics泛型

欢迎转载,但请保留作者链接:http://www.jianshu.com/p/f258c907019d

相关文章:

吐槽:这是目前最不深入的一篇了,因为关于泛型实在有太多需要注意的地方,本文仅人工过滤出了较常见的重点内容,后期再进行不定时更新吧。

必要性

在程序日益复杂庞大的今天,编写泛用性代码的价值愈发变得巨大。
而要做到这一点,其诀窍仅只两字而已——解耦

最简单的解耦,无疑是使用基类替代子类。然而由于Java仅支持单继承,这种解耦方法所带来的局限性未免过大,有种“只准投胎一次”的感觉。

使用接口替代具体类算是更进了一步,算是多给了一条命吧,但限制仍旧存在。要是我们所写的代码本身就是为了应用于“某种不确定的类型”呢?

这时候就轮到泛型登场了。


简单泛型

虽然理想远大。但Java引入泛型的初衷,也许只是为了创造容器类也说不定。

站在类库设计者的角度,我们不妨走上一遭。

得益于单根继承结构,我们可以这样来设计一个持有单个对象的容器:

public class Holder1 {
    private Object a;
    public Holder1(Object a) { 
        this.a = a; 
    }
    Object get() { 
        return a; 
    }
} 

这个容器确实能持有多种类型的对象,但通常而言我们只会用它来存储一种对象。也就是说虽然设计时希望能存储任意类型,但使用时却能够只存储我们想要的确定类型

泛型可以达到这一目的,与此同时,这也能使编译器为我们提供编译期检查。

class Automobile {}

public class Holder2<T> {
    private T a;
    public Holder2(T a) { 
          this.a = a; 
    }
    public void set(T a) {
          this.a = a;
    }
    public T get() {
         return a; 
    }

    public static void main(String[] args) {
        Holder2<Automobile> h2 =
            new Holder2<Automobile>(new Automobile());
            Automobile a = h2.get(); // No cast needed
            // h2.set("Not an Automobile"); // Error
            // h2.set(1); // Error
    }
}

如你所见,使用方法即为在类名后添加尖括号,然后填写类型参数“T”。使用时用明确的类型参数替换掉“T”,即为该容器指定了其存储的确定类型


泛型方法

泛型可以应用于方法,只需要将泛型参数列表放在方法返回值之前即可。
下面这个例子中,f()的效果看起来像是重载过一样:

//: generics/GenericMethods.java

public class GenericMethods {
    public <T> void f(T x) {
        System.out.println(x.getClass().getName());
    }
    public static void main(String[] args) {
        GenericMethods gm = new GenericMethods();
        gm.f("");
        gm.f(1);
        gm.f(1.0);
        gm.f(1.0F);
        gm.f(‘c’);
        gm.f(gm);
    }
} /* Output:
java.lang.String
java.lang.Integer
java.lang.Double
java.lang.Float
java.lang.Character
GenericMethods
*///:~

能这样做的原因在于编译器拥有称为类型参数推断的功能,能为我们找出具体的类型。
注意,如果调用f()时传入了基本数据类型,自动打包机制将会被触发,将基本数据类型包装为对应的对象。


擦除

Java泛型是使用擦除来实现的,这意味着在泛型代码内部,无法获得关于类型参数的信息
谨记,泛型类型参数将擦除到它的第一个边界,默认边界为Object;对于<T extends Bound>,第一个边界为Bound,即像是在类的声明中使用Bound替换掉T一样。

以下例子说明了这一问题:

//: generics/ErasedTypeEquivalence.java
import java.util.*;

public class ErasedTypeEquivalence {
    public static void main(String[] args) {
        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.println(c1 == c2);
    }
} /* Output:
true
*///:~

尽管运行时指定了不同的泛型参数,但"ArrayList<String>"与"ArrayList<Integer>"事实上却被擦除成了相同的原生类型“ArrayList”来进行处理;用类字面常量来进行说明应该会更为直观:“c1”与"c2"的值为“ArrayList.class”,而不是“ArrayList<String>.class”与“ArrayList<Integer>.class”

知道了这一点后,你或许能猜测出容器类的一些具体实现细节了。
打开ArrayList的源码,会发现在其内部,用来存储数据的数组是这样定义的:

    /**
     * The elements in this list, followed by nulls.
     */
    transient Object[] array;

而其get()方法则是这样:

    @SuppressWarnings("unchecked") @Override public E get(int index) {
        if (index >= size) {
            throwIndexOutOfBoundsException(index, size);
        }
        return (E) array[index];
    }

注意,当E的第一个边界为Object时,那么这个方法实际上就根本没有进行转型(从Object到Object)。
知道了这一点后,你大概会对以下代码为何能符合预期地运行感到疑惑:

//: generics/GenericHolder.java
public class GenericHolder<T> {
    private T obj;
    public void set(T obj) { this.obj = obj; }
    public T get() { return obj; }

    public static void main(String[] args) {
        GenericHolder<String> holder =
            new GenericHolder<String>();
        holder.set("Item");
        String s = holder.get(); // Why it works?
    }
} ///:~

使用 javap -c 反编译,我们可以找到答案:

public void set(java.lang.Object);
0: aload_0
1: aload_1
2: putfield #2; //Field obj:Object;
5: return
public java.lang.Object get();
0: aload_0
1: getfield #2; //Field obj:Object;
4: areturn
public static void main(java.lang.String[]);
0: new #3; //class GenericHolder
3: dup
4: invokespecial #4; //Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5; //String Item
11: invokevirtual #6; //Method set:(Object;)V
14: aload_1
15: invokevirtual #7; //Method get:()Object;
18: checkcast #8; //class java/lang/String --------Watch this line--------
21: astore_2
22: return

奥秘就是,编译器在编译期为我们执行类型检查,然后插入了转型代码。

再看下面这个例子:

//: generics/ArrayMaker.java
import java.lang.reflect.*;
import java.util.*;
public class ArrayMaker<T> {
    private Class<T> kind;
    public ArrayMaker(Class<T> kind) { this.kind = kind; }
    @SuppressWarnings("unchecked")
    T[] create(int size) {
        return (T[])Array.newInstance(kind, size);
    }

    public static void main(String[] args) {
        ArrayMaker<String> stringMaker =
            new ArrayMaker<String>(String.class);
        String[] stringArray = stringMaker.create(9);
        System.out.println(Arrays.toString(stringArray));
    }
} /* Output:
[null, null, null, null, null, null, null, null, null]
*///:~

因为擦除的关系,kind只是被存储为Class,使用“Array.newInstance();”创建数组也就只能得到非具体的结果,实际使用中我们需要对其进行向下转型,但是并没有足够的类型信息用以进行类型检查,所以对于编译器报错,只能采用注解“@SuppressWarnings("unchecked")”强行将其消去。


通配符

有些时候你需要限定条件,使用通配符可以满足这一特性。

这是指定上界的情况:

//: generics/GenericsAndCovariance.java
import java.util.*;
public class GenericsAndCovariance {
    public static void main(String[] args) {
        // Wildcards allow covariance:
        List<? extends Fruit> flist = new ArrayList<Apple>();
        // Compile Error: can’t add any type of object:
        // flist.add(new Apple());
        // flist.add(new Fruit());
        // flist.add(new Object());
        flist.add(null); // Legal but uninteresting
        // We know that it returns at least Fruit:
        Fruit f = flist.get(0);
    }
} ///:~

flist的类型为List<? extends Fruit>,读作“任何从Fruit继承而来的类型构成的列表”。但这并不意味着这个List将持有任何类型的Fruit,通配符引用的其实是明确的类型,这个例子中它意味着“某种指定了上界为Fruit的具体类型”。

造成flist的add()完全不可用的原因是,在这种情况下add()的参数也变成了“? extends Fruit”。下面这个例子可以帮助你进行理解:

//: generics/Holder.java
public class Holder<T> {
    private T value;
    public Holder() {}
    public Holder(T val) { value = val; }
    public void set(T val) { value = val; }
    public T get() { return value; }
    public boolean equals(Object obj) {
        return value.equals(obj);
    }

    public static void main(String[] args) {
        Holder<Apple> Apple = new Holder<Apple>(new Apple());
        Apple d = Apple.get();
        Apple.set(d);
        // Holder<Fruit> Fruit = Apple; // Cannot upcast
        Holder<? extends Fruit> fruit = Apple; // OK
        Fruit p = fruit.get();
        d = (Apple)fruit.get(); // Returns ‘Object’
        try {
            Orange c = (Orange)fruit.get(); // No warning
        } catch(Exception e) {
             System.out.println(e); 
        }

        // fruit.set(new Apple()); // Cannot call set()
        // fruit.set(new Fruit()); // Cannot call set()
        System.out.println(fruit.equals(d)); // OK
    }
} /* Output: (Sample)
java.lang.ClassCastException: Apple cannot be cast to Orange
true
*///:~

同样的道理,对于上例中的fruit来说,其set()方法的参数变成了“? extends Fruit”,这意味着其接受的参数可以是任意类型,只需满足上界为Fruit即可,而编译器无法验证“任意类型”的类型安全性。

反过来看看指定下界的效果:

//: generics/SuperTypeWildcards.java
import java.util.*;

class Jonathan extends Apple {}

public class SuperTypeWildcards {
    static void writeTo(List<? super Apple> apples) {
        apples.add(new Apple());
        apples.add(new Jonathan());
        // apples.add(new Fruit()); // Error
    }
} ///:~

可以看到,写入操作变得合法。显然,Apple类型满足下界需求,执行写入操作没有安全性问题,而Jonathan是Apple的子类,经过向上转型,也可以符合需求,而Apple的基类Fruit则仍然由于类型不定而被拒绝。


基本类型不能作为类型参数

不能创建List<int>之类,而需使用List<Integer>,但因为自动包装机制的存在,所以写入数据时可以使用基本数据类型。


实现参数化接口

一个类不能实现同一个泛型接口的两种变体,因为擦除会让它们变成相同的接口:

//: generics/MultipleInterfaceVariants.java
// {CompileTimeError} (Won’t compile)
interface Payable<T> {}

class Employee implements Payable<Employee> {}

class Hourly extends Employee
    implements Payable<Hourly> {} ///:~

Hourly不能编译。但是,如果从Payable的两种用法中移除掉泛型参数(就像编译器在擦除阶段做的那样),这段代码将能够编译。


重载

以下代码无法编译,因为擦除会让两个方法产生相同的签名:

//: generics/UseList.java
// {CompileTimeError} (Won’t compile)
import java.util.*;
public class UseList<W,T> {
    void f(List<T> v) {}
    void f(List<W> v) {}
} ///:~

自限定类型

class SelfBounded<T extends SelfBounded<T>> { // ...

待补充...

参考资料

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

推荐阅读更多精彩内容