Gestion des cartes dans Groovy vs Java

Java est un excellent langage de programmation, mais parfois je veux un langage de type Java qui soit juste un peu plus flexible et compact. C’est alors que j’opte pour Sensationnel.
Dans un article récent, j’ai passé en revue certaines des différences entre créer et initialiser des cartes dans Groovy et faire la même chose en Java. En bref, Groovy a une syntaxe concise pour configurer des cartes et accéder aux entrées de carte par rapport à l’effort nécessaire en Java.
Cet article approfondira davantage les différences de gestion des cartes entre Groovy et Java. Pour cela, j’utiliserai le exemple de tableau des employés utilisé pour la démonstration de la bibliothèque JavaScript DataTables. Pour continuer, commencez par vous assurer que les versions récentes de Groovy et Java sont installées sur votre ordinateur.
Contents
Installer Java et Groovy
Groovy est basé sur Java et nécessite également une installation Java. Une version récente et/ou décente de Java et de Groovy se trouve peut-être déjà dans les dépôts de votre distribution Linux, ou vous pouvez télécharger et installer Groovy à partir du Site Web d’Apache Groovy. Une bonne option pour les utilisateurs de Linux est SDKMan, qui peut être utilisé pour obtenir plusieurs versions de Java, Groovy et de nombreux autres outils connexes. Pour cet article, j’utilise les versions du SDK de :
- Java : version 11.0.12-open d’OpenJDK 11
- Groovy : version 3.0.8.
Retour au problème : les cartes
Premièrement, d’après mon expérience, les cartes et les listes (ou du moins les tableaux) se retrouvent souvent dans le même programme. Par exemple, le traitement d’un fichier d’entrée est très similaire au passage d’une liste ; souvent, je le fais lorsque je veux catégoriser les données rencontrées dans le fichier d’entrée (ou la liste), en stockant une sorte de valeur dans des tables de recherche, qui ne sont que des cartes.
Deuxièmement, Java 8 a introduit toute la fonctionnalité Streams et les lambdas (ou fonctions anonymes). D’après mon expérience, la conversion de données d’entrée (ou de listes) en cartes implique souvent l’utilisation de Java Streams. De plus, Java Streams est le plus flexible lorsqu’il s’agit de flux d’objets typés, offrant des fonctionnalités de regroupement et d’accumulation prêtes à l’emploi.
Traitement de la liste des employés en Java
Voici un exemple concret basé sur ces dossiers d’employés fictifs. Vous trouverez ci-dessous un programme Java qui définit une classe Employee pour contenir les informations sur les employés, crée une liste d’instances Employee et traite cette liste de différentes manières :
1 import java.lang.*;
2 import java.util.Arrays;
3 import java.util.Locale;
4 import java.time.format.DateTimeFormatter;
5 import java.time.LocalDate;
6 import java.time.format.DateTimeParseException;
7 import java.text.NumberFormat;
8 import java.text.ParseException;
9 import java.util.stream.Collectors;
10 public class Test31 {
11 static public void main(String args[]) {
12 var employeeList = Arrays.asList(
13 new Employee("Tiger Nixon", "System Architect",
14 "Edinburgh", "5421", "2011/04/25", "$320,800"),
15 new Employee("Garrett Winters", "Accountant",
16 "Tokyo", "8422", "2011/07/25", "$170,750"),
...
81 new Employee("Martena Mccray", "Post-Sales support",
82 "Edinburgh", "8240", "2011/03/09", "$324,050"),
83 new Employee("Unity Butler", "Marketing Designer",
84 "San Francisco", "5384", "2009/12/09", "$85,675")
85 );
86 // calculate the average salary across the entire company
87 var companyAvgSal = employeeList.
88 stream().
89 collect(Collectors.averagingDouble(Employee::getSalary));
90 System.out.println("company avg salary = " + companyAvgSal);
91 // calculate the average salary for each location,
92 // compare to the company average
93 var locationAvgSal = employeeList.
94 stream().
95 collect(Collectors.groupingBy((Employee e) ->
96 e.getLocation(),
97 Collectors.averagingDouble(Employee::getSalary)));
98 locationAvgSal.forEach((k,v) ->
99 System.out.println(k + " avg salary = " + v +
100 "; diff from avg company salary = " +
101 (v - companyAvgSal)));
102 // show the employees in Edinburgh approach #1
103 System.out.print("employee(s) in Edinburgh (approach #1):");
104 var employeesInEdinburgh = employeeList.
105 stream().
106 filter(e -> e.getLocation().equals("Edinburgh")).
107 collect(Collectors.toList());
108 employeesInEdinburgh.
109 forEach(e ->
110 System.out.print(" " + e.getSurname() + "," +
111 e.getGivenName()));
112 System.out.println();
113 // group employees by location
114 var employeesByLocation = employeeList.
115 stream().
116 collect(Collectors.groupingBy(Employee::getLocation));
117 // show the employees in Edinburgh approach #2
118 System.out.print("employee(s) in Edinburgh (approach #2):");
119 employeesByLocation.get("Edinburgh").
120 forEach(e ->
121 System.out.print(" " + e.getSurname() + "," +
122 e.getGivenName()));
123 System.out.println();
124 }
125 }
126 class Employee {
127 private String surname;
128 private String givenName;
129 private String role;
130 private String location;
131 private int extension;
132 private LocalDate hired;
133 private double salary;
134 public Employee(String fullName, String role, String location,
135 String extension, String hired, String salary) {
136 var nn = fullName.split(" ");
137 if (nn.length > 1) {
138 this.surname = nn[1];
139 this.givenName = nn[0];
140 } else {
141 this.surname = nn[0];
142 this.givenName = "";
143 }
144 this.role = role;
145 this.location = location;
146 try {
147 this.extension = Integer.parseInt(extension);
148 } catch (NumberFormatException nfe) {
149 this.extension = 0;
150 }
151 try {
152 this.hired = LocalDate.parse(hired,
153 DateTimeFormatter.ofPattern("yyyy/MM/dd"));
154 } catch (DateTimeParseException dtpe) {
155 this.hired = LocalDate.EPOCH;
156 }
157 try {
158 this.salary = NumberFormat.getCurrencyInstance(Locale.US).
159 parse(salary).doubleValue();
160 } catch (ParseException pe) {
161 this.salary = 0d;
162 }
163 }
164 public String getSurname() { return this.surname; }
165 public String getGivenName() { return this.givenName; }
166 public String getLocation() { return this.location; }
167 public int getExtension() { return this.extension; }
168 public LocalDate getHired() { return this.hired; }
169 public double getSalary() { return this.salary; }
170 }
Wow, c’est beaucoup de code pour un simple programme de démonstration ! Je vais d’abord le parcourir en morceaux.
À partir de la fin, les lignes 126 à 170 définissent le Employee
classe utilisée pour stocker les données des employés. La chose la plus importante à mentionner ici est que les champs de la fiche de l’employé sont de types différents, et en Java cela conduit généralement à définir ce type de classe. Vous pouvez rendre ce code un peu plus compact en utilisant @Data du projet Lombok annotation pour générer automatiquement les getters (et setters) pour le Employee
classer. Dans les versions plus récentes de Java, je peux déclarer ce genre de choses comme un enregistrement plutôt qu’une classe, puisque le but est de stocker des données. Stockage des données sous forme de liste de Employee
instances facilite l’utilisation des flux Java.
Les lignes 12 à 85 créent la liste des Employee
instances, alors maintenant vous avez déjà traité 119 des 170 lignes.
Il y a neuf lignes d’instructions d’importation au début. Fait intéressant, il n’y a pas d’importations liées à la carte ! C’est en partie parce que j’utilise des méthodes de flux qui produisent des cartes comme résultats, et en partie parce que j’utilise le var
mot clé pour déclarer des variables, de sorte que le type est déduit par le compilateur.
Les parties intéressantes du code ci-dessus se produisent dans les lignes 86 à 123.
Aux lignes 87-90, je convertis employeeList
dans un flux (ligne 88) puis utilisez collect()
pour appliquer le Collectors.averagingDouble()
méthode à la Employee::getSalary
(ligne 89) pour calculer le salaire moyen dans l’ensemble de l’entreprise. Il s’agit d’un pur traitement de liste fonctionnelle ; aucune carte n’est impliquée.
Aux lignes 93-101, je convertis employeeList
dans un ruisseau à nouveau. J’utilise alors le Collectors.groupingBy()
méthode pour créer une carte dont les clés sont les emplacements des employés, renvoyés par e.getLocation()
et dont les valeurs sont le salaire moyen pour chaque emplacement, renvoyé par Collectors.averagingDouble()
encore appliqué à la Employee::getSalary
méthode appliquée à chaque employé du sous-ensemble de sites, plutôt qu’à l’ensemble de l’entreprise. C’est le groupingBy()
La méthode crée des sous-ensembles par emplacement, qui sont ensuite moyennés. Utilisation des lignes 98 à 101 forEach()
pour parcourir l’emplacement d’impression des entrées de la carte, le salaire moyen et la différence entre les moyennes de l’emplacement et la moyenne de l’entreprise.
Supposons maintenant que vous vouliez examiner uniquement les employés situés à Édimbourg. Une façon d’accomplir cela est montrée dans les lignes 103-112, où j’utilise le flux filter()
méthode pour créer une liste des seuls employés basés à Édimbourg et le forEach()
méthode pour imprimer leurs noms. Pas de cartes ici non plus.
Une autre façon de résoudre ce problème est illustrée aux lignes 113-123. Dans cette méthode, je crée une carte où chaque entrée contient une liste d’employés par emplacement. Tout d’abord, aux lignes 113-116, j’utilise le groupingBy()
méthode pour produire la carte que je veux avec des clés d’emplacements d’employés dont les valeurs sont des sous-listes d’employés à cet emplacement. Ensuite, aux lignes 117-123, j’utilise le forEach()
méthode pour imprimer la sous-liste des noms des employés du site d’Édimbourg.
Lorsque nous compilons et exécutons ce qui précède, la sortie est :
company avg salary = 292082.5
San Francisco avg salary = 284703.125; diff from avg company salary = -7379.375
New York avg salary = 410158.3333333333; diff from avg company salary = 118075.83333333331
Singapore avg salary = 357650.0; diff from avg company salary = 65567.5
Tokyo avg salary = 206087.5; diff from avg company salary = -85995.0
London avg salary = 322476.25; diff from avg company salary = 30393.75
Edinburgh avg salary = 261940.7142857143; diff from avg company salary = -30141.78571428571
Sydney avg salary = 90500.0; diff from avg company salary = -201582.5
employee(s) in Edinburgh (approach #1): Nixon,Tiger Kelly,Cedric Frost,Sonya Flynn,Quinn Rios,Dai Joyce,Gavin Mccray,Martena
employee(s) in Edinburgh (approach #2): Nixon,Tiger Kelly,Cedric Frost,Sonya Flynn,Quinn Rios,Dai Joyce,Gavin Mccray,Martena
Traitement de la liste des employés dans Groovy
Groovy a toujours fourni des fonctionnalités améliorées pour le traitement des listes et des cartes, en partie en étendant la bibliothèque Java Collections et en partie en fournissant des fermetures, qui ressemblent un peu aux lambdas.
L’un des résultats est que les cartes dans Groovy peuvent facilement être utilisées avec différents types de valeurs. Par conséquent, vous ne pouvez pas être poussé à créer la classe Employé auxiliaire ; à la place, vous pouvez simplement utiliser une carte. Examinons une version Groovy de la même fonctionnalité :
1 import java.util.Locale
2 import java.time.format.DateTimeFormatter
3 import java.time.LocalDate
4 import java.time.format.DateTimeParseException
5 import java.text.NumberFormat
6 import java.text.ParseException
7 def employeeList = [
8 ["Tiger Nixon", "System Architect", "Edinburgh",
9 "5421", "2011/04/25", "\$320,800"],
10 ["Garrett Winters", "Accountant", "Tokyo",
11 "8422", "2011/07/25", "\$170,750"],...
76 ["Martena Mccray", "Post-Sales support", "Edinburgh",
77 "8240", "2011/03/09", "\$324,050"],
78 ["Unity Butler", "Marketing Designer", "San Francisco",
79 "5384", "2009/12/09", "\$85,675"]
80 ].collect { ef ->
81 def surname, givenName, role, location, extension, hired, salary
82 def nn = ef[0].split(" ")
83 if (nn.length > 1) {
84 surname = nn[1]
85 givenName = nn[0]
86 } else {
87 surname = nn[0]
88 givenName = ""
89 }
90 role = ef[1]
91 location = ef[2]
92 try {
93 extension = Integer.parseInt(ef[3]);
94 } catch (NumberFormatException nfe) {
95 extension = 0;
96 }
97 try {
98 hired = LocalDate.parse(ef[4],
99 DateTimeFormatter.ofPattern("yyyy/MM/dd"));
100 } catch (DateTimeParseException dtpe) {
101 hired = LocalDate.EPOCH;
102 }
103 try {
104 salary = NumberFormat.getCurrencyInstance(Locale.US).
105 parse(ef[5]).doubleValue();
106 } catch (ParseException pe) {
107 salary = 0d;
108 }
109 [surname: surname, givenName: givenName, role: role,
110 location: location, extension: extension, hired: hired, salary: salary]
111 }
112 // calculate the average salary across the entire company
113 def companyAvgSal = employeeList.average { e -> e.salary }
114 println "company avg salary = " + companyAvgSal
115 // calculate the average salary for each location,
116 // compare to the company average
117 def locationAvgSal = employeeList.groupBy { e ->
118 e.location
119 }.collectEntries { l, el ->
120 [l, el.average { e -> e.salary }]
121 }
122 locationAvgSal.each { l, a ->
123 println l + " avg salary = " + a +
124 "; diff from avg company salary = " + (a - companyAvgSal)
125 }
126 // show the employees in Edinburgh approach #1
127 print "employee(s) in Edinburgh (approach #1):"
128 def employeesInEdinburgh = employeeList.findAll { e ->
129 e.location == "Edinburgh"
130 }
131 employeesInEdinburgh.each { e ->
132 print " " + e.surname + "," + e.givenName
133 }
134 println()
135 // group employees by location
136 def employeesByLocation = employeeList.groupBy { e ->
137 e.location
138 }
139 // show the employees in Edinburgh approach #2
140 print "employee(s) in Edinburgh (approach #1):"
141 employeesByLocation["Edinburgh"].each { e ->
142 print " " + e.surname + "," + e.givenName
143 }
144 println()
Parce que j’écris juste un script ici, je n’ai pas besoin de mettre le corps du programme dans une méthode à l’intérieur d’une classe ; Groovy s’en charge pour nous.
Aux lignes 1 à 6, je dois encore importer les classes nécessaires à l’analyse des données. Groovy importe pas mal de choses utiles par défaut, y compris java.lang.*
et java.util.*
.
Aux lignes 7 à 90, j’utilise le support syntaxique de Groovy pour les listes en tant que valeurs séparées par des virgules entre parenthèses [
and ]
. Dans ce cas, il y a une liste de listes ; chaque sous-liste contient les données des employés. Notez que vous avez besoin du \
en face de la $
dans le domaine du salaire. C’est parce qu’un $
apparaissant à l’intérieur d’une chaîne entourée de guillemets doubles indique la présence d’un champ dont la valeur doit être interpolée dans la chaîne. Une alternative serait d’utiliser des guillemets simples.
Mais je ne veux pas travailler avec une liste de listes ; Je préférerais avoir une liste de cartes analogue à la liste des instances de la classe Employee dans la version Java. J’utilise la collection Groovy .collect()
aux lignes 90 à 111 pour décomposer chaque sous-liste de données d’employés et la convertir en une carte. La méthode collect prend un argument Groovy Closure, et la syntaxe pour créer une fermeture entoure le code avec {
et }
et répertorie les paramètres comme a, b, c ->
d’une manière similaire aux lambdas de Java. La plupart du code ressemble assez à la méthode du constructeur dans la classe Java Employee, sauf qu’il y a des éléments dans la sous-liste plutôt que des arguments du constructeur. Cependant, les deux dernières lignes—
[surname: surname, givenName: givenName, role: role,location: location, extension: extension, hired: hired, salary: salary]
— créer une carte avec des clés surname
, givenName
, role
, location
, extension
, hired
et salary
. Et, puisqu’il s’agit de la dernière ligne de la fermeture, la valeur renvoyée à l’appelant est cette carte. Pas besoin d’instruction de retour. Inutile de citer ces valeurs clés ; Groovy suppose qu’il s’agit de chaînes. En fait, s’il s’agissait de variables, il faudrait les mettre entre parenthèses pour indiquer la nécessité de les évaluer. La valeur attribuée à chaque touche apparaît sur son côté droit. Notez qu’il s’agit d’une carte dont les valeurs sont de différents types : les quatre premières sont String
alors int
, LocalDate
et double
. Il aurait été possible de définir les sous-listes avec des éléments de ces différents types, mais j’ai choisi d’adopter cette approche car les données seraient souvent lues sous forme de valeurs de chaîne à partir d’un fichier texte.
Les bits intéressants apparaissent dans les lignes 112-144. J’ai conservé le même type d’étapes de traitement que dans la version Java.
Aux lignes 112-114, j’utilise la collection Groovy average()
méthode, qui aime collect()
prend un argument de fermeture, itérant ici sur la liste des cartes d’employés et en choisissant le salary
évaluer. Notez que l’utilisation de ces méthodes sur la classe Collection signifie que vous n’avez pas besoin d’apprendre à transformer des listes, des cartes ou tout autre élément en flux, puis d’apprendre les méthodes de flux pour gérer vos calculs, comme en Java. Pour ceux qui aiment Java Streams, ils sont disponibles dans les nouvelles versions de Groovy.
Aux lignes 115-125, je calcule le salaire moyen par lieu. D’abord, aux lignes 117-119, je transforme employeeList
qui est une liste de cartes, dans une carte, à l’aide de la collection groupBy()
méthode, dont les clés sont les valeurs d’emplacement et dont les valeurs sont des sous-listes liées des cartes d’employés appartenant à cet emplacement. Ensuite, je traite ces entrées de carte avec le collectEntries()
méthode, en utilisant la average()
méthode pour calculer le salaire moyen pour chaque emplacement.
Notez que collectEntries()
passe chaque clé (emplacement) et valeur (sous-liste d’employés à cet emplacement) dans la fermeture (le l, el ->
string) et s’attend à ce qu’une liste à deux éléments de clé (emplacement) et de valeur (salaire moyen à cet emplacement) soit renvoyée, en les convertissant en entrées de carte. Une fois que j’ai la carte des salaires moyens par lieu, locationAvgSal
je peux l’imprimer en utilisant la Collection each()
méthode, qui prend également une fermeture. Lorsque each()
est appliqué à une carte, il passe en clé (localisation) et en valeur (salaire moyen) de la même manière que collectEntries()
.
Aux lignes 126-134, je filtre les employeeList
pour obtenir une sous-liste de employeesInEdinburgh
en utilisant le findAll()
méthode, qui est analogue à Java Streams filter()
méthode. Et encore une fois, j’utilise le each()
méthode pour imprimer la sous-liste des employés à Édimbourg.
Aux lignes 135 à 144, j’adopte l’approche alternative consistant à regrouper les employeeList
dans une carte des sous-listes d’employés à chaque emplacement, employeesByLocation
. Ensuite, aux lignes 139 à 144, je sélectionne la sous-liste des employés à Édimbourg, en utilisant l’expression employeesByLocation[“Edinburgh”]
et le each()
méthode pour imprimer la sous-liste des noms d’employés à cet emplacement.
Pourquoi je préfère souvent Groovy
C’est peut-être juste ma familiarité avec Groovy, accumulée au cours des 12 dernières années environ, mais je me sens plus à l’aise avec l’approche Groovy pour améliorer Collection avec toutes ces méthodes qui prennent une fermeture comme argument, plutôt que l’approche Java de convertir le list, map ou tout ce qui est à portée de main à un flux, puis en utilisant des flux, des lambdas et des classes de données pour gérer les étapes de traitement. Il me semble que je passe beaucoup plus de temps avec les équivalents Java avant que quelque chose ne fonctionne.
Je suis également un grand fan du typage statique fort et des types paramétrés, tels que Map
comme trouvé en Java. Cependant, au jour le jour, je trouve que l’approche plus détendue des listes et des cartes acceptant différents types me soutient mieux dans le monde réel des données sans nécessiter beaucoup de code supplémentaire. La saisie dynamique peut certainement revenir mordre le programmeur. Pourtant, même en sachant que je peux activer la vérification de type statique dans Groovy, je parie que je ne l’ai pas fait plus d’une poignée de fois. Peut-être que mon appréciation pour Groovy vient de mon travail, qui consiste généralement à mettre en forme un tas de données, puis à les analyser; Je ne suis certainement pas votre développeur moyen. Alors Groovy est-il vraiment un Java plus Pythonique ? Nourriture pour la pensée.
J’aimerais voir dans Java et Groovy quelques installations supplémentaires comme average()
et averagingDouble()
. Des versions à deux arguments pour produire des moyennes pondérées et des méthodes statistiques au-delà de la moyenne, comme la médiane, l’écart type, etc., seraient également utiles. Tabnine offre des suggestions intéressantes sur la mise en œuvre de certaines d’entre elles.
Ressources géniales
La Site Apache Groovy a beaucoup de grande documentation. D’autres bonnes sources incluent la page de référence pour Améliorations groovy de la classe Java Collectionl’introduction plus didactique de travailler avec des collectionset M. Haki. La Site de Baeldung fournit de nombreux tutoriels utiles en Java et Groovy. Et une très bonne raison d’apprendre Groovy est d’apprendre Graalsun framework Web complet merveilleusement productif construit sur d’excellents composants comme Hibernate, Spring Boot et Micronaut.