Dlaczego kolekcje Java nie usuwają metod ogólnych?

144

Dlaczego Collection.remove (Object o) nie jest ogólna?

Wydaje się, że Collection<E>mógłboolean remove(E o);

Następnie, gdy przypadkowo spróbujesz usunąć (na przykład) Set<String>zamiast każdego pojedynczego ciągu z a Collection<String>, byłby to błąd czasu kompilacji zamiast późniejszego problemu z debugowaniem.

Chris Mazzola
źródło
13
To naprawdę może cię ugryźć, gdy połączysz to z autoboxingiem. Jeśli spróbujesz usunąć coś z listy i przekażesz temu Integer zamiast int, wywoła metodę remove (Object).
ScArcher2,
2
Podobne pytanie dotyczące mapy: stackoverflow.com/questions/857420/ ...
AlikElzin-kilaka

Odpowiedzi:

73

Josh Bloch i Bill Pugh odnoszą się do tego problemu w Java Puzzlers IV: The Phantom Reference Menace, Attack of the Clone i Revenge of The Shift .

Josh Bloch mówi (6:41), że próbowali wygenerować metodę pobierania Map, metodę usuwania i kilka innych, ale „to po prostu nie działało”.

Istnieje zbyt wiele rozsądnych programów, których nie można wygenerować, jeśli zezwolisz tylko na typ ogólny kolekcji jako typ parametru. Podany przez niego przykład to przecięcie a Listz Numbersi a Listz Longs.

