it-swarm.com.de

Java 8: Obligatorisch geprüfte Ausnahmebehandlung in Lambda-Ausdrücken. Warum obligatorisch, nicht optional?

Ich spiele mit den neuen Lambda-Funktionen in Java 8 und habe festgestellt, dass die von Java 8 angebotenen Vorgehensweisen wirklich nützlich sind. Ich frage mich jedoch, gibt es eine gute Art und Weise, um das folgende Szenario zu umgehen. Angenommen, Sie verfügen über einen Objektpool-Wrapper, für den eine Art Factory erforderlich ist, um den Objektpool aufzufüllen, beispielsweise (mithilfe von Java.lang.functions.Factory):

public class JdbcConnectionPool extends ObjectPool<Connection> {

    public ConnectionPool(int maxConnections, String url) {
        super(new Factory<Connection>() {
            @Override
            public Connection make() {
                try {
                    return DriverManager.getConnection(url);
                } catch ( SQLException ex ) {
                    throw new RuntimeException(ex);
                }
            }
        }, maxConnections);
    }

}

Nach der Umwandlung der funktionalen Schnittstelle in einen Lambda-Ausdruck wird der obige Code folgendermaßen:

public class JdbcConnectionPool extends ObjectPool<Connection> {

    public ConnectionPool(int maxConnections, String url) {
        super(() -> {
            try {
                return DriverManager.getConnection(url);
            } catch ( SQLException ex ) {
                throw new RuntimeException(ex);
            }
        }, maxConnections);
    }

}

Nicht ganz so schlimm, aber die geprüfte Ausnahme Java.sql.SQLException erfordert einen try/catch-Block innerhalb des Lambda. In meinem Unternehmen verwenden wir seit langem zwei Schnittstellen:

  • IOut<T>, der äquivalent zu Java.lang.functions.Factory ist;
  • und eine spezielle Schnittstelle für die Fälle, in denen normalerweise die Weitergabe von Ausnahmen erforderlich ist: interface IUnsafeOut<T, E extends Throwable> { T out() throws E; }.

Sowohl IOut<T> als auch IUnsafeOut<T> sollten während der Migration auf Java 8 entfernt werden, es gibt jedoch keine genaue Übereinstimmung für IUnsafeOut<T, E>. Wenn die Lambda-Ausdrücke mit geprüften Ausnahmen umgehen könnten, als wären sie nicht markiert, könnte es möglich sein, im Konstruktor wie folgt zu verwenden:

super(() -> DriverManager.getConnection(url), maxConnections);

