Java泛型

3/22/2022 泛型

# 通配符

  1. <?> 无限制通配符

  2. <? extends E>extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类

  3. <? super E>super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类

  4. 使用原则:

    • 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限

    • 如果参数化类型表示一个 T 的生产者,使用 <?extendsT>;

    • 如果它表示一个 T 的消费者,就使用 <?superT>;

    • 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。

# 泛型的类型擦除

Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。理解类型擦除对于用好泛型是很有帮助的,尤其是一些看起来“疑难杂症”的问题,弄明白了类型擦除也就迎刃而解了。

  1. 擦除类定义中的类型参数 - 无限制类型擦除:当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如<T>和<?>的类型参数都被替换为Object。

img

  1. 擦除类定义中的类型参数 - 有限制类型擦除:当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object。

img

  1. 擦除方法定义中的类型参数:擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义中的有限制类型参数为例。

img

# 泛型方法的调用

  1. 在调用泛型方法时,可以指定泛型,也可以不指定泛型:

    • 在不指定泛型的情况下,泛型变量的类型为该方法中的几种类型的同一父类的最小级,直到Object。

    • 在指定泛型的情况下,该方法的几种类型必须是该泛型的实例的类型或者其子类。

  2. 实例分析:

public class Test {  
    public static void main(String[] args) {  

        /**不指定泛型的时候*/  
        int i = Test.add(1, 2); //这两个参数都是Integer,所以T为Integer类型  
        Number f = Test.add(1, 1.2); //这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Number  
        Object o = Test.add(1, "asd"); //这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Object  

        /**指定泛型的时候*/  
        int a = Test.\<Integer\>add(1, 2); //指定了Integer,所以只能为Integer类型或者其子类  
        int b = Test.\<Integer\>add(1, 2.2); //编译错误,指定了Integer,不能为Float  
        Number c = Test.\<Number\>add(1, 2.2); //指定为Number,所以可以为Integer和Float  
    }  

    //这是一个简单的泛型方法  
    public static \<T\> T add(T x,T y){  
        return y;  
    }  
}

# 泛型的编译期检查

  1. 为了让泛型与以前的旧代码兼容,各种引用传值之间,以下的赋值和转换都是被允许,只是会出现警告。
ArrayList\<String\> list1 = new ArrayList(); //第一种 情况
ArrayList list2 = new ArrayList\<String\>(); //第二种 情况
  1. 类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象

  2. 实例分析

public class Test {  

    public static void main(String[] args) {  

        ArrayList\<String\> list1 = new ArrayList();  
        list1.add("1"); //编译通过  
        list1.add(1); //编译错误  
        String str1 = list1.get(0); //返回类型就是String  

        ArrayList list2 = new ArrayList\<String\>();  
        list2.add("1"); //编译通过  
        list2.add(1); //编译通过  
        Object object = list2.get(0); //返回类型就是Object  

        new ArrayList\<String\>().add("11"); //编译通过  
        new ArrayList\<String\>().add(22); //编译错误  

        String str2 = new ArrayList\<String\>().get(0); //返回类型就是String  
    }  
} 

# 引用传递时泛型中的参数类型不适用继承关系

  1. 首先来看下面的两种引用传递形式:这两种写法都是错误,会发生编译错误。
ArrayList\<String\> list1 = new ArrayList\<Object\>(); //编译错误  
ArrayList\<Object\> list2 = new ArrayList\<String\>(); //编译错误
  1. 第一种写法的另一种形式:
ArrayList\<Object\> list1 = new ArrayList\<Object\>();  
list1.add(new Object());  
list1.add(new Object());  
ArrayList\<String\> list2 = list1; //编译错误

分析:我们先假设它编译没错。那么当我们使用list2引用用get()方法取值的时候,返回的都是String类型的对象(上面提到了,类型检测是根据引用来决定的),可是它里面实际上已经被我们存放了Object类型的对象,这样就会有ClassCastException了。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。(这也是泛型出现的原因,就是为了解决类型转换的问题,我们不能违背它的初衷)。

  1. 第二种写法的另一种形式:
ArrayList\<String\> list1 = new ArrayList\<String\>();  
list1.add(new String());  
list1.add(new String());

ArrayList\<Object\> list2 = list1; //编译错误

分析:这样的情况比第一种情况好的多,最起码,在我们用list2取值的时候不会出现ClassCastException,因为是从String转换为Object。可是,这样做有什么意义呢,泛型出现的原因,就是为了解决类型转换的问题。我们使用了泛型,到头来,还是要自己强转,违背了泛型设计的初衷。所以java不允许这么干。再说,你如果又用list2往里面add()新的对象,那么到时候取得时候,我怎么知道我取出来的到底是String类型的,还是Object类型的呢?

# 泛型的多态与桥接法

  1. 泛型类:
class Pair\<T\> {  

    private T value;  

    public T getValue() {  
        return value;  
    }  

