代码之家  ›  专栏  ›  技术社区  ›  fastcodejava

有什么简单的方法可以解释为什么我不能列出动物列表吗?[复制品]

  •  22
  • fastcodejava  · 技术社区  · 15 年前

    我知道为什么不应该那样做。但有没有办法向外行解释为什么这是不可能的。你可以很容易地向外行解释: Animal animal = new Dog(); . 狗是一种动物,但狗的单子不是动物的单子。

    13 回复  |  直到 12 年前
        1
  •  47
  •   Brian Agnew    15 年前

    假设您创建了一个 . 然后你声明这是 列表<动物> 把它交给同事。他, 不合理 ,相信他可以 在里面。

    然后他把它还给你,你现在有一个清单 ,用 在中间。混乱接踵而至。

    需要注意的是,由于列表的易变性,这个限制是存在的。例如,在scala中,可以声明 是一个列表 动物 . 这是因为scala列表(默认情况下)是不可变的,因此添加 一份清单 会给你一个 新的 名单 动物 .

        2
  •  8
  •   Dan Puzey    15 年前

    你要找的答案是关于协方差和反方差的概念。有些语言支持这些功能(例如,net 4增加了对这些功能的支持),但是一些基本问题可以通过如下代码来说明:

    List<Animal> animals = new List<Dog>();
    
    animals.Add(myDog); // works fine - this is a list of Dogs
    animals.Add(myCat); // would compile fine if this were allowed, but would crash!
    

    因为猫是从动物身上派生出来的,所以编译时检查会建议将其添加到列表中。但是,在运行时,不能将猫添加到狗的列表中!

    所以,虽然看起来直观上很简单,但这些问题实际上是非常复杂的。

    下面是.net 4中co/contrance的msdn概述: http://msdn.microsoft.com/en-us/library/dd799517(VS.100).aspx 这也适用于Java,虽然我不知道Java的支持是什么样的。

        3
  •  8
  •   Carl Manaster    15 年前

    我能给出的最好的回答是: 因为在设计泛型时,他们不想重复对Java的数组类型系统做出的不安全的决定。 .

    这在数组中是可能的:

    Object[] objArray = new String[] { "Hello!" };
    objArray[0] = new Object();
    

    这段代码编译得很好 因为数组的类型系统在Java中工作。它会引起 ArrayStoreException 在运行时。

    决定不允许泛型出现这种不安全的行为。

    另见: Java Arrays Break Type Safety ,许多人认为 Java Design Flaws .

        4
  •  5
  •   Reverend Gonzo    15 年前

    你要做的是:

    List<? extends Animal> animals = new ArrayList<Dog>()
    

    这应该管用。

        5
  •  4
  •   Thomas Padron-McCarthy    15 年前

    列表<动物> 是可以插入任何动物(例如猫或章鱼)的对象。安 阵列列表<狗> 不是。

        6
  •  4
  •   Ramon Leon    15 年前

    我想说最简单的答案是忽略猫和狗,它们是不相关的。重要的是名单本身。

    List<Dog> 
    

    List<Animal> 
    

    是不同的类型,那只狗是从动物身上衍生出来的,与此毫无关系。

    此语句无效

    List<Animal> dogs = new List<Dog>();
    

    因为同样的原因这个是

    AnimalList dogs = new DogList();
    

    虽然dog可以从animal继承,但是

    列表<动物>
    

    不是从由

    List<Dog>
    

    假设因为两个类是相关的,所以将它们用作泛型参数会使这些泛型类也相关,这是错误的。你当然可以在

    List<Animal>
    

    这并不意味着

    列表<狗>
    

    列表<动物>
    
        7
  •  3
  •   AakashM    15 年前

    假设你 能够 这样做。有人交给我的东西之一 List<Animal> 可以合理预期的是 Giraffe 对它。如果有人试图添加 长颈鹿 animals ?运行时错误?这似乎违背了编译时输入的目的。

        8
  •  2
  •   Rune    15 年前

    注意如果你有

    List<Dog> dogs = new ArrayList<Dog>()
    

    如果你能的话

    List<Animal> animals = dogs;
    

    这确实 dogs 变成一个 List<Animal> . 动物的数据结构仍然是 ArrayList<Dog> ,因此如果尝试插入 Elephant 进入之内 animals ,实际上是将它插入 阵列列表<狗> 这是行不通的(大象显然太大了;-)。

        9
  •  2
  •   retronym    15 年前

    首先,让我们来定义我们的动物王国:

    interface Animal {
    }
    
    class Dog implements Animal{
        Integer dogTag() {
            return 0;
        }
    }
    
    class Doberman extends Dog {        
    }
    

    考虑两个参数化接口:

    interface Container<T> {
        T get();
    }
    
    interface Comparator<T> {
        int compare(T a, T b);
    }
    

    以及实现这些 T Dog .

    class DogContainer implements Container<Dog> {
        private Dog dog;
    
        public Dog get() {
            dog = new Dog();
            return dog;
        }
    }
    
    class DogComparator implements Comparator<Dog> {
        public int compare(Dog a, Dog b) {
            return a.dogTag().compareTo(b.dogTag());
        }
    }
    

    在这种情况下,你的要求是很合理的 Container 接口:

    Container<Dog> kennel = new DogContainer();
    
    // Invalid Java because of invariance.
    // Container<Animal> zoo = new DogContainer();
    
    // But we can annotate the type argument in the type of zoo to make
    // to make it co-variant.
    Container<? extends Animal> zoo = new DogContainer();
    

    那么为什么Java不自动执行这个操作呢?想想这意味着什么 Comparator .

    Comparator<Dog> dogComp = new DogComparator();
    
    // Invalid Java, and nonsensical -- we couldn't use our DogComparator to compare cats!
    // Comparator<Animal> animalComp = new DogComparator();
    
    // Invalid Java, because Comparator is invariant in T
    // Comparator<Doberman> dobermanComp = new DogComparator();
    
    // So we introduce a contra-variance annotation on the type of dobermanComp.
    Comparator<? super Doberman> dobermanComp = new DogComparator();
    

    如果Java自动允许 Container<Dog> 分配给 Container<Animal> ,人们还希望 Comparator<Dog> 可以分配给 Comparator<Animal> ,这毫无意义——怎么可能 比较仪<Dog> 比较两只猫?

    那么 集装箱 比较器 ?集装箱 生产 类型的值 T 比较器 消耗 他们。这些对应于 协变的 反变异体 类型参数的用法。

    有时类型参数在两个位置都使用,使接口 不变量 .

    interface Adder<T> {
       T plus(T a, T b);
    }
    
    Adder<Integer> addInt = new Adder<Integer>() {
       public Integer plus(Integer a, Integer b) {
            return a + b;
       }
    };
    Adder<? extends Object> aObj = addInt;
    // Obscure compile error, because it there Adder is not usable
    // unless T is invariant.
    //aObj.plus(new Object(), new Object());
    

    出于向后兼容性的原因,Java默认值为 不变性 . 必须显式选择与 ? extends X ? super X 变量、字段、参数或方法返回的类型。

    这真是个麻烦——每次有人使用A泛型类型时,他们都必须做出这个决定!当然是 集装箱 比较器 应该可以一劳永逸地宣布这件事。

    这称为“声明站点差异”,在scala中可用。

    trait Container[+T] { ... }
    trait Comparator[-T] { ... }
    
        10
  •  2
  •   Edward Kmett    15 年前

    如果你不能改变列表,那么你的推理就完全正确了。不幸的是 List<> 被强制操纵。也就是说你可以换一个 List<Animal> 通过添加新的 Animal 对它。如果你被允许使用 List<Dog> 作为一个 列表<动物> 你可能会得到一个列表,其中还包含 Cat .

    如果 列表& lt; 无法突变(如scala),那么你可以治疗 列表<狗> 作为一个 列表<动物> . 例如,使用协变和逆变泛型参数,c使这种行为成为可能。

    这是一个更普遍的例子 Liskov substitution principal .

    基因突变导致的问题在别处也会发生。考虑类型 Square Rectangle .

    是一个 正方形 矩形 ?当然——从数学的角度来看。

    你可以定义 矩形 提供可读性的类 getWidth getHeight 性质。

    你甚至可以添加计算 area perimeter ,基于这些属性。

    你可以定义一个 正方形 子类的类 矩形 使两者都 获得宽度 小精灵 返回相同的值。

    但是当你开始允许通过 setWidth setHeight ?

    现在, 正方形 不再是 矩形 . 为了保持不变量,对这些属性中的一个进行变异就必须默默地改变另一个属性,而liskov的替换原理将被违反。更改a的宽度 正方形 会产生意想不到的副作用。为了保持正方形,你也必须改变高度,但你只要求改变宽度!

    你不能用你的 正方形 只要你能用 矩形 . 所以, 在有突变的情况下 正方形 不是一个 矩形 !

    你可以用一种新方法 矩形 知道如何用新的宽度或高度克隆矩形,然后 正方形 可以安全地移交给 矩形 在克隆过程中,但现在不再对原始值进行变异。

    类似地 列表<狗> 不能是 列表<动物> 当它的接口允许您向列表中添加新项时。

        11
  •  1
  •   helpermethod    15 年前

    这是因为泛型类型是 invariant .

        12
  •  0
  •   Glen Best    12 年前

    英文答案:

    如果 List<Dog> 是一个 List<Animal> ,前者必须支持(继承)后者的所有操作。增加一只猫可以做到后者,但不是前者。所以“是A”关系失败了。

    编程答案:

    类型安全

    一种保守的语言默认设计选择,可阻止此损坏:

    List<Dog> dogs = new List<>();
    dogs.add(new Dog("mutley"));
    List<Animal> animals = dogs;
    animals.add(new Cat("felix"));  
    // Yikes!! animals and dogs refer to same object.  dogs now contains a cat!!
    

    为了有一个子类型关系,必须使用“castability/“subsitability”标准。

    1. 合法对象子副本-Decentant支持对祖先的所有操作:

      // Legal - one object, two references (cast to different type)
      Dog dog = new Dog();
      Animal animal = dog;  
      
    2. 合法的集合替换-在子代上支持对祖先的所有操作:

      // Legal - one object, two references (cast to different type)
      List<Animal> list = new List<Animal>()
      Collection<Animal> coll = list;  
      
    3. 非法的泛型替换(类型参数的转换)-decentant中不支持的操作:

      // Illegal - one object, two references (cast to different type), but not typesafe
      List<Dog> dogs = new List<Dog>()
      List<Animal> animals = list;  // would-be ancestor has broader ops than decendant
      

    然而

    根据泛型类的设计,类型参数可以在“安全位置”中使用,这意味着强制转换/替换有时可以成功,而不会破坏类型安全性。协方差意味着一般的重复 G<U> 可以替代 G<T> 如果u是t的同一类型或子类型,则表示泛型实例。 G&L.; 能取代 G&T;T & GT; 如果U是T的同一类型或超类型,这是两种情况下的安全位置:

    • 协变位置:

      • 方法返回类型 (泛型类型的输出)-子类型必须具有同等/更严格的限制,因此其返回类型符合祖先
      • 不可变字段的类型 (由所有者类设置,然后是“仅内部输出”)—子类型必须更严格,因此当它们设置不可变字段时,它们符合祖先

      在这些情况下,可以安全地允许将类型参数替换为像这样的decentant:

      SomeCovariantType<Dog> decendant = new SomeCovariantType<>;
      SomeCovariantType<? extends Animal> ancestor = decendant;
      

      通配符加上“extends”表示使用站点指定的协方差。

    • 控制位置:

      • 方法参数类型 (输入到泛型类型)-子类型必须同样/更适合,这样在传递祖先的参数时它们不会中断
      • 类型参数上界 (内部类型实例化)-子类型必须同样/更适合,这样当祖先设置变量值时它们不会中断

      在这些情况下,允许将类型参数替换为具有如下祖先的类型参数是安全的:

      SomeContravariantType<Animal> decendant = new SomeContravariantType<>;
      SomeContravariantType<? super Dog> ancestor = decendant;
      

      通配符加上“super”表示使用站点指定的反方差。

    使用这两个习惯用法需要开发人员付出额外的努力和注意,以获得“可替代性能力”。Java需要手动开发人员努力确保类型参数在共变/逆变位中真正使用(因此类型安全)。我不知道为什么-例如scala编译器会检查这个:-/。你基本上是在告诉编译器‘相信我,我知道我在做什么,这是类型安全的’。

    • 不变位置

      • 可变字段类型 (内部输入和输出)-可以由所有祖先类和子类读写-读是协变的,写是逆变的;结果是不变的
      • (如果类型参数同时用于协变和逆变位置,则这将导致不变性)
        13
  •  -1
  •   Vibin Jith    15 年前

    通过继承,实际上是为几个类创建公共类型。这里有一种常见的动物类型。通过在动物类型中创建数组并保留类似类型(继承类型狗、猫等)的值,可以使用它。

    如:

     dim animalobj as new List(Animal)
      animalobj(0)=new dog()
       animalobj(1)=new Cat()
    

    ……

    知道了?