Vous aimez ce que vous lisez sur ce blog ?
Envie d'aller plus loin avec véritable formation d'expertise en Java ?
Venez suivre ma formation Masterclasse Expertise Java !

"Même un développeur experimenté a besoin de continuer à apprendre. Et dans cette formation... j'ai appris beaucoup !" - A.G., Java Champion

Sessions intra-entreprises sur demande : contact[at]mokatech.net.
Inscrivez-vous vite !

Java WTF #1 : List.toArray()

Pour transformer une Liste en tableau, la méthode habituelle consiste à utiliser List.toArray(), héritée de l'interface Collection. Cette méthode possède deux variantes :

  • Une simple, qui renvoie un tableau d'Object que vous devez ensuite caster dans le bon type ;
  • Une plus complexe, qui prend en paramètre un tableau du type désiré et le remplit avec les valeurs de la liste. Elle est généralement préférée, parce qu'elle permet éventuellement de recycler un tableau existant, et surtout parce qu'elle évite le cast.
public Object[] toArray()
public <T> T[] toArray(T[] a)

Mais examinons de plus près la Javadoc de la seconde méthode...

Returns an array containing all of the elements in this list in proper sequence (from first to last element); the runtime type of the returned array is that of the specified array. If the list fits in the specified array, it is returned therein. Otherwise, a new array is allocated with the runtime type of the specified array and the size of this list.

Jusque-là tout paraît normal, mais...

If the list fits in the specified array with room to spare (i.e., the array has more elements than the list), the element in the array immediately following the end of the collection is set to null.

wtf.jpeg WTF ?
Donc, si on fournit un tableau plus grand que nécessaire, toArray() rajoute sournoisement un null après les éléments de la liste. Allez, c'est offert de bon coeur ; cadeau de la maison ! Un beau null dans la force de l'âge, une chouette bombe à retardement, à consommer de préférence quand votre application s'y attendra le moins.

Mais à quoi cela peut-il donc servir ? Il doit bien exister un use-case majeur qui justifierait ce comportement surprenant ?
La Javadoc avance timidement une explication :

(This is useful in determining the length of the list only if the caller knows that the list does not contain any null elements.)

Effectivement, si jamais votre architecte vous interdit d'utiliser List.size(), fonction notoirement dangereuse il est vrai, vous pouvez toujours vous rabattre sur List.toArray() et traquer le null terminal, en croisant naturellement les doigts pour que votre liste n'en comporte pas elle-même.
Ou pas.

Bref, je me gratte toujours la tête quant à l'utilité de ce null...
A part pour créer un quiz peut-être ? J'ai hésité à vous proposer cet article sous cette forme ; mais il n'aurait pas été très intéressant. Allez, je vous le mets quand même, pour le fun. Maintenant que vous connaissez l'astuce, vous devriez pouvoir trouver facilement le résultat de l'exécution de ce code :

public class Quiz {
 
    public static void main(String[] args) {
 
        List<String> words = new ArrayList<String>();
        words.add("Hello");
        words.add("Java");
        words.add("Quiz");
        words.add("#45");
 
        String[] bigStringArray = new String[words.size() * 2];
        bigStringArray = toSafeArray(words, bigStringArray);
        for (String s : bigStringArray) {
            System.out.println(s.toUpperCase());
        }
 
    }
 
    private static String[] toSafeArray(List<String> words, String[] array) {
        Arrays.fill(array, ""); // Ensure no nulls if the dest array is bigger
        return words.toArray(array);
    }
 
}

Je ne sais pas si ce billet inaugure ou non une série de billets à part entière, mais si je trouve d'autres bizarreries dans le JDK, je ne manquerai pas de vous les présenter !


Commentaires

1. Le jeudi 5 mai 2011, 06:51 par X-Blaster

Dans le cas de la methode <T> T toArray(T a), le tableau est fournit par le développeur et il est assez difficile de gérer un tableau "trop grand" pas rapport au nombre d’éléments dans la liste.

La fonction ne pouvant pas changer la taille de ce tableau en mémoire (et c'est bien normal d'ailleurs !), je comprend parfaitement que le développeur de la JDK qui a dû se dire "si un idiot donne un tableau trop grand je vais balancer un 'null' a la fin".

De toute façon je ne vois clairement pas ce que l'on pourrait faire de plus dans ce genre de cas et si vous avez une meilleure idée, proposez un patch ;).

WTF pour ma part refusé.

2. Le jeudi 5 mai 2011, 08:55 par Raphaël Lemaire

