美文网首页
泛型:边界和通配符

泛型:边界和通配符

作者: 安静的蓝孩子 | 来源:发表于2019-05-23 14:30 被阅读0次

    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<T>{
        public void add(T t) {
        }
    }
    
    
    Box<Number> box = new Box<Number>();
    box.add(new Integer(10));   // OK
    box.add(new Double(10.1));  // OK
    

    接下来思考一个问题

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

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

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

    关系图

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

    2. 边界

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

    public class Box<T extends Number> {
    
        private T t;
    
        public void set(T t) {
            this.t = t;
        }
    
        public static void main(String[] args) {
            Box<Integer> box = new Box<>();
            box.set(new Integer(10));
            
            Box<String> box1 = new Box<String>(); //报错
        }
    }
    

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

    多边界约束

    <T extends B1 & B2 & B3>
    

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

    Class A { /* ... */ }
    interface B { /* ... */ }
    interface C { /* ... */ }
    
    class D <T extends A & B & C> { /* ... */ }
    

    下边这个会编译报错

    class D <T extends B & A & C> { /* ... */ }
    

    3. 通配符

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

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

    上界通配符

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

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

    public class Box {
    
        public double sumOfList1(List<Number> list) {
            double s = 0.0;
            for (Number n : list)
                s += n.doubleValue();
            return s;
        }
    
        public double sumOfList2(List<? extends Number> list) {
            double s = 0.0;
            for (Number n : list)
                s += n.doubleValue();
            return s;
        }
    }
    

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

    public static void main(String[] args) {
            Box box = new Box();
            List<Number> listNumber = Arrays.asList(1, 2, 3);
            List<Integer> 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> 的大多数方法都不依赖于 T 时,更常使用 CLass<?>

    比如:

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

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

    public static void printList(List<?> list) {
        for (Object elem: list)
            System.out.print(elem + " ");
        System.out.println();
    }
    
    List<Integer> li = Arrays.asList(1, 2, 3);
    List<String>  ls = Arrays.asList("one", "two", "three");
    printList(li);
    printList(ls);
    

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

    下界通配符

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

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

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

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

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







    参考:

    Wildcards

    相关文章:

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

    相关文章

      网友评论

          本文标题:泛型:边界和通配符

          本文链接:https://www.haomeiwen.com/subject/bqnvzqtx.html