Metode memiliki penghapusan yang sama dengan metode lain dalam jenis

381

Mengapa tidak sah memiliki dua metode berikut di kelas yang sama?

class Test{
   void add(Set<Integer> ii){}
   void add(Set<String> ss){}
}

Saya mendapatkan compilation error

Metode add (Set) memiliki add erasure yang sama (Set) seperti metode lain dalam tipe Test.

sementara saya bisa mengatasinya, saya bertanya-tanya mengapa javac tidak suka ini.

Saya dapat melihat bahwa dalam banyak kasus, logika kedua metode tersebut akan sangat mirip dan dapat digantikan oleh satu

public void add(Set<?> set){}

metode, tetapi ini tidak selalu terjadi.

Ini sangat menjengkelkan jika Anda ingin memiliki dua constructorsargumen seperti itu karena Anda tidak dapat mengubah nama salah satu saja constructors.

Omry Yadan
sumber
1
bagaimana jika Anda kehabisan struktur data dan Anda masih membutuhkan lebih banyak versi?
Omry Yadan
1
Anda bisa membuat kelas khusus yang mewarisi dari versi dasar.
willlma
1
OP, apakah Anda menemukan beberapa solusi untuk masalah konstruktor? Saya perlu menerima dua jenis Listdan saya tidak tahu bagaimana menanganinya.
Tomáš Zato - Pasang kembali Monica
9
Ketika bekerja dengan Java, saya sangat merindukan C # ...
Svish
1
@ TomášZato, saya menyelesaikannya dengan menambahkan param boneka ke konstruktor: Boolean noopSignatureOverload.
Nthalk

Jawaban:

357

Aturan ini dimaksudkan untuk menghindari konflik dalam kode lawas yang masih menggunakan tipe mentah.

Berikut ilustrasi mengapa ini tidak diizinkan, diambil dari JLS. Misalkan, sebelum obat generik diperkenalkan ke Java, saya menulis beberapa kode seperti ini:

class CollectionConverter {
  List toList(Collection c) {...}
}

Anda memperpanjang kelas saya, seperti ini:

class Overrider extends CollectionConverter{
  List toList(Collection c) {...}
}

Setelah pengenalan obat generik, saya memutuskan untuk memperbarui perpustakaan saya.

class CollectionConverter {
  <T> List<T> toList(Collection<T> c) {...}
}

Anda tidak siap untuk membuat pembaruan apa pun, sehingga Anda meninggalkan Overriderkelas Anda sendirian. Untuk mengganti toList()metode ini dengan benar , perancang bahasa memutuskan bahwa jenis mentah "override-equivalen" untuk semua jenis yang dibuat. Ini berarti bahwa meskipun tanda tangan metode Anda tidak lagi secara resmi sama dengan tanda tangan superclass saya, metode Anda masih menimpanya.

Sekarang, waktu berlalu dan Anda memutuskan siap memperbarui kelas Anda. Tetapi Anda sedikit mengacaukan, dan alih-alih mengedit toList()metode mentah yang ada, Anda menambahkan metode baru seperti ini:

class Overrider extends CollectionConverter {
  @Override
  List toList(Collection c) {...}
  @Override
  <T> List<T> toList(Collection<T> c) {...}
}

Karena mengesampingkan ekuivalensi jenis mentah, kedua metode berada dalam bentuk yang valid untuk mengganti toList(Collection<T>)metode. Tetapi tentu saja, kompiler perlu menyelesaikan satu metode. Untuk menghilangkan ambiguitas ini, kelas tidak diperbolehkan memiliki beberapa metode yang override-equivalen — yaitu, beberapa metode dengan tipe parameter yang sama setelah penghapusan.

Kuncinya adalah bahwa ini adalah aturan bahasa yang dirancang untuk menjaga kompatibilitas dengan kode lama menggunakan tipe mentah. Ini bukan batasan yang diperlukan oleh penghapusan parameter tipe; karena resolusi metode terjadi pada waktu kompilasi, menambahkan tipe generik ke pengidentifikasi metode sudah cukup.

