Je parcourais le code du JDK (JDK 12, mais cela s'applique aussi aux plus anciens) et j'ai trouvé des constructions bizarres, et je ne comprends pas pourquoi elles ont été utilisées. Prenons par exemple Map.computeIfPresent
car c'est simple :
default V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
Object oldValue;
if ((oldValue = this.get(key)) != null) {
V newValue = remappingFunction.apply(key, oldValue);
if (newValue != null) {
this.put(key, newValue);
return newValue;
} else {
this.remove(key);
return null;
}
} else {
return null;
}
}
Cette construction if ((oldValue = this.get(key)) != null)
m'a surpris. Je savais que c'était possible, parce qu'il n'y a rien de vraiment spécial, mais dans un code de production normal, je considérerais cela comme une odeur de code. Pourquoi ne pas l'écrire de façon normale ( Object oldValue = this.get(key)
) ? Il doit y avoir une optimisation de l'embrayage, c'est ce que j'ai pensé.
Écriture d'une version plus petite pour vérifier le bytecode :
int computeIfPresent(int key) {
Integer oldValue;
if ((oldValue = get(key)) != null) {
return oldValue;
} else {
return 2;
}
}
Sortie du bytecode :
int computeIfPresent(int);
Code:
0: aload_0
1: iload_1
2: invokevirtual #2 // Method get:(I)Ljava/lang/Integer;
5: dup
6: astore_2
7: ifnull 15
10: aload_2
11: invokevirtual #3 // Method java/lang/Integer.intValue:()I
14: ireturn
15: iconst_2
16: ireturn
Bytecode pour la version "normale" avec initialisation classique des variables :
int computeIfPresent(int);
Code:
0: aload_0
1: iload_1
2: invokevirtual #2 // Method get:(I)Ljava/lang/Integer;
5: astore_2
6: aload_2
7: ifnull 15
10: aload_2
11: invokevirtual #3 // Method java/lang/Integer.intValue:()I
14: ireturn
15: iconst_2
16: ireturn
La seule différence est dup + astore_2
vs astore_2 + aload_2
. Je pourrais même soupçonner que la première version "optimisée pour l'embrayage" est pire, parce que dup
est utilisé et la pile est plus grande sans raison. Peut-être que mon exemple était trop simple et que l'optimisation s'étend beaucoup dans un contexte plus compliqué.
Il s'agit d'un exemple simple, qui n'est certainement pas présent dans le code du JDK. HashMap.java
Il y a des tonnes de fragments de ce type, parfois plusieurs sur la même ligne :
if ((first = tab[i = (n - 1) & hash]) != null)
Même si c'est très simple, je dois m'arrêter un instant et réfléchir à ce que ce code fait réellement, à cause de ces constructions.
Quelle est la véritable raison de l'utilisation de ces constructions ? Je suis sûr qu'il ne s'agit pas simplement d'un mauvais code. À mon avis, la qualité du code en souffre beaucoup, donc le bénéfice doit être substantiel. Ou simplement la règle leave small optimizations to JIT
ne s'applique pas au JDK, parce qu'il doit être le plus performant possible ?
Ou s'agit-il simplement d'une règle extrême ? initialize variables as late as possible
? :)