Java 8: pourquoi devriez-vous vous en soucier ?

Chapitre 1. Java 8: pourquoi devriez-vous vous soucier?

Ce chapitre couvre les points suivants

  • Pourquoi Java continue d’évoluer ?
  • Modification de l’arrière-plan informatique : multicore et traitement de grands ensembles de données (Big data)
  • Pression à évoluer : de nouvelles architectures favorisent le style fonctionnel sur l’impératif
  • Présentation des nouvelles fonctionnalités essentielles de Java 8: lambdas, stream, méthodes par défaut dans les interfaces

Depuis la sortie du JDK 1.0 (Java 1.0) en 1996, Java a gagné un grand nombre d’étudiants, de gestionnaires de projet et de programmeurs qui sont des utilisateurs actifs. C’est un langage expressif et continue d’être utilisé pour des projets à la fois grands et petits. Son évolution (via l’ajout de nouvelles fonctionnalités) de Java 1.1 (1997) à Java 7 (2011) a été bien gérée. Java 8 a été publié en mars 2014 pour traiter les données et ressemble de très près à la façon dont on réfléchit dans les langues qu’on utilise au quotidien. Donc, la question est la suivante : pourquoi devriez-vous vous en soucier ?

Je suis soutient que les changements apportés à Java 8 sont à bien des égards plus profonds que tout autre changement de Java dans son histoire. La bonne nouvelle est que les modifications vous permettent d’écrire des programmes plus facilement : au lieu d’écrire un code détaillé comme suit (pour trier une liste de pommes dans l’inventaire en fonction de leur poids),

 

En java 8, il est possible d’écrire un code plus concis qui traduit mieux la fonctionnalité à implémenter, donc plus lisible :

Il trie l’inventaire en comparant le poids de la pomme. Ne vous inquiétez pas pour ce code pour l’instant. Le tutoriel expliquera ce qu’il fait et comment vous pouvez écrire un code similaire !

Il existe également une influence sur le matériel : les processeurs sont devenus multi core: le processeur de votre ordinateur portable ou de votre ordinateur de bureau a probablement au moins quatre cœurs CPU. Mais la grande majorité des programmes Java existants n’utilisent qu’un de ces noyaux et laissent les trois autres inactifs (ou dépensent une petite fraction de leur puissance de traitement en fonction d’une partie du système d’exploitation ou d’un vérificateur de virus).

Avant Java 8, les experts peuvent vous dire que vous devez utiliser des threads pour utiliser ces noyaux. Le problème est que travailler avec des threads est difficile et susceptible d’erreurs. Java a suivi un chemin évolutif consistant à essayer continuellement de rendre le parallelisme plus facile et moins susceptible d’erreurs. Java 1.0 a eu des threads et des verrous et même un modèle de mémoire – la meilleure pratique à l’époque – mais ces primitives se sont avérées trop difficiles à utiliser de manière fiable dans des équipes de projet non expérimentées. Java 5 a ajouté des blocs de construction industriels comme les pools de threads et les collections simultanées. Java 7 a ajouté le framework fork/ join, rendant le parallélisme plus pratique mais toujours difficile. Java 8 a une nouvelle façon plus simple de penser au parallélisme. Mais vous devez encore respecter certaines règles, que vous apprendrez dans ce tutoriel !

À partir de ces deux exemples (code plus concis et utilisation plus simple des processeurs multi core), j’espère vous avoir donné un avant-goût de ce qui vous attend :

  • The Streams API
  • Techniques pour transmettre le code aux méthodes
  • Méthodes par défaut dans les interfaces

Java 8 fournit une nouvelle API (appelée Streams) qui prend en charge de nombreuses opérations parallèles pour traiter les données et ressemble de très près à la façon dont on réfléchit lorsqu’on utilise les langages de requête de base de données : vous exprimez ce que vous voulez d’une manière plus déclarative et la mise en œuvre (ici la bibliothèque Streams) choisit le meilleur mécanisme d’exécution de bas niveau. En conséquence, il évite la nécessité d’écrire un code qui utilise le mot clé synchronized, ce qui n’est pas seulement très susceptible de générer des erreurs, mais est également plus coûteux pour les CPU multicœur, que ce que vous pouvez imaginer.

D’un point de vue légèrement révisionniste, l’ajout de Streams en Java 8 peut être considéré comme une cause directe des deux autres ajouts à la plateforme : la transmission de fonctions aux méthodes (références de méthodes, lambdas) et les méthodes par défaut dans les interfaces.

Mais voir la transmission de fonction en paramètre comme une simple conséquence de l’API Streams minimise sa gamme d’utilisations dans Java 8. Il vous donne une nouvelle façon concise d’exprimer le paramétrage du comportement. Supposons que vous souhaitez écrire deux méthodes qui ne diffèrent que de quelques lignes de code ; vous pouvez maintenant passer le code des parties qui diffèrent en tant qu’argument (cette technique de programmation est plus courte, plus claire et moins propice aux erreurs que la tendance commune à faire du copier-coller). Certains diraient que le paramétrage du comportement pouvait, avant Java 8, être mis en place à l’aide de classes anonymes, mais je laisserai l’exemple de la première page de ce chapitre, qui montre une concision accrue avec Java 8, parler d’elle-même en termes de clarté !

La fonctionnalité Java 8 de transmettre le code aux méthodes (également de pouvoir le renvoyer et l’intégrer dans les structures de données) permet également d’accéder à toute une gamme de techniques supplémentaires appelées communément, programmation fonctionnelle. En un mot, un tel code, peut être transmis et combiné de manière à produire des idiomes de programmation puissants que vous verrez sous la forme Java 8 dans mon tutoriel.

