Défis de Java 8

1. Vue d'ensemble

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

Et, bien que ce ne soit pas une liste complète, il s'agit d'une collection subjective 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, et cela fonctionne tout à fait bien pour des scénarios simples.

Le Stream utilise le ForkJoinPool commun par défaut - divise 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 de spécifier quel ForkJoinPool utiliser et donc, si l'un des threads est bloqué, tous les autres, utilisant le pool partagé, devront attendre que les tâches de longue durée soient terminées.

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 nouveau ForkJoinPool séparé et toutes les tâches générées par le flux parallèle utiliseront le pool spécifié et non dans le pool partagé par défaut.

Il convient de noter qu'il existe un autre problème potentiel: «cette technique consistant à soumettre une tâche à un pool de fork-join, pour exécuter le flux parallèle dans ce pool est une 'astuce' d'implémentation et n'est pas garantie de fonctionner» , selon Stuart Marks - Développeur Java et OpenJDK d'Oracle. Une nuance importante à garder à l'esprit lors de l'utilisation de cette technique.

3. Diminution de la débuggabilité

Le nouveau style de codage simplifie notre code source, mais peut causer des maux de tête lors du débogage .

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 passons une chaîne vide 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'une chaîne vide est transmise:

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é des outils solides pour le débogage des flux Java.

4. Méthodes renvoyant nul ou facultatif

L'option facultative a été introduite dans Java 8 pour fournir un moyen sûr d'exprimer le caractère facultatif.

Facultatif , 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, et Facultatif est utilisé pour envelopper cette valeur à l'intérieur - ce qui s'est avéré pratique.

Malheureusement, en raison de la rétrocompatibilité Java, nous nous sommes parfois retrouvés avec des API Java mélangeant deux conventions différentes. Dans la même classe, nous pouvons trouver des méthodes retournant des valeurs nulles ainsi que des méthodes retournant des options.

5. Trop d'interfaces fonctionnelles

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

  • Consommateur - représente une opération qui prend certains arguments et ne renvoie aucun résultat
  • Fonction - représente une fonction qui prend des arguments et produit un résultat
  • Opérateur - représente une opération sur certains arguments de type et renvoie un résultat du même type que les opérandes
  • Prédicat - représente un prédicat ( fonction à valeur booléenne ) de certains arguments
  • Fournisseur - représente un fournisseur qui ne prend aucun argument et renvoie des résultats

De plus, nous avons des types supplémentaires pour travailler avec des primitives:

  • IntConsumer
  • IntFonction
  • IntPredicate
  • IntSupplier
  • IntToDoubleFunction
  • IntToLongFunction
  • … Et les mêmes alternatives pour les Longs et les Doubles

En outre, des types spéciaux pour les fonctions avec l'arité de 2:

  • BiConsommateur
  • BiPredicate
  • BinaryOperator
  • BiFonction

En conséquence, l'ensemble du package contient 44 types fonctionnels, ce qui peut certainement commencer à prêter à confusion.

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

Les exceptions vérifiées doivent être soit détectées immédiatement, soit déclarées. Étant donné que les interfaces fonctionnelles java.util.function ne déclarent pas de levée d'exceptions, le code qui lève l'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 bloc try-catch et à relancer RuntimeException :

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, lancer RuntimeException contredit le but de l'exception vérifiée et rend l'ensemble du code enveloppé avec du code standard, que nous essayons de réduire en tirant parti des expressions lambda. L'une des solutions hacky est de s'appuyer sur le hack sournois.

Une autre solution consiste à écrire une interface fonctionnelle consommateur - qui peut lever 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 dans les expressions Java 8 Lambda.

8 . Conclusion

Dans cet article rapide, nous avons discuté de certains des inconvénients de Java 8.

Alors que certains d'entre eux étaient des choix de conception délibérés faits par les 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 éventuels problèmes et limites.