Skip to content

右侧栏

泛型

Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。本文综合多篇文章后,总结了Java 泛型的相关知识,希望可以提升你对Java中泛型的认知效率。

自动装箱与拆箱

在Java开发中,自动装箱(Autoboxing)和自动拆箱(Unboxing)是指基本数据类型与其对应的包装类之间的自动转换。这些特性可以使代码更加简洁和易读,但在实际项目中也带来了某些潜在的问题。

自动装箱:Java编译器在需要时会自动将基本数据类型转换为对应的包装类。例如,将一个int赋值给Integer

java
int num = 10;
Integer boxedNum = num;//自动装箱

自动拆箱:Java编译器在需要时会自动将包装类转换为对应的基本数据类型。例如,将一个Integer赋值给int

java
Integer boxedNum  = 10;
int num  =  boxedNum; //自动拆箱

注意事项:

  1. 性能影响:频繁的自动装箱和拆箱操作会导致额外的对象创建,影响性能,特别是在循环中频繁使用时。

    java
    for (int i = 0; i < 1000; i++) {
        Integer boxedInt = i; // 每次循环都会创建一个新的Integer对象
    }
    //建议:在性能关键的代码中,尽量使用基本数据类型,避免频繁的自动装箱和拆箱
  2. 空指针异常:在自动拆箱时,如果包装类对象为null,会导致NullPointerException

    java
    Integer nullInteger = null;
    int value = nullInteger; // 这会抛出NullPointerException
    //建议:在进行拆箱之前,始终检查包装类对象是否为null,或使用Optional类来处理可能的null值。
  3. 相等性比较:使用==比较包装类对象时,比较的是对象的引用,而不是值。

    java
    Integer a = 128;
    Integer b = 128;
    System.out.println(a == b); // 输出false,因为128超出了-128到127的缓存范围
    
    int e = 123;
    Integer c = 123;
    Integer d = 123;      	
    System.out.println(e == c);//true
    System.out.println(c == d);//true 因为被自动缓存
  4. 整数缓存:Java会缓存一定范围内的整数值(通常是-128到127)。在这个范围内的装箱对象可能会被重用,而超出范围的值则不会。

    在Java中,基本数据类型的比较使用的是值比较,而对象的比较默认使用的是引用比较。但是,Java对于值从-128到127之间的Integer对象做了特殊处理,它们会被自动缓存,所以当你创建这个范围内的Integer对象时,实际上会返回相同的引用。

    java
    Integer x = 127;
    Integer y = 127;
    System.out.println(x == y); // 输出true,因为它们被缓存
    
    Integer m = 128;
    Integer n = 128;
    System.out.println(m == n); // 输出false,因为它们没有被缓存

泛型的基本使用

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

泛型类

  • 从一个简单的泛型类看起

    java
    package com.hongsipeng.generics;
    
    import lombok.Data;
    
    /**
     * @author hongsipeng@qq.com
     * @description 泛型测试类 基础泛型类
     * @since 2024/8/20 17:35
     */
    public class GenericsDemo {
        public static void main(String[] args) {
            Demo<String> stringDemo = new Demo<>();
            stringDemo.setVar("基础泛型类");
            System.out.println(stringDemo.getVar());
        }
    }
    
    @Data
    class Demo<T> { //次处可以随便写标识符号,T是type 的简称
        private T var; //var 类型由T指定,即由外部指定
    
        public T getVar() { //返回的值的类型由外部指定
            return var;
        }
    
        public Demo<T> setVar(T var) {//设置的类型也由外部指定
            this.var = var;
            return this;
        }
    }
  • 多元泛型

    java
    package com.hongsipeng.generics;
    
    /**
     * @author hongsipeng@qq.com
     * @description 泛型测试类,多元泛型
     * @since 2024/8/20 18:43
     */
    public class GenericsDemo1 {
        public static void main(String[] args) {
            Demo1<String, Integer> demo1 = new Demo1<>();//定义两个泛型类型的对象
            demo1.setKey("阿汤哥");
            demo1.setValue(65);
            System.out.print("姓名:" + demo1.getKey());      // 取得信息
            System.out.println(",年龄:" + demo1.getValue());       // 取得信息
        }
    }
    
    class Demo1<K, V> { // 此处指令的两个泛型类型
        private K key;// 此变量的类型由外部决定
        private V value;// 此变量的类型由外部决定
    
        public K getKey() {
            return key;
        }
    
        public V getValue() {
            return value;
        }
    
        public Demo1<K, V> setKey(K key) {
            this.key = key;
            return this;
        }
    
        public Demo1<K, V> setValue(V value) {
            this.value = value;
            return this;
        }
    }

