Apa alasan mengapa Map.get (kunci Objek) tidak generik

405

Apa alasan di balik keputusan untuk tidak memiliki metode get sepenuhnya generik di antarmuka java.util.Map<K, V>.

Untuk memperjelas pertanyaan, tanda tangan dari metode ini adalah

V get(Object key)

dari pada

V get(K key)

dan saya bertanya-tanya mengapa (hal yang sama untuk remove, containsKey, containsValue).

WMR
sumber
3
Pertanyaan serupa mengenai Pengumpulan: stackoverflow.com/questions/104799/...
AlikElzin-kilaka
1
Luar biasa. Saya menggunakan Java sejak 20+ tahun, dan hari ini saya menyadari masalah ini.
GhostCat

Jawaban:

260

Seperti yang disebutkan oleh orang lain, alasan mengapa get(), dll. Tidak umum karena kunci entri yang Anda ambil tidak harus sama dengan objek yang Anda lewati get(); spesifikasi metode hanya mensyaratkan bahwa mereka sama. Ini mengikuti dari bagaimana equals()metode mengambil dalam Objek sebagai parameter, bukan hanya tipe yang sama seperti objek.

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

berita baru
sumber
28
Lalu mengapa ada V Get(K k)di C #?
134
Pertanyaannya adalah, jika Anda ingin menelepon m.get(linkedList), mengapa Anda tidak mendefinisikan mtipe sebagai Map<List,Something>? Saya tidak bisa memikirkan usecase di mana menelepon m.get(HappensToBeEqual)tanpa mengubah Maptipe untuk mendapatkan antarmuka masuk akal.
Elazar Leibovich
58
Wow, cacat desain yang serius. Anda juga tidak mendapat peringatan kompiler, kacau. Saya setuju dengan Elazar. Jika ini benar-benar berguna, yang saya ragu sering terjadi, getByEquals (Object key) terdengar lebih masuk akal ...
mmm
37
Keputusan ini sepertinya dibuat berdasarkan kemurnian teoretis dan bukan kepraktisan. Untuk sebagian besar penggunaan, pengembang lebih suka melihat argumen dibatasi oleh jenis template, daripada membuatnya tidak terbatas untuk mendukung kasus tepi seperti yang disebutkan oleh newacct dalam jawabannya. Meninggalkan tanda tangan tidak-templated menciptakan lebih banyak masalah daripada memecahkannya.
Sam Goldberg
14
@newacct: "safe type safe" adalah klaim kuat untuk konstruk yang dapat gagal secara tak terduga saat runtime. Jangan mempersempit pandangan Anda ke peta hash yang kebetulan bekerja dengan itu. TreeMapmungkin gagal saat Anda meneruskan objek dengan tipe yang salah ke getmetode tetapi dapat lewat sesekali, misalnya saat peta kosong. Dan bahkan lebih buruk, dalam kasus disediakan Comparatordalam comparemetode (yang memiliki tanda tangan generik!) Bisa disebut dengan argumen dari tipe yang salah tanpa peringatan dicentang. Ini adalah perilaku yang rusak.
Holger
105

Seorang programmer Java yang luar biasa di Google, Kevin Bourrillion, menulis tentang masalah ini dalam sebuah posting blog beberapa waktu lalu (diakui dalam konteks Setalih-alih Map). Kalimat yang paling relevan:

Secara seragam, metode Java Collections Framework (dan Google Collections Library juga) tidak pernah membatasi jenis parameternya kecuali jika perlu untuk mencegah agar koleksi tidak rusak.

Saya tidak sepenuhnya yakin saya setuju dengan itu sebagai prinsip -. NET tampaknya baik-baik saja memerlukan jenis kunci yang tepat, misalnya - tetapi layak mengikuti alasan di posting blog. (Setelah disebutkan. NET, ada baiknya menjelaskan bagian dari alasan mengapa itu bukan masalah di. NET adalah bahwa ada masalah yang lebih besar di. NET varians yang lebih terbatas ...)

