Mengapa Koleksi Java tidak menghapus metode yang umum?

144

Mengapa Collection.remove (Object o) tidak generik?

Sepertinya Collection<E>bisa sajaboolean remove(E o);

Kemudian, ketika Anda secara tidak sengaja mencoba untuk menghapus (misalnya) Set<String>alih-alih setiap String individu dari a Collection<String>, itu akan menjadi kesalahan waktu kompilasi alih-alih masalah debugging nanti.

Chris Mazzola
sumber
13
Ini benar-benar dapat menggigit Anda ketika Anda menggabungkannya dengan autoboxing. Jika Anda mencoba untuk menghapus sesuatu dari Daftar dan Anda memberikannya Integer alih-alih sebuah int, ia memanggil metode remove (Object).
ScArcher2
2
Pertanyaan serupa mengenai Peta: stackoverflow.com/questions/857420/…
AlikElzin-kilaka

Jawaban:

73

Josh Bloch dan Bill Pugh merujuk pada masalah ini di Java Puzzlers IV: The Phantom Reference Menace, Attack of the Clone, dan Revenge of The Shift .

Josh Bloch mengatakan (6:41) bahwa mereka berusaha membuat metode get get Map, menghapus metode dan lainnya, tetapi "itu tidak berhasil".

Ada terlalu banyak program masuk akal yang tidak dapat dibuat jika Anda hanya mengizinkan tipe umum koleksi sebagai tipe parameter. Contoh yang diberikan oleh dia adalah persimpangan a Listdari Numberdan Listdari Longs.