泛型接口

  • 简单的泛型接口

    java
    package com.hongsipeng.generics;
    
    /**
     * @author hongsipeng@qq.com
     * @description 泛型接口
     * @since 2024/8/22 15:36
     */
    interface GenericsInterface<T> { // 在接口上定义泛型
        T get();// 定义抽象方法,抽象方法的返回值就是泛型类型
    }
    
    class Demo2 implements GenericsInterface<String> { // 定义泛型接口实现类,实现指定具体类型
        @Override
        public String get() {
            return "泛型接口测试";
        }
    }
    
    class Demo3<T> implements GenericsInterface<T> { // 定义泛型接口的实现类,实现不指定类型
        private T t; // 定义属性
        public Demo3(T t) {
            this.t = t;
        }
        @Override
        public T get() {
            return t;
        }
    }
    
    public class GenericsDemo2 {
        public static void main(String[] args) {
            GenericsInterface<String> demo2 = new Demo2();
            String s = demo2.get();
            System.out.println("s = " + s);
            GenericsInterface<Integer> demo3 = new Demo3<>(20); // 声明接口对象,通过子类实例化对象
            Integer i = demo3.get();
            System.out.println("i = " + i);
    
        }
    }

泛型方法

定义泛型方法时,必须在返回值前边加一个<T>,来声明这是一个泛型方法,持有一个泛型<T>,然后才可以用泛型 T 作为方法的返回值

泛型方法,是在调用方法的时候指明泛型的具体类型。重点看下泛型的方法

  • 泛型方法语法格式

    java
    package com.hongsipeng.generics;
    
    /**
     * @author hongsipeng@qq.com
     * @description 泛型方法测试
     * @since 2024/8/22 16:12
     */
    public class GenericsDemo3 {
    
        /**
         * 泛型方法
         *
         * @param c   泛型对应的class,用来创建泛型对象
         * @param <T> 申明一个泛型T
         * @return T 泛型T
         * @throws InstantiationException
         * @throws IllegalAccessException
         */
        public <T> T getObject(Class<T> c) throws InstantiationException, IllegalAccessException {
            T t = c.newInstance();
            return t;
        }
    
        public <T> String getObject1(Class<T> c) throws InstantiationException, IllegalAccessException {
            c.newInstance();
            return "null";
        }
    
        public static <T, K> K getObject2(Class<T> c) throws InstantiationException, IllegalAccessException {
            T instance = c.newInstance(); // 创建T类型的实例
            // 假设K可以是T的某种包装类型或转换类型
            return (K) instance; // 这只是一个假设的实现,需要根据实际情况返回K类型
        }
    
        public static void main(String[] args) throws InstantiationException, IllegalAccessException {
            //一个方法返回两个不同的类型,编译时并不会报错
            Integer object2 = getObject2(String.class);
            String s = getObject2(String.class);
        }
    }

    image-20240822163458178

  • 调用泛型方法语法格式

    java
    public static void main(String[] args) throws InstantiationException, IllegalAccessException {
    
            GenericsDemo3 genericsDemo3 = new GenericsDemo3();
            //调用泛型方法
            String string = genericsDemo3.getObject1(String.class);
    
        }
     }

    image-20240822163929459

    上述方法使用了反射类创建对象,为什么要是用泛型参数c来创建对象呢?既然是泛型方法,就代表着我们不知道具体的类型是什么,也不知道构造方法如何,因此没有办法去new一个对象,但可以利用参数c的newInstance方法去创建对象,也就是利用反射创建对象。

  • 为什么要使用泛型方法呢?

    因为泛型类要在实例化的时候就指明类型,如果想换一种类型,不得不重新new一次,可能不够灵活,而泛型方法可以在调用的时候指明类型,更加灵活。