Jon Skeet
sumber
4
Apocalisp: itu tidak benar, situasinya masih sama.
Kevin Bourrillion
9
@ user102008 Tidak, posnya tidak salah. Meskipun a Integerdan a Doubletidak pernah bisa setara satu sama lain, masih merupakan pertanyaan yang wajar untuk bertanya apakah a Set<? extends Number>mengandung nilai new Integer(5).
Kevin Bourrillion
33
Saya tidak pernah ingin memeriksa keanggotaan di Set<? extends Foo>. Saya sangat sering mengubah jenis kunci peta dan kemudian merasa frustrasi karena kompilator tidak dapat menemukan semua tempat di mana kode perlu diperbarui. Saya benar-benar tidak yakin bahwa ini adalah tradeoff yang benar.
Porculus
4
@EarthEngine: Selalu rusak. Itulah intinya - kode rusak, tetapi kompiler tidak dapat menangkapnya.
Jon Skeet
1
Dan itu masih rusak, dan hanya menyebabkan kita bug ... jawaban yang luar biasa.
GhostCat
28

Kontrak dinyatakan sebagai berikut:

Lebih formal lagi, jika peta ini berisi pemetaan dari kunci k ke nilai v sedemikian rupa sehingga (kunci == null? K == null: key.equals (k) ), maka metode ini mengembalikan v; jika tidak mengembalikan null. (Paling tidak mungkin ada satu pemetaan seperti itu.)

(penekanan saya)

dan karena itu, pencarian kunci yang sukses tergantung pada implementasi kunci metode input kesetaraan. Itu tidak selalu tergantung pada kelas k.

Brian Agnew
sumber
4
Itu juga tergantung pada hashCode(). Tanpa implementasi hashCode () yang tepat, implementasi yang baik equals()agak tidak berguna dalam kasus ini.
rudolfson
5
Saya kira, pada prinsipnya, ini akan memungkinkan Anda menggunakan proksi ringan untuk kunci, jika membuat ulang seluruh kunci tidak praktis - selama equals () dan kode hash () diimplementasikan dengan benar.
Bill Michell
5
@rudolfson: Sejauh yang saya ketahui, hanya HashMap yang bergantung pada kode hash untuk menemukan bucket yang benar. TreeMap, misalnya, menggunakan pohon pencarian biner, dan tidak peduli dengan kode hash ().
Rob
4
Sebenarnya, get()tidak perlu mengambil argumen tipe Objectuntuk memuaskan kontak. Bayangkan metode get terbatas pada tipe kunci K- kontrak masih akan valid. Tentu saja, penggunaan di mana jenis waktu kompilasi bukan subkelas Ksekarang akan gagal dikompilasi, tetapi itu tidak membatalkan kontrak, karena kontrak secara implisit mendiskusikan apa yang terjadi jika kode dikompilasi.
BeeOnRope
16

Ini adalah penerapan Hukum Postel, "menjadi konservatif dalam apa yang Anda lakukan, menjadi liberal dalam apa yang Anda terima dari orang lain."

Pemeriksaan kesetaraan dapat dilakukan terlepas dari jenisnya; yang equalsmetode didefinisikan pada Objectkelas dan menerima setiap Objectsebagai parameter. Jadi, masuk akal untuk kesetaraan kunci, dan operasi berdasarkan kesetaraan kunci, untuk menerima Objectjenis apa pun .

Ketika sebuah peta mengembalikan nilai kunci, ia menyimpan sebanyak mungkin informasi jenis, dengan menggunakan parameter tipe.

erickson
sumber
4
Lalu mengapa ada V Get(K k)di C #?
1
Itu ada V Get(K k)di C # karena itu juga masuk akal. Perbedaan antara pendekatan Java dan .NET hanya benar-benar yang memblokir hal-hal yang tidak cocok. Di C # itu kompiler, di Jawa itu koleksi. Saya marah tentang kelas pengumpulan .NET yang tidak konsisten sesekali, tetapi Get()dan Remove()hanya menerima jenis yang cocok tentu saja mencegah Anda dari melewatkan nilai yang salah secara tidak sengaja.
Wormbo
26
Ini adalah salah penerapan Hukum Postel. Jadilah liberal dalam apa yang Anda terima dari orang lain, tetapi tidak terlalu liberal. API idiot ini berarti Anda tidak dapat membedakan antara "tidak ada dalam koleksi" dan "Anda membuat kesalahan pengetikan statis". Ribuan jam programmer yang hilang bisa dicegah dengan get: K -> boolean.
Hakim Mental
1
Tentu saja itu seharusnya contains : K -> boolean.
Hakim Mental
4
Postel salah .
Alnitak
13

Saya pikir bagian Tutorial Generik ini menjelaskan situasi (penekanan saya):

