Pengecualian dalam Java 8 Lambda Expressions

1. Gambaran keseluruhan

Di Java 8, Lambda Expressions mulai memfasilitasi pengaturcaraan fungsional dengan memberikan cara yang ringkas untuk menyatakan perilaku. Walau bagaimanapun, Interface Fungsi yang disediakan oleh JDK tidak menangani pengecualian dengan sangat baik - dan kodnya menjadi verbose dan membebankan ketika mengendalikannya.

Dalam artikel ini, kami akan meneroka beberapa cara untuk menangani pengecualian semasa menulis ungkapan lambda.

2. Mengendalikan Pengecualian yang Tidak Diperiksa

Pertama, mari kita fahami masalah dengan contoh.

Kami mempunyai Senarai dan kami mahu membahagikan pemalar, katakan 50 dengan setiap elemen senarai ini dan mencetak hasilnya:

List integers = Arrays.asList(3, 9, 7, 6, 10, 20); integers.forEach(i -> System.out.println(50 / i));

Ungkapan ini berfungsi tetapi ada satu masalah. Sekiranya salah satu elemen dalam senarai adalah 0 , maka kita mendapat ArithmeticException: / dengan sifar . Mari perbaiki dengan menggunakan blok cubaan tangkapan tradisional sehingga kami mencatat pengecualian dan meneruskan pelaksanaan untuk elemen seterusnya:

List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(i -> { try { System.out.println(50 / i); } catch (ArithmeticException e) { System.err.println( "Arithmetic Exception occured : " + e.getMessage()); } });

Penggunaan catch-catch menyelesaikan masalah, tetapi kesimpulan Lambda Expression hilang dan ia bukan lagi fungsi kecil sebagaimana mestinya.

Untuk mengatasi masalah ini, kita boleh menulis pembungkus lambda untuk fungsi lambda . Mari lihat kod untuk melihat cara kerjanya:

static Consumer lambdaWrapper(Consumer consumer) { return i -> { try { consumer.accept(i); } catch (ArithmeticException e) { System.err.println( "Arithmetic Exception occured : " + e.getMessage()); } }; }
List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(lambdaWrapper(i -> System.out.println(50 / i)));

Pada mulanya, kami menulis kaedah pembungkus yang akan bertanggung jawab menangani pengecualian dan kemudian meneruskan ekspresi lambda sebagai parameter untuk kaedah ini.

Kaedah pembungkus berfungsi seperti yang dijangkakan tetapi, anda mungkin berpendapat bahawa pada dasarnya menghilangkan blok cubaan dari ungkapan lambda dan memindahkannya ke kaedah lain dan ia tidak mengurangkan bilangan baris kod yang sebenarnya ditulis.

Ini berlaku dalam kes ini di mana pembungkus khusus untuk kes penggunaan tertentu tetapi kita dapat menggunakan generik untuk memperbaiki kaedah ini dan menggunakannya untuk pelbagai senario lain:

static  Consumer consumerWrapper(Consumer consumer, Class clazz) { return i -> { try { consumer.accept(i); } catch (Exception ex) { try { E exCast = clazz.cast(ex); System.err.println( "Exception occured : " + exCast.getMessage()); } catch (ClassCastException ccEx) { throw ex; } } }; }
List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach( consumerWrapper( i -> System.out.println(50 / i), ArithmeticException.class));

Seperti yang dapat kita lihat, iterasi kaedah pembungkus kami mengambil dua argumen, ekspresi lambda dan jenis Pengecualian yang harus ditangkap. Pembungkus lambda ini mampu menangani semua jenis data, bukan hanya Integers , dan menangkap pengecualian jenis tertentu dan bukan Pengecualian superclass .

Juga, perhatikan bahawa kami telah menukar nama kaedah dari lambdaWrapper menjadi consumerWrapper . Ini kerana kaedah ini hanya menangani ungkapan lambda untuk Interface Fungsional jenis Pengguna . Kita boleh menulis kaedah pembungkus yang serupa untuk Antaramuka Fungsional lain seperti Fungsi , BiFungsi , BiConsumer dan sebagainya.

3. Mengendalikan Pengecualian yang Diperiksa

Mari ubah contoh dari bahagian sebelumnya dan bukannya mencetak ke konsol, mari tulis ke fail.