泛型的上下限

  • 先看如下代码

    java
    package com.hongsipeng.generics;
    
    import java.util.List;
    
    /**
     * @author hongsipeng@qq.com
     * @description 泛型的上下限制
     * @since 2024/8/22 17:24
     */
    public class GeneticsDemo4 {
        class A{}
        class B extends A {}
    
        // 如下两个方法不会报错
        public static void funA(A a) {
            // ...
        }
        public static void funB(B b) {
            funA(b);
            // ...
        }
    
        // 如下funD方法会报错
        public static void funC(List<A> listA) {
            // ...
        }
        public static void funD(List<B> listB) {
            // funC(listB); // Unresolved compilation problem: The method doPrint(List<A>) in the type test is not applicable for the arguments (List<B>)
            // ...
        }
        public static void funC1(List<? extends A> listA) {
            // ...
        }
        public static void funD1(List<B> listB) {
            funC1(listB); // OK
            // ...
        }
    }

    为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制。<? extends A>表示该类型参数可以是A(上边界)或者A的子类类型。编译时擦除到类型A,即用A类型代替类型参数。这种方法可以解决开始遇到的问题,编译器知道类型参数的范围,如果传入的实例类型B是在这个范围内的话允许转换,这时只要一次类型转换就可以了,运行时会把对象当做A的实例看待。

  • 泛型上下限的引入

    java
    package com.hongsipeng.generics;
    
    /**
     * @author hongsipeng@qq.com
     * @description 泛型上下限
     * @since 2024/8/22 17:31
     */
    public class GenericsDemo5 {
        public static void main(String[] args) {
            // 声明Integer的泛型对象
            Info<Integer> i1 = new Info<>();
            // 使用其他类型编译会报错
            // Info<String> s1 = new Info<>();
    
            Info1<String> stringInfo1 = new Info1<>();
            stringInfo1.setVar("string 下限");
            fun(stringInfo1);
            Info1<Object> objectInfo1 = new Info1<>();
            objectInfo1.setVar("object 下限");
            fun(objectInfo1);
        }
    
        public static void fun(Info1<? super String> temp) {
            // 只能接收String或Object类型的泛型,String类的父类只有Object类
            System.out.print(temp + ", ");
        }
    }
    
    class Info<T extends Number> {// 此处泛型只能是数字类型
        private T var; // 定义泛型变量
        public void setVar(T var) {
            this.var = var;
        }
        public T getVar() {
            return this.var;
        }
        public String toString() {
            return this.var.toString();
        }
    }
    
    class Info1<T> {
        // 定义泛型变量
        private T var;
    
        public void setVar(T var) {
            this.var = var;
        }
    
        public T getVar() {
            return this.var;
        }
    
        public String toString() {    // 直接打印
            return this.var.toString();
        }
    }
    java
    <?> 无限制通配符
    <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
    <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类
    
    // 使用原则《Effictive Java》
    // 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限
    1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
    2. 如果它表示一个 T 的消费者,就使用 < ? super T>
    3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。
  • 再看一个实际例子,加深印象

    java
    private  <E extends Comparable<? super E>> E max(List<? extends E> e1) {
        if (e1 == null){
            return null;
        }
        //迭代器返回的元素属于 E 的某个子类型
        Iterator<? extends E> iterator = e1.iterator();
        E result = iterator.next();
        while (iterator.hasNext()){
            E next = iterator.next();
            if (next.compareTo(result) > 0){
                result = next;
            }
        }
        return result;
    }

    上述代码中的类型参数 E 的范围是<E extends Comparable<? super E>>,我们可以分步查看:

    • 要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)
    • Comparable< ? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super
    • 而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大

    多个限制使用&符号

    java
    public class Client {
        //工资低于2500元的上斑族并且站立的乘客车票打8折
        public static <T extends Staff & Passenger> void discount(T t){
            if(t.getSalary()<2500 && t.isStanding()){
                System.out.println("恭喜你!您的车票打八折!");
            }
        }
        public static void main(String[] args) {
            discount(new Me());
        }
    }

泛型数组

具体可以参考下文中关于泛型数组的理解

java
package com.hongsipeng.generics;


import java.util.ArrayList;
import java.util.List;

/**
 * @author hongsipeng@qq.com
 * @description 泛型数组测试
 * @since 2024/8/23 16:07
 */
public class GenericsDemo6 {
    ArrayList<String> strings = new ArrayList<>();
    ArrayList<?> s = new ArrayList<>();
    // List<String> list1 =  new ArrayList<String>[10]; //编译错误
    // 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,但是会有警告

    public static void main(String[] args) {
        // 返回泛型数组
        Integer[] integerArray = fun1(1, 2, 3, 4, 5, 6, 7, 8, 9);
        fun2(integerArray);
    }

    public static <T> T[] fun1(T... arg) {// 接收可变参数
        return arg;// 返回泛型数组
    }

    public static <T> void fun2(T param[]) {// 输出
        System.out.println("接受泛型数组");
        for (T e : param) {
            System.out.println(e + "");
        }
    }
    // /new ArrayList<String>

}

深入理解泛型

我们通过泛型背后的类型擦除以及相关问题来进一步理解泛型。

如何理解Java中的泛型是伪泛型?泛型中类型擦除

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