Le chapitre commence par une discussion de haut niveau sur la façon dont les languages évoluent, se poursuit avec des sections sur les principales fonctionnalités de Java 8, puis présente les concepts de programmation fonctionnelle que les nouvelles fonctionnalités de java 8 simplifient et que les nouvelles architectures informatiques favorisent. En resumé :

  • la section 1.1 traite du processus d’évolution et des concepts, dont Java manquait auparavant, pour exploiter le parallélisme multi core d’une manière simple.
  • La section 1.2 explique pourquoi le passage de fonction en paramètre est un idiom de programmation tellement puissant.
  • La section 1.3 fait de même pour l’API Streams, c’est à dire la capacité de cette API à représenter des données séquentielles et indiquer si celles-ci peuvent être traitées en parallèle ou pas.
  • La section 1.4 explique comment la nouvelle fonctionnalité Java 8 des méthodes par défaut dans les interfaces leur permet, ainsi qu’aux bibliothèques d’évoluer plus facilement.
  • La section 1.5 se penche sur les idiomes de programmation fonctionnelle en Java et d’autres langages utilisant la JVM.

Appréciez la balade !

1.1. POURQUOI JAVA CONTINUE D’EVOLUER?

Avec les années 1960, il s’agissait de la quête du langage de programmation parfait. Peter Landin, célèbre informaticien de son époque, a noté en 1966 dans un article historique qu’il y avait déjà eu 700 langages de programmation et spéculé sur ce que seraient les prochains 700, y compris des arguments pour une programmation fonctionnelle similaire à celle dans Java 8.

Plusieurs milliers de langages de programmation plus tard, les universitaires ont conclu que les langages de programmation se comportaient comme un écosystème : de nouveaux langages apparaissent et les anciens langages sont supplantés à moins d’évoluer. Nous espérons tous un langage universel parfait, mais en réalité, certains langages sont mieux adaptés à certaines niches. Par exemple, le C et C + + restent populaires pour la construction de systèmes d’exploitation et d’autres systèmes embarqués en raison de leur faible encombrement et ce malgré leur manque de sécurité de programmation. Moi même titulaire d’un diplôme d’ingénieur en électronique des systèmes embarqués j’ai eu à mainte reprises l’occasion de constater à quel point Java était juste inadapté aux environnements soumis aux contraintes de temps réel, et de mémoire très limitée à l’instar de C et C++. Et ceci bien que leur manque de sécurité peut conduire à des programmes qui crash de manière imprévisible et à l’exposition de failles de sécurité pour les virus et autres. En effet, les langages sécurisés tels que Java et C# ont supplanté C et C++ dans diverses applications lorsque l’empreinte d’exécution supplémentaire est acceptable.

