Java泛型-你可能需要知道这些

本博文为Java泛型扫盲文,争取读完后能理解泛型并使用泛型。

1. 几个知识点

1.1 什么是泛型

泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。

泛型的使用,有哪些好处呢?

  1. 类型安全,编译时就进行检查。
  2. 消除了强制类型转换。
  3. 提高性能。

1.2 几个注意点

  1. 泛型的类型参数类型不能为基本类型。
    没有ArrayList<double>,只有ArrayList<Double>。
    因为当类型擦除后,ArrayList的原始类中的类型变量(T)替换为Object,但Object类型不能存储double值。

  2. 泛型的类型参数可以有多个,使用,隔开。
    Node<T,E,V,K>

  3. 不能对确切的泛型类型使用instanceof操作。

如下面的操作是非法的,编译时会出错。

    if( arrayList instanceof ArrayList<String>){}//编译错误
    Pair<String> pair = new Pair<>();
    pair.setValue("123");
    println(pair.getValue());
    println(pair instanceof Pair<String>); // 编译错误
  1. 不能创建一个确切的泛型类型的数组。


    List<String>[] datas = new ArrayList<>[10]; // error

    List<String>[] datas = new ArrayList[10]; // OK

    List<?>[] ls = new ArrayList<?>[10]; // Ok


采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。

  1. 泛型在静态方法和静态类中的问题

泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数。

如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。

  class StaticGenerator<T> {
        
        public static T one;   //编译错误

        public static T print(T one) { //编译错误
            return null;
        }

        public static <E>  E print(E one){
            return one;
        }
    }

1.3 原生类型

每个泛型都定义了一个原生态类型(raw type),即不带任何实际参数的泛型名称。
例如: 与List<E>对应的原生态类型是List,原生态类型可以存放任何类型的Object。

原生态类型存在的问题:编译时不会检查加入集合的数据类型,这可能导致在后续读数据进行类型转换时出错。


        // arrayList为原生类型
        List arrayList = new ArrayList();
        // 我们可以随意的向其添加数据,编译时不会检查集合的数据类型
        arrayList.add("123");
        arrayList.add(1);


        for (int i = 0; i < arrayList.size(); i++) {
            // 此处抛出异常
            // java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
            String item = (String) arrayList.get(i);
            System.out.println("泛型测试 item = " + item);
        }


        List<String> strList = new ArrayList<String>();
        strList.add("123");
        // 因为arrayList为原生类型,我们可以把任意List<E>赋值给它
        arrayList = strList;


1.4 List与List<Object>的区别

  1. List为原生态类型,逃避了类型检查,后续可能导致类型转换异常。
  2. List<Object>为泛型类型,允许插入任意的对象,但是存在类型检查(保证存放数据类型一致性)。
  3. 可以把任意的List<E>赋值给List,但是不能赋值给List<Object>。
  4. List<Object>在遍历时会自动将数据转换为Object类型,无需显式的转换。
        List list = new ArrayList();

        // 我们可以向objList添加任意类型的数据
        List<Object> objList = new ArrayList<>();
        objList.add("123");
        objList.add(123);
        objList.add(5.0f);

        for (Object obj : objList) {
            println(obj.toString());
        }


        List<String> strList = new ArrayList<>();
        strList.add("1");
        strList.add("2");

        list = strList; // OK

        // error 存在类型检查,我们不能将List<E>赋值给List<Object>
        // 错误: 不兼容的类型: List<String>无法转换为List<Object>
        objList = strList;


1.5 类型擦除

首先来看个例子。


        List<String> strList = new ArrayList<String>();
        List<Integer> intList = new ArrayList<Integer>();

        println(strList.getClass().toString()); //  class java.util.ArrayList
        println(intList.getClass().toString()); //  class java.util.ArrayList
        println("strList==intList:" + (intList.getClass() == strList.getClass()));
        //strList==intList:true

我们再来看下编译后的代码:

        ArrayList strList = new ArrayList();
        ArrayList intList = new ArrayList();
        println(strList.getClass().toString());
        println(intList.getClass().toString());
        println("strList==intList:" + (intList.getClass() == strList.getClass()));

我们看到在编码时声明的<String>、<Integer>都被擦除掉了,最终存放数据的是对应的原生类型。

再看一个关于泛型类的例子:

class Pair<T> {
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

public static testGeneric(){
    Pair<String> pair = new Pair<>();
    pair.setValue("123");
    println(pair.getValue());
}

我们来看下对应编译后的代码:

class Pair<T> {
    private T value;

    Pair() {
    }

