Java筑基——泛型的点点滴滴

1. 泛型的作用

Java 中的泛型是在 Java 1.5以后引入的。它主要有两个作用:

  • 泛型通过使用参数化类型的概念,使代码可以应用于多种类型,可以理解“泛型”这个术语的意思是:“适用于许多许多的类型”,这其实就是提高了代码的可重用性。

    下面通过代码说明一下:
    这是PointInt.java 类,它存放两个 int 类型的变量:xy

    public class PointInt {
        private int x;
        private int y;
    
        public PointInt(int x, int y) {
            this.x = x;
            this.y = y;
        }
    
        public int getX() {
            return x;
        }
    
        public void setX(int x) {
            this.x = x;
        }
    
        public int getY() {
            return y;
        }
    
        public void setY(int y) {
            this.y = y;
        }
    }
    

    这里是 PointFloat.java 类,它存放了两个 float 类型的变量:xy

    public class PointFloat {
        private float x;
        private float y;
    
        public PointFloat(float x, float y) {
            this.x = x;
            this.y = y;
        }
    
        public float getX() {
            return x;
        }
    
        public void setX(float x) {
            this.x = x;
        }
    
        public float getY() {
            return y;
        }
    
        public void setY(float y) {
            this.y = y;
        }
    }
    

    可以看到,上面的两个类,除了变量类型一个是 int,一个是 float 之外,没有任何区别了。那么,我们可能会考虑使用一个参数形式的类型,代表可能传入的类型,如 intfloat 等,也就是说,在使用这一种参数形式的类型时,我们可以传入 intfloat,这种参数形式的类型就可以接收 intfloat 等。事实上,这是可行的,这就是参数化类型的概念。需要特别说明的是,方法中的形式参数的输入是值,而泛型的类型参数需要输入的是类型。

    代码如下:

    public class Point<T> {
        private T x;
        private T y;
    
        public Point(T x, T y) {
            this.x = x;
            this.y = y;
        }
    
        public T getX() {
            return x;
        }
    
        public void setX(T x) {
            this.x = x;
        }
    
        public T getY() {
            return y;
        }
    
        public void setY(T y) {
            this.y = y;
        }
    }
    

    下边是测试代码:

    public class Test {
        public static void main(String[] args) {
            Point<Integer> integerPoint = new Point<>(1, 2);
            System.out.println("x = " + integerPoint.getX() + ", y = " + integerPoint.getY());
            Point<Float> floatPoint = new Point<>(1f, 2f);
            System.out.println("x = " + floatPoint.getX() + ", y = " + floatPoint.getY());
        }
    }
    /*
     打印結果:
     x = 1, y = 2
     x = 1.0, y = 2.0
     */
    

    在上面的例子中,在类名 Point 之后,跟着一个尖括号(<>),尖括号里面的T就是类型参数(Type parameter)(也称为类型变量),这样我们就定义了一个泛型类。

    这里需要注意的一点是,传入<T> 内部,用来替换T的具体类型,不能是 intfloat,而应该是 IntegerFloat。这是因为 Java 泛型的一个局限性:基本类型无法作为类型参数。

    可以看到,新的Point.java 类完全可以替代原来的PointInt.javaPointFloat.java 两个类,这样我们使用一份代码,同时适用于 intfloat 两种类型。

  • 泛型通过在使用时指定实际的类型能够把运行期可能出现的类型转换异常,提前到编译期处理(这是因为编译器会执行额外的类型检查),起到了提前预防代码错误的作用;也避免了强制类型转换。
    这里我们通过 ArrayList 这个容器类的使用来说明:

    public class ArrayListNonGenericTest {
        public static void main(String[] args) {
            // 这是一个存放 String 数据的集合
            ArrayList nonGenericStringList = new ArrayList();
            nonGenericStringList.add("hello");
            nonGenericStringList.add("hi");
            // 一个无心的程序员却存入了 int 类型,但是编译器没有任何提示
            nonGenericStringList.add(1);
            
            // 另外一个程序员,看到这个 nonGenericStringList,知道这是一个
            // 存放 String 数据的集合,就想看看这里面都有哪些元素。
            for (int i = 0; i < nonGenericStringList.size(); i++) {
              String e = (String) nonGenericStringList.get(i);
                System.out.println("e = " + e);
            }
        }
    }
    /*
    打印结果:
    e = hello
    e = hi
    Exception in thread "main" java.lang.ClassCastException: java.base/java.lang.Integer cannot be cast to java.base/java.lang.String
            at com.java.advanced.features.generics.intro.ArrayListNonGenericTest.main(ArrayListNonGenericTest.java:17)
    */
    

    可以看到由于在大家认为是存放 String 类型的集合中,存入了 int 类型,而在遍历时取出元素时,需要强转成 String 类型时,发生了把 int 类型去转换成 String 类型的类型转换异常。这种错误通过使用泛型,是可以避免的。代码如下:

    public class ArrayListGenericTest {
        public static void main(String[] args) {
            // 这是一个存放 String 数据的集合
            ArrayList<String> genericStringList = new ArrayList();
            genericStringList.add("hello");
            genericStringList.add("hi");
            // 一个无心的程序员意图存入了 int 类型,但是编译器会报错:
            // add (java.lang.String) in ArrayList cannot be applied to (int)
            // 这就起到了提前预防的作用
            // genericStringList.add(1);
    
            // 另外一个程序员,看到这个 nonGenericStringList,知道这是一个
            // 存放 String 数据的集合,就想看看这里面都有哪些元素。
            for (int i = 0; i < genericStringList.size(); i++) {
                // 取出元素时,无需再做强转
    //            String e = (String) genericStringList.get(i);
                String e = genericStringList.get(i);
                System.out.println("e = " + e);
            }
        }
    }
    /*
    打印结果:
    e = hello
    e = hi
     */
    

    上面的代码有两点需要注意:第一,我们在定义集合时,就指定了这是一个存放 String 的集合,这样在存入意外的 int 类型时,编译器就马上提示出类型不匹配的错误;第二,在从集合中取出它持有的元素时,自动地就是正确的类型,即 String 类型,而无需再去手动做强转的操作了。

2. 泛型类

我们通过一个 Holder.java 的泛型类来说明一下:

class Food { }

class Fruit extends Food {}

class Apple extends Fruit {}

class Rice extends Food {}

public class Holder<T> {
    private T a;

    public Holder(T a) {
        this.a = a;
    }

    public T get() {
        return a;
    }

    public void set(T a) {
        this.a = a;
    }

