Apakah Daftar <Dog> adalah subkelas Daftar <Animal>? Mengapa generik Java tidak secara implisit polimorfik?

770

Saya agak bingung tentang bagaimana generik Java menangani warisan / polimorfisme.

Asumsikan hierarki berikut -

Hewan (Induk)

Anjing - Kucing (Anak-Anak)

Jadi misalkan saya punya metode doSomething(List<Animal> animals). Dengan semua aturan pewarisan dan polimorfisme, saya akan berasumsi bahwa a List<Dog> adalah a List<Animal>dan a List<Cat> adalah a List<Animal>- sehingga salah satu dapat diteruskan ke metode ini. Tidak begitu. Jika saya ingin mencapai perilaku ini, saya harus secara eksplisit memberi tahu metode untuk menerima daftar subkelas Hewan apa pun dengan mengatakan doSomething(List<? extends Animal> animals).

Saya mengerti bahwa ini adalah perilaku Java. Pertanyaan saya adalah mengapa ? Mengapa polimorfisme umumnya tersirat, tetapi ketika datang ke generik itu harus ditentukan?

froadie
sumber
17
Dan pertanyaan tata bahasa yang sama sekali tidak berhubungan yang mengganggu saya sekarang - haruskah judul saya menjadi "mengapa tidak generik Java" atau "mengapa bukan generik Java"? Apakah "generik" jamak karena huruf s atau singular karena itu satu kesatuan?
froadie
25
obat generik seperti yang dilakukan di Jawa adalah bentuk polimorfisme parametrik yang sangat buruk. Jangan terlalu mempercayai mereka (seperti dulu), karena suatu hari Anda akan mengalami kesulitan yang menyedihkan: Ahli bedah mengembangkan Handable <Scalpel>, Handable <Sponge> KABOOM! Apakah tidak menghitung [TM]. Ada batasan generik Java Anda. Setiap OOA / OOD dapat diterjemahkan dengan baik ke dalam Java (dan MI dapat dilakukan dengan sangat baik menggunakan antarmuka Java) tetapi obat generik tidak memotongnya. Mereka baik untuk "koleksi" dan pemrograman prosedural yang mengatakan (yang merupakan apa yang kebanyakan programmer Java tetap melakukannya ...).
SyntaxT3rr0r
8
Kelas super Daftar <Dog> bukan Daftar <Animal> tetapi Daftar <?> (Yaitu daftar jenis yang tidak dikenal). Generik menghapus informasi jenis dalam kode yang dikompilasi. Ini dilakukan agar kode yang menggunakan generik (java 5 & di atas) kompatibel dengan versi java sebelumnya tanpa generik.
rai.skumar
9
@froadie karena sepertinya tidak ada yang merespon ... itu pasti "mengapa tidak generik Java ...". Masalah lainnya adalah bahwa "generik" sebenarnya merupakan kata sifat, dan "generik" mengacu pada kata benda jamak yang dijatuhkan yang dimodifikasi oleh "generik". Anda bisa mengatakan "fungsi itu generik", tetapi itu akan lebih rumit daripada mengatakan "fungsi itu generik". Namun, agak sulit untuk mengatakan "Java memiliki fungsi dan kelas generik", bukan hanya "Java memiliki generik". Sebagai seseorang yang menulis tesis master mereka tentang kata sifat, saya pikir Anda telah menemukan pertanyaan yang sangat menarik!
dantiston

Jawaban:

916

Tidak, List<Dog>ini bukan sebuah List<Animal>. Pertimbangkan apa yang dapat Anda lakukan dengan List<Animal>- Anda dapat menambahkan hewan apa pun ke dalamnya ... termasuk kucing. Sekarang, bisakah Anda secara logis menambahkan kucing ke sampah anak-anak anjing? Benar-benar tidak.

// 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?

Tiba-tiba Anda memiliki kucing yang sangat bingung.