    public T getValue() {
        return this.value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

public static testGeneric(){
    // 泛型T被擦除了
    // T 属于无限制的泛型
    Pair pair = new Pair();
    pair.setValue("123");
    // getValue进行了强转,Pair实际存储的为Object
    println((String)pair.getValue());
}


得出以下结论:

  1. 在编译期间,所有的泛型信息都会被擦除,List<Integer>和List<String>类型,在编译后都会变成List类型(原始类型)。
    Java中的泛型基本上都是在编译器这个层次来实现的,这也是Java的泛型被称为“伪泛型”的原因。

  2. 原生类型就是泛型类型擦除了泛型信息后,在字节码中真正的类型。
    无论何时定义一个泛型类型,相应的原始类型都会被自动提供。
    原始类型的名字就是删去类型参数后的泛型类型的类名。
    擦除类型变量,并替换为限定类型(T为无限定的类型变量,用Object替换)。


2. 通配符

2.1 无限制通配符

如果要使用放心,且不确定或者不关心实际的类型参数,就可以使用一个问号代替。

我们不能将任何元素(除了null)放入Collection<?>,而且你也无法猜测你取出的数据是什么类型。

  1. List<?>相当于是List<E>的父类。

    static void showList(List<?> list) {
        for (Object obj : list) {
            println(obj.toString());
        }
    }


    public static void main(String[] args) {
        List<String> strList = new ArrayList();
        strList.add("123");
        strList.add("456");


        List<Integer> integerList = new ArrayList<>();
        integerList.add(789);


        List<Object> objectList = new ArrayList<>();
        objectList.add("123");
        objectList.add(456);

        showList(strList);
        showList(integerList);
        showList(objectList);

    }

  1. 我们不能往List<?>添加子元素,除非是null.
    List<?> objList = new ArrayList<>();
    objList.add(null);
    objList.add("123"); // compile error

理解一下这段代码,因为?为无限制通配符,我们也不知道objList到底存放的数据是什么类型,而且你也无法猜测你取出的数据是什么类型。

由于泛型类型检查的原因,我们无法向里面添加任何数据。

2.2 上限通配符

首先来看一个例子:


    static void showNumerList(List<Number> list) {
        for (Number obj : list) {
            println(obj.toString());
        }
    }

    static void showExtendList(List<? extends Number> list) {
        for (Number obj : list) {
            println(obj.toString());
        }
    }

    public static void main(String[] args) {
        List<Number> numList = new ArrayList<>();
        numList.add(1234);

        List<Integer> intList = new ArrayList<>();
        intList.add(123);
        intList.add(456);

        List<Double> doubleList = new ArrayList<>();
        doubleList.add(1.0);
        doubleList.add(2.0);

        showExtendList(numList);
        showExtendList(intList);
        showExtendList(doubleList);

        showNumerList(numList);
        showNumerList(intList); // Error 不兼容的类型: List<Integer>无法转换为List<Number>
        showNumerList(doubleList); //  Error 不兼容的类型: List<Double>无法转换为List<Number>

    }


由以上的代码分析可知:

  1. List<Integer>不是List<Number>的子类,虽然Integer是Number的子类。

  2. List<? extends Number>是List<E>的父类,其中E继承了Number(也包括Number)。

2.3 下限通配符


    static void showSuperList(List<? super Integer> list) {
        for (Object obj : list) {
            println(obj.toString());
        }
    }


    public static void main(String[] args) {
        List<Number> numList = new ArrayList<>();
        numList.add(1234);

        List<Integer> intList = new ArrayList<>();
        intList.add(123);
        intList.add(456);

        List<Double> doubleList = new ArrayList<>();
        doubleList.add(1.0);
        doubleList.add(2.0);

        showSuperList(numList);
        showSuperList(intList); 
        showSuperList(doubleList);// Error 不兼容的类型: List<Double>无法转换为List<? super Integer>

    }

由以上代码分析可知:

  1. ? super E:代表所有E继承的类。(也包括E)。

  2. List<? super E>是所有List<T>的父类,其中T是E的父类。


3. PECS

有限制的通配符:extends\super
? extends T:继承了T的某个类型。
? super T:T的某个超类类型。

为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用有限制的通配符。
如果某个参数既是生产者也是消费者,那么通配符对你就没有什么好处:因为你需要严格的类型匹配,这是不使用任何通配符得到的。

PECS:product-extends,consumer-super.

不要使用通配符作为返回类型。
如果类型参数在方法声明中出现过一次,就可以使用通配符取代它。

3.1 List<? extends E>

List<? extends E>只能遍历数据,而无法添加数据。

理解一下,继承E的子类有很多,但是我们不确定向list添加的元素的具体类型。

因此List<? extends E>只能读取,而不能添加。

3.2 ? super E

List<? super E>只能添加数据,而无法准确的遍历数据。

理解一下,E的父类有很多,我们可以只添加固定的E类型数据,但是读取的时候由于无法确定具体的类型,只能以Object形式读取。

3.3 总结

  1. 如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)

  2. 如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)

  3. 如果既要存又要取,那么就不要使用任何通配符。

4. 总结

泛型在Java中有着很重要的地位,如果能合理使用,能大大提高我们的编码效率,希望通过这篇博文,你能有所收获。

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

推荐阅读更多精彩内容

  • 泛型 泛型(Generic Type)简介 通常情况的类和函数,我们只需要使用具体的类型即可:要么是基本类型,要么...
    Tenderness4阅读 1,383评论 4 2
  • tags:泛型 categories:笔记 date: 2017-06-10 11:24:24 在java代码开发...
    行径行阅读 1,617评论 0 1
  • 为什么需要泛型? 通过泛型可以定义类型安全的数据结构,而无须使用实际的数据类型(可扩展)。这能够显著提高性能并得到...
    一只好奇的茂阅读 1,231评论 2 39
  • 锐普的PPT杀手训练营基础课程为我们学习PPT提供了一个框架,可作为PPT入门学习者学习参考。纵观市面上大部分的P...
    小成大数阅读 3,208评论 1 112
  • 废话不说,干货如下,摘抄了18点对我个人写作有价值的的话,大家各取所需。当然,书里有更多更详细的经验,希望大家去看...
    筑牙阅读 2,296评论 3 25