Java编程思想__泛型(五)

边界

  • 边界使得你可以在用于泛型的参数类型上设置限制条件。
  • 尽管这使得你可以强制规定泛型可以应用的类型,但是其潜在的一个重要的效果是你可以按照自己的边界类型来调用方法。
  • 因为擦除移除了类型信息,所以,可以用无边界泛型参数调用的方法只是那些可以用 Object调用的方法。但是如果能够将这个参数限制为某个类型子集,那么你就可以用这些类型子集来调用方法。
  • 为了执行这种限制,Java 泛型重用了 extends 关键字。对你来说有一点很重要,既要理解 extends 关键字在泛型边界上下文环境中和在普通情况下所具有的意义是完全不同的。
public interface HasColor {

   public void getColor();
}

class Colored<T extends HasColor>{
    T t;
    Colored(T t){
        this.t=t;
    }
    T getT(){
        return t;
    }
    //界限允许您调用方法
    void color(){
        t.getColor();
    }
}
  1. 这只是单一界限,如果多重限制的话,这样写是行不通的, extends 后面第一个必须是类,然后在是接口。如下
class Dimension{
    public int x,y,z;
}

class ColoredDimension<T extends Dimension & HasColor>{
    T t;

    public ColoredDimension(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }
    void color(){
        t.getColor();
    }

    int getX(){
        return t.x;
    }
    int getY(){
        return t.y;
    }

    int getZ(){
        return t.z;
    }
}
  1. 界限的 extends 后面是否可以跟多个 类(class),这样做是不可以的。与继承一样,您只能拥有一个具体的类,但是可以有多个接口。
interface Weight{
    int weight();
}

class Solid<T extends Dimension & HasColor & Weight>{
    T t;

    public Solid(T t) {
        this.t = t;
    }
    public T getT() {
        return t;
    }
    void color(){
        t.getColor();
    }
    int getX(){
        return t.x;
    }
    int getY(){
        return t.y;
    }
    int getZ(){
        return t.z;
    }
    int weight(){
        return t.weight();
    }
}
  1. 接下来我们创建测试一下创建一个 即继承某个类,又实现多个接口的类。
class Bounded extends Dimension implements HasColor,Weight{
    @Override
    public void getColor() {
        System.out.println("Bounded getColor method");
    }
    @Override
    public int weight() {
        return 0;
    }
}
class BasicBounds{
    public static void main(String[] args) {
        Solid<Bounded> solid=new Solid<>(new Bounded());
        solid.color();
        solid.getX();
        solid.weight();
    }
}
  1. 你可能已经观察到了, BasicBounds.java 看上去包含可以通过继承可以消除冗余。下面,可以看到如何在继承的每个层次上添加边界限制。
public class HoldItem <T>{
    T t;

    public HoldItem(T t) {
        this.t = t;
    }
    T getT(){
        return t;
    }
}

class Colored2 <T extends HasColor> extends HoldItem<T>{

    public Colored2(T t) {
        super(t);
    }
    void color(){
        t.getColor();
    }
}


class ColoredDimesion2 <T extends  Dimension & HasColor> extends Colored2<T>{

    public ColoredDimesion2(T t) {
        super(t);
    }
    int getX(){
        return t.x;
    }
    int getY(){
        return t.y;
    }
    int getZ(){
        return t.z;
    }
}

class Solid2 <T extends Dimension & HasColor & Weight> extends ColoredDimesion2<T>{
    public Solid2(T t) {
        super(t);
    }
    int weight(){
        return t.weight();
    }
}

class InheritBounds{
    public static void main(String[] args) {
        Solid2<Bounded> solid2=new Solid2<>(new Bounded());
        solid2.color();
        solid2.getX();
        solid2.weight();
    }
}
  1. HoldItem 直接持有一个对象,因此这种行为被继承到了 Colored2 中,它也要求其参数与 HoldColor 一致。ColoredDimension2 和 Solid2 进一步扩展了这个层次结构,并且每个层次上都添加了边界。现在这些方法被继承,因而不必在每个类中重复。
public interface SuperPower {}

interface XRayVision extends SuperPower{
    void seeThroughWalls();
}

interface SuperHearing extends SuperPower{
    void hearThroughNoises();
}

interface SuperSmell extends SuperPower{
    void trackBySmell();
}

class SuperHerp<T extends SuperPower>{
    T t;

    public SuperHerp(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }
}

class SuperSleuth<T extends XRayVision> extends SuperHerp<T> {

    public SuperSleuth(T t) {
        super(t);
    }
    void see(){
        t.seeThroughWalls();
    }
}

