Mengapa beberapa orang mengklaim bahwa implementasi Java generik buruk?

115

Saya kadang-kadang mendengar bahwa dengan obat generik, Java tidak melakukannya dengan benar. (referensi terdekat, di sini )

Maafkan saya yang belum berpengalaman, tapi apa yang bisa membuat mereka lebih baik?

sdellysse
sumber
23
Saya tidak melihat alasan untuk menutup ini; dengan semua omong kosong yang beredar tentang obat generik, memiliki beberapa klarifikasi antara perbedaan antara Java dan C # dapat membantu orang yang mencoba menghindari semburan informasi yang salah.
Rob

Jawaban:

146

Buruk:

  • Informasi jenis hilang pada waktu kompilasi, jadi pada waktu eksekusi Anda tidak dapat membedakan jenis apa yang "dimaksudkan"
  • Tidak dapat digunakan untuk tipe nilai (ini masalah besar - di .NET List<byte>benar - benar didukung oleh byte[]misalnya, dan tidak diperlukan tinju)
  • Sintaks untuk memanggil metode generik menyebalkan (IMO)
  • Sintaks untuk batasan bisa membingungkan
  • Karakter pengganti umumnya membingungkan
  • Berbagai batasan karena hal di atas - casting dll

Baik:

  • Karakter pengganti memungkinkan kovarian / kontravarian ditentukan di sisi panggilan, yang sangat rapi dalam banyak situasi
  • Lebih baik daripada tidak sama sekali!
Jon Skeet
sumber
51
@ Paul: Saya pikir saya sebenarnya lebih suka kerusakan sementara tetapi API yang layak sesudahnya. Atau ambil rute .NET dan perkenalkan jenis koleksi baru. (.NET generik di 2.0 tidak merusak 1.1 aplikasi.)
Jon Skeet
6
Dengan basis kode yang besar, saya menyukai kenyataan bahwa saya dapat mulai mengubah tanda tangan dari satu metode untuk menggunakan obat generik tanpa mengubah semua orang yang memanggilnya.
Paul Tomblin
38
Tapi kelemahannya sangat besar, dan permanen. Ada kemenangan jangka pendek yang besar, dan kerugian jangka panjang IMO.
Jon Skeet
11
Jon - "Ini lebih baik daripada tidak sama sekali" adalah subjektif :)
MetroidFan2002
6
@Rogerio: Anda mengklaim bahwa item "Jelek" pertama saya tidak benar. Item "Buruk" saya yang pertama mengatakan bahwa informasi hilang pada waktu kompilasi. Agar itu tidak benar, Anda harus mengatakan bahwa informasi tidak hilang pada waktu kompilasi ... Adapun untuk membingungkan pengembang lain, Anda tampaknya satu-satunya yang bingung dengan apa yang saya katakan.
Jon Skeet
26

Masalah terbesar adalah bahwa Java generik adalah satu-satunya hal yang waktu kompilasi, dan Anda dapat menumbangkannya pada saat run-time. C # dipuji karena melakukan lebih banyak pemeriksaan waktu berjalan. Ada beberapa diskusi yang sangat bagus dalam posting ini , dan ini terhubung ke diskusi lain.

Paul Tomblin
sumber
Itu sebenarnya bukan masalah, itu tidak pernah dibuat untuk runtime. Seolah-olah Anda mengatakan bahwa perahu menjadi masalah karena mereka tidak dapat mendaki gunung. Sebagai bahasa, Java melakukan apa yang dibutuhkan banyak orang: mengekspresikan tipe dengan cara yang bermakna. Untuk informasi jenis runtime, orang masih dapat meneruskan Classobjek.
Vincent Cantin
1
Dia benar. Anologi Anda salah karena orang yang merancang perahu HARUS merancangnya untuk mendaki gunung. Tujuan desain mereka tidak dipikirkan dengan baik untuk apa yang dibutuhkan. Sekarang kita semua terjebak hanya dengan perahu dan kita tidak bisa mendaki gunung.
WiegleyJ
1
@VincentCantin - itu pasti masalah. Karenanya, kami semua mengeluh tentang itu. Generik Java setengah matang.
Josh M.
17

