Défis de Java 8

Les défis de Java 8

1. Vue d'ensemble

Java 8 a introduit de nouvelles fonctionnalités, principalement axées sur l'utilisation d'expressions lambda. Dans cet article rapide, nous allons examiner les inconvénients de certains d'entre eux.

Et, bien qu'il ne s'agisse pas d'une liste complète, il s'agit d'un ensemble subjectif des plaintes les plus courantes et les plus populaires concernant les nouvelles fonctionnalités de Java 8.

2. Flux Java 8 et pool de threads

Tout d’abord, les flux parallèles sont destinés à faciliter le traitement parallèle des séquences en parallèle, et cela fonctionne assez bien pour des scénarios simples.

Le Stream utilise leForkJoinPool commun par défaut - scinde les séquences en petits morceaux et effectue des opérations en utilisant plusieurs threads.

Cependant, il y a un hic. Il n'y a pas de bon moyen despecify which ForkJoinPool to use et par conséquent, si l'un des threads est bloqué, tous les autres, utilisant le pool partagé, devront attendre la fin des tâches de longue durée.

Heureusement, il existe une solution de contournement pour cela:

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

Cela créera un nouveauForkJoinPool séparé et toutes les tâches générées par le flux parallèle utiliseront le pool spécifié et non celui par défaut partagé.

Il est intéressant de noter qu’il existe un autre problème potentiel:“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”, selon Stuart Marks - développeur Java et OpenJDK d’Oracle. Une nuance importante à garder à l’esprit lorsque vous utilisez cette technique.

3. Diminution de la débuggabilité

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

Tout d'abord, regardons cet exemple simple:

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

Il s'agit d'un code Java impératif standard qui s'explique par lui-même.

Si nous transmettons desString vides comme entrée - en conséquence - le code lèvera une exception, et dans la console de débogage, nous pouvons voir:

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

Maintenant, réécrivons le même code à l'aide de l'API Stream et voyons ce qui se passe lorsqu'unString vide est passé:

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

La pile d'appels ressemblera à:

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)

C’est le prix que nous payons pour exploiter plusieurs couches d’abstraction dans notre code. Cependant, les IDE ont déjà développé de solides outils pour le débogage des flux Java.

4. Méthodes renvoyantNull ouOptional

Optional a été introduit dans Java 8 pour fournir un moyen sûr d'exprimer le caractère facultatif.

Optional, indique explicitement que la valeur de retour n'est peut-être pas présente. Par conséquent, l'appel d'une méthode peut renvoyer une valeur, etOptional est utilisé pour envelopper cette valeur à l'intérieur - ce qui s'est avéré pratique.

Malheureusement, en raison de la compatibilité ascendante Java, nous avons parfois eu recours à des API Java mélangeant deux conventions différentes. Dans la même classe, on peut trouver des méthodes retournant des nulls ainsi que des méthodes retournantOptionals.

5. Trop d'interfaces fonctionnelles

Dans le packagejava.util.function, nous avons une collection de types de cibles pour les expressions lambda. Nous pouvons les distinguer et les regrouper comme suit:

  • Consumer - représente une opération qui prend des arguments et ne renvoie aucun résultat

  • Function - représente une fonction qui prend des arguments et produit un résultat

  • Operator - représente une opération sur certains arguments de type et renvoie un résultat du même type que les opérandes

  • Predicate - représente un prédicat (fonctionboolean-valued) de certains arguments

  • Supplier - représente un fournisseur qui ne prend aucun argument et renvoie des résultats

De plus, nous avons d'autres types pour travailler avec des primitives:

  • IntConsumer

  • IntFonction

  • IntPredicate

  • IntSupplier

  • IntToDoubleFunction

  • IntToLongFunction

  • … Et mêmes alternatives pourLongs etDoubles

De plus, des types spéciaux pour les fonctions avec l’arité de 2:

  • BiConsommateur

  • BiPredicate

  • BinaryOperator

  • BiFonction

En conséquence, l'ensemble du paquet contient 44 types fonctionnels, ce qui peut certainement commencer à être déroutant.

6. Exceptions vérifiées et expressions Lambda

Les exceptions vérifiées étaient déjà un problème problématique et controversé avant Java 8. Depuis l'arrivée de Java 8, le nouveau problème est apparu.

Les exceptions cochées doivent être soit immédiatement détectées, soit déclarées. Puisque les interfaces fonctionnelles dejava.util.function ne déclarent pas de levée d'exceptions, le code qui lève une exception vérifiée échouera lors de la 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));

Une façon de surmonter ce problème consiste à encapsuler l'exception vérifiée dans un bloctry-catch et à renvoyerRuntimeException:

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

Cela fonctionnera. Cependant, lancerRuntimeException contredit l'objectif de l'exception vérifiée et rend l'ensemble du code enveloppé avec du code standard, que nous essayons de réduire en exploitant les expressions lambda. L'une des solutions hacky estto rely on the sneaky-throws hack.

Une autre solution consiste à écrire une interface fonctionnelle client, qui peut générer une 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);
        }
    };
}

Malheureusement, nous encapsulons toujours l'exception vérifiée dans une exception d'exécution.

Enfin, pour une solution et une explication approfondies du problème, nous pouvons explorer les détails suivants:Exceptions in Java 8 Lambda Expressions.

8. Conclusion

Dans ce bref résumé, nous avons présenté certains des inconvénients de Java 8.

Certains d'entre eux étaient des choix de conception délibérés faits par des architectes du langage Java et, dans de nombreux cas, il existe une solution de contournement ou une solution alternative; nous devons être conscients de leurs problèmes possibles et de leurs limites.