Des bizarreries dans le JDK, cela doit pouvoir se trouver facilement. Par exemple les mois qui commencent à zéro et les jours à un d'ans l'API date. Ou Stack qui hérite de Vector.

3. Le jeudi 5 mai 2011, 09:01 par reda

En effet, c'est étrange.

Une NPE à la 5ème itération.

4. Le jeudi 5 mai 2011, 12:29 par Colin Hebert

Je suis assez d'accord avec X-Blaster, le système est relativement proche de ce que l'on avait à l'époque en C pour gérer les chaînes de caractère et être sûr que l'on avait atteint la "fin" avec un joli '\0'.
C'est pas ce qui est de plus beau certes, mais il n'y a pas beaucoup d'autres moyens de gérer ça proprement.

En plus de use case du toArray() avec un tableau pré-initialisé est généralement pour éviter de ré-allouer un tableau à chaque fois, de fait on peut déduire que ce tableau est fait pour être utilisé à plusieurs reprises et que donc il n'est pas possible de savoir combien d'éléments ont étés ajoutés lors du dernier appel à toArray().

Par contre comme dit dans la doc, dans le cas "extrême" de la liste contenant un null (couplée avec un toArray avec en paramètre un tableau "non propre", et de taille supérieure au contenu de la liste) tout est foutu.

5. Le jeudi 5 mai 2011, 12:35 par Roland M

Ne serait-ce pas d'inspiration (certes, bizarre) des chaînes en C (http://en.wikipedia.org/wiki/C_stri...), qui se terminent par un NULL ?

6. Le jeudi 5 mai 2011, 14:24 par Olivier Croisier

En C, on est obligés de mettre le caractère de terminaison (\0) pour signaler la fin du tableau, parce qu'au-délà de ce terminateur, les cases du tableau pointent sur une zone mémoire potentiellement non initialisée.
En Java, c'est impossible : les éléments d'un tableau sont toujours initialisés à la valeur par défaut du type de ses éléments (null pour des références, 0 pour des entiers, etc.). Il n'y a donc pas besoin de garde-fou en bout de tableau.

Quant à l'argument qui consiste à dire que cela permet de compter les éléments copiés... il suffit d'appeler la méthode size() de la liste pour le connaître !

Notez également que System.arrayCopy(), qui est à peu près l'équivalent mais pour transférer des éléments depuis un tableau, n'ajoute pas de null, lui.

Je maintiens donc mon WTF tant qu'on ne m'a pas présenté un use-case sérieux pour cette particularité.

7. Le jeudi 5 mai 2011, 16:40 par Colin Hebert

Pour ce qui est du use-case sérieux, il sera relativement difficile à trouver, parce qu'utiliser "toArray(array)" dans un autre contexte que "toArray(new Type[0]);" ou "toArray(new Type[collection.size()]);" (pour éviter l'instantiation par introspection) est déjà en soit peu sérieux et plutôt risqué si on ne sait pas exactement ce qu'on fait (IMHO).

