List <Dog>はList <Animal>のサブクラスですか?Javaジェネリックが暗黙的にポリモーフィックでないのはなぜですか?


770

Javaジェネリックスが継承/ポリモーフィズムをどのように処理するかについて少し混乱しています。

次の階層を想定-

動物(親)

- (子供)

それで私がメソッドを持っているとしましょうdoSomething(List<Animal> animals)。継承とポリモーフィズムのすべてのルールでは、私はそれを引き受けるList<Dog> List<Animal>List<Cat> あるList<Animal>ので、どちらかがこのメソッドに渡すことができます- 。そうではありません。この動作を実現したい場合は、と言って、Animalのサブクラスのリストを受け入れるようにメソッドに明示的に指示する必要がありますdoSomething(List<? extends Animal> animals)

これはJavaの動作であることを理解しています。私の質問はなぜですか?なぜポリモーフィズムは一般的に暗黙的ですが、ジェネリックになるとそれを指定する必要がありますか?


17
今私を悩ませていますし、全く関係のない文法の質問-私のタイトルは「なぜあるべきではありません Javaのジェネリック」または「理由はない Javaのジェネリック」?? 「ジェネリック」はsのため複数形ですか、それとも1つのエンティティーのため単数形ですか?
froadie

25
Javaで行われるジェネリックは、パラメトリックポリモーフィズムの非常に貧弱な形式です。(私が使用のように)1日あなたは、ハード彼らの哀れな限界を打つだろうから、それらに信仰にあまり入れないでください: 外科医はHandable <メス>、Handable <スポンジ>拡張 KABOOMを![TM]を計算しません。Javaジェネリックの制限があります。OOA / OODはJavaにうまく変換できます(そしてMIはJavaインターフェースを使用して非常にうまく実行できます)が、ジェネリックはそれをカットしません。「コレクション」や手続き型プログラミングでは問題ありません(ほとんどのJavaプログラマーがとにかくそうしています...)。
SyntaxT3rr0r 2010

8
List <Dog>のスーパークラスはList <Animal>ではなくList <?>(つまり、不明なタイプのリスト)です。ジェネリックスは、コンパイルされたコードの型情報を消去します。これは、ジェネリックス(java 5以上)を使用しているコードがジェネリックスのない以前のバージョンのjavaと互換性があるように行われます。
rai.skumar

4
関連するSOの質問- <の使い方は?<
SomeObject

9
@froadie誰も応答しないようだったので...それは間違いなく「なぜJavaの総称ではないのか...」でなければなりません。もう1つの問題は、「総称」は実際には形容詞であるため、「総称」は「総称」によって変更されたドロップされた複数名詞を指しているということです。「その関数はジェネリックです」と言うこともできますが、「その関数はジェネリック」と言うよりも面倒です。ただし、「Javaにはジェネリックがあります」ではなく、「Javaにはジェネリックな関数とクラスがあります」と言うのは少し面倒です。形容詞で修士論文を書いた人として、あなたは非常に興味深い質問に出くわしたと思います!
ダンティストン2017年

回答:


916

いいえ、List<Dog>ありませんList<Animal>。あなたが何ができるかを考えてくださいList<Animal>-あなたはそれにどんな動物も加えることができます ...猫を含みます。今、あなたは子犬のトイレに猫を論理的に追加できますか?絶対違う。

// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new ArrayList<Dog>(); // ArrayList implements List
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?

突然あなたは非常に混乱した猫を飼っています。

現在、であること知らないため、Catをに追加することはできません。値を取得して、それがになることを確認できますが、任意の動物を追加することはできません。逆の場合も同様です。その場合、に安全にを追加できますが、から取得されるものについては何もわからないので、である可能性があります。List<? extends Animal>List<Cat>AnimalList<? super Animal>AnimalList<Object>


50
興味深いことに、犬のすべてのリストがある直感を教えてくれるだけで同じように、実際に動物のリスト。ポイントは、すべての動物のリストが犬のリストであるとは限らないため、猫を追加することによるリストの変異が問題であるということです。
Ingo