泛型擦除的原则是:

  • 消除类型参数声明,即删除 <> 及其包围的部分。
  • 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数最左边的限定类型(即父类)。
  • 为了保证类型安全,必要时会插入强制类型转换代码。
  • 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性**”**

那么泛型是如何进行擦除的呢?

  • 擦除类定义中的类型参数-无限制类型擦除

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

    image-20240823172111605

  • 擦书类定义中类型参数-有限制的类型参数

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

  • 擦除方法定义中的类型参数

    擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅已擦除方法定义中的有限制类型参数为例。image-20240823173259918

如何证明类型擦出呢?

通过两个例子证明Java类型的类型擦除

  • 原始类型相等

    java
    package com.hongsipeng.generics;
    
    import java.util.ArrayList;
    
    /**
     * @author hongsipeng@qq.com
     * @description 泛型擦除
     * @since 2024/8/23 17:45
     */
    public class GenericsCaChu {
        public static void main(String[] args) {
    
            ArrayList<String> stringArrayList = new ArrayList<>();
    
            ArrayList<Integer> integerArrayList = new ArrayList<>();
    
            stringArrayList.add("a");
            integerArrayList.add(1);
            System.out.println(stringArrayList.getClass());
            System.out.println(stringArrayList.getClass() == integerArrayList.getClass());//true
        }
    }

    在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList<String>泛型类型的,只能存储字符串;一个是ArrayList<Integer>泛型类型的,只能存储整数,最后,我们通过对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下原始类型(擦除了泛型信息,最后在字节码中的类型变量的真正类型)。

  • 通过反射添加其它类型元素

    java
     ArrayList<Integer> integerArrayList2 = new ArrayList<>();
            integerArrayList2.add(2); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
            integerArrayList2.getClass().getMethod("add", Object.class).invoke(integerArrayList2, "add");
    
            for (int i = 0; i < integerArrayList2.size(); i++) {
                System.out.println(integerArrayList2.get(i));
            }

    在程序中定义了一个ArrayList泛型类型实例化为Integer对象,如果直接调用add()方法,那么只能存储整数数据,不过当我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型。

如何理解泛型的编译期检查?

既然说类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?

java 编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,再进行编译

java
public static  void main(String[] args) {  

    ArrayList<String> list = new ArrayList<String>();  
    list.add("123");  
    list.add(123);//编译错误  
}

在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。

那么,这个类型检查是针对谁的呢?我们先看看参数化类型和原始类型的兼容。

以 ArrayList举例子,以前的写法:

以 ArrayList举例子,以前的写法:

java
ArrayList list = new ArrayList();

现在的写法:

java
ArrayList<String> list = new ArrayList<String>();

如果是与以前的代码兼容,各种引用传值之间,必然会出现如下的情况:

java
ArrayList<String> list1 = new ArrayList(); //第一种 情况
ArrayList list2 = new ArrayList<String>(); //第二种 情况

这样是没有错误的,不过会有个编译时警告。

不过在第一种情况,可以实现与完全使用泛型参数一样的效果,第二种则没有效果。因为类型检查就是编译时完成的,new ArrayList()只是在内存中开辟了一个存储空间,可以存储任何类型对象,而真正涉及类型检查的是它的引用,因为我们是使用它引用list1来调用它的方法,比如说调用add方法,所以list1引用能完成泛型类型的检查。而引用list2没有使用泛型,所以不行。

举例子:

java
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  
    }  
}

通过上面的例子,我们可以明白,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。

如何理解泛型的多态,泛型的桥接方法

类型擦除会造成多态的冲突,而JVM解决办法是桥接方法。

现在有这样一个泛型类:

java
class Pair<T> {  

    private T value;  

    public T getValue() {  
        return value;  
    }  

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

然后我们想要一个子类继承它。

java
class DateInter extends Pair<Date> {  

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

    @Override  
    public Date getValue() {  
        return super.getValue();  
    }  
}

在这个子类中,我们设定父类的泛型类型为Pair<Date>,在子类中,我们覆盖了父类的两个方法,我们的原意是这样的:将父类的泛型类型限定为Date,那么父类里面的两个方法的参数都为Date类型。

java
public Date getValue() {  
    return value;  
}  

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

所以,我们在子类中重写这两个方法一点问题也没有,实际上,从他们的@Override标签中也可以看到,一点问题也没有,实际上是这样的吗?

分析:实际上,类型擦除后,父类的的泛型类型全部变为了原始类型Object,所以父类编译之后会变成下面的样子:

java
class Pair {  
    private Object value;  