Sekarang, Anda tidak dapat menambahkan a Catke List<? extends Animal>karena Anda tidak tahu itu a List<Cat>. Anda dapat mengambil nilai dan tahu bahwa itu akan menjadi Animal, tetapi Anda tidak dapat menambahkan hewan yang sewenang-wenang. Kebalikannya berlaku untuk List<? super Animal>- dalam hal ini Anda dapat menambahkannya Animaldengan aman, tetapi Anda tidak tahu apa-apa tentang apa yang bisa diambil darinya, karena itu bisa saja a List<Object>.

Jon Skeet
sumber
50
Menariknya, setiap daftar anjing adalah memang daftar hewan, seperti intuisi memberitahu kita. Intinya adalah, bahwa tidak setiap daftar hewan adalah daftar anjing, maka mutasi daftar dengan menambahkan kucing adalah masalahnya.
Ingo
66
@Ingo: Tidak, tidak juga: Anda bisa menambahkan kucing ke daftar hewan, tetapi Anda tidak bisa menambahkan kucing ke daftar anjing. Daftar anjing hanya daftar binatang jika Anda menganggapnya dalam arti hanya baca.
Jon Skeet
12
@ JonSkeet - Tentu saja, tetapi siapa yang mengamanatkan membuat daftar baru dari kucing dan daftar anjing benar-benar mengubah daftar anjing? Ini adalah keputusan implementasi yang sewenang-wenang di Jawa. Yang bertentangan dengan logika dan intuisi.
Ingo
7
@Ingo: Saya tidak akan menggunakan itu "tentu" untuk memulai. Jika Anda memiliki daftar yang mengatakan di bagian atas "Hotel yang mungkin ingin kami kunjungi" dan kemudian seseorang menambahkan kolam renang ke dalamnya, apakah menurut Anda itu valid? Tidak - ini daftar hotel, yang bukan daftar bangunan. Dan itu tidak seperti saya bahkan mengatakan "Daftar anjing bukan daftar hewan" - Saya memasukkannya dalam istilah kode , dalam font kode. Saya benar-benar tidak berpikir ada ambiguitas di sini. Lagi pula, menggunakan subkelas akan salah - ini tentang kompatibilitas penugasan, bukan subklas.
Jon Skeet
14
@ruakh: Masalahnya adalah bahwa Anda kemudian punting ke waktu eksekusi sesuatu yang dapat diblokir pada waktu kompilasi. Dan saya berpendapat bahwa array kovarians adalah kesalahan desain untuk memulai.
Jon Skeet
84

Apa yang Anda cari disebut parameter tipe kovarian . Ini berarti bahwa jika satu jenis objek dapat diganti dengan yang lain dalam suatu metode (misalnya, Animaldapat diganti dengan Dog), hal yang sama berlaku untuk ekspresi menggunakan objek tersebut (sehingga List<Animal>dapat diganti dengan List<Dog>). Masalahnya adalah bahwa kovarians tidak aman untuk daftar yang dapat berubah secara umum. Misalkan Anda memiliki List<Dog>, dan sedang digunakan sebagai List<Animal>. Apa yang terjadi ketika Anda mencoba menambahkan Kucing ke ini List<Animal>yang benar-benar aList<Dog> ? Secara otomatis memungkinkan parameter tipe menjadi kovarian merusak sistem tipe.

Akan berguna untuk menambahkan sintaks untuk memungkinkan parameter tipe ditentukan sebagai kovarian, yang menghindari ? extends Foodeklarasi metode in, tetapi hal itu menambah kompleksitas tambahan.

Michael Ekstrand
sumber
44

Alasan a List<Dog>bukan a List<Animal>, adalah bahwa, misalnya, Anda dapat memasukkan a Catke dalam List<Animal>, tetapi tidak ke List<Dog>... Anda dapat menggunakan wildcard untuk membuat obat generik lebih dapat diperluas jika memungkinkan; misalnya, membaca dari a List<Dog>mirip dengan membaca dari List<Animal>- tetapi tidak menulis.

