t
t
t
t
t t C) Conseils pour une programation stylée en java
tttt
C) Conseils pour une programation stylée en Java
Texte original t Traducteur : Jérome QUELIN
t
t
///
Ce chapitre contient 2 pages
1 2
    
t t t
t t t
t
t t t

Implementation

t

Implémentation

t t t
  1. In general, follow the Sun coding conventions. These are available at java.sun.com/docs/codeconv/index.html (the code in this book follows these conventions as much as I was able). These are used for what constitutes arguably the largest body of code that the largest number of Java programmers will be exposed to. If you doggedly stick to the coding style you’ve always used, you will make it harder for your reader. Whatever coding conventions you decide on, ensure they are consistent throughout the project. There is a free tool to automatically reformat Java code at: home.wtal.de/software-solutions/jindent.

  2. Whatever coding style you use, it really does make a difference if your team (and even better, your company) standardizes on it
    . This means to the point that everyone considers it fair game to fix someone else’s coding style if it doesn’t conform. The value of standardization is that it takes less brain cycles to parse the code, so that you can focus more on what the code means.

  3. Follow standard capitalization rules
    . Capitalize the first letter of class names. The first letter of fields, methods, and objects (references) should be lowercase. All identifiers should run their words together, and capitalize the first letter of all intermediate words. For example:ThisIsAClassName thisIsAMethodOrFieldName Capitalize all the letters of static final primitive identifiers that have constant initializers in their definitions. This indicates they are compile-time constants.Packages are a special case—they are all lowercase letters, even for intermediate words. The domain extension (com, org, net, edu, etc.) should also be lowercase. (This was a change between Java 1.1 and Java 2.)

  4. Don’t create your own “decorated” private data member names
    . This is usually seen in the form of prepended underscores and characters. Hungarian notation is the worst example of this, where you attach extra characters that indicate data type, use, location, etc., as if you were writing assembly language and the compiler provided no extra assistance at all. These notations are confusing, difficult to read, and unpleasant to enforce and maintain. Let classes and packages do the name scoping for you.

  5. Follow a “canonical form”
    when creating a class for general-purpose use. Include definitions for equals( ), hashCode( ), toString( ), clone( ) (implement Cloneable), and implement Comparable and Serializable.

  6. Use the JavaBeans “get,” “set,” and “is” naming conventions
    for methods that read and change private fields, even if you don’t think you’re making a JavaBean at the time. Not only does it make it easy to use your class as a Bean, but it’s a standard way to name these kinds of methods and so will be more easily understood by the reader.

  7. For each class you create, consider including a static public test( ) that contains code to test that class
    . You don’t need to remove the test code to use the class in a project, and if you make any changes you can easily rerun the tests. This code also provides examples of how to use your class.

  8. Sometimes you need to inherit in order to access protected members of the base class
    . This can lead to a perceived need for multiple base types. If you don’t need to upcast, first derive a new class to perform the protected access. Then make that new class a member object inside any class that needs to use it, rather than inheriting.

  9. Avoid the use of final methods for efficiency purposes
    . Use final only when the program is running, but not fast enough, and your profiler has shown you that a method invocation is the bottleneck.

  10. If two classes are associated with each other in some functional way (such as containers and iterators), try to make one an inner class of the other
    . This not only emphasizes the association between the classes, but it allows the class name to be reused within a single package by nesting it within another class. The Java containers library does this by defining an inner Iterator class inside each container class, thereby providing the containers with a common interface. The other reason you’ll want to use an inner class is as part of the private implementation. Here, the inner class beneficial for implementation hiding rather than the class association and prevention of namespace pollution noted above.

  11. Anytime you notice classes that appear to have high coupling with each other, consider the coding and maintenance improvements you might get by using inner classes
    . The use of inner classes will not uncouple the classes, but rather make the coupling explicit and more convenient.

  12. Don’t fall prey to premature optimization
    . This way lies madness. In particular, don’t worry about writing (or avoiding) native methods, making some methods final, or tweaking code to be efficient when you are first constructing the system. Your primary goal should be to prove the design, unless the design requires a certain efficiency.

  13. Keep scopes as small as possible so the visibility and lifetime of your objects are as small as possible
    . This reduces the chance of using an object in the wrong context and hiding a difficult-to-find bug. For example, suppose you have a container and a piece of code that iterates through it. If you copy that code to use with a new container, you may accidentally end up using the size of the old container as the upper bound of the new one. If, however, the old container is out of scope, the error will be caught at compile-time.

  14. Use the containers in the standard Java library
    . Become proficient with their use and you’ll greatly increase your productivity. Prefer ArrayList for sequences, HashSet for sets, HashMap for associative arrays, and LinkedList for stacks (rather than Stack) and queues.

  15. For a program to be robust, each component must be robust
    . Use all the tools provided by Java: access control, exceptions, type checking, and so on, in each class you create. That way you can safely move to the next level of abstraction when building your system.

  16. Prefer compile-time errors to run-time errors
    . Try to handle an error as close to the point of its occurrence as possible. Prefer dealing with the error at that point to throwing an exception. Catch any exceptions in the nearest handler that has enough information to deal with them. Do what you can with the exception at the current level; if that doesn’t solve the problem, rethrow the exception.

  17. Watch for long method definitions
    . Methods should be brief, functional units that describe and implement a discrete part of a class interface. A method that is long and complicated is difficult and expensive to maintain, and is probably trying to do too much all by itself. If you see such a method, it indicates that, at the least, it should be broken up into multiple methods. It may also suggest the creation of a new class. Small methods will also foster reuse within your class. (Sometimes methods must be large, but they should still do just one thing.)

  18. Keep things as “private as possible.”
    Once you publicize an aspect of your library (a method, a class, a field), you can never take it out. If you do, you’ll wreck somebody’s existing code, forcing them to rewrite and redesign. If you publicize only what you must, you can change everything else with impunity, and since designs tend to evolve this is an important freedom. In this way, implementation changes will have minimal impact on derived classes. Privacy is especially important when dealing with multithreading—only private fields can be protected against un-synchronized use.

  19. Use comments liberally, and use the javadoc comment-documentation syntax to produce your program documentation
    . However, the comments should add geniune meaning to the code; comments that only reiterate what the code is clearly expressing are annoying. Note that the typical verbose detail of Java class and method names reduce the need for as many comments.

  20. Avoid using “magic numbers”
    —which are numbers hard-wired into code. These are a nightmare if you need to change them, since you never know if “100” means “the array size” or “something else entirely.” Instead, create a constant with a descriptive name and use the constant identifier throughout your program. This makes the program easier to understand and much easier to maintain.

  21. When creating constructors, consider exceptions
    . In the best case, the constructor won’t do anything that throws an exception. In the next-best scenario, the class will be composed and inherited from robust classes only, so they will need no cleanup if an exception is thrown. Otherwise, you must clean up composed classes inside a finally clause. If a constructor must fail, the appropriate action is to throw an exception, so the caller doesn’t continue blindly, thinking that the object was created correctly.

  22. If your class requires any cleanup when the client programmer is finished with the object, place the cleanup code in a single, well-defined method
    —with a name like cleanup( ) that clearly suggests its purpose. In addition, place a boolean flag in the class to indicate whether the object has been cleaned up so that finalize( ) can check for “the death condition” (see Chapter 4).

  23. The responsibility of finalize( ) can only be to verify “the death condition” of an object for debugging.
    (See Chapter 4.) In special cases, it might be needed to release memory that would not otherwise be released by the garbage collector. Since the garbage collector might not get called for your object, you cannot use finalize( ) to perform necessary cleanup. For that you must create your own “cleanup” method. In the finalize( ) method for the class, check to make sure that the object has been cleaned up and throw a class derived from RuntimeException if it hasn’t, to indicate a programming error. Before relying on such a scheme, ensure that finalize( ) works on your system. (You might need to call System.gc( ) to ensure this behavior.)

  24. If an object must be cleaned up (other than by garbage collection) within a particular scope, use the following approach:
    Initialize the object and, if successful, immediately enter a try block with a finally clause that performs the cleanup.

  25. When overriding finalize( ) during inheritance, remember to call super.finalize( ).
    (This is not necessary if Object is your immediate superclass.) You should call super.finalize( ) as the final act of your overridden finalize( ) rather than the first, to ensure that base-class components are still valid if you need them.

  26. When you are creating a fixed-size container of objects, transfer them to an array
    —especially if you’re returning this container from a method. This way you get the benefit of the array’s compile-time type checking, and the recipient of the array might not need to cast the objects in the array in order to use them. Note that the base-class of the containers library, java.util.Collection, has two toArray( ) methods to accomplish this.

  27. Choose interfaces over abstract classes
    . If you know something is going to be a base class, your first choice should be to make it an interface, and only if you’re forced to have method definitions or member variables should you change it to an abstract class. An interface talks about what the client wants to do, while a class tends to focus on (or allow) implementation details.

  28. Inside constructors, do only what is necessary to set the object into the proper state
    . Actively avoid calling other methods (except for final methods) since those methods can be overridden by someone else to produce unexpected results during construction. (See Chapter 7 for details.) Smaller, simpler constructors are less likely to throw exceptions or cause problems.

  29. To avoid a highly frustrating experience, make sure that there is only one unpackaged class of each name anywhere in your classpath
    . Otherwise, the compiler can find the identically-named other class first, and report error messages that make no sense. If you suspect that you are having a classpath problem, try looking for .class files with the same names at each of the starting points in your classpath. Ideally, put all your classes within packages.

  30. Watch out for accidental overloading
    . If you attempt to override a base-class method and you don’t quite get the spelling right, you’ll end up adding a new method rather than overriding an existing method. However, this is perfectly legal, so you won’t get any error message from the compiler or run-time system—your code simply won’t work correctly.

  31. Watch out for premature optimization
    . First make it work, then make it fast—but only if you must, and only if it’s proven that there is a performance bottleneck in a particular section of your code. Unless you have used a profiler to discover a bottleneck, you will probably be wasting your time. The hidden cost of performance tweaks is that your code becomes less understandable and maintainable.

  32. Remember that code is read much more than it is written
    . Clean designs make for easy-to-understand programs, but comments, detailed explanations, and examples are invaluable. They will help both you and everyone who comes after you. If nothing else, the frustration of trying to ferret out useful information from the online Java documentation should convince you.