L’occupation antérieure d’une niche tend à décourager les concurrents. La modification d’un nouveau langage et d’une chaîne d’outils est souvent trop pénible pour une seule fonctionnalité, mais les nouveaux arrivants finiront par déplacer les langages existants, à moins que ces derniers n’évoluent suffisamment (les lecteurs plus âgés sont souvent en mesure de citer une gamme de langages dans lesquelles ils ont déjà codé, mais dont la popularité a diminué : Ada, Algol, COBOL, Pascal, Delphi et SNOBOL, pour n’en citer que quelques-uns.

Vous êtes un programmeur Java, et Java a réussit à coloniser (et à supplanter les langages concurrents) une grande niche d’écosystème de tâches de programmation au cours des 15 dernières années. Examinons certaines raisons à cela.

 

1.1.1. La place de Java dans l’écosystème des langages de programmation

Java a bien démarré. Dès le début, il s’agissait d’un langage orienté objet avec de nombreuses bibliothèques utiles. Il a également pris en charge la concurrence à petite échelle dès le premier jour, avec son support intégré pour les Thread et les verrous (ce qui lui a conféré une légitimité précoce, en tant que langage dotée d’une neutralité parfaite au niveau de la plateforme matérielle). En outre, la décision de compiler en Java vers le byte code de la JVM (un code machine que bientôt tous les navigateurs prendrons en charge) signifiait qu’il est devenu le langage de choix pour les programmes d’applet internet (vous souvenez-vous des applets ?). En effet, il existe un réel danger que la machine virtuelle Java (JVM) et son bytecode soient considérés comme plus importants que le langage Java lui même et que, pour certaines applications, Java puisse être remplacé par l’un de ses langages concurrents telles que Scala ou Groovy, qui fonctionne également sur la JVM. Diverses mises à jour récentes de la JVM (par exemple, le nouveau bytecode invokedynamic dans JDK7) visent à aider ces langages concurrents à fonctionner plus facilement sur la JVM et à interagir avec Java. Java a également réussi à coloniser divers aspects de l’informatique embarquée (depuis les cartes à puce, les grille-pains jusqu’aux systèmes de freinage automobile).



Comment Java est il devenu le langage de programmation le plus utilisé?

L’orienté objet est devenu à la mode dans les années 1990 pour deux raisons : son concept d’encapsulation a entraîné moins de problèmes d’ingénierie logicielle que ceux de C ; Il peut se résumer comme suit : tout est un objet. Le dicton « write once, run everywhere » et la capacité des premiers navigateurs à exécuter des applets de code Java (en toute sécurité) lui ont donné une place dans les universités et écoles d’ingénieur, dont les diplômés étaient de futurs industriels. Du coup le langage s’est lui aussi répandus en entreprise. Il y avait eu une résistance initiale au coût de fonctionnement supplémentaire de Java sur C / C ++, mais les machines sont devenues plus rapides au fil du temps.

 



 

Mais le climat change dans l’écosystème des langages de programmation ; les programmeurs traitent de plus en plus de données, (Big data) et souhaitent exploiter efficacement les ordinateurs Multi-coeur pour les traiter. Et cela implique l’utilisation d’un traitement en parallèle plus fréquemment, quelque chose dont Java n’était pas au courant.

Vous avez peut-être rencontré des concepts de programmation provenant d’autres niches de programmation (par exemple, le map-reduce de google) qui vous aident à travailler avec de gros volumes de données et des CPU multi-coeur. La figure ci-dessous résume l’écosystème des langages en images : pensez au paysage comme l’espace des besoins en programmation et la végétation dominante pour un morceau de terrain particulier, comme langage préférée pour ce programme. Le changement climatique est l’idée que le matériel ou la nouvelle façon de programmer change, évolue (par exemple, « Pourquoi ne puis-je pas programmer dans un certain style en SQL ?») Cela signifie que certains langages deviennent des langages de choix pour les nouveaux projets, tout comme l’augmentation des températures régionales signifie que les raisins se développent maintenant dans les latitudes plus élevées. Mais bien sûr, il y a de l’hystérésis : beaucoup d’anciens agriculteurs continueront d’augmenter les cultures traditionnelles. En résumé, de nouveaux langages apparaissent et deviennent de plus en plus populaires car ils se sont rapidement adaptés au changement climatique.

 

Le principal avantage de Java 8 pour un programmeur est qu’il fournit plus d’outils et de concepts de programmation pour résoudre plus rapidement des nouveaux et anciens problèmes de programmation de manière plus concise et qui seront plus facilement maintenable. Bien que les concepts soient nouveaux en Java, ils se sont révélés puissants dans l’invasion de certaines niches sur le marché de l’IT, au sein des langages de programmation. Ici je mets en évidence et développe les trois concepts de programmation qui ont été apportés avec Java 8, lui permettant d’exploiter le parallélisme et d’écrire un code plus concis.

 

1.1.2. Process des streams

Le premier concept de programmation est le traitement des flux de données, encore appelé communément Streams. Un flux est une séquence de données qui sont produites de manière conceptuelle une à la fois. Un programme peut lire des éléments à partir d’un flux de saisie un par un et en écrire dans un flux de sortie. Le flux de sortie d’un programme pourrait bien être le flux d’entrée d’un autre.

Un exemple pratique dans Unix ou Linux, où de nombreux programmes fonctionnent en lisant des données à partir d’une entrée standard (stdin dans Unix et C, System.in en Java). Après avoir traiter les données reçues en entrée, ils écrivent le résultat dans la sortie standard (stdout dans Unix et C, System.out en Java). Par exemple : le programme cat d’UNIX crée un flux en concaténant deux fichiers, tr traduit les caractères dans un flux, sort trie les lignes dans un flux et la commande tail-3 donne les trois dernières lignes dans un flux. Le système Unix permet à ces programmes d’être liés avec des pipes (|):

Cette commande (en supposant que file1 et file2 contiennent un seul mot par ligne) imprime les trois mots contenus dans les fichiers, dans l’ordre du dictionnaire, après avoir été traduits en minuscules. On peut dire que le type prend un flux de lignes en entrée et produit un autre flux de lignes en sortie, comme illustré à la figure 1.2. Notez que dans Unix, les commandes (cat, tr, sort et tail) sont exécutées simultanément, de sorte que le tri puisse traiter les premières lignes avant que cat ou tr ne soit terminé. Une analogie plus mécanique est une ligne d’assemblage de voiture où un flux de voitures est mis en file d’attente entre les stations de traitement qui prennent chacune une voiture, la modifie et la transmette à la station suivante pour un traitement ultérieur ; le traitement dans des stations séparées est généralement simultané même si la ligne de montage est physiquement une séquence.

Java 8 ajoute une API Streams (notez le S majuscule) dans java.util.stream dans ce sens ; Le Stream<T> est une séquence d’éléments de type T. Vous pouvez le considérer comme un itérateur fantaisiste pour l’instant. L’API Streams comporte de nombreuses méthodes qui peuvent être enchaînées pour former un pipeline complexe, tout comme les commandes Unix ont été enchaînées dans l’exemple précédent. La motivation principale pour cela est que vous pouvez maintenant programmer dans Java 8 avec un niveau d’abstraction plus élevé, en structurant votre façon de penser dans la gestion des flux de données : par exemple transformer un flux de donnés d’un type à un autre (de la même façon que vous réfléchissez lors de l’écriture de requêtes de base de données), plutôt qu’un élément à la fois, (dans une boucle for). Un autre avantage est que Java 8 peut exécuter de manière transparente votre pipeline d’opérations de Stream sur plusieurs cœurs CPU : il s’agit là de la gestion de façon transparente du parallélisme de java 8. Nous évitant ainsi de travailler directement avec des Threads. Je couvrirai l’API Java 8 Streams en détail dans les chapitres 4-7.

 

1.1.3. Passer le code aux méthodes avec paramétrage du comportement

Le deuxième concept de programmation ajouté à Java 8 est la possibilité de passer du code à une API. Cela semble terriblement abstrait. Pourtant dans Unix, vous pouvez utiliser la commande de tri de façon personnalisé. Bien que la commande de tri supporte des paramètres de ligne de commande pour effectuer différents types de tri prédéfinis tels que l’ordre inversé, ceux-ci restent limités.

Par exemple, disons que vous avez une collection d’identifiants de facture avec un format similaire à 2013UK0001, 2014US0002, …. Les quatre premiers chiffres représentent l’année, les deux lettres suivantes, un code de pays et les quatre derniers chiffres, l’ID d’un client. Vous voudrez peut-être trier ces ID de facture par année ou peut-être utiliser l’ID de client ou même le code de pays. Ce que vous voulez vraiment, c’est la capacité de dire à la commande de tri de prendre comme argument une commande définie par l’utilisateur : un code distinct passé à la commande de tri.

De même en Java, il est possible de vouloir mettre en place un tri personnalisé. Vous pouvez écrire une méthode compareUsingCustomerId pour comparer deux ID de facture mais, avant Java 8, vous ne pouviez pas passer cette méthode à une autre méthode ! Vous pouviez à la place créer un objet Comparator et lui passer l’implémentation de tri voulue, comme nous l’avons montré au début de ce chapitre, mais cela est très verbeux. Java 8 ajoute donc la possibilité de passer des méthodes (votre code) comme arguments à d’autres méthodes. La figure 1.3, basée sur la figure 1.2, illustre cette idée.

Je résumerai comment cela fonctionne dans la section 1.2 de ce chapitre, mais laisserai les détails complets aux chapitres 2 et 3. Les chapitres 13 et 14 examineront des choses plus avancées que vous pouvez faire en utilisant cette fonctionnalité, avec des techniques inhérentes de programmation fonctionnelle.

1.1.4. Parallélisme et partage de données modifiables

Le troisième concept de programmation est plutôt plus implicite et résulte de l’expression « parallélisme presque gratuit » dans notre discussion précédente sur le traitement des Stream. Quelles modifications aurez-vous à faire ? Vous devrez peut-être apporter de petits changements dans la façon dont vous codez les fonctions que vous passez aux méthodes. Au début, ces changements peuvent vous sembler bizarre, mais une fois que vous vous habituerez, vous les aimerez. Le principe est de fournir un comportement qui est sûr d’exécuter simultanément les différentes parties du programme. En règle générale, cela signifie écrire un code qui n’accède pas aux données modifiables partagées pour faire son travail. Parfois, on parle de fonctions pures ou de fonctions sans effets de bord ou de fonctions apatrides, et j’en parlerai en détail dans les chapitres 7 et 13. Le parallélisme définit de cette façon n’est légitime qu’en supposant que plusieurs copies de votre code peuvent fonctionner indépendamment. S’il y a une variable ou un objet partagé, qui est utilisé, les choses ne fonctionnent plus : que faire si deux processus veulent modifier la variable partagée en même temps ? (La section 1.3 donne une explication plus détaillée avec un diagramme).

Les Stream de Java 8 exploitent le parallélisme plus facilement que l’API Threads existante de Java, donc, bien qu’il soit possible d’utiliser le mot clé synchronised de java pour gérer le fait que les données partagées soient modifiables, java 8 combat cette pratique. En effet l’utilisation du mot clé synchronised sur plusieurs noyaux de traitement est souvent beaucoup plus coûteuse qu’on ne le pense, car la synchronisation oblige le code à s’exécuter de manière séquentielle, ce qui va à l’encontre du but premier du parallélisme.

Ces 2 points, à savoir :

  • Aucune donnée mutable partagée
  • La possibilité de passer des fonctions à d’autres méthodes

Sont les pierres angulaires de ce qui est généralement décrit comme le paradigme de la programmation fonctionnelle, que vous verrez en détail dans les chapitres 13 et 14. En revanche, dans le paradigme de programmation impératif, vous décrivez généralement un programme en fonction d’une séquence d’énoncés qui modifient l’état.

Le fait d’exiger qu’une donnée partagée ne soit pas modifiable signifie qu’une méthode est décrite uniquement par la façon dont elle transforme les arguments qu’elle reçoit en résultats ; en d’autres termes, elle se comporte comme une fonction mathématique et sans effets de bord.

1.1.5. Java doit évoluer

Vous avez déjà vu des évolutions dans Java. Par exemple, l’introduction de génériques et l’utilisation de List<String> au lieu de juste List, peut avoir été déroutante au début. Mais vous connaissez maintenant ce style et les avantages qu’il apporte (corriger plus d’erreurs au moment de la compilation et rendre le code plus facile à lire, car vous savez directement quel est le type de la liste).

D’autres changements ont rendu les taches communes plus faciles à implémenter, par exemple, en utilisant une boucle for au lieu d’exposer l’utilisation d’un Iterator. Les principaux changements de Java 8 reflètent une migration de l’orienté objet classique, qui lui se concentre souvent sur la mutation des valeurs existantes, vers le spectre de programmation fonctionnel dans lequel ce qui importe est « ce que » vous voulez faire en termes généraux et pas « comment vous pouvez atteindre cet objectif. Notez que la programmation classique orientée objet et la programmation fonctionnelle, peuvent sembler être en conflit. Mais l’idée est d’obtenir le meilleur des deux paradigmes de programmation, en fonction du besoin ! Nous en discuterons en détail dans les 2 prochaines sections : Les fonctions en java et les nouveautés de l’API Stream.

Une ligne à retenir est peut-être la suivante : les langages doivent évoluer pour suivre l’évolution des attentes matérielles ou des programmeurs (si vous avez besoin d’un exemple, considérez que COBOL était une fois l’un des langages les plus importantes dans le commerce). Pour perdurer, Java doit évoluer en ajoutant de nouvelles fonctionnalités. Cette évolution sera inutile à moins que les nouvelles fonctionnalités ne soient utilisées, alors, en utilisant Java 8, vous protégez votre mode de vie en tant que programmeur Java. En plus, je suis sûre que nous allons tous aimer utiliser les nouvelles fonctionnalités de Java 8. Demandez à quiconque qui a utilisé Java 8 s’il est prêt à revenir en arrière ! En outre, les nouvelles fonctionnalités de Java 8 pourraient, dans l’analogie de l’écosystème, permettre à Java de conquérir le territoire de programmation-task actuellement occupé par d’autres langages, de sorte que les programmeurs Java 8 soient encore plus demandés.

Nous présentons maintenant les nouveaux concepts dans Java 8, un par un.

1.2. Fonctions en Java

Le mot function dans les langages de programmation est couramment utilisée comme synonyme de méthode, en particulier une méthode statique ; Elles sont souvent comparées à des fonctions mathématiques, qui sont sans effets secondaires. Heureusement, comme vous le verrez, lorsque Java 8 se réfère à des fonctions, cette comparaison coïncide presque.

Java 8 ajoute des fonctions comme nouvelles formes de valeurs. Celles-ci facilitent l’utilisation de Streams, (couvert dans la section 1.3), que Java 8 fournit pour exploiter la programmation parallèle sur les processeurs multicore. Je commencerai par montrer que les fonctions en tant que valeurs sont utiles en elles-mêmes.

Pensez aux valeurs possibles manipulées par les programmes Java. Tout d’abord, il existe des valeurs primitives telles que 42 (de type int) et 3.14 (de type double). Deuxièmement, les valeurs peuvent être des objets (plus strictement, des références à des objets). La seule façon d’obtenir l’une d’entre elles est d’utiliser le mot clé new, peut-être par l’intermédiaire d’une méthode factory ou la fonction d’une bibliothèque ; les références d’objet pointent sur les instances d’une classe. Les exemples incluent « abc » (de type String), New Integer (1111) (de type Integer) et new HashMap <Integer, String> (100) via l’appel explicite d’un constructeur de la classe HashMap. Même les tableaux sont des objets. Où se trouve donc le problème ?

Pour répondre à cette question, nous noterons que le but premier de tout langage de programmation est de manipuler des valeurs qui, en suivant la tradition historique du langage de programmation, sont donc appelées valeurs de première classe (dans le jargon informatique, on dit citoyen de première classe). D’autres structures dans nos langages de programmation, qui peuvent nous aider à exprimer la structure des valeurs mais qui ne peuvent pas être transmises pendant l’exécution du programme, sont des citoyens de deuxième classe. Les valeurs énumérées précédemment (Integer, String) sont des citoyens Java de première classe, mais divers autres concepts Java, tels que les méthodes et les classes, illustrent les citoyens de deuxième classe. Les méthodes sont bien utilisées pour définir des classes qui, à leur tour, peuvent être instanciées pour produire des valeurs, mais elles ne sont pas non plus des valeurs elles-mêmes. Cela importe-t-il donc ? Oui, il s’avère que le fait de pouvoir passer des méthodes au moment de l’exécution, et donc d’en faire des citoyens de première classe, est très utile dans la programmation, et les concepteurs Java 8 ont ajouté cette capacité à Java. Par ailleurs, vous pourriez vous demander si transformer d’autres citoyens de deuxième classe comme les classes en citoyens de première classe pourrait être une bonne idée. Plusieurs langages de programmation ont exploré cette piste : Smalltalk et JavaScript.

1.2.1. Les méthodes et lambdas comme citoyens de première classe

Les expériences dans d’autres langages telles que Scala et Groovy ont démontré que permettre aux méthodes d’être utilisés comme valeurs de première classe rendait la programmation plus facile. Et une fois que les programmeurs se familiarisent avec une fonctionnalité puissante, ils deviennent réticents à utiliser des langages sans ça ! Ainsi, les concepteurs de Java 8 ont décidé de permettre aux méthodes d’être des valeurs, afin de vous faciliter la programmation. De plus, la fonctionnalité Java 8, des méthodes comme valeurs est à la base de diverses autres fonctionnalités Java 8 (telles que Streams).

La première nouvelle caractéristique Java 8 que nous présentons est celle des références de méthodes. Supposons que vous souhaitez filtrer tous les fichiers cachés dans un répertoire. Vous devez commencer à écrire une méthode qui, selon un fichier donné, vous dira s’il est caché ou non. Heureusement, il existe une telle méthode dans la classe File appelée isHidden. Elle peut être considérée comme une fonction qui prend un fichier et renvoie un booléen. Mais pour l’utiliser pour le filtrage, vous devez l’envelopper dans un objet FileFilter que vous passerez ensuite à la methode File.listFiles. Comme ceci :

 

C’est horrible ! Bien qu’il ne s’agisse que de trois lignes, il s’agit de trois lignes opaques. Vous avez déjà une méthode isHidden que vous pourriez utiliser. Pourquoi devez-vous l’envelopper dans une classe verbeuse de FileFilter, pour ensuite l’instancier ? Parce que c’est ce que vous aviez à faire avant Java 8 ! Maintenant, en Java 8, vous pouvez :

N’est-ce pas cool ? Vous avez déjà la fonction isHidden disponible, vous l’envoyez simplement à la méthode listFiles en utilisant la référence de méthode Java 8 : syntaxe (c’est-à-dire « utiliser cette méthode comme valeur. J’expliquerai plus tard comment fonctionnent ce mécanisme. Un avantage est que votre code se lit maintenant plus facilement, vu qu’il traduit directement l’énoncé du problème. Voici un aperçu de ce qui arrive avec Java 8 : les méthodes ne sont plus des citoyennes de seconde classe. De la même façon que vous utilisez une référence d’objet lorsque vous passez un objet (et les références d’objet sont créées par new), dans Java 8 lorsque vous écrivez File::isHidden, vous créez une référence de méthode, qui peut également être transmise. Ce concept est décrit en détail dans le chapitre 3. Étant donné que les méthodes contiennent du code (le corps exécutable d’une méthode), les références de méthodes permettent d’utiliser le code de passage comme dans la figure 1.3. La figure 1.4 illustre le concept. Vous verrez également un exemple concret (en sélectionnant les pommes dans un inventaire) :

En plus de permettre aux méthodes (nommées) d’être des valeurs de première classe, Java 8 permet aussi de travailler avec les fonctions en tant que valeurs, y compris lambdas (ou fonctions anonymes). Par exemple, vous pouvez maintenant écrire (int x) -> x + 1 pour signifier « la fonction qui, lorsqu’elle est appelée avec l’argument x, renvoie la valeur x + 1. » Vous pourriez vous demander pourquoi cela est nécessaire car vous pouvez définir une méthode add1 dans une classe MyMathsUtils puis écrire MyMathsUtils::add1! Oui, vous pourriez, mais la nouvelle syntaxe lambda est plus concise pour les cas où vous ne disposez pas d’une méthode et d’une classe appropriées. Le chapitre 3 explore en détail les lambdas. Les programmes utilisant ces concepts sont censés être écrits dans le style de programmation fonctionnelle : cette phrase signifie « écrire des programmes qui passent des fonctions en tant que valeur de première classe ».

1.2.2. Passer du code: un exemple

Regardons un exemple (décrit plus en détail au chapitre 2, « Passer des fonctions en paramètre ») sur la façon dont cette fonctionnalité vous aide à écrire du code de meilleure qualité. Tout le code pour les exemples est disponible sur la page GitHub du livre (https://github.com/ftounga/java8features). Supposons que vous ayez une classe Apple avec une méthode getColor et un inventaire contenant une liste de pommes ; Vous voudrez par exemple sélectionner toutes les pommes de couleur verte et les retourner dans une liste. Avant Java 8, vous auriez écrit cette méthode :

Sauf que cette fois vous voudrez recupérer plutôt les pommes avec un poids supérieur à 150. Vous aller surement faire un copier coller du code précédent et changer juste le test fait dans le bloc if :

Nous connaissons tous les dangers du copier-coller dans l’ingénierie logicielle (la mise à jour ou correction de bug qui n’est pas reporté partout). Ces deux méthodes ne varient que sur une seule ligne : la condition en surbrillance à l’intérieur du bloc if. Si la différence entre les deux méthodes dans le code surligné était simple, comme par exemple récupérer les pommes qui ont un poids compris dans un certain intervalle, cela aurait été acceptable, vous auriez pu juste passer la valeur des limites en paramètre(0.80g<x<150g). Mais ici on voit bien que les conditions de filtrage sont totalement différentes : l’une est faite sur la couleur et l’autre sur le poids. Heureusement Java 8 permet de passer des fonctions en paramètre, évitant ainsi la duplication. Vous pouvez plutôt écrire ceci :

Et pour utiliser ces fonctions, vous pouvez écrire soit:

ou

J’expliquerai comment cela fonctionne dans les 2 prochains chapitres. L’idée principale à retenir ici pour le moment est qu’il est possible de passer des fonctions en paramètre en Java 8 !



Qu’est-ce qu’un prédicat?

Le code précédent a passé la méthode Apple :: isGreenApple (qui prend un Apple en argument et renvoie un boolean) à filterApples, qui elle même attendait un paramètre Predicate- <Apple>. Le mot prédicat est souvent utilisé en mathématiques pour signifier une fonction qui prend une valeur en argument et renvoie vrai ou faux. Vous verrez plus loin, que Java 8 vous permet également d’écrire la fonction <Apple, Boolean> – mais l’utilisation de Predicate <Apple> est plus standard (et légèrement plus efficace car il permet d’éviter l’auto-boxing entre une Booléen et un booléen.

 



1.2.3 Passer des méthodes aux Lambdas

Passer des méthodes comme des paramètres est clairement utile, mais il est un peu ennuyeux d’avoir à écrire une définition pour des méthodes courtes telles que isHeavyApple et isGreenApple lorsqu’elles ne sont utilisées qu’une ou deux fois. Mais Java 8 a résolu cela aussi. Il introduit une nouvelle notation (fonctions anonymes, ou lambdas) qui vous permet d’écrire juste :

ou

ou même

Vous n’avez même plus besoin d’écrire une définition de méthode qui ne sera utilisée qu’une seule fois. En plus le code est plus clair et concis parce que vous n’avez pas besoin de chercher le comportement que vous aller passez. Mais si une telle lambda dépasse de quelques lignes (de sorte que son comportement ne soit pas instantanément clair), vous devriez plutôt utiliser une référence à une méthode avec un nom descriptif au lieu d’utiliser un lambda anonyme. La clarté du code devrait être votre but ultime.

Les concepteurs de Java 8 auraient presque pu s’arrêter ici, et peut-être l’auraient-ils fait avant les processeurs multicœurs ! La programmation de style fonctionnel présentée jusqu’ici s’avère plutôt puissante, comme vous pouvez le constater. Java aurait pu être amélioré en rajoutant juste un filtre et quelques méthodes génériques telles que :

 

En conséquence, vous n’auriez même plus à écrire des méthodes comme filterApples. Par exemple pour l’appel précédent :

on aurait plutôt:

Mais, pour des raisons axées sur une meilleure exploitation du parallélisme, les concepteurs ne l’ont pas fait. Java 8 contient une toute nouvelle API de Collections appelée Streams. Elle contient un ensemble d’opérations semblables au filtre que les programmeurs fonctionnels peuvent connaître (par exemple, map, reduce), ainsi que des méthodes de conversion entre Collections et Streams, ce que nous verrons maintenant.

1.3. Streams

Presque chaque application Java crée et traite des collections. Mais travailler avec des collections n’est pas toujours simple. Par exemple, disons que vous devez filtrer les transactions coûteuses à partir d’une liste, puis les regrouper par devise. Vous devriez écrire beaucoup de code « boilerplate » pour la mise en œuvre de ce traitement.

En plus il est difficile de comprendre en un coup d’œil ce que fait le code étant donné qu’il y a beaucoup de blocs d’instructions imbriqués. En utilisant l’API Stream le problème peut être réglé de la façon suivante :

Ne vous inquiétez pas pour ce code pour l’instant car il peut sembler un peu tricky pour le moment. Les chapitres 4-7 sont consacrés à expliquer comment comprendre l’API Streams. Pour l’instant, il convient de noter que l’API Streams offre un moyen très différent de traiter les données par rapport à l’API Collections. En utilisant une collection, vous gérez vous-même le processus d’itération. Vous devez effectuer une itération à travers chaque élément un par un en utilisant une boucle, puis traiter les éléments. En revanche, en utilisant l’API Streams, vous n’avez pas besoin de penser en termes de boucles du tout. Le traitement des données se produit à l’intérieur de la bibliothèque. Nous appelons cette idée d’itération interne. Nous y reviendrons au chapitre 4.

Le deuxième point de difficulté lorsque vous travaillez avec les collections, est la façon de traiter les données si vous en avez un grand nombre ; Un CPU unique ne serait pas capable de traiter cette énorme quantité de données, mais vous avez probablement un ordinateur multicœur sur votre bureau. Idéalement, vous souhaiterez partager le travail entre les différents cœurs de CPU disponibles sur votre machine pour réduire le temps de traitement. En effet, si vous avez huit cœurs, ils devraient être en mesure de traiter les données 8 fois plus vite.

 



Multicore

Tous les nouveaux ordinateurs de bureau et portables sont des ordinateurs multicœurs. Au lieu d’un seul CPU, ils en ont quatre, huit CPU ou même plus (généralement appelés noyaux). Le problème est qu’un programme Java classique n’utilise qu’un seul de ces noyaux, la capacité de calcul des autres CPU est donc négligée. De même, de nombreuses entreprises utilisent des clusters (ordinateurs reliés par des réseaux rapides) pour pouvoir traiter efficacement de grandes quantités de données. Java 8 apporte de nouveaux styles de programmation pour mieux exploiter ces ordinateurs. Le moteur de recherche de Google est un exemple de code trop gros pour fonctionner sur un seul ordinateur. Il lit chaque page sur Internet et crée un index, reliant chaque mot apparaissant sur n’importe quelle page Internet à chaque URL contenant ce mot. Ensuite, lorsque vous effectuez une recherche Google impliquant plusieurs mots, le logiciel peut rapidement utiliser cet index pour vous donner un ensemble de pages Web contenant ces mots. Essayez d’imaginer comment coder cet algorithme en Java (même pour un index plus petit que celui géré par Google).



1.3.1. Le multithreading est difficile

Le problème est que l’exploitation du parallélisme en écrivant du code multithread (en utilisant l’API Threads des versions précédentes de Java) est difficile. Vous devez penser différemment : les threads peuvent accéder et mettre à jour les variables partagées en même temps. En conséquence, les données pourraient changer de façon inattendue, si elles ne sont pas coordonnées correctement (avec le mot clé synchronized). Ce modèle est plus difficile à mettre en place qu’un modèle séquentiel étape par étape. Par exemple, la figure 1.5 montre un problème possible avec deux threads essayant d’ajouter un nombre à une variable partagée sum, s’ils ne sont pas correctement synchronisés.

Java 8 prend également en charge les deux problèmes (boilerplate, la loudeur lié au traitement des collections et les difficultés pour tirer avantage du parallélisme) avec l’API Streams (java.util.stream). Le premier motif de conception est qu’il existe de nombreux modèles de traitement de données (similaires à filterApples de la section précédente, ou des opérations familières dans les langages de requête de base de données comme SQL) qui sont utilisées encore et encore et qui bénéficieraient à faire partie d’une bibliothèque: le filtrage des données sur la base d’un critère (par exemple, les pommes lourdes), l’extraction de données (par exemple, extraction du poids de chaque pomme dans une liste) ou le regroupement de données (par exemple, regroupement d’une liste de nombres dans des listes  avec les nombres paires d’un côté et les impairs de l’autre), et ainsi de suite. Le deuxième facteur de motivation est que de telles opérations peuvent souvent être parallélisées. Par exemple, comme illustré sur la figure 1.6, le filtrage d’une liste sur deux CPU peut être fait en demandant à un CPU de traiter la première moitié d’une liste et le deuxième CPU de traiter l’autre moitié. On appelle cette partie le forking step (1). Les CPU filtrent ensuite leurs demi-listes respectives (2). Enfin (3), un CPU joindra les deux résultats.

 

Pour l’instant, nous dirons simplement que la nouvelle API Streams se comporte de manière très similaire à l’API Collections existante de Java : les deux permettent l’accès à des séquences de données. Mais il est utile pour l’instant de garder à l’esprit que l ‘API Collections se concentre principalement sur le stockage et l’accès aux données, alors que Streams est principalement sur la description des calculs effectués sur les données. Le point clé ici est que Streams permet et encourage les éléments d’un flux à être traités en parallèle. Bien que cela puisse sembler étrange au début, le moyen le plus rapide de filtrer une collection (en utilisant filterApples sur une liste dans la section précédente) est de la convertir en Stream, de la traiter en parallèle et de la reconvertir en List comme montré dans les 2 cas ci-dessous :  en série et parallèles. Encore une fois, nous allons juste dire « parallélisme presque gratuit » et fournir un avant-goût de la façon dont vous pouvez filtrer les pommes lourdes d’une liste séquentiellement ou en parallèle en utilisant l’API Streams et une expression lambdas :

Traitement séquentiel :

Traitement en parallèle:

Le chapitre 7 explore le traitement parallèle des données dans Java 8 et ses performances plus en détail. L’un des problèmes pratiques que les développeurs Java 8 ont révélé dans l’évolution de Java avec toutes ces nouveautés était l’évolution des interfaces existantes. Par exemple, la méthode Collections.sort appartient vraiment à l’interface List mais n’a jamais été incluse. Idéalement, vous souhaitez faire list.sort (comparator) au lieu de Collections.sort (liste, comparateur). Cela peut sembler banal mais, avant Java 8, vous ne pouvez mettre à jour une interface que si vous mettez à jour toutes les classes qui l’implémentent : un cauchemar logistique ! Ce problème a été résolu en Java 8 avec l’arrivée des méthodes par défaut dans les interfaces.



Parallélisme en Java : aucun état modifiable partagé

Les gens ont toujours dit que le parallélisme en Java est difficile, et que tout ce qui concerne la synchronisation est sujet aux erreurs. Où est la baguette magique dans Java 8? Il y a en fait deux. Tout d’abord, la bibliothèque gère le partitionnement – décomposant un grand flux en plusieurs flux plus petits pour être traités en parallèle pour vous. Deuxièmement, ce parallélisme ne fonctionne que si les méthodes transmises aux méthodes de l’API Stream, comme la méthode filter, n’interagissent pas, par exemple, en ayant des objets partagés modifiables. Mais il s’avère que cette restriction semble tout à fait naturelle en tant que codeur (voir, par exemple, notre exemple Apple :: isGreenApple). En effet, bien que le sens premier du mot « fonctionnelle » dans la programmation fonctionnelle signifie «utiliser des fonctions comme valeurs de première classe», il a souvent une nuance secondaire de «pas d’interaction pendant l’exécution.



1.4. Méthodes par défaut

Les méthodes par défaut sont ajoutées à Java 8 en grande partie pour permettre aux concepteurs de bibliothèques d’écrire des interfaces plus évolutives. Elles sont importantes étant donné que vous les rencontrerez de plus en plus dans les interfaces. Mais vu que relativement peu de programmeurs auront besoin d’écrire eux-mêmes des méthodes par défaut et qu’elles sont là pour faciliter l’évolution des interfaces plutôt que d’aider à écrire un programme particulier, je ne m’attarderai pas sur le sujet et vous montrerai juste des exemples :

Dans la section 1.3, je vous ai présenté le bout de code suivant :

Mais il y a un problème : une Liste <T> antérieure à Java 8 ne dispose pas de méthodes Stream ou de parallèle Stream – et l’interface Collection <T> qu’elle implémente non plus- car ces méthodes n’avaient pas été conçues ! Et sans ces méthodes, ce code ne sera pas compilé. La solution la plus simple que vous auriez utilisée pour vos propres interfaces aurait été que les concepteurs Java 8 ajoutent simplement la méthode stream à l’interface Collection et ajoutent l’implémentation dans la classe ArrayList.

Mais faire cela aurait été un cauchemar pour les utilisateurs. Il existe de nombreux Framework alternatifs qui implémentent des interfaces à partir de l’API Collections. Ajouter une nouvelle méthode à une interface signifie que toutes les classes concrètes doivent fournir une implémentation. Les concepteurs de langage n’ont aucun contrôle sur toutes les implémentations existantes de Collections, donc on se serait retrouvés devant un petit dilemme : comment faire évoluer les interfaces publiées sans casser les implémentations existantes de ladite interface ?

La solution Java 8 est de briser le dernier lien : une interface peut maintenant contenir des signatures de méthodes pour lesquelles les classe qui l’implémente ne fournisse pas d’implémentations pour cette méthode ! Alors qui les implémente ? Les implémentations sont directement renseignées au niveau de l’interface (on parle d’implémentations par défaut) plutôt que dans la classe d’implémentation. Cela permet à un concepteur d’interface d’agrandir une interface au-delà des méthodes initialement planifiées, sans rompre le code existant. Java 8 utilise le nouveau mot-clé default dans la spécification d’interface pour y parvenir. Par exemple, dans Java 8, vous pouvez maintenant appeler la méthode sort directement dans une liste. Ceci est rendu possible par la méthode par défaut suivante dans List, qui appelle la méthode statique Collections.sort:

Cela signifie que toutes les classes concrètes de List ne doivent pas implémenter explicitement la méthode sort, alors que dans les versions antérieures de Java, ces classes concrètes ne parviendraient pas à recompiler, sauf si elles fournissaient une implémentation.

Mais attendez une seconde, une seule classe peut implémenter plusieurs interfaces, n’est-ce pas ? Donc, si vous avez plusieurs implémentations par défaut dans plusieurs interfaces, cela signifie-t-il que vous avez une forme d’héritage multiple en Java ? Oui, dans une certaine mesure ! Nous montrons dans le chapitre 9 qu’il existe certaines restrictions qui empêchent des problèmes tels que le fameux de l’héritage en diamant de C++

1.5. D’autres bénéfices de la programmation fonctionnelle

Les sections précédentes introduisaient deux idées fondamentales issues de la programmation fonctionnelle qui font maintenant partie de Java : l’utilisation de méthodes et de lambdas comme valeurs de premier ordre et que des fonctions et méthodes peuvent être exécutés en parallèle en l’absence d’utilisation de valeurs partagées modifiables. Ces deux idées sont exploitées par la nouvelle API Streams que nous avons décrite précédemment. Les langages fonctionnels communs (SML, OCaml, Haskell) fournissent également d’autres fonctionnalités pour aider les programmeurs. L’une d’elles permet d’éviter le null par l’utilisation explicite de types de données plus descriptifs. 

Dans Java 8, il existe une classe Optional<T> qui, si elle est utilisée régulièrement, peut vous aider à éviter les exceptions NullPointer. C’est un conteneur qui peut contenir ou non une valeur. Optional<T> inclut des méthodes pour traiter explicitement le cas où une valeur est absente et, par conséquent, vous pouvez éviter les exceptions NullPointer. J’en parlerai plus en détail dans le chapitre 10.

1.6. Résumé

Voici les principaux concepts que vous devriez retenir de ce chapitre :

  • Gardez à l’esprit l’idée d’un écosystème des langages et la pression d’évoluer qui en resulte. Bien que Java puisse être extrêmement compétitif en ce moment, rappelez vous d’autres langages tels que COBOL qui n’ont pas évolué.
  • Les ajouts de base à Java 8 fournissent de nouveaux concepts et fonctionnalités passionnantes pour faciliter l’écriture de programmes à la fois efficaces et concis.
  • Les processeurs multicœurs ne sont pas entièrement pris en charge par la pratique de programmation Java existante.
  • Les fonctions sont des valeurs de première classe ; rappelez-vous comment les méthodes peuvent être transmises en tant que valeurs fonctionnelles et comment les fonctions anonymes (lambdas) sont écrites.
  • Le concept de Streams de Java 8 généralise de nombreux aspects des collections, mais permet à la fois un code plus lisible et permet de traiter en parallèle les éléments d’un flux de données.
  • Vous pouvez utiliser une méthode par défaut dans une interface pour fournir un corps de méthode si une classe d’implémentation choisit de ne pas le faire.
  • D’autres idées intéressantes de la programmation fonctionnelle incluent le traitement de la valeur nulle et l’utilisation du pattern matching.

 

     Précédent                                                                                                   Suivant