Je dois souvent intervenir sur des programmes qui utilisent des maps et qui parcourent les couples clé-valeur d’une mauvaise manière. En effet, les développeurs partent des clés et recherchent les valeurs associées dans la map. Or ils font cela pour l’ensemble des éléments.
Pour commencer, partons d’une simple map.
Version Java 1.4 :
1 2 3 4 5 |
Version Java 5 :
1 2 3 4 5 |
Les développeurs écrivent généralement (disons trop souvent) le code suivant :
Version Java 1.4 :
1 2 3 4 5 6 7 |
Version Java 5 :
1 2 3 4 5 6 |
Ce qui est mauvais, dans ce code, c’est l’appel à « map.get ». Dans l’exemple, avec seulement trois valeurs, ce n’est pas bien grave. Mais sur une application en Prod, avec des milliers d’utilisateurs et des listes bien plus grosses, ça commence à peser. Il faut utiliser « map.entrySet » qui renvoie directement des couples clé-valeur :
En Java 1.4 :
1 2 3 4 5 6 7 8 |
En Java 5 :
1 2 3 4 5 6 7 |
Pas besoin, donc, de lancer une recherche (qui peut être coûteuse), pour chaque clé, puisqu’on a déjà la donnée toute prête…
Pour m’amuser, j’ai écris un petit programme permettant de mesurer tout ça. J’ai un objet Chien, qui possède un nom, un sexe, une race, etc. :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | public class Chien { private String nom; private Double poids; private Double taille; private Genre sexe; private Race race; @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((nom == null) ? 0 : nom.hashCode()); result = prime * result + ((race == null) ? 0 : race.hashCode()); result = prime * result + ((sexe == null) ? 0 : sexe.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Chien other = (Chien) obj; if (nom == null) { if (other.nom != null) return false; } else if (!nom.equals(other.nom)) return false; if (race != other.race) return false; if (sexe != other.sexe) return false; return true; } @Override public String toString() { return "Chien [nom=" + nom + ", poids=" + poids + ", taille=" + taille + ", sexe=" + sexe + ", race=" + race + "]"; } // Getter-setter ... } |
Je construit plein de chiens avec des sexes et des races choisis de manière aléatoire :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | private static Chien createRandomChien(final String nom) { final Chien chien = new Chien(); chien.setNom(nom); chien.setRace(Race.getAleatoire()); chien.setSexe(Genre.getAleatoire()); return chien; } private static List<Chien> createRandomChiens() { final List<Chien> chiens = new LinkedList<Chien>(); final int NB = 10000000; for (int i = 0; i < NB; i++) { final Chien chien = createRandomChien("Milou" + i); chiens.add(chien); } return chiens; } |
Et j'envoie tout ça dans des maps :
1 2 3 4 5 6 7 8 |
J'ai utilisé deux maps distinctes pour que des raisons de chauffe.
Ensuite, j'applique les deux méthodes, avec "map.keySet" et "map.entrySet". Au passage, je note la durée des traitements :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | final long time1 = System.currentTimeMillis(); for (final String key : map1.keySet()) { final Chien value = map1.get(key); } final long time2 = System.currentTimeMillis(); for (final Entry<String, Chien> entry : map2.entrySet()) { final Chien chien = entry.getValue(); } final long time3 = System.currentTimeMillis(); final long duree1 = time2 - time1; final long duree2 = time3 - time2; System.out.println(duree1); System.out.println(duree2); |
Pour finir, je fais varier le nombre d'éléments dans les maps :
1 2 3 4 5 | NB: 100 duree1: 0 duree2: 0 NB: 1000 duree1: 1 duree2: 0 NB: 10000 duree1: 2 duree2: 1 NB: 100000 duree1: 10 duree2: 7 NB: 1000000 duree1: 95 duree2: 62 |
Le résultat n'est pas forcément évident mais on comprend bien le problème…
Avec d’aussi petits temps mesurés tu aurais très bien pu avoir des temps de réponse non ordonnés en cas de petit lag système. Je te suggèrerai de faire une boucle pour effectuer 10.000 fois chaque test et de comparer les temps totaux.
C’est le même principe que pour mesurer l’épaisseur d’une feuille de papier avec un double décimètre, ce n’est pas évident, mais si par contre tu mesures un bloc de 1000 pages et que tu divises par 1000, tout de suite le résultat devient beaucoup plus significatif
Sérieusement, tu te doutes bien que c’est déjà ce que je fais !
Sans davantage de précisions j’avais compris que tu faisais varier le nombre d’éléments dans une map, mais pas que tu exécutais le test plusieurs fois avant de mesurer le temps d’exécution. Cette impression est renforcée par le fait que tu donnes très peu de chiffres significatifs dans tes mesures. A partir du moment où tu fais un nombre de boucles suffisant pour te permettre une bonne précision de mesure, je pense que tu peux te permettre de mettre au moins trois chiffres significatifs
Ca fait plaisir de lire qu’on est pas seul ! T’as pas le même soucis avec l’auto(un)boxing à gogo ou la concaténation de chaînes ?
Je pense qu’il doit y avoir un paquet d’autres exemples, de prochains billets ?
On fait du Java 1.4 chez nous. Là on est en train de changer tous les bloc booléens :
2
Boolean result = new Boolean(value);
par
Le second bloc renvoie une valeur statique alors que le premier passe par un constructeur. C’est pas bien méchant mais on en a beaucoup. Et je ne parle même pas de quand on veut directement les constantes…
Avec l’autoboxing, c’est masqué. Donc c’est le drame… D’ailleurs, il ne faut pas oublier qu’un Boolean a trois états : true, false et null. Dans l’autoboxing, ça joue ! J’avais écris un petit billet là -dessus.
Salut,
Même si je suis plutôt d’accord concernant le fond du billet, il faut faire attention aux tests qui ont un résultat de moins de 30ms…
En effet currentTimeMillis() n’a pas forcément une bonne précision et peut amener des erreurs…
Sinon vivement Java 8 qu’on puisse utiliser la méthode forEach() :
Map map = …
map.forEach((key,value) -> {
// Traitement key-value
System.out.println(« key: » + key + » value: » + value);
});
Effectivement. Je crois me souvenir que la précision de Java 1.4 est de l’ordre de 10ms. Ca doit être sensiblement la même chose pour Java 7…
L’idée, ici, c’est surtout la « propreté sémantique » du code. L’exemple, avec mesure des performances, est surtout un prétexte pour proposer un cas concret.