class CanineHero<T extends SuperHearing & SuperSmell> extends SuperHerp<T>{

    public CanineHero(T t) {
        super(t);
    }
    void hear(){
        t.hearThroughNoises();
    }
    void smell(){
        t.trackBySmell();
    }
}

class SuperHearSmell implements SuperHearing,SuperSmell{

    @Override
    public void hearThroughNoises() {}
    @Override
    public void trackBySmell() {}
}

class DogBoy extends CanineHero<SuperHearSmell>{

    public DogBoy() {
        super(new SuperHearSmell());
    }
}

class EpicBattle{
    //bounds in generic methods 泛型方法的界限
    static <T extends SuperHearing> void useSuperHearing(SuperHerp<T> tSuperHerp){
        tSuperHerp.getT().hearThroughNoises();
    }

    static  <T extends SuperHearing & SuperSmell> void superFind(SuperHerp<T> superHerp){
        superHerp.getT().hearThroughNoises();
        superHerp.getT().trackBySmell();
    }

    public static void main(String[] args) {
        DogBoy dogBoy=new DogBoy();
        useSuperHearing(dogBoy);
        superFind(dogBoy);

        //you can do this  你可以这样做
        //List<? extends SuperHearing> audioBoys
        //But you can't do this 但是你做不到
        //List<? extends SuperHearing & SuperSmell> dogBoys

    }
}
  1. 注意,通配符被限制为单一边界。

通配符

  • 我们开始入手的示例要展示数组的一种特殊行为: 可以向导出类型的数组赋予基类型的数组引用。
public class Fruit {
}

class Apple extends Fruit {
}

class Jonathan extends Apple {
}

class Orange extends Fruit {
}

class CovariantArrays {
    public static void main(String[] args) {
        Fruit[] fruits = new Apple[10];
        fruits[0] = new Apple();
        fruits[1] = new Jonathan();
        //运行时类型是 apple[] 而不是 Fruit[] 或 Orange
        try {
            //编译器允许您添加 Fruit
            //ArrayStoreException 异常
            fruits[2] = new Fruit();
        } catch (Exception e) {
            System.err.println(e);
        }
        try {
            //编译器不允许你添加 Orange
            //ArrayStoreException 异常
            fruits[3] = new Orange();
        }catch (Exception e){
            System.err.println(e);
        }
    }
}

//运行结果为
java.lang.ArrayStoreException: generic.Fruit
java.lang.ArrayStoreException: generic.Orange
  1. main() 中的第一行创建了一个Apple 数组,并将其赋值给了一个 Fruit 数组的引用。这是有意义的,因为 Apple 也是一种 Fruit ,因此Apple 数组应该也是一个 Fruit 数组。
  2. 但是,如果实际的数组类型是 Apple[] ,你应该只能在其中放置 Apple 和 Apple 的子类型,这在编译期和运行时都可以工作。但是请注意,编译器允许你将 Fruit 放置到这个数组中,这对于编译器来说是有意义的,因为它有一个 Fruit[] 引用___它有什么理由不允许将 Fruit 对象或者任何从 Fruit 集成出来的对象(如 Orange),放置到这个数组中呢?
  3. 因此,在编译期, 这是允许的,但是,运行时的数组机制知道它处理的是 Apple[] ,因此会在向数组中放置异构类型时抛出异常(ArrayStoreException)
  4. 实际上,向上转型不适合在这里。你真正做的是将一个数组赋值给另一个数组。数组的行为应该是它可以持有其他对象,这里只是因为我们能够向上转型而已,所以很明显,数组对象可以保留有关它们包含的对象类型的规则。就好像数组对它们持有的对象时有意识的,因此在编译期检查和运行时检查之间,你不能滥用它们。
  5. 对数组的这种赋值并不是那么可怕,因为在运行时可以发现你已经插入了不正确的类型。但是泛型的主要目标之一是将这种错误监测移入到编译期。因此当我们试图使用泛型容器来代替数组时,会发生什么呢?