Masalah utamanya adalah Java sebenarnya tidak memiliki generik saat runtime. Ini adalah fitur waktu kompilasi.

Ketika Anda membuat kelas generik di Java, mereka menggunakan metode yang disebut "Type Erasure" untuk benar-benar menghapus semua tipe generik dari kelas dan pada dasarnya menggantinya dengan Object. Versi tinggi dari obat generik adalah bahwa kompilator hanya menyisipkan cast ke jenis generik yang ditentukan setiap kali muncul di badan metode.

Ini memiliki banyak kerugian. Salah satu yang terbesar, IMHO, adalah Anda tidak dapat menggunakan refleksi untuk memeriksa tipe generik. Jenis sebenarnya tidak generik dalam kode byte dan karenanya tidak dapat diperiksa sebagai generik.

Gambaran umum yang bagus tentang perbedaan di sini: http://www.jprl.com/Blog/archive/development/2007/Aug-31.html

JaredPar
sumber
2
Saya tidak yakin mengapa Anda mendapat suara negatif. Dari apa yang saya pahami, Anda benar, dan tautan itu sangat informatif. Dipilih.
Jason Jackson
@Jason, terima kasih. Ada kesalahan ketik dalam versi asli saya, saya rasa saya mendapat suara negatif untuk itu.
JaredPar
3
Err, karena Anda bisa menggunakan refleksi untuk melihat tipe generik, tanda tangan metode generik. Anda tidak bisa menggunakan refleksi untuk memeriksa informasi umum yang terkait dengan instance parametrized. Misalnya, jika saya memiliki kelas dengan metode foo (Daftar <String>), saya dapat secara reflektif menemukan "String"
oxbow_lakes
Ada banyak artikel yang mengatakan penghapusan tipe membuat tidak mungkin menemukan parameter tipe sebenarnya. Katakanlah: Object x = new X [Y] (); dapatkah Anda menunjukkan bagaimana, diberikan x, untuk menemukan tipe Y?
pengguna48956
@oxbow_lakes - harus menggunakan refleksi untuk menemukan tipe generik hampir sama buruknya dengan tidak bisa melakukan itu. Misalnya itu solusi yang buruk.
Josh M.
14
  1. Implementasi runtime (yaitu bukan penghapusan tipe);
  2. Kemampuan untuk menggunakan tipe primitif (ini terkait dengan (1));
  3. Sementara karakter pengganti berguna, sintaksis dan mengetahui kapan harus menggunakannya adalah sesuatu yang membingungkan banyak orang. dan
  4. Tidak ada peningkatan kinerja (karena (1); Java generik adalah gula sintaksis untuk Objek castingi).

(1) menyebabkan beberapa perilaku yang sangat aneh. Contoh terbaik yang dapat saya pikirkan adalah. Menganggap:

public class MyClass<T> {
  T getStuff() { ... }
  List<String> getOtherStuff() { ... }
}

kemudian nyatakan dua variabel:

MyClass<T> m1 = ...
MyClass m2 = ...

Sekarang hubungi getOtherStuff():

List<String> list1 = m1.getOtherStuff(); 
List<String> list2 = m2.getOtherStuff(); 

Argumen kedua memiliki tipe generik yang dilepaskan oleh kompilator karena ini adalah tipe mentah (artinya tipe berparameter tidak disediakan) meskipun tidak ada hubungannya dengan tipe berparameter.

Saya juga akan menyebutkan deklarasi favorit saya dari JDK:

public class Enum<T extends Enum<T>>

Terlepas dari wildcarding (yang merupakan tas campuran) saya hanya berpikir .Net generik lebih baik.

