Soluzione alternativa per Java verificate eccezioni


49

Apprezzo molto le nuove funzionalità di Java 8 su lambdas e le interfacce dei metodi predefiniti. Tuttavia, mi annoio ancora con le eccezioni verificate. Ad esempio, se voglio solo elencare tutti i campi visibili di un oggetto, vorrei semplicemente scrivere questo:

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

Tuttavia, poiché il getmetodo potrebbe generare un'eccezione controllata, che non è d'accordo con il Consumercontratto di interfaccia, allora devo prendere quell'eccezione e scrivere il seguente codice:

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

Tuttavia, nella maggior parte dei casi, voglio solo generare l'eccezione come a RuntimeExceptione lasciare che il programma gestisca o meno l'eccezione senza errori di compilazione.

Quindi, vorrei avere la tua opinione sulla mia controversa soluzione alternativa per il fastidio delle eccezioni verificate. A tal fine, ho creato un'interfaccia ausiliaria ConsumerCheckException<T>e una funzione di utilità rethrow( aggiornata secondo il suggerimento del commento di Doval ) come segue:

  @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/javaposse@googlegroups.com/msg05984.html
       */
      public static <T extends Throwable> T sneakyThrow(Throwable t) {
          throw (T) t;
      }
  }

E ora posso solo scrivere:

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

Non sono sicuro che questo sia il modo di dire migliore per aggirare le eccezioni verificate, ma come ho spiegato, vorrei avere un modo più conveniente di ottenere il mio primo esempio senza occuparmi delle eccezioni verificate e questo è il modo più semplice che ho trovato per farlo.



2
Oltre al link di Robert, dai un'occhiata anche a Eccezioni verificate di nascosto . Se lo desideri, puoi usare sneakyThrowinside of rethrowper lanciare l'eccezione originale e controllata invece di racchiuderla in a RuntimeException. In alternativa puoi usare l' @SneakyThrowsannotazione di Project Lombok che fa la stessa cosa.
Doval,

1
Si noti inoltre che Consumers in forEachpuò essere eseguito in modo parallelo quando si usano Streams in parallelo . Un lanciatore generato dal withing del consumatore si propaga quindi al thread chiamante, che 1) non bloccherà gli altri consumatori che gestiscono contemporaneamente, il che può essere o non essere appropriato, e 2) se più di uno dei consumatori lancia qualcosa, solo uno dei lanci sarà visto dal thread chiamante.
Joonas Pulakka,


2
Le lambda sono così brutte.
Tulains Córdova,

Risposte:


16