class NonCovariantGenerics{
    //编译异常 不兼容的类型
    List<Fruit> fruitList=new ArrayList<Apple>();
}
  1. 尽管你在第一次阅读这段代码时会认为 不能将一个Apple 容器赋值给一个 Fruit容器。 别忘了,泛型不仅和容器相关正确的说法是 不能把一个涉及 Apple 泛型赋值给一个涉及 Fruit 的泛型。
  2. 如果就像在数组的情况中一样,编译器对代码的了解足够多,可以确定所涉及到的容器,那么它可能会留下一些余地。但是它不知道任何有关这方面的信息,因此它拒绝向上转型。
  3. 然而实际上这根本不是向上转型___Apple 的List 不是 Fruit 的List 。Apple 的List 将持有Apple 和 Apple 的子类型,而Fruit 的List 将持有任何类型的 Fruit ,诚然这包括 Apple 在内,但是它不是一个 Apple 的List,它人就是 Fruit 的List 。Apple 的List 在类型上不等价 Fruit 的List , 即使 Apple 是一种Fruit类型。
  • 真正的问题是我们再谈论容器的类型,而不是容器持有的类型。与数组不同,泛型没有内建的协变类型。这是因为数组在语言中是完全定义的,因此可以内建了编译期和运行时的检查,但是在使用泛型时,编译期和运行时系统都不知道你想用类型做些什么,以及应该采用什么样的规则。
class GenericAndGCovariange{
    public static void main(String[] args) {
        //wildcards allow covariance 通配符允许协方差
        List<? extends Fruit> list=new ArrayList<Apple>();
        //编译错误:无法添加任何类型的对象
        
        //list.add(new Apple());
        //list.add(new Orange());
        //list.add(new Fruit());
        
        //合法但无趣
        list.add(null);
        //我们知道它至少返回 fruit
        Fruit fruit = list.get(0);
    }
}
  1. list 类型现在是 List<? extends Fruit> , 你可以将其读作 : 具有任何从 Fruit继承的类型的列表。但是,这实际上并不意味着这个List 将持有任何类型的 Fruit。
  2. 通配符引用的是明确的类型,因此它意味着 某种list引用没有指定的具体类型。因此这个被复制的List 必须持有诸如 Fruit 或 Apple 这样的某种执行类型,但是为了向上转型为 list ,这个类型是什么并没有人关心。
  3. 如果唯一的限制是这个List 要持有某种具体的 Fruit 或 Fruit 子类型,但是你实际上并不关心它是什么,那么你能用这样的List 做什么呢?如果不知道List持有什么类型,那么你怎么才能安全地向其中添加对象呢? 就像在 CovariantArrays.java中向上转型数组一样,你不能,除非编译器而不是运行时系统可以阻止这种操作的发生。你很快就会发现这一问题。
  4. 你可能会认为,事情变得有点走极端了,因为现在你甚至不能向刚刚声明过将持有 Apple 对对象的List 中放置一个 Apple 对象了。是的,但是编译器并不知道这一点。 List<? extends Fruit> 可以合法地指向一个 List<Orange> 。一旦执行这种类型的向上转型,你就将丢失掉向其中传递任何对象的能力,甚至是传递Object 也不行。
  5. 另一方面,如果你调用一个返回Fruit 的方法,则是安全的,因为你知道在这个List中的任何对象至少具有Fruit 类型,因此编译器将允许这么做。

 

编译器有多聪明

  • 现在,你可能会猜想自己被阻止去调用任何接受参数的方法,请考虑如下程序。
class CompilerIntelligence{
    public static void main(String[] args) {
        List<? extends Fruit> list= Arrays.asList(new Apple());
        //没有警告
        Apple apple= (Apple) list.get(0);
        //Argument is Object
        list.contains(new Apple());
        list.indexOf(new Apple());

    }
}
  1. 你可以看到,对contains() 和 indexOf() 调用,这俩个方法都接受 Apple 对象作为参数,而这些调用都可以正常执行。这意味着编译器实际上将检查代码,以查看是否有某个特定的方法修改了它的对象?
  2. 通过查看ArrayList文档,我们可以发现,编译器并没有这么聪明。尽管 add() 将接受一个具有泛型参数类型的参数,但是 contains() 和 indexOf() 将接受Object 类型的参数。因此当你指定一个 ArrayList<? extends Fruit> 时, add() 的参数就变成了 ? extends Fruit 。
  3. 从这个描述中,编译器并不了解这里需要Fruit 的那个具体子类型,因此它不会接受任何类型的 Fruit。如果先将 Apple 向上转型为 Fruit ,也无关紧要___编译器将直接拒绝对参数列表中涉及通配符的方法(如 add()) 的调用。
  • 在使用 contains() 和 indexOf() 时,参数类型是Object , 因此不涉及任何通配符,而编译器也将允许这个调用。这意味着将由泛型类的涉及者来决定哪些调用是安全的,并使用Object类型作为其参数类型。为了在类型中使用了通配符的情况下禁止这类调用,我们需要在参数列表中使用类型参数。
public class Holder<T> {
    private T t;
    public Holder(T t) {
        this.t = t;
    }
    public Holder() {
    }
    public T getT() {
        return t;
    }
    public void setT(T t) {
        this.t = t;
    }
    @Override
    public boolean equals(Object object) {
        return t.equals(object);
    }

