Points clés
1. Usages des usines statiques plutôt que des constructeurs : nommage, contrôle et flexibilité
Proposer une méthode d’usine statique au lieu d’un constructeur public présente à la fois des avantages et des inconvénients.
Avantages liés au nommage. Les méthodes d’usine statiques permettent d’utiliser des noms explicites, contrairement aux constructeurs, ce qui améliore la lisibilité et la facilité d’utilisation du code. Cela s’avère particulièrement utile lorsque les paramètres du constructeur ne précisent pas clairement l’objet créé. Par exemple, une méthode nommée BigInteger.probablePrime() est plus parlante qu’un constructeur BigInteger(int, int, Random).
Contrôle des instances. Les usines statiques ne sont pas obligées de créer un nouvel objet à chaque appel, ce qui permet aux classes immuables de mettre en cache des instances et d’éviter des créations inutiles. Cela peut considérablement améliorer les performances, notamment pour des objets fréquemment demandés. La méthode Boolean.valueOf(boolean) illustre bien ce principe, puisqu’elle retourne toujours les mêmes instances Boolean.TRUE ou Boolean.FALSE.
Flexibilité des sous-types. Les usines statiques peuvent retourner des objets de n’importe quel sous-type de leur type de retour, ce qui permet aux API de fournir des objets sans rendre leurs classes publiques. Cela favorise une API compacte et autorise la variation de la classe retournée selon les paramètres d’entrée ou même entre différentes versions, renforçant ainsi la maintenabilité et la flexibilité. C’est le fondement des frameworks de fournisseurs de services comme la Java Cryptography Extension (JCE).
2. Implémentation du singleton : constructeurs privés et accès public
Un singleton est simplement une classe instanciée une seule fois [Gamma98, p. 127].
Garantir l’unicité. Les singletons, représentant des composants uniques du système, sont assurés en rendant le constructeur privé et en fournissant un membre statique public pour y accéder. Cela garantit qu’une seule instance existe. Deux approches courantes existent : utiliser un champ final ou une méthode d’usine statique.
Approche par champ final. Le membre statique public est un champ final initialisé avec l’instance singleton. Cette méthode est simple et explicite quant au fait que la classe est un singleton. Par exemple :
public static final Elvis INSTANCE = new Elvis();
Approche par usine statique. Une méthode d’usine statique publique retourne l’instance singleton. Cela offre la flexibilité de modifier l’implémentation sans changer l’API. Par exemple :
public static Elvis getInstance() { return INSTANCE; }
Considération liée à la sérialisation. Pour préserver la propriété singleton lors de la sérialisation, il faut fournir une méthode readResolve qui retourne l’instance existante, empêchant ainsi la création de nouvelles instances lors de la désérialisation. Cela garantit que le singleton reste unique même après ces opérations.
3. Classes utilitaires : garantir la non-instanciabilité
Ces classes utilitaires ne sont pas conçues pour être instanciées : une instance serait dénuée de sens.
Rôle des classes utilitaires. Les classes utilitaires regroupent des méthodes et champs statiques, organisant des fonctionnalités liées à des valeurs primitives, des tableaux ou des objets implémentant des interfaces spécifiques. Elles ne sont pas destinées à être instanciées, car une instance serait inutile. On peut citer java.lang.Math ou java.util.Arrays en exemples.
Empêcher l’instanciation. Pour garantir la non-instanciabilité, une pratique simple consiste à inclure un constructeur privé explicite unique. Cela empêche le compilateur de générer un constructeur public sans paramètre par défaut, assurant que la classe ne peut être instanciée de l’extérieur.
Effets secondaires. Cette pratique empêche également la sous-classification, car les sous-classes n’auraient pas de constructeur accessible pour invoquer celui de la superclasse. Il est judicieux d’ajouter un commentaire expliquant la raison de ce constructeur privé, car cela peut sembler contre-intuitif.
4. Réutilisation des objets : efficacité et immutabilité
Il est souvent approprié de réutiliser un objet unique plutôt que de créer à chaque fois un nouvel objet fonctionnellement équivalent.
Avantages de la réutilisation. Réutiliser des objets, en particulier immuables, est plus efficace et élégant que d’en créer de nouveaux à répétition. En effet, la création d’objets et la collecte des déchets peuvent être coûteuses, surtout pour des objets lourds. Par exemple, utiliser String s = "silly"; est préférable à String s = new String("silly");.
Objets immuables. Les objets immuables peuvent toujours être réutilisés en toute sécurité. Par exemple, Boolean.valueOf(String) est préférable à Boolean(String) car la première méthode peut réutiliser des instances existantes.
Objets mutables. Les objets mutables peuvent être réutilisés s’ils ne sont pas modifiés. Par exemple, initialiser statiquement des instances de Calendar et Date une seule fois, plutôt que de les recréer à chaque appel de méthode, peut améliorer significativement les performances.
5. Gestion de la mémoire : éliminer les références obsolètes
Une référence obsolète est simplement une référence qui ne sera plus jamais déréférencée.
Fuites de mémoire. Les fuites de mémoire dans les langages à collecte automatique surviennent lorsque des références à des objets sont conservées involontairement, empêchant leur collecte. Cela peut entraîner une dégradation des performances, une augmentation de l’empreinte mémoire, voire une erreur OutOfMemoryError.
Identifier les fuites. Une source fréquente de fuites est constituée par les classes qui gèrent leur propre mémoire, comme une classe Stack. Lorsqu’un élément est libéré, toutes les références aux objets qu’il contient doivent être mises à null.
Solutions. Pour éviter les fuites, il faut mettre à null les références dès qu’elles deviennent obsolètes. Il faut aussi être vigilant avec les caches, qui peuvent conserver des références longtemps après qu’elles soient devenues inutiles. Utiliser WeakHashMap pour les caches dont les entrées sont pertinentes uniquement tant qu’il existe des références aux clés en dehors du cache.
6. Contrat de la méthode equals : réflexivité, symétrie et transitivité
La méthode equals implémente une relation d’équivalence.
Relation d’équivalence. Lorsqu’on redéfinit la méthode equals, il est crucial de respecter son contrat général, qui définit une relation d’équivalence. Ce contrat inclut la réflexivité (x.equals(x) doit retourner vrai), la symétrie (x.equals(y) doit être vrai si et seulement si y.equals(x) est vrai) et la transitivité (si x.equals(y) et y.equals(z) sont vrais, alors x.equals(z) doit être vrai).
Violation de la symétrie. Ne pas respecter la symétrie peut entraîner des comportements imprévisibles lorsque les objets sont utilisés dans des collections. Par exemple, une classe CaseInsensitiveString qui tente d’interagir avec des chaînes ordinaires peut violer la symétrie.
Violation de la transitivité. La transitivité peut être violée lorsqu’on étend une classe instanciable en ajoutant un aspect affectant les comparaisons equals. Pour éviter cela, il est préférable de privilégier la composition à l’héritage.
Non-nullité. La méthode equals doit retourner faux pour toute référence non nulle x lorsque x.equals(null) est invoqué. L’opérateur instanceof gère cela automatiquement.
7. Redéfinition de hashCode : cohérence avec equals
Il faut redéfinir hashCode dans toute classe qui redéfinit equals.
Importance de hashCode. Redéfinir hashCode est essentiel lorsqu’on redéfinit equals afin de garantir que des objets égaux ont des codes de hachage égaux. Ne pas le faire viole le contrat général de Object.hashCode et empêche la classe de fonctionner correctement dans des collections basées sur le hachage comme HashMap ou HashSet.
Fonction de hachage légale mais médiocre. Une méthode hashCode triviale qui retourne toujours la même valeur est légale mais entraîne de mauvaises performances pour les tables de hachage, car tous les objets sont placés dans le même compartiment.
Recette pour une bonne fonction de hachage. Une bonne fonction de hachage tend à produire des codes différents pour des objets différents. Une recette simple consiste à :
- Initialiser un entier
resultà une valeur constante non nulle (par exemple 17). - Pour chaque champ significatif
fde l’objet, calculer un code de hachagecet l’intégrer dansresultparresult = 37 * result + c;. - Retourner
result.
8. Redéfinition de toString : représentation concise et informative
La méthode toString est automatiquement invoquée lorsque votre objet est passé à println, à l’opérateur de concaténation de chaînes (+), ou, depuis la version 1.4, à assert.
Importance de toString. Redéfinir la méthode toString fournit une représentation textuelle concise et informative d’un objet, rendant la classe plus agréable à utiliser. Cela est particulièrement utile pour le débogage et la journalisation.
Contenu de toString. Lorsque c’est pertinent, la méthode toString doit retourner toutes les informations intéressantes contenues dans l’objet. Si l’objet est volumineux ou contient un état peu propice à une représentation textuelle, un résumé doit être fourni.
Spécification du format. Lors de l’implémentation de toString, il faut décider s’il faut spécifier le format de la valeur retournée dans la documentation. Spécifier le format offre une représentation standard et non ambiguë, mais limite la flexibilité pour des modifications futures. Si le format est spécifié, il faut fournir un constructeur ou une usine statique correspondante.
9. Interface Cloneable : mise en œuvre judicieuse
L’interface Cloneable était destinée à être un mixin (Item 16) pour indiquer que les objets autorisent le clonage.
Limites de Cloneable. L’interface Cloneable ne déclare pas de méthode clone, et la méthode clone de Object est protégée. Cela rend difficile l’appel de clone sur un objet simplement parce qu’il implémente Cloneable.
Contrat du clone. Le contrat général de la méthode clone est faible, indiquant qu’elle crée et retourne une copie de l’objet. Cependant, il ne précise pas comment cette copie doit être réalisée ni si les constructeurs doivent être appelés.
Implémentation de Cloneable. Pour bien implémenter Cloneable, une classe doit redéfinir la méthode clone en public, appelant d’abord super.clone, puis corrigeant les champs qui nécessitent une copie. Cela signifie généralement copier les objets mutables constituant la structure interne « profonde » de l’objet.
Alternatives à Cloneable. Une bonne approche pour copier un objet est de fournir un constructeur de copie ou une méthode d’usine statique. Ces méthodes n’utilisent pas un mécanisme de création d’objet externe risqué et ne contredisent pas l’usage approprié des champs finals.
10. Interface Comparable : ordre naturel
En implémentant Comparable, une classe indique que ses instances ont un ordre naturel.
Avantages de Comparable. Implémenter l’interface Comparable permet à une classe d’interagir avec des algorithmes génériques et des collections qui dépendent de l’ordre, comme Arrays.sort ou TreeSet.
Contrat de compareTo. Le contrat général de la méthode compareTo est similaire à celui de equals, exigeant réflexivité, transitivité et symétrie. Il est fortement recommandé que (x.compareTo(y)==0) == (x.equals(y)).
Écriture d’une méthode compareTo. Lors de l’écriture de compareTo, il faut comparer les champs de référence en appelant récursivement leur méthode compareTo. Les champs primitifs sont comparés avec les opérateurs relationnels < et >. Si la classe possède plusieurs champs significatifs, ils doivent être comparés dans l’ordre de leur importance.
11. Minimiser l’accessibilité : cacher l’information
Le facteur le plus important qui distingue un module bien conçu d’un module mal conçu est le degré auquel le module cache ses données internes et autres détails d’implémentation aux autres modules.
Avantages du masquage de l’information. Le masquage de l’information, ou encapsulation, découple les modules, leur permettant d’être développés, testés, optimisés et modifiés indépendamment. Cela accélère le développement, facilite la maintenance, augmente la réutilisation du logiciel et diminue les risques.
Niveaux d’accès. Le langage Java fournit des modificateurs d’accès (private, package-private, protected et public) pour aider au masquage de l’information. La règle générale est de rendre chaque classe ou membre aussi inaccessible que possible.
Champs publics. Les classes publiques devraient rarement, voire jamais, avoir des champs publics (contrairement aux méthodes publiques). L’exception concerne les champs public static final contenant des valeurs primitives ou des références à des objets immuables.
12. Favoriser l’immuabilité : simplicité et sécurité des threads
Une classe immuable est simplement une classe dont les instances ne peuvent pas être modifiées.
Avantages de l’immuabilité. Les classes immuables sont plus faciles à concevoir, implémenter et utiliser que les classes mutables. Elles sont moins sujettes aux erreurs et plus sûres.
Règles pour l’immuabilité. Pour rendre une classe immuable :
- Ne pas fournir de méthodes modifiant l’objet (mutateurs).
- S’assurer qu’aucune méthode ne peut être redéfinie (rendre la classe finale ou utiliser des constructeurs privés et des usines statiques).
- Rendre tous les champs finals.
- Rendre tous les champs privés.
- Garantir un accès exclusif à tout composant mutable (faire des copies défensives).
Inconvénients de l’immuabilité. Le seul véritable inconvénient des classes immuables est qu’elles nécessitent un objet distinct pour chaque valeur différente. Pour y remédier, il est conseillé de fournir des constantes publiques static final pour les valeurs fréquemment utilisées et d’envisager une classe compagnon mutable publique.
Résumé des avis
Effective Java est unanimement reconnu comme une lecture incontournable pour les développeurs Java, salué pour ses explications claires et ses conseils pratiques. Les lecteurs apprécient l’expertise de Bloch ainsi que l’accent mis sur l’écriture d’un code de haute qualité, facile à maintenir. Nombreux sont ceux qui considèrent cet ouvrage comme un passage obligé, tant pour les débutants que pour les programmeurs expérimentés. Le livre aborde de nombreuses fonctionnalités de Java, présente les meilleures pratiques et met en garde contre les erreurs fréquentes. Si certains jugent que certaines parties sont un peu dépassées ou trop techniques, la majorité s’accorde à dire qu’il améliore considérablement les compétences en programmation et la compréhension des subtilités du langage Java.
Les lecteurs ont aussi lu
FAQ
What's Effective Java about?
- Java best practices: Effective Java by Joshua Bloch is a comprehensive guide focused on best practices for Java programming. It aims to help developers write clear, correct, robust, and reusable code.
- Practical advice: The book is tailored for developers familiar with Java who wish to enhance their coding skills. It covers a wide range of topics, including object creation, methods, classes, and interfaces.
- Structured format: The content is organized into distinct items, each addressing a specific aspect of Java programming. This structure allows readers to easily navigate and apply the advice.
Why should I read Effective Java?
- Improve coding skills: Reading Effective Java can significantly enhance your Java programming abilities by providing insights not commonly found in other resources.
- Expert insights: Joshua Bloch, a key contributor to the Java platform, shares his extensive experience through practical examples and rules of thumb.
- Code quality focus: The book emphasizes writing code that is not only functional but also maintainable and efficient, leading to fewer bugs and easier code management.
What are the key takeaways of Effective Java?
- Clarity and simplicity: The book stresses the importance of writing clear and simple code, ensuring understandability and maintainability over time.
- Composition over inheritance: Bloch advises using composition instead of inheritance to create more flexible and robust code structures, reducing dependencies and potential fragility.
- Static factory methods: The book recommends using static factory methods for object creation, offering better readability and flexibility.
What are some specific rules from Effective Java?
- Static Factory Methods: Consider providing static factory methods instead of constructors for better naming and performance.
- Singleton Property: Enforce the singleton property with a private constructor to ensure a class has only one instance.
- Override Equals and HashCode: Always override
hashCodewhen you overrideequalsto ensure correct functioning of hash-based collections.
How does Effective Java address object creation?
- Static Factory Methods: Suggests using static factory methods for object creation instead of constructors for better naming and performance.
- Avoiding Duplicate Objects: Advises against creating duplicate objects, especially for immutable types, to improve memory management.
- Enforcing Noninstantiability: Discusses enforcing noninstantiability with a private constructor for utility classes to prevent unnecessary instances.
What are the benefits of immutability discussed in Effective Java?
- Thread Safety: Immutable objects are inherently thread-safe, eliminating the need for synchronization and simplifying concurrent programming.
- Simplicity and Clarity: Immutability leads to simpler code, making reasoning about the code easier and reducing the likelihood of bugs.
- Reuse and Performance: Immutable objects can be shared freely, allowing for efficient memory usage and reduced overhead of object creation.
How does Effective Java suggest handling exceptions?
- Use Exceptions for Exceptional Conditions: Advises using exceptions only for exceptional conditions, not for regular control flow, to maintain clarity and avoid performance penalties.
- Document Exceptions: Emphasizes documenting exceptions that a method can throw to improve code readability and help users handle potential errors.
- Favor Standard Exceptions: Recommends using standard exceptions provided by the Java platform for consistency and familiarity.
What is the significance of the Effective Java item on interfaces?
- Define Types with Interfaces: Emphasizes using interfaces to define types, not to export constants, keeping the API clean and focused on behavior.
- Favor Interfaces Over Abstract Classes: Advises using interfaces for flexibility, allowing multiple implementations without single inheritance constraints.
- Avoid Constant Interfaces: Warns against using constant interfaces, suggesting utility classes instead to group constants.
How does Effective Java recommend designing for inheritance?
- Design and Document for Inheritance: If a class is designed for inheritance, it must document the effects of overriding methods to ensure subclass understanding.
- Prohibit Unnecessary Inheritance: Suggests making classes final or using private constructors to prevent unintended inheritance, reducing fragility.
- Provide Protected Methods: Expose protected methods judiciously to allow subclass extension while avoiding excessive implementation detail exposure.
How does Effective Java address concurrency?
- Synchronize Access to Shared Data: Emphasizes synchronizing access to shared mutable data to prevent data corruption and ensure thread safety.
- Avoid Excessive Synchronization: Advises minimizing work within synchronized blocks to enhance performance and avoid deadlocks.
- Document Thread Safety: Stresses the importance of documenting the thread safety of classes and methods for safe multithreaded use.
What is the double-checked locking idiom, and why is it problematic?
- Lazy Initialization Purpose: Used to reduce synchronization overhead when lazily initializing a resource by checking if it's initialized before locking.
- Inconsistent States Risk: Without proper synchronization, threads may see a partially constructed object, leading to inconsistent states and bugs.
- Avoidance Recommendation: Advises against using double-checked locking due to its complexity and potential pitfalls, suggesting simpler alternatives.
What are the best quotes from Effective Java and what do they mean?
- "Programs must be written for people to read, and only incidentally for machines to execute." Emphasizes writing code understandable to humans, not just optimized for machines.
- "The single most important factor that distinguishes a well-designed module from a poorly designed one is the degree to which the module hides its internal data." Highlights the principle of encapsulation for maintainable and flexible code.
- "Don't create a new object when you should reuse an existing one." Encourages object reuse, especially for immutable objects, to enhance performance and reduce memory usage.