Vantaggi, svantaggi e limiti della tua tecnica:

  • Se il codice chiamante deve gestire l'eccezione selezionata, è NECESSARIO aggiungerlo alla clausola dei tiri del metodo che contiene il flusso. Il compilatore non ti costringerà più ad aggiungerlo, quindi è più facile dimenticarlo. Per esempio:

    public void test(Object p) throws IllegalAccessException {
        Arrays.asList(p.getClass().getFields()).forEach(rethrow(f -> System.out.println(f.get(p))));
    }
    
  • Se il codice chiamante gestisce già l'eccezione selezionata, il compilatore ricorderà di aggiungere la clausola plausibili alla dichiarazione del metodo che contiene lo stream (in caso contrario, verrà indicato: l'eccezione non viene mai generata nel corpo dell'istruzione try corrispondente) .

  • In ogni caso, non sarai in grado di circondare il flusso stesso per catturare l'eccezione selezionata INSIDE il metodo che contiene il flusso (se provi, il compilatore dirà: L'eccezione non viene mai generata nel corpo dell'istruzione try corrispondente).

  • Se stai chiamando un metodo che letteralmente non può mai gettare l'eccezione che dichiara, allora non dovresti includere la clausola dei tiri. Ad esempio: new String (byteArr, "UTF-8") genera UnsupportedEncodingException, ma UTF-8 è garantito dalle specifiche Java sempre presenti. Qui, la dichiarazione di lancio è una seccatura e qualsiasi soluzione per silenziarla con una piastra di cottura minima è benvenuta.

  • Se odi le eccezioni verificate e ritieni che non dovrebbero mai essere aggiunte al linguaggio Java (un numero crescente di persone la pensa in questo modo, e io NON sono una di queste), non aggiungere l'eccezione controllata ai tiri clausola del metodo che contiene il flusso. L'eccezione selezionata si comporterà quindi come un'eccezione NON selezionata.

  • Se stai implementando un'interfaccia rigorosa in cui non hai la possibilità di aggiungere una dichiarazione di lancio, e tuttavia lanciare un'eccezione è del tutto appropriata, quindi avvolgere un'eccezione solo per ottenere il privilegio di lanciarla si traduce in uno stack stack con eccezioni spurie che non fornire informazioni su ciò che è effettivamente andato storto. Un buon esempio è Runnable.run (), che non genera alcuna eccezione verificata. In questo caso, è possibile decidere di non aggiungere l'eccezione selezionata alla clausola throws del metodo che contiene il flusso.

  • In ogni caso, se decidi di NON aggiungere (o dimenticare di aggiungere) l'eccezione verificata alla clausola di tiri del metodo che contiene il flusso, tieni presente queste 2 conseguenze del lancio di eccezioni CHECKED:

    1. Il codice chiamante non sarà in grado di prenderlo per nome (se si tenta, il compilatore dirà: l'eccezione non viene mai generata nel corpo dell'istruzione try corrispondente). Bolle e probabilmente verrà catturato nel ciclo principale del programma da qualche "catch Exception" o "catch Throwable", che può essere comunque quello che vuoi.

    2. Infrange il principio della minima sorpresa: non sarà più sufficiente catturare RuntimeException per essere in grado di garantire la cattura di tutte le possibili eccezioni. Per questo motivo, credo che ciò non dovrebbe essere fatto nel codice del framework, ma solo nel codice aziendale che controlli completamente.

Riferimenti:

NOTA: se si decide di utilizzare questa tecnica, è possibile copiare la LambdaExceptionUtilclasse di supporto da StackOverflow: https://stackoverflow.com/questions/27644361/how-can-i-throw-checked-exceptions-from-inside-java-8 -stream . Ti dà l'implementazione completa (Funzione, Consumatore, Fornitore ...), con esempi.


6

In questo esempio, può mai davvero fallire? Non credo, ma forse il tuo caso è speciale. Se davvero "non può fallire", ed è solo una cosa fastidiosa del compilatore, mi piace racchiudere l'eccezione e lanciare un Errorcommento con "non può accadere". Rende le cose molto chiare per la manutenzione. Altrimenti si chiederanno "come può succedere" e "chi diavolo lo gestisce?"

Questo in una pratica controversa quindi YMMV. Probabilmente otterrò alcuni voti negativi.


3
+1 perché genererai un errore. Quante volte sono finito dopo ore di debug in un blocco catch contenente solo un commento a riga singola "impossibile" ...
Axel,

3
-1 perché genererai un errore. Gli errori indicano che qualcosa non va in JVM e i livelli superiori possono gestirli di conseguenza. Se scegli in questo modo, dovresti lanciare RuntimeException. Altre possibili soluzioni alternative sono assert (necessità di -ea flag abilitato) o registrazione.
duros,

3
@duros: a seconda del motivo per cui l'eccezione "non può accadere", il fatto che venga lanciata può indicare che qualcosa è gravemente sbagliato. Supponiamo, ad esempio, se uno chiama cloneun tipo sigillato Fooche è noto per supportarlo e lo lancia CloneNotSupportedException. Come è potuto accadere a meno che il codice non fosse collegato a qualche altro tipo inaspettato Foo? E se ciò accade, ci si può fidare di qualcosa?
supercat

