Czy List <Dog> jest podklasą List <Animal>? Dlaczego generyczne produkty Java nie są domyślnie polimorficzne?

770

Jestem trochę zdezorientowany, jak generycy Java radzą sobie z dziedziczeniem / polimorfizmem.

Załóżmy następującą hierarchię -

Animal (Parent)

Pies - Kot (Dzieci)

Załóżmy, że mam metodę doSomething(List<Animal> animals). Przez wszystkich zasad dziedziczenia i polimorfizmu, chciałbym założyć, że List<Dog> jestList<Animal> i List<Cat> jestList<Animal> - a więc albo można być przekazywane do tej metody. Skąd. Jeśli chcę osiągnąć takie zachowanie, muszę wyraźnie powiedzieć metodzie, aby zaakceptowała listę dowolnej podklasy Zwierząt, mówiąc doSomething(List<? extends Animal> animals).

Rozumiem, że takie jest zachowanie Javy. Moje pytanie brzmi: dlaczego ? Dlaczego polimorfizm jest na ogół domniemany, ale jeśli chodzi o leki generyczne, należy je określić?

froadie
źródło
17
I zupełnie niepowiązane z gramatyką pytanie, które mnie teraz niepokoi - czy mój tytuł powinien brzmieć „dlaczego nie są to ogólne rodzaje Javy” lub „dlaczego nie są ogólne” Czy „rodzajowe” jest w liczbie mnogiej z powodu s czy liczby pojedynczej, ponieważ jest to jeden byt?
froadie
25
generics, takie jak wykonane w Javie, są bardzo słabą formą parametrycznego polimorfizmu. Nie pokładaj w nich zbyt wiele wiary (jak kiedyś), ponieważ pewnego dnia mocno uderzysz w ich żałosne ograniczenia: chirurg rozszerza Handable <Scalpel>, Handable <Sponge> KABOOM! Sposób nie obliczyć [TM]. Jest twoje ogólne ograniczenie Java. Każde OOA / OOD można dobrze przetłumaczyć na Javę (a MI można zrobić bardzo ładnie za pomocą interfejsów Java), ale generyczne po prostu tego nie wycinają. Nadają się do „kolekcji” i programowania proceduralnego, które mówi (i tak robi większość programistów Java, więc ...).
Składnia T3rr0r
8
Super klasa List <Dog> nie jest Listą <Animal>, ale Listą <?> (Tj. Listą nieznanego typu). Generics usuwa informacje o typie w skompilowanym kodzie. Odbywa się to w taki sposób, aby kod korzystający z generics (java 5 i wyżej) był zgodny z wcześniejszymi wersjami java bez generics.
rai.skumar
9
@froadie, ponieważ nikt nie wydawał się odpowiadać ... zdecydowanie powinno brzmieć „dlaczego nie są to ogólne elementy Javy…”. Innym problemem jest to, że „rodzajowy” jest w rzeczywistości przymiotnikiem, a zatem „rodzajowy” odnosi się do rzeczownika w liczbie mnogiej zmodyfikowanej przez „rodzajowy”. Można powiedzieć „ta funkcja jest ogólna”, ale byłoby to bardziej kłopotliwe niż powiedzenie „ta funkcja jest ogólna”. Jednak nieco trudniej jest powiedzieć „Java ma ogólne funkcje i klasy”, a nie tylko „Java ma ogólne”. Jako ktoś, kto napisał pracę magisterską na temat przymiotników, myślę, że natknąłeś się na bardzo interesujące pytanie!
dantiston

Odpowiedzi:

916

Nie, List<Dog>to nieList<Animal> . Zastanów się, co możesz zrobić z List<Animal>- możesz do niego dodać dowolne zwierzę ... w tym kota. Czy możesz logicznie dodać kota do miotu szczeniąt? Absolutnie nie.

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

Nagle masz bardzo zdezorientowanego kota.

Teraz nie możesz dodać Catdo, List<? extends Animal>ponieważ nie wiesz, że to List<Cat>. Możesz odzyskać wartość i wiedzieć, że będzie Animal, ale nie możesz dodawać dowolnych zwierząt. Odwrotnie jest List<? super Animal>w przypadku - w takim przypadku możesz Animalbezpiecznie do niego dodać , ale nie wiesz nic o tym, co można z niego odzyskać, ponieważ może to być List<Object>.

