Cet article est le premier d’une série dédiée, comme le titre l’indique, aux mystères du langage Java. Attention toutefois, les résultats ou les démonstrations exposés peuvent différencier en fonction de la JVM utilisée ! De manière générale, nous utiliserons la JVM HotSpot maintenue par Oracle.

Calculer une somme d’entiers : trop facile !

Tout développeur a déjà eu l’occasion d’écrire une boucle permettant de calculer la somme d’une liste d’entiers en manipulant le type primitif int :

public static int sum(List<Integer> ints) {
	int s = 0;

	for (int n : ints) {
		s += n;
	}

	return s;
}

Qu’en est-il maintenant, si vous vous retrouvez face à la même situation mais en souhaitant retourner le wrapper Integer ? Bon nombre de développeurs profiteront de l’autoboxing apparu à partir de la version 5 de Java et écriront, pour certains d’entre eux, le code suivant :

public static Integer sumInteger(List<Integer> ints) {
	Integer s = 0;

	for (int n : ints) {
		s += n;
	}

	return s;
}

Écrivons maintenant quelques tests unitaires nous permettant de vérifier le bon fonctionnement de nos méthodes utilitaires :

@Test
public void testSmallInteger() {
	List<Integer> smalls = Arrays.asList(1, 2, 3);

	assertTrue(sumInteger(smalls) == sum(smalls));
	assertTrue(sumInteger(smalls) == sumInteger(smalls));
}
@Test
public void testBigInteger() {
	List<Integer> bigs = Arrays.asList(100, 200, 300);

	assertTrue(sumInteger(bigs) == sum(bigs));
	assertTrue(sumInteger(bigs) == sumInteger(bigs));
}

Trop facile, et pourtant…

Et pourtant l’exécution des tests unitaires précédents conduira à un échec. Plus précisément, JUnit nous indique que l’assertion définie ligne 13  n’est pas respectée. Cela signifie que sumInteger(bigs) est différent de sumInteger(bigs) alors que sumInteger(smalls) est équivalent à sumInteger(smalls) ! On ne nous dit pas tout1 !

Nous pouvons donc émettre deux hypothèses :

  • La première : il existe une différence entre le fait de manipuler le wrapper Integer plutôt que de manipuler le types primitifs int,
  • La seconde : il existe une différence de comportement entre la manipulation d’entiers de faible valeur (1, 2, 3) et des entiers d’une valeur plus importante (100, 200, 300).
La première hypothèse peut rapidement être mise hors de cause étant donné que nos tests unitaires sont opérationnels pour les tests suivants :
assertTrue(sumInteger(smalls) == sum(smalls)); // OK
assertTrue(sumInteger(bigs) == sum(bigs)); // OK

Reste la seconde hypothèse : une différence due à la valeur des entiers…

Les mains dans le cambouis !

Nous allons explorer la méthode sumInteger() pour donner une explication censée. Pour ce faire, nous allons décompiler la classe Java à partir de laquelle nous obtenons l’implémentation suivante :

public static Integer sumInteger(List ints) {
	Integer s = Integer.valueOf(0);

	for (Iterator localIterator = ints.iterator(); localIterator.hasNext();) {
		int n = ( (Integer) localIterator.next()).intValue();
		s = Integer.valueOf(s.intValue() + n);
	}
	return s;
}

Nous remarquons que les informations au niveau du type paramétré de List ont disparu (c’est ce que l’on appelle le type erasure, ce qui indique donc que les generics sont utilisés à la compilation et non pas au runtime). Au delà de cette remarque, nous pouvons voir que les primitif int sont transformés en Integer via la méthode Integer.valueOf().

Continuons notre exploration en jetant un œil à l’implémentation de cette fameuse méthode :

/**
 * Returns a <tt>Integer</tt> instance representing the specified
 * <tt>int</tt> value. If a new <tt>Integer</tt> instance is not required,
 * this method should generally be used in preference to the constructor
 * {@link #Integer(int)}, as this method is likely to yield significantly
 * better space and time performance by caching frequently requested values.
 *
 * @param i
 *            an <code>int</code> value.
 * @return a <tt>Integer</tt> instance representing <tt>i</tt>.
 * @since 1.5
 */
public static Integer valueOf(int i) {
	if (i >= -128 && i <= IntegerCache.high)
		return IntegerCache.cache[i + 128];
	else
		return new Integer(i);
}

Après avoir lu de long en large la documentation de cette fameuse méthode, nous nous rendons compte d’une chose : un cache est utilisé afin d’améliorer l’allocation mémoire ainsi que les performances pour les valeurs les plus souvent utilisées. Reste à savoir comment ces valeurs sont définies…

Rien de bien compliqué en soi lorsque nous continuons notre analyse de la classe Integer et plus spécifiquement la classe interne IntegerCache. En fait, par défaut, la JVM maintient un cache d’entiers pour les valeurs allant de -128 à 127 (la valeur maximale peut-être redéfinie en ajoutant la propriété java.lang.Integer.IntegerCache.high dans les options de la VM).

/**
 * Cache to support the object identity semantics of autoboxing for values
 * between -128 and 127 (inclusive) as required by JLS.
 *
 * The cache is initialized on first usage. During VM initialization the
 * getAndRemoveCacheProperties method may be used to get and remove any
 * system properites that configure the cache size. At this time, the size
 * of the cache may be controlled by the vm option
 * -XX:AutoBoxCacheMax=<size>.
 */

// value of java.lang.Integer.IntegerCache.high property (obtained during VM init)
private static String integerCacheHighPropValue;

L’échec du test unitaire dédié aux valeurs plus importantes devient donc logique puisque le cache s’arrête à la valeur 127 et que dans notre cas, la somme des valeurs vaut 600… Un nouvel objet de type Integer étant créé à chaque appel de la méthode sumInteger() et étant donné que le signe == vérifie qu’il s’agit de la même référence d’objet, le test abouti à un échec.

A l’inverse, le cache d’entiers est utilisé pour la somme des entiers de faible valeur étant donné que celle-ci vaut 6. Il est intéressant d’ajouter une valeur à la liste des petits entiers pour vérifier notre assertion :

List<Integer> smalls1 = Arrays.asList(1, 2, 3);
assertTrue(sumInteger(smalls1) == sumInteger(smalls1)); // OK

List<Integer> smalls2 = Arrays.asList(1, 2, 3, 122);
assertTrue(sumInteger(smalls2) == sumInteger(smalls2)); // Fail !

Le second test tombe en échec car la somme vaut 1 + 2 +3 +122 = 128, valeur se trouvant en dehors des limites du cache.

Conclusion

Pour conclure ce premier billet dédié aux mystères de Java, il est important :

  • De ne pas comparer les objets wrapper en utilisant le signe == mais plutôt en utilisant la méthode equals(),
  • D’utiliser la méthode Integer.valueOf() afin de profiter des effets bénéfiques du cache, ou alors laisser faire l’autoboxing.
  1. Toute ressemblance avec les sketchs d’Anne Roumanoff est à proscrire []