66
@Ingo:いいえ、実際はそうではありません。猫を動物のリストに追加することはできますが、猫を犬のリストに追加することはできません。読み取り専用の意味で考えると、犬のリストは動物のリストにすぎません。
Jon Skeet 2013年

12
@JonSkeet-もちろんですが、猫と犬のリストから新しいリストを作成すると、実際に犬のリストが変更されることを義務付けているのは誰ですか?これは、Javaでの任意の実装決定です。論理と直感に反するもの。
2013年

7
@Ingo:そもそも「確かに」は使っていなかっただろう。上部に「行きたいホテル」というリストがあり、誰かがプールを追加した場合、それは有効だと思いますか?いいえ-ホテルのリストであり、建物のリストではありません。そして、「犬のリストは動物のリストではない」と私が言ったようではありません-私はそれをコード用語で、コードフォントで入れました。ここには曖昧さはないと思います。とにかくサブクラスの使用は正しくありません-サブクラスではなく、割り当ての互換性についてです。
Jon Skeet、2013年

14
@ruakh:問題は、コンパイル時にブロックできる何かを実行時間にパントさせていることです。そして、配列の共分散は、最初の設計ミスであったと私は主張します。
Jon Skeet 2013

84

あなたが探しているものは共変型と呼ばれていますパラメーターます。つまり、あるタイプのオブジェクトをメソッド内で別のタイプのオブジェクトAnimalに置き換えることができる(たとえば、で置き換えることができるDog)場合、それらのオブジェクトを使用する式にも同じことが適用されます(したがって、List<Animal>で置き換えることができますList<Dog>)。問題は、一般に、可変リストに対して共分散が安全でないことです。がありList<Dog>、それがとして使用されているとしList<Animal>ます。あなたはこれに猫を追加しようとすると何が起こるのList<Animal>は本当にありますかList<Dog>?型パラメーターを共変に自動的に許可すると、型システムが壊れます。

構文を追加して、型パラメーターを共変として指定できるようにすると便利です。これにより? extends Foo、メソッド内の宣言が回避されますが、これにより複雑さが増します。


44

a List<Dog>がでない理由はList<Animal>、たとえば、a Catをに挿入できますが、...には挿入できList<Animal>ないためList<Dog>です。ワイルドカードを使用して、ジェネリックを可能な限り拡張可能にすることができます。たとえば、a List<Dog>からの読み取りはaからの読み取りと似てList<Animal>いますが、書き込みではありません。

Java言語でのジェネリックJavaのチュートリアルからジェネリック医薬品のセクションには、いくつかのものがある理由としては非常に良い、綿密な説明を持っているか、多型ではないか、ジェネリックを許可しました。


36

にどのような答えに加えるべきだと思うポイント言及は、

List<Dog> じゃない List<Animal> はJavaではあり

それも真実です

犬のリストは英語の動物のリストです(まあ、合理的な解釈の下で)

OPの直感が機能する方法(もちろん完全に有効です)は後者の文です。ただし、この直感を適用すると、型システムでJava風ではない言語が得られます。私たちの言語で犬のリストに猫を追加できると仮定します。それはどういう意味ですか?これは、リストが犬のリストではなくなり、単に動物のリストのままであることを意味します。哺乳類のリストと四足動物のリスト。

別の言い方をすると、A List<Dog>、Javaのは英語で「犬のリスト」を意味するのではなく、「犬を飼うことができ、他には何もないリスト」を意味します。

より一般的には、OPの直感は、オブジェクトに対する操作がその型を変更できる言語に向いています。つまり、オブジェクトの型は、その値の(動的)関数です。


はい、人間の言葉はもっとあいまいです。ただし、犬のリストに別の動物を追加しても、それは動物のリストですが、犬のリストではなくなります。人間、ファジーロジックの違いは、通常それを実現するのに問題はありません。
Vlasec 2017年