Si on part sur le principe que l'usage est fait pour éviter d'initialiser un array "trop souvent" (c'est ce qui me semble être le plus probable dans ce cas) si celui-ci est particulièrement grand, alors on peut supposer que l'avantage du stop "null" permet d'éviter d'avoir à conserver l'information sur la taille supposée dans une autre variable, qui devrait être passée en paramètre, en même temps que le tableau, dans le cas où par exemple le traitement du contenu du tableau se ferait dans d'autres méthodes.

Encore une fois, cet exemple est clairement capilotracté (quoi que?), mais je ne pense pas (ou en tout cas je n'arrive pas à en imaginer un maintenant) qu'il existe un véritable autre cas ou le toArray() utilisé avec un tableau trop grand aurait un intérêt et ne pourrait être remplacé par un autre code plus clair et performant et ne serait pas la bad practice à l'origine du problème.

Donc finalement la réponse serait plus "pourquoi pas?".
Si le développeur sait ce qu'il fait, et qu'il passe délibérément un tableau trop grand pour un cas très particulier, ce null lui permettra de travailler sans connaitre la taille de la collection* et lui simplifiera la vie.
Si le développeur ne sait pas ce qu'il fait, typiquement le résultat serait pour lui un puzzle, mais ça signifierait qu'il sait déjà que toArray() va réécrire le contenu de son tableau (sinon pour lui le puzzle sera de découvrir pourquoi son tableau d'origine est ruiné) sans savoir comment fonctionne le toArray, ce qui est relativement peu probable puisque cette méthode est peu explicite et intuitive et il me semble saugrenu d'utiliser cette méthode quasi cachée "sans faire exprès".

Au final, cette méthode n'est définitivement pas un exemple de beauté et est très particulière, c'est sûr, cependant il me semble que c'est juste un choix entre "est-ce qu'il est risqué de laisser un développeur se planter sur l'usage d'une méthode" et "est-ce qu'il est réellement avantageux de ne pas avoir à conserver la taille de la collection au moment de l'assignation au tableau".
Le choix fait repose plus sur l'aspect pratique que sur la défense d'un usage abusif.
Après est-ce que ce choix est mauvais ? Après tout ce n'est qu'un usage très particulier d'une méthode peu commune présente uniquement pour assurer un pont avec de vieilles API array based.

* sous condition que la collection ne contienne pas d'élément null, sinon il faudrait effectivement se soucier de la taille de la collection et on retomberait dans le cas de la taille à conserver séparément.
8. Le jeudi 5 mai 2011, 16:44 par Colin Hebert

J'ai oublié de parler du System.arrayCopy() et du Arrays.copyOf(). Il faut noter que les deux prennent en paramètre la longueur de ce que l'on souhaite copier. Ce qui de fait enlève toute utilité d'un stop "null".

9. Le jeudi 5 mai 2011, 17:22 par Olivier Michallat

Dans le cas d'une collection concurrente (ou d'une collection normale dans du code concurrent mal écrit ;-), la taille peut changer entre le début et la fin de l'exécution de la méthode.

Voir par exemple le toArray de ConcurrentLinkedQueue.

10. Le jeudi 5 mai 2011, 20:45 par Olivier Michallat

Pour préciser mon commentaire précédent, voici un exemple : https://gist.github.com/957409

Avec un thread fou qui modifie la file en continu, on observe facilement un décalage entre le résultat de size() et le nombre d'éléments effectivement copiés par toArray() (que l'appel à size soit avant ou après - déplacer la ligne 38 pour tester).

Donc, en supposant que je copie une file qui croît ou décroît tout le temps (au hasard, la file d'un pool de threads), et en supposant que je réutilise le même tableau à chaque fois (peut-être pour des questions de perfs), l'élément null final est le seul moyen de savoir ce que j'ai vraiment copié lors du dernier appel.

C'est un cas tordu, mais qui me semble néanmoins sérieux.

11. Le vendredi 6 mai 2011, 13:21 par HollyDays

Dans ce genre de cas, j'aime bien me poser la question : «mais ça date de quand, ce truc ?» C'est toujours instructif de remonter le fil. Et si on retrouve le moment où ce que l'on cherche est apparu, on retrouve aussi plus facilement la raison pour laquelle ce que l'on cherche a été introduit. Et à défaut, cela permet d'éliminer toute une série d'hypothèses.

Ici, mauvaise nouvelle : je n'ai pas pu retrouver la date d'introduction de cette fonctionnalité. Aussi loin que j'ai pu remonter, elle existait déjà :
http://download.oracle.com/javase/6...
http://download.oracle.com/javase/1...
http://download.oracle.com/javase/1...
http://download.oracle.com/javase/1...
http://web.cs.mun.ca/~michael/java/...

Comme vous le savez peut-être, le package "collections" a été introduit en standard dans le JDK 1.2, fin 1998. Mais ce package existait déjà avant, distribué par Sun sous la forme d'un add-on au JDK 1.1. Malheureusement, je n'ai pas retrouvé le javadoc de cet add-on (je n'ai retrouvé que le JavaDoc du JDK 1.1 standard). (Avec un peu de chance, je dois encore avoir ça sur de vieilles disquettes. Le problème, c'est que je n'ai plus de lecteur ! :-) )

Néanmoins, tout laisse à penser que cette fonctionnalité existe depuis le tout début de l'interface Collection en Java. Ce qui ne fait que renforcer le mystère soulevé par Olivier. Et qui, au passage, invalide l'hypothèse (pourtant très intéressante) d'Olivier Michallat ci-dessus.

12. Le mercredi 25 mai 2011, 08:03 par Suhrid Karthik

Google Translate is good, but it also tries to translate the code from French to English :) Apart from funny things like "public static void hand(String args)", it also destroys the formatting.
I wonder if you could try to configure it to skip the code blocks ? Thanks a lot!

13. Le mercredi 25 mai 2011, 10:10 par Olivier Croisier

This is one shortcoming of Google Translate indeed, I cannot tell it what to translate or not.
But you can see the original, non-translated text when you hover over it.
Another option could be to open two tabs on the same page and translate only one.
I know this is not very satisfying, but I cannot do much more :/

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.