【Java编程的逻辑】 泛型 & 参数限定 & 通配符

类型参数的限定

无论是泛型类、泛型方法还是泛型接口,关于类型参数,我们都知之甚少,只能把它当作Object,但Java支持限定这个参数的一个上界,也就是说:参数必须为给定的上界类型或其子类型,这个限定是通过extends关键字来表示的。

上界为某个具体类

public class NumberPair<U extends Number> {

}

指定边界之后,类型擦除时就不会转换为Object了,而是会转换为它的边界类型。

上界为某个接口

在泛型方法中,我们想要参数必须实现了某个接口。

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

<T extends Comparable<T>>可能会令人比较费解,这称为递归类型限制。 T表示一种数据类型,必须实现Comparable接口,且必须可以和同类型的元素进行比较。

上界为其他类型参数

假设我们有这么一个类,该类主要仿写集合中的一些功能。

public class DynamicArray<E> { 
    private Object[] elementData;
    private static final int DEFAULT_CAPACITY = 10;
    private int size;
    public DynamicArray() {
        this.elementData = new Object[DEFAULT_CAPACITY];
    }

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

现在我们想添加一个功能,就是一次插入一个DynamicArray。我们可能会想当然的这么想

public void addAll(DynamicArray<E> c) {
    for(int i = 0; i < c.size; i++) {
        add(c.get(i));
    }
}

测试:

public static void main(String[] args) {
    DynamicArray<Number> numbers = new DynamicArray<>();
    DynamicArray<Integer> ints = new DynamicArray<>();
    numbers.add(99);
    ints.add(100);
    numbers.addAll(ints);
}

这个时候,其实最后一行的addAll方法是会报错的。
numbers是一个Number类型的容器,ints是一个Integer类型的容器,Integer是Number的子类,我们希望把ints添加到numbers中,应该说是比较合理的。

但是我们这样的addAll方法是有问题的,我们可以这样来假设,假设刚才的addAll方法是可以编译成功的

DynamicArray<Integer> ints = new DynamicArray<>();
DynamicArray<Number> numbers = ints;    // 假设合法
numbers.add(new Double(3.14));          

这样是不是就发现问题了?破坏了Java泛型关于类型安全的保证

这里的原因是:Integer是Number的子类,但是DynamicArray<Integer>不是DynamicArray<Number>的子类。

但是这里的需求是没有问题的,将Integer添加到Number容器中。我们可以通过类型限定来解决:

public <T extends E> void addAll(DynamicArray<T> c) {
    for(int i = 0; i < c.size; i++) {
        add(c.get(i));
    }
}

通配符

在上面的那个示例中,addAll方法,我们使用了其他类型参数作为上界。但是这种写法有点繁琐,我们可以通过通配符写出更简洁的形式。

public void addAll(DynamicArray<? extends E> c) {
    for(int i = 0; i < c.size; i++) {
        add(c.get(i));
    }
}

?表示通配符,<? extends E>表示有限定通配符
那么问题就来了<T extedns E><? extends E>有什么关系区别呢?

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

理解通配符

上面提到了有限定通配符,相应的还有无限定通配符,这里我们继续来扩展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;
}

上面两种方法可以达到同样的作用,不过通配符形式更为简洁。 虽然通配符形式更为简洁,但上面两种通配符都有一个重要的限制:只能读,不能写

public static void main(String[] args) {
    DynamicArray<Integer> ints = new DynamicArray<>();
    DynamicArray<? extends Number> numbers = ints;
    Integer a = 998;
    ints.add(a);
    numbers.add(a);
    numbers.add((Number)a);
    numbers.add((Object)a);
}

后面numbers.add()三种方式都是非法的。 因为<? extends Number>表示的是Number的某个子类,但是不知道具体子类型,如果允许写入,Java就无法确保类型安全了,所以这样的写法就是非法的了。

既然无法写入,那么这种形式的实际意义是什么呢?
我们可以将公共API的方法使用通配符形式,但内部调用带类型参数的方法

private static <T> void swapInternal(DynamicArray<T> arr, int i, int j) {
    T temp = arr.get(i);
    arr.set(i, arr.get(j));
    arr.set(j, temp);
}

public static void swap(DynamicArray<?> arr, int i, int j) {
    swap(arr, i, j);
}
发布了98 篇原创文章 · 获赞 97 · 访问量 27万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章