    public Object getValue() {  
        return value;  
    }  

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

再看子类的两个重写的方法的类型:

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

先来分析setValue方法,父类的类型是Object,而子类的类型是Date,参数类型不一样,这如果实在普通的继承关系中,根本就不会是重写,而是重载。 我们在一个main方法测试一下:

java
public static void main(String[] args) throws ClassNotFoundException {  
        DateInter dateInter = new DateInter();  
        dateInter.setValue(new Date());                  
        dateInter.setValue(new Object()); //编译错误  
}

如果是重载,那么子类中两个setValue方法,一个是参数Object类型,一个是Date类型,可是我们发现,根本就没有这样的一个子类继承自父类的Object类型参数的方法。所以说,却是是重写了,而不是重载了。

为什么会这样呢

原因是这样的,我们传入父类的泛型类型是Date,Pair<Date>,我们的本意是将泛型类变为如下:

java
class Pair {  
    private Date value;  
    public Date getValue() {  
        return value;  
    }  
    public void setValue(Date value) {  
        this.value = value;  
    }  
}

然后再子类中重写参数类型为Date的那两个方法,实现继承中的多态。

可是由于种种原因,虚拟机并不能将泛型类型变为Date,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。这样,类型擦除就和多态有了冲突。JVM知道你的本意吗?知道!!!可是它能直接实现吗,不能!!!如果真的不能的话,那我们怎么去重写我们想要的Date类型参数的方法啊。

于是JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。

首先,我们用javap -c className的方式反编译下DateInter子类的字节码,结果如下:

java
class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {  
  com.tao.test.DateInter();  
    Code:  
       0: aload_0  
       1: invokespecial #8                  // Method com/tao/test/Pair."<init>":()V  
       4: return  

  public void setValue(java.util.Date);  //我们重写的setValue方法  
    Code:  
       0: aload_0  
       1: aload_1  
       2: invokespecial #16                 // Method com/tao/test/Pair.setValue:(Ljava/lang/Object;)V  
       5: return  

  public java.util.Date getValue();    //我们重写的getValue方法  
    Code:  
       0: aload_0  
       1: invokespecial #23                 // Method com/tao/test/Pair.getValue:()Ljava/lang/Object;  
       4: checkcast     #26                 // class java/util/Date  
       7: areturn  

  public java.lang.Object getValue();     //编译时由编译器生成的桥方法  
    Code:  
       0: aload_0  
       1: invokevirtual #28                 // Method getValue:()Ljava/util/Date 去调用我们重写的getValue方法;  
       4: areturn  

  public void setValue(java.lang.Object);   //编译时由编译器生成的桥方法  
    Code:  
       0: aload_0  
       1: aload_1  
       2: checkcast     #26                 // class java/util/Date  
       5: invokevirtual #30                 // Method setValue:(Ljava/util/Date; 去调用我们重写的setValue方法)V  
       8: return  
}

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

所以,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突。

不过,要提到一点,这里面的setValue和getValue这两个桥方法的意义又有不同。

setValue方法是为了解决类型擦除与多态之间的冲突。

而getValue却有普遍的意义,怎么说呢,如果这是一个普通的继承关系:

那么父类的getValue方法如下:

java
public Object getValue() {  
    return super.getValue();  
}

而子类重写的方法是:

java
public Date getValue() {  
    return super.getValue();  
}

其实这在普通的类继承中也是普遍存在的重写,这就是协变。

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

如何理解基本类型不能作为泛型类型?

类型擦除后,原始类型变为Object,但是Object类型不能存储Int值,只能引用Integer的值。

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

我们先来看下Oracle官网提供的一个例子:

java
List<String>[] lsa = new List<String>[10]; // Not really allowed.
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 支持泛型数组初始化操作就是搬起石头砸自己的脚。

而对于下面的代码来说是成立的:

java
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

所以说采用通配符的方式初始化泛型数组是允许的,因为对于通配符的方式最后取出数据是要做显式类型转换的,符合预期逻辑。综述就是说Java 的泛型数组初始化时数组类型不能是具体的泛型类型,只能是通配符的形式,因为具体类型会导致可存入任意类型对象,在取出时会发生类型转换异常,会与泛型的设计思想冲突,而通配符形式本来就需要自己强转,符合预期。

Oracle 官方文档:https://docs.oracle.com/javase/tutorial/extra/generics/fineprint.html在新窗口打开

更进一步的,我们看看如下的代码:

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,但是会有警告

因为在 Java 中是不能创建一个确切的泛型类型的数组的,除非是采用通配符的方式且要做显式类型转换才可以。

本站访客数 人次 本站总访问量