java泛型全面分析

一. 为什么需要泛型

先来说说一个简单的实例。

class Store {
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

public class GenericTest {

    public static void main(String[] args)  {
        Store store = new Store();
        store.setData("zhang");
        // 这里必须强转
        String data = (String) store.getData();
        System.out.println(data);
    }
}

这个例子很简单,有一个仓库Store,里面储存了一个数据data,我们在主函数中向仓库中存入数据,然后又取出数据。为了让仓库Store可以存储各种各样的数据,所以数据data的类型是Object。

但是这里有个问题,如果我们不小心存了一个int数据类型,而取数据时又把它强转成String类型,那么就会抛出ClassCastException异常,程序终止。

这种错误是非常严重的,它会导致程序的崩溃,那么我们有没有好的方法来阻止这类错误的产生呢?这里产生问题的原因是我们添加了错误类型的数据,那么我们就必须提醒开发者,这里是错误操作。

要解决这类问题,就需要使用到泛型。

二. 泛型简单使用

class Store<T> {
    private T data;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

public class GenericTest {

    public static void main(String[] args)  {
        Store<String> store = new Store();
        // 不能设置int类型,只能设置String以及String的子类
        store.setData(1);
        // 不需要强制转换
        String data = store.getData();
        System.out.println(data);
    }
}

将上面例子改成泛型实现,会发现有三点变化:

  1. Store<String>: 参数化类型的Store,它的实际类型参数是String,关于这个含义在下一章介绍。
  2. 当向Store存入int类型,会直接报错,告诉开发者不能存入int类型,只能存入String以及String的子类。
  3. 取出数据时,不需要强制转换。

这里我们就看到了泛型T最大的作用:通过编译器检查,来约束T的接受类型。

例如这里对于Store<String>这个参数化类型,它的setData方法参数T的接受类型只能是String以及String的子类。因此它的getData方法返回的T类型就肯定是String或者String的子类,所以可以直接用String类型接受,不需要强制转换。

三. 泛型参数意义

在上面的例子涉及到三个重要术语:

  1. 类型参数(type parameter): 就是 Store<T>中的T。类型参数可以是一个或者多个。
  2. 泛型(generic): 声明中具有一个或者多个类型参数的类或者接口都称为泛型。例如Store<T>
  3. 参数化类型(parameterized type): 就是用实际类型参数替代泛型声明中的类型参数。即这里的Store<String>。

除了这些,泛型还有原生态(raw type)类型,无限制通配符类型(unbounded wildcard type),和有限制通配符类型(bounded wildcard type)等等,将在后面介绍。

四. 泛型子类型

如果我们想创建一个新方法,来打印Store中的存储的数据data,例如这样:

public class GenericTest {
    // 打印仓库Store中存储的数据data
    public static void print(Store<String> store) {
        String str = store.getData();
        System.out.println(str);
    }

    public static void main(String[] args)  {
        Store<String> sStore = new Store();
        sStore.setData("zhang");
        print(sStore);
    }
}

但是现在print方法只能接受Store<String>这个参数化类型,我们想让方法更加通用,可以打印任何数据,那么很容易想到这样进行改动:

public class GenericTest {
    // 打印仓库Store中存储的数据data
    public static void print(Store<Object> store) {
        Object obj = store.getData();
        System.out.println(obj.toString());
    }

    public static void main(String[] args)  {
        Store<String> sStore = new Store();
        sStore.setData("zhang");
        // 这里报错。就是不能将Store<String>实例赋值给Store<Object>对象实例
        print(sStore);
    }
}

这样改动后,发现程序居然报错了,Store<String>实例赋值给Store<Object>对象实例。

这个就很奇怪了,按照多态理论,子类对象可以赋值给父类对象。String是Object的子类,但是Store<String>对象居然不能赋值给Store<Object>对象。也叫做泛型是不协变的(covariant)。

4.1 泛型是不协变的

在java中数组是协变的,即如果Sub是Super的子类型,那么数组类型Sub[]也是Super[]的子类型。但是对于泛型来说,参数化类型Store<Sub>却不是参数化类型Store<Super>的子类型。
那么泛型为什么不支持协变呢?主要是因为泛型的作用约束:

对于一个泛型Store<T>,如果我们创建了一个参数化类型Store<String>的对象sStore,当我们设置T值的时候(即setData(T data)方法),编译器会检查,保证设置的T值只能是String以及String子类。
但是如果我们泛型支持协变的话,就会破坏这个编译器检查,但是泛型失去作用。

例如,下面这个例子:

    public static void main(String[] args)  {
        Store<String> sStore = new Store();
        // 如果泛型支持协变,这里赋值允许
        Store<Object> store = sStore;

        // 向store存储一个整数类型是可以的,因为Integer是Object的子类
        store.setData(new Integer(10));
        // 从sStore获取数据,因为是泛型不需要强制转换
        String s = sStore.getData();
    }

如果泛型支持协变,那么我们就可以通过store向参数化类型Store<String>实例sStore中,设置整数类型变量,因为store表明的参数化类型是Store<Object>,所以可以设置整数类型变量(Integer是Object子类).
这样就躲过了泛型的编译器检查,导致sStore储存了Integer类型,泛型的约束就不存在了。

4.2 原生态(raw type)类型

我们知道泛型是不协变的,那么想实现通用方法,即可以实现接受不同参数化类型参数,该如何实现呢?

public class GenericTest {
    // 打印仓库Store中存储的数据data
    public static void print(Store store) {
        Object obj = store.getData();
        System.out.println(obj.toString());
    }

    public static void main(String[] args)  {
        Store<String> sStore = new Store();
        sStore.setData("zhang");
        print(sStore);
    }
}

这里代码不会报错,我们使用原生态类型Store接受参数化类型Store<String>。

所谓原生态类型,就是移除了所有泛型信息,就相当于没有使用泛型一样,那么也就没有编译器检查,已经泛型约束了。就如同最开始使用Object类型data的那个Store。

在代码中,我们尽量不要使用原生态类型,因为它会破坏泛型约束,导致安全性问题:

    public static void main(String[] args)  {
        Store<String> sStore = new Store();
        // 使用原生态类型Store来接收
        Store store = sStore;

        // 向store存储一个整数类型是可以的。默认可以接收任何类型
        store.setData(new Integer(10));
        // 从sStore获取数据,因为是泛型不需要强制转换。
        // 运行时,这里直接抛出ClassCastException异常
        String s = sStore.getData();
    }

使用原生态类型导致程序运行时,抛出了ClassCastException异常,因为我们向store储存了整数数据。

4.3 无限制通配符类型(unbounded wildcard type)

原生态类型是不安全的,可能会抛出异常。java中提供了一种安全的替代方法,就是无限制通配符类型。如果我们不关心实际类型参数,那么我们可以使用问号?来替代,即Store<?>。
无限制通配符类型可以接受任何参数化类型,而且它保证了泛型的安全。

    public static void main(String[] args)  {
        Store<String> sStore = new Store();
        // 使用无限制通配符类型Store<?>来接收
        Store<?> store = sStore;

        // 这里报错。不能向Store<?>类型存储任何值,除非是null
        store.setData(new Integer(10));

        // 任何参数化类型获取的值,都是Object类型或者其子类。
        Object obj = store.getData();
        // 从sStore获取数据,因为是泛型不需要强制转换。
        // 因为不能通过store设置值,所以sStore还是只能由它自身设置,
        // 因为有泛型约束,不会有强转问题。
        String s = sStore.getData();
    }

与原生态类型相比较,不允许向无限制通配符类型设置任何值。

  1. 无限制通配符类型可以接受任何参数化类型,如果可以给它设置值,例如这里store接受的是Store<String>的实例sStore,向store中设置了整型数据,那么从sStore获取数据时,就会有强转异常。所以就不允许设置值。
  2. 从无限制通配符类型获取值是可以的,但是获取值的类型只能是Object类型。

4.4 有限制通配符类型(bounded wildcard type)

无限制通配符类型虽然解决了泛型安全问题,但是有两个缺点:

  1. 不能设置值。
  2. 只能获取Object类型的值。

针对这个两个缺点,我们使用有限制通配符类型来解决。

4.4.1 有限制通配符super

为什么不能设置值,是因为无限制通配符类型可以接受任何参数化类型,所以我们没有办法设置任何类型,因为对于任何一个具体参数化类型,都有可能是错误的。

例如我们在setData设置的是Integer类型,但是store接收的参数化类型Store<String>,那么就产生了错误。

怎么解决这个问题呢?我们可以让store接收一定限制的参数化类型,而不是任何参数化类型。

可以让store接收实际类型参数是String以及String父类的参数化类型,即Store<String>、Store<CharSequence>、Store<Object>等等。
对于这些参数化类型,它们都可以设置String类型的数据,这是因为多态的原因。

    public static void main(String[] args)  {
        Store<String> sStore = new Store();
        // 使用有限制通配符 Store<? super String>来接收
        Store<? super String> store = sStore;

        // 可以向Store<? super String>中存储String以及String子类型的值
        store.setData("zhang");

        // 获取Object类型是可以的。因为Object本来就可以代表任何类型
        Object obj = store.getData();

        // 这里是错误的,获取String类型是不可以的。
        // 因为store可能是Store<CharSequence>参数化类型,
        // 获取的值就不一定是String类型。
        String str = store.getData();

        // 这里是正确的。
        String s = sStore.getData();
    }

与无限制通配符类型相比较:

  1. 对于Store<? super Sub>来说,它可以接收实际类型参数是Sub以及Sub父类的参数化类型,它可以设置Sub以及Sub子类的数据。
  2. 也是只能获取Object类型数据。这里可能会有疑惑,我们不是设置Sub以及Sub子类的数据,那么获取的数据应该也是Sub类型啊。那是因为我们不只是通过有限制通配符类型来设置数据啊。
    public static void main(String[] args)  {
        Store<CharSequence> cStore = new Store();
        // 使用有限制通配符 Store<? super String>来接收
        Store<? super String> store = cStore;

        // 可以向Store<? super String>中存储String以及String子类型的值
        store.setData("zhang");

        // 通过cStore设置StringBuilder类型
        cStore.setData(new StringBuilder("sb"));

        // 这里是错误的。store中存储的是StringBuilder类型,强转错误。
        String str = store.getData();

    }

在这个方法中,我们通过参数化类型cStore来设置值,导致类型错误。所以从super有限制通配符中获取数据类型也只能是Object类型,因此它只能解决设置值的问题。

4.4.2 有限制通配符extends

要想获取指定类型的数据。我们也要让store接收一定限制的参数化类型,而不是任何参数化类型。

可以让store接收实际类型参数是String以及String子类的参数化类型。那么我们就可以从中获取String类型的数据。

    public static void main(String[] args)  {
        Store<String> sStore = new Store();
        // 使用有限制通配符 Store<? extends String>来接收
        Store<? extends String> store = sStore;

        // 不可以向Store<? extends String>类型中储存任何值。除非是null
        store.setData("zhang");

        // 因为store接受的参数化类型的实际参数类型是String以及String子类。
        // 所以从中获取的数据一定可以用String类型接受
        String str = store.getData();

        // 这里是正确的。
        String s = sStore.getData();
    }

与无限制通配符类型相比较:

  1. 对于Store<? extends Sub>来说, 它可以接收实际类型参数是Sub以及Sub子类的参数化类型,因此它获取的数据一定可以用Sub类型接受。
  2. 不能向Store<? super Sub>中设置任何类型的数据。
    public static void main(String[] args)  {
        Store<Integer> iStore = new Store<>();
        // 将iStore赋值给store变量
        Store<? extends Number> store = iStore;

        // 假如这里可以设置Number类型的数据。
        store.setData(new Double(1.0));

        // 获取Number类型的数据是没问题的
        Number number = store.getData();

        // 这里就有问题了,因为我们存入的是Number类型,
        // 而获取的是Integer,就有可能出现错误
        Integer i = iStore.getData();
    }

如果可以设置数据,就会破坏泛型的约束,产生安全问题。

4.5 小结

无限制通配符类型与有限制通配符类型都是只能作为接收类型,而不能作为创建类型。只能创建含有实际类型参数的参数化类型泛型。

        // 错误,不能创建无限制通配符类型的泛型
        Store<?> store1 = new Store<?>();

        // 错误,不能创建有限制通配符类型的泛型
        Store<? extends Number> store2 = new Store<? extends Number>();

