superleeyom / blog

:bookmark: 个人博客仓库,用于记录一些幼稚的想法和脑残的瞬间,欢迎 star、watch,该仓库为个人博客,请不要提 issue ,该仓库后端参考了 @yihong0618 的 gitblog 项目,前端参考了@LoeiFy 的 Mirror 项目,感谢!
https://blog.leeyom.top
220 stars 20 forks source link

Java泛型的回顾之旅 #1

Open superleeyom opened 4 years ago

superleeyom commented 4 years ago

“泛型”的字面意思就是广泛的类型。类、接口和方法代码可以应用于非常广泛的类型,代码与它们能够操作的数据类型不再绑定在一起,同一套代码可以用于多种数据类型,这样,不仅可以复用代码,降低耦合,而且可以提高代码的可读性和安全性。在实际开发中,经常会使用到泛型,但语法非常令人费解,而且容易混淆对于其中的一些细节的地方,所以翻书去回顾下。

泛型类

首先看一段简单代码:

public class Pair<T> {
    T first;
    T second;

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public T getSecond() {
        return second;
    }
}

这就是一个简单的泛型类,T 表示类型参数,泛型就是类型参数化,处理的数据类型不是固定的,而是可以作为参数传入。如下代码所示,对于构造方法 Pair(T first, T second) 既可以传 Integer 类型的参数,也可以传 String 类型的参数:

Pair<Integer> minmax = new Pair<Integer>(1,100);
Pair<String> kv = new Pair<String>("name", "老王");

参数类型也可以多种,类 Pair 可以改成如下所示:

public class Pair<U, V> {
    U first;
    V second;

    public Pair(U first, V second) {
        this.first = first;
        this.second = second;
    }

    public U getFirst() {
        return first;
    }

    public V getSecond() {
        return second;
    }
}

这样一来,构造方法 Pair(U first, V second) 就可以接收不同类型的参数了,既可以是 Integer,也可以是 String:

Pair<String> kv = new Pair<String>(1, "老王");

那假如我们不用泛型类,参数类型直接用 Oeject ,其实也可以满足基本的需求,将类 Pair 修改成如下的方式:

public class Pair {
    Object first;
    Object second;

    public Pair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }

    public Object getFirst() {
        return first;
    }

    public Object getSecond() {
        return second;
    }
}
Pair minmax = new Pair(1,100);
Integer min = (Integer)minmax.getFirst();
Integer max = (Integer)minmax.getSecond();

但是这样会有什么坏处呢,假如我们在写代码的时候,不小心把类型弄错,但是编译器在编译的时候是不会有任何问题,在运行时候,就会抛类型转换异常:

Pair minmax = new Pair("王二狗",100);
Integer min = (Integer)minmax.getFirst();//其实这里已经类型转换异常,但是编译器编译的时候不会报错
Integer max = (Integer)minmax.getSecond();

但是如果使用了泛型的话,就可以避免这类错误,在编译期间,编译器就会报错:

Pair<String, Integer> pair = new Pair<>("王二狗",1);
Integer min = minmax.getFirst();//提示编译错误,String 类型,无法转换为 Integer 类型
Integer max = minmax.getSecond();

总结一下就是:Java 泛型是通过擦除实现的,对于泛型类,Java 编译器会将泛型代码转换为普通的非泛型代码,就像上面的普通 Pair 类代码及其使用代码一样,将类型参数 T 擦除,替换为 Object,插入必要的强制类型转换,泛型的两个好处就是:更好的安全性,更好的可读性。

泛型接口

接口也是可以泛型的,比如 Comparator 接口都是泛型的:

public interface Comparator<T> {
    int compare(T o1, T o2);
}

那么在实现这两个类的时候,实现方法里面就得指定具体的参数类型:

public class StringComparator implements Comparator<String> {
    @Override
    public int compare(String o1, String o2) {
        // 业务逻辑...
        return 0;
    }
}

泛型方法

除了泛型类,方法也可以是泛型的,而且,一个方法是不是泛型的,与它所在的类是不是泛型没有什么关系。我们看个例子就知道了:

public static < U, V > Pair<U, V> makePair( U first, V second ){
    Pair<U, V> pair = new Pair<>( first, second );
    return(pair);
}

参数类型的限定

上界为指定的类

如果没有参数类型的限定,那么类型参数 T 在擦除的时候,只能把它当作 Object,但 Java 支持限定这个参数的一个上界,也就是说,参数必须为给定的上界类型或其子类型,这个限定是通过 extends 关键字来表示的。例如,上面的 Pair 类,可以定义一个子类NumberPair,限定两个类型参数必须为 Number:

public class NumberPair<U extends Number, V extends Number> extends Pair<U, V> {
    public NumberPair(U first, V second) {
        super(first, second);
    }
}

限定类型后,如果类型使用错误,编译器会提示。指定边界后,类型擦除时就不会转换为 Object 了,而是会转换为它的边界类型 Number,这也是容易理解的。

上界为指定的接口

上面看到上界是指定的类,当然了,上界也可以是指定的接口,那么类型 T,就必须实现该上界接口,如下所示:

public static < T extends Comparable<T> > T max( T[] arr ){
    T max = arr[0];
    for ( int i = 1; i < arr.length; i++ ){
        if ( arr[i].compareTo( max ) > 0 ){
            max = arr[i];
        }
    }
    return(max);
}