    public void setValue(T value) {  
        this.value = value;  
    }  
}
  1. 子类:
class DateInter extends Pair\<Date\> {  

    @Override  
    public void setValue(Date value) {  
        super.setValue(value);  
    }  

    @Override  
    public Date getValue() {  
        return super.getValue();  
    }  
}
  1. 实际上,类型擦除后,父类的的泛型类型全部变为了原始类型Object,所以父类编译之后会变成下面的样子:
class Pair {  
    private Object value;  

    public Object getValue() {  
        return value;  
    }  

    public void setValue(Object  value) {  
        this.value = value;  
    }  
} 
  1. 那么我们可能就会想,父类的类型是Object,而子类的类型是Date,参数类型不一样,这如果实在普通的继承关系中,根本就不会是重写,而是重载。 我们使用下面的代码来测试一下:
public static void main(String[] args) throws ClassNotFoundException {  
        DateInter dateInter = new DateInter();  
        dateInter.setValue(new Date());                  
        dateInter.setValue(new Object()); //编译错误  
}
  1. 如果是重载,那么子类中两个setValue方法,一个是参数Object类型,一个是Date类型,可是我们发现,根本就没有这样的一个子类继承自父类的Object类型参数的方法。所以说,却是是重写了,而不是重载了

  2. 为什么会这样呢?这似乎与我们之前介绍的类型擦除相冲突了。实际上,JVM使用了一种桥方法来解决这个问题。

  3. 从编译的结果来看,我们本意重写setValue和getValue方法的子类,竟然有4个方法,其实不用惊奇,最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。

  4. 并且,还有一点也许会有疑问,子类中的巧方法Object getValue()和Date getValue()是同时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别。%

# 泛型类型不能实例化

  1. 不能实例化泛型类型, 这本质上是由于类型擦除决定的:

  2. 我们可以看到如下代码会在编译器中报错:T test = new T(); // ERROR

  3. 因为在 Java 编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了,此外由于T 被擦除为 Object,如果可以 new T() 则就变成了 new Object(),失去了本意。

  4. 如果我们确实需要实例化一个泛型,应该如何做呢?可以通过反射实现

# 泛型数组不能采用具体的泛型类型进行初始化

  1. Java 的泛型数组初始化时数组类型不能是具体的泛型类型,只能是通配符的形式,因为具体类型会导致可存入任意类型对象,在取出时会发生类型转换异常,会与泛型的设计思想冲突,而通配符形式本来就需要自己强转,符合预期

  2. 如,以下的代码将是被禁止的:

List\<String\>[] lsa = new List\<String\>[10]; // 实际上不被允许,这里假设不会报编译期错误
Object o = lsa;
Object[] oa = (Object[]) o;
List\<Integer\> li = new ArrayList\<Integer\>();
li.add(new Integer(3));
oa[1] = li; // Unsound, but passes run time store check
String s = lsa[1].get(0); // Run-time error ClassCastException.

分析:由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。

  1. 以下的代码是成立的,即使用通配符进行泛型数组的初始化:
List\<?\>[] lsa = new List\<?\>[10]; // OK, array of unbounded wildcard type.
Object o = lsa;
Object[] oa = (Object[]) o;
List\<Integer\> li = new ArrayList\<Integer\>();
li.add(new Integer(3));
oa[1] = li; // Correct.
Integer i = (Integer) lsa[1].get(0); // OK
  1. 因为在 Java 中是不能创建一个确切的泛型类型的数组的,除非是采用通配符的方式且要做显式类型转换才可以。具体可以看以下的情况,这些问题经常被程序员忽略,进而导致代码报错。
List\<String\>[] list11 = new ArrayList\<String\>[10]; //编译错误,非法创建 
List\<String\>[] list12 = new ArrayList\<?\>[10]; //编译错误,需要强转类型 
List\<String\>[] list13 = (List\<String\>[]) new ArrayList\<?\>[10]; //OK,但是会有警告 
List\<?\>[] list14 = new ArrayList\<String\>[10]; //编译错误,非法创建 
List\<?\>[] list15 = new ArrayList\<?\>[10]; //OK 
List\<String\>[] list6 = new ArrayList[10]; //OK,但是会有警告

# 泛型类中的静态方法和静态变量

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

  2. 因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。如下代码,会在编译期报错:

public class Test2\<T\> {    
    public static T one;   //编译错误    
    public static  T show(T one){ //编译错误    
        return null;    
    }    
}
  1. 但是,对于静态方法,与上述方法不同的另一种静态方法是不会报错的。那就是泛型方法,尽管这是一个静态方法,但是在泛型方法中使用的T是自己在方法中定义的 T,而不是泛型类中的T。
public class Test2\<T\> {    

    public static \<T \>T show(T one){ //这是正确的    
        return null;    
    }    
}

# 参考

Java 基础 - 泛型机制详解 | Java 全栈知识体系 (opens new window)

Last Updated: 4/5/2022, 11:45:16 AM