oct.
2010
Au coeur du JDK : l'interface Iterable
Connaissez-vous l'interface java.lang.Iterable
?
Mais si, vous la connaissez : vous l'utilisez tous les jours, lorsque vous parcourez une Collection. Hé bien, voyons maintenant comment vous pouvez l'utiliser à votre avantage lorsque vous concevez un modèle objet.
Iterable, mode d'emploi
L'interface Iterable
signale que la classe qui l'implémente est composée d'un ensemble de sous-éléments que le code appelant peut parcourir.
Elle déclare pour cela une unique méthode iterator(), devant renvoyer un Iterator
sur l'ensemble des sous-éléments.
public interface Iterable<T> { /** * Returns an iterator over a set of elements of type T. * @return an Iterator. */ Iterator<T> iterator(); }
L'interface java.util.Collection
étend Iterable
, ce qui nous autorise à parcourir ses implémentations (notamment List
et Set
) de manière uniforme :
List<String> words = Arrays.asList("Hello", "World"); Iterator<String> it = words.iterator(); while(it.hasNext()) { System.out.println(it.next()); }
C'est également Iterable qui vous permet d'utiliser la boucle "foreach" apparue avec Java 5 (JLS $14.14.2).
EnhancedForStatement:
for ( VariableModifiersopt Type Identifier: Expression) StatementThe Expression must either have type Iterable or else it must be of an array type (§10.1), or a compile-time error occurs.
List<String> words = Arrays.asList("Hello", "World"); for (String word : words) { System.out.println(word); }
Implémenter Iterable dans votre code
Lorsque nous concevons un modèle objet, il arrive fréquemment que nous devions modéliser des relations de type "1-N avec attributs". Leur implémentation la plus évidente est une classe possédant possédant des champs correspondant aux attributs de la relation, ainsi qu'une collection encapsulant les sous-éléments.
Une partition musicale en est un bon exemple : elle est constituée d'une séquence de notes (les sous-éléments), ainsi que de divers attributs comme le nom du morceau.
En voici une implémentation naïve :
public class Partition { private String nom; private List<Note> notes; public String getNom() { return nom; } public void setNom(String nom) { this.nom = nom; } public List<Note> getNotes() { return notes; } public void setNotes(List<Note> notes) { this.notes = notes; } }
Ce type d'implémentation est très répandu, mais malheureusement très mauvais du point de vue de la conception objet, car il rompt le principe d'encapsulation : l'accesseur de la liste des notes permet de modifier celle-ci sans que la Partition
ne soit avertie, et expose son implémentation interne.
Il faut donc trouver un moyen pour autoriser le développeur à parcourir la liste sans l'exposer directement.
C'est là qu'intervient l'interface Iterable.
Voici le code remanié :
public class Partition implements Iterable<Note> { private final String nom; private final List<Note> notes = new ArrayList<Note>(); public Partition(String nom, List<Note> notes) { this.nom = nom; if (notes != null) { this.notes.addAll(notes); } } public String getNom() { return nom; } public Iterator<Note> iterator() { return notes.iterator(); } }
Ce remaniement amène un second bénéfice, pour l'utilisateur cette fois : il est beaucoup plus facile et naturel de parcourir la partition, note par note.
Partition partition = new Partition("Au clair de la lune", Arrays.asList(C, C, C, D, E, D, C, E, D, D, C)); for(Note note : partition) { Instrument.play(note); }
Notez que, le plus souvent, il suffit de renvoyer l'itérateur fourni par la collection encapsulée.
Conclusion
En conclusion, lorsque vous encapsulez une collection au sein d'une classe, pensez à l'interface Iterable
; votre code sera mieux encapsulé et plus facile à utiliser !
Vous trouverez un exemple d'utilisation complet dans mon programme permettant de calculer les accords de guitare.
Commentaires
Implémenter Iterable n'est une condition ni nécessaire, ni suffisante pour garantir l'encapsulation.
Pour que l'encapsulation de la partition soit respectée, il suffit :
Implémenter l'interface Iterable ne permet, en lui-même en aucun cas de garantir l'encapsulation, car d'après la spec d'iterator (http://download.oracle.com/javase/6...), l'appel de la méthode remove() sur l'iterator modifie la collection sous-jacente (si elle est implémentée).
J'ai dit une petite bêtise dans mon commentaire précédent. Le fait de faire une copie défensive dans l'accesseur ne suffit pas, il faut aussi faire une copie défensive dans le setter, de manière à ce que si une référence est gardée sur la liste mise en paramètre modifie cette liste, elle n'ait pas d'impact sur la liste encapsulée...
Hello,
Merci pour cet article intéressant sur ce point technique.
Cordialement,
Bonjour,
J'avais utilisé cette méthode au début de java 5, mais j'ai finalement trouvé que syntaxiquement c'etait moins clair que d'utiliser un getter. Il faut savoir que la partition contient des notes même si fonctionnellement ca a du sens et QUID d'un objet metier avec deux listes de sous-elements ?
L'approche est tres sexy mais dans la pratique je crains un plus grand risque d'erreur de compréhension.
A utiliser avec précaution dans des concepts purement technique.
@Benoît
Effectivement, il faudrait wrapper la collection avec Collection.unmodifiableList, pour éviter la suppression via l'itérateur. Quoi qu'il en soit, cette technique améliore l'encapsulation mais ne garantit pas l'immuabilité, puisque nous ne contrôlons pas les éléments de la liste. Mais d'un point de vue design objet, c'est quand même mieux que de publier directement la sous-liste via un getter.
@n!co
Bonne remarque, mais j'ai précisé que le meilleur cas d'application est lorsqu'on modélise une simple relation "1-N avec association", auquel cas une seule sous-liste est encapsulée, et la relation entre la classe et sa sous-liste est évidente.
Effectivement, dans des cas plus complexes, Iterable n'est sans doute plus la meilleure solution.
Salut,
Plutôt que d'implémenter Iterable, on peut très bien se contenter d'utiliser un getter renvoyant un Iterable protégé via Collections.unmodifiableList() :
@@ public Iterable<Note> getNodes() {
}
@@
En particulier si la classe possède plusieurs listes ;)
On pourrait éventuellement remplacer unmodifiableList() par une méthode "unmodifiableIterable()" qui briderait le remove() de l'Iterator...
a++
La solution proposée par adiGuba est sans doute la plus simple et la meilleure. C'est celle qui est le plus dans l'esprit "objet" (toujours utiliser le type le plus général possible), et, pour l'avoir mise en œuvre de nombreuses fois dans le passé, je peux confirmer qu'elle marche très bien.
Si on voulait vraiment chercher la petite bête, on pourrait regretter le fait que chaque appel à getNotes() instancie un nouvel objet, ce qui est un peu bête. Mais le remède est aisé : si getNotes() est vraiment appelé si souvent que cela, il suffit de mettre la collection non modifiable en attribut supplémentaire de la classe et le tour est joué.
Une dernière petite remarque : nul besoin de remplacer unmodifiableList() par un "unmodifiableIterable()" pour brider le remove() de l'Iterator. La classe Collection.UnmodifiableCollection fait cela déjà très bien (cf. java/util/Collections.java:1012 dans les sources du JDK 1.6.0_21). ;-)
Merci HollyDays . Explication claire et net ... Bavo