"Anda perlu memastikan bahwa API generik tidak terlalu membatasi; API harus terus mendukung kontrak asli API. Pertimbangkan lagi beberapa contoh dari java.util.Collection. API pra-generik terlihat seperti:

interface Collection { 
  public boolean containsAll(Collection c);
  ...
}

Upaya naif untuk menghasilkan itu adalah:

interface Collection<E> { 
  public boolean containsAll(Collection<E> c);
  ...
}

Meskipun ini jenis aman, itu tidak sesuai dengan kontrak asli API. Metode containAll () bekerja dengan segala jenis koleksi yang masuk. Ini hanya akan berhasil jika koleksi yang masuk benar-benar hanya berisi contoh E, tetapi:

  • Tipe statis dari koleksi yang masuk mungkin berbeda, mungkin karena penelepon tidak tahu jenis yang tepat dari koleksi yang dikirimkan, atau mungkin karena itu adalah Koleksi <S>, di mana S adalah subtipe dari E.
  • Sah-sah saja untuk memanggil containAll () dengan koleksi jenis yang berbeda. Rutin seharusnya bekerja, mengembalikan false. "
Yardena
sumber
2
lalu mengapa tidak containsAll( Collection< ? extends E > c )?
Hakim Mental
1
@JudgeMental, meskipun tidak diberikan sebagai contoh di atas perlu juga untuk memungkinkan containsAlldengan Collection<S>mana Sadalah supertype dari E. Ini tidak akan diizinkan jika itu containsAll( Collection< ? extends E > c ). Lebih jauh, seperti yang secara eksplisit dinyatakan dalam contoh, sah untuk meneruskan koleksi dari tipe yang berbeda (dengan nilai pengembalian kemudian menjadi false).
davmac
Seharusnya tidak perlu untuk mengizinkan containAll dengan koleksi supertype dari E. Saya berpendapat bahwa perlu untuk melarang panggilan itu dengan pemeriksaan tipe statis untuk mencegah bug. Itu adalah kontrak konyol, yang menurut saya adalah inti dari pertanyaan awal.
Hakim Mental
6

Alasannya adalah bahwa penahanan ditentukan oleh equalsdanhashCode yang merupakan metode Objectdan keduanya mengambil Objectparameter. Ini adalah cacat desain awal di perpustakaan standar Jawa. Ditambah dengan keterbatasan dalam sistem tipe Java, itu memaksa apa pun yang bergantung pada equals dan kode hash untuk mengambil Object.

Satu-satunya cara untuk memiliki tabel hash tipe-aman dan kesetaraan di Jawa adalah untuk menghindari Object.equals dan Object.hashCodedan menggunakan pengganti generik. Java fungsional dilengkapi dengan kelas tipe hanya untuk tujuan ini: Hash<A>dan Equal<A>. Pembungkus HashMap<K, V>disediakan yang mengambil Hash<K>dan Equal<K>dalam konstruktornya. Kelas getdan containsmetode ini karenanya mengambil argumen tipe generik K.

Contoh:

HashMap<String, Integer> h =
  new HashMap<String, Integer>(Equal.stringEqual, Hash.stringHash);

h.add("one", 1);

h.get("one"); // All good

h.get(Integer.valueOf(1)); // Compiler error
Apocalisp
sumber
4
Ini dengan sendirinya tidak mencegah tipe 'get' dari yang dinyatakan sebagai "V get (kunci K)", karena 'Objek' selalu merupakan leluhur K, jadi "key.hashCode ()" masih akan valid.
finnw
1
Meskipun tidak mencegahnya, saya pikir itu menjelaskannya. Jika mereka mengganti metode equals untuk memaksa kesetaraan kelas, mereka tentu tidak bisa memberi tahu orang-orang bahwa mekanisme yang mendasari untuk menemukan objek di peta menggunakan equals () dan hashmap () ketika metode prototipe untuk metode-metode tersebut tidak kompatibel.
cgp
5

Kesesuaian.

Sebelum obat generik tersedia, baru saja dapatkan (Objek o).

Jika mereka mengubah metode ini untuk mendapatkan (<K> o) itu akan berpotensi memaksa pemeliharaan kode besar ke pengguna java hanya untuk membuat kompilasi kode kerja lagi.

Mereka bisa memperkenalkan tambahan metode , katakanlah get_checked (<K> o) dan hentikan metode get () lama sehingga ada jalur transisi yang lebih lembut. Tetapi untuk beberapa alasan, ini tidak dilakukan. (Situasi yang kita hadapi sekarang adalah Anda perlu menginstal alat seperti findBugs untuk memeriksa kompatibilitas tipe antara argumen get () dan tipe kunci yang dideklarasikan <K> dari peta.)