Jon Skeet
źródło
50
Co ciekawe, każda lista psów jest rzeczywiście listą zwierząt, tak jak mówi nam intuicja. Chodzi o to, że nie każda lista zwierząt jest listą psów, stąd problemem jest mutatacja listy przez dodanie kota.
Ingo
66
@Ingo: Nie, niezupełnie: możesz dodać kota do listy zwierząt, ale nie możesz dodać kota do listy psów. Lista psów jest tylko listą zwierząt, jeśli rozpatrzysz ją tylko do odczytu.
Jon Skeet
12
@JonSkeet - Oczywiście, ale kto nakazuje, aby tworzenie nowej listy od kota i listy psów faktycznie zmienia listę psów? Jest to arbitralna decyzja implementacyjna w Javie. Taki, który jest sprzeczny z logiką i intuicją.
Ingo
7
@Ingo: Na początku nie użyłbym tego „na pewno”. Jeśli masz listę z napisem „Hotele, do których chcielibyśmy się udać”, a następnie ktoś dodał basen, czy uważasz, że to jest ważne? Nie - to lista hoteli, która nie jest listą budynków. I to nie tak, że nawet powiedziałem „Lista psów nie jest listą zwierząt” - umieszczam ją w kategoriach kodowych, czcionką kodową. Naprawdę nie sądzę, żeby była tu jakaś dwuznaczność. Korzystanie z podklasy i tak byłoby niepoprawne - chodzi o zgodność przypisań, a nie podklasę.
Jon Skeet
14
@ruakh: Problem polega na tym, że wykonujesz kurs do czasu wykonania czegoś, co można zablokować w czasie kompilacji. I twierdzę, że kowariancja tablicowa była początkowo błędem projektowym.
Jon Skeet
84

To, czego szukasz, nazywa się parametrami typu kowariantnego . Oznacza to, że jeśli jeden typ obiektu można zastąpić innym w metodzie (na przykład Animalmożna go zastąpić Dog), to samo dotyczy wyrażeń używających tych obiektów (więc List<Animal>można je zastąpić List<Dog>). Problem polega na tym, że kowariancja nie jest ogólnie bezpieczna dla list zmiennych. Załóżmy, że masz List<Dog>, i jest używany jako List<Animal>. Co się stanie, gdy spróbujesz dodać do tego kota, List<Animal>który jest naprawdę List<Dog>? Automatyczne zezwolenie na współzmienne parametrów typu powoduje uszkodzenie systemu typów.

Przydałoby się dodać składnię, aby umożliwić określenie parametrów typu jako kowariantnych, co pozwala uniknąć ? extends Foodeklaracji metod, ale powoduje dodatkową złożoność.

Michael Ekstrand
źródło
44

Powodem a List<Dog>nie jest to List<Animal>, że na przykład możesz wstawić a Catdo List<Animal>, ale nie do List<Dog>... możesz użyć symboli wieloznacznych, aby w miarę możliwości generyczne były bardziej rozszerzalne; na przykład czytanie z a List<Dog>jest podobne do czytania z List<Animal>- ale nie pisania.

Informacje ogólne w języku Java oraz sekcja Ogólne informacje z samouczków Java zawierają bardzo dobre, dogłębne wyjaśnienie, dlaczego niektóre rzeczy są lub nie są polimorficzne lub dozwolone w przypadku leków ogólnych.

Michael Aaron Safyan
źródło
36

Myślę, że należy wspomnieć o tym, o czym wspominają inne odpowiedzi, że w tym czasie

List<Dog>nie jest List<Animal> w Javie

prawdą jest również to

Lista psów to-lista zwierząt po angielsku (cóż, przy rozsądnej interpretacji)

Sposób, w jaki działa intuicja OP - co oczywiście jest całkowicie aktualne - to drugie zdanie. Jeśli jednak zastosujemy tę intuicję, otrzymamy język, który nie jest typowy dla języka Java w swoim systemie typów: Załóżmy, że nasz język pozwala na dodanie kota do naszej listy psów. Co to by znaczyło? Oznaczałoby to, że lista przestaje być listą psów i pozostaje jedynie listą zwierząt. I lista ssaków i lista czworokątów.

Innymi słowy: List<Dog>w języku Java nie oznacza „listy psów” w języku angielskim, oznacza „listę, która może mieć psy i nic więcej”.

Mówiąc bardziej ogólnie, intuicja OP nadaje się do języka, w którym operacje na obiektach mogą zmieniać ich typ , a raczej typ (typy) obiektu jest (dynamiczną) funkcją jego wartości.