配列との絶え間ない比較をさらに混乱させる人がいるので、この答えは私にとってそれを裏付けました。私の問題は言語の直感でした。
FLonLon

32

Genericsの要点は、それを許可しないということです。そのタイプの共分散を可能にする配列の状況を考えます。

  Object[] objects = new String[10];
  objects[0] = Boolean.FALSE;

そのコードは正常にコンパイルされますが、ランタイムエラー(java.lang.ArrayStoreException: java.lang.Boolean 2行目)。タイプセーフではありません。ジェネリックスのポイントは、コンパイル時の型安全性を追加することです。そうでない場合は、ジェネリックスのないプレーンクラスをそのまま使用できます。

現在、より柔軟に対応する必要がある場合があり、? super Classそれ? extends Classが目的です。前者はCollection(たとえば)型に挿入する必要がある場合であり、後者は型セーフな方法で型から読み取る必要がある場合です。しかし、両方を同時に行う唯一の方法は、特定のタイプを用意することです。


13
おそらく、配列の共分散は言語設計のバグです。型の消去のため、同じ動作はジェネリックコレクションでは技術的に不可能であることに注意してください。
Michael Borgwardt 2010

私はジェネリックスの要点はそれがそれを許さないということだと思います。」確信が持てない:JavaとScalaの型システムは健全ではない:ヌルポインターの存在の危機(OOPSLA 2016で提示)(修正されたように思われる)
David Tonhofer

15

問題を理解するには、配列と比較すると便利です。

List<Dog>のサブクラスではありませんList<Animal>
しかし、 Dog[] あるのサブクラスAnimal[]

配列は再構成可能で、共変です。
Reifiableは、型情報が実行時に完全に利用可能であることを意味します。
したがって、配列は実行時の型保証を提供しますが、コンパイル時の型保証は提供しません。

    // All compiles but throws ArrayStoreException at runtime at last line
    Dog[] dogs = new Dog[10];
    Animal[] animals = dogs; // compiles
    animals[0] = new Cat(); // throws ArrayStoreException at runtime

ジェネリックの逆も同様です。
ジェネリックは消去され、不変です。
したがって、ジェネリックスは実行時の型安全性を提供できませんが、コンパイル時の型安全性を提供します。
以下のコードでは、ジェネリックスが共変であった場合、3行目でヒープ汚染を引き起こす可能性があります

    List<Dog> dogs = new ArrayList<>();
    List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
    animals.add(new Cat());

2
正確にそのため、と主張される可能性がありますJavaで配列が壊れている
leonbloy

配列が共変であることはコンパイラの「機能」です。
Cristik

6

ここで与えられた答えは私を完全に納得させませんでした。代わりに、別の例を示します。

public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) {
    consumer.accept(supplier.get());
}

いいですね。ただし、渡すことができるのはConsumersとSuppliers のみですAnimalMammal消費者はいるが、Duck供給者がいる場合、どちらも動物ですが、それらは適合しないはずです。これを禁止するために、追加の制限が追加されました。

上記の代わりに、使用するタイプ間の関係を定義する必要があります。

例:

public <A extends Animal> void passOn(Consumer<A> consumer, Supplier<? extends A> supplier) {
    consumer.accept(supplier.get());
}

消費者に適切なタイプのオブジェクトを提供するサプライヤーのみを使用できることを確認します。

OTOH、私たちにもできる

public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<A> supplier) {
    consumer.accept(supplier.get());
}

ここで、他の方法を使用します。のタイプを定義し、Supplierそれをに入れることができるように制限しConsumerます。

私たちにもできる

public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<? extends A> supplier) {
    consumer.accept(supplier.get());
}

直感的な関係を持つ場合には、Life- > Animal- > Mammal- > DogCatなど、私たちも置くことができるMammalLife消費者ではなく、String中にLife消費者。


1
4つのバージョンのうち、#2はおそらく正しくありません。たとえば、(Consumer<Runnable>, Supplier<Dog>)while Dogをサブタイプとして呼び出すことはできませんAnimal & Runnable
ZhongYu '27 / 07/27

