Mengapa Java tidak mengizinkan subclass generik dari Throwable?


146

Menurut Sepecification Bahasa Jawa , edisi ke-3:

Ini adalah kesalahan waktu kompilasi jika kelas generik adalah subkelas langsung atau tidak langsung dari Throwable.

Saya ingin memahami mengapa keputusan ini dibuat. Apa yang salah dengan pengecualian umum?

(Sejauh yang saya tahu, obat generik hanyalah kompilasi waktu sintaksis gula, dan mereka akan diterjemahkan ke Objectdalam .classfile, sehingga secara efektif menyatakan kelas generik adalah seolah-olah semua yang ada di dalamnya adalah Object. Tolong perbaiki saya jika saya salah .)


1
Argumen tipe generik digantikan oleh batas atas, yang secara default adalah Object. Jika Anda memiliki sesuatu seperti Daftar <? extends A>, lalu A digunakan dalam file kelas.
Torsten Marek

@Torsten terima kasih. Saya tidak memikirkan kasus itu sebelumnya.
Hosam Aly

2
Ini pertanyaan wawancara yang bagus, yang ini.
skaffman

@ TorstenMarek: Jika ada panggilan myList.get(i), jelas getmasih mengembalikan sebuah Object. Apakah kompiler memasukkan gips Auntuk menangkap beberapa kendala saat runtime? Jika tidak, OP benar bahwa pada akhirnya akan turun ke Objects saat runtime. (File kelas tentu berisi metadata tentang A, tetapi itu hanya metadata AFAIK.)
Mihai Danila

Jawaban:


155

Seperti yang dikatakan oleh tanda, tipe tidak dapat diverifikasi, yang merupakan masalah dalam kasus berikut:

try {
   doSomeStuff();
} catch (SomeException<Integer> e) {
   // ignore that
} catch (SomeException<String> e) {
   crashAndBurn()
}

Keduanya SomeException<Integer>dan SomeException<String>terhapus ke tipe yang sama, tidak ada cara bagi JVM untuk membedakan instance pengecualian, dan oleh karena itu tidak ada cara untuk mengetahui catchblok mana yang harus dieksekusi.


3
tapi apa artinya "dapat diverifikasi"?
aberrant80

61
Jadi aturannya tidak boleh "tipe generik tidak dapat subclass Throwable" tetapi sebaliknya "catch clauses harus selalu menggunakan tipe mentah".
Archie

3
Mereka hanya bisa melarang menggunakan dua blok tangkap dengan tipe yang sama bersama-sama. Sehingga menggunakan SomeExc <Integer> saja akan sah, hanya menggunakan SomeExc <Integer> dan SomeExc <String> bersama-sama akan ilegal. Itu tidak akan membuat masalah, atau akankah itu?
Viliam Búr

3
Oh, sekarang aku mengerti. Solusi saya akan menyebabkan masalah dengan RuntimeExceptions, yang tidak harus dideklarasikan. Jadi jika SomeExc adalah subclass dari RuntimeException, saya bisa melempar dan secara eksplisit menangkap SomeExc <Integer>, tapi mungkin beberapa fungsi lainnya secara diam-diam melempar SomeExc <String> dan blok tangkap saya untuk SomeExc <Integer> akan secara tidak sengaja menangkap itu juga.
Viliam Búr

4
@ SuperJedi224 - Tidak. Itu memang benar - mengingat kendala bahwa obat generik harus kompatibel.
Stephen C

14

Berikut adalah contoh sederhana cara menggunakan pengecualian:

class IntegerExceptionTest {
  public static void main(String[] args) {
    try {
      throw new IntegerException(42);
    } catch (IntegerException e) {
      assert e.getValue() == 42;
    }
  }
}

Tubuh pernyataan COBALAH melempar pengecualian dengan nilai yang diberikan, yang ditangkap oleh klausa tangkapan.

Sebaliknya, definisi pengecualian baru berikut ini dilarang, karena itu menciptakan tipe parameter:

class ParametricException<T> extends Exception {  // compile-time error
  private final T value;
  public ParametricException(T value) { this.value = value; }
  public T getValue() { return value; }
}

Upaya untuk mengkompilasi laporan di atas kesalahan:

% javac ParametricException.java
ParametricException.java:1: a generic class may not extend
java.lang.Throwable
class ParametricException<T> extends Exception {  // compile-time error
                                     ^
1 error

Pembatasan ini masuk akal karena hampir semua upaya untuk menangkap pengecualian seperti itu harus gagal, karena jenisnya tidak dapat diverifikasi. Orang mungkin mengharapkan penggunaan pengecualian sebagai sesuatu seperti berikut ini:

class ParametricExceptionTest {
  public static void main(String[] args) {
    try {
      throw new ParametricException<Integer>(42);
    } catch (ParametricException<Integer> e) {  // compile-time error
      assert e.getValue()==42;
    }
  }
}

Ini tidak diizinkan, karena jenis dalam klausa tangkapan tidak dapat diverifikasi. Pada saat penulisan ini, kompiler Sun melaporkan kaskade kesalahan sintaksis dalam kasus seperti ini:

% javac ParametricExceptionTest.java
ParametricExceptionTest.java:5: <identifier> expected
    } catch (ParametricException<Integer> e) {
                                ^
ParametricExceptionTest.java:8: ')' expected
  }
  ^