einpoklum
źródło
Tak, ludzki język jest bardziej niewyraźny. Jednak po dodaniu innego zwierzęcia do listy psów nadal jest to lista zwierząt, ale nie jest to już lista psów. Różnica polega na tym, że człowiek, z logiką rozmytą, zwykle nie ma problemu z uświadomieniem sobie tego.
Vlasec
Jako osoba, która uważa, że ​​ciągłe porównywanie tablic jest jeszcze bardziej mylące, ta odpowiedź mnie zaskoczyła. Moim problemem była intuicja językowa.
FLonLon
32

Powiedziałbym, że sednem Generics jest to, że na to nie pozwala. Rozważ sytuację z tablicami, które pozwalają na tego rodzaju kowariancję:

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

Ten kod kompiluje się dobrze, ale generuje błąd czasu wykonywania ( java.lang.ArrayStoreException: java.lang.Booleanw drugim wierszu). Nie jest bezpieczny. Celem Generics jest dodanie bezpieczeństwa typu kompilacji w czasie, w przeciwnym razie możesz po prostu trzymać się zwykłej klasy bez generics.

Teraz są czasy, gdzie trzeba być bardziej elastycznym i to, co ? super Classi ? extends Classsłużą. Pierwsze z nich ma miejsce, gdy trzeba wstawić do typu Collection(na przykład), a drugie, gdy trzeba z niego czytać, w sposób bezpieczny dla typu. Ale jedynym sposobem na zrobienie obu naraz jest posiadanie określonego typu.

Yishai
źródło
13
Prawdopodobnie kowariancja tablicowa jest błędem w projektowaniu języka. Należy pamiętać, że z powodu usunięcia typu to samo zachowanie jest technicznie niemożliwe w przypadku zbioru ogólnego.
Michael Borgwardt,
Powiedziałbym, że sednem Generics jest to, że na to nie pozwala ”. Nigdy nie możesz być pewien: systemy typów Javy i Scali są niesłyszalne: egzystencjalny kryzys wskaźników zerowych (przedstawiony na OOPSLA 2016) (wydaje się, że poprawiony)
David Tonhofer
15

Aby zrozumieć problem, warto porównać tablice.

List<Dog>nie jest podklasą List<Animal>.
Ale Dog[] jest podklasą Animal[].

Tablice są powtarzalne i kowariantne .
Reifiable oznacza, że ​​informacje o ich typie są w pełni dostępne w czasie wykonywania.
Dlatego tablice zapewniają bezpieczeństwo typu wykonawczego, ale nie bezpieczeństwo typu kompilacji.

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

I odwrotnie, w przypadku generycznych:
Generyczne są usuwane i niezmienne .
Dlatego generyczne nie mogą zapewnić bezpieczeństwa typu środowiska wykonawczego, ale zapewniają bezpieczeństwo typu kompilacji.
W poniższym kodzie, jeśli zmienne ogólne byłyby kowariantne, możliwe będzie zanieczyszczenie hałdy na linii 3.

    List<Dog> dogs = new ArrayList<>();
    List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
    animals.add(new Cat());
przewyższyć
źródło
2
Można argumentować, że właśnie z tego powodu tablice w Javie są zepsute ,
leonbloy
Tablice będące kowariantami to „funkcja” kompilatora.
Cristik
6

Odpowiedzi tutaj podane nie do końca mnie przekonały. Zamiast tego robię inny przykład.

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

brzmi dobrze, prawda? Ale można przekazać tylko ConsumerS i SupplierS na Animals. Jeśli maszMammal konsumenta, ale Duckdostawcę, nie powinien on pasować, chociaż oba są zwierzętami. Aby temu zapobiec, dodano dodatkowe ograniczenia.

Zamiast powyższego musimy zdefiniować relacje między używanymi typami.

Np.

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

dba o to, abyśmy mogli korzystać tylko z usług dostawcy, który zapewnia nam odpowiedni rodzaj przedmiotu dla konsumenta.

OTOH, równie dobrze możemy to zrobić

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

gdzie idziemy w drugą stronę: definiujemy typ Supplieri ograniczamy możliwość umieszczenia go w pliku Consumer.

Możemy nawet zrobić

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

gdzie, posiadający intuicyjne relacje Life-> Animal-> Mammal-> Dog, Catitd., możemy nawet umieścić Mammalna Lifekonsumenta, ale nie Stringdo Lifekonsumenta.

glglgl
źródło
1
Spośród 4 wersji numer 2 jest prawdopodobnie nieprawidłowy. np. nie możemy tego nazwać, (Consumer<Runnable>, Supplier<Dog>)dopóki Dogjest podtypAnimal & Runnable
ZhongYu
5