static void writeToFile(Integer integer) throws IOException { // logic to write to file which throws IOException }

Perhatikan bahawa kaedah di atas boleh membuang IOException.

List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(i -> writeToFile(i));

Pada penyusunan, kami mendapat ralat:

java.lang.Error: Unresolved compilation problem: Unhandled exception type IOException

Oleh kerana IOException adalah pengecualian yang diperiksa, kita mesti mengatasinya secara eksplisit . Kami mempunyai dua pilihan.

Pertama, kita mungkin membuang pengecualian di luar kaedah kita dan menjaganya di tempat lain.

Sebagai alternatif, kita dapat mengatasinya dalam kaedah yang menggunakan ungkapan lambda.

Mari kita terokai kedua-dua pilihan.

3.1. Melontarkan Pengecualian Tercatat dari Lambda Expressions

Mari lihat apa yang berlaku apabila kita menyatakan IOException mengenai kaedah utama :

public static void main(String[] args) throws IOException { List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(i -> writeToFile(i)); }

Namun, kami mendapat kesalahan yang sama dengan IOException yang tidak ditangani semasa penyusunan .

java.lang.Error: Unresolved compilation problem: Unhandled exception type IOException

Ini kerana ungkapan lambda serupa dengan Kelas Dalaman Tanpa Nama.

Dalam kes kami, kaedah writeToFile adalah pelaksanaan antara muka fungsi Pengguna .

Mari kita lihat definisi Pengguna :

@FunctionalInterface public interface Consumer { void accept(T t); }

Seperti yang kita lihat, kaedah penerimaan tidak menyatakan pengecualian yang diperiksa. Inilah sebabnya mengapa writeToFile tidak dibenarkan membuang IOException.

Cara yang paling mudah adalah menggunakan blok cubaan menangkap , membungkus pengecualian yang diperiksa menjadi pengecualian yang tidak dicentang dan mengubahnya semula:

List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(i -> { try { writeToFile(i); } catch (IOException e) { throw new RuntimeException(e); } }); 

Ini mendapat kod untuk disusun dan dijalankan. Walau bagaimanapun, pendekatan ini memperkenalkan isu yang sama yang telah kita bincangkan di bahagian sebelumnya - ini adalah verbose dan tidak praktikal.

Kita boleh menjadi lebih baik daripada itu.

Mari buat antara muka fungsional tersuai dengan kaedah penerimaan tunggal yang memberikan pengecualian.

@FunctionalInterface public interface ThrowingConsumer { void accept(T t) throws E; }

Dan sekarang, mari kita laksanakan kaedah pembungkus yang dapat mengubah semula pengecualian:

static  Consumer throwingConsumerWrapper( ThrowingConsumer throwingConsumer) { return i -> { try { throwingConsumer.accept(i); } catch (Exception ex) { throw new RuntimeException(ex); } }; }

Akhirnya, kami dapat mempermudah cara kami menggunakan kaedah writeToFile :

List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(throwingConsumerWrapper(i -> writeToFile(i)));

Ini masih merupakan jalan penyelesaian, tetapi hasil akhirnya kelihatan cukup bersih dan pastinya lebih mudah dijaga .

Kedua-duanya, ThrowingConsumer dan throwingConsumerWrapper adalah generik dan boleh dengan mudah digunakan semula di tempat yang berbeza permohonan kami.

3.2. Handling a Checked Exception in Lambda Expression

In this final section, we'll modify the wrapper to handle checked exceptions.

Since our ThrowingConsumer interface uses generics, we can easily handle any specific exception.

static  Consumer handlingConsumerWrapper( ThrowingConsumer throwingConsumer, Class exceptionClass) { return i -> { try { throwingConsumer.accept(i); } catch (Exception ex) { try { E exCast = exceptionClass.cast(ex); System.err.println( "Exception occured : " + exCast.getMessage()); } catch (ClassCastException ccEx) { throw new RuntimeException(ex); } } }; }

Let's see how to use it in practice:

List integers = Arrays.asList(3, 9, 7, 0, 10, 20); integers.forEach(handlingConsumerWrapper( i -> writeToFile(i), IOException.class));

Note, that the above code handles only IOException, whereas any other kind of exception is rethrown as a RuntimeException .

4. Conclusion

In this article, we showed how to handle a specific exception in lambda expression without losing the conciseness with the help of wrapper methods. We also learned how to write throwing alternatives for the Functional Interfaces present in JDK to either throw or handle a checked exception.

Another way would be to explore the sneaky-throws hack.

Kod sumber lengkap antara muka Fungsional dan kaedah pembungkus boleh dimuat turun dari sini dan kelas ujian dari sini, di Github.

Sekiranya anda mencari penyelesaian berfungsi di luar kotak, projek ThrowingFunction patut disemak.