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?
sumber
Jawaban:
Tidak,
List<Dog>
ini bukan sebuahList<Animal>
. Pertimbangkan apa yang dapat Anda lakukan denganList<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.Tiba-tiba Anda memiliki kucing yang sangat bingung.
Sekarang, Anda tidak dapat menambahkan a
Cat
keList<? extends Animal>
karena Anda tidak tahu itu aList<Cat>
. Anda dapat mengambil nilai dan tahu bahwa itu akan menjadiAnimal
, tetapi Anda tidak dapat menambahkan hewan yang sewenang-wenang. Kebalikannya berlaku untukList<? super Animal>
- dalam hal ini Anda dapat menambahkannyaAnimal
dengan aman, tetapi Anda tidak tahu apa-apa tentang apa yang bisa diambil darinya, karena itu bisa saja aList<Object>
.sumber
Apa yang Anda cari disebut parameter tipe kovarian . Ini berarti bahwa jika satu jenis objek dapat diganti dengan yang lain dalam suatu metode (misalnya,
Animal
dapat diganti denganDog
), hal yang sama berlaku untuk ekspresi menggunakan objek tersebut (sehinggaList<Animal>
dapat diganti denganList<Dog>
). Masalahnya adalah bahwa kovarians tidak aman untuk daftar yang dapat berubah secara umum. Misalkan Anda memilikiList<Dog>
, dan sedang digunakan sebagaiList<Animal>
. Apa yang terjadi ketika Anda mencoba menambahkan Kucing ke iniList<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 Foo
deklarasi metode in, tetapi hal itu menambah kompleksitas tambahan.sumber
Alasan a
List<Dog>
bukan aList<Animal>
, adalah bahwa, misalnya, Anda dapat memasukkan aCat
ke dalamList<Animal>
, tetapi tidak keList<Dog>
... Anda dapat menggunakan wildcard untuk membuat obat generik lebih dapat diperluas jika memungkinkan; misalnya, membaca dari aList<Dog>
mirip dengan membaca dariList<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.
sumber
Suatu hal yang saya pikir harus ditambahkan ke apa yang disebutkan oleh jawaban lain adalah sementara
memang benar demikian
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.
sumber
Saya akan mengatakan bahwa inti dari Generics adalah tidak mengizinkannya. Pertimbangkan situasi dengan array, yang memungkinkan jenis kovarian:
Kode itu mengkompilasi dengan baik, tetapi melempar kesalahan runtime (
java.lang.ArrayStoreException: java.lang.Boolean
di 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 Class
dan? extends Class
untuk apa. Yang pertama adalah ketika Anda harus memasukkan ke dalam suatu tipeCollection
(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.sumber
Untuk memahami masalah ini, penting untuk membuat perbandingan dengan array.
List<Dog>
adalah tidak subclass dariList<Animal>
.Tetapi
Dog[]
adalah subclass dariAnimal[]
.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.
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.
sumber
Jawaban yang diberikan di sini tidak sepenuhnya meyakinkan saya. Jadi sebagai gantinya, saya membuat contoh lain.
kedengarannya bagus, bukan? Tetapi Anda hanya dapat melewati
Consumer
s danSupplier
s untukAnimal
s. Jika Anda memilikiMammal
konsumen, tetapiDuck
pemasok, 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.,
memastikan bahwa kami hanya dapat menggunakan pemasok yang memberikan kami jenis objek yang tepat untuk konsumen.
OTOH, kita juga bisa melakukannya
di mana kita pergi dengan cara lain: kita mendefinisikan jenis
Supplier
dan membatasi bahwa itu dapat dimasukkan ke dalamConsumer
.Kami bahkan bisa melakukannya
di mana, memiliki hubungan yang intuitif
Life
->Animal
->Mammal
->Dog
,Cat
dll., kita bahkan bisa memasukkan aMammal
keLife
konsumen, tetapi bukanString
keLife
konsumen.sumber
(Consumer<Runnable>, Supplier<Dog>)
sementaraDog
adalah subtipe dariAnimal & Runnable
Logika dasar untuk perilaku tersebut adalah yang
Generics
mengikuti mekanisme penghapusan tipe. Jadi pada saat run time Anda tidak memiliki cara untuk mengidentifikasi jeniscollection
tidak seperti diarrays
mana tidak ada proses penghapusan seperti itu. Jadi kembali ke pertanyaan Anda ...Jadi misalkan ada metode seperti yang diberikan di bawah ini:
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 ....
sumber
Subtyping adalah invarian untuk jenis diparameterisasi. Meskipun kelas
Dog
adalah subtipeAnimal
, tipe parameterList<Dog>
bukan subtipeList<Animal>
. Sebaliknya, subtipe kovarian digunakan oleh array, sehingga jenis arrayDog[]
adalah subtipe dariAnimal[]
.Subtipe invarian memastikan bahwa batasan tipe yang dipaksakan oleh Java tidak dilanggar. Pertimbangkan kode berikut yang diberikan oleh @Jon Skeet:
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.
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
javap
kelas di bawah ini:Menggunakan perintah
javap -c Demonstration
, ini menunjukkan bytecode Java berikut: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.
sumber
Sebenarnya Anda dapat menggunakan antarmuka untuk mencapai apa yang Anda inginkan.
}
Anda kemudian dapat menggunakan koleksi menggunakan
sumber
Jika Anda yakin bahwa item daftar adalah subclass dari tipe super yang diberikan, Anda bisa melemparkan daftar menggunakan pendekatan ini:
Ini berguna ketika Anda ingin meneruskan daftar di konstruktor atau beralih di atasnya
sumber
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 ituCollection<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 konversiCollection<? extends T>
ke aCollection<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 menggunakanCollection<? extends Object>
dalam kasus ini, tetapi saya membuatnya mudah untuk diilustrasikan menggunakan DownCastCollection.Sekarang kelasnya:
}
sumber
Collections.unmodifiableCollection
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 memanggiladd
operasi, ia melempar pengecualian, bahkan jika Anda melemparkannya.Mari kita ambil contoh dari tutorial JavaSE
Jadi mengapa daftar anjing (lingkaran) tidak boleh dianggap secara implisit daftar binatang (bentuk) adalah karena situasi ini:
Jadi Java "architects" memiliki 2 opsi yang mengatasi masalah ini:
jangan menganggap bahwa subtipe secara implisit itu adalah supertype, dan memberikan kesalahan kompilasi, seperti yang terjadi sekarang
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.
sumber
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 denganList
bukan hirarki sama sekali).Argumen lain mengapa kovarians tidak masuk akal dalam kasus kelas generik adalah fakta bahwa pada dasarnya semua kelas adalah sama - adalah
List
contoh. MengkhususkanList
dengan mengisi argumen generik tidak memperluas kelas, itu hanya membuatnya bekerja untuk argumen generik tertentu.sumber
Masalahnya telah diidentifikasi dengan baik. Tapi ada solusinya; buat doSomething generik:
sekarang Anda dapat memanggil doSomething dengan List <Dog> atau List <Cat> atau List <Animal>.
sumber
solusi lain adalah membuat daftar baru
sumber
Selanjutnya ke jawaban oleh Jon Skeet, yang menggunakan kode contoh ini:
Pada level terdalam, masalahnya di sini adalah
dogs
dananimals
bagikan referensi. Itu berarti bahwa salah satu cara untuk membuat karya ini adalah dengan menyalin seluruh daftar, yang akan mematahkan persamaan referensi:Setelah menelepon
List<Animal> animals = new ArrayList<>(dogs);
, Anda tidak dapat secara langsung menetapkananimals
salah satudogs
ataucats
:karena itu Anda tidak dapat memasukkan subtipe yang salah
Animal
ke dalam daftar, karena tidak ada subtipe yang salah - objek subtipe apa? extends Animal
pun dapat ditambahkananimals
.Jelas, ini mengubah semantik, karena daftar
animals
dandogs
tidak lagi dibagikan, jadi menambahkan ke satu daftar tidak menambah yang lain (yang persis apa yang Anda inginkan, untuk menghindari masalah yangCat
dapat ditambahkan ke daftar yang hanya seharusnya berisiDog
objek). Juga, menyalin seluruh daftar bisa jadi tidak efisien. Namun, ini memecahkan masalah kesetaraan jenis, dengan memecah persamaan referensi.sumber