The Generik di Jawa Bahasa dan Seksi di Generik dari Tutorial Java memiliki sangat baik, mendalam penjelasan mengapa beberapa hal yang atau tidak polimorfik atau diizinkan dengan obat generik.

Michael Aaron Safyan
sumber
36

Suatu hal yang saya pikir harus ditambahkan ke apa yang disebutkan oleh jawaban lain adalah sementara

List<Dog>bukan-a List<Animal> di Jawa

memang benar demikian

Daftar anjing adalah-daftar hewan dalam bahasa Inggris (well, dengan interpretasi yang masuk akal)

Cara intuisi OP bekerja - yang tentu saja benar-benar valid - adalah kalimat terakhir. Namun, jika kita menerapkan intuisi ini kita mendapatkan bahasa yang bukan Java-esque dalam sistem tipenya: Misalkan bahasa kita memungkinkan menambahkan kucing ke daftar anjing kita. Apa artinya itu? Ini berarti bahwa daftar tersebut tidak lagi menjadi daftar anjing, dan tetap hanya daftar binatang. Dan daftar mamalia, dan daftar binatang berkaki empat.

Dengan kata lain: A List<Dog>di Jawa tidak berarti "daftar anjing" dalam bahasa Inggris, itu berarti "daftar yang dapat memiliki anjing, dan tidak ada yang lain".

Secara umum, intuisi OP cocok dengan bahasa di mana operasi pada objek dapat mengubah tipenya , atau lebih tepatnya, tipe objek adalah fungsi (dinamis) dari nilainya.

einpoklum
sumber
Ya, bahasa manusia lebih kabur. Tapi tetap saja, begitu Anda menambahkan hewan yang berbeda ke daftar anjing, itu masih daftar binatang, tetapi bukan lagi daftar anjing. Bedanya, manusia, dengan logika fuzzy, biasanya tidak punya masalah untuk menyadarinya.
Vlasec
Sebagai seseorang yang menemukan perbandingan konstan untuk array bahkan lebih membingungkan, jawaban ini cocok untuk saya. Masalah saya adalah intuisi bahasa.
FLonLon
32

Saya akan mengatakan bahwa inti dari Generics adalah tidak mengizinkannya. Pertimbangkan situasi dengan array, yang memungkinkan jenis kovarian:

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

Kode itu mengkompilasi dengan baik, tetapi melempar kesalahan runtime ( java.lang.ArrayStoreException: java.lang.Booleandi baris kedua). Ini bukan typesafe. Inti dari Generics adalah untuk menambahkan keamanan tipe waktu kompilasi, jika tidak Anda bisa tetap menggunakan kelas biasa tanpa generik.

Sekarang ada saat-saat di mana Anda harus lebih fleksibel dan untuk itulah ? super Classdan ? extends Classuntuk apa. Yang pertama adalah ketika Anda harus memasukkan ke dalam suatu tipe Collection(misalnya), dan yang terakhir adalah ketika Anda perlu membaca darinya, dengan tipe yang aman. Tetapi satu-satunya cara untuk melakukan keduanya pada saat yang sama adalah memiliki tipe tertentu.

Yishai
sumber
13
Arguably, array covariance adalah bug desain bahasa. Perhatikan bahwa karena penghapusan tipe, perilaku yang sama secara teknis tidak mungkin untuk pengumpulan generik.
Michael Borgwardt
" Saya akan mengatakan bahwa inti dari Generics adalah tidak mengizinkan hal itu. " Anda tidak akan pernah bisa memastikan: Sistem Tipe Java dan Scala Tidak Sehat: Krisis Eksistensial Null Pointer (disajikan pada OOPSLA 2016) (karena tampaknya sudah dikoreksi)
David Tonhofer
15

Untuk memahami masalah ini, penting untuk membuat perbandingan dengan array.