max 方法计算一个泛型数组中的最大值,计算最大值需要进行元素之间的比较,要求元素实现 Comparable 接口,所以给类型参数设置了一个上边界 Comparable, T 必须实现 Comparable 接口。

上界为其他类型

上界类型,除了类、接口,也可以是其他类型,举个例子:

public class DynamicArray<E> {

    private static final int DEFAULT_CAPACITY = 10;
    private int size;
    private Object[] elementData;

    public DynamicArray() {
        this.elementData = new Object[DEFAULT_CAPACITY];
    }

    private void ensureCapacity(int minCapacity) {
        int oldCapacity = elementData.length;
        if (oldCapacity >= minCapacity) {
            return;
        }
        int newCapacity = oldCapacity * 2;
        if (newCapacity < minCapacity) {
            newCapacity = minCapacity;
        }
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    public void add(E e) {
        ensureCapacity(size + 1);
        elementData[size++] = e;
    }

    public E get(int index) {
        return (E) elementData[index];
    }

    public int size() {
        return size;
    }

    public E set(int index, E element) {
        E oldValue = get(index);
        elementData[index] = element;
        return oldValue;
    }
}
public class DynamicArray<E> {
    public < T extends E > void addAll( DynamicArray<T> c ){
        for ( int i = 0; i < c.size; i++ ){
            // 业务逻辑...
        }
    }
}

E 是 DynamicArray 的类型参数,T 是 addAll 的类型参数,T 的上界限定为 E。

解析通配符

有限定通配符

<? extends E> 表示有限定通配符,匹配 E 或 E 的某个子类型,具体什么子类型是未知的,如:

public class DynamicArray<E> {
    public < T extends E > void addAll( DynamicArray<T> c ){
        for ( int i = 0; i < c.size; i++ ){
            // 业务逻辑...
        }
    }
}

改成有限定通配符方式:

public class DynamicArray<E> {
    public void addAll( DynamicArray<? extends E> c ){
        for ( int i = 0; i < c.size; i++ ){
            // 业务逻辑...
        }
    }
}

<T extends E><? extends E> 到底有什么关系?

  1. <T extends E> 用于定义类型参数,它声明了一个类型参数T,可放在泛型类定义中类名后面、泛型方法返回值前面
  2. <? extends E> 用于实例化类型参数,它用于实例化泛型变量中的类型参数,只是这个具体类型是未知的,只知道它是E或E的某个子类型。

无限定通配符

形如 DynamicArray<? > ,称为无限定通配符,举个例子,在 DynamicArray 中查找指定的元素:

public static int indexOf(DynamicArray<?> arr, Object elm){
    for (int i=0; i<arr.size(); i++){
        if(arr.get(i).equals(elm)){
            return i;
        }
    }
    return -1;
}

无限定通配符,也可以改成类型参数,如下,两者写法是等价的:

public static <T> int indexOf(DynamicArray<T> arr, Object elm){
    for (int i=0; i<arr.size(); i++){
        if(arr.get(i).equals(elm)){
            return i;
        }
    }
    return -1;
}

但是通配符形式是比较简洁,但是有一个重要的限制:只能读,不能写,如下所示:

DynamicArray<Integer> ints = new DynamicArray<>();
DynamicArray<? extends Number> numbers = ints;
Integer a = 200;
numbers.add(a);//错误!
numbers.add((Number)a);//错误!
numbers.add((Object)a);//错误!

因为 ?问号就是表示类型安全无知, ? extends Number 表示是Number的某个子类型,但不知道具体子类型,如果允许写入,Java 就无法确保类型安全性,所以干脆禁止。现在我们再来看泛型方法到底应该用通配符的形式还是加类型参数。两者到底有什么关系?我们总结如下:

  1. 通配符形式都可以用类型参数的形式来替代,通配符能做的,用类型参数都能做。
  2. 通配符形式可以减少类型参数,形式上往往更为简单,可读性也更好,所以,能用通配符的就用通配符。
  3. 如果类型参数之间有依赖关系,或者返回值依赖类型参数,或者需要写操作,则只能用类型参数。
  4. 通配符形式和类型参数往往配合使用。

超类型通配符

<? super E> ,称为超类型通配符,表示E的某个父类型,它与 <? extends E> 正好相反,有了它,可以灵活的进行写入。我们给 DynamicArray 添加一个方法,将当前容器中的元素添加到传入的目标容器中:

public void copyTo(DynamicArray<E> dest){
    for (int i=0; i<size; i++){
        dest.add(get(i));
    }
}
DynamicArray<Integer> ints = new DynamicArray<Integer>();
ints.add(100);
ints.add(34);
DynamicArray<Number> numbers = new DynamicArray<Number>();
ints.copyTo(numbers);

Integer 是 Number 的子类,将 Integer 对象拷贝入 Number 容器,这种用法应该是合情合理的,但 Java会 提示编译错误,理由我们之前也说过了,期望的参数类型是 DynamicArray<Number> , DynamicArray<Integer> 并不适用。这里使用超类型通配符就可以解决这个问题:

public void copyTo(DynamicArray<? super E> dest){
    for (int i=0; i<size; i++){
        dest.add(get(i));
    }
}

这样,编译器就不会报错了,所以总结一下:

  1. <? super E> 用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象,它不能被类型参数形式替代。
  2. <? ><? extends E> 用于灵活读取,使得方法可以读取E或E的任意子类型的容器对象,它们可以用类型参数的形式替代,但通配符形式更为简洁。

局限性

superdicdi commented 1 year ago

写的很详细!