tuan
sumber
6
Mengapa add () dapat mengambil parameter yang diketik tetapi remove () tidak bisa masih sedikit di luar pemahaman saya. Josh Bloch akan menjadi referensi pasti untuk pertanyaan Koleksi. Mungkin hanya itu yang saya dapatkan tanpa mencoba membuat implementasi pengumpulan yang serupa dan melihatnya sendiri. :( Terima kasih.
Chris Mazzola
2
Chris - baca Java Generics Tutorial PDF, itu akan menjelaskan alasannya.
JeeBee
42
Sebenarnya, ini sangat sederhana! Jika add () mengambil objek yang salah, itu akan merusak koleksi. Itu akan berisi hal-hal yang tidak seharusnya! Itu bukan kasus untuk menghapus (), atau berisi ().
Kevin Bourrillion
12
Kebetulan, aturan dasar itu - menggunakan parameter tipe untuk mencegah kerusakan aktual hanya pada koleksi - diikuti secara konsisten di seluruh perpustakaan.
Kevin Bourrillion
3
@KevinBourrillion: Saya telah bekerja dengan obat generik selama bertahun-tahun (di Jawa dan C #) tanpa pernah menyadari bahwa aturan "kerusakan sebenarnya" bahkan ada ... tetapi sekarang saya telah melihatnya secara langsung menyatakan bahwa itu masuk akal 100%. Terima kasih untuk ikannya !!! Kecuali sekarang saya merasa terdorong untuk kembali dan melihat implementasi saya, untuk melihat apakah beberapa metode dapat dan karenanya harus mengalami degenerifikasi. Mendesah.
corlettk
74

remove()(dalam Mapmaupun dalam Collection) tidak generik karena Anda harus dapat meneruskan semua jenis objek remove(). Objek yang dihapus tidak harus bertipe sama dengan objek yang Anda lewati remove(); hanya mensyaratkan bahwa mereka setara. Dari spesifikasi remove(), remove(o)menghapus objek eseperti itu (o==null ? e==null : o.equals(e))adalah true. Perhatikan bahwa tidak ada yang membutuhkan odan ememiliki tipe yang sama. Ini mengikuti dari kenyataan bahwa equals()metode mengambil Objectparameter sebagai, bukan hanya tipe yang sama dengan objek.

Meskipun, mungkin secara umum benar bahwa banyak kelas telah equals()didefinisikan sehingga objeknya hanya bisa sama dengan objek kelasnya sendiri, yang tentu saja tidak selalu demikian. Misalnya, spesifikasi untuk List.equals()mengatakan bahwa dua objek Daftar sama jika keduanya Daftar dan memiliki konten yang sama, bahkan jika mereka berbeda implementasi List. Jadi kembali ke contoh dalam pertanyaan ini, adalah mungkin untuk memiliki Map<ArrayList, Something>dan bagi saya untuk memanggil remove()dengan LinkedListargumen sebagai, dan itu harus menghapus kunci yang merupakan daftar dengan konten yang sama. Ini tidak akan mungkin terjadi jika remove()bersifat generik dan membatasi jenis argumennya.

berita baru
sumber
1
Tetapi jika Anda mendefinisikan Peta sebagai Peta <Daftar, Sesuatu> (bukan ArrayList), akan mungkin untuk menghapus menggunakan LinkedList. Saya pikir jawaban ini salah.
AlikElzin-kilaka
3
Jawabannya tampaknya benar, tetapi tidak lengkap. Itu hanya cocok untuk bertanya mengapa sih mereka tidak menggeneralisasi equals()metode juga? Saya bisa melihat lebih banyak manfaat untuk mengetik keamanan daripada pendekatan "libertarian" ini. Saya pikir sebagian besar kasus dengan implementasi saat ini adalah untuk bug yang masuk ke kode kita daripada tentang keceriaan fleksibilitas yang remove()dibawa metode ini.
kellog
2
@kellogs: Apa yang Anda maksud dengan "menggeneralisasikan equals()metode"?
Newacct
5
@MattBall: "di mana T adalah kelas mendeklarasikan" Tapi tidak ada sintaksis seperti itu di Jawa. Tharus dinyatakan sebagai parameter tipe di kelas, dan Objecttidak memiliki parameter tipe apa pun. Tidak ada cara untuk memiliki tipe yang mengacu pada "kelas yang mendeklarasikan".
Newacct
3
Saya pikir kellog mengatakan bagaimana jika kesetaraan adalah antarmuka generik Equality<T>dengannya equals(T other). Maka Anda bisa remove(Equality<T> o)dan ohanya beberapa objek yang dapat dibandingkan dengan yang lain T.
barat
11

Karena jika parameter tipe Anda adalah wildcard, Anda tidak dapat menggunakan metode penghapusan generik.

Saya ingat untuk kembali ke pertanyaan ini dengan metode get (Object) Map. Metode get dalam kasus ini tidak generik, meskipun seharusnya diharapkan akan melewati objek dengan tipe yang sama dengan parameter tipe pertama. Saya menyadari bahwa jika Anda mengitari Maps dengan wildcard sebagai parameter tipe pertama, maka tidak ada cara untuk mengeluarkan elemen dari Peta dengan metode itu, jika argumen itu generik. Argumen wildcard tidak dapat benar-benar dipenuhi, karena kompiler tidak dapat menjamin bahwa jenisnya benar. Saya berspekulasi bahwa alasan add bersifat generik adalah karena Anda diharapkan untuk memastikan bahwa jenisnya benar sebelum menambahkannya ke koleksi. Namun, saat menghapus objek, jika jenisnya salah maka tidak akan cocok dengan apa pun.

Saya mungkin tidak menjelaskannya dengan sangat baik, tetapi tampaknya cukup logis bagi saya.

Bob Gettys
sumber
1
Bisakah Anda menguraikan sedikit ini?
Thomas Owens
6

Selain jawaban lain, ada alasan lain mengapa metode ini harus menerima Object, yang merupakan predikat. Pertimbangkan contoh berikut:

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

Intinya adalah bahwa objek yang diteruskan ke removemetode bertanggung jawab untuk mendefinisikan equalsmetode. Membangun predikat menjadi sangat sederhana dengan cara ini.

Hosam Aly
sumber
Investor? (Filler filler filler)
Matt R
3
Daftar ini diimplementasikan sebagai yourObject.equals(developer), sebagaimana didokumentasikan dalam Collections API: java.sun.com/javase/6/docs/api/java/util/…
Hosam Aly
13
Ini sepertinya menyalahgunakan saya
RAY
7
Ini penyalahgunaan karena objek predikat Anda melanggar kontrak dari equalsmetode ini, yaitu simetri. Metode remove hanya terikat pada spesifikasinya selama objek Anda memenuhi spec equals / hashCode, jadi implementasi apa pun akan bebas melakukan perbandingan dengan cara sebaliknya. Selain itu, objek predikat Anda tidak menerapkan .hashCode()metode (tidak dapat menerapkan secara konsisten dengan yang sama), sehingga panggilan hapus tidak akan berfungsi pada koleksi berbasis Hash (seperti HashSet atau HashMap.keys ()). Bahwa ia bekerja dengan ArrayList adalah keberuntungan murni.
Paŭlo Ebermann
3
(Saya tidak membahas pertanyaan tipe umum - ini sudah dijawab sebelumnya -, hanya penggunaan Anda yang setara dengan predikat di sini.) Tentu saja HashMap dan HashSet memeriksa kode hash, dan TreeSet / Map menggunakan pemesanan elemen-elemen tersebut. . Namun, mereka sepenuhnya menerapkan Collection.remove, tanpa melanggar kontraknya (jika pemesanan konsisten dengan yang sama). Dan beragam ArrayList (atau AbstractCollection, saya pikir) dengan panggilan yang sama berbalik masih akan menerapkan kontrak dengan benar - itu adalah kesalahan Anda jika itu tidak berfungsi sebagaimana dimaksud, karena Anda melanggar equalskontrak.
Paŭlo Ebermann
5

Asumsikan satu memiliki koleksi Cat, dan beberapa referensi objek jenis Animal, Cat, SiameseCat, dan Dog. Menanyakan koleksi apakah berisi objek yang dirujuk oleh Catatau SiameseCatreferensi tampaknya masuk akal. Bertanya apakah itu berisi objek yang dirujuk oleh Animalreferensi mungkin tampak cerdik, tapi itu masih masuk akal. Objek yang dimaksud mungkin, setelah semua, menjadi aCat , dan mungkin muncul dalam koleksi.

Lebih jauh, bahkan jika objek kebetulan bukan sesuatu yang lain Cat, tidak ada masalah untuk mengatakan apakah itu muncul dalam koleksi - cukup jawab "tidak, tidak". Koleksi "lookup-style" dari beberapa jenis harus dapat menerima referensi supertipe apa pun secara bermakna dan menentukan apakah objek tersebut ada di dalam koleksi. Jika referensi objek yang diteruskan adalah tipe yang tidak terkait, tidak mungkin koleksi tersebut dapat memuatnya, jadi kueri dalam beberapa hal tidak bermakna (itu akan selalu menjawab "tidak"). Meskipun demikian, karena tidak ada cara untuk membatasi parameter menjadi subtipe atau supertipe, itu paling praktis untuk hanya menerima jenis apa pun dan menjawab "tidak" untuk objek apa pun yang jenisnya tidak terkait dengan koleksi.

supercat
sumber
1
"Jika referensi objek yang lewat adalah dari jenis yang tidak terkait, tidak mungkin koleksi mungkin mengandung itu" Salah. Itu hanya harus mengandung sesuatu yang setara dengannya; dan objek dari kelas yang berbeda dapat sama.
Newacct
"Mungkin tampak cerdik, tapi itu masih masuk akal" Apakah itu? Pertimbangkan dunia di mana objek dari satu jenis tidak selalu dapat diperiksa untuk kesetaraan dengan objek dari jenis lain, karena jenis apa yang dapat sama dengan parameternya (mirip dengan bagaimana Comparableparameterisasi untuk jenis yang dapat Anda bandingkan). Maka tidak masuk akal untuk membiarkan orang melewati sesuatu dari jenis yang tidak terkait.
Newacct
@newacct: Ada perbedaan mendasar antara perbandingan magnitudo dan perbandingan kesetaraan: objek yang diberikan Adan Bsatu jenis, dan Xdan yang Ylainnya, sedemikian rupa sehingga A> B, dan X> Y. Baik A> Ydan Y< A, atau X> Bdan B< X. Hubungan-hubungan itu hanya dapat ada jika perbandingan besarnya mengetahui tentang kedua tipe tersebut. Sebaliknya, metode perbandingan kesetaraan objek dapat dengan mudah menyatakan dirinya tidak setara dengan apa pun dari jenis lainnya, tanpa harus mengetahui apa pun tentang jenis lainnya yang dimaksud. Objek bertipe Catmungkin tidak tahu apakah itu ...
supercat
... "lebih besar dari" atau "kurang dari" objek bertipe FordMustang, tetapi seharusnya tidak mengalami kesulitan untuk mengatakan apakah itu sama dengan objek seperti itu (jawabannya, jelas, menjadi "tidak").
supercat
4

Saya selalu mengira ini karena remove () tidak punya alasan untuk peduli jenis objek yang Anda berikan. Cukup mudah, apa pun, untuk memeriksa apakah objek itu adalah salah satu yang terdapat dalam Koleksi, karena ia dapat memanggil equals () pada apa pun. Penting untuk memeriksa tipe pada add () untuk memastikan bahwa itu hanya berisi objek dari tipe itu.

ColinD
sumber
0

Itu kompromi. Kedua pendekatan memiliki keunggulan:

  • remove(Object o)
    • lebih fleksibel. Misalnya memungkinkan untuk beralih melalui daftar angka dan menghapusnya dari daftar panjang.
    • kode yang menggunakan fleksibilitas ini dapat lebih mudah dihasilkan
  • remove(E e) membawa lebih banyak jenis keamanan untuk apa yang sebagian besar program ingin lakukan dengan mendeteksi bug halus pada waktu kompilasi, seperti keliru mencoba menghapus integer dari daftar celana pendek.

Kompatibilitas mundur selalu menjadi tujuan utama ketika mengembangkan Java API, oleh karena itu hapus (Objek o) dipilih karena membuatnya membuat kode yang ada lebih mudah. Jika kompatibilitas mundur TIDAK menjadi masalah, saya kira para desainer akan memilih hapus (E e).

Stefan Feuerhahn
sumber
-1

Hapus bukan metode generik sehingga kode yang ada menggunakan koleksi non-generik akan tetap dikompilasi dan masih memiliki perilaku yang sama.

Lihat http://www.ibm.com/developerworks/java/library/j-jtp01255.html untuk detailnya.

Sunting: Seorang komentator bertanya mengapa metode add bersifat generik. [... menghapus penjelasan saya ...] Komentator kedua menjawab pertanyaan dari firebird84 jauh lebih baik daripada saya.

Jeff C
sumber
2
Lalu mengapa metode add generik?
Bob Gettys
@ firebird84 remove (Object) mengabaikan objek dari tipe yang salah, tetapi menghapus (E) akan menyebabkan kesalahan kompilasi. Itu akan mengubah perilaku.
noah
: shrug: - perilaku runtime tidak berubah; kesalahan kompilasi bukan perilaku runtime . "Perilaku" metode add berubah dengan cara ini.
Jason S
-2

Alasan lain adalah karena antarmuka. Berikut adalah contoh untuk menunjukkannya:

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
sumber
Anda menunjukkan sesuatu yang bisa Anda hindari karena remove()tidak kovarian. Namun, pertanyaannya adalah apakah ini harus diizinkan. ArrayList#remove()bekerja dengan cara kesetaraan nilai, bukan referensi kesetaraan. Mengapa Anda berharap bahwa a Bakan sama dengan a A? Dalam contoh Anda, itu bisa saja, tapi itu harapan yang aneh. Saya lebih suka melihat Anda memberikan MyClassargumen di sini.
seh
-3

Karena itu akan merusak kode yang ada (pra-Java5). misalnya,

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

Sekarang Anda mungkin mengatakan kode di atas salah, tetapi anggaplah bahwa o berasal dari sekumpulan objek yang heterogen (yaitu berisi string, angka, objek, dll.). Anda ingin menghapus semua pertandingan, yang sah karena menghapus hanya akan mengabaikan non-string karena mereka tidak setara. Tetapi jika Anda membuatnya menghapus (String o), itu tidak lagi berfungsi.

Nuh
sumber
4
Jika saya instantiate Daftar <String> Saya hanya berharap bisa memanggil List.remove (someString); Jika saya perlu mendukung kompatibilitas ke belakang, saya akan menggunakan Daftar mentah - Daftar <?> Maka saya dapat memanggil list.remove (someObject), bukan?
Chris Mazzola
5
Jika Anda mengganti "menghapus" dengan "menambahkan" kemudian bahwa kode sama rusak oleh apa yang sebenarnya dilakukan di Jawa 5.
DJClayworth