cletus
sumber
Tetapi kompilator akan memberi tahu Anda, terutama dengan opsi lint rawtypes.
Tom Hawtin - tackline
Maaf, mengapa baris terakhir merupakan kesalahan kompilasi? Saya mengotak-atiknya di Eclipse & tidak bisa membuatnya gagal di sana - jika saya menambahkan cukup banyak hal untuk dikompilasi.
oconnor0
public class Redundancy<R extends Redundancy<R>>;)
java.is.for.desktop
@ oconnor0 Ini bukan kegagalan kompilasi, tetapi peringatan kompiler, tanpa alasan yang jelas:The expression of type List needs unchecked conversion to conform to List<String>
artbristol
Enum<T extends Enum<T>>mungkin tampak aneh / berlebihan pada awalnya tetapi sebenarnya cukup menarik, setidaknya dalam batasan Java / generiknya. Enum memiliki values()metode statis yang memberikan larik elemennya yang diketik sebagai enum, bukan Enum, dan jenis itu ditentukan oleh parameter generik, artinya Anda inginkan Enum<T>. Tentu saja, pengetikan itu hanya masuk akal dalam konteks tipe enumerasi, dan semua enum adalah subkelas Enum, jadi Anda inginkan Enum<T extends Enum>. Namun, Java tidak suka mencampurkan tipe mentah dengan generik, sehingga Enum<T extends Enum<T>>konsistensi.
JAB
10

Saya akan memberikan pendapat yang sangat kontroversial. Generik memperumit bahasa dan memperumit kode. Misalnya, saya memiliki peta yang memetakan string ke daftar string. Di masa lalu, saya dapat menyatakan ini sebagai

Map someMap;

Sekarang, saya harus menyatakannya sebagai

Map<String, List<String>> someMap;

Dan setiap kali saya menyebarkannya ke beberapa metode, saya harus mengulangi pernyataan panjang yang besar itu lagi. Menurut pendapat saya, semua pengetikan ekstra itu mengalihkan perhatian pengembang dan membawanya keluar dari "zona". Selain itu, ketika kode diisi dengan banyak celah, terkadang sulit untuk kembali lagi nanti dan dengan cepat menyaring semua celah untuk menemukan logika penting.

Java sudah memiliki reputasi buruk sebagai salah satu bahasa paling verbose yang umum digunakan, dan bahasa generik hanya menambah masalah itu.

Dan apa yang sebenarnya Anda beli untuk semua verbositas ekstra itu? Berapa kali Anda benar-benar mengalami masalah saat seseorang memasukkan Integer ke dalam koleksi yang seharusnya menyimpan String, atau saat seseorang mencoba menarik String dari koleksi Integer? Dalam 10 tahun pengalaman saya bekerja membangun aplikasi Java komersial, ini tidak pernah menjadi sumber kesalahan yang besar. Jadi, saya tidak begitu yakin apa yang Anda dapatkan untuk verbositas ekstra. Bagi saya, itu benar-benar hanya sebagai beban birokrasi ekstra.

Sekarang saya akan menjadi sangat kontroversial. Apa yang saya lihat sebagai masalah terbesar dengan koleksi di Java 1.4 adalah perlunya typecast di mana-mana. Saya melihat typecasts itu sebagai tambahan, verbose cruft yang memiliki banyak masalah yang sama seperti obat generik. Jadi, misalnya, saya tidak bisa begitu saja

List someList = someMap.get("some key");

Saya harus melakukan

List someList = (List) someMap.get("some key");

Alasannya, tentu saja, adalah get () mengembalikan Object yang merupakan supertipe List. Jadi tugas tidak dapat dilakukan tanpa typecast. Sekali lagi, pikirkan tentang seberapa besar aturan itu membeli Anda. Dari pengalaman saya, tidak banyak.

Saya pikir Java akan jauh lebih baik jika 1) tidak menambahkan generik tetapi 2) sebaliknya mengizinkan casting implisit dari supertipe ke subtipe. Biarkan cast yang salah ditangkap pada waktu proses. Maka saya bisa memiliki kesederhanaan dalam mendefinisikan

Map someMap;

dan kemudian melakukan

List someList = someMap.get("some key");

semua kekurangannya akan hilang, dan saya pikir saya tidak akan memperkenalkan sumber bug baru yang besar ke dalam kode saya.

