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

2,102 阅读8分钟

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

1. 几个知识点

1.1 什么是泛型

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

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

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

1.2 几个注意点

  1. 泛型的类型参数类型不能为基本类型。 没有ArrayList,只有ArrayList。 因为当类型擦除后,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对应的原生态类型是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的区别
  1. List为原生态类型,逃避了类型检查,后续可能导致类型转换异常。
  2. List为泛型类型,允许插入任意的对象,但是存在类型检查(保证存放数据类型一致性)。
  3. 可以把任意的List赋值给List,但是不能赋值给List。
  4. List在遍历时会自动将数据转换为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()));
    
    

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

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

    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和List类型,在编译后都会变成List类型(原始类型)。 Java中的泛型基本上都是在编译器这个层次来实现的,这也是Java的泛型被称为“伪泛型”的原因。

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


    2. 通配符

    2.1 无限制通配符

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

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

    1. List<?>相当于是List的父类。
    
        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不是List的子类,虽然Integer是Number的子类。

    2. List<? extends Number>是List的父类,其中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是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中有着很重要的地位,如果能合理使用,能大大提高我们的编码效率,希望通过这篇博文,你能有所收获。