5

このような動作の基本ロジックはGenerics、型消去のメカニズムに従います。だから、実行時に、あなたはのタイプ識別場合は方法がないcollectionとは異なり、arraysそのような消去処理はありません。だからあなたの質問に戻ってきます...

したがって、以下のようなメソッドがあるとします。

add(List<Animal>){
    //You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism
}

ここで、javaが呼び出し元がこのメソッドにタイプAnimalのリストを追加することを許可する場合、コレクションに間違ったものを追加する可能性があり、実行時にもタイプの消去のために実行されます。配列の場合、そのようなシナリオではランタイム例外が発生しますが...

したがって、本質的にこの動作は実装されているため、間違ったものをコレクションに追加することはできません。今では、ジェネリックなしでレガシーJavaとの互換性を提供するために、型の消去が存在すると思います。


4

パラメータ化された型の場合、サブタイピングは不変です。クラスDogがのサブタイプであるAnimalとしても、パラメータ化されたタイプList<Dog>はのサブタイプではありませんList<Animal>。対照的に、共変サブタイプは配列で使用されるため、配列型Dog[]はのサブタイプですAnimal[]

不変のサブタイピングにより、Javaによって強制される型制約に違反しないことが保証されます。@Jon Skeetによって与えられた次のコードを考えてください:

List<Dog> dogs = new ArrayList<Dog>(1);
List<Animal> animals = dogs;
animals.add(new Cat()); // compile-time error
Dog dog = dogs.get(0);

@Jon Skeetによって述べられているように、このコードは違法です。

上記を配列の類似のコードと比較することは有益です。

Dog[] dogs = new Dog[1];
Object[] animals = dogs;
animals[0] = new Cat(); // run-time error
Dog dog = dogs[0];

コードは合法です。ただし、配列ストア例外をスローします。配列は、JVMが共変サブタイプのタイプセーフを実施できるように、実行時にそのタイプを保持します。

これをさらに理解するためjavapに、以下のクラスによって生成されたバイトコードを見てみましょう。

import java.util.ArrayList;
import java.util.List;

public class Demonstration {
    public void normal() {
        List normal = new ArrayList(1);
        normal.add("lorem ipsum");
    }

    public void parameterized() {
        List<String> parameterized = new ArrayList<>(1);
        parameterized.add("lorem ipsum");
    }
}

コマンドを使用するとjavap -c Demonstration、次のJavaバイトコードが表示されます。

Compiled from "Demonstration.java"
public class Demonstration {
  public Demonstration();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void normal();
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: iconst_1
       5: invokespecial #3                  // Method java/util/ArrayList."<init>":(I)V
       8: astore_1
       9: aload_1
      10: ldc           #4                  // String lorem ipsum
      12: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      17: pop
      18: return

  public void parameterized();
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: iconst_1
       5: invokespecial #3                  // Method java/util/ArrayList."<init>":(I)V
       8: astore_1
       9: aload_1
      10: ldc           #4                  // String lorem ipsum
      12: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      17: pop
      18: return
}

メソッド本体の変換されたコードが同一であることを確認します。コンパイラーは、パラメーター化された各タイプを消去で置き換えました。このプロパティは、下位互換性を損なうものではなかったことを意味します。

結論として、コンパイラはパラメータ化された各型を消去によって置き換えるため、パラメータ化された型の実行時の安全性は不可能です。これにより、パラメーター化された型は構文糖にすぎません。


3

実際、あなたはあなたが望むものを達成するためにインターフェースを使うことができます。

public interface Animal {
    String getName();
    String getVoice();
}
public class Dog implements Animal{
    @Override 
    String getName(){return "Dog";}
    @Override
    String getVoice(){return "woof!";}

}

あなたはそれからコレクションを使うことができます

List <Animal> animalGroup = new ArrayList<Animal>();
animalGroup.add(new Dog());

1

リスト項目がその特定のスーパータイプのサブクラスであることが確実な場合は、このアプローチを使用してリストをキャストできます。