ParametricExceptionTest.java:9: '}' expected
}
 ^
3 errors

Karena pengecualian tidak bisa parametrik, sintaks dibatasi sehingga jenisnya harus ditulis sebagai pengidentifikasi, tanpa parameter berikut.


2
Apa yang Anda maksud ketika Anda mengatakan 'dapat diverifikasi'? 'dapat diverifikasi' bukan sebuah kata.
ForYourOwnGood

1
Saya sendiri tidak tahu kata itu, tetapi pencarian cepat di google membuat saya seperti ini: java.sun.com/docs/books/jls/third_edition/html/…
Hosam Aly


13

Ini pada dasarnya karena dirancang dengan cara yang buruk.

Masalah ini mencegah desain abstrak yang bersih misalnya,

public interface Repository<ID, E extends Entity<ID>> {

    E getById(ID id) throws EntityNotFoundException<E, ID>;
}

Fakta bahwa klausa tangkapan akan gagal untuk obat generik tidak diverifikasi bukan alasan untuk itu. Kompiler hanya dapat melarang jenis generik beton yang memperpanjang Throwable atau melarang generik di dalam klausa tangkapan.



1
Satu-satunya cara mereka dapat mendesainnya lebih baik adalah dengan membuat ~ 10 tahun kode pelanggan tidak kompatibel. Itu adalah keputusan bisnis yang layak. Desainnya benar ... mengingat konteksnya .
Stephen C

1
Jadi bagaimana Anda akan menangkap pengecualian ini? Satu-satunya cara yang akan berhasil adalah menangkap jenis mentah EntityNotFoundException. Tapi itu akan membuat obat generik tidak berguna.
Frans

4

Generik diperiksa pada waktu kompilasi untuk ketepatan jenis. Informasi tipe umum kemudian dihapus dalam proses yang disebut tipe erasure . Misalnya, List<Integer>akan dikonversi ke jenis non-generik List.

Karena penghapusan tipe , parameter tipe tidak dapat ditentukan pada saat run-time.

Anggaplah Anda diizinkan untuk memperpanjang Throwableseperti ini:

public class GenericException<T> extends Throwable

Sekarang mari kita pertimbangkan kode berikut:

try {
    throw new GenericException<Integer>();
}
catch(GenericException<Integer> e) {
    System.err.println("Integer");
}
catch(GenericException<String> e) {
    System.err.println("String");
}

Karena tipe erasure , runtime tidak akan tahu blok tangkapan mana yang harus dieksekusi.

Oleh karena itu adalah kesalahan waktu kompilasi jika kelas generik adalah subclass langsung atau tidak langsung dari Throwable.

Sumber: Masalah dengan tipe erasure


Terima kasih. Ini adalah jawaban yang sama dengan yang diberikan oleh Torsten .
Hosam Aly

Bukan itu. Jawaban Torsten tidak membantu saya, karena tidak menjelaskan apa jenis penghapusan / reifikasi.
Selamat Malam Nerd Pride

2

Saya berharap itu karena tidak ada cara untuk menjamin parameterisasi. Pertimbangkan kode berikut:

try
{
    doSomethingThatCanThrow();
}
catch (MyException<Foo> e)
{
    // handle it
}

Seperti yang Anda perhatikan, parameterisasi hanyalah gula sintaksis. Namun, kompiler mencoba memastikan bahwa parameterisasi tetap konsisten di semua referensi ke objek dalam lingkup kompilasi. Dalam kasus pengecualian, kompiler tidak memiliki cara untuk menjamin bahwa MyException hanya dibuang dari ruang lingkup yang sedang diproses.


Ya, tapi mengapa itu tidak ditandai sebagai "tidak aman", seperti pada gips misalnya?
eljenso

Karena dengan para pemain, Anda memberi tahu kompiler "Saya tahu bahwa jalur eksekusi ini menghasilkan hasil yang diharapkan." Dengan pengecualian, Anda tidak bisa mengatakan (untuk semua kemungkinan pengecualian) "Saya tahu di mana ini dilemparkan." Tapi, seperti yang saya katakan di atas, itu dugaan; Saya tidak ada di sana.
kdgregory

"Aku tahu bahwa jalur eksekusi ini menghasilkan hasil yang diharapkan." Anda tidak tahu, Anda harap begitu. Itu sebabnya generik dan downcast secara statis tidak aman, tetapi mereka tetap diizinkan. Saya meningkatkan jawaban Torsten, karena di sana saya melihat masalahnya. Di sini saya tidak.
eljenso

Jika Anda tidak tahu bahwa suatu objek adalah tipe tertentu, Anda tidak harus menuangnya. Seluruh ide para pemain adalah bahwa Anda memiliki lebih banyak pengetahuan daripada kompiler, dan menjadikan pengetahuan itu secara eksplisit bagian dari kode.
kdgregory

Ya, dan di sini Anda mungkin memiliki lebih banyak pengetahuan daripada kompiler juga, karena Anda ingin melakukan konversi yang tidak dicentang dari MyException ke MyException <Foo>. Mungkin Anda "tahu" itu akan menjadi MyException <Foo>.
eljenso
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.