List<Dog>adalah tidak subclass dari List<Animal>.
Tetapi Dog[] adalah subclass dari Animal[].

Array dapat diverifikasi dan kovarian .
Dapat diverifikasi artinya informasi tipenya tersedia sepenuhnya pada saat runtime.
Oleh karena itu array menyediakan keamanan tipe runtime tetapi tidak untuk tipe keamanan waktu kompilasi.

    // 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

Ini sebaliknya untuk obat generik:
Obat generik dihapus dan tidak tetap .
Oleh karena itu obat generik tidak dapat memberikan keamanan tipe runtime, tetapi mereka memberikan keamanan tipe waktu kompilasi.
Dalam kode di bawah ini jika obat generik bersifat kovarian, dimungkinkan untuk membuat timbunan polusi di baris 3.

    List<Dog> dogs = new ArrayList<>();
    List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
    animals.add(new Cat());
outdev
sumber
2
Dapat dikatakan bahwa, justru karena itu, Array di Jawa rusak ,
leonbloy
Array menjadi kovarian adalah "fitur" kompiler.
Cristik
6

Jawaban yang diberikan di sini tidak sepenuhnya meyakinkan saya. Jadi sebagai gantinya, saya membuat contoh lain.

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

kedengarannya bagus, bukan? Tetapi Anda hanya dapat melewati Consumers dan Suppliers untuk Animals. Jika Anda memiliki Mammalkonsumen, tetapi Duckpemasok, mereka seharusnya tidak cocok meskipun keduanya adalah hewan. Untuk melarang ini, pembatasan tambahan telah ditambahkan.

Alih-alih di atas, kita harus mendefinisikan hubungan antara tipe yang kita gunakan.

E. g.,

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

memastikan bahwa kami hanya dapat menggunakan pemasok yang memberikan kami jenis objek yang tepat untuk konsumen.

OTOH, kita juga bisa melakukannya

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

di mana kita pergi dengan cara lain: kita mendefinisikan jenis Supplierdan membatasi bahwa itu dapat dimasukkan ke dalam Consumer.

Kami bahkan bisa melakukannya

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

di mana, memiliki hubungan yang intuitif Life-> Animal-> Mammal-> Dog, Catdll., kita bahkan bisa memasukkan a Mammalke Lifekonsumen, tetapi bukan Stringke Lifekonsumen.

glglgl
sumber
1
Di antara 4 versi, # 2 mungkin salah. misalnya kita tidak bisa menyebutnya dengan (Consumer<Runnable>, Supplier<Dog>)sementara Dogadalah subtipe dariAnimal & Runnable
Zhongyu
5

Logika dasar untuk perilaku tersebut adalah yang Genericsmengikuti mekanisme penghapusan tipe. Jadi pada saat run time Anda tidak memiliki cara untuk mengidentifikasi jenis collectiontidak seperti di arraysmana tidak ada proses penghapusan seperti itu. Jadi kembali ke pertanyaan Anda ...

Jadi misalkan ada metode seperti yang diberikan di bawah ini:

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

Sekarang jika java memungkinkan penelepon untuk menambahkan Daftar jenis Hewan ke metode ini maka Anda dapat menambahkan hal yang salah ke dalam koleksi dan pada saat dijalankan juga akan berjalan karena jenis penghapusan. Sementara dalam hal array, Anda akan mendapatkan pengecualian waktu berjalan untuk skenario seperti itu ...

Jadi pada intinya perilaku ini diimplementasikan sehingga seseorang tidak dapat menambahkan hal yang salah ke dalam koleksi. Sekarang saya percaya ada penghapusan tipe untuk memberikan kompatibilitas dengan legacy java tanpa obat generik ....

Hitesh
sumber
4

Subtyping adalah invarian untuk jenis diparameterisasi. Meskipun kelas Dogadalah subtipe Animal, tipe parameter List<Dog>bukan subtipe List<Animal>. Sebaliknya, subtipe kovarian digunakan oleh array, sehingga jenis array Dog[]adalah subtipe dari Animal[].

