Utilisez map.entrySet

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
final Map map = new HashMap();

map.put("rouge", "FF0000");
map.put("vert", "00FF00");
map.put("bleu", "0000FF");

Version Java 5 :

1
2
3
4
5
final Map<String, String> map = new HashMap<String, String>();

map.put("rouge", "FF0000");
map.put("vert", "00FF00");
map.put("bleu", "0000FF");

Les développeurs écrivent généralement (disons trop souvent) le code suivant :

Version Java 1.4 :

1
2
3
4
5
6
7
for (final Iterator iter = map.keySet().iterator(); iter.hasNext();) {
    final String key = (String) iter.next();
    final String value = (String) map.get(key);

    // Traitement key-value
    System.out.println("key: " + key + " value: " + value);
}

Version Java 5 :

1
2
3
4
5
6
for(final String key : map.keySet()) {
    final String value = map.get(key);

    // Traitement key-value
    System.out.println("key: " + key + " value: " + value);
}

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
for (final Iterator iter = map.entrySet().iterator(); iter.hasNext();) {
    final Entry entry = (Entry) iter.next();
    final String key = (String) entry.getKey();
    final String value = (String) entry.getValue();

    // Traitement key-value
    System.out.println("key: " + key + " value: " + value);
}

En Java 5 :

1
2
3
4
5
6
7
for(final Entry<String, String> entry : map.entrySet()) {
    final String key =  entry.getKey();
    final String value =  entry.getValue();

    // Traitement key-value
    System.out.println("key: " + key + " value: " + value);
}

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
final List<Chien> chiens = createRandomChiens();

final Map<String, Chien> map1 = new HashMap<String, Chien>();
final Map<String, Chien> map2 = new HashMap<String, Chien>();
for (final Chien chien : chiens) {
    map1.put(chien.getNom(), chien);
    map2.put(chien.getNom(), chien);
}

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…

7 réflexions au sujet de « Utilisez map.entrySet »

  1. Avatar de AldianAldian

    Pour finir, je fais varier le nombre d’éléments dans les maps (…). 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 ;)

      1. Avatar de AldianAldian

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

    1. Avatar de thierrylerthierryler Auteur de l’article

      On fait du Java 1.4 chez nous. Là on est en train de changer tous les bloc booléens :

      1
      2
      boolean value = ...
      Boolean result = new Boolean(value);

      par

      1
      Boolean result = Boolean.valueOf(value);

      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.

  2. Avatar de adiGubaadiGuba

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

    1. Avatar de thierrylerthierryler Auteur de l’article

      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.

Laisser un commentaire