Mengapa saya harus peduli bahwa Java tidak memiliki obat generik yang direifikasi?

102

Ini muncul sebagai pertanyaan yang saya ajukan dalam sebuah wawancara baru-baru ini karena kandidat ingin melihat ditambahkan ke bahasa Jawa. Ini biasanya diidentifikasi sebagai masalah bahwa Java tidak memiliki obat generik , tetapi ketika didorong, kandidat tidak dapat benar-benar memberi tahu saya hal-hal yang dapat dia capai jika mereka ada di sana.

Jelas karena tipe mentah diperbolehkan di Java (dan pemeriksaan tidak aman), adalah mungkin untuk menumbangkan obat generik dan berakhir dengan List<Integer>yang (misalnya) sebenarnya berisi Strings. Ini jelas bisa dibuat mustahil jika informasi jenis direifikasi; tapi pasti ada lebih dari ini !

Dapatkah orang memposting contoh hal-hal yang benar-benar ingin mereka lakukan , apakah obat generik reified tersedia? Maksud saya, jelas Anda bisa mendapatkan tipe a Listsaat runtime - tetapi apa yang akan Anda lakukan dengannya?

public <T> void foo(List<T> l) {
   if (l.getGenericType() == Integer.class) {
       //yeah baby! err, what now?

EDIT : Pembaruan cepat untuk ini karena jawabannya tampaknya terutama berkaitan dengan kebutuhan untuk memasukkan a Classsebagai parameter (misalnya EnumSet.noneOf(TimeUnit.class)). Saya mencari lebih banyak hal di sepanjang garis di mana hal ini tidak mungkin . Sebagai contoh:

List<?> l1 = api.gimmeAList();
List<?> l2 = api.gimmeAnotherList();

if (l1.getGenericType().isAssignableFrom(l2.getGenericType())) {
    l1.addAll(l2); //why on earth would I be doing this anyway?
oxbow_lakes
sumber
Apakah ini berarti bahwa Anda bisa mendapatkan kelas tipe generik saat runtime? (jika demikian, saya punya contoh!)
James B
Saya pikir sebagian besar keinginan dengan obat generik yang dapat diubah berasal dari orang-orang yang menggunakan obat generik terutama dengan koleksi, dan ingin koleksi tersebut berperilaku lebih seperti array.
kdgregory
2
Pertanyaan yang lebih menarik (bagi saya): apa yang diperlukan untuk mengimplementasikan generik gaya C ++ di Java? Tampaknya dapat dilakukan pada waktu proses, tetapi akan merusak semua pemuat kelas yang ada (karena findClass()harus mengabaikan parameterisasi, tetapi defineClass()tidak bisa). Dan seperti yang kita ketahui, The Powers That Be memegang sangat penting kompatibilitas.
kdgregory
1
Sebenarnya, Java menyediakan generik yang direifikasi dengan cara yang sangat terbatas . Saya memberikan detail lebih lanjut di utas SO ini: stackoverflow.com/questions/879855/…
Richard Gomes
The JavaOne Keynote menunjukkan bahwa Java 9 akan mendukung reifikasi.
Rangi Keen

Jawaban:

81

Dari beberapa kali saya menemukan "kebutuhan" ini, akhirnya bermuara pada konstruksi ini:

public class Foo<T> {

    private T t;

    public Foo() {
        this.t = new T(); // Help?
    }

}

Ini berfungsi di C # dengan asumsi bahwa Tmemiliki konstruktor default . Anda bahkan bisa mendapatkan jenis runtime typeof(T)dan mendapatkan konstruktornya Type.GetConstructor().

Solusi umum Java adalah meneruskan Class<T>argumen as.

public class Foo<T> {

    private T t;

    public Foo(Class<T> cls) throws Exception {
        this.t = cls.newInstance();
    }

}

(itu tidak perlu dilewatkan sebagai argumen konstruktor, karena argumen metode juga baik-baik saja, di atas hanyalah sebuah contoh, juga try-catchdihilangkan agar singkatnya)

Untuk semua konstruksi tipe umum lainnya, tipe sebenarnya dapat dengan mudah diselesaikan dengan sedikit bantuan refleksi. T&J di bawah mengilustrasikan kasus dan kemungkinan penggunaan:

BalusC
sumber
5
Ini bekerja (misalnya) di C #? Bagaimana Anda tahu bahwa T memiliki konstruktor default?
Thomas Jung
4
Ya, itu benar. Dan ini hanyalah contoh dasar (anggaplah kita bekerja dengan javabeans). Intinya adalah bahwa dengan Java Anda tidak bisa mendapatkan kelas selama runtime dengan T.classatau T.getClass(), sehingga Anda bisa mengakses semua bidang, konstruktor, dan metodenya. Itu membuat konstruksi juga tidak mungkin.
BalusC
3
Ini tampaknya sangat lemah bagi saya sebagai "masalah besar", terutama karena ini hanya mungkin berguna dalam hubungannya dengan beberapa refleksi yang sangat rapuh di sekitar konstruktor / parameter dll.
oxbow_lakes
33
Ia mengkompilasi dalam C # asalkan Anda mendeklarasikan tipe sebagai: public class Foo <T> di mana T: new (). Yang akan membatasi tipe T yang valid untuk yang berisi konstruktor tanpa parameter.
Martin Harris
3
@ Colin Anda sebenarnya dapat memberi tahu java bahwa tipe generik tertentu diperlukan untuk memperluas lebih dari satu kelas atau antarmuka. Lihat bagian "Beberapa Batas" di docs.oracle.com/javase/tutorial/java/generics/bounded.html . (Tentu saja, Anda tidak dapat mengatakan bahwa objek tersebut akan menjadi salah satu dari himpunan tertentu yang tidak berbagi metode yang dapat diabstraksi menjadi antarmuka, yang dapat diterapkan di C ++ menggunakan spesialisasi template.)
JAB
99

Hal yang paling sering mengganggu saya adalah ketidakmampuan untuk memanfaatkan beberapa pengiriman di beberapa jenis generik. Hal berikut ini tidak mungkin dan ada banyak kasus di mana itu akan menjadi solusi terbaik:

public void my_method(List<String> input) { ... }
public void my_method(List<Integer> input) { ... }
RHSeeger
sumber
5
Reifikasi sama sekali tidak diperlukan untuk dapat melakukan itu. Pemilihan metode dilakukan pada waktu kompilasi ketika informasi jenis waktu kompilasi tersedia.
Tom Hawtin - tackline
26
@ Tom: ini bahkan tidak dapat dikompilasi karena jenis penghapusan. Keduanya dikompilasi sebagai public void my_method(List input) {}. Namun saya tidak pernah menemukan kebutuhan ini, hanya karena mereka tidak memiliki nama yang sama. Jika mereka memiliki nama yang sama, saya akan mempertanyakan apakah public <T extends Object> void my_method(List<T> input) {}bukan ide yang lebih baik.
BalusC
1
Hm, saya cenderung menghindari overloading dengan jumlah parameter yang identik sama sekali, dan lebih suka sesuatu seperti myStringsMethod(List<String> input)dan myIntegersMethod(List<Integer> input)bahkan jika overloading untuk kasus seperti itu mungkin terjadi di Java.
Fabian Steeg
4
@Fabian: Yang berarti Anda harus memiliki kode terpisah, dan mencegah keuntungan yang Anda peroleh <algorithm>di C ++.
David Thornley
Saya bingung, ini pada dasarnya sama dengan poin kedua saya, namun saya mendapat 2 suara negatif? Adakah yang peduli untuk mencerahkan saya?
rsp
34

Keamanan jenis muncul dalam pikiran. Downcasting ke tipe parametrized akan selalu tidak aman tanpa reified generics:

List<String> myFriends = new ArrayList();
myFriends.add("Alice");
getSession().put("friends", myFriends);
// later, elsewhere
List<Friend> myFriends = (List<Friend>) getSession().get("friends");
myFriends.add(new Friend("Bob")); // works like a charm!
// and so...
List<String> myFriends = (List<String>) getSession().get("friends");
for (String friend : myFriends) print(friend); // ClassCastException, wtf!? 

Selain itu, abstraksi akan lebih sedikit bocor - setidaknya abstraksi yang mungkin tertarik dengan informasi runtime tentang parameter tipenya. Saat ini, jika Anda memerlukan informasi runtime jenis apa pun tentang jenis salah satu parameter umum, Anda juga harus meneruskannya Class. Dengan begitu, antarmuka eksternal Anda bergantung pada implementasi Anda (apakah Anda menggunakan RTTI tentang parameter Anda atau tidak).

gustafc
sumber
1
Ya - Saya memiliki cara untuk mengatasinya sehingga saya membuat ParametrizedListsalinan data dalam jenis pemeriksaan koleksi sumber. Ini agak mirip Collections.checkedListtetapi dapat diunggulkan dengan koleksi untuk memulai.
oxbow_lakes
@tackline - yah, beberapa abstraksi akan lebih sedikit bocor. Jika Anda memerlukan akses untuk mengetik metadata dalam implementasi Anda, antarmuka eksternal akan memberi tahu Anda karena klien perlu mengirimi Anda objek kelas.
gustafc
... artinya dengan reified generics, Anda dapat menambahkan hal-hal seperti T.class.getAnnotation(MyAnnotation.class)(where Tis a generic type) tanpa mengubah antarmuka eksternal.
gustafc
@gustafc: jika menurut Anda template C ++ memberikan keamanan tipe yang lengkap, baca ini: kdgregory.com/index.php?page=java.generics.cpp
kdgregory
@kdgregory: Saya tidak pernah mengatakan bahwa C ++ adalah tipe 100% aman - hanya saja keamanan tipe kerusakan penghapusan. Seperti yang Anda katakan, "C ++, ternyata, memiliki bentuk penghapusan tipe sendiri, yang dikenal sebagai cetakan penunjuk gaya C." Tetapi Java hanya melakukan cast dinamis (tidak menafsirkan ulang), jadi reifikasi akan menyambungkan keseluruhan ini dalam sistem tipe.
gustafc
26

Anda akan dapat membuat array umum dalam kode Anda.

public <T> static void DoStuff() {
    T[] myArray = new T[42]; // No can do
}
Turnor
sumber
ada apa dengan Object? Larik objek adalah larik referensi. Ini tidak seperti data objek yang duduk di tumpukan - semuanya ada di tumpukan.
Menjalankan Biron
19
Ketik keamanan. Saya bisa meletakkan apa pun yang saya inginkan di Object [], tetapi hanya Strings in String [].
Turnor
3
Ran: Tanpa menyindir: Anda mungkin lebih suka menggunakan bahasa scripting daripada Java, maka Anda memiliki fleksibilitas variabel yang tidak diketik di mana saja!
domba terbang
2
Array adalah kovarian (dan karenanya tidak aman untuk mengetik) dalam kedua bahasa. String[] strings = new String[1]; Object[] objects = strings; objects[0] = new Object();Mengompilasi dengan baik dalam kedua bahasa. Berjalan tidak baik.
Martijn
16

Ini adalah pertanyaan lama, ada banyak sekali jawaban, tetapi saya pikir jawaban yang ada melenceng.

"reified" hanya berarti nyata dan biasanya hanya berarti kebalikan dari penghapusan tipe.

Masalah besar terkait dengan Java Generics:

  • Persyaratan tinju yang mengerikan ini dan memutuskan hubungan antara jenis primitif dan referensi. Ini tidak terkait langsung dengan reifikasi atau penghapusan tipe. C # / Scala perbaiki ini.
  • Tidak ada tipe diri. JavaFX 8 harus menghapus "pembangun" karena alasan ini. Sama sekali tidak ada hubungannya dengan penghapusan tipe. Scala memperbaiki ini, tidak yakin tentang C #.
  • Tidak ada varian tipe sisi deklarasi. C # 4.0 / Scala memiliki ini. Sama sekali tidak ada hubungannya dengan penghapusan tipe.
  • Tidak bisa membebani void method(List<A> l)dan method(List<B> l). Hal ini karena penghapusan tipe tetapi sangat kecil.
  • Tidak ada dukungan untuk refleksi jenis runtime. Ini adalah inti dari penghapusan tipe. Jika Anda menyukai kompiler super canggih yang memverifikasi dan membuktikan sebanyak mungkin logika program Anda pada waktu kompilasi, Anda harus menggunakan refleksi sesedikit mungkin dan jenis penghapusan jenis ini tidak akan mengganggu Anda. Jika Anda menyukai lebih banyak patchy, scripty, pemrograman tipe dinamis dan tidak terlalu peduli tentang kompiler yang membuktikan sebanyak mungkin logika Anda benar, maka Anda menginginkan refleksi yang lebih baik dan memperbaiki penghapusan tipe itu penting.
pengguna2684301
sumber
1
Kebanyakan saya merasa kesulitan dengan kasus serialisasi. Anda sering ingin bisa mengendus jenis kelas hal-hal umum mendapatkan serial tetapi Anda terhenti karena jenis penghapusan. Itu membuat sulit untuk melakukan sesuatu seperti inideserialize(thingy, List<Integer>.class)
Cogman
3
Saya rasa ini adalah jawaban terbaik. Terutama bagian yang menjelaskan masalah apa yang sebenarnya pada dasarnya karena penghapusan jenis dan apa saja masalah desain bahasa Java. Hal pertama yang saya katakan kepada orang-orang yang memulai erasure-merengek adalah bahwa Scala dan Haskell juga bekerja berdasarkan tipe erasure.
aemxdp
13

Serialisasi akan lebih mudah dengan reifikasi. Yang kami inginkan adalah

deserialize(thingy, List<Integer>.class);

Yang harus kita lakukan adalah

deserialize(thing, new TypeReference<List<Integer>>(){});

terlihat jelek dan bekerja dengan funky.

Ada juga kasus di mana akan sangat membantu untuk mengatakan sesuatu seperti

public <T> void doThings(List<T> thingy) {
    if (T instanceof Q)
      doCrazyness();
  }

Hal-hal ini tidak sering menggigit, tetapi akan menggigit saat terjadi.

Cogman
sumber
Ini. Seribu kali ini. Setiap kali saya mencoba untuk menulis kode deserialization di Java saya menghabiskan hari meratapi bahwa saya tidak bekerja di hal lain
Dasar
11

Paparan saya tentang Java Geneircs sangat terbatas, dan terlepas dari poin-poin yang telah disebutkan oleh jawaban lain, terdapat skenario yang dijelaskan dalam buku Java Generics and Collections , oleh Maurice Naftalin dan Philip Walder, di mana obat generik yang direifikasi berguna.

Karena jenis tidak dapat diubah, tidak mungkin memiliki pengecualian berparameter.

Misalnya pernyataan formulir di bawah ini tidak valid.

class ParametericException<T> extends Exception // compile error

Ini karena klausa catch memeriksa apakah pengecualian yang ditampilkan cocok dengan tipe yang diberikan. Pemeriksaan ini sama dengan pemeriksaan yang dilakukan oleh uji contoh dan karena jenisnya tidak dapat diubah, bentuk pernyataan di atas tidak valid.

Jika kode di atas valid maka penanganan pengecualian dengan cara di bawah ini akan dimungkinkan:

try {
     throw new ParametericException<Integer>(42);
} catch (ParametericException<Integer> e) { // compile error
  ...
}

Buku tersebut juga menyebutkan bahwa jika generik Java didefinisikan mirip dengan cara kerangka C ++ didefinisikan (perluasan), hal itu dapat mengarah pada penerapan yang lebih efisien karena ini menawarkan lebih banyak peluang untuk pengoptimalan. Tetapi tidak menawarkan penjelasan lebih dari ini, jadi penjelasan (petunjuk) dari orang-orang yang berpengetahuan akan sangat membantu.

sateesh
sumber
1
Ini poin yang valid tetapi saya tidak begitu yakin mengapa kelas pengecualian jadi parametrized akan berguna. Bisakah Anda mengubah jawaban Anda untuk memuat contoh singkat kapan ini mungkin berguna?
oxbow_lakes
@oxbow_lakes: Maaf, pengetahuan saya tentang Java Generics sangat terbatas dan saya sedang berusaha untuk memperbaikinya. Jadi sekarang saya tidak dapat memikirkan contoh apa pun di mana pengecualian parametrized dapat berguna. Akan mencoba memikirkannya. Terima kasih.
sateesh
Ini bisa bertindak sebagai pengganti beberapa warisan jenis pengecualian.
meriton
Peningkatan kinerja adalah bahwa saat ini, parameter tipe harus mewarisi dari Object, membutuhkan tinju tipe primitif, yang memaksakan eksekusi dan overhead memori.
meriton
8

Array mungkin akan bermain jauh lebih baik dengan obat generik jika mereka direifikasi.

Hank Gay
sumber
2
Tentu, tapi mereka masih punya masalah. List<String>bukan List<Object>.
Tom Hawtin - tackline
Setuju - tetapi hanya untuk primitif (Integer, Long, dll). Untuk Objek "biasa", ini sama. Karena primitif tidak bisa menjadi tipe parameter (masalah yang jauh lebih serius, setidaknya IMHO), saya tidak melihat ini sebagai rasa sakit yang nyata.
Menjalankan Biron
3
Masalah dengan array adalah kovariansinya, tidak ada hubungannya dengan reifikasi.
Kambuh
5

Saya memiliki pembungkus yang menyajikan jdbc resultet sebagai iterator, (itu berarti saya dapat menguji unit operasi yang berasal dari database jauh lebih mudah melalui injeksi ketergantungan).

API terlihat seperti di Iterator<T>mana T adalah beberapa tipe yang dapat dibangun hanya dengan menggunakan string dalam konstruktor. Iterator kemudian melihat string yang dikembalikan dari kueri sql dan kemudian mencoba mencocokkannya dengan konstruktor tipe T.

Dengan cara generik saat ini diimplementasikan, saya juga harus meneruskan kelas objek yang akan saya buat dari resultet saya. Jika saya mengerti dengan benar, jika obat generik direifikasi, saya bisa memanggil T.getClass () mendapatkan konstruktornya, dan kemudian tidak perlu menampilkan hasil Class.newInstance (), yang akan jauh lebih rapi.

Pada dasarnya, saya pikir itu membuat penulisan API (daripada hanya menulis aplikasi) lebih mudah, karena Anda dapat menyimpulkan lebih banyak dari objek, dan dengan demikian lebih sedikit konfigurasi yang diperlukan ... Saya tidak menghargai implikasi anotasi sampai saya melihat mereka digunakan dalam hal-hal seperti spring atau xstream sebagai ganti rim config.

James B
sumber
Namun, lulus di kelas tampaknya lebih aman bagi saya. Dalam kasus apa pun, membuat instance dari kueri database secara reflektif sangat rentan terhadap perubahan seperti refactoring (dalam kode dan database Anda). Saya rasa saya sedang mencari hal-hal yang tidak memungkinkan untuk menyediakan kelasnya
oxbow_lakes
5

Satu hal yang menyenangkan adalah menghindari tinju untuk tipe primitif (nilai). Ini agak terkait dengan keluhan larik yang diajukan orang lain, dan dalam kasus di mana penggunaan memori dibatasi, hal itu sebenarnya dapat membuat perbedaan yang signifikan.

Ada juga beberapa jenis masalah saat menulis kerangka di mana kemampuan untuk merefleksikan tipe berparameter itu penting. Tentu saja ini bisa diatasi dengan melewatkan objek kelas pada saat runtime, tapi ini mengaburkan API dan menempatkan beban tambahan pada pengguna framework.

kvb
sumber
2
Ini pada dasarnya bermuara pada bisa melakukan di new T[]mana T adalah tipe primitif!
oxbow_lakes
2

Bukan berarti Anda akan mencapai sesuatu yang luar biasa. Ini hanya akan lebih sederhana untuk dipahami. Penghapusan jenis sepertinya merupakan waktu yang sulit bagi pemula, dan pada akhirnya membutuhkan pemahaman seseorang tentang cara kerja compiler.

Pendapat saya adalah, bahwa obat generik hanyalah tambahan yang menghemat banyak pengecoran yang berlebihan.

Bozho
sumber
Inilah yang pasti obat generik di Jawa . Dalam C # dan bahasa lain, mereka adalah alat yang ampuh
Dasar
1

Sesuatu yang terlewatkan oleh semua jawaban di sini yang selalu memusingkan saya adalah karena jenisnya dihapus, Anda tidak dapat mewarisi antarmuka generik dua kali. Ini bisa menjadi masalah saat Anda ingin membuat antarmuka yang halus.

    public interface Service<KEY,VALUE> {
           VALUE get(KEY key);
    }

    public class PersonService implements Service<Long, Person>,
        Service<String, Person> //Can not do!!
ExCodeCowboy
sumber
0

Inilah salah satu yang menarik perhatian saya hari ini: tanpa reifikasi, jika Anda menulis metode yang menerima daftar vararg dari item umum ... penelepon dapat BERPIKIR bahwa mereka aman untuk mengetik, tetapi secara tidak sengaja memasukkan bahan mentah lama, dan meledakkan metode Anda.

Sepertinya tidak mungkin itu akan terjadi? ... Tentu, sampai ... Anda menggunakan Kelas sebagai tipe data Anda. Pada titik ini, pemanggil Anda dengan senang hati akan mengirimi Anda banyak objek Kelas, tetapi kesalahan ketik sederhana akan mengirimi Anda objek Kelas yang tidak sesuai dengan T, dan bencana melanda.

(NB: Saya mungkin telah membuat kesalahan di sini, tapi mencari-cari di sekitar "varargs generik", di atas tampaknya hanya apa yang Anda harapkan. Hal yang membuat masalah ini praktis adalah penggunaan Kelas, saya pikir - penelepon tampaknya kurang hati-hati :()


Misalnya, saya menggunakan paradigma yang menggunakan objek Kelas sebagai kunci dalam peta (ini lebih kompleks daripada peta sederhana - tetapi secara konseptual itulah yang terjadi).

misalnya ini berfungsi dengan baik di Java Generics (contoh sepele):

public <T extends Component> Set<UUID> getEntitiesPossessingComponent( Class<T> componentType)
    {
        // find the entities that are mapped (somehow) from that class. Very type-safe
    }

misalnya tanpa reifikasi di Java Generics, yang ini menerima objek "Kelas" APA PUN. Dan itu hanya sebagian kecil dari kode sebelumnya:

public <T extends Component> Set<UUID> getEntitiesPossessingComponents( Class<T>... componentType )
    {
        // find the entities that are mapped (somehow) to ALL of those classes
    }

Metode di atas harus ditulis ribuan kali dalam satu proyek - sehingga kemungkinan terjadinya kesalahan manusia menjadi tinggi. Kesalahan debugging terbukti "tidak menyenangkan". Saat ini saya mencoba mencari alternatif, tapi jangan berharap banyak.

Adam
sumber