泛型:边界和通配符

1. 关系

Java 中,可以给一个对象赋值另一个兼容的对象,例如,我们可以把 Integer 赋值给 Object,因为 ObjectInteger 的超类。

Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger;   // OK

在面向对象语言中,这种关系称为 "is a",即 Integer is a Object。 但是, Integer 同时也是 Number 的一个子类,所以以下的代码也是允许的。

public void someMethod(Number n) { /* ... */ }

someMethod(new Integer(10));   // OK
someMethod(new Double(10.1));   // OK

同样的,在泛型中,如果类型是和 Number 兼容的,则可以调用 add 方法添加。

public class Box{
    public void add(T t) {
    }
}


Box box = new Box();
box.add(new Integer(10));   // OK
box.add(new Double(10.1));  // OK

接下来思考一个问题

public void boxTest(Box n) { /* ... */ }

这个函数会接收一个什么类型的参数?
我们可以直接看到它需要的是 Box,但是,当我们传入一个 Box 或者 Box 时,这是否允许?
答案是否定的,原因是 BoxBox 都不是 Box 的子类。

这是一个关于泛型通常会陷入的误区。

泛型:边界和通配符_第1张图片
关系图

从上面的关系图中可以看出,尽管 IntegerNumber 的子类,但是 Box 并不是 Box 的子类,它们是没有关系的。

2. 边界

有时我们可能需要限制参数的类型,比如在一个只操作数字的方法中,可能只需要 Number 类型或者其子类的参数,这就是边界参数的作用。

public class Box {

    private T t;

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

    public static void main(String[] args) {
        Box box = new Box<>();
        box.set(new Integer(10));
        
        Box box1 = new Box(); //报错
    }
}

在上面的例子中,通过 extends 关键词,约束了 Box 的上边界为 Number
因此,在实例化 Box 时,指定的类型必须是 number 的子类。IntegerNumber 的子类,所以能通过编译,String 不是,会编译报错。

多边界约束


具有多个边界的类型变量是该边界中列出的所有类型的子类型。如果其中一个边界是类,则必须首先指定它。例如:

Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }

class D  { /* ... */ }

下边这个会编译报错

class D  { /* ... */ }

3. 通配符

在代码中,通配符使用 ? 字符,用来表示未知的类型。

通配符可以用于多种情况:作为参数、字段或局部变量的类型甚至有时作为返回类型。

上界通配符

声明一个上界通配符,只需要在 ? 后面跟上 extends 关键词,比如

如果我们想定义一个函数,其接收一个 Numberlist 或者是 Number 的子类比如 Integer,Double,Float等的,我们看看下面的写法:

public class Box {

    public double sumOfList1(List list) {
        double s = 0.0;
        for (Number n : list)
            s += n.doubleValue();
        return s;
    }

    public double sumOfList2(List list) {
        double s = 0.0;
        for (Number n : list)
            s += n.doubleValue();
        return s;
    }
}

这里定义了两个函数,分别是 sumOfList1(List list)sumOfList2(List list) ,不同之处是后者使用了通配符,那他们的使用有什么区别呢?

public static void main(String[] args) {
        Box box = new Box();
        List listNumber = Arrays.asList(1, 2, 3);
        List listInteger = Arrays.asList(1, 2, 3);
        //调用使用了通配符的函数
        double result = box.sumOfList2(listNumber);
        double result1 = box.sumOfList2(listInteger);
        
        //调用未使用通配符的函数  
        double result2 = box.sumOfList1(listNumber);
        double result3 = box.sumOfList1(listInteger); //编译报错
    }

上面调用的代码可以看出,使用了通配符的函数, list 包裹的类型可以是 Number 及其子类的,而未使用通配符的,只能使用 Number 类型的。上界通配符放松对变量的限制。

无限通配符

无限通配符是对通配符 (?) 的一种特殊使用,用法如, List,这个 list 的类型是未知的。

无限通配符的两个引用场景:

  • 编写的方法能直接使用 Object 提供的方法实现的。
  • 当使用泛型类的方法不依赖于泛型参数时。比如,List.size, List.clear。当类 Class 的大多数方法都不依赖于 T 时,更常使用 CLass

比如:

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

这个函数的目的是打印 list 的所有的类型,但是现在它只能打印 Object 实例的列表,它是不能打印 List, List, List 等类型的列表的,因为它们都不是 List 的子类,无法传入。
要想实现此目的,我们可以使用无限通配符。

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

List li = Arrays.asList(1, 2, 3);
List  ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);

因为多余任一实现类型的 AList 都是 List 的子类。

下界通配符

上界通配符通过 的形式,将未知类型限制其为 A 的子类。相似的,下界通配符是将未知类型限制为某个类型的父类。

声明一个下界通配符,只需要在 ? 后面跟上 super 关键词,比如

public static void addNumbers(List list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }

上面的函数中,List 限制了只能传入 List, List, 或者 List,因为 Integer 的父类只有 Number, Object

需要注意的,可以使用通配符指定上界,也可以指定下界,但不能两者同时指定







参考:

Wildcards


相关文章:

泛型:为什么使用泛型与泛型的基本使用
泛型:边界和通配符
泛型:类型擦除

你可能感兴趣的:(泛型:边界和通配符)