Argumen yang berkaitan dengan semantik. Equals () adalah palsu, saya pikir. (Secara teknis mereka benar, tapi saya masih berpikir mereka palsu. Tidak ada desainer yang warasnya akan membuat o1.equals (o2) benar jika o1 dan o2 tidak memiliki superclass umum.)

Erwin Smout
sumber
4

Ada satu alasan lagi yang berat, itu tidak bisa dilakukan secara teknis, karena itu brokes Map.

Java memiliki konstruksi generik seperti polimorfik <? extends SomeClass>. Ditandai referensi tersebut dapat menunjuk ke jenis yang ditandatangani <AnySubclassOfSomeClass>. Tetapi generik polimorfik membuat referensi itu hanya bisa dibaca . Kompiler memungkinkan Anda untuk menggunakan tipe generik hanya sebagai tipe metode pengembalian (seperti getter sederhana), tetapi memblokir metode yang tipe generiknya argumen (seperti setter biasa). Itu berarti jika Anda menulis Map<? extends KeyType, ValueType>, kompiler tidak memungkinkan Anda memanggil metode get(<? extends KeyType>), dan peta tidak akan berguna. Satu-satunya solusi adalah untuk membuat metode ini tidak generik: get(Object).

Ya ampun
sumber
mengapa metode set sangat diketik?
Sentenza
jika Anda maksud 'put': Metode put () mengubah peta dan itu tidak akan tersedia dengan obat generik seperti <? extends SomeClass>. Jika Anda menyebutnya, Anda mendapat pengecualian kompilasi. Peta seperti itu akan "dibaca"
Owheee
1

Kompatibilitas mundur, saya kira. Map(atau HashMap) masih perlu dukungan get(Object).

Anton Gogolev
sumber
13
Tetapi argumen yang sama dapat dibuat untuk put(yang memang membatasi jenis generik). Anda mendapatkan kompatibilitas mundur dengan menggunakan tipe mentah. Generik adalah "opt-in".
Thilo
Secara pribadi, saya pikir alasan yang paling mungkin untuk keputusan desain ini adalah kompatibilitas.
geekdenz
1

Saya melihat ini dan berpikir mengapa mereka melakukannya dengan cara ini. Saya tidak berpikir salah satu jawaban yang ada menjelaskan mengapa mereka tidak bisa membuat antarmuka generik baru hanya menerima jenis yang tepat untuk kunci. Alasan sebenarnya adalah bahwa meskipun mereka memperkenalkan obat generik, mereka TIDAK membuat antarmuka baru. Antarmuka Peta adalah Peta non-generik lama yang sama yang hanya berfungsi sebagai versi generik dan non-generik. Dengan cara ini jika Anda memiliki metode yang menerima Peta non-generik, Anda dapat meneruskannya Map<String, Customer>dan itu akan tetap berfungsi. Pada saat yang sama kontrak untuk menerima Obyek menerima sehingga antarmuka baru harus mendukung kontrak ini juga.

Menurut pendapat saya mereka harus menambahkan antarmuka baru dan mengimplementasikan keduanya pada koleksi yang ada tetapi mereka memutuskan mendukung antarmuka yang kompatibel bahkan jika itu berarti desain yang lebih buruk untuk metode get. Perhatikan bahwa koleksi itu sendiri akan kompatibel dengan metode yang ada hanya antarmuka tidak akan.

Stilgar
sumber
0

Kami sedang melakukan refactoring besar sekarang dan kami kehilangan get () yang sangat diketik ini untuk memastikan bahwa kami tidak melewatkan get () dengan tipe lama.

Tapi saya menemukan trik penyelesaian / jelek untuk memeriksa waktu kompilasi: membuat antarmuka Peta dengan get sangat diketik, berisiKey, menghapus ... dan memasukkannya ke paket java.util proyek Anda.

Anda akan mendapatkan kesalahan kompilasi hanya untuk memanggil get (), ... dengan tipe yang salah, semua yang lain tampaknya ok untuk kompiler (setidaknya di dalam gerhana kepler).

Jangan lupa untuk menghapus antarmuka ini setelah memeriksa bangunan Anda karena ini bukan yang Anda inginkan di runtime.

henva
sumber