Das sieht viel sauberer aus. Ich sehe, dass ich die Superklasse ObjectPool umschreiben kann, um unseren IUnsafeOut<T> zu akzeptieren, aber soweit ich weiß, ist Java 8 noch nicht fertig, daher könnte es einige Änderungen geben:

  • etwas Ähnliches wie IUnsafeOut<T, E> implementieren? (Um ehrlich zu sein, halte ich das für schmutzig - das Subjekt muss auswählen, was akzeptiert werden soll: entweder Factory oder "unsichere Factory", die keine kompatiblen Methodensignaturen haben kann.)
  • ignorieren von geprüften Ausnahmen in Lambdas, also keine Notwendigkeit für IUnsafeOut<T, E>-Surrogate? (Warum nicht? Zum Beispiel eine weitere wichtige Änderung: OpenJDK, das ich verwende, javac erfordert nicht, dass Variablen und Parameter als final deklariert sind, um in einer anonymen Klasse [funktionales Interface] oder Lambda-Ausdruck erfasst zu werden.

Die Frage ist also im Allgemeinen: Gibt es eine Möglichkeit, geprüfte Ausnahmen in Lambdas zu umgehen, oder ist dies in der Zukunft so lange geplant, bis Java 8 endgültig veröffentlicht wird?


Update 1

Hm-m-m, soweit ich weiß, was wir derzeit haben, scheint es momentan keinen Weg zu geben, obwohl der Artikel aus dem Jahr 2010 datiert ist: Brian Goetz erklärt die Transparenz von Ausnahmen in Java . Wenn sich an Java 8 nichts geändert hat, könnte dies als Antwort betrachtet werden. Brian sagt auch, dass interface ExceptionalCallable<V, E extends Exception> (was ich als IUnsafeOut<T, E extends Throwable> aus unserem Code-Erbe erwähnt habe) ziemlich nutzlos ist, und ich stimme ihm zu.

Vermisse ich noch etwas?

68

Ich bin nicht sicher, ob ich Ihre Frage wirklich beantworte, aber konnten Sie so etwas nicht einfach verwenden?

public final class SupplierUtils {
    private SupplierUtils() {
    }

    public static <T> Supplier<T> wrap(Callable<T> callable) {
        return () -> {
            try {
                return callable.call();
            }
            catch (RuntimeException e) {
                throw e;
            }
            catch (Exception e) {
                throw new RuntimeException(e);
            }
        };
    }
}

public class JdbcConnectionPool extends ObjectPool<Connection> {

    public JdbcConnectionPool(int maxConnections, String url) {
        super(SupplierUtils.wrap(() -> DriverManager.getConnection(url)), maxConnections);
    }
}
42
JB Nizet

In der Lambda-Mailingliste wurde dies ausführlich diskutiert . Wie Sie sehen können, schlug Brian Goetz vor, als Alternative dazu Ihren eigenen Kombinator zu schreiben:

Oder Sie könnten Ihren eigenen trivialen Kombinator schreiben:

static<T> Supplier<T> exceptionWrappingSupplier(Supplier<T> b) {
     return e -> {
         try { b.accept(e); }
         catch (Exception e) { throw new RuntimeException(e); }
     };
}

Sie können es einmal schreiben, in weniger als der Zeit, die Sie zum Schreiben Ihres ursprüngliche E-Mail. Und in ähnlicher Weise einmal für jede Art von SAM, die Sie verwenden.

Ich würde es lieber als "Glas 99% voll" betrachten, anstatt das Alternative. Nicht alle Probleme erfordern neue Sprachfunktionen wie Lösungen. (Ganz zu schweigen davon, dass neue Sprachfunktionen immer __. Neue Probleme verursachen.)

Zu dieser Zeit hieß das Consumer-Interface Block.

Ich denke, das entspricht der Antwort von JB Nizet .

Später erklärt Brian warum dies wurde so entworfen (der Grund des Problems)

Ja, Sie müssen Ihre eigenen außergewöhnlichen SAMs bereitstellen. Aber dann Lambda Konvertierung würde gut mit ihnen funktionieren.

Das EG diskutierte dazu zusätzliche Sprach- und Bibliotheksunterstützung Problem und am Ende das Gefühl, dass dies ein schlechter Kosten/Nutzen war Abtausch.

Bibliotheksbasierte Lösungen verursachen eine zweifache Explosion bei SAM-Typen (außergewöhnlich Vs not), die schlecht mit vorhandenen kombinatorischen Explosionen interagieren für primitive Spezialisierung.

Die verfügbaren sprachbasierten Lösungen waren Verlierer eines Kompromiss zwischen Komplexität und Wert. Zwar gibt es eine Alternative Lösungen, die wir weiter erforschen werden - aber eindeutig nicht für 8 und wahrscheinlich auch nicht für 9.

In der Zwischenzeit haben Sie die Werkzeuge, um das zu tun, was Sie wollen. Ich verstehe das Sie bevorzugen, dass wir die letzte Meile für Sie bereitstellen (und zweitens ist Ihre -Anfrage wirklich eine dünn verschleierte Anfrage nach "Warum nicht einfach Aufgegebene Ausnahmen bereits aufgeben"), aber ich denke, das aktuell Zustand lässt Sie Ihre Arbeit erledigen.

34
Edwin Dalorzo

September 2015:

Sie können dazu ET verwenden. ET ist eine kleine Java 8-Bibliothek für die Konvertierung/Übersetzung von Ausnahmen.

Mit ET können Sie schreiben:

super(() -> et.withReturningTranslation(() -> DriverManager.getConnection(url)), maxConnections);

Mehrzeilige Version:

super(() -> {
  return et.withReturningTranslation(() -> DriverManager.getConnection(url));
}, maxConnections);

Alles, was Sie zuvor tun müssen, ist das Erstellen einer neuen ExceptionTranslator-Instanz:

ExceptionTranslator et = ET.newConfiguration().done();

Diese Instanz ist threadsicher und kann von mehreren Komponenten gemeinsam genutzt werden. Sie können auch spezifischere Ausnahmekonvertierungsregeln (z. B. FooCheckedException -> BarRuntimeException) konfigurieren, wenn Sie möchten. Wenn keine anderen Regeln verfügbar sind, werden markierte Ausnahmen automatisch in RuntimeException konvertiert.

(Haftungsausschluss: Ich bin der Autor von ET)

5
micha

Haben Sie die Verwendung einer Wrapper-Klasse für RuntimeException (nicht geprüft) in Betracht gezogen, um die ursprüngliche Ausnahme aus dem Lambda-Ausdruck zu schmuggeln, und dann die umschlossene Ausnahme back in ihre ursprünglich geprüfte Ausnahme umwandeln?

class WrappedSqlException extends RuntimeException {
    static final long serialVersionUID = 20130808044800000L;
    public WrappedSqlException(SQLException cause) { super(cause); }
    public SQLException getSqlException() { return (SQLException) getCause(); }
}

public ConnectionPool(int maxConnections, String url) throws SQLException {
    try {
        super(() -> {
            try {
                return DriverManager.getConnection(url);
            } catch ( SQLException ex ) {
                throw new WrappedSqlException(ex);
            }
        }, maxConnections);
    } catch (WrappedSqlException wse) {
        throw wse.getSqlException();
    }
}

Wenn Sie Ihre eigene eindeutige Klasse erstellen, sollte verhindert werden, dass eine andere ungeprüfte Ausnahme für die in Ihrem Lambda eingeschlossene Ausnahme missverstanden wird, selbst wenn die Ausnahme irgendwo in der Pipeline serialisiert wird, bevor Sie sie fangen und erneut werfen.

Hmm ... Das einzige Problem, das ich hier sehe, ist, dass Sie dies in einem Konstruktor mit einem Aufruf von super () tun, der laut Gesetz die erste Anweisung in Ihrem Konstruktor sein muss. Zählt try als vorherige Anweisung? Ich habe dies (ohne den Konstruktor) in meinem eigenen Code.

4
GlenPeterson

Wir haben in meinem Unternehmen ein internes Projekt entwickelt, das uns dabei geholfen hat. Wir haben vor zwei Monaten beschlossen, an die Börse zu gehen.

Das haben wir uns ausgedacht:

@FunctionalInterface
public interface ThrowingFunction<T,R,E extends Throwable> {
R apply(T arg) throws E;

/**
 * @param <T> type
 * @param <E> checked exception
 * @return a function that accepts one argument and returns it as a value.
 */
static <T, E extends Exception> ThrowingFunction<T, T, E> identity() {
    return t -> t;
}

/**
 * @return a Function that returns the result of the given function as an Optional instance.
 * In case of a failure, empty Optional is returned
 */
static <T, R, E extends Exception> Function<T, Optional<R>> lifted(ThrowingFunction<T, R, E> f) {
    Objects.requireNonNull(f);

    return f.lift();
}

static <T, R, E extends Exception> Function<T, R> unchecked(ThrowingFunction<T, R, E> f) {
    Objects.requireNonNull(f);

    return f.uncheck();
}

default <V> ThrowingFunction<V, R, E> compose(final ThrowingFunction<? super V, ? extends T, E> before) {
    Objects.requireNonNull(before);

    return (V v) -> apply(before.apply(v));
}

default <V> ThrowingFunction<T, V, E> andThen(final ThrowingFunction<? super R, ? extends V, E> after) {
    Objects.requireNonNull(after);

    return (T t) -> after.apply(apply(t));
}

/**
 * @return a Function that returns the result as an Optional instance. In case of a failure, empty Optional is
 * returned
 */
default Function<T, Optional<R>> lift() {
    return t -> {
        try {
            return Optional.of(apply(t));
        } catch (Throwable e) {
            return Optional.empty();
        }
    };
}

/**
 * @return a new Function instance which wraps thrown checked exception instance into a RuntimeException
 */
default Function<T, R> uncheck() {
    return t -> {
        try {
            return apply(t);
        } catch (final Throwable e) {
            throw new WrappedException(e);
        }
    };
}

}

https://github.com/TouK/ThrowingFunction/

3

Paguro bietet funktionale Schnittstellen, die geprüfte Ausnahmen umschließen . Ich habe ein paar Monate nachdem Sie Ihre Frage gestellt haben, daran gearbeitet, so dass Sie wahrscheinlich Teil der Inspiration waren!

Sie werden feststellen, dass es in Paguro nur 4 funktionale Schnittstellen gibt, im Vergleich zu den 43 Schnittstellen, die in Java 8 enthalten sind.

Paguro hat Single-Pass-Transformationen in seine unveränderlichen Sammlungen eingebaut (von Clojure kopiert). Diese Transformationen entsprechen in etwa den Clojure-Transducern oder Java 8-Streams, akzeptieren jedoch funktionale Schnittstellen, die geprüfte Ausnahmen umschließen. Siehe: die Unterschiede zwischen Paguro- und Java 8-Streams .

1
GlenPeterson

Das Umschließen der Ausnahme auf die beschriebene Weise funktioniert nicht. Ich habe es ausprobiert und erhalte immer noch Compiler-Fehler, was eigentlich der Spezifikation entspricht: Der Lambda-Ausdruck löst die Ausnahme aus, die mit dem Zieltyp des Methodenarguments nicht kompatibel ist: Callable; call () wirft es nicht, damit ich den Lambda-Ausdruck nicht als Callable übergeben kann.

Im Grunde gibt es keine Lösung: Wir sind mit Schreibkesselplatten beschäftigt. Das einzige, was wir tun können, ist unsere Meinung zu äußern, dass dies korrigiert werden muss. Ich denke, dass die Spezifikation einen Zieltyp nicht einfach blind löschen sollte, basierend auf inkompatiblen ausgelösten Ausnahmen: Er sollte anschließend prüfen, ob die ausgelöste inkompatible Ausnahme abgefangen oder als Würfe im aufrufenden Bereich deklariert wird Ich schlage vor, wir können sie als leise markierte Ausnahmebedingung markieren (leise in dem Sinne, dass der Compiler nicht prüfen sollte, aber die Laufzeitumgebung trotzdem fangen sollte). Die Markierungen werden mit => im Gegensatz zu -> .__ ist keine Diskussionsseite, aber da dies IS die einzige Lösung für die Frage ist, lassen Sie sich hören und ändern Sie diese Spezifikation!

Sie can werfen aus Ihren Lambdas, sie müssen nur "mit Ihrem eigenen Weg" deklariert werden (was sie leider im JDK-Standard nicht wiederverwendbar macht, aber hey, wir tun, was wir können) .

@FunctionalInterface
public interface SupplierIOException {
   MyClass get() throws IOException;
}

Oder die generischere Version:

public interface ThrowingSupplier<T, E extends Exception> {
  T get() throws E;
}

ref hier . Es wird auch erwähnt, dass "sneakyThrow" verwendet wird, um die markierten Ausnahmen nicht zu deklarieren, sie aber trotzdem zu werfen. Es tut mir weh, vielleicht eine Option.

0
rogerdpack