    public static void main(String[] args) {
        Holder<Fruit> fruitHolder = new Holder<>(new Fruit());
        fruitHolder.set(new Fruit());
        fruitHolder.set(new Apple());
        // 编译报错:需要的是 Fruit 类型,但是传入的是 Food 类型
        // fruitHolder.set(new Food());
        // 编译报错:需要的是 Fruit 类型,但是传入的是 Rice 类型
        // fruitHolder.set(new Rice());
    }
}

上面的例子中一些类的继承关系如下图:

定义一个泛型类,就需要把类型参数,用尖括号括起来,放在类名的后面。可以看代码,这里我们是把类型参数 T,用尖括号<>括起来放在了类名 Holder 后面。

在使用泛型类时,使用实际的类型替换参数类型。在这里,我们使用 Fruit 这个实际的类型替换了参数类型 T。那么,在构造方法中我们可以传入哪些对象呢?我们只能传入实际的类型(或其子类)。具体到我们的例子,我们可以传入 Fruit 对象,还有 Fruit 的子类 Apple 对象(为什么还可以传入子类对象呢?这是因为多态与泛型不冲突)。

正如我们使用函数时的参数列表可以有多个一样,泛型类的类型参数也可以有多个,并且仍然是使用逗号隔开。这里我们举一个二元的元组的例子:

public class TwoTuple<A, B> {
    public final A first;
    public final B second;

    public TwoTuple(A first, B second) {
        this.first = first;
        this.second = second;
    }

    @Override
    public String toString() {
        return "(" +
                "first=" + first +
                ", second=" + second +
                ')';
    }
}

当然,对于三元元组,四元元组等,只需要添加更多的类型参数就可以了。

可能不少人对于类型参数的命名有点疑问,是不是只能用大写字母啊?有没有什么规范啊?
实际上,只要符合变量命名规范,都是合法的,也不限于单个字母。但是,这里面为了提高可读性,有一些约定的命名:

  • E,代表 Element,集合中的元素,常见在集合类中,举例:List<E>ArrayList<E>
  • K, V 代表 Key-Value 键值对,Map 中的键值对,举例:Map<K,V>
  • T,代表 Type,类型;
  • N,代表 Number,数字。

我们可以从源码里面看一下,大神们都是怎么用命名泛型变量的:

  • Android 中处理异步任务的 AsyncTask 类:
    public abstract class AsyncTask<Params, Progress, Result>
    
  • Retrofit2 中的 HttpServiceMethod 类:
    abstract class HttpServiceMethod<ResponseT, ReturnT> extends ServiceMethod<ReturnT>
    
  • Android 中的 Pair 类,用于表示一个 2 维元组:
    public class Pair<F, S>
    
  • RxJava2 中的 SingleOperator 类:
    public interface SingleOperator<Downstream, Upstream>
    

从这些源码中的泛型变量的命名中,可以得到结论:对于泛型类型参数的命名并非只能是单个的大写字母,只要表达的含义清楚明白就行。

3. 泛型接口

我们通过一个生成器接口来说明:

public interface Generator<T> {
    T next();
}

可以看到,接口使用泛型与类使用泛型没有什么区别。

在实现泛型接口时,有两种实现方式:

这里我们还是通过实现 Generator 接口来进行说明

  • 泛型类的方式

    class A {
        private static int count;
        private final int i = count++;
    
        @Override
        public String toString() {
            return "A " + i;
        }
    }
    class B {
        private static int count;
        private final int i = count++;
    
        @Override
        public String toString() {
            return "B " + i;
        }
    }
    
    public class GenericGeneratorImpl<T> implements Generator<T> {
        // 变量 type 是一个 Class 引用,它指向某个 Class 对象,比如 int.class, String.class 等
        private Class<T> type;
    
        public GenericGeneratorImpl(Class<T> type) {
            this.type = type;
        }
    
        @Override
        public T next() {
            try {
                // 注意如果 type 没有默认的构造器,那么此处调用它的 newInstance() 方法,会抛出异常。
                return type.newInstance();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    	// 这是一个泛型方法
        public static <T> Generator<T> create(Class<T> type) {
            return new GenericGeneratorImpl<>(type);
        }
    
        public static void main(String[] args) {
            Generator<A> generator = GenericGeneratorImpl.create(A.class);
            for (int i = 0; i < 5; i++) {
                System.out.println(generator.next());
            }
           Generator<B> bGenerator = GenericGeneratorImpl.create(B.class);
           for (int i = 0; i < 3; i++) {
                System.out.println(bGenerator.next());
           }
        }
        
    }
    
    /*
    打印结果:
    A 0
    A 1
    A 2
    A 3
    A 4
    B 0
    B 1
    B 2
     */
    

    上面的例子中,使用 GenericGeneratorImpl 泛型类实现了泛型接口Generator。在实例化 GenericGeneratorImpl 时,我们分别传递了类型实参为 AB

  • 非泛型类的方式

    public class NonGenericGeneratorImpl implements Generator<Integer> {
        private Random random = new Random(32);
        @Override
        public Integer next() {
            return random.nextInt(100);
        }
    
        public static void main(String[] args) {
            NonGenericGeneratorImpl nonGenericGenerator = new NonGenericGeneratorImpl();
            for (int i = 0; i < 5; i++) {
                System.out.println(nonGenericGenerator.next());
            }
        }
    }
    /*
    打印结果:
    77
    31
    85
    41
    39
     */
    

那么,泛型类的方式和非泛型类的方式有什么区别呢?或者在实际开发中该如何选用呢?
区别:泛型类的方式在创建类的实例时,必须需要指定实际的类型;而非泛型类的方式在创建类的实例时,和普通的类是一样的;
选择:泛型类的方式可以让调用者(也可以说是客户)来决定传入哪种实际的类型,而非泛型的方式则相当于是直接写死了,不给客户留有选择的余地。

4. 泛型方法

在类中包含的参数化方法,就是泛型方法。需要注意的是,泛型方法的标志是这个方法拥有自己定义的泛型参数。是不是泛型方法,与它所在的类是泛型类,还是非泛型类,没有任何关系。

public class GenericMethods {
    public <T> void f(T x) {
        System.out.println(x.getClass().getName());
    }

    public static void main(String[] args) {
        GenericMethods genericMethods = new GenericMethods();
        genericMethods.f("Hello");
        genericMethods.f(1);
        genericMethods.f(true);
        // 显式的类型说明,那么括号里只能传入声明的类型
        genericMethods.<Double>f(1.0D);
    }
}

定义泛型方法,就需要将泛型参数列表,即上面例子中的 <T> 放在返回值的前面。

从上面的例子可以看出:

泛型方法能够使得方法独立于类而产生变化,也就是说,泛型方法使用什么实际的类型与它所在的类没有任何关系。

使用泛型方法时,可以不必指定参数类型,因为编译器会推断出具体的类型,这就是类型参数推断(type argument inference)。而使用泛型类,则必须在创建对象的时候指定类型参数的值。这是一个区别的地方。

为什么说泛型方法能够使得方法独立于类而产生变化?这里通过在泛型类中定义的泛型方法来说明一下:

public class GenericClassGenericMethod<T> {
    private T x;

    public GenericClassGenericMethod(T x) {
        this.x = x;
    }
    // 这是个普通方法,只不是使用了泛型参数而已
    public T e(T t) {
        return t;
    }
    // 这是个泛型方法,因为它自己定义了泛型参数列表<T>
    // 请注意:这里的<T> 和 GenericClassGenericMethod 后面的<T> 没有任何关系
    // 但是泛型方法定义的泛型参数和所在类的泛型参数用一样的字母,是不好的写法。
    public <T> T f(T t) {
        return t;
    }
    // 这是个泛型方法,它自己定义了泛型参数列表<E>
    public <E> E g(E e) {
        return e;
    }
    // 这种写法是错误的,因为一个 static 的方法,无法访问泛型类的类型参数
//    public static T staticE(T t) {
//        return t;
//    }

    // 这是个静态泛型方法
    public static <F> F staticF(F f) {
        return f;
    }

    public static void main(String[] args) {
        GenericClassGenericMethod<String> genericClassGenericMethod = new GenericClassGenericMethod<>("Hello");
        // e() 方法是一个普通方法,它只允许使用创建 GenericClassGenericMethod 类时指定的类型参数的值:String
        System.out.println(genericClassGenericMethod.e("Hi"));
        // 编译报错:需要的是String类型,传入的是 int 类型。
        // System.out.println(genericClassGenericMethod.e(1));
        // f() 方法和 g() 方法是泛型方法,它们使用什么实际的类型来替换类型参数,由它们自己决定。
        System.out.println(genericClassGenericMethod.f(true));
        System.out.println(genericClassGenericMethod.<Boolean>f(false));
        System.out.println(genericClassGenericMethod.g("Hello"));
        System.out.println(genericClassGenericMethod.g(1f));
        System.out.println(genericClassGenericMethod.g(1.0d));
        // staticF() 是静态泛型方法
        System.out.println(GenericClassGenericMethod.staticF("staticF"));
    }
}
/*
打印结果:
Hi
true
false
Hello
1.0
1.0
staticF
 */

下面举一个泛型方法与可变参数列表结合的例子:

public class GenericMethodVarargs {
    public static <T> List<T> toList(T... args) {
        List<T> result = new ArrayList<>();
        for (T item : args) {
            result.add(item);
        }
        return result;
    }

    public static void main(String[] args) {
        System.out.println(GenericMethodVarargs.toList(1, 2, 3, 4));
        System.out.println(GenericMethodVarargs.toList("a", "b", "c", "d", "e"));
        // 在这里,可以体会显式的类型说明的作用
        // 本来,我们是传入一个都是 String 类型元素的可变参数列表,但是有一个不和谐的 int 类型:88,也被传入了,但是编译器没有提示出问题。
        System.out.println(GenericMethodVarargs.toList("h", "e", "l", "l", "o", 88));
        // 如果采用显式的类型说明,编译器就会给出出错的提示: 期望的是 String 类型元素的可变参数列表,但是实际上里面包含了 int 类型。
        // System.out.println(GenericMethodVarargs.<String>toList("h", "e", "l", "l", "o", 88));
    }
}
/*
打印结果:
[1, 2, 3, 4]
[a, b, c, d, e]
[h, e, l, l, o, 88]
 */

5. 泛型擦除

5.1 擦除是什么?

通过下面的代码来进行演示:

public class ErasedTypeEquivalence {
    public static void main(String[] args) {
        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.println("c1 = " + c1);
        System.out.println("c2 = " + c2);
        System.out.println("c1 == c2: " + (c1 == c2));
    }
}

/*
打印结果:
c1 = class java.util.ArrayList
c2 = class java.util.ArrayList
c1 == c2: true
 */

ArrayList<String> 是存放 String 类型的集合,ArrayList<Integer> 是存放 Integer 类型的集合。我们不能把一个 String 放入 ArrayList<Integer> 中,同样地,我们也不能把一个 Integer 放入 ArrayList<String> 中。

因此,我们有理由认为 ArrayList<String>ArrayList<Integer> 是不同的类型。

但是,程序的输出结果却表明它们是相同的类型。

查看一下对应的 ErasedTypeEquivalence.class 文件,可以看到尖括号不见了:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.java.advanced.features.generics.genericerase;

import java.util.ArrayList;

public class ErasedTypeEquivalence {
    public ErasedTypeEquivalence() {
    }

    public static void main(String[] var0) {
        Class var1 = (new ArrayList()).getClass();
        Class var2 = (new ArrayList()).getClass();
        System.out.println("c1 = " + var1);
        System.out.println("c2 = " + var2);
        System.out.println("c1 == c2: " + (var1 == var2));
    }
}

这是什么原因呢?

先留着这个问题,我们再看一个例子:

class Pair<First, Second> {
}

public class MissingInformation {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        Map<String, Integer> map = new HashMap<>();
        Pair<Integer, Integer> pair = new Pair<>();
        System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(pair.getClass().getTypeParameters()));
    }
}
/*
打印结果:
[E]
[K, V]
[First, Second]
 */

Class.getTypeParameters() 的作用是返回一个 TypeVariable 数组,表示由泛型声明所声明的类型参数。TypeVariable 是一个接口,它的实现类是 TypeVariableImpl。打印出的信息是通过 TypeVariableString getName(); 方法获取的。而 getName() 方法仅仅是返回用作参数占位符的标识符。其实,我们想要获取的是替换了类型参数的实际类型,如StringInteger

这又是什么原因呢?

这都是因为 Java 泛型使用擦除来实现,而擦除的作用就是当我们在使用泛型时,任何具体的类型信息都会被擦除,我们只能知道我们在使用一个对象。

回到 ErasedTypeEquivalence.java 的例子中,List<String>List<Integer> 在运行时实际上是相同的类型,因为它们都被会被擦除成它们的“原生”类型,即 List

到这里,或许有人会想:擦除了又能怎么样呢?或者这是 Java 的设计者们的精巧设计呢?这应该不会有什么影响吧?

我们还是通过一个例子来说明,看 EraseBadEffect.java

// 这是画笔类
class Paint {
    void draw() {
        System.out.println("Paint.draw() called");
    }
}
// 这是画家类,它是一个泛型类
class Painter<T> {
    private T t;