Subtipe invarian memastikan bahwa batasan tipe yang dipaksakan oleh Java tidak dilanggar. Pertimbangkan kode berikut yang diberikan oleh @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);

Seperti yang dinyatakan oleh @Jon Skeet, kode ini ilegal, karena jika tidak maka akan melanggar batasan jenis dengan mengembalikan kucing ketika seekor anjing diharapkan.

Ini instruktif untuk membandingkan kode analog di atas untuk array.

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

Kode ini legal. Namun, melempar pengecualian toko array . Array membawa tipenya pada saat run-time dengan cara ini JVM dapat menegakkan keamanan jenis subtipe kovarian.

Untuk memahami ini lebih jauh, mari kita lihat bytecode yang dihasilkan oleh javapkelas di bawah ini:

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");
    }
}

Menggunakan perintah javap -c Demonstration, ini menunjukkan bytecode Java berikut:

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
}

Perhatikan bahwa kode yang diterjemahkan dari badan metode adalah identik. Compiler mengganti setiap tipe parameter dengan penghapusannya . Properti ini sangat penting artinya tidak merusak kompatibilitas.

Sebagai kesimpulan, keamanan run-time tidak dimungkinkan untuk tipe parameter, karena kompiler menggantikan setiap tipe parameter oleh erasure-nya. Hal ini membuat tipe parameter tidak lebih dari gula sintaksis.

Root G
sumber
3

Sebenarnya Anda dapat menggunakan antarmuka untuk mencapai apa yang Anda inginkan.

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

}

Anda kemudian dapat menggunakan koleksi menggunakan

List <Animal> animalGroup = new ArrayList<Animal>();
animalGroup.add(new Dog());
Angel Koh
sumber
1

Jika Anda yakin bahwa item daftar adalah subclass dari tipe super yang diberikan, Anda bisa melemparkan daftar menggunakan pendekatan ini:

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

Ini berguna ketika Anda ingin meneruskan daftar di konstruktor atau beralih di atasnya

sagit
sumber
2
Ini akan menciptakan lebih banyak masalah daripada yang sebenarnya dipecahkan
Ferrybig
Jika Anda mencoba untuk menambahkan Cat ke daftar, pasti akan membuat masalah, tetapi untuk tujuan perulangan saya pikir itu satu-satunya jawaban non verbose.
sagits
1

The jawaban serta jawaban lain benar. Saya akan menambahkan jawaban-jawaban itu dengan solusi yang saya pikir akan sangat membantu. Saya pikir ini sering muncul dalam pemrograman. Satu hal yang perlu diperhatikan adalah bahwa untuk Koleksi (Daftar, Set, dll.) Masalah utamanya adalah menambah Koleksi. Di situlah hal-hal rusak. Bahkan menghapus tidak apa-apa.

Dalam kebanyakan kasus, kita dapat menggunakan Collection<? extends T>lebih daripada itu Collection<T>dan itu harus menjadi pilihan pertama. Namun, saya menemukan kasus di mana tidak mudah untuk melakukannya. Masih diperdebatkan apakah itu selalu merupakan hal terbaik untuk dilakukan. Saya menyajikan di sini kelas DownCastCollection yang dapat mengambil konversi Collection<? extends T>ke a Collection<T>(kita dapat mendefinisikan kelas serupa untuk Daftar, Set, NavigableSet, ..) yang akan digunakan ketika menggunakan pendekatan standar sangat merepotkan. Di bawah ini adalah contoh bagaimana menggunakannya (kita juga bisa menggunakan Collection<? extends Object>dalam kasus ini, tetapi saya membuatnya mudah untuk diilustrasikan menggunakan 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));
}

Sekarang kelasnya:

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();

}

}

