Solusi untuk Java memeriksa pengecualian

49

Saya menghargai banyak fitur Java 8 baru tentang lambdas dan antarmuka metode standar. Namun, saya masih bosan dengan pengecualian yang diperiksa. Misalnya, jika saya hanya ingin membuat daftar semua bidang yang terlihat dari suatu objek, saya hanya ingin menulis ini:

    Arrays.asList(p.getClass().getFields()).forEach(
        f -> System.out.println(f.get(p))
    );

Namun, karena getmetode ini dapat membuang pengecualian yang diperiksa, yang tidak setuju dengan Consumerkontrak antarmuka, maka saya harus menangkap pengecualian itu dan menulis kode berikut:

    Arrays.asList(p.getClass().getFields()).forEach(
            f -> {
                try {
                    System.out.println(f.get(p));
                } catch (IllegalArgumentException | IllegalAccessException ex) {
                    throw new RuntimeException(ex);
                }
            }
    );

Namun dalam kebanyakan kasus saya hanya ingin pengecualian untuk dilemparkan sebagai RuntimeExceptiondan biarkan program menangani, atau tidak, pengecualian tanpa kesalahan kompilasi.

Jadi, saya ingin pendapat Anda tentang solusi kontroversial saya untuk gangguan pengecualian yang diperiksa. Untuk itu, saya membuat antarmuka tambahan ConsumerCheckException<T>dan fungsi utilitas rethrow( diperbarui sesuai dengan saran komentar Doval ) sebagai berikut:

  @FunctionalInterface
  public interface ConsumerCheckException<T>{
      void accept(T elem) throws Exception;
  }

  public class Wrappers {
      public static <T> Consumer<T> rethrow(ConsumerCheckException<T> c) {
        return elem -> {
          try {
            c.accept(elem);
          } catch (Exception ex) {
            /**
             * within sneakyThrow() we cast to the parameterized type T. 
             * In this case that type is RuntimeException. 
             * At runtime, however, the generic types have been erased, so 
             * that there is no T type anymore to cast to, so the cast
             * disappears.
             */
            Wrappers.<RuntimeException>sneakyThrow(ex);
          }
        };
      }

      /**
       * Reinier Zwitserloot who, as far as I know, had the first mention of this
       * technique in 2009 on the java posse mailing list.
       * http://www.mail-archive.com/[email protected]/msg05984.html
       */
      public static <T extends Throwable> T sneakyThrow(Throwable t) {
          throw (T) t;
      }
  }

Dan sekarang saya bisa menulis:

    Arrays.asList(p.getClass().getFields()).forEach(
            rethrow(f -> System.out.println(f.get(p)))
    );

Saya tidak yakin bahwa ini adalah ungkapan terbaik untuk membalikkan pengecualian yang diperiksa, tetapi seperti yang saya jelaskan, saya ingin memiliki cara yang lebih nyaman untuk mencapai contoh pertama saya tanpa berurusan dengan pengecualian yang diperiksa dan ini adalah cara yang lebih sederhana yang saya temukan untuk melakukannya.

Fernando Miguel Carvalho
sumber
2
Selain tautan Robert, lihat juga Pengecualian Terpercaya Sneakily Throwing . Jika Anda mau, Anda bisa menggunakan sneakyThrowbagian dalam rethrowuntuk membuang yang asli, memeriksa pengecualian alih-alih membungkusnya dalam RuntimeException. Atau Anda dapat menggunakan @SneakyThrowsanotasi dari Project Lombok yang melakukan hal yang sama.
Doval
1
Perhatikan juga bahwa Consumers dalam forEachdapat dieksekusi secara paralel saat menggunakan paralel Streams. Suatu throwable yang muncul dari dalam konsumen kemudian akan menyebar ke thread panggilan, yang 1) tidak akan menghentikan konsumen yang berjalan bersamaan lainnya, yang mungkin atau mungkin tidak sesuai, dan 2) jika lebih dari satu konsumen melempar sesuatu, hanya salah satu throwables akan terlihat oleh utas panggilan.
Joonas Pulakka
2
Lambdas sangat jelek.
Tulains Córdova

