Herausforderungen in Java 8

Herausforderungen in Java 8

1. Überblick

In Java 8 wurden einige neue Funktionen eingeführt, die sich hauptsächlich mit der Verwendung von Lambda-Ausdrücken befassten. In diesem kurzen Artikel werden wir uns die Nachteile einiger von ihnen ansehen.

Obwohl dies keine vollständige Liste ist, handelt es sich um eine subjektive Sammlung der häufigsten und beliebtesten Beschwerden bezüglich neuer Funktionen in Java 8.

2. Java 8 Stream und Thread Pool

Erstens sollen parallele Streams eine einfache parallele Verarbeitung von Sequenzen ermöglichen, und das funktioniert in einfachen Szenarien ganz in Ordnung.

Der Stream verwendet die standardmäßigen, gemeinsamenForkJoinPool - teilt Sequenzen in kleinere Blöcke auf und führt Operationen mit mehreren Threads aus.

Es gibt jedoch einen Haken. Es gibt keinen guten Weg zuspecify which ForkJoinPool to use. Wenn einer der Threads hängen bleibt, müssen alle anderen, die den gemeinsam genutzten Pool verwenden, auf den Abschluss der lang laufenden Aufgaben warten.

Glücklicherweise gibt es dafür eine Problemumgehung:

ForkJoinPool forkJoinPool = new ForkJoinPool(2);
forkJoinPool.submit(() -> /*some parallel stream pipeline */)
  .get();

Dadurch wird ein neues, separatesForkJoinPoolerstellt, und alle vom parallelen Stream generierten Aufgaben verwenden den angegebenen Pool und nicht den gemeinsam genutzten Standardpool.

Es ist erwähnenswert, dass es einen weiteren potenziellen Haken gibt:“this technique of submitting a task to a fork-join pool, to run the parallel stream in that pool is an implementation ‘trick' and is not guaranteed to work”, so Stuart Marks - Java- und OpenJDK-Entwickler von Oracle. Eine wichtige Nuance, die Sie bei dieser Technik berücksichtigen sollten.

3. Verminderte Debugbarkeit

The new coding style simplifies our source code, yetcan cause headaches while debugging it.

Schauen wir uns zunächst dieses einfache Beispiel an:

public static int getLength(String input) {
    if (StringUtils.isEmpty(input) {
        throw new IllegalArgumentException();
    }
    return input.length();
}

List lengths = new ArrayList();

for (String name : Arrays.asList(args)) {
    lengths.add(getLength(name));
}

Dies ist ein standardmäßiger imperativer Java-Code, der selbsterklärend ist.

Wenn wir leereString als Eingabe übergeben - als Ergebnis - löst der Code eine Ausnahme aus, und in der Debug-Konsole können wir sehen:

at LmbdaMain.getLength(LmbdaMain.java:19)
at LmbdaMain.main(LmbdaMain.java:34)

Lassen Sie uns nun denselben Code mithilfe der Stream-API neu schreiben und sehen, was passiert, wenn ein leeresString übergeben wird:

Stream lengths = names.stream()
  .map(name -> getLength(name));

Die Aufrufliste sieht folgendermaßen aus:

at LmbdaMain.getLength(LmbdaMain.java:19)
at LmbdaMain.lambda$0(LmbdaMain.java:37)
at LmbdaMain$$Lambda$1/821270929.apply(Unknown Source)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.LongPipeline.reduce(LongPipeline.java:438)
at java.util.stream.LongPipeline.sum(LongPipeline.java:396)
at java.util.stream.ReferencePipeline.count(ReferencePipeline.java:526)
at LmbdaMain.main(LmbdaMain.java:39)

Dies ist der Preis, den wir für die Nutzung mehrerer Abstraktionsschichten in unserem Code zahlen. IDEs haben jedoch bereits solide Tools zum Debuggen von Java-Streams entwickelt.

4. MethodenNull oderOptional zurückgeben

Optional wurde in Java 8 eingeführt, um eine typsichere Möglichkeit zum Ausdruck von Optionalität zu bieten.

Optional gibt explizit an, dass der Rückgabewert möglicherweise nicht vorhanden ist. Daher kann der Aufruf einer Methode einen Wert zurückgeben, undOptional wird verwendet, um diesen Wert darin zu verpacken - was sich als praktisch herausstellte.

Leider kam es aufgrund der Java-Abwärtskompatibilität manchmal vor, dass Java-APIs zwei verschiedene Konventionen mischten. In derselben Klasse finden wir Methoden, die Nullen zurückgeben, sowie Methoden, dieOptionals. zurückgeben

5. Zu viele funktionale Schnittstellen

Im Paketjava.util.function haben wir eine Sammlung von Zieltypen für Lambda-Ausdrücke. Wir können sie unterscheiden und gruppieren als:

  • Consumer - stellt eine Operation dar, die einige Argumente akzeptiert und kein Ergebnis zurückgibt

  • Function - repräsentiert eine Funktion, die einige Argumente akzeptiert und ein Ergebnis erzeugt

  • Operator - repräsentiert eine Operation für einige Typargumente und gibt ein Ergebnis des gleichen Typs wie die Operanden zurück

  • Predicate - repräsentiert ein Prädikat (boolean-wertige Funktion) einiger Argumente

  • Supplier - repräsentiert einen Lieferanten, der keine Argumente akzeptiert und Ergebnisse zurückgibt

Zusätzlich haben wir zusätzliche Typen für die Arbeit mit Grundelementen:

  • IntConsumer

  • IntFunction

  • IntPredicate

  • IntSupplier

  • IntToDoubleFunction

  • IntToLongFunction

  • … Und die gleichen Alternativen fürLongs undDoubles

Weiterhin spezielle Typen für Funktionen mit der Arität 2:

  • BiConsumer

  • BiPredicate

  • BinaryOperator

  • BiFunktion

Infolgedessen enthält das gesamte Paket 44 Funktionstypen, die sicherlich verwirrend werden können.

6. Überprüfte Ausnahmen und Lambda-Ausdrücke

Überprüfte Ausnahmen waren bereits vor Java 8 ein problematisches und umstrittenes Problem. Seit der Einführung von Java 8 ist das neue Problem aufgetreten.

Überprüfte Ausnahmen müssen entweder sofort abgefangen oder gemeldet werden. Da die Funktionsschnittstellen vonjava.util.functionkeine Ausnahmen auslösen, schlägt der Code, der die geprüfte Ausnahme auslöst, beim Kompilieren fehl:

static void writeToFile(Integer integer) throws IOException {
    // logic to write to file which throws IOException
}
List integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> writeToFile(i));

Eine Möglichkeit, dieses Problem zu lösen, besteht darin, die aktivierte Ausnahme in einentry-catch-Block zu packen undRuntimeException erneut zu werfen:

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

Das wird funktionieren. Das Auslösen vonRuntimeException widerspricht jedoch dem Zweck der überprüften Ausnahme und führt dazu, dass der gesamte Code mit Boilerplate-Code umbrochen wird, den wir durch die Verwendung von Lambda-Ausdrücken reduzieren möchten. Eine der hackigen Lösungen istto rely on the sneaky-throws hack.

Eine andere Lösung besteht darin, ein Consumer Functional Interface zu schreiben, das eine Ausnahme auslösen kann:

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

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

Leider verpacken wir die aktivierte Ausnahme immer noch in eine Laufzeitausnahme.

Schließlich können wir für eine eingehende Lösung und Erklärung des Problems den folgenden tiefen Tauchgang untersuchen:Exceptions in Java 8 Lambda Expressions.

8. Fazit

In dieser kurzen Beschreibung haben wir einige der Nachteile von Java 8 erörtert.

Während einige davon bewusst von Java-Architekten entworfen wurden und es in vielen Fällen eine Problemumgehung oder alternative Lösung gibt; Wir müssen uns ihrer möglichen Probleme und Einschränkungen bewusst sein.