1
@supercat eccellente esempio. Altri generano eccezioni dalla mancata codifica di stringhe o da MessageDigests Se UTF-8 o SHA mancano, il tempo di esecuzione è probabilmente danneggiato.
user949300,

1
@duros È un'idea sbagliata completa. An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch.(citato dal Javadoc di Error) Questo è esattamente quel tipo di situazione, quindi lungi dall'essere inadeguata, lanciare un Errorqui è l'opzione più appropriata.
biziclop,

1

un'altra versione di questo codice in cui l'eccezione selezionata è appena ritardata:

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
}

la magia è che se chiami:

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

quindi il tuo codice sarà obbligato a cogliere l'eccezione


Cosa succede se questo viene eseguito su un flusso parallelo in cui gli elementi vengono consumati in thread diversi?
prunge il

0

subdolo: è fantastico! Sono assolutamente sento il vostro dolore.

Se devi rimanere in Java ...

Paguro ha interfacce funzionali che racchiudono le eccezioni verificate, quindi non devi più pensarci. Include raccolte immutabili e trasformazioni funzionali lungo le linee dei trasduttori Clojure (o Java 8 Streams) che accettano le interfacce funzionali e avvolgono le eccezioni verificate.

C'è anche VAVr e The Eclipse Collections da provare.

Altrimenti, usa Kotlin

Kotlin è compatibile in modo bidirezionale con Java, non ha eccezioni verificate, senza primitive (beh, non è il programmatore a cui pensare), un sistema di tipo migliore, assume l'immutabilità, ecc. Anche se ho curato la libreria di Paguro sopra, io ' sto convertendo tutto il codice Java che posso in Kotlin. Qualunque cosa facessi in Java ora preferisco fare in Kotlin.


Qualcuno ha votato in negativo, il che va bene, ma non imparerò nulla a meno che tu non mi dica perché lo hai votato in negativo.
GlenPeterson,

1
+1 per la tua libreria in github, puoi anche consultare eclipse.org/collections o project vavr che forniscono raccolte funzionali e immutabili. Cosa ne pensi di questi?
firephil,

-1

Le eccezioni verificate sono molto utili e la loro gestione dipende dal tipo di prodotto di cui il codice fa parte:

  • una biblioteca
  • un'applicazione desktop
  • un server in esecuzione all'interno di una casella
  • un esercizio accademico

Di solito una libreria non deve gestire le eccezioni verificate, ma deve essere dichiarata come lanciata dall'API pubblica. Il raro caso in cui potrebbero essere racchiusi in un semplice RuntimeExcepton è, ad esempio, creare all'interno del codice della libreria un documento xml privato e analizzarlo, creare un file temporaneo e quindi leggerlo.

Per le applicazioni desktop è necessario avviare lo sviluppo del prodotto creando il proprio framework di gestione delle eccezioni. Usando il tuo framework di solito avvolgerai un'eccezione controllata in un paio di 2-4 wrapper (le tue sottoclassi personalizzate di RuntimeExcepion) e le gestirai da un unico gestore delle eccezioni.

Per i server di solito il tuo codice verrà eseguito all'interno di alcuni framework. Se non usi nessuno (un server nudo) crea il tuo framework simile (basato su Runtime) descritto sopra.

Per gli esercizi accademici puoi sempre avvolgerli direttamente in RuntimeExcepton. Qualcuno può creare anche un piccolo framework.


-1

Potresti voler dare un'occhiata a questo progetto ; L'ho creato appositamente a causa del problema delle eccezioni controllate e delle interfacce funzionali.

Con esso puoi farlo invece di scrivere il tuo codice personalizzato:

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

Poiché tutte quelle interfacce Throwing * estendono le loro controparti non Throwing, puoi quindi usarle direttamente nel flusso:

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

Oppure puoi semplicemente:

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

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

E altre cose.


Meglio ancorafields.forEach((ThrowingConsumer<Field>) f -> out.println(f.get(o)))
user2418306

I downvoter potrebbero spiegare?
fge,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.