Podstawową logiką takiego zachowania jest Genericsmechanizm usuwania typu. Tak więc w czasie wykonywania nie ma sposobu, aby zidentyfikować typ, w collectionprzeciwieństwie do tego, arraysgdzie nie ma takiego procesu usuwania. Wracając do pytania ...

Załóżmy, że istnieje metoda podana poniżej:

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

Teraz, jeśli java pozwala dzwoniącemu na dodanie listy typu Animal do tej metody, możesz dodać niewłaściwą rzecz do kolekcji, a także w czasie wykonywania będzie działać z powodu usunięcia typu. Podczas gdy w przypadku tablic otrzymasz wyjątek czasu wykonywania dla takich scenariuszy ...

Zasadniczo zachowanie to jest realizowane w taki sposób, że nie można dodać niewłaściwej rzeczy do kolekcji. Teraz uważam, że istnieje możliwość usunięcia typu, aby zapewnić zgodność ze starszą wersją języka Java bez generycznych ...

Hitesh
źródło
4

Podtypowanie jest niezmienne dla sparametryzowanych typów. Mimo że klasa Dogjest podtypem Animal, sparametryzowany typ List<Dog>nie jest podtypem List<Animal>. W przeciwieństwie do tego, tablice wykorzystują kowariantne podtypy, więc typ tablicy Dog[]jest podtypem Animal[].

Niezmienne podtypowanie zapewnia, że ​​ograniczenia typu wymuszone przez Javę nie zostaną naruszone. Rozważ następujący kod podany przez @Jona Skeeta:

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

Jak stwierdził @Jon Skeet, ten kod jest nielegalny, ponieważ w przeciwnym razie naruszyłby ograniczenia typu, zwracając kota, gdy pies tego oczekiwał.

Pouczające jest porównanie powyższego z analogicznym kodem dla tablic.

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

Kod jest legalny. Zgłasza jednak wyjątek magazynu tablic . Tablica przenosi swój typ w czasie wykonywania, w ten sposób JVM może wymusić bezpieczeństwo typu podtypu kowariantnego.

Aby to zrozumieć, spójrzmy na kod bajtowy wygenerowany przez javapponiższą klasę:

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

Za pomocą polecenia javap -c Demonstrationwyświetla się następujący kod bajtowy 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
}

Zauważ, że przetłumaczony kod treści metod jest identyczny. Kompilator zastąpił każdy sparametryzowany typ swoim skasowaniem . Ta właściwość ma kluczowe znaczenie, co oznacza, że ​​nie złamała kompatybilności wstecznej.

Podsumowując, bezpieczeństwo w czasie wykonywania nie jest możliwe dla sparametryzowanych typów, ponieważ kompilator zastępuje każdy sparametryzowany typ przez jego usunięcie. To sprawia, że ​​sparametryzowane typy są niczym więcej niż cukrem syntaktycznym.

Root G
źródło
3

W rzeczywistości możesz użyć interfejsu, aby osiągnąć to, co chcesz.

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

}

możesz następnie użyć kolekcji za pomocą

List <Animal> animalGroup = new ArrayList<Animal>();
animalGroup.add(new Dog());
Angel Koh
źródło
1

Jeśli masz pewność, że elementy listy są podklasami danego podtypu, możesz rzucić listę przy użyciu tego podejścia:

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

Jest to przydatne, gdy chcesz przekazać listę w konstruktorze lub iterować nad nią

sagits
źródło
2
To stworzy więcej problemów, niż faktycznie rozwiązuje
Ferrybig
Jeśli spróbujesz dodać kota do listy, na pewno spowoduje to problemy, ale dla celów zapętlenia uważam, że jest to jedyna nieokreślona odpowiedź.
sagits
1

odpowiedź , jak również inne odpowiedzi są poprawne. Dodam do tych odpowiedzi rozwiązanie, które moim zdaniem będzie pomocne. Myślę, że często pojawia się to w programowaniu. Należy zauważyć, że w przypadku kolekcji (list, zestawów itp.) Głównym problemem jest dodawanie do kolekcji. Tam rzeczy się psują. Nawet usunięcie jest w porządku.