erickson
sumber
3
Jawaban dan contoh yang bagus! Saya tidak yakin, bagaimanapun, jika saya sepenuhnya memahami kalimat terakhir Anda ("Karena resolusi metode terjadi pada waktu kompilasi, sebelum penghapusan, jenis reifikasi tidak diperlukan untuk membuat ini berfungsi."). Bisakah Anda menguraikan sedikit?
Jonas Eicher
2
Masuk akal. Saya baru saja menghabiskan waktu memikirkan reifikasi jenis dalam metode templat, tapi ya: kompiler memastikan metode yang tepat akan dipilih sebelum penghapusan jenis. Cantik. Jika tidak ternoda oleh masalah kompatibilitas kode legacy.
Jonas Eicher
1
@daveloyall Tidak, saya tidak mengetahui opsi seperti itu untuk javac.
erickson
13
Bukan pertama kali saya menemukan kesalahan Java yang tidak ada kesalahan sama sekali dan bisa dikompilasi jika hanya penulis Jawa yang menggunakan peringatan seperti yang dilakukan orang lain. Hanya mereka yang berpikir mereka tahu segalanya dengan lebih baik.
Tomáš Zato - Pasang kembali Monica
1
@ TomášZato Tidak, saya tidak tahu solusi yang bersih untuk itu. Jika Anda menginginkan sesuatu yang kotor, Anda bisa melewati List<?>dan beberapa enumyang Anda tentukan untuk menunjukkan tipe. Jenis-logika spesifik sebenarnya bisa dalam metode pada enum. Sebagai alternatif, Anda mungkin ingin membuat dua kelas yang berbeda, bukan satu kelas dengan dua konstruktor yang berbeda. Logika umum adalah dalam superclass atau objek helper yang didelegasikan kedua tipe.
erickson
118

Java generics menggunakan tipe erasure. Bit dalam kurung sudut ( <Integer>dan <String>) akan dihapus, sehingga Anda akan berakhir dengan dua metode yang memiliki tanda tangan yang identik (yang add(Set)Anda lihat dalam kesalahan). Itu tidak diizinkan karena runtime tidak akan tahu mana yang harus digunakan untuk setiap kasus.

Jika Java pernah mendapat reified generics, maka Anda bisa melakukan ini, tapi itu mungkin tidak mungkin sekarang.

GaryF
sumber
24
Saya minta maaf tetapi jawaban Anda (dan jawaban lainnya) tidak menjelaskan mengapa ada kesalahan di sini. Resolusi kelebihan dilakukan pada waktu kompilasi dan kompiler pasti memiliki jenis informasi yang diperlukan untuk memutuskan metode mana yang akan dihubungkan oleh alamat atau dengan metode apa pun yang dirujuk dalam bytecode yang saya percaya bukan tanda tangan. Saya bahkan berpikir beberapa kompiler akan memungkinkan ini untuk dikompilasi.
Stilgar
5
@ Stilgar apa yang harus menghentikan metode yang dipanggil atau diperiksa melalui refleksi? Daftar metode yang dikembalikan oleh Class.getMethods () akan memiliki dua metode yang identik, yang tidak masuk akal.
Adrian Mouat
5
Informasi refleksi dapat / harus mengandung metadata yang diperlukan untuk bekerja dengan obat generik. Jika tidak, bagaimana kompiler Java mengetahui tentang metode generik ketika Anda mengimpor pustaka yang sudah dikompilasi?
Stilgar
4
Maka metode getMethod perlu diperbaiki. Sebagai contoh, perkenalkan overload yang menetapkan overload generik dan buat metode asli mengembalikan versi non-generik saja, tidak mengembalikan metode apa pun yang dianotasi sebagai generik. Tentu saja ini seharusnya dilakukan dalam versi 1.5. Jika mereka melakukannya sekarang, mereka akan merusak kompatibilitas metode ini. Saya mendukung pernyataan saya bahwa penghapusan tipe tidak menentukan perilaku ini. Ini adalah implementasi yang tidak mendapatkan pekerjaan yang cukup mungkin karena sumber daya yang terbatas.
Stilgar
1
Ini bukan jawaban yang tepat, tetapi meringkas masalah dengan cepat dalam fiksi yang berguna: metode tanda tangan terlalu mirip, kompiler mungkin tidak tahu bedanya, dan kemudian Anda akan mendapatkan "masalah kompilasi yang belum terselesaikan."
worc
46