(List<Animal>) (List<?>) dogs

これは、リストをコンストラクターで渡すか、反復する場合に役立ちます。


2
これにより、実際に解決するよりも多くの問題が発生します
Ferrybig

リストに猫を追加しようとすると、問題が発生することを確認してください。ただし、ループの目的では、これが唯一の非冗長な答えだと思います。
16

1

答え だけでなく、他の答えは正しいです。これらの答えに役立つと思う解決策を追加します。これはプログラミングで頻繁に発生すると思います。注意すべき点の1つは、コレクション(リスト、セットなど)の主な問題はコレクションへの追加です。それは物事が壊れるところです。取り外してもOKです。

ほとんどの場合、Collection<? extends T>代わりに使用することができ、それがCollection<T>最初の選択です。しかし、それが簡単ではないケースを見つけています。それが常に最善の方法であるかどうかについては、議論の余地があります。ここでは、標準のアプローチを使用する場合に使用するa Collection<? extends T>をaに変換できるCollection<T>(List、Set、NavigableSetなどの同様のクラスを定義できます)DownCastCollectionクラスを提示します。以下は、それを使用する方法の例です(Collection<? extends Object>この場合も使用できますが、DownCastCollectionの使用を説明するために簡単にしています。

/**Could use Collection<? extends Object> and that is the better choice. 
* But I am doing this to illustrate how to use DownCastCollection. **/

public static void print(Collection<Object> col){  
    for(Object obj : col){
    System.out.println(obj);
    }
}
public static void main(String[] args){
  ArrayList<String> list = new ArrayList<>();
  list.addAll(Arrays.asList("a","b","c"));
  print(new DownCastCollection<Object>(list));
}

今クラス:

import java.util.AbstractCollection;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;

public class DownCastCollection<E> extends AbstractCollection<E> implements Collection<E> {
private Collection<? extends E> delegate;

public DownCastCollection(Collection<? extends E> delegate) {
    super();
    this.delegate = delegate;
}

@Override
public int size() {
    return delegate ==null ? 0 : delegate.size();
}

@Override
public boolean isEmpty() {
    return delegate==null || delegate.isEmpty();
}

@Override
public boolean contains(Object o) {
    if(isEmpty()) return false;
    return delegate.contains(o);
}
private class MyIterator implements Iterator<E>{
    Iterator<? extends E> delegateIterator;

    protected MyIterator() {
        super();
        this.delegateIterator = delegate == null ? null :delegate.iterator();
    }

    @Override
    public boolean hasNext() {
        return delegateIterator != null && delegateIterator.hasNext();
    }

    @Override
    public  E next() {
        if(!hasNext()) throw new NoSuchElementException("The iterator is empty");
        return delegateIterator.next();
    }

    @Override
    public void remove() {
        delegateIterator.remove();

    }

}
@Override
public Iterator<E> iterator() {
    return new MyIterator();
}



@Override
public boolean add(E e) {
    throw new UnsupportedOperationException();
}

@Override
public boolean remove(Object o) {
    if(delegate == null) return false;
    return delegate.remove(o);
}

@Override
public boolean containsAll(Collection<?> c) {
    if(delegate==null) return false;
    return delegate.containsAll(c);
}

@Override
public boolean addAll(Collection<? extends E> c) {
    throw new UnsupportedOperationException();
}

@Override
public boolean removeAll(Collection<?> c) {
    if(delegate == null) return false;
    return delegate.removeAll(c);
}

@Override
public boolean retainAll(Collection<?> c) {
    if(delegate == null) return false;
    return delegate.retainAll(c);
}

@Override
public void clear() {
    if(delegate == null) return;
        delegate.clear();

}

}


これは良いアイデアであり、Java SEにはすでに存在しています。; )Collections.unmodifiableCollection
Radiodef

1
そうですが、私が定義するコレクションは変更できます。
dan b