t
  1. D'une manière générale, suivre les conventions de codage de Sun. Celles-ci sont disponibles à java.sun.com/docs/codeconv/index.html (le code fourni dans ce livre respecte ces conventions du mieux que j'ai pu). Elles sont utilisées dans la majeure partie du code à laquelle la majorité des programmeurs Java seront exposés. Si vous décidez de vous en tenir obstinément à vos propres conventions de codage, vous rendrez la tâche plus ardue au lecteur. Quelles que soient les conventions de codage retenues, s'assurer qu'elles sont respectées dans tout le projet. Il existe un outil gratuit de reformatage de code Java disponible à home.wtal.de/software-solutions/jindent/.
  2. Quelles que soient les conventions de codage utilisées, cela change tout de les standardiser au sein de l'équipe (ou même mieux, au niveau de l'entreprise). Cela devrait même aller jusqu'au point où tout le monde devrait accepter de voir son style de codage modifié s'il ne se conforme pas aux règles de codage en vigueur. La standardisation permet de passer moins de temps à analyser la forme du code afin de se concentrer sur son sens.
  3. Suivre les règles standard de capitalisation. Capitaliser la première lettre des noms de classe. La première lettre des variables, méthodes et des objets (références) doit être une minuscule. Les identifiants doivent être formés de mots collés ensemble, et la première lettre des mots intermédiaires doit être capitalisée. Ainsi : CeciEstUnNomDeClasse, ceciEstUnNomDeMethodeOuDeVariable. Capitaliser toutes les lettres des identifiants déclarés comme static final et initialisés par une valeur constante lors de leur déclaration. Cela indique que ce sont des constantes dès la phase de compilation. Les packages constituent un cas à part - ils doivent être écrits en minuscules, même pour les mots intermédiaires. Les extensions de domaine (com, org, net, edu, etc.) doivent aussi être écrits en minuscules (ceci a changé entre Java 1.1 et Java 2).
  4. Ne pas créer des noms « décorés » de données membres privées. On voit souvent utiliser des préfixes constitués d'underscores et de caractères. La notation hongroise en est le pire exemple, où on préfixe le nom de variable par son type, son utilisation, sa localisation, etc., comme si on écrivait en assembleur ; et le compilateur ne fournit aucune assistance supplémentaire pour cela. Ces notations sont confuses, difficiles à lire, à mettre en oeuvre et à maintenir. Laisser les classes et les packages s'occuper de la portée des noms.
  5. Suivre une « forme canonique » quand on crée une classe pour un usage générique. Inclure les définitions pour equals(), hashCode(), toString(), clone() (implémentation de Cloneable), et implémenter Comparable et Serializable.
  6. Utiliser les conventions de nommage « get », « set » et « is » de JavaBeans pour les méthodes qui lisent et changent les variables private, même si on ne pense pas réaliser un JavaBean au moment présent. Non seulement cela facilitera l'utilisation de la classe comme un Bean, mais ce sont des noms standards pour ce genre de méthode qui seront donc plus facilement comprises par le lecteur.
  7. Pour chaque classe créée, inclure une méthode static public test() qui contienne du code testant cette classe. On n'a pas besoin d'enlever le code de test pour utiliser la classe dans un projet, et on peut facilement relancer les tests après chaque changement effectué dans la classe. Ce code fournit aussi des exemples sur l'utilisation de la classe.
  8. Il arrive qu'on ait besoin de dériver une classe afin d'accéder à ses données protected.Ceci peut conduire à percevoir un besoin pour de nombreux types de base. Si on n'a pas besoin de surtyper, il suffit de dériver une nouvelle classe pour accéder aux accès protégés, puis de faire de cette classe un objet membre à l'intérieur des classes qui en ont besoin, plutôt que dériver à nouveau la classe de base.
  9. Eviter l'utilisation de méthodes final juste pour des questions d'efficacité.Utiliser final seulement si le programme marche, mais pas assez rapidement, et qu'un profilage a montré que l'invocation d'une méthode est le goulot d'étranglement.
  10. Si deux classes sont associées d'une certaine manière (telles que les conteneurs et les itérateurs), essayer de faire de l'une une classe interne à l'autre. Non seulement cela fait ressortir l'association entre les classes, mais cela permet de réutiliser le nom de la classe à l'intérieur du même package en l'incorporant dans une autre classe. La bibliothèque des conteneurs Java réalise cela en définissant une classe interne Iterator à l'intérieur de chaque classe conteneur, fournissant ainsi une interface commune aux conteneurs. Utiliser une classe interne permet aussi une implémentation private. Le bénéfice de la classe interne est donc de cacher l'implémentation en plus de renforcer l'association de classes et de prévenir de la pollution de l'espace de noms.
  11. Quand on remarque que certaines classes sont liées entre elles, réfléchir aux gains de codage et de maintenance réalisés si on en faisait des classes internes l'une à l'autre. L'utilisation de classes internes ne va pas casser l'association entre les classes, mais au contraire rendre cette liaison plus explicite et plus pratique.
  12. C'est pure folie que de vouloir optimiser trop prématurément. En particulier, ne pas s'embêter à écrire (ou éviter) des méthodes natives, rendre des méthodes final, ou stresser du code pour le rendre efficace dans les premières phases de construction du système. Le but premier est de valider la conception, sauf si la conception spécifie une certaine efficacité.
  13. Restreindre autant que faire se peut les portées afin que la visibilité et la durée de vie des objets soient la plus faible possible. Cela réduit les chances d'utiliser un objet dans un mauvais contexte et la possibilité d'ignorer un bug difficile à détecter. Par exemple, supposons qu'on dispose d'un conteneur et d'une portion de code qui itère en son sein. Si on copie ce code pour l'utiliser avec un nouveau conteneur, il se peut qu'on en arrive à utiliser la taille de l'ancien conteneur comme borne supérieure du nouveau. Cependant, si l'ancien conteneur est hors de portée, l'erreur sera reportée lors de la compilation.
  14. Utiliser les conteneurs de la bibliothèque Java standard. Devenir compétent dans leur utilisation garantit un gain spectaculaire dans la productivité. Préférer les ArrayList pour les séquences, les HashSet pour les sets, les HashMap pour les tableaux associatifs, et les LinkedList pour les piles (plutôt que les Stack) et les queues.
  15. Pour qu'un programme soit robuste, chaque composant doit l'être. Utiliser tout l'arsenal d'outils fournis par Java : contrôle d'accès, exceptions, vérification de types, etc. dans chaque classe créée. Ainsi on peut passer sans crainte au niveau d'abstraction suivant lorsqu'on construit le système.
  16. Préférer les erreurs de compilation aux erreurs d'exécution. Essayer de gérer une erreur aussi près de son point d'origine que possible. Mieux vaut traiter une erreur quand elle arrive que générer une exception. Capturer les exceptions dans le gestionnaire d'exceptions le plus proche qui possède assez d'informations pour les traiter. Faire ce qu'on peut avec l'exception au niveau courant ; si cela ne suffit pas, relancer l'exception.
  17. Eviter les longues définitions de méthodes. Les méthodes doivent être des unités brèves et fonctionnelles qui décrivent et implémentent une petite part de l'interface d'une classe. Une méthode longue et compliquée est difficile et chère à maintenir, et essaye probablement d'en faire trop par elle-même. Une telle méthode doit, au minimum, être découpée en plusieurs méthodes. Cela peut aussi être le signe qu'il faudrait créer une nouvelle classe. De plus, les petites méthodes encouragent leur réutilisation à l'intérieur de la classe (quelquefois les méthodes sont grosses, mais elles doivent quand même ne réaliser qu'une seule opération).
  18. Rester « aussi private que possible ». Une fois rendu public un aspect de la bibliothèque (une méthode, une classe, une variable), on ne peut plus l'enlever. Si on le fait, on prend le risque de ruiner le code existant de quelqu'un, le forçant à le réécrire ou même à revoir sa conception. Si on ne publie que ce qu'on doit, on peut changer tout le reste en toute impunité ; et comme la modélisation est sujette à changements, ceci est une facilité de développement à ne pas négliger. De cette façon, les changements dans l'implémentation auront un impact minimal sur les classes dérivées. La privatisation est spécialement importante lorsqu'on traite du multithreading - seuls les champs private peuvent être protégés contre un accès non synchronized.
  19. Utiliser les commentaires sans restrictions, et utiliser la syntaxe de documentation de javadoc pour produire la documentation du programme. Cependant, les commentaires doivent ajouter du sens au code ; les commentaires qui ne font que reprendre ce que le code exprime clairement sont ennuyeux. Notez que le niveau de détails typique des noms des classes et des méthodes de Java réduit le besoin de commentaires.
  20. Eviter l'utilisation des « nombres magiques » - qui sont des nombres codés en dur dans le code. C'est un cauchemar si on a besoin de les changer, on ne sait jamais si « 100 » représente « la taille du tableau » ou « quelque chose dans son intégralité ». A la place, créer une constante avec un nom explicite et utiliser cette constante dans le programme. Cela rend le programme plus facile à comprendre et bien plus facile à maintenir.
  21. Quand on crée des constructeurs, réfléchir aux exceptions. Dans le meilleur des cas, le constructeur ne fera rien qui générera une exception. Dans le meilleur des cas suivant, la classe sera uniquement composée et dérivée de classes robustes, et aucun nettoyage ne sera nécessaire si une exception est générée. Sinon, il faut nettoyer les classes composées à l'intérieur d'une clause finally. Si un constructeur échoue dans la création, l'action appropriée est de générer un exception afin que l'appelant ne continue pas aveuglément en pensant que l'objet a été créé correctement.
  22. Si la classe nécessite un nettoyage lorsque le programmeur client en a fini avec l'objet, placer la portion de code de nettoyage dans une seule méthode bien définie - avec un nom explicite tel que cleanup() qui suggère clairement sa fonction. De plus, utiliser un flag boolean dans la classe pour indiquer si l'objet a été nettoyé afin que finalize() puisse vérifier la « condition de destruction » (cf Chapitre 4).
  23. La seule responsabilité qui incombe à finalize() est de vérifier la « condition de destruction » d'un objet pour le débuggage(cf Chapitre 4). Certains cas spéciaux nécessitent de libérer de la mémoire qui autrement ne serait pas restituée par le ramasse-miettes. Comme il existe une possibilité pour que le ramasse-miettes ne soit pas appelé pour un objet, on ne peut utiliser finalize() pour effectuer le nettoyage nécessaire. Pour cela il faut créer sa propre méthode de nettoyage. Dans la méthode finalize() de la classe, vérifier que l'objet a été nettoyé, et génèrer une exception dérivée de RuntimeException si cela n'est pas le cas, afin d'indiquer une erreur de programmation. Avant de s'appuyer sur un tel dispositif, s'assurer que finalize() fonctionne sur le système considéré (un appel à System.gc() peut être nécessaire pour s'assurer de ce fonctionnement).
  24. Si un objet doit être nettoyé (autrement que par le ramasse-miettes) à l'intérieur d'une portée particulière, utiliser l'approche suivante : initialiser l'objet et, en cas de succès, entrer immédiatement dans un block try avec une clause finally qui s'occupe du nettoyage.
  25. Lors de la redéfinition de finalize() dans un héritage, ne pas oublier d'appeler super.finalize()(ceci n'est pas nécessaire si Object est la classe parente immédiate). Un appel à super.finalize() doit être la dernière instruction de la méthode finalize() redéfinie plutôt que la première, afin de s'assurer que les composants de la classe de base soient toujours valides si on en a besoin.
  26. Lors de la création d'un conteneur d'objets de taille fixe, les transférer dans un tableau - surtout si on retourne le conteneur depuis une méthode. De cette manière on bénéficie de la vérification de types du tableau lors de la compilation, et le récipiendiaire du tableau n'a pas besoin de transtyper les objets du tableau pour les utiliser. Notez que la classe de base de la bibliothèque de conteneurs, java.util.Collection, possède deux méthodes toArray() pour accomplir ceci.
  27. Préférer les interfaces aux classes abstract.Si on sait que quelque chose va être une classe de base, il faut en faire une interface, et ne la changer en classe abstract que si on est obligé d'y inclure des définitions de méthodes et des variables membres. Une interface parle de ce que le client veut faire, tandis qu'une classe a tendance à se focaliser sur (ou autorise) les détails de l'implémentation.
  28. A l'intérieur des constructeurs, ne faire que ce qui est nécessaire pour mettre l'objet dans un état stable. Eviter autant que faire se peut l'appel à d'autres méthodes (les méthodes final exceptées), car ces méthodes peuvent être redéfinies par quelqu'un d'autre et produire des résultats inattendus durant la construction (se référer au chapitre 7 pour plus de détails). Des constructeurs plus petits et plus simples ont moins de chances de générer des exceptions ou de causer des problèmes.
  29. Afin d'éviter une expérience hautement frustrante, s'assurer qu'il n'existe qu'une classe non packagée de chaque nom dans tout le classpath. Autrement, le compilateur peut trouver l'autre classe de même nom d'abord, et renvoyer des messages d'erreur qui n'ont aucun sens. Si un problème de classpath est suspecté, rechercher les fichiers .class avec le même nom à partir de chacun des points de départ spécifiés dans le classpath, l'idéal étant de mettre toutes les classes dans des packages.
  30. Surveiller les surcharges accidentelles. Si on essaye de redéfinir une méthode de la classe de base et qu'on se trompe dans l'orthographe de la méthode, on se retrouve avec une nouvelle méthode au lieu d'une méthode existante redéfinie. Cependant, ceci est parfaitement légal, et donc ni le compilateur ni le système d'exécution ne signaleront d'erreur - le code ne fonctionnera pas correctement, c'est tout.
  31. Ne pas optimiser le code trop prématurément. D'abord le faire marcher, ensuite l'optimiser - mais seulement si on le doit, et seulement s'il est prouvé qu'il existe un goulot d'étranglement dans cette portion précise du code. A moins d'avoir utilisé un profileur pour découvrir de tels goulots d'étranglement, vous allez probablement perdre votre temps pour rien. De plus, à force de triturer le code pour le rendre plus rapide, il devient moins compréhensible et maintenable, ce qui constitue le coût caché de la recherche de la performance à tout prix.
  32. Se rappeler que le code est lu bien plus souvent qu'il n'est écrit. Une modélisation claire permet de créer des programmes faciles à comprendre, mais les commentaires, des explications détaillées et des exemples sont inestimables. Ils vous seront utiles autant qu'aux personnes qui viendront après vous. Et si cela ne vous suffit pas, la frustration ressentie lorsqu'on tente de dénicher une information utile depuis la documentation online de Java devrait vous convaincre.
t t t
[85] Explained to me by Andrew Koenig. [ Previous Chapter ] [ Short TOC ] [ Table of Contents ] [ Index ] [ Next Chapter ] Last Update:04/24/2000 t [85]Qui m'a été expliquée par Andrew Koenig.
t t t
t t t
t t
\\\
    
t t t
t
     
Sommaire Le site de Bruce Eckel