第二十八条 利用有限制通配符来提升API的灵活性

书中提到,参数化类型是不可变的,意思就是两个截然不同的类型 Type1 和 Type2, List<Type1> 即不是 List<Type2> 的子类型,也不是它的超类型。即使 Type1 是 Type2 的子类,List<Type1>也不是 List<Type2> 的子类。最直观的的例子, String 是 Object 的子类,但   List<String> 却不是 List<Object> 的子类,为什么呢? List<Object> 能装进去任意对象,List<String> 只能装进去 String 类型, 基类有的功能,子类都是可以用的,但这个例子中,明显不符合这个特征。

    public class Stack<E> {
        private static final int DEFAULT_INITIAL_CAPACITY = 16;
        private E[] elements;
        private int size = 0;

        
        public void push(E e) {
           
        }
        
    }

假如添加一个新方法,

    public void pushAll(Iterable<E> src){
        for(E e : src){
            push(e);
        }
    }

解释一下,Iterable 是个接口,Collection 集合是实现这个接口的,如果不实现它,迭代器 和 增强for循环恐怕都不能直接使用。pushAll(Iterable<E> src) 这个方法咋一看没问题,实际上不完美,因为实际中 Iterable<E> src 和 Stack<E> 元素类型不一定一样。例如,有 Stack<Number> ,调用了 pushAll()方法,穿进去的参数为 Integer 类型,我们知道, Integer 是 Number 的子类,所以实际中很可能有这个需求,但如果我们真的这么写了,是会报错的,编译状态就出错了。

    Stack<Number> stack = new Stack<>();
    ArrayList<Integer> list = new ArrayList<>();
    stack.pushAll(list);

ArrayList 是 实现 Iterable 接口的,所以可以这么用。很明显,pushAll()方法报错了,Number in Stack cannot be applied to Integer。如果都是用 Number 类型,则可以

    Stack<Number> stack = new Stack<>();
    ArrayList<Number> list = new ArrayList<>();
    stack.pushAll(list); 
这种写法是可以的,但局限性太大了。 有办法解决这种问题吗? 有限制通配符类型,此刻闪亮登场,来解决问题。参数类型应该是 E的子类型的Iterable 接口,通配符写法为Iterable<? extendsE>,在此注意,每个类型都是自己的子类型, 所以  Iterable<? extends E> 的意思是,既可以是 E,也可以是 E 的子类型。

    public void pushAll(Iterable<? extends E> src){
        for(E e : src){
            push(e);
        }
    }
这样,就可以了。修改后,Integer 类型的 list 集合,就可以当做参数传进去了。如果,我们再写一个方法与pushAll()相对应,初次写法如下

    public void popAll(Collection<E> dst){
        while (!isEmpty()){
            dst.add(pop());
        }
    }
意思就是穿进去一个 集合,然后把Stack中的元素,装到该集合中。猛一看,没问题。但实际上犯了pushAll()方法一开始犯的毛病。如果参数类型一样,可以编译通过。但实际中,往往也有子类父类的使用。比如, Object 是 Number 的父类, Collection<Object> 集合里面添加一个 Number 对象是再正常不过了,但此时,调用这个方法,编译都通不过

    Stack<Number> stack = new Stack<>();
    ArrayList<Integer> list = new ArrayList<>();
    stack.pushAll(list);

    ArrayList<Object> objList = new ArrayList<>();
    stack.popAll(objList);
最后一行代码会报错,提示  Number in Stack cannot be applied to Object, Collection<Object> 不是 Collection<Number> 的子类型。 怎么办呢?还是通配符, 找个 E 的超类的集合,写法如下 Collection<? super E> dst ,同理,意思是 既可以是 E,也可以是 E 的基类型。

    public void popAll(Collection<? super E> dst){
        while (!isEmpty()){
            dst.add(pop());
        }
    }
这样编译就通过了。 看到这,发现优先通配符还挺好用的。书中给了两个名词,生产者 和 消费者。 pushAll()方法中src参数通过产生E来提供Stack使用,因此是生产者,用<? extends T>,popAll()方法中dst参数通过Stack,填装Stack的对象,消费E的实例,所以是消费者,使用 <? super T>。 生产者 和 消费者 是个使用通配符的原则。如果感觉这个地方太绕口,根据实际业务逻辑来做判断也行。就像上面 使用 集合 添加新的对象一样, 基类的集合可以装进去子类的对象,记住类似这句话的逻辑,也能写出好的有限制通配符。