dan b
sumber
Ini adalah ide yang bagus, sehingga sudah ada di Java SE. ; )Collections.unmodifiableCollection
Radiodef
1
Benar tapi koleksi yang saya definisikan dapat dimodifikasi.
dan b
Ya, itu bisa dimodifikasi. Collection<? extends E>sudah menangani perilaku itu dengan benar, kecuali jika Anda menggunakannya dengan cara yang tidak aman-tipe (misalnya melemparkannya ke sesuatu yang lain). Satu-satunya keuntungan yang saya lihat di sana adalah, ketika Anda memanggil addoperasi, ia melempar pengecualian, bahkan jika Anda melemparkannya.
Vlasec
0

Mari kita ambil contoh dari tutorial 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) {
        ...
    }
}

Jadi mengapa daftar anjing (lingkaran) tidak boleh dianggap secara implisit daftar binatang (bentuk) adalah karena situasi ini:

// drawAll method call
drawAll(circleList);


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

Jadi Java "architects" memiliki 2 opsi yang mengatasi masalah ini:

  1. jangan menganggap bahwa subtipe secara implisit itu adalah supertype, dan memberikan kesalahan kompilasi, seperti yang terjadi sekarang

  2. pertimbangkan subtipe sebagai supertype dan batasi saat kompilasi metode "add" (jadi pada metode drawAll, jika daftar lingkaran, subtipe bentuk, akan diteruskan, kompiler harus mendeteksi itu dan membatasi Anda dengan kesalahan kompilasi untuk melakukan bahwa).

Untuk alasan yang jelas, itu memilih cara pertama.

Aurelius
sumber
0

Kita juga harus mempertimbangkan bagaimana kompiler mengancam kelas generik: di "instantiate" jenis yang berbeda setiap kali kita mengisi argumen generik.

Dengan demikian kita memiliki ListOfAnimal, ListOfDog, ListOfCat, dll, yang kelas yang berbeda yang akhirnya sampai sedang "diciptakan" oleh compiler ketika kita menentukan argumen generik. Dan ini adalah hierarki datar (sebenarnya berkaitan dengan Listbukan hirarki sama sekali).

Argumen lain mengapa kovarians tidak masuk akal dalam kasus kelas generik adalah fakta bahwa pada dasarnya semua kelas adalah sama - adalah Listcontoh. Mengkhususkan Listdengan mengisi argumen generik tidak memperluas kelas, itu hanya membuatnya bekerja untuk argumen generik tertentu.

Cristik
sumber
0

Masalahnya telah diidentifikasi dengan baik. Tapi ada solusinya; buat doSomething generik:

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

sekarang Anda dapat memanggil doSomething dengan List <Dog> atau List <Cat> atau List <Animal>.

gerardw
sumber
0

solusi lain adalah membuat daftar baru

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

Selanjutnya ke jawaban oleh Jon Skeet, yang menggunakan kode contoh ini:

// 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?

Pada level terdalam, masalahnya di sini adalah dogsdan animalsbagikan referensi. Itu berarti bahwa salah satu cara untuk membuat karya ini adalah dengan menyalin seluruh daftar, yang akan mematahkan persamaan referensi:

// 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

Setelah menelepon List<Animal> animals = new ArrayList<>(dogs);, Anda tidak dapat secara langsung menetapkan animalssalah satu dogsatau cats:

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

karena itu Anda tidak dapat memasukkan subtipe yang salah Animalke dalam daftar, karena tidak ada subtipe yang salah - objek subtipe apa ? extends Animalpun dapat ditambahkan animals.

Jelas, ini mengubah semantik, karena daftar animalsdan dogstidak lagi dibagikan, jadi menambahkan ke satu daftar tidak menambah yang lain (yang persis apa yang Anda inginkan, untuk menghindari masalah yang Catdapat ditambahkan ke daftar yang hanya seharusnya berisi Dogobjek). Juga, menyalin seluruh daftar bisa jadi tidak efisien. Namun, ini memecahkan masalah kesetaraan jenis, dengan memecah persamaan referensi.

Luke Hutchison
sumber