Jawaban:

16

Keuntungan, kerugian, dan keterbatasan teknik Anda:

  • Jika kode panggilan untuk menangani pengecualian yang dicentang Anda HARUS menambahkannya ke klausa pelemparan dari metode yang berisi aliran. Kompiler tidak akan memaksa Anda untuk menambahkannya lagi, jadi lebih mudah untuk melupakannya. Sebagai contoh:

    public void test(Object p) throws IllegalAccessException {
        Arrays.asList(p.getClass().getFields()).forEach(rethrow(f -> System.out.println(f.get(p))));
    }
  • Jika kode panggilan sudah menangani pengecualian yang dicentang, kompiler AKAN mengingatkan Anda untuk menambahkan klausa pelemparan ke deklarasi metode yang berisi aliran (jika Anda tidak melakukannya akan mengatakan: Pengecualian tidak pernah dilemparkan ke badan pernyataan percobaan yang sesuai) .

  • Dalam hal apa pun, Anda tidak akan dapat mengelilingi aliran itu sendiri untuk menangkap pengecualian yang dicentang DI DALAM metode yang berisi aliran (jika Anda mencoba, kompiler akan mengatakan: Pengecualian tidak pernah dilemparkan ke badan pernyataan percobaan yang sesuai).

  • Jika Anda memanggil metode yang secara harfiah tidak pernah dapat membuang pengecualian yang dinyatakannya, maka Anda tidak boleh memasukkan klausa pelemparan. Sebagai contoh: String baru (byteArr, "UTF-8") melempar UnsupportedEncodingException, tetapi UTF-8 dijamin oleh spesifikasi Java untuk selalu hadir. Di sini, deklarasi melempar adalah gangguan dan solusi apa pun untuk membungkamnya dengan platplate minimal dipersilahkan.

  • Jika Anda membenci pengecualian yang dicentang dan merasa mereka tidak boleh ditambahkan ke bahasa Jawa untuk memulai (semakin banyak orang berpikir seperti ini, dan saya BUKAN salah satu dari mereka), maka jangan tambahkan pengecualian yang dicentang ke lemparan klausa metode yang berisi aliran. Pengecualian yang dicentang akan, berperilaku, seperti pengecualian yang tidak dicentang.

  • Jika Anda menerapkan antarmuka yang ketat di mana Anda tidak memiliki opsi untuk menambahkan deklarasi lemparan, namun melempar pengecualian sepenuhnya sesuai, maka membungkus pengecualian hanya untuk mendapatkan hak istimewa melemparkannya menghasilkan stacktrace dengan pengecualian palsu yang berkontribusi tidak ada informasi tentang apa yang sebenarnya salah. Contoh yang baik adalah Runnable.run (), yang tidak membuang pengecualian yang dicentang. Dalam hal ini, Anda dapat memutuskan untuk tidak menambahkan pengecualian yang dicentang ke klausa melempar dari metode yang berisi aliran.

  • Dalam kasus apa pun, jika Anda memutuskan untuk TIDAK menambahkan (atau lupa menambahkan) pengecualian yang dicentang untuk klausa melempar dari metode yang berisi aliran, perhatikan 2 konsekuensi dari melemparkan pengecualian yang DIPERIKSA:

    1. Kode panggilan tidak akan dapat menangkapnya dengan nama (jika Anda mencoba, kompiler akan mengatakan: Pengecualian tidak pernah dilemparkan ke badan pernyataan percobaan yang sesuai). Ini akan menggelembung dan mungkin ditangkap dalam loop program utama oleh "catch Exception" atau "catch Throwable", yang mungkin merupakan apa yang Anda inginkan.

    2. Itu melanggar prinsip paling tidak mengejutkan: itu tidak akan lagi cukup untuk menangkap RuntimeException untuk dapat menjamin menangkap semua pengecualian yang mungkin. Untuk alasan ini, saya percaya ini tidak boleh dilakukan dalam kode kerangka kerja, tetapi hanya dalam kode bisnis yang sepenuhnya Anda kontrol.