Clint Miller
sumber
15
Maaf, saya tidak setuju dengan semua yang Anda katakan. Tapi saya tidak akan menolaknya karena Anda membantahnya dengan baik.
Paul Tomblin
2
Tentu saja, ada banyak contoh sistem besar dan sukses yang dibangun dalam bahasa seperti Python dan Ruby yang melakukan apa yang saya sarankan dalam jawaban saya.
Clint Miller
Saya pikir seluruh keberatan saya untuk jenis ide ini bukan karena itu ide yang buruk, tetapi karena bahasa modern seperti Python dan Ruby membuat hidup lebih mudah dan lebih mudah bagi pengembang, pengembang pada gilirannya menjadi puas secara intelektual dan akhirnya memiliki yang lebih rendah. pemahaman tentang kode mereka sendiri.
Dan Tao
4
"Dan apa yang sebenarnya Anda beli untuk semua verbositas ekstra itu? ..." Ini memberi tahu programmer pemeliharaan yang buruk apa yang seharusnya ada dalam koleksi itu. Saya menemukan ini menjadi masalah bekerja dengan aplikasi Java komersial.
richj
4
"Berapa kali Anda benar-benar mengalami masalah saat seseorang memasukkan [x] ke dalam koleksi yang seharusnya berisi [y]?" - Ya ampun, aku sudah tidak bisa menghitung! Juga bahkan jika tidak ada bug, itu adalah pembunuh keterbacaan. Dan memindai banyak file untuk mengetahui objek apa yang akan menjadi (atau debugging) benar-benar menarik saya keluar dari zona tersebut. Anda mungkin menyukai Haskell - bahkan pengetikan yang kuat tetapi kurang cruft (karena tipe disimpulkan).
Martin Capodici
8

Efek samping lain dari mereka adalah waktu kompilasi dan bukan waktu berjalan adalah Anda tidak dapat memanggil konstruktor dari tipe generik. Jadi Anda tidak dapat menggunakannya untuk mengimplementasikan pabrik generik ...


   public class MyClass {
     public T getStuff() {
       return new T();
     }
    }

--jeffk ++

jdkoftinoff.dll
sumber
6