    public static void main(String[] args) {
        Holder<Apple> holder = new Holder<>(new Apple());
        Apple apple = holder.getT();
        holder.setT(apple);

        //Holder<Fruit> fruitHolder=holder;  无法向上转型
        Holder<? extends Fruit> fruit = holder;
        Fruit fruit1 = fruit.getT();
        //返回的结果是 object
        apple= (Apple) fruit.getT();
        try {
            Orange orange = (Orange) fruit.getT();
        }catch (Exception e){
            System.out.println(e);
        }
        //fruit.setT(new Apple());
        //fruit.setT(new Fruit());
        System.out.println(fruit.equals(apple));
    }
}

//运行结果为
java.lang.ClassCastException: generic.Apple cannot be cast to generic.Orange
true
  1. Holder 有一个接受 T类型对象的set() 方法,一个 get() 方法,以及一个接受 Object 对象的 equals() 方法。 
  2. 正如你已经看到的,如果创建了一个 Holder<Apple> ,不能将其向上转型为 Holder<Fruit> ,但是可以将其向上转型为 Holder<? extends Fruit> 。
  3. 如果调用 getT() ,它只会返回一个 Fruit ___这就是在给定 任何扩展自 Fruit 的对象 这一边界之后,它所能知道的一切了。
  4. 如果能够了解更多的信息,那么你可以转型到某种具体的Fruit 类型,而这不是导致任何警告,但是你存在着得到 ClassCastException 的风险。
  5. setT() 方法不能工作于 Apple 或 Fruit ,因为 setT() 的参数也是 ? extends Fruit 这意味着它可以是任何事物,而编译器无法验证任何事物的类型安全性。
  6. 但是 , equals() 方法工作良好,因为它将Object 类型而并非T 类型的参数。因此,编译器只关注传递进来和要返回的对象类型,它并不会分析代码,以查看是否执行了任何实际的写入和读取操作。

 

逆变

  • 还可以走另外一条路,即使用 超类型通配符。这里声明通配符是由某个特定类的任何基类来界定的,方法是指定 <? super MyClass>  甚至或者使用类型参数: <? super T> (尽管你不能对泛型参数给出一个超类型边界,即不能声明 <T super MyClass>)。 这使得你可以安全地传递一个类型对象到泛型类型中。
  • 因此,有了超类型通配符,就可以向 Collection写入了如下。
public class SuperTypeWildcards {
    public static void main(String[] args) {
        List<? super Apple> apples=new ArrayList<>();
        apples.add(new Apple());
        apples.add(new Jonathan());
        
        //apples.add(new Fruit()); 编译失败
    }
}
  1. 参数 Apple 是 Apple 的某种基类型的List , 这样你就知道向其中添加 Apple 或Apple 子类型是安全的。
  2. 但是,既然Apple 是下界,那么你可以知道向这样的 List 中添加 Fruit是不安全的,因为这将使这个 List 敞开口子,从而可以向其中添加非Apple类型的对象,而这时违反静态类型安全的。
  3. 因此你可能会根据如何能够向一个泛型类型 写入(传递给一个方法), 以及如何能够从一个泛型类型中 读取(从一个方法中返回) , 来着手思考子类型和超类型边界。
  4. 超类型边界放松了在可以向方法传递的参数上所作的限制。
public class GenericWriting {
    static List<Apple> apples=new ArrayList<>();
    static List<Fruit> fruits=new ArrayList<>();


    static void f1(){
        writeExact(apples,new Apple());
        writeExact(fruits,new Apple());
    }
    static <T> void writeWithWildcard(List<? super T> list,T item){
        list.add(item);
    }


    static <T> void writeExact(List<T> list,T item){
        list.add(item);
    }

    static void f2(){
        writeWithWildcard(apples,new Apple());
        writeWithWildcard(fruits,new Apple());
    }

    public static void main(String[] args) {
        f1();
        f2();
    }
}
  1. writeExact() 方法使用了一个确切参数类型(无通配符) 。 
  2. 在 writeWithWildcard() 中,其参数现在是 List<? super T> ,因此这个List 将持有从T 导出的某种具体;类型,这样就可以安全地将一个T 类型的对象或者从T 导出的任何对象作为参数传递给List 的方法。
  3. 在 f2() 中可以看到这一点,在这个方法中我们仍旧可以像前面那样,将Apple 放置到List<Apple> 中,但是现在我们可以如你所期望的那样,将Apple放置到List<Fruit>。

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章