Referensi:

CATATAN: Jika Anda memutuskan untuk menggunakan teknik ini, Anda dapat menyalin LambdaExceptionUtilkelas helper dari StackOverflow: https://stackoverflow.com/questions/27644361/how-can-i-throw-checked-exceptions-from-inside-java-8 -streams . Ini memberi Anda implementasi lengkap (Fungsi, Konsumen, Pemasok ...), dengan contoh.

MarcG
sumber
6

Dalam contoh ini, bisakah itu benar-benar gagal? Jangan berpikir begitu, tapi mungkin kasus Anda spesial. Jika itu benar-benar "tidak bisa gagal", dan itu hanya kompiler yang menjengkelkan, saya suka membungkus pengecualian dan melemparkan Errordengan komentar "tidak dapat terjadi". Menjadikan semuanya sangat jelas untuk pemeliharaan. Kalau tidak, mereka akan bertanya-tanya "bagaimana ini bisa terjadi" dan "siapa yang menangani ini?"

Ini dalam praktik yang kontroversial jadi YMMV. Saya mungkin akan mendapatkan beberapa downvotes.

pengguna949300
sumber
3
+1 karena Anda akan melempar Kesalahan. Berapa kali saya berakhir setelah berjam-jam melakukan debugging di blok tangkap yang hanya berisi satu komentar baris "tidak dapat terjadi" ...
Axel
3
-1 karena Anda akan melempar Kesalahan. Kesalahan mengindikasikan bahwa ada sesuatu yang salah dalam JVM dan level atas dapat menanganinya. Jika Anda memilih seperti itu, Anda harus melempar RuntimeException. Solusi lain yang mungkin adalah asserts (need -ea flag enabled) atau logging.
duros
3
@duros: Bergantung pada mengapa pengecualian "tidak dapat terjadi", fakta bahwa itu dilempar dapat mengindikasikan bahwa ada sesuatu yang sangat salah. Misalkan, misalnya, jika seseorang memanggil clonejenis Fooyang disegel yang diketahui mendukungnya, dan ia melempar CloneNotSupportedException. Bagaimana itu bisa terjadi kecuali kode terhubung dengan beberapa jenis yang tidak terduga lainnya Foo? Dan jika itu terjadi, adakah yang bisa dipercaya?
supercat
1
@supercat contoh yang sangat baik. Orang lain melempar pengecualian dari Penyandian String yang hilang atau MessageDigests Jika UTF-8 atau SHA hilang runtime Anda kemungkinan rusak.
user949300
1
@duros Itu kesalahpahaman yang lengkap. An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch.(dikutip dari Javadoc of Error) Ini adalah situasi yang persis seperti itu, karena itu jauh dari tidak memadai, melempar di Errorsini adalah pilihan yang paling tepat.
biziclop
1

versi lain dari kode ini di mana pengecualian yang diperiksa hanya ditunda:

public class Cocoon {
         static <T extends Throwable> T forgetThrowsClause(Throwable t) throws T{
            throw (T) t;
        }

        public static <X, T extends Throwable> Consumer<X> consumer(PeskyConsumer<X,T> touchyConsumer) throws T {
            return new Consumer<X>() {
                @Override
                public void accept(X t) {
                    try {
                        touchyConsumer.accept(t);
                    } catch (Throwable exc) {
                        Cocoon.<RuntimeException>forgetThrowsClause(exc) ;
                    }

                }
            } ;
        }
// and so on for Function, and other codes from java.util.function
}

keajaibannya adalah jika Anda menelepon:

 myArrayList.forEach(Cocoon.consumer(MyClass::methodThatThrowsException)) ;

maka kode Anda wajib menangkap pengecualian

beruang java
sumber
Apa yang terjadi jika ini dijalankan pada aliran paralel di mana elemen dikonsumsi di utas yang berbeda?
pangkas
0

sneakyThrow itu keren! Aku benar-benar merasakan sakitmu.

Jika Anda harus tinggal di Jawa ...