        // 错误,不能创建有限制通配符类型的泛型
        Store<? super Number> store3 = new Store<? super Number>();

        // 正确,创建参数化类型
        Store<Number> store4 = new Store<Number>();

        // 正确,创建参数化类型,省略实际类型参数
        Store<Number> store5 = new Store<>();
        // 正确,创建参数化类型,省略实际类型参数
        Store<Number> store6 = new Store();
  1. 无限制通配符类型Store<?>:可以接收任何参数化类型。为了保证泛型约束,所以不能向无限制通配符类型设置任何数据,从无限制通配符类型中只能获取到Object类型的数据。
  2. super有限制通配符类型Store<? super Sub>:只能接收实际类型参数是Sub以及Sub父类的参数化类型。所以可以向super有限制通配符类型中设置Sub以及Sub子类类型的数据。但是也只能从中获取Object类型数据。
  3. extends有限制通配符类型Store<? extends Sub>:只能接收实际类型参数是Sub以及Sub子类的参数化类型。所以从中获取数据一定可以被Sub类型接收。但是也不能向super有限制通配符类型设置任何数据。

总结就是:
Store<?>不能设置数据,但是能获取Object类型的数据。
Store<? super Sub>能设置Sub类型的数据,但是只能获取Object类型的数据。
Store<? extends Sub>不能设置数据,但是能获取Sub类型的数据。

五. 泛型方法

我们知道java中方法通常由四部分组成:1. 修饰符:public、protected、private或者没有修饰符。2. 方法返回值。3. 方法名。4. 方法参数。如果方法是静态方法,还要加上static关键字。
那么什么是泛型方法?就是在修饰符之后和方法返回值之前添加泛型声明。

public <T> Store<T> genericFun(Store<T> store) { }

泛型方法的作用是什么呢?假如我们有一个方法来复制Store变量。首先我们尝试不用泛型方法:

    public static void main(String[] args)  {

        Store<Integer> store = new Store<>();
        store.setData(100);
        
        Store<Integer> copy = copy(store);
        System.out.println(copy.getData());
    }

    private static Store<Integer> copy(Store<Integer> store) {
        Store<Integer> result = new Store<>();
        Integer t = store.getData();
        result.setData(t);
        return store;
    }

但是如果我们想复制Store<String> 变量,就会发现copy方法不能使用了。要使copy方法可以针对任何参数化类型,那么就必须使用泛型方法。

    public static void main(String[] args)  {
        Store<Integer> store = new Store<>();
        store.setData(100);

        Store<Integer> copy = copy(store);
        System.out.println(copy.getData());
    }


    private static <T> Store<T> copy(Store<T> store) {
        Store<T> result = new Store<>();
        T t = store.getData();
        result.setData(t);
        return store;
    }

现在我们有了新的需求,可以比较Store中储存数据的大小。

    public static void main(String[] args)  {
        Store<Integer> store1 = new Store<>();
        store1.setData(100);

        Store<Integer> store2 = new Store<>();
        store2.setData(200);

        Store<Integer> store = max(store1, store2);
        System.out.println(store.getData());
    }

    private static <T extends Comparable<T>> Store<T> max(
                Store<T> store1, Store<T> store2) {
        T t1 = store1.getData();
        T t2 = store2.getData();
        T t = t1.compareTo(t2) > 0 ? t1 : t2;

        Store<T> result = new Store<>();
        result.setData(t);
        return result;
    }

这里的类型参数变得不一样了,它不再是简简单单地一个字母T,而是T extends Comparable<T>。在泛型的声明中有下列三种格式:

  1. T:类型参数
  2. T extends Number: 有限制类型参数
  3. T extends Comparable<T>: 递归类型限制
    注:这个与有限制通配符类型是两个概念,这个是用在泛型声明中。还有它不支持super关键字。

如果我们声明泛型的类型参数是Comparable的子类型,那么就可以将它当成Comparable变量使用,就可以调用Comparable中的方法。

但是这种实现方式还是有点缺陷,参数和返回值都必须是同一个参数化类型,就如这里只能是Store<Integer>参数化类型, 那我们想比较Store<Integer>与Store<Double>大小应该怎么改动呢?
我们想到了上一节说的有限制通配符类型,对Store<T>来说,我们想从Store<T>中获取值,那么就使用extends有限制通配符类型Store<? extends T>。