Ini karena Java Generics diimplementasikan dengan Type Erasure .

Metode Anda akan diterjemahkan, pada waktu kompilasi, ke sesuatu seperti:

Resolusi metode terjadi pada waktu kompilasi dan tidak mempertimbangkan parameter tipe. ( lihat jawaban erickson )

void add(Set ii);
void add(Set ss);

Kedua metode memiliki tanda tangan yang sama tanpa parameter tipe, karenanya kesalahan.

bruno conde
sumber
21

Masalahnya adalah itu Set<Integer>dan Set<String>sebenarnya diperlakukan sebagai Setdari JVM. Memilih jenis untuk Set (String atau Integer dalam kasus Anda) hanya gula sintaksis yang digunakan oleh kompiler. JVM tidak dapat membedakan antara Set<String>dan Set<Integer>.

kgiannakakis
sumber
7
Memang benar bahwa runtime JVM tidak memiliki informasi untuk membedakan masing-masing Set, tetapi karena resolusi metode terjadi pada waktu kompilasi, ketika informasi yang diperlukan tersedia, ini tidak relevan. Masalahnya adalah bahwa membiarkan kelebihan ini akan bertentangan dengan penyisihan untuk jenis mentah, sehingga mereka dibuat ilegal dalam sintaksis Java.
erickson
@erickson Bahkan ketika kompiler tahu metode apa untuk memanggil, tidak bisa seperti pada bytecode, mereka berdua terlihat persis sama. Anda harus mengubah cara pemanggilan metode ditentukan, karena (Ljava/util/Collection;)Ljava/util/List;tidak berfungsi. Anda dapat menggunakan (Ljava/util/Collection<String>;)Ljava/util/List<String>;, tetapi itu adalah perubahan yang tidak kompatibel dan Anda akan mengalami masalah yang tidak dapat diselesaikan di tempat-tempat yang Anda miliki adalah tipe yang terhapus. Anda mungkin harus menghapus sepenuhnya, tetapi itu cukup rumit.
maaartinus
@maaartinus Ya, saya setuju Anda harus mengubah specifier metode. Saya mencoba untuk mendapatkan beberapa masalah yang tidak terpecahkan yang membuat mereka menyerah pada upaya itu.
erickson
7

Tentukan satu Metode tanpa tipe suka void add(Set ii){}

Anda dapat menyebutkan jenisnya saat memanggil metode berdasarkan pilihan Anda. Ini akan bekerja untuk semua jenis set.

Idrees Ashraf
sumber
3

Bisa jadi kompiler menerjemahkan Set (Integer) ke Set (Object) dalam kode byte java. Jika demikian, Set (Integer) hanya akan digunakan pada tahap kompilasi untuk memeriksa sintaks.

Rossoft
sumber
5
Secara teknis ini hanya tipe mentah Set. Generik tidak ada dalam kode byte, mereka sintaksis gula untuk casting dan memberikan keamanan tipe waktu kompilasi.
Andrzej Doyle
1

Saya menabrak ini ketika mencoba menulis sesuatu seperti: Continuable<T> callAsync(Callable<T> code) {....} dan Continuable<Continuable<T>> callAsync(Callable<Continuable<T>> veryAsyncCode) {...} Mereka menjadi untuk kompiler 2 definisi Continuable<> callAsync(Callable<> veryAsyncCode) {...}

Penghapusan tipe secara harfiah berarti menghapus informasi argumen tipe dari obat generik. Ini SANGAT menjengkelkan, tetapi ini adalah batasan yang akan ada dengan Java untuk sementara waktu. Untuk kasus konstruktor tidak banyak yang bisa dilakukan, 2 subclass baru khusus dengan parameter yang berbeda dalam konstruktor misalnya. Atau gunakan metode inisialisasi sebagai gantinya ... (konstruktor virtual?) Dengan nama yang berbeda ...

untuk metode operasi yang sama penggantian nama akan membantu, seperti

class Test{
   void addIntegers(Set<Integer> ii){}
   void addStrings(Set<String> ss){}
}

Atau dengan beberapa nama yang lebih deskriptif, mendokumentasikan diri sendiri untuk kasus oyu, suka addNamesdan addIndexesatau semacamnya.

Kote Isaev
sumber