Herausforderungen in Java 8

1. Übersicht

Java 8 führte einige neue Funktionen ein, 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

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

Der Stream verwendet den üblichen ForkJoinPool - teilt Sequenzen in kleinere Blöcke auf und führt Operationen mit mehreren Threads aus.

Es gibt jedoch einen Haken. Es gibt keine gute Möglichkeit, anzugeben, welcher ForkJoinPool verwendet werden soll. 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 neuer, separater ForkJoinPool erstellt, 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: „Diese Technik, eine Aufgabe an einen Fork-Join-Pool zu senden, um den parallelen Stream in diesem Pool auszuführen, ist ein Implementierungstrick und kann nicht garantiert funktionieren“ , so Stuart Marks - Java- und OpenJDK-Entwickler von Oracle. Eine wichtige Nuance, die Sie bei der Verwendung dieser Technik berücksichtigen sollten.

3. Verminderte Debugbarkeit

Der neue Codierungsstil vereinfacht unseren Quellcode, kann jedoch beim Debuggen Kopfschmerzen verursachen .

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 einen leeren String als Eingabe übergeben, löst der Code eine Ausnahme aus, und in der Debug-Konsole können wir Folgendes 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 leerer String übergeben wird:

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

Der Aufrufstapel 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)

Das 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. Methoden, die Null oder Optional 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 das Aufrufen einer Methode einen Wert zurückgeben, und Optional wird dieser Wert darin eingeschlossen - was sich als praktisch herausstellte.

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

5. Zu viele funktionale Schnittstellen

Im Paket java.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
  • Funktion - stellt eine Funktion dar, die einige Argumente akzeptiert und ein Ergebnis erzeugt
  • Operator - Stellt eine Operation für einige Typargumente dar und gibt ein Ergebnis des gleichen Typs wie die Operanden zurück
  • Prädikat - repräsentiert ein Prädikat ( boolesche Funktion) einiger Argumente
  • Lieferant - Stellt einen Lieferanten dar, 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ür Longs und Doubles

Darüber hinaus spezielle Typen für Funktionen mit der Arität 2:

  • BiConsumer
  • BiPredicate
  • BinaryOperator
  • BiFunktion

Infolgedessen enthält das gesamte Paket 44 Funktionstypen, was sicherlich verwirrend sein kann.

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

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

Checked exceptions must be either caught immediately or declared. Since java.util.function functional interfaces do not declare throwing exceptions, code that throws checked exception will fail during compilation:

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));

One way to overcome this problem is to wrap checked exception in a try-catch block and rethrow RuntimeException:

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

This will work. However, throwing RuntimeException contradicts the purpose of checked exception and makes the whole code wrapped with boilerplate code, which we're trying to reduce by leveraging lambda expressions. One of the hacky solutions is to rely on the sneaky-throws hack.

Another solution is to write a Consumer Functional Interface – that can throw an exception:

@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.

Um eine detaillierte Lösung und Erklärung des Problems zu erhalten, können wir schließlich den folgenden tiefen Tauchgang untersuchen: Ausnahmen in Java 8 Lambda Expressions.

8 . Fazit

In diesem kurzen Artikel haben wir einige der Nachteile von Java 8 besprochen.

Während einige von ihnen absichtliche Entwurfsentscheidungen von Java-Spracharchitekten waren 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.