Paguro memiliki antarmuka fungsional yang membungkus pengecualian yang diperiksa sehingga Anda tidak perlu memikirkan hal ini lagi. Ini mencakup koleksi yang tidak dapat diubah dan transformasi fungsional di sepanjang garis transduser Clojure (atau Java 8 Streams) yang mengambil antarmuka fungsional dan membungkus pengecualian yang diperiksa.

Ada juga VAVr dan The Eclipse Collections untuk dicoba.

Kalau tidak, gunakan Kotlin

Kotlin kompatibel 2 arah dengan Java, tidak memiliki pengecualian yang diperiksa, tidak ada primitif (well, bukan programmer yang harus memikirkan), sistem tipe yang lebih baik, mengasumsikan imutabilitas, dll. Meskipun saya membuat perpustakaan Paguro di atas, saya ' m mengkonversi semua kode Java yang saya bisa ke Kotlin. Apa pun yang saya lakukan di Jawa sekarang saya lebih suka melakukannya di Kotlin.

GlenPeterson
sumber
Seseorang yang memilih ini, itu baik-baik saja, tapi saya tidak akan belajar apa-apa kecuali Anda memberi tahu saya mengapa Anda memilihnya.
GlenPeterson
1
+1 untuk pustaka Anda di github, Anda dapat memeriksa juga eclipse.org/collections atau project vavr yang menyediakan koleksi fungsional dan tidak berubah. Apa pendapat Anda tentang ini?
firephil
-1

Pengecualian yang diperiksa sangat berguna dan penanganannya tergantung pada jenis produk yang Anda kode bagian dari:

  • Perpustakaan
  • aplikasi desktop
  • server berjalan dalam beberapa kotak
  • latihan akademis

Biasanya pustaka tidak boleh menangani pengecualian yang dicentang, tetapi menyatakan sebagai dilemparkan oleh API publik. Kasus yang jarang terjadi ketika mereka dapat dibungkus dengan RuntimeExcepton biasa adalah, misalnya, membuat di dalam kode perpustakaan beberapa dokumen xml pribadi dan menguraikannya, membuat beberapa file sementara dan kemudian membacanya.

Untuk aplikasi desktop, Anda harus memulai pengembangan produk dengan membuat kerangka kerja penanganan pengecualian Anda sendiri. Menggunakan kerangka kerja Anda sendiri biasanya Anda akan membungkus pengecualian yang diperiksa menjadi beberapa dari dua hingga empat pembungkus (subkelas kustom RuntimeExcepion) dan menanganinya dengan satu Pengecualian.

Untuk server biasanya kode Anda akan berjalan di dalam kerangka kerja tertentu. Jika Anda tidak menggunakan (server kosong) buat kerangka kerja Anda sendiri yang serupa (berdasarkan Runtimes) yang dijelaskan di atas.

Untuk latihan akademis, Anda selalu dapat membungkusnya langsung ke dalam RuntimeExcepton. Seseorang dapat membuat kerangka kerja kecil juga.

Razmik
sumber
-1

Anda mungkin ingin melihat proyek ini ; Saya membuatnya secara khusus karena masalah pengecualian diperiksa dan antarmuka fungsional.

Dengannya Anda dapat melakukan ini alih-alih menulis kode khusus:

// I don't know the type of f, so it's Foo...
final ThrowingConsumer<Foo> consumer = f -> System.out.println(f.get(p));

Karena semua antarmuka Throwing * memperluas rekan-rekan non Throwing mereka, Anda kemudian dapat menggunakannya secara langsung dalam aliran:

Arrays.asList(p.getClass().getFields()).forEach(consumer);

Atau Anda bisa:

import static com.github.fge.lambdas.consumers.Consumers.wrap;

Arrays.asList(p.getClass().getFields())
    .forEach(wrap(f -> System.out.println(f.get(p)));

Dan hal lainnya.

Fge
sumber
Lebih baik lagifields.forEach((ThrowingConsumer<Field>) f -> out.println(f.get(o)))
user2418306
Akankah para downvoter menjelaskan?
fge