 public static void main(String[] args)  {
        Store<Integer> store1 = new Store<>();
        store1.setData(100);

        Store<Double> store2 = new Store<>();
        store2.setData(200.0);

        // 注意:这里编译不通过,报错。
        /**
         * Integer和Double有共同的父类型Number,
         * 使用Store<? extends T>来接收这两个参数化类型是没问题的。
         * 那么这里报错的原因是什么呢?那是因为我们对T有限制,T必须是Comparable的子类型。
         * Integer和Double都实现了Comparable接口,是Comparable的子类型,
         * 但是它们的父类型Number没有实现Comparable接口,所以这里推导T是Number类型,
         * 但是又不满足泛型声明时T extends Comparable<T>条件,
         * 所以这下面句代码报错。除非它们两个父类型也是Comparable接口的子类型,才能通过。
         */
        Store<Number> store = max(store1, store2);
        System.out.println(store.getData());
    }

    private static <T extends Comparable<T>> Store<T> max(
            Store<? extends T> store1, Store<? extends T> store2) {
        // 从Store<? extends T>中获取值,没问题
        T t1 = store1.getData();
        T t2 = store2.getData();
        T t = t1.compareTo(t2) > 0 ? t1 : t2;

        Store<T> result = new Store<>();
        result.setData(t);
        return result;
    }

很遗憾,这里直接报错了。Integer和Double有共同的父类型Number,那么使用Store<? extends T>来接收这两个参数化类型是没问题的,T就是Number类型,但是Number类型不是Comparable接口子类型,所以它没有compareTo的方法,运行max方法就会产生错误。
所以这里max(store1, store2)代码直接报错,除非它们两个父类型也是Comparable接口的子类型,才能通过。

六. 总结

泛型分为两个部分:

6.1 泛型声明

  1. <T>: 类型参数
  2. <T extends Number>: 有限制类型参数
  3. <T extends Comparable<T>>: 递归类型限制
    泛型声明一般用在类、接口或者泛型方法上。

6.2 泛型实例

  1. 参数化类型:如Store<String>,其中String是它的实际类型参数。记住创建泛型实例只能创建参数化类型实例,也就是说创建泛型实例,必须要确定实际类型参数,否则是无法创建的。
  2. 原生态类型Store:它可以接收任何参数化类型,并且它能设置任何类型数据,还可以获取Object类型的数据。所以它不能保证泛型约束,运行时可能会抛出异常。
  3. 无限制通配符类型Store<?>:它可以接收任何参数化类型,但是不能设置数据,只能能获取Object类型的数据。与原生态类型相比较,它保证了泛型约束。
  4. super有限制通配符类型Store<? super Sub>:只能接收实际类型参数是Sub以及Sub父类的参数化类型。能设置Sub类型的数据,但是只能获取Object类型的数据。
  5. extends有限制通配符类型Store<? extends Sub>:能接收实际类型参数是Sub以及Sub子类的参数化类型。不能设置数据,但是能获取Sub类型的数据。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容

  • 泛型为集合提供了编译时类型检查。 23、不要在代码中使用原生态类型 声明中具有一个或多个类型参数的类或接口统称为泛...
    Alent阅读 911评论 6 2
  • object 变量可指向任何类的实例,这让你能够创建可对任何数据类型进程处理的类。然而,这种方法存在几个严重的问题...
    CarlDonitz阅读 906评论 0 5
  • 在之前的文章中分析过了多态,可以知道多态本身是一种泛化机制,它通过基类或者接口来设计,使程序拥有一定的灵活性,但是...
    _小二_阅读 676评论 0 0
  • 泛型是Java 1.5引入的新特性。泛型的本质是参数化类型,这种参数类型可以用在类、变量、接口和方法的创建中,分别...
    何时不晚阅读 3,023评论 0 2
  • 我还以为没有什么事情,可是,当我走出了地铁站,我的眼就泪掉下来了。 跟着她们三个人,我形如一个陌生人。 她们有说有...
    减肥的女孩阅读 231评论 0 0