前面讲过一个方法
    public static <E> Set union(Set<E> s1, Set<E> s2) {
        Set result = new HashSet(s1);
        result.addAll(s2);
        return result;
    }
按照今天的讲法,可以修改为 

    public static <E> Set union(Set<? extends E> s1, Set<? extends E> s2) {
        Set result = new HashSet(s1);
        result.addAll(s2);
        return result;
    }
但实际上,这种写法也有一定局限性。比如 E 的类型是 Object, 任何对象都是 Object 的子类, 那么任何不相干的两个 Set 集合, 都可以调用这个方法。如果我们能确定只用于 Number 类型,可以把功能缩小,改为 

    public static <E> Set union(Set<? extends Number> s1, Set<? extends Number> s2) {
        Set result = new HashSet(s1);
        result.addAll(s2);
        return result;
    }
这样就能保证两个Set集合的元素,都是 Number 本身,或者是它的子类。
我们再来看看上一章的一个例子,

    public static <T extends Comparator<T>>T max(List<T> list){
        
    }
我们可以修改为
    public static <T extends Comparator<? super T>>T max(List<T> list){

    }
此时,如果进一步,则为

    public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
        Iterator<? extends T> iterator = list.iterator();
        T result = iterator.next();
        T t;
        while (iterator.hasNext()) {
            t = iterator.next();
            if (t.compareTo(result) > 0) {
                result = t;
            }
        }
        return result;
    }
好吧,这个方法里,有 extends ,也有 super, 关键是 两一块出现了。我刚开始看到这也蒙了,一年后,随着见识增长,才明白为什么这么写,这么写到底有什么好处。List<? extends T> list的意思是,传进来的参数类型集合中的元素,既可以是 T, 也可以是T的子类,大大增加了该方法的灵活性。<T extends Comparable<? super T>> 意思是该T 必须实现 Comparable 接口,这有实现这个接口了,才能比较。注意,前面的两点都好理解,最惹人头痛的地方来了, 为什么是 Comparable<? super T> 而不是 Comparable<T> ,Comparable<T> 意思是 T 类型本身 实现了 Comparable接口,Comparable<? super T> 意思 是 T 或者 T 的父类,只要任意一个实现 Comparable接口就行了,灵活性更高,更符合实际业务需求。举例说明

class Shape implements Comparable<Shape>{

    @Override
    public int compareTo(Shape o) {
        return o.hashCode();
    }
}

class Rectangle extends Shape{

}

class Circle extends Shape{

}

Shape 是形状,Rectangle 是长方形,是个子类, Circle 是圆形,也是子类。如果说,使用 <T extends Comparator<T>> 这种方法修饰的话,咱们创建一个 List<Rectangle> rectangleList = new ArrayList<>();  max(rectangleList); 编译会报错,因为此时 T 是 Rectangle 而非是 Shape, <T extends Comparator<T>> 的意思是 要 T 本身实现 Comparator 接口,如果修改 Shape 和
Rectangle 类,则编译可以通过。

class Shape {
}

class Rectangle extends Shape implements Comparable<Rectangle>{

    @Override
    public int compareTo(Rectangle o) {
        return o.hashCode();
    }
}

但是改动太大,Shape 的作用根本就没体现。 Shape 已经实现了 Comparable 接口, 新添加一个子类,子类完全可以直接使用 compareTo()方法,不用每写一个子类,就去实现一遍,如果不满足子类的业务修改,可以重写该方法,继承体系的好处就是这里。所以还是得让父类去实现这个方法,这样,子类都方便管理,也增加灵活性。<T extends Comparable<? super T>> 可以理解为 T 就是Shape,List<? extends T> 集合中的元素对象,既可以是 Shape,也可以是 Rectangle 或者 Circle, 只要基类 Shape 实现了 Comparable 接口, 子类就可以当做参数直接传进该方法。大家仔细想想这个道理,并且 Collections 中的sort()方法,排序,也是这么写的 public static <T extends Comparable<? super T>> void sort(List<T> list) {} 异曲同工之妙。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值