dmeister
źródło
6
Dlaczego add () może przyjmować wpisany parametr, ale remove () nie może, jest nadal nieco poza moim zrozumieniem. Josh Bloch byłby ostatecznym odniesieniem do pytań dotyczących kolekcji. To może być wszystko, co otrzymam bez próby wykonania podobnej implementacji kolekcji i przekonania się na własne oczy. :( Dzięki.
Chris Mazzola,
2
Chris - przeczytaj samouczek Java Generics w formacie PDF, wyjaśni, dlaczego.
JeeBee
42
Właściwie to bardzo proste! Jeśli add () wziąłby zły obiekt, zepsułby kolekcję. Zawierałby rzeczy, których nie powinien! Tak nie jest w przypadku remove () ani contains ().
Kevin Bourrillion
12
Nawiasem mówiąc, ta podstawowa zasada - używanie parametrów typu, aby zapobiec faktycznemu uszkodzeniu kolekcji - jest bezwzględnie przestrzegana w całej bibliotece.
Kevin Bourrillion
3
@KevinBourrillion: Od lat pracuję z lekami generycznymi (zarówno w Javie, jak i C #), nie zdając sobie nawet sprawy, że reguła „rzeczywistego uszkodzenia” w ogóle istnieje ... ale teraz, gdy widziałem, że jest to wprost napisane, ma to w 100% doskonały sens. Dzięki za rybę !!! Z tym, że teraz czuję się zmuszony cofnąć się i spojrzeć na moje realizacje, aby zobaczyć, czy niektóre metody mogą i dlatego powinny zostać zdegenerowane. Westchnienie.
corlettk
74

remove()(in Maporaz in Collection) nie jest typowe, ponieważ powinieneś być w stanie przekazać dowolny typ obiektu do remove(). Usunięty obiekt nie musi być tego samego typu, co obiekt, do którego przekazujesz remove(); wymaga tylko, aby byli równi. W opisie remove(), remove(o)usuwa obiektu etak, że (o==null ? e==null : o.equals(e))jest true. Zauważ, że nie ma nic wymagającego oi ebyć tego samego typu. Wynika to z faktu, że equals()metoda przyjmuje Objectparametr as, a nie tylko ten sam typ, co obiekt.

Chociaż może być powszechnie prawdą, że wiele klas zostało equals()zdefiniowanych tak, że ich obiekty mogą być równe tylko obiektom swojej własnej klasy, z pewnością nie zawsze tak jest. Na przykład specyfikacja dla List.equals()mówi, że dwa obiekty List są równe, jeśli oba są Listami i mają tę samą zawartość, nawet jeśli są różnymi implementacjami List. Wracając do przykładu w tym pytaniu, możliwe jest Map<ArrayList, Something>wywołanie a i dla mnie remove()z LinkedListargumentem jako, i powinno to usunąć klucz, który jest listą o tej samej zawartości. Nie byłoby to możliwe, gdyby remove()były ogólne i ograniczały typ argumentu.

newacct
źródło
1
Ale gdybyś zdefiniował Mapę jako Map <List, Something> (zamiast ArrayList), byłoby możliwe usunięcie za pomocą LinkedList. Myślę, że ta odpowiedź jest nieprawidłowa.
AlikElzin-kilaka
3
Odpowiedź wydaje się poprawna, ale niepełna. Skłania się tylko do pytania, dlaczego, u licha, nie uogólnili również equals()metody? Mogłem dostrzec więcej korzyści z bezpieczeństwa typów zamiast tego „libertariańskiego” podejścia. Myślę, że większość przypadków z obecną implementacją dotyczy błędów dostających się do naszego kodu, a nie radości, jaką remove()zapewnia ta metoda.
kellogs
2
@kellogs: Co miałbyś na myśli mówiąc „uogólniaj equals()metodę”?
newacct
5
@MattBall: "gdzie T jest klasą deklarującą" Ale w Javie nie ma takiej składni. Tmusi być zadeklarowany jako parametr typu w klasie i Objectnie ma żadnych parametrów typu. Nie ma sposobu, aby typ odwoływał się do „deklarującej klasy”.
newacct
3
Myślę panującą mówi co jeśli równości były rodzajowy interfejs Equality<T>z equals(T other). Wtedy mógłbyś mieć remove(Equality<T> o)i ojest to tylko przedmiot, który można porównać do innego T.
weston
11

Ponieważ jeśli parametr typu jest symbolem wieloznacznym, nie można użyć ogólnej metody usuwania.

Wydaje mi się, że natknąłem się na to pytanie z metodą Get (Object) Map. Metoda get w tym przypadku nie jest ogólna, chociaż powinna rozsądnie oczekiwać, że zostanie przekazany obiekt tego samego typu, co pierwszy parametr typu. Zdałem sobie sprawę, że jeśli przekazujesz Mapy z symbolem wieloznacznym jako pierwszym parametrem typu, nie ma sposobu, aby uzyskać element z mapy za pomocą tej metody, jeśli ten argument był ogólny. Argumenty symboli wieloznacznych nie mogą być tak naprawdę spełnione, ponieważ kompilator nie może zagwarantować, że typ jest poprawny. Spekuluję, że powodem dodania jest to, że oczekuje się, że przed dodaniem go do kolekcji należy zagwarantować, że typ jest poprawny. Jednak podczas usuwania obiektu, jeśli typ jest nieprawidłowy, i tak nie będzie pasował.

Prawdopodobnie nie wyjaśniłem tego zbyt dobrze, ale wydaje mi się to wystarczająco logiczne.

Bob Gettys
źródło
1
Czy mógłbyś to trochę rozwinąć?
Thomas Owens,
6

Oprócz innych odpowiedzi istnieje jeszcze jeden powód, dla którego metoda powinna akceptować an Object, czyli predykaty. Rozważ następujący przykład:

class Person {
    public String name;
    // override equals()
}
class Employee extends Person {
    public String company;
    // override equals()
}
class Developer extends Employee {
    public int yearsOfExperience;
    // override equals()
}

class Test {
    public static void main(String[] args) {
        Collection<? extends Person> people = new ArrayList<Employee>();
        // ...

        // to remove the first employee with a specific name:
        people.remove(new Person(someName1));

        // to remove the first developer that matches some criteria:
        people.remove(new Developer(someName2, someCompany, 10));

        // to remove the first employee who is either
        // a developer or an employee of someCompany:
        people.remove(new Object() {
            public boolean equals(Object employee) {
                return employee instanceof Developer
                    || ((Employee) employee).company.equals(someCompany);
        }});
    }
}

Chodzi o to, że obiekt przekazywany do removemetody jest odpowiedzialny za zdefiniowanie equalsmetody. Budowanie predykatów staje się w ten sposób bardzo proste.

Hosam Aly
źródło
Inwestor? (Wypełniacz wypełniający)
Matt R
3
Lista jest zaimplementowana zgodnie z yourObject.equals(developer)dokumentacją w Collection API: java.sun.com/javase/6/docs/api/java/util/…
Hosam Aly
13
Wydaje mi się, że to nadużycie
RAY,
7
Jest to nadużycie, ponieważ obiekt predykatu łamie kontrakt equalsmetody, a mianowicie symetrię. Metoda remove jest powiązana ze specyfikacją tylko wtedy, gdy obiekty spełniają specyfikację equals / hashCode, więc każda implementacja mogłaby wykonać porównanie w drugą stronę. Ponadto obiekt predykatu nie implementuje .hashCode()metody (nie może implementować konsekwentnie do równości), dlatego wywołanie remove nigdy nie będzie działać na kolekcji opartej na Hash (takiej jak HashSet lub HashMap.keys ()). To, że działa z ArrayList, to czysty przypadek.
Paŭlo Ebermann
3
(Nie omawiam pytania o typ ogólny - na to odpowiadano wcześniej - tylko tutaj używasz równości dla predykatów.) Oczywiście HashMap i HashSet sprawdzają kod skrótu, a TreeSet / Map używają kolejności elementów . Mimo to w pełni realizują Collection.remove, bez zrywania umowy (o ile kolejność jest zgodna z równymi). A zróżnicowana ArrayList (lub, jak sądzę, AbstractCollection) z odwróconym wywołaniem equals nadal poprawnie implementowałaby umowę - to Twoja wina, jeśli nie działa zgodnie z przeznaczeniem, ponieważ łamiesz equalsumowę.
Paŭlo Ebermann
5

Załóżmy, jeden posiada kolekcję Cat, a niektóre odniesienia Przedmiot typów Animal, Cat, SiameseCati Dog. Zadawanie kolekcję, czy zawiera on przedmiotu wskazuje się przez podanie Catlub SiameseCatodniesienie wydaje się rozsądny. Pytanie, czy zawiera obiekt, do którego Animalodnosi się odwołanie, może wydawać się podejrzane, ale nadal jest całkowicie uzasadnione. Przedmiotowy obiekt może w końcu być a Cati może pojawić się w kolekcji.

Co więcej, nawet jeśli obiekt jest czymś innym niż a Cat, nie ma problemu z stwierdzeniem, czy występuje w kolekcji - wystarczy odpowiedzieć „nie, nie ma”. Pewnego typu kolekcja w stylu wyszukiwania powinna być w stanie w znaczący sposób akceptować odwołania do dowolnego nadtypu i określać, czy obiekt istnieje w kolekcji. Jeśli przekazane odwołanie do obiektu jest niepowiązanego typu, nie ma możliwości, aby kolekcja mogła go zawierać, więc zapytanie jest w pewnym sensie bez znaczenia (zawsze odpowie „nie”). Niemniej jednak, ponieważ nie ma sposobu na ograniczenie parametrów do podtypów lub nadtypów, najbardziej praktyczne jest po prostu zaakceptowanie dowolnego typu i udzielenie odpowiedzi „nie” dla obiektów, których typ nie jest powiązany z typem kolekcji.

superkat
źródło
1
„Jeśli przekazane odwołanie do obiektu jest niepowiązanego typu, nie ma możliwości, aby kolekcja mogła je zawierać”. Musi tylko zawierać coś równego temu; a obiekty różnych klas mogą być równe.
newacct
„może wydawać się podejrzane, ale nadal jest całkowicie rozsądne”. Rozważmy świat, w którym obiekt jednego typu nie zawsze może być sprawdzony pod kątem równości z obiektem innego typu, ponieważ jaki typ może być równy jest sparametryzowany (podobnie jak Comparablejest sparametryzowany dla typów, z którymi można porównać). Wtedy nie byłoby rozsądne pozwolić ludziom przekazać coś niepowiązanego typu.
newacct
@newacct: Istnieje fundamentalna różnica między porównaniem wielkości a porównaniem równości: dane obiekty Ai Bjednego typu Xoraz Yinne, takie jak A> Bi X> Y. Albo A> Yi Y< A, albo X> Bi B< X. Relacje te mogą istnieć tylko wtedy, gdy porównania wielkości zawierają informacje o obu typach. W przeciwieństwie do tego metoda porównywania równości obiektu może po prostu zadeklarować, że jest nierówna z jakimkolwiek innym typem, bez konieczności posiadania jakiejkolwiek wiedzy na temat tego drugiego typu. Obiekt typu Catmoże nie mieć pojęcia, czy to ...
supercat
... „większy niż” lub „mniejszy niż” obiekt typu FordMustang, ale nie powinno mieć trudności ze stwierdzeniem, czy jest równy takiemu obiektowi (odpowiedź oczywiście brzmi „nie”).
supercat
4

Zawsze myślałem, że dzieje się tak, ponieważ remove () nie ma powodu, by przejmować się typem obiektu, który mu podasz. Niezależnie od tego, łatwo jest sprawdzić, czy ten obiekt jest jednym z tych, które zawiera Kolekcja, ponieważ może wywołać equals () na czymkolwiek. Konieczne jest sprawdzenie typu w add (), aby upewnić się, że zawiera tylko obiekty tego typu.

ColinD
źródło
0

To był kompromis. Oba podejścia mają swoją zaletę:

  • remove(Object o)
    • jest bardziej elastyczny. Na przykład pozwala na iterację listy numerów i usunięcie ich z listy długich.
    • kod korzystający z tej elastyczności można łatwiej wygenerować
  • remove(E e) zapewnia większe bezpieczeństwo typów do tego, co większość programów chce zrobić, wykrywając subtelne błędy w czasie kompilacji, na przykład omyłkową próbę usunięcia liczby całkowitej z listy skrótów.

Kompatybilność wsteczna zawsze była głównym celem podczas ewolucji API Java, dlatego wybrano opcję remove (Object o), ponieważ ułatwiło to generowanie istniejącego kodu. Gdyby kompatybilność wsteczna NIE była problemem, myślę, że projektanci wybraliby usuń (E e).

Stefan Feuerhahn
źródło
-1

Remove nie jest metodą ogólną, więc istniejący kod korzystający z kolekcji innej niż ogólna będzie nadal kompilowany i nadal będzie miał takie samo zachowanie.

Szczegółowe informacje można znaleźć pod adresem http://www.ibm.com/developerworks/java/library/j-jtp01255.html .

Edycja: komentator pyta, dlaczego metoda dodawania jest ogólna. [... usunąłem moje wyjaśnienie ...] Drugi komentator odpowiedział na pytanie od firebird84 znacznie lepiej niż ja.

Jeff C
źródło
2
Dlaczego więc metoda dodawania jest ogólna?
Bob Gettys,
@ firebird84 remove (Object) ignoruje obiekty niewłaściwego typu, ale remove (E) spowodowałoby błąd kompilacji. To zmieniłoby zachowanie.
noah
: wzruszając ramionami: - zachowanie środowiska wykonawczego nie ulega zmianie; błąd kompilacji nie jest zachowaniem w czasie wykonywania . W ten sposób zmienia się „zachowanie” metody add.
Jason S
-2

Innym powodem są interfejsy. Oto przykład, aby to pokazać:

public interface A {}

public interface B {}

public class MyClass implements A, B {}

public static void main(String[] args) {
   Collection<A> collection = new ArrayList<>();
   MyClass item = new MyClass();
   collection.add(item);  // works fine
   B b = item; // valid
   collection.remove(b); /* It works because the remove method accepts an Object. If it was generic, this would not work */
}
Patrick
źródło
Pokazujesz coś, czemu możesz ujść na sucho, ponieważ remove()nie jest to kowariantne. Pytanie jednak brzmi, czy powinno to być dozwolone. ArrayList#remove()działa na zasadzie równości wartości, a nie równości odniesień. Dlaczego miałbyś oczekiwać, że a Bbędzie równe a A? W twoim przykładzie może tak być, ale to dziwne oczekiwanie. Wolałbym, żebyś MyClassprzedstawił tutaj argument.
seh
-3

Ponieważ spowodowałoby to uszkodzenie istniejącego kodu (sprzed wersji Java5). na przykład,

Set stringSet = new HashSet();
// do some stuff...
Object o = "foobar";
stringSet.remove(o);

Teraz możesz powiedzieć, że powyższy kod jest błędny, ale przypuśćmy, że o pochodzi z heterogenicznego zbioru obiektów (tj. Zawiera łańcuchy, liczbę, obiekty itp.). Chcesz usunąć wszystkie dopasowania, co było legalne, ponieważ remove zignorowałoby po prostu elementy niebędące ciągami, ponieważ nie były równe. Ale jeśli usuniesz (String o), to już nie działa.

noah
źródło
4
Gdybym utworzył instancję List <String>, spodziewałbym się, że będę mógł wywołać tylko List.remove (someString); Jeśli potrzebuję obsługi wstecznej kompatybilności, użyłbym surowej listy List - List <?>, A następnie mogę wywołać list.remove (someObject), nie?
Chris Mazzola,
5
Jeśli zastąpisz „usuń”
słowem