    public Painter(T t) {
        this.t = t;
    }
    // 画家开始工作
    public void work() {
        // 这里打算调用 Paint 类的 draw() 方法
        // 但实际上,编译器已经提示:Cannot resolve method 'draw()'
        t.draw();
    }

}

public class EraseBadEffect {
    public static void main(String[] args) {
        Paint paint = new Paint();
        Painter<Paint> painter = new Painter<>(paint);
        painter.work();
    }
}

正是因为擦除,Java 编译器没有办法将 work() 方法必须能够在 t上调用 draw() 这一需求映射到 Paint 拥有 draw() 方法这一事实上。

好了,我们已经看到擦除导致的问题,那么怎么解决呢?

这里需要初步引入边界的概念,通过给定泛型类的边界,这样就通知了编译器只能接受遵循这个边界的类型。

// 这是画笔类
class Paint {
    void draw() {
        System.out.println("Paint.draw() called");
    }
}

// 这是画家类,它是一个泛型类
class Painter<T extends Paint> {
    private T t;

    public Painter(T t) {
        this.t = t;
    }
    // 画家开始工作
    public void work() {
        // 这里打算调用 Paint 类的 draw() 方法
         t.draw();
    }

}

public class EraseBadEffectFixed {
    public static void main(String[] args) {
        Paint paint = new Paint();
        Painter<Paint> painter = new Painter<>(paint);
        painter.work();
    }
}

/*
打印结果:
Paint.draw() called
 */

可以看到,有了边界后,代码就可以正常编译运行了。
边界<T extends Paint>,声明 T 必须具有类型 Paint 或者从 Paint 导出(或者说派生)的类型。这里重用了 extends 关键字。

为了验证上面的话,这里我们用 javap -c Painter 反编译这个类对应的 class 文件,看一下:

Compiled from "EraseBadEffectFixed.java"
class com.java.advanced.features.generics.genericerase.fix.Painter<T extends com.java.advanced.features.generics.genericerase.fix.Paint> {
  public com.java.advanced.features.generics.genericerase.fix.Painter(T);
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: aload_1
       6: putfield      #2                  // Field t:Lcom/java/advanced/features/generics/genericerase/fix/Paint;
       9: return

  public void work();
    Code:
       0: aload_0
       1: getfield      #2                  // Field t:Lcom/java/advanced/features/generics/genericerase/fix/Paint;
       4: invokevirtual #3                  // Method com/java/advanced/features/generics/genericerase/fix/Paint.draw:()V
       7: return
}

可以看到,编译器确实把类型参数替换为了它的擦除。

既然擦除会带来一些问题,那么 Java 为什么还要采用擦除呢?

《Java 编程思想》中有很好的描述,等于说明了擦除的前世今生:

擦除是 Java 的泛型实现中的一种折中,泛型并不是 Java 1.0 开始就有的组成部分。如果在 Java 1.0 就有泛型的话,那么泛型将使用具体化,使类型参数保持为第一类实体,这样我们就能够在类型参数上执行基于类型的语言操作和反射操作。事实上, Java 1.5 才引入泛型,所以 Java 基于擦除实现了泛型:泛型类型被当作第二类类型来处理,即不能在某些重要的上下文环境中使用的类型。泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都会被擦除,替换为它们的非泛型上界。

擦除使得 Java 泛型能够支持向后兼容性,以及支持迁移兼容性。
擦除主要的正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入 Java 语言。

5.2 擦除时使用边界替换类型参数

对于泛型类或泛型接口以及泛型方法而言,如果类型参数没有边界,Java 编译器会用 Object 来替换类型参数;如果类型参数有边界(可能有多个边界),那么 Java 编译器会用第一个边界类来替换类型参数。

这里以泛型类来举例说明:

泛型类 Node<T> 的类型参数 T 没有边界:

public class Node<T> {
    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() {
        return data;
    }

    public static void main(String[] args) {

    }
}

类型擦除后,Java 编译器就用 Object 来替换类型参数:

public class Node {
    private Object data;
    private Node next;

    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Object getData() {
        return data;
    }
}

泛型类 Node<T extends Comparable> 的类型参数 T 有边界 Comparable

public class Node<T extends Comparable<T>> {
    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() {
        return data;
    }
}

类型擦除后,Java 编译器会用边界 Comparable 来替换类型参数 T:

public class Node {
    private Comparable data;
    private Node next;

    public Node(Comparable data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Comparable getData() {
        return data;
    }
}

5.3 擦除会插入类型转换来保持类型安全

边界:对象进入和离开方法的地点。这些正是编译器在编译器执行类型检查和插入转型代码的地点。

这里我们通过一个简单的非泛型容器类和泛型容器类作为对比来说明上述的概念。

SimpleHolder.java 是一个非泛型容器类:

public class SimpleHolder {
    private Object obj;

    public Object get() {
        return obj;
    }

    public void set(Object obj) {
        this.obj = obj;
    }

    public static void main(String[] args) {
        SimpleHolder holder = new SimpleHolder();
        holder.set("Hello");
        String str = (String) holder.get();
    }
}

通过 javap -c SimpleHolder 反编译对应的 .class 文件,

Compiled from "SimpleHolder.java"
public class com.java.advanced.features.generics.genericerase.bound.SimpleHolder {
  public com.java.advanced.features.generics.genericerase.bound.SimpleHolder();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public java.lang.Object get();
    Code:
       0: aload_0
       1: getfield      #2                  // Field obj:Ljava/lang/Object;
       4: areturn

  public void set(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field obj:Ljava/lang/Object;
       5: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class com/java/advanced/features/generics/genericerase/bound/SimpleHolder
       3: dup
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #5                  // String Hello
      11: invokevirtual #6                  // Method set:(Ljava/lang/Object;)V
      14: aload_1
      15: invokevirtual #7                  // Method get:()Ljava/lang/Object;
      18: checkcast     #8                  // class java/lang/String
      21: astore_2
      22: return
}

可以看到 set() 方法直接存储值,get() 方法被调用时会直接返回值,在 18:位置可以看到在调用 get() 的时候进行了 checkcast (转型)。

再看一下泛型容器类:GenericHolder.java

public class GenericHolder<T> {
    private T obj;

    public T get() {
        return obj;
    }

    public void set(T obj) {
        this.obj = obj;
    }

    public static void main(String[] args) {
        GenericHolder<String> holder = new GenericHolder<>();
        holder.set("Hello");
        String str = holder.get();
    }
}

同样地,查看对应的反编译代码:

Compiled from "GenericHolder.java"
public class com.java.advanced.features.generics.genericerase.bound.GenericHolder<T> {
  public com.java.advanced.features.generics.genericerase.bound.GenericHolder();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public T get();
    Code:
       0: aload_0
       1: getfield      #2                  // Field obj:Ljava/lang/Object;
       4: areturn