W większości przypadków możemy użyć Collection<? extends T>raczej wtedy Collection<T>i to powinien być pierwszy wybór. Znajduję jednak przypadki, w których nie jest to łatwe. To zależy od tego, czy zawsze jest to najlepsza rzecz do zrobienia. Prezentuję tutaj klasę DownCastCollection, która może przekonwertować a Collection<? extends T>na Collection<T>(możemy zdefiniować podobne klasy dla List, Set, NavigableSet, ..), które będą używane podczas korzystania ze standardowego podejścia jest bardzo niewygodne. Poniżej znajduje się przykład tego, jak go używać (moglibyśmy również użyć Collection<? extends Object>w tym przypadku, ale w prosty sposób zilustruję go za pomocą 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));
}

Teraz klasa:

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
źródło
To dobry pomysł, ponieważ istnieje już w Javie SE. ; )Collections.unmodifiableCollection
Radiodef,
1
Tak, ale kolekcję, którą definiuję, można modyfikować.
dan b
Tak, można to zmienić. Collection<? extends E>już obsługuje to zachowanie poprawnie, chyba że użyjesz go w sposób, który nie jest bezpieczny dla typu (np. rzutujesz go na coś innego). Jedyną korzyścią, jaką widzę, jest to, że po wywołaniu addoperacji generuje wyjątek, nawet jeśli ją rzuciłeś.
Vlasec
0

Weźmy przykład z samouczka 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) {
        ...
    }
}

Dlaczego lista psów (kółek) nie powinna być uważana za domyślną listę zwierząt (kształtów) z powodu tej sytuacji:

// drawAll method call
drawAll(circleList);


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

Zatem „architekci” Java mieli 2 opcje, które rozwiązują ten problem:

  1. nie bierz pod uwagę, że podtyp jest domyślnie jego nadtypem i podaj błąd kompilacji, tak jak dzieje się to teraz

  2. rozważ podtyp jako jego nadtyp i ogranicz przy kompilacji metodę „dodaj” (więc w metodzie drawAll, jeśli zostanie przekazana lista kręgów, podtyp kształtu, kompilator powinien to wykryć i ograniczyć ci błąd kompilacji do zrobienia że).

Z oczywistych względów wybrał pierwszy sposób.

aureliusz
źródło
0

Powinniśmy również wziąć pod uwagę, w jaki sposób kompilator zagraża klasom ogólnym: w „tworzy” instancję innego typu za każdym razem, gdy wypełniamy ogólne argumenty.

Tak więc mamy ListOfAnimal, ListOfDog, ListOfCat, itp, które są odrębne klasy, które w końcu jest „stworzone” przez kompilator, gdy określamy ogólne argumenty. I to jest płaska hierarchia (właściwie Listto wcale nie jest hierarchia).

Kolejnym argumentem, dlaczego kowariancja nie ma sensu w przypadku klas ogólnych, jest fakt, że u podstawy wszystkie klasy są takie same - są Listinstancjami. Specjalizacja Listpoprzez wypełnienie ogólnego argumentu nie rozszerza klasy, po prostu sprawia, że ​​działa ona dla tego konkretnego ogólnego argumentu.

Cristik
źródło
0

Problem został dobrze zidentyfikowany. Ale jest rozwiązanie; make doSomething generic:

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

teraz możesz wywołać doSomething za pomocą List <Dog> lub List <Cat> lub List <Animal>.

gerardw
źródło
0

innym rozwiązaniem jest zbudowanie nowej listy

List<Dog> dogs = new ArrayList<Dog>(); 
List<Animal> animals = new ArrayList<Animal>(dogs);
animals.add(new Cat());
ejaenv
źródło
0

W nawiązaniu do odpowiedzi Jona Skeeta, który używa tego przykładowego kodu:

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

Na najgłębszym poziomie problem polega na tym dogsi animalsdziel się referencją. Oznacza to, że jednym ze sposobów na wykonanie tej pracy byłoby skopiowanie całej listy, co złamałoby równość referencji:

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

Po zadzwonieniu List<Animal> animals = new ArrayList<>(dogs);nie można bezpośrednio przypisać animalsani jednego, dogsani jednegocats :

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

dlatego nie można umieścić niewłaściwego podtypu Animalna liście, ponieważ nie ma niewłaściwego podtypu - żadnego obiektu podtypu? extends Animal można do niego dodaćanimals .

Oczywiście zmienia to semantykę, ponieważ listy animalsi dogsnie są już udostępniane, więc dodawanie do jednej listy nie dodaje się do drugiej (co jest dokładnie tym, czego chcesz, aby uniknąć problemu, że Catmożna dodać do listy, która jest tylko powinien zawierać Dogobiekty). Również skopiowanie całej listy może być nieefektywne. Rozwiązuje to jednak problem równoważności typów, przerywając równość odniesienia.

Luke Hutchison
źródło