• PECS法则与extends和super关键字


    通配符

    在本文的前面的部分里已经说过了泛型类型的子类型的不相关性。但有些时候,我们希望能够像使用普通类型那样使用泛型类型:

    • 向上造型一个泛型对象的引用
    • 向下造型一个泛型对象的引用

    向上造型一个泛型对象的引用

    例如,假设我们有很多箱子,每个箱子里都装有不同的水果,我们需要找到一种方法能够通用的处理任何一箱水果。更通俗的说法,A是B的子类型,我们需要找到一种方法能够将C<A>类型的实例赋给一个C<B>类型的声明。

    为了完成这种操作,我们需要使用带有通配符的扩展声明,就像下面的例子里那样:

    1
    2
    List<Apple> apples = new ArrayList<Apple>();
    List<? extends Fruit> fruits = apples;

    “? extends”是泛型类型的子类型相关性成为现实:Apple是Fruit的子类型,List<Apple> 是 List<? extends Fruit> 的子类型。

    向下造型一个泛型对象的引用

    现在我来介绍另外一种通配符:? super。如果类型B是类型A的超类型(父类型),那么C<B> 是 C<? super A> 的子类型:

    1
    2
    List<Fruit> fruits = new ArrayList<Fruit>();
    List<? super Apple> = fruits;

    为什么使用通配符标记能行得通?

    原理现在已经很明白:我们如何利用这种新的语法结构?

    ? extends

    让我们重新看看这第二部分使用的一个例子,其中谈到了Java数组的子类型相关性:

    1
    2
    3
    Apple[] apples = new Apple[1];
    Fruit[] fruits = apples;
    fruits[0] = new Strawberry();

    就像我们看到的,当你往一个声明为Fruit数组的Apple对象数组里加入Strawberry对象后,代码可以编译,但在运行时抛出异常。

    现在我们可以使用通配符把相关的代码转换成泛型:因为Apple是Fruit的一个子类,我们使用? extends 通配符,这样就能将一个List<Apple>对象的定义赋到一个List<? extends Fruit>的声明上:

    1
    2
    3
    List<Apple> apples = new ArrayList<Apple>();
    List<? extends Fruit> fruits = apples;
    fruits.add(new Strawberry());

    这次,代码就编译不过去了!Java编译器会阻止你往一个Fruit list里加入strawberry。在编译时我们就能检测到错误,在运行时就不需要进行检查来确保往列表里加入不兼容的类型了。即使你往list里加入Fruit对象也不行:

    1
    fruits.add(new Fruit());

    你没有办法做到这些。事实上你不能够往一个使用了? extends的数据结构里写入任何的值。

    原因非常的简单,你可以这样想:这个? extends T 通配符告诉编译器我们在处理一个类型T的子类型,但我们不知道这个子类型究竟是什么。因为没法确定,为了保证类型安全,我们就不允许往里面加入任何这种类型的数据。另一方面,因为我们知道,不论它是什么类型,它总是类型T的子类型,当我们在读取数据时,能确保得到的数据是一个T类型的实例:

    1
    Fruit get = fruits.get(0);

    ? super

    使用 ? super 通配符一般是什么情况?让我们先看看这个:

    1
    2
    List<Fruit> fruits = new ArrayList<Fruit>();
    List<? super Apple> = fruits;

    我们看到fruits指向的是一个装有Apple的某种超类(supertype)的List。同样的,我们不知道究竟是什么超类,但我们知道Apple和任何Apple的子类都跟它的类型兼容。既然这个未知的类型即是Apple,也是GreenApple的超类,我们就可以写入:

    1
    2
    fruits.add(new Apple());
    fruits.add(new GreenApple());

    如果我们想往里面加入Apple的超类,编译器就会警告你:

    1
    2
    fruits.add(new Fruit());
    fruits.add(new Object());

    因为我们不知道它是怎样的超类,所有这样的实例就不允许加入。

    从这种形式的类型里获取数据又是怎么样的呢?结果表明,你只能取出Object实例:因为我们不知道超类究竟是什么,编译器唯一能保证的只是它是个Object,因为Object是任何Java类型的超类。

    存取原则和PECS法则

    总结 ? extends 和 the ? super 通配符的特征,我们可以得出以下结论:

    • 如果你想从一个数据类型里获取数据,使用 ? extends 通配符
    • 如果你想把对象写入一个数据结构里,使用 ? super 通配符
    • 如果你既想存,又想取,那就别用通配符。

    这就是Maurice Naftalin在他的《Java Generics and Collections》这本书中所说的存取原则,以及Joshua Bloch在他的《Effective Java》这本书中所说的PECS法则。

    Bloch提醒说,这PECS是指”Producer Extends, Consumer Super”,这个更容易记忆和运用。

    http://www.importnew.com/14985.html

    什么是PECS? 

    PECS指“Producer Extends,Consumer Super”。换句话说,如果参数化类型表示一个生产者,就使用<? extends T>;如果它表示一个消费者,就使用<? super T>,可能你还不明白,不过没关系,接着往下看好了。

    下面是一个简单的Stack的API接口:

    1
    2
    3
    4
    5
    6
    public class  Stack<E>{
        public Stack();
        public void push(E e):
        public E pop();
        public boolean isEmpty();
    }

    假设想增加一个方法,按顺序将一系列元素全部放入Stack中,你可能想到的实现方式如下:

    1
    2
    3
    4
    public void pushAll(Iterable<E> src){
        for(E e : src)
            push(e)
    }

    假设有个Stack<Number>,想要灵活的处理Integer,Long等Number的子类型的集合

    1
    2
    3
    Stack<Number> numberStack = new Stack<Number>();
    Iterable<Integer> integers = ....;
    numberStack.pushAll(integers);

    此时代码编译无法通过,因为对于类型Number和Integer来说,虽然后者是Number的子类,但是对于任意Number集合(如List<Number>)不是Integer集合(如List<Integer>)的超类,因为泛型是不可变的。

    幸好java提供了一种叫有限通配符的参数化类型,pushAll参数替换为“E的某个子类型的Iterable接口”:

    1
    2
    3
    4
    public void pushAll(Iterable<? extends E> src){
        for (E e: src)
            push(e);
    }

    这样就可以正确编译了,这里的<? extends E>就是所谓的 producer-extends。这里的Iterable就是生产者,要使用<? extends E>。因为Iterable<? extends E>可以容纳任何E的子类。在执行操作时,可迭代对象的每个元素都可以当作是E来操作。

    与之对应的是:假设有一个方法popAll()方法,从Stack集合中弹出每个元素,添加到指定集合中去。

    1
    2
    3
    4
    5
    public void popAll(Collection<E> dst){
           if(!isEmpty()){
                    dst.add(pop());
            }
    }

    假设有一个Stack<Number>和Collection<Object>对象:

    1
    2
    3
    Stack<Number> numberStack = new Stack<Number>();
    Collection<Object> objects = ...;
    numberStack.popAll(objects);

    同样上面这段代码也无法通过,解决的办法就是使用Collection<? super E>。这里的objects是消费者,因为是添加元素到objects集合中去。使用Collection<? super E>后,无论objects是什么类型的集合,满足一点的是他是E的超类,所以不管这个参数化类型具体是什么类型都能将E装进objects集合中去。

    总结:

    1. 如果你是想遍历collection,并对每一项元素操作时,此时这个集合是生产者(生产元素),应该使用 Collection<? extends Thing>.
    2. 如果你是想添加元素到collection中去,那么此时集合是消费者(消费元素)应该使用Collection<? super Thing>

    注:此文根据《Effective Java》以及Java Generics: What is PECS? 整理成文。想了解更多有关泛型相关知识,请读者阅读《Effective Java》的第五章。

    http://www.importnew.com/8966.html

    泛型是在Java 1.5中被加入了,这里不讨论泛型的细节问题,这个在Thinking in Java第四版中讲的非常清楚,这里要讲的是super和extends关键字,以及在使用这两个关键字的时候为什么会不同的限制。 
       首先,我们定义两个类,A和B,并且假设B继承自A。下面的代码中,定义了几个静态泛型方法,这几个例子随便写的,并不是特别完善,我们主要考量编译失败的问题: 

    复制代码
    public class Generic{
    //方法一
    public static <T extends A> void get(List<T extends A> list)
    {
        list.get(0);
    }
    
    //方法二
    public static <T extends A> void set(List<T extends A> list, A a)
    {
        list.add(a);
    }
    
    //方法三
    public static <T super B> void get(List<T super B> list)
    {
        list.get(0);
    }
    
    //方法四
    public static <T super B> void set(List<T super B> list, B b)
    {
        list.add(b);
    }
    }
    复制代码

    编译之后,我们会发现,方法二和方法三没有办法通过编译。按照Thinking in Java上的说法,super表示下界,而extends表示上界,方法二之所以没有办法通过,是因为被放到List里面去的可能是A,也可能是任何A的子类,所以编译器没有办法确保类型安全。而方法三之所以编译失败,则是因为编译器不知道get出来的是B还是B的其他的什么子类,因为set方法四允许在list放入B,也允许在list中放入B的子类,也就没有办法保证类型安全。 
      上面的这段解释听起来可能有点奇怪,都是因为编译器无法判断要获取或者设置的是A和B本身还是A和B的其他的子类才导致的失败。那么Java为什么不干脆用一个关键字来搞定呢? 
      如果从下面的角度来解释,就能把这个为什么编译会出错的问题解释的更加的直白和清除,也让人更容易理解,先看下面的代码,还是A和B两个类,B继承自A: 

    复制代码
    public class Generic2{
       public static void main(String[] args){
          List<? extends A> list1 = new ArrayList<A>();
          List<? extends A> list2 = new ArrayList<B>();
          List<? super B> list3 = new ArrayList<B>();
          List<? super B> list4 = new ArrayList<A>();
       }
    }
    复制代码

       从上面这段创建List的代码我们就更加容易理解super和extends关键字的含义了。首先要说明的一点是,Java强制在创建对象的时候必须给类型参数制定具体的类型,不能使用通配符,也就是说new ArrayList<? extends A>(),new ArrayList<?>()这种形式的初始化语句是不允许的。 
       从上面main函数的第一行和第二行,我们可以理解extends的含义,在创建ArrayList的时候,我们可以指定A或者B作为具体的类型,也就是,如果<? extends X>,那么在创建实例的时候,我们就可以用X或者扩展自X的类为泛型参数来作为具体的类型,也可以理解为给?号指定具体类型,这就是extends的含义。 
       同样的,第三行和第四行就说明,如果<? super X>,那么在创建实例的时候,我们可以指定X或者X的任何的超类来作为泛型参数的具体类型。 
       当我们使用List<? extends X>这种形式的时候,调用List的add方法会导致编译失败,因为我们在创建具体实例的时候,可能是使用了X也可能使用了X的子类,而这个信息编译器是没有办法知道的,同时,对于ArrayList<T>来说,只能放一种类型的对象。这就是问题的本质。而对于get方法来说,由于我们是通过X或者X的子类来创建实例的,而用超类来引用子类在Java中是合法的,所以,通过get方法能够拿到一个X类型的引用,当然这个引用可以指向X也可以指向X的任何子类。 
       而当我们使用List<? super X>这种形式的时候,调用List的get方法会失败。因为我们在创建实例的时候,可能用了X也可能是X的某一个超类,那么当调用get的时候,编译器是无法准确知晓的。而调用add方法正好相反,由于我们使用X或者X的超类来创建的实例,那么向这个List中加入X或者X的子类肯定是没有问题的(?超类有多个,编译器怎么知道是哪个真超类),因为超类的引用是可以指向子类的。 
      最后还有一点,这两个关键字的出现都是因为Java中的泛型没有协变特性的倒置的。

  • 相关阅读:
    NHibernate 入门必看——NHibernate Made Simple
    ASP.NET 的多线程
    asp.net 禁止用户二次登录(转)
    marquee标记用法及在asp.net中的应用(转)
    解决Visual Studio 2005显示中文乱码(zhuan)
    ms sql 触发器( 转)
    Asp.net 页面导航的几种方法与比较
    ASP.NET1.1(VB):DataGrid中"加入序号列"和"截取定长字符串追加'...
    解决“Internet Explorer 无法打开 Internet站点已终止操作”问题(转)
    ASP.NET 2.0的页面指令集(转)
  • 原文地址:https://www.cnblogs.com/dldrjyy13102/p/8297045.html
Copyright © 2020-2023  润新知