Generik Java diperiksa kebenarannya pada waktu kompilasi dan kemudian semua informasi tipe dihapus (proses ini disebut penghapusan tipe . Jadi, generik List<Integer>akan direduksi menjadi tipe mentahnya , non-generik List, yang dapat berisi objek kelas arbitrer.

Ini menghasilkan kemampuan untuk memasukkan objek arbitrer ke daftar pada waktu proses, serta sekarang tidak mungkin untuk mengetahui jenis apa yang digunakan sebagai parameter generik. Yang terakhir pada gilirannya menghasilkan

ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if(li.getClass() == lf.getClass()) // evaluates to true
  System.out.println("Equal");
Anton Gogolev
sumber
5

Saya berharap ini adalah wiki sehingga saya bisa menambahkan ke orang lain ... tapi ...

Masalah:

  • Type Erasure (tidak ada ketersediaan runtime)
  • Tidak ada dukungan untuk tipe primatif
  • Ketidaksesuaian dengan Anotasi (keduanya ditambahkan di 1.5. Saya masih tidak yakin mengapa anotasi tidak mengizinkan generik selain mempercepat fitur)
  • Ketidaksesuaian dengan Array. (Terkadang saya benar-benar ingin melakukan sesuatu seperti Class <? Extends MyObject >[], tapi saya tidak diizinkan)
  • Sintaks dan perilaku wildcard yang aneh
  • Fakta bahwa dukungan generik tidak konsisten di seluruh kelas Java. Mereka menambahkannya ke sebagian besar metode koleksi, tetapi sesekali, Anda mengalami instance di mana tidak ada.
Laplie Anderson
sumber
5

Mengabaikan seluruh jenis kekacauan penghapusan, obat generik seperti yang ditentukan tidak berfungsi.

Ini mengkompilasi:

List<Integer> x = Collections.emptyList();

Tetapi ini adalah kesalahan sintaks:

foo(Collections.emptyList());

Dimana foo didefinisikan sebagai:

void foo(List<Integer> x) { /* method body not important */ }

Jadi apakah pemeriksaan jenis ekspresi bergantung pada apakah ia ditugaskan ke variabel lokal atau parameter sebenarnya dari panggilan metode. Gila apa itu?

Nat
sumber
3
Kesimpulan yang tidak konsisten dari javac adalah omong kosong.
mP.
3
Saya akan berasumsi bahwa alasan formulir terakhir ditolak adalah karena mungkin ada lebih dari satu versi "foo" karena metode overloading.
Jason Creighton
2
kritik ini tidak lagi berlaku karena Java 8 memperkenalkan inferensi tipe target yang ditingkatkan
oldrinb
4

Pengenalan generik ke Java adalah tugas yang sulit karena arsitek mencoba menyeimbangkan fungsionalitas, kemudahan penggunaan, dan kompatibilitas mundur dengan kode lama. Sangat diharapkan, kompromi harus dilakukan.

Ada beberapa yang juga merasa bahwa implementasi Java generik meningkatkan kompleksitas bahasa ke tingkat yang tidak dapat diterima (lihat " Generics Anggap Berbahaya " dari Ken Arnold ). FAQ Generik Angelika Langer memberikan ide yang cukup bagus tentang bagaimana hal-hal bisa menjadi rumit.

Zach Scrivena
sumber
3

Java tidak memberlakukan Generik pada waktu proses, hanya pada waktu kompilasi.

Ini berarti Anda dapat melakukan hal-hal menarik seperti menambahkan jenis yang salah ke Koleksi generik.

Powerlord
sumber
1
Kompiler akan memberi tahu Anda bahwa Anda telah mengacau jika Anda mengelolanya.
Tom Hawtin - tackline
@ Tom - belum tentu. Ada beberapa cara untuk mengelabui kompiler.
Paul Tomblin
2
Apakah Anda tidak mendengarkan apa yang kompiler memberitahu Anda ?? (lihat komentar pertama saya)
Tom Hawtin - tackline
1
@Tom, jika Anda memiliki ArrayList <String> dan Anda meneruskannya ke metode gaya lama (katakanlah, warisan atau kode pihak ketiga) yang mengambil Daftar sederhana, metode itu kemudian dapat menambahkan apa pun yang disukainya ke Daftar itu. Jika diterapkan pada waktu proses, Anda akan mendapatkan pengecualian.
Paul Tomblin
1
statis void addToList (Daftar daftar) {list.add (1); } Daftar <String> list = new ArrayList <String> (); addToList (daftar); Itu mengkompilasi, dan bahkan bekerja saat runtime. Satu-satunya saat Anda melihat masalah adalah ketika Anda menghapus dari daftar mengharapkan string dan mendapatkan int.
Laplie Anderson
3

Java generik hanya untuk waktu kompilasi dan dikompilasi menjadi kode non-generik. Di C #, MSIL terkompilasi sebenarnya adalah generik. Ini memiliki implikasi yang sangat besar pada kinerja karena Java masih melakukan transmisi selama runtime. Lihat di sini untuk lebih lanjut .

Szymon Rozga
sumber
2

Jika Anda mendengarkan Java Posse # 279 - Wawancara dengan Joe Darcy dan Alex Buckley , mereka membicarakan masalah ini. Itu juga tertaut ke posting blog Neal Gafter berjudul Reified Generics for Java yang mengatakan:

Banyak orang tidak puas dengan batasan yang disebabkan oleh cara generik diimplementasikan di Java. Secara khusus, mereka tidak senang bahwa parameter tipe generik tidak direifikasi: mereka tidak tersedia pada waktu proses. Generik diimplementasikan menggunakan erasure, di mana parameter tipe generik dibuang begitu saja saat runtime.

Posting blog itu, merujuk pada entri lama, Puzzling Through Erasure: bagian jawaban , yang menekankan poin tentang kompatibilitas migrasi dalam persyaratan.

Tujuannya adalah untuk menyediakan kompatibilitas ke belakang dari kode sumber dan objek, dan juga kompatibilitas migrasi.

Kevin Hakanson
sumber