はい、変更できます。Collection<? extends E>ただし、タイプセーフではない方法で使用しない限り(たとえば、他の何かにキャストするなど)、その動作はすでに正しく処理されています。私が見る唯一の利点は、addオペレーションを呼び出すと、それをキャストしても例外がスローされることです。
Vlasec 2017年

0

JavaSE チュートリアルの例を見てみましょう

public abstract class Shape {
    public abstract void draw(Canvas c);
}

public class Circle extends Shape {
    private int x, y, radius;
    public void draw(Canvas c) {
        ...
    }
}

public class Rectangle extends Shape {
    private int x, y, width, height;
    public void draw(Canvas c) {
        ...
    }
}

したがって、犬(円)のリストを暗黙的に動物(形)のリストと見なすべきではないのは、この状況が原因です。

// drawAll method call
drawAll(circleList);


public void drawAll(List<Shape> shapes) {
   shapes.add(new Rectangle());    
}

したがって、Javaの「アーキテクト」には、この問題に対処する2つのオプションがありました。

  1. サブタイプが暗黙的にスーパータイプであることを考慮せず、今のようにコンパイルエラーを発生させる

  2. サブタイプをスーパータイプと見なし、「add」メソッドのコンパイル時に制限します(drawAllメソッドで、円のリスト、シェイプのサブタイプが渡される場合、コンパイラーはそれを検出し、コンパイルエラーで実行を制限します。それ)。

明らかな理由から、それが最初の方法を選びました。


0

コンパイラがジェネリッククラスを脅かす方法も考慮する必要があります。ジェネリック引数を入力するたびに、異なるタイプを「インスタンス化」します。

したがって、我々が持っているListOfAnimalListOfDogListOfCat、など、最後まで我々は、一般的な引数を指定して、コンパイラによって「作成」されていることを別個のクラスです。そして、これはフラットな階層です(実際にListは、階層ではありません)。

ジェネリッククラスの場合に共分散が意味をなさないもう1つの理由は、ベースではすべてのクラスが同じであり、Listインスタンスであるという事実です。Listジェネリック引数を入力してa を特殊化しても、クラスは拡張されません。特定のジェネリック引数で機能するだけです。


0

問題は十分に特定されています。しかし、解決策があります。作るdoSomethingのが一般的な:

<T extends Animal> void doSomething<List<T> animals) {
}

これで、DoSomethingをList <Dog>、List <Cat>、またはList <Animal>のいずれかで呼び出すことができます。


0

別の解決策は、新しいリストを作成することです

List<Dog> dogs = new ArrayList<Dog>(); 
List<Animal> animals = new ArrayList<Animal>(dogs);
animals.add(new Cat());

0

このコード例を使用するJon Skeetによる回答に加えて:

// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new ArrayList<Dog>(); // ArrayList implements List
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?

最も深いレベルでは、ここでの問題はそれでdogsありanimals、参照を共有します。つまり、この作業を行う1つの方法は、リスト全体をコピーすることです。これにより、参照の等価性が失われます。

// This code is fine
List<Dog> dogs = new ArrayList<Dog>();
dogs.add(new Dog());
List<Animal> animals = new ArrayList<>(dogs); // Copy list
animals.add(new Cat());
Dog dog = dogs.get(0);   // This is fine now, because it does not return the Cat

呼び出した後List<Animal> animals = new ArrayList<>(dogs);、あなたはその後、直接割り当てることができませんanimalsどちらかにdogscats

// These are both illegal
dogs = animals;
cats = animals;

したがって、間違ったサブタイプAnimalがないため、リストに間違ったサブタイプを入れることはできません。サブタイプのオブジェクトを? extends Animalに追加できますanimals

明らかに、これはリスト以来、セマンティクスを変更animalsし、dogsもはやという問題を回避するために、正確に何をしたいである(他に追加されることはありませんつのリストに追加して、共有されていないCatだけで、リストに追加することができDogオブジェクトを含むことになっています)。また、リスト全体をコピーすると非効率になる場合があります。ただし、これは参照の等価性を壊すことにより、型の等価性の問題を解決します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.