  public void set(T);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field obj:Ljava/lang/Object;
       5: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class com/java/advanced/features/generics/genericerase/bound/GenericHolder
       3: dup
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #5                  // String Hello
      11: invokevirtual #6                  // Method set:(Ljava/lang/Object;)V
      14: aload_1
      15: invokevirtual #7                  // Method get:()Ljava/lang/Object;
      18: checkcast     #8                  // class java/lang/String
      21: astore_2
      22: return
}

可以看到,对进入 set() 的类型没有进行检查,因为这是由编译器完成的;我们看一下 18:的位置,在获取 get() 方法的值之后仍然要转型。

这与之前的 SimpleHolder.java 的区别是:这里的转型是由编译器自动插入的,而SimpleHolder 中的转型是手写的。

5.4 擦除会生成桥接方法来保持继承的泛型类型里的多态

看下边的例子:

class Node<T> {
    public T data;

    public Node(T data) {
        this.data = data;
    }

    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

class MyNode extends Node<Integer> {

    public MyNode(Integer data) {
        super(data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

在类型擦除后,会变成下边这样:

class Node {
    public Object data;

    public Node(Object data) {
        this.data = data;
    }

    public void setData(Object data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

class MyNode extends Node {

    public MyNode(Integer data) {
        super(data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

可以看到,MyNode 类中的 setData(Integer data) 方法没有覆盖父类 Node 中的 setData(Object data) 方法,也就打破了多态。

为了解决这个问题,编译器会生成一个合成的方法(synthetic method),也叫桥接方法(bridge method)。

class MyNode extends Node {

    public MyNode(Integer data) {
        super(data);
    }
    // 编译器生成的桥接方法
    public void setData(Object data) {
        setData((Integer) data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

使用 javap -v MyNode.class 查看:

PS H:\IdeaProjects\Java_01_AdvancedFeatures\out\production\Java_01_AdvancedFeatures\com\java\advanced\features\generics\tutorial\t08_type_erasure\_03_effects_of_type_erasure_and_bridge_methods\test1> javap -v MyNode.class
Classfile /H:/IdeaProjects/Java_01_AdvancedFeatures/out/production/Java_01_AdvancedFeatures/com/java/advanced/features/generics/tutorial/t08_type_erasure/_03_effects_of_type_erasure_and_bridge_methods/test1/MyNode.class
  Last modified 2021-3-5; size 1183 bytes
  MD5 checksum 0f885ea2994e85ff117d0fe747cc53bb
  Compiled from "Test.java"
class com.java.advanced.features.generics.tutorial.t08_type_erasure._03_effects_of_type_erasure_and_bridge_methods.test1.MyNode extends com.java.advanced.features.generics.tutorial.t08_type_erasure._03_effects_of_type_erasure_and_bridge_methods.test1.Node<java.lang.Integer>
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #9.#25         // com/java/advanced/features/generics/tutorial/t08_type_erasure/_03_effects_of_type_erasure_and_bridge_methods/test1/Node."<init>":(Ljava/lang/Object;)V
   #2 = Fieldref           #26.#27        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #28            // MyNode.setData
   #4 = Methodref          #29.#30        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Methodref          #9.#31         // com/java/advanced/features/generics/tutorial/t08_type_erasure/_03_effects_of_type_erasure_and_bridge_methods/test1/Node.setData:(Ljava/lang/Object;)V
   #6 = Class              #32            // java/lang/Integer
   #7 = Methodref          #8.#33         // com/java/advanced/features/generics/tutorial/t08_type_erasure/_03_effects_of_type_erasure_and_bridge_methods/test1/MyNode.setData:(Ljava/lang/Integer;)V
   #8 = Class              #34            // com/java/advanced/features/generics/tutorial/t08_type_erasure/_03_effects_of_type_erasure_and_bridge_methods/test1/MyNode
   #9 = Class              #35            // com/java/advanced/features/generics/tutorial/t08_type_erasure/_03_effects_of_type_erasure_and_bridge_methods/test1/Node
  #10 = Utf8               <init>
  #11 = Utf8               (Ljava/lang/Integer;)V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               LocalVariableTable
  #15 = Utf8               this
  #16 = Utf8               Lcom/java/advanced/features/generics/tutorial/t08_type_erasure/_03_effects_of_type_erasure_and_bridge_methods/test1/MyNode;
  #17 = Utf8               data
  #18 = Utf8               Ljava/lang/Integer;
  #19 = Utf8               setData
  #20 = Utf8               (Ljava/lang/Object;)V
  #21 = Utf8               Signature
  #22 = Utf8               Lcom/java/advanced/features/generics/tutorial/t08_type_erasure/_03_effects_of_type_erasure_and_bridge_methods/test1/Node<Ljava/lang/Integer;>;
  #23 = Utf8               SourceFile
  #24 = Utf8               Test.java
  #25 = NameAndType        #10:#20        // "<init>":(Ljava/lang/Object;)V
  #26 = Class              #36            // java/lang/System
  #27 = NameAndType        #37:#38        // out:Ljava/io/PrintStream;
  #28 = Utf8               MyNode.setData
  #29 = Class              #39            // java/io/PrintStream
  #30 = NameAndType        #40:#41        // println:(Ljava/lang/String;)V
  #31 = NameAndType        #19:#20        // setData:(Ljava/lang/Object;)V
  #32 = Utf8               java/lang/Integer
  #33 = NameAndType        #19:#11        // setData:(Ljava/lang/Integer;)V
  #34 = Utf8               com/java/advanced/features/generics/tutorial/t08_type_erasure/_03_effects_of_type_erasure_and_bridge_methods/test1/MyNode
  #35 = Utf8               com/java/advanced/features/generics/tutorial/t08_type_erasure/_03_effects_of_type_erasure_and_bridge_methods/test1/Node
  #36 = Utf8               java/lang/System
  #37 = Utf8               out
  #38 = Utf8               Ljava/io/PrintStream;
  #39 = Utf8               java/io/PrintStream
  #40 = Utf8               println
  #41 = Utf8               (Ljava/lang/String;)V
{
  public com.java.advanced.features.generics.tutorial.t08_type_erasure._03_effects_of_type_erasure_and_bridge_methods.test1.MyNode(java.lang.Integer);
    descriptor: (Ljava/lang/Integer;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: invokespecial #1                  // Method com/java/advanced/features/generics/tutorial/t08_type_erasure/_03_effects_of_type_erasure_and_bridge_methods/test1/Node."<init>":(Ljava/lang/Object;)V
         5: return
      LineNumberTable:
        line 23: 0
        line 24: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/java/advanced/features/generics/tutorial/t08_type_erasure/_03_effects_of_type_erasure_and_bridge_methods/test1/MyNode;
            0       6     1  data   Ljava/lang/Integer;

  public void setData(java.lang.Integer);
    descriptor: (Ljava/lang/Integer;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String MyNode.setData
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: aload_0
         9: aload_1
        10: invokespecial #5                  // Method com/java/advanced/features/generics/tutorial/t08_type_erasure/_03_effects_of_type_erasure_and_bridge_methods/test1/Node.setData:(Ljava/lang/Object;)V
        13: return
      LineNumberTable:
        line 27: 0
        line 28: 8
        line 29: 13
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  this   Lcom/java/advanced/features/generics/tutorial/t08_type_erasure/_03_effects_of_type_erasure_and_bridge_methods/test1/MyNode;
            0      14     1  data   Ljava/lang/Integer;

  public void setData(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    **flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC**
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #6                  // class java/lang/Integer
         5: invokevirtual #7                  // Method setData:(Ljava/lang/Integer;)V
         8: return
      LineNumberTable:
        line 20: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/java/advanced/features/generics/tutorial/t08_type_erasure/_03_effects_of_type_erasure_and_bridge_methods/test1/MyNode;
}
Signature: #22                          // Lcom/java/advanced/features/generics/tutorial/t08_type_erasure/_03_effects_of_type_erasure_and_bridge_methods/test1/Node<Ljava/lang/Integer;>;
SourceFile: "Test.java"
PS H:\IdeaProjects\Java_01_AdvancedFeatures\out\production\Java_01_AdvancedFeatures\com\java\advanced\features\generics\tutorial\t08_type_erasure\_03_effects_of_type_erasure_and_bridge_methods\test1>

注意到 flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC 表示桥接,合成的标记,所在的方法就是桥接方法。

6. 边界

边界就是在泛型的参数类型上设置限制条件。这样可以强制规定泛型可以应用的类型,更重要的是可以按照自己的边界类型来调用方法。

对于无界泛型参数而言,只能调用属于 Object 类的方法;如果把泛型参数限制为某个类型的子集,就可调用这些类型子集的方法。

为了执行这种限制,Java 泛型重用了 extends 关键字。

语法是这样的:

  • <T extends Bound>Bound 可以类,也可以是接口;
  • <T extends InterfaceBound1 & InterfaceBound2...& InterfaceBoundn>InterfaceBound1InterfaceBound2,…,InterfaceBoundn 它们代表多个接口边界,多个边界使用 & 进行连接,顺序先后不会造成错误;
  • <T extends ClassBound & InterfaceBound1 & InterfaceBound2...& InterfaceBoundn>:多个边界中只能有一个边界是类,并且类边界必须放在最靠前的位置,后面跟着的是一系列的接口。就这一点来看,和类的继承有共通之处。

下面是一个演示例子:

interface Flyable {
    void fly();
}

class FlyPower<T extends Flyable> {
    T item;

    FlyPower(T item) {
        this.item = item;
    }

    void showFly() {
        item.fly();
    }
}

class Person {
    String name;
    int age;
}

class FlyPowerPerson<T extends Person & Flyable> {
    T item;

    FlyPowerPerson(T item) {
        this.item = item;
    }

    void showFly() {
        item.fly();
    }

    String getName() {
        return item.name;
    }

    int getAge() {
        return item.age;
    }
}

interface SuperHearing {
    void hearSubtleNoises();
}

interface SuperSmell {
    void trackBySmell();
}

interface SuperVision {
    void seeThroughWalls();
}

class SuperPowerPerson<T extends Person & Flyable & SuperHearing & SuperSmell & SuperVision> {
    T item;
    SuperPowerPerson(T item) {
        this.item = item;
    }

    void showFly() {
        item.fly();
    }

    String getName() {
        return item.name;
    }

    int getAge() {
        return item.age;
    }

    void showSuperSmell() {
        item.trackBySmell();
    }

    void showSuperHearing() {
        item.hearSubtleNoises();
    }

    void showSuperVision() {
        item.seeThroughWalls();
    }
}

class SuperMan extends Person implements Flyable, SuperVision, SuperHearing, SuperSmell {

    @Override
    public void fly() {
        System.out.println("fly");
    }

    @Override
    public void hearSubtleNoises() {
        System.out.println("hearSubtleNoises");
    }

    @Override
    public void trackBySmell() {
        System.out.println("trackBySmell");
    }

    @Override
    public void seeThroughWalls() {
        System.out.println("seeThroughWalls");
    }
}

public class BoundsDemo {
    public static void main(String[] args) {
        SuperMan superMan = new SuperMan();
        superMan.name = "SuperMan";
        superMan.age = 18;
        SuperPowerPerson<SuperMan> superPowerPerson =
                new SuperPowerPerson<>(superMan);
        System.out.println(superPowerPerson.getAge());
        System.out.println(superPowerPerson.getName());
        superPowerPerson.showFly();
        superPowerPerson.showSuperHearing();
        superPowerPerson.showSuperSmell();
        superPowerPerson.showSuperVision();
    }
}
/*
打印结果:
18
SuperMan
fly
hearSubtleNoises
trackBySmell
seeThroughWalls
 */

7. 通配符

我们从数组的协变性作为引入点。
首先,看一下下面这个类的继承结构图,因为本部分我们会多次用到这个类的继承结构。

package com.java.advanced.features.generics.genericerase.wildcard;

public class CovariantArrays {
    public static void main(String[] args) {
        Fruit[] fruits = new Apple[10];
        fruits[0] = new Apple();
        fruits[1] = new Hongfushi();
        try {
            fruits[0] = new Fruit();
        } catch (Exception e) {
            System.out.println(e);
        }
        try {
            fruits[0] = new Orange();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}

/*
打印结果:
java.lang.ArrayStoreException: com.java.advanced.features.generics.genericerase.wildcard.Fruit
java.lang.ArrayStoreException: com.java.advanced.features.generics.genericerase.wildcard.Orange
 */

知识点补充:什么是协变,逆变,不变?

逆变与协变用来描述类型转换后的继承关系。
定义 A,B 两个类型,A 是由 B 派生出来的子类(A ≤ B),f(·) 表示类型转换,如new List();
f(⋅) 是逆变(contravariant)的,当 A ≤ B 时有 f(B) ≤ f(A) 成立;
f(⋅) 是协变(covariant)的,当 A ≤ B 时有 f(A) ≤ f(B) 成立;
f(⋅) 是不变(invariant)的,当 A ≤ B 时上述两个式子均不成立,即 f(A) 与 f(B) 相互之间没有继承关系。

CovariantArrays 里的 Fruit[] fruits = new Apple[10]; 来看,代入上述的概念中:
f(X) = X[],令 AAppleBFruit,则有 Apple[] 可以赋值给 Fruit[],即 f(A) ≤ f(B) 成立,所以可以知道数组是协变的。

那么,如果我们用泛型容器来代替数组时,会是什么情况呢?

package com.java.advanced.features.generics.genericerase.wildcard;

import java.util.ArrayList;

public class NonConvariantGenerics {
    public static void main(String[] args) {
        // 编译错误:
//        Incompatible types.
//        Required:
//        ArrayList<com.java.advanced.features.generics.genericerase.wildcard.Fruit>
//        Found:
//        ArrayList<com.java.advanced.features.generics.genericerase.wildcard.Apple>

//        ArrayList<Fruit> fruitList = new ArrayList<Apple>();

        // 编译错误:
//        Incompatible types.
//        Required:
//        ArrayList<com.java.advanced.features.generics.genericerase.wildcard.Apple>
//        Found:
//        ArrayList<com.java.advanced.features.generics.genericerase.wildcard.Fruit>
        
//        ArrayList<Apple> appleList = new ArrayList<Fruit>();
    }
}

可以看到,令 f(X) = ArrayList(X)AAppleBFruit,当 A ≤ B 时,f(A) ≤ f(B) 不成立,f(B) ≤ f(A) 也不成立。所以,泛型是不变的。

对于 ArrayList<Fruit> fruitList = new ArrayList<Apple>(); 编译报错的理解:不能把一个涉及 Apple 的泛型赋值给一个涉及 Fruit 的泛型。AppleArrayList 可以持有 Apple 及其 Apple 的子类对象,而 FruitArrayList 可以持有 Fruit 及其 Fruit 的子类对象。AppleArrayListFruitArrayList 是两种不同的类型。

那么,有没有可能在这两种类型之间建立某种类型的向上转型关系呢?有的,这就是协变

7.1 上边界通配符,使 Java 泛型支持协变

协变是通配符所允许的。

package com.java.advanced.features.generics.genericerase.wildcard;

import java.util.ArrayList;

public class GenericsAndCovariance {
    public static void main(String[] args) {
        ArrayList<? extends Fruit> fruitList = new ArrayList<Apple>();
        // 修改操作
        // ===增加===
        // 不允许添加任何类型的对象
//        fruitList.add(new Apple());
//        fruitList.add(new Fruit());
//        fruitList.add(new Orange());
//        fruitList.add(new Object());
        // 可以添加 null,但是没有意义
        fruitList.add(null);
        // ===改变===
        // 不允许改变任何类型的对象
        // fruitList.set(0, new Apple());
        // ===删除===
        fruitList.remove(new Apple());
        fruitList.remove(new Fruit());
        // 获取操作
        Fruit fruit = fruitList.get(0);
    }
}

我们来看一下 ArrayList<? extends Fruit> fruitList = new ArrayList<Apple>(); 为什么是正确的?

f(A) = ArrayList(A)AAppleB 这时为 FruitA ≤ B ,而f(A) ≤ f(B)。因此,我们得出结论,通配符允许泛型支持协变,或者说,通配符允许子类的泛型类型属于泛型类型的子类。

<? extends Fruit> 这种写法就是声明了一个上边界通配符,其中 ? 就是通配符,extends 关键字在这里是被重用了,它的含义是上边界是类的话,它限定的类型可以是上边界类或者其子类(即类继承关系的 extends);上边界是接口的话,它限定的类型可以是上边界接口,子接口,或其实现类(即 implements 的意思)。在这里,Fruit 就是上界。

但是,上面的例子中,我们看到向 fruitList 中添加非 null 的元素都是不成功的,连添加 Apple 对象都不可以,这是为什么?

第一种解释方式(正向解释):

? extends Fruit 表示的是从 Fruit 继承的(包括 Fruit 本身)的某个未指定的具体类型,可以是 FruitAppleOrangeHongfushi。而现在给它一个确定的 Apple 对象,但是连它自己都不知道具体是哪个类型,编译器无法验证类型的安全性,所以无法添加。

第二种解释方式(反向解释):

看下面的示例代码:

public class GenericsAndCovariance {
    public static void main(String[] args) {
        List<Orange> orangeList = new ArrayList<>();
        addApple(orangeList);
        Orange orange = orangeList.get(0);
    }

    private static void addApple(List<? extends Fruit> fruits) {
        fruits.add(new Apple());
    }
}

假设上边的代码是正确的,也就是说,可以向 List<? extends Fruit> fruits 里面添加 Apple 对象。那么,Orange orange = orangeList.get(0); 这行从 orangeList 集合里取出的就是 Apple 对象。然后,把 Apple 对象赋值给 Orange 类型的变量,就会发生类型转换异常。所以说,不能向 List<? extends Fruit> fruits 里面添加 Apple 对象。

但是,Fruit fruit = fruitList.get(0); 这是安全的,这是因为多态,Fruit 作为父类,总可以指向它的子类对象。

总结一下:使用<? extends Bound> 的变量只能取其中的值,不可以修改。

这是一个使用泛型协变的例子:

package com.java.advanced.features.generics.genericerase.wildcard;

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

public class GenericsAndCovarianceUseCase {
    public static void main(String[] args) {
        List<Apple> appleList = new ArrayList<>();
        appleList.add(new Apple());
        appleList.add(new Apple());
        appleList.add(new Apple());
        traverseFruitList(appleList);
        List<Orange> orangeList = new ArrayList<>();
        orangeList.add(new Orange());
        orangeList.add(new Orange());
        traverseFruitList(orangeList);
    }

    public static void traverseFruitList(List<? extends Fruit> fruitList) {
        for (Fruit fruit : fruitList) {
            System.out.println(fruit);
        }
    }
}

7.2 下边界通配符,使 Java 泛型支持逆变

package com.java.advanced.features.generics.genericerase.wildcard;

import java.util.ArrayList;

public class GenericAndContravariance {
    public static void main(String[] args) {
        ArrayList<? super Apple> apples = new ArrayList<Fruit>();
        // 存
        apples.add(new Apple());
        apples.add(new Hongfushi());
        // 编译错误
        // apples.add(new Fruit());
        // 取
        Object object = apples.get(0);
    }
}

我们来看一下,ArrayList<? super Apple> apples = new ArrayList<Fruit>(); 为什么是逆变的?

f(X) = ArrayList(X)AAppleBFruitAB,有 f(B) ≤ f(A)。所以通配符允许泛型逆变。

<? super Apple> 这种写法就声明了一个下边界通配符super 在这里是被重用了,它的含义是下边界是类时,它限定的类型可以是类本身及其父类(或父接口);下边界是接口时,它限定的类型可以是接口本身及其父接口。

ArrayList<? super Apple> 表示是 Apple 的某种基类型的 ArrayList,也就是说,可能是 ArrayList<Apple>,也可能是 ArrayList<Fruit>,还可能是 ArrayList<Object>。因此,我们向 apples 里面添加 Apple 或者 Apple 的子类型对象一定是安全的,而向 apples 里面添加 Fruit 对象或者 Object 对象,就不能保证是安全的。

获取值的时候,会返回一个 Object 类型的值,而不能获取实际的类型参数代表的类型。那么为什么返回的是 Object 类型呢?这是因为 ? super Apple 表示 Apple 类及其父类的某种类型,这里返回 Object 类型一定是安全的。

总结一下:使用<? super Bound> 的变量只能存放值,不可以获取。

下面举一个使用泛型逆变的例子:

package com.java.advanced.features.generics.genericerase.wildcard;

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

public class GenericAndContravarianceUseCase {
    public static void main(String[] args) {
        List<Fruit> fruitList = new ArrayList<>();
        collectApple(fruitList, new Apple());
        System.out.println(fruitList);
        List<Apple> appleList = new ArrayList<>();
        collectApple(appleList, new Apple());
        collectApple(appleList, new Hongfushi());
        System.out.println(appleList);
    }

    public static <T> void collectApple(List<? super T> list, T item) {
        list.add(item);
    }
}

7.3 无边界通配符

List<?> 是使用了无边界通配符的泛型类型,意思是这个集合的元素类型可以匹配任何对象。

public class GenericsAndWildcard {
    public static void main(String[] args) {
        ArrayList<?> arrayList = new ArrayList<>();
        ArrayList<Apple> appleArrayList = new ArrayList<>();
        ArrayList<Fruit> fruitArrayList = new ArrayList<>();
        ArrayList<Object> objectArrayList = new ArrayList<>();
        ArrayList<String> stringArrayList = new ArrayList<>();

        arrayList = appleArrayList; // OK
        arrayList = fruitArrayList; // OK
        arrayList = objectArrayList; // OK
        arrayList = stringArrayList; // OK
    }
}

实际上,List<?> 等价于 List<? extends Object>,也就是说,这是上边界为 Object 的上边界通配符。因此,List<?> 类型的变量同样只能读取,不能修改。

public class GenericsAndWildcard {
    public static void main(String[] args) {
        ArrayList<?> arrayList = new ArrayList<>();
         // ====不可以添加===
//        arrayList.add(new Apple()); // 错误
//        arrayList.add(new Object()); // 错误
//        arrayList.add(""); // 错误
        // ====可以删除====
        arrayList.remove(""); // OK
        // ====获取元素====
        Object o = arrayList.get(0); // OK, 返回类型是 Object 类型。
    }
}

无边界通配符在两种场景下特别有用:

使用 Object 类提供的功能就可以实现的方法;

public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + " ");
    System.out.println();
}

泛型类中不依赖类型参数的方法,如 Class<?>

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement {
	public static Class<?> forName(String className)
            throws ClassNotFoundException {
    	Class<?> caller = Reflection.getCallerClass();
    	return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
	}
}

8 泛型的限制

泛型不能显式地引用在运行时类型的操作里,如转型,instanceof 操作和 new 表达式。

package com.java.advanced.features.generics.genericerase.cost;

public class EraseCost<T> {
    private final int SIZE = 10;

    public void f(Object arg) {
        // if (arg instanceof T) {} // 在 T 处编译报错:Class or array expected
        // T value = new T(); // 在第二个 T 处编译报错:Type parameter 'T' cannot be instantiated directly
        // T[] array = new T[SIZE]; // 在第二个 T 处编译报错:Type parameter 'T' cannot be instantiated directly
        T t = (T) arg; // 警告:Unchecked cast
    }
}

运行时类型查询只适用于原生类型(Raw Type)

这一点在 5.1 中已有例子说明。

不能创建泛型类型的数组,只可以声明一个泛型类型的数组引用

package com.java.advanced.features.generics.genericerase.cost;

class Generic<T> {}

public class ArrayOfGenericReference {
    static Generic<Integer>[] gia; // 这样是 ok 的
    // static Generic<Integer>[] array = new Generic<Integer>[10]; // 编译报错:Generic array creation
}

不能声明类型为类型参数的静态字段

public class MobileDevice<T> {
    // 编译报错
    // 'com.java.advanced.features.generics.tutorial.t09_restrictions_on_generics._03_cannot_declare_static_fields
    // _whose_types_are_type_parameters.MobileDevice.this' cannot be referenced from a static context
//     private static T os;

    public static void main(String[] args) {
        // 假定允许声明静态字段的类型为类型参数,那么因为 static 字段是被 android,ios,
        // harmonyOS 对象所共享的,不能确定类型参数的实参到底是哪一个。
        MobileDevice<Android> android = new MobileDevice<>();
        MobileDevice<Ios> ios = new MobileDevice<>();
        MobileDevice<HarmonyOS> harmonyOS = new MobileDevice<>();
    }
}

class Android {}

class Ios {}

class HarmonyOS {}

泛型类不可以直接或间接地继承 Throwable

// 泛型类不可以直接地或间接地继承 Throwable。
class MathException<T> extends  Throwable { // 编译报错:Generic class may not extend 'java.lang.Throwable'
}

class QueueFullException<T> extends Exception { // 编译报错:Generic class may not extend 'java.lang.Throwable'
}

方法中不可以捕获类型参数的实例,但是可以在 throws 语句中使用类型参数

public class Test {

    public static <T extends Exception, J> void execute(List<J> jobs) {
//        try {
//            for (J job : jobs) {
//
//            }
//        } catch (T e) { // 编译报错: Cannot catch type parameters
//            e.printStackTrace();
//        }
    }

    public static <T extends Exception> void canThrowsTypeParameter() throws T { // 正确的

    }
}

一个类不可以重载在类型擦除后有同样的方法签名的方法

public class Test {
    public void print(Set<String> list) { // 'print(Set<String>)' clashes with 'print(Set<Integer>)'; both methods have same erasure
    }

    public void print(Set<Integer> list) {
    }
}

9 泛型的几个问题

类型边界与通配符边界的区别是什么?

类型边界可以有多个,通配符边界只可以有一个。

List<?>List<Object> 一样吗?

不一样,List<?> 等价于 List<? extends Object>List<String>List<Apple>List<Object>等都是它的子类型;可以给 List<Object> 插入 Object 对象以及任何 Object 子类对象,但是只可以给 List<?> 插入 null 元素。

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

willwaywang6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值