FAQ Langage JavaConsultez toutes les FAQ
Nombre d'auteurs : 42, nombre de questions : 297, dernière mise à jour : 19 septembre 2017 Ajouter une question
Cette FAQ a été réalisée à partir des questions fréquemment posées sur le forum Java de http://java.developpez.com ainsi que l'expérience personnelle des auteurs.
Nous tenons à souligner que cette FAQ ne garantit en aucun cas que les informations qu'elle propose sont correctes. Les auteurs font leur maximum, mais l'erreur est humaine. Cette FAQ ne prétend pas non plus être complète. Si vous trouvez une erreur, ou que vous souhaitez nous aider en devenant rédacteur, lisez ceci.
Sur ce, nous vous souhaitons une bonne lecture.
- Qu'est-ce qu'une classe ?
- Qu'est-ce qu'un package ?
- Qu'est-ce que l'héritage ?
- Qu'est-ce qu'une classe abstraite ?
- Qu'est-ce qu'une classe interne ?
- Quels sont les différents types de classes internes ?
- Qu'est-ce qu'une interface ?
- Qu'est-ce que la sérialisation ?
- Quelles sont les règles à respecter pour redéfinir/implémenter une méthode ?
- Qu'est-ce que la surcharge des méthodes ?
- Qu'est-ce qu'un getter ?
- Qu'est-ce qu'un setter ?
- Qu'est-ce que les Generics (types paramétrés) ?
- Qu'est-ce que l'auto-boxing/auto-unboxing ?
- Qu'est-ce qu'une annotation ?
- Qu'est-ce qu'une enum (type énuméré) ?
- Qu'est-ce qu'une constante ?
- Qu'est-ce qu'un membre « synthetic » ?
- Qu'est-ce qu'une méthode « bridge » ?
- Comment spécifier qu'un paramètre doit implémenter plusieurs interfaces ?
- Qu'est-ce qu'une interface fonctionnelle ?
- Qu'est-ce qu'un lambda ?
- Qu'est-ce qu'une référence de méthode ?
- Qu'est-ce qu'une méthode par défaut (Defender Method) ?
- Qu'est-ce qu'un objet immuable ?
- Java est-il Little-Endian ou Big-Endian ?
- Qu'est-ce qu'un singleton ?
- Pourquoi et comment redéfinir la méthode equals() ?
- Pourquoi et comment redéfinir la méthode hashCode() ?
- Qu'est-ce qu'un POJO ?
- Comment copier un objet ?
- Comment cloner un objet ?
- Comment cloner un objet en profondeur ?
Une classe est constituée :
- de données, ce qu'on appelle des attributs ou membres ;
- de procédures et/ou des fonctions, ce qu'on appelle des méthodes.
Une classe est un modèle de définition pour des objets :
- ayant même structure (même ensemble d'attributs) ;
- ayant même comportement (mêmes méthodes) ;
- ayant une sémantique commune.
Les objets sont des représentations dynamiques, du modèle défini pour eux au travers de la classe (instanciation) :
- une classe permet généralement d'instancier (créer) plusieurs objets ;
- chaque objet est instance d'une classe et une seule.
La classe suivante contient trois attributs (puissance, estDemarre et vitesse). Elle dispose également d'un sélecteur (deQuellePuissance) et de modificateurs (demarre et accelere) :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class Voiture { private int puissance; private boolean estDemarree; private double vitesse; public int deQuellePuissance() { return puissance; } public void demarre() { estDemarree = true; } public void accelere(double v) { if (estDemarree) { vitesse = vitesse + v } } } |
Un package ou paquetage est un moyen d'organiser les classes de votre code. Cette unité organisationnelle est à rapprocher du mécanisme des espaces de nommage (naming space) qui existe dans d'autres langages même si les packages fonctionnent un peu différemment.
En Java, un package correspond toujours à un ou plusieurs répertoires ayant exactement le même nom, situés sur le disque, dans une ou plusieurs archives JAR ou plus généralement sur le CLASSPATH. Les classes qui sont déclarées comme appartenant à ce package doivent être physiquement situées dans un de ces répertoires.
Un package est identifié par son nom court (le nom du répertoire lui-même) mais c'est surtout son nom long qui le rend unique. Le nom long ou nom complet est constitué du cheminement complet des noms de tous les packages intermédiaires depuis la racine du CLASSPATH jusqu'au répertoire final, séparés par des points (.). Par exemple, si nous avons un répertoire d situé dans l’arborescence suivante :
Code Texte : | Sélectionner tout |
1 2 3 4 | a ∟ b ∟ c ∟ d |
le package final aura pour pour nom court d, mais son nom long est a.b.c.d. Tous les éléments contenus dans ce répertoire font partie du package a.b.c.d. Si nous retrouvons exactement la même arborescence ailleurs sur le CLASSPATH, alors les éléments de ce second répertoire feront également partie du package a.b.c.d.
S'il existe un autre package nommé d, mais dont le nom long est différent, par exemple a.b.e.f.g.d), alors il ne s'agit pas du même package puisque l'arborescence des répertoires est totalement différente !
Déclaration
Il nous faut donc une arborescence de répertoire décrivant le package. Quand une classe est écrite dans ce package, son entête doit obligatoirement débuter par :
Code Java : | Sélectionner tout |
package <nom long du package>;
Par exemple :
Code Java : | Sélectionner tout |
1 2 3 4 5 | package a.b.c.d; public class Voiture { // Déclaration du reste de la classe. [...] |
Cette classe a pour nom court Voiture, mais pour nom long ou nom complet a.b.c.d.Voiture.
Le code de la classe ne compilera pas si la déclaration du package dans l’entête ne correspond pas au chemin des répertoires dans lesquels se trouve le fichier source.
Package sans nom
La racine de l'arborescence des packages ne porte pas de nom, on dit qu'il s'agit du package sans nom ou package par défaut. Il peut être difficile de référencer des classes ou des ressources qui sont placées sur le package racine ; il est donc recommandé de ne pas l'utiliser.
Nommage
Par défaut, le nom des packages est limité par l'ensemble des caractères communément admis pour nommer des répertoires dans un système de fichiers. De plus, il est impossible d'utiliser le symbole - tandis que le symbole _ est admis. Le nom court d'un package ne peut pas non plus débuter par un chiffre (mais on peut trouver des chiffres à des positions ultérieures). La casse (majuscules/minuscules) utilisée dans l’écriture du nom des packages et des répertoires est importante.
Par convention, on écrit le nom des packages en minuscules.
Il existe de plus quelques règles et interdictions concernant l'API Java standard :
- les packages dont l'arborescence commence par java sont réservés à l’implémentation de l'API standard dans la JVM (ce qui inclut les classes de base, les collections, AWT, etc.) ;
- les packages dont l'arborescence commence par javax sont réservés au mécanisme des extensions standard de l'API Java (ce qui inclut Swing, l'API d'impression, l'API XML, etc.) ;
- les packages dont l'arborescence commence par javafx sont réservés au toolkit JavaFX.
Vous devez absolument éviter de les utiliser.
De la même manière, vous devez éviter d'utiliser une arborescence de package qui pourrait déjà être prise par un projet préexistant ou une bibliothèque tierce sous peine de conflit dans les définitions de vos classes.
De manière à créer des arborescences de stockage de classes uniques, Sun Microsystems, puis Oracle Corporation suggèrent aux entreprises et organisations d'utiliser le chemin inverse de celui de leur nom de domaine. Par exemple, Oracle utilise régulièrement les noms de package com.sun et com.oracle pour la base des arborescences de ses projets. De manière similaire, un projet Java développé par l’équipe de Développez dont le site est disponible sur l'URL http://java.developpez.com pourrait très bien utiliser le nom de package com.developpez.java pour son arborescence.
Pour les particuliers, vous pouvez utiliser votre adresse mél, votre nom de famille ou encore votre surnom/avatar dans des jeux en ligne ou des réseaux sociaux pour composer la base de votre package personnel. Par exemple : com.monfournisseur.dupont74.jean.
Accès
Pour accéder aux classes d'un package, il existe deux solutions :
- il suffit d’écrire le nom complet (ou nom long) de la classe à chaque endroit où ce type est utilisé. Par exemple :
Code Java : Sélectionner tout a.b.c.d.Voiture maVoiture = new a.b.c.d.Voiture();
- on peut utiliser une directive import pour que le compilateur accepte d'utiliser le nom court de la classe. Par exemple :
Code Java : Sélectionner tout 1
2
3
4
5import a.b.c.d.Voiture; [...] Voiture maVoiture = new Voiture();
Il est également possible d’utiliser la syntaxe large (wildcard) en utilisant le caractère étoile (*). Par exemple :Code Java : Sélectionner tout 1
2
3
4
5import a.b.c.d.*; [...] Voiture maVoiture = new Voiture();
Cela permet, non seulement d'utiliser le nom court de la classe Voiture, mais également celui de toutes les autres classes disponibles dans le package a.b.c.d. À la compilation, le compilateur cherchera à remplacer les noms courts par les noms longs appropriés. Des erreurs seront générées en cas d’ambigüité.
Note : il n'y a pas besoin d'importer les classes du package java.lang. Étant donné qu'il s'agit des classes de base au cœur de l'API Java, ce package est automatiquement importé de manière implicite. Ainsi, il n'y a pas lieu de faire :
Code Java : | Sélectionner tout |
1 2 3 4 5 | import java.lang.System; [...] System.out.println("Salut !"); |
Ou :
Code Java : | Sélectionner tout |
java.lang.System.out.println("Salut !");
On peut directement écrire à la place :
Code Java : | Sélectionner tout |
System.out.println("Salut !");
Conflits
En cas de conflit dans le nom d'une classe, il n'est plus possible de procéder à des importations. Pour résoudre l’ambigüité, il suffit de se forcer à utiliser le nom complet de la classe. Par exemple :
Code Java : | Sélectionner tout |
1 2 | java.util.Date uneDateJava = [...] java.sql.Date uneDateSQL = [...] |
Ici, étant donné que nous avons utilisé le nom complet des classes, il n'y a pas d’ambigüité entre les classes java.util.Date et java.sql.Date. Un package sert donc à identifier une classe de manière unique. Ainsi, des classes portant le même nom, mais placées dans des packages différents ne sont pas la même classe : java.util.Date et java.sql.Date sont deux classes bien distinctes !
Attention : importer un package, n'importe pas automatiquement ses sous-packages. Un sous-package est totalement indépendant de son package parent. On fera donc :
Code Java : | Sélectionner tout |
1 2 3 4 | import a; import a.b; import a.b.c; import a.b.c.d; |
L'héritage est un des principaux concepts de la programmation orientée objet. L'héritage permet de définir une relation de « filiation » entre classes. Ainsi une classe fille (ou sous-classe) étend une classe mère (ou super-classe). L'héritage permet en général de spécialiser une classe.
Pour indiquer qu'une classe hérite d'une autre, il faut utiliser le mot-clé extends :
Code Java : | Sélectionner tout |
1 2 3 | public class Fille extends Mere{ // Ici, le code spécifique de la classe fille. } |
L'héritage implique plusieurs choses.
La fille hérite du type de la mère
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public class Couleur(){ String nom; public Couleur(String nom){ this.nom = nom; } } public class Rouge extends Couleur{ public Rouge(){ super("rouge"); } } public class AutreClasse(){ public void setCouleur(Couleur uneCouleur){ [...] } public static void main(String[] args){ AutreClasse ac = new AutreClasse(); ac.setCouleur(new Couleur("vert")); ac.setCouleur(new Rouge()); } } |
Ici, la classe Rouge étend la classe Couleur, cela signifie qu'une instance de la classe Rouge est également une instance de la classe Couleur. Il est donc possible de passer une instance de la classe Rouge en paramètre d'une méthode qui accepte une instance de la classe Couleur.
La fille hérite de plusieurs attributs, méthodes et constructeurs de la mère
L'accès à ces attributs ou méthodes se fait avec le mot-clé super. Voici comment est définie l'accessibilité aux composantes de la super-classe, en fonction des modificateurs :
Mot-clé | Accès |
public | Oui |
Oui, seulement si la classe fille se trouve dans le même package que la super-classe. | |
protected | Oui, quel que soit le package de définition de la classe fille. |
private | Non. |
Une classe abstraite est une classe incomplète. Elle regroupe un ensemble de variables et de méthodes, mais certaines de ses méthodes ne contiennent pas d'instruction, elles devront être définies dans une classe héritant de cette classe abstraite. Une classe abstraite doit être marquée du mot-clé abstract.
Toute méthode déclarée abstraite, doit également être marquée du mot-clé abstract et ne doit pas avoir de corps. L’implémentation de ces méthodes est laissée aux soins des classes filles de cette classe.
Une classe qui contient au moins une méthode abstraite ou qui hérite d'une classe abstraite et n’implémente aucune des méthodes abstraites de sa classe mère doit obligatoirement être elle-même marquée du mot-clé abstract.
À quoi ça sert ?
En général, cela sert à définir les grandes lignes du comportement d'une classe d'objets sans forcer l'implémentation des détails de l'algorithme. Prenons l'exemple d'une chaîne de montage automobile, laquelle sort un modèle de voiture particulier. On peut choisir de faire une nouvelle chaîne de montage pour le modèle à pare-chocs métallisés, une pour le modèle à pare-chocs en plastique, etc. Ou on peut décider de faire une chaîne de montage générique, de laquelle sortiront des véhicules non finis, que l'on terminera sur d'autres petites chaînes.
Comment ça marche ?
Comme indiqué ci-dessus, une classe abstraite est incomplète, elle ne peut donc pas être instanciée et doit être héritée. Certaines classes abstraites disposeront de méthodes abstraites (que les classes enfants devront implémenter). Voici un exemple de déclaration :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | /** La classe abstraite employée : */ public abstract class Employe { [...] /** définition d'une méthode abstraite * on notera qu'il n'y a pas d'instruction et un point-virgule à la fin */ public abstract void licencier(); } |
Ici, la classe Employe est abstraite et déclare une méthode abstraite nommée licencier().
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // Class Ouvrier public class Ouvrier extends Employe { // définition du code de licencier @Override public void licencier() { // On définit le code. System.out.println("Dehors !"); } } // Class Patron public class Patron extends Employe { // Définition du code de la méthode licencier. @Override public void licencier() { System.out.println("Veuillez comprendre que dans la conjoncture actuelle ... !"); } } |
Ici, les classes Ouvrier et Patron héritent toutes les deux de la classe Employe. Comme ces deux classes ne sont pas abstraites, elles doivent chacune définir la méthode licencier() et chacune dispose d'une implémentation différente.
Il est également possible d'instancier une classe abstraite directement en créant une classe anonyme qui implémente les méthodes déclarées abstraites dans sa classe mère. Par exemple :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 | Employe bob = new Employe() { @Override public void licencier() { System.out.println("Cherche un nouveau boulot."); } }; |
Ici, nous avons créé une nouvelle classe anonyme qui hérite de la classe Employe.
Note : lorsqu'une classe fille implémente une méthode définie dans une classe abstraite, on peut précéder cette méthode de l'annotation @Override.
Une classe interne (ou nested class) est une classe déclarée à l'intérieur d'une autre classe. Elle possède les mêmes possibilités qu'une autre classe, mais elle est toujours dépendante de sa classe conteneur et ne peut être utilisée que par la classe conteneur.
Classe interne
Par exemple :
Code Java : | Sélectionner tout |
1 2 3 4 | class Outer { class Inner { } } |
Ici Inner est une classe interne à la classe Outer.
Une classe interne peut accéder de manière transparente aux membres de l'instance de la classe dont elle fait partie. Par exemple dans ce code :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 | class Outer{ int i = 100; class Inner { int k = i; } } |
Une classe interne ne peut par contre pas comporter de contexte statique.
Classe interne statique
On peut aussi déclarer une classe interne statique, ce qui fait qu'elle ne sera plus liée à l'instance de la classe conteneur et qu'elle pourra déclarer des contextes statiques.
Code Java : | Sélectionner tout |
1 2 3 4 | class Outer { static class Inner { } } |
Mais elle ne pourra plus utiliser les variables d'instance de la classe conteneur, mais seulement les variables de classe (statiques). Par exemple :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 | class Outer { static int i = 100; static class Inner { int k = i; } } |
Une classe interne est une classe qui est déclarée à l'intérieur d'une autre classe. Le principal avantage des classes internes vient du fait qu'elles ont accès à tous les membres de leur classe conteneur quel que soit le niveau de visibilité. Ainsi les membres private de la classe conteneur sont visibles par toutes ses classes internes.
Note : en réalité le compilateur générera implicitement une méthode d'accès synthétique de visibilité package-private.
On distingue toutefois quatre grands types de classes internes :
- les classes internes statiques (static nested classes), qui correspondent à de simples classes déclarées à l'intérieur d'une autre classe ;
- les classes internes (inner classes), qui conservent un lien fort avec une instance de la classe conteneur ;
- les classes locales, déclarées dans une méthode, et qui ne sont valides qu'à l'intérieur de ce bloc.
- les classes anonymes, déclarées en ligne dans le code.
Les deux premiers types (static nested classes et inner classes) étant déclarés au même niveau que les membres de la classe, ils peuvent donc bénéficier des mêmes possibilités de visibilité : public, protected, package-only (aucun modificateur) ou private. Une classe private ne peut être utilisée que par la classe conteneur tandis qu'une classe protected peut également être utilisée par une classe fille ou une classe du même package.
Note : les classes standard ne peuvent pas être déclarées protected ou private, puisque cela n'aurait aucun sens (une classe ne peut hériter d'une autre classe que si cette dernière lui est visible).
Classes internes statiques
Les classes internes statiques (static nested classes) correspondent tout simplement à des classes standard déclarées à l'intérieur d'une autre classe. Elles se distinguent par la présence du mot-clé static dans leur définition.
Par exemple :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | public class TopLevelClass { private String privateField; public static class StaticNestedClassComparator implements Comparator<TopLevelClass> { public int compare(TopLevelClass o1, TopLevelClass o2) { // On accède directement aux champs privés : return o1.privateField.compareTo(o2.privateField); } } } |
Ces méthodes peuvent être instanciées directement en les préfixant du nom de la classe conteneur (à condition qu'elle soit visible bien entendu) :
Code Java : | Sélectionner tout |
TopLevelClass.StaticNestedClassComparator instance = new TopLevelClass.StaticNestedClassComparator();
Classes internes
Les classes internes (inner classes) ne sont pas déclarées avec le mot-clé static, et elles gagnent par la même occasion un lien étroit avec une instance de la classe conteneur. En effet les classes internes ne peuvent être instanciées qu'à partir d'une instance de la classe parente, avec laquelle elle gardera une relation pendant toute son existence.
Il est ainsi possible d'accéder à l'instance courante via la notation NomDeLaClasseConteneur.this. Par exemple :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class TopLevelClass { private String privateField; public class InnerClassRunnable implements Runnable { public void run() { // On peut accéder directement aux champs privés de l'instance lié : System.out.println(TopLevelClass.this.privateField); } } public InnerClassRunnable create() { // On crée une instance de l'inner-class qui sera liée avec l'instance courante (this) return new InnerClassRunnable(); } } |
En contrepartie, il est ainsi obligatoire d'instancier ce type de classe depuis une des méthodes d'instances de la classe conteneur (par exemple create() ici), ou en utilisant une référence de l'instance de la classe parente :
Code Java : | Sélectionner tout |
1 2 | TopLevelClass topLevelInstance = new TopLevelClass(); TopLevelClass.InnerClassRunnable innerInstance = topLevelInstance.new InnerClassRunnable(); |
Mais ce type d'écriture est généralement déconseillé.
Note : pour réaliser ce lien entre la classe interne et la classe conteneur, le compilateur rajoutera automatiquement un paramètre du type de la classe conteneur à chacun des constructeurs de la classe interne, ainsi qu'un attribut d'instance qui conservera cette valeur. Le code généré est donc sensiblement identique au code suivant :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | public class InnerClassRunnable implements Runnable { final TopLevelClass topLevelClass; public InnerClassRunnable(TopLevelClass topLevelClass) { this.topLevelClass = topLevelClass; } public void run() { // On peut accéder directement aux champs privés de l'instance liée : System.out.println(topLevelClass.privateField); } } |
Note : les interfaces, annotations ou enums déclarées à l’intérieur d'une classe ne peuvent pas être liées avec une instance de la classe parente, et sont donc implicitement déclarées statiques (même en l'absence du mot-clé static).
Classes locales
Les classes locales sont des classes déclarées au sein même d'une méthode ou d'un bloc de code. Elles ne peuvent donc être utilisées qu'à l'intérieur de ce même bloc et seront complètement inexistantes de l'extérieur.
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public static void main(final String[] args) throws Exception { // Déclaration de la classe au sein d'une méthode : class LocalClass { public void sayHello() { System.out.println("Hello World"); } } LocalClass c1 = new LocalClass(); LocalClass c2 = new LocalClass(); c1.sayHello(); c2.sayHello(); } |
Classe anonyme
Les classes anonymes correspondent à une variante des classes locales, dans le sens où elles sont aussi déclarées à l'intérieur d'un bloc de code. Elles permettent de définir une classe à « usage unique » en implémentant une seule interface ou en héritant d'une classe directement, par exemple :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public static void main(final String[] args) throws Exception { // Implémentation d'une interface : Runnable task = new Runnable() { public void run() { System.out.println("Hello World"); } }; // Héritage d'une classe : Thread thread = new Thread() { public void run() { System.out.println("Hello World"); } }; thread.start(); new Thread(task).start(); } |
Note : lorsqu'elles sont utilisées dans une méthode d'instance, les classes locales et les classes anonymes peuvent être liées à l'instance courante (this) de la même manière que les classes internes non statiques.
Note : il faut noter également que mis à part pour les classes internes statiques, les classes internes n'acceptent pas d'attribut statique, mis à part s'il s'agit de constantes.
Une interface représente un contrat passé entre plusieurs classes (polymorphisme). En général mais pas toujours, l'interface porte un nom de type adjectif compose autour du suffixe able qui signifie « capable de », ce qui permet de préciser les aptitudes complémentaires d'une classe. Par exemple :
- Runnable = capable d'être exécuté ;
- Drawable = capable de s'afficher.
Concept
En général, l'interface spécifie dans son contrat un ensemble de méthodes qui devront être implémentées par les classes qui s'engagent à respecter le contrat. Néanmoins, ceci n'est pas nécessaire et certaines interfaces servent uniquement de marqueur, comme l'interface Serialisable qui permet simplement de préparer une classe à la sérialisation.
La notion d'interface permet ainsi de :
- découper, de manière à la fois élégante et très puissante, l'aptitude (contrat) de l'implémentation.
Par exemple l'interface Enumeration permet de parcourir un ensemble d'objets d'un bout à l'autre sans se préoccuper de l'implémentation sous-jacente (un tableau, une table de hachage, une collection, etc.). En effet il suffit de faire une boucle :Code Java : Sélectionner tout 1
2
3
4while (Enumeration e = ...; e.hasNextElement(); ) { MonObjet o = (MonObjet)e.next(); // Faire qqc avec o }
- de bien séparer les activités, et d'améliorer ainsi la lecture du code.
En effet, la seule lecture de la déclaration de la classe nous permet de prendre connaissance de l'intégralité des activités de celle-ci. Par exemple une classe, disons Ensemble qui implémente l'interface Sortable nous renseigne dès sa déclaration sur la notion d'aptitude au tri qui lui a été attribuée.
Utilisation
Les interfaces sont définies grâce au mot-clé interface. Une interface peut hériter d'un nombre infini d'autres interfaces grâce au mot-clé extends. Les méthodes ainsi que les membres statiques finals définis dans une interface sont implicitement définis avec l'accesseur public (même si ce dernier n'est pas déclaré).
Les méthodes décrites dans une interface ne contiennent pas de corps, seule leur signature est présente. Le mot-clé abstract ne peut pas être utilisé dans une interface.
Par exemple :
Code Java : | Sélectionner tout |
1 2 3 | public interface Sortable { void sort(); } |
Ici, notre interface Sortable définit la signature d'une méthode sort() que peut implémenter une classe fille. Une classe fille doit utiliser le mot-clé implements pour indiquer qu'elle hérite d'une interface. Une classe peut hériter d'un nombre infini d'interfaces.
Code Java : | Sélectionner tout |
1 2 3 4 5 6 | public class MaListe implements Sortable { @Override public void sort() { // Code du tri ici. } } |
Il est bien sûr également possible d’implémenter une interface dans une classe anonyme :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 | Sortable monSortable = new Sortable() { @Override public void sort() { // Code du tri ici. } } |
Si la classe fille n’implémente pas les méthodes définies dans l'interface, elle doit être déclarée abstraite avec le mot-clé abstract.
Note : lorsqu'une classe fille implémente une méthode définie dans une interface, on peut précéder cette méthode de l'annotation @Override.
Il est également possible de définir des membres statiques finals (des constantes statiques) dans une interface :
Code Java : | Sélectionner tout |
1 2 3 | public interface Constants { public static final double PI2 = 2*Math.PI; } |
Ainsi toutes les classes (et interfaces) qui héritent de cette interface Constants hériteront du membre statique final PI2.
Depuis le JDK8, il est de plus possible de définir des méthodes par défaut ainsi que des méthodes statiques dans des interfaces.
Une méthode par défaut (defender method) permet de spécifier une implémentation par défaut fournie par l'interface. Le but des méthodes par défaut est de ménager les possibilités d’évolution d'une API en offrant une implémentation générique censée toujours fonctionner avec les classes et instances qui implémentent le type fourni par l'interface. Une méthode par défaut est préfixée par le mot-clé default.
Par exemple :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 | public interface Localizable { String getDisplayName(Locale locale); default String getDisplayName() { return getDisplayName(Locale.getDefault()); } } |
Ici, notre interface Localizable a défini la méthode par défaut getDisplayName() qui se contente d’utiliser la locale par défaut du système. Ces méthodes sont toujours surchargeables par les classes filles qui héritent de l'interface.
En savoir plus
- Pour en savoir plus sur la notion d'interface, outre les tutoriels Java, il est conseillé de chercher de l'information autour des design patterns.
- Pour mieux comprendre la notion de séparation des activités, il peut être intéressant de lire les documentations concernant jakarta-avalon .
Java permet de sauvegarder l'état d'un objet à un instant donné dans un flux d'octets. On dirigera généralement ce flux dans un fichier pour effectuer une sauvegarde. Le principal avantage de la sérialisation, c'est d'être complètement intégré à l'API Java et donc de ne nécessiter presque aucun code supplémentaire.
Lorsqu'on redéfinit une méthode d'une classe parente, ou lors de l'implémentation d'une méthode d'une classe abstraite ou d'une interface, on doit obligatoirement conserver la signature exacte de la méthode d'origine : c'est le contrat que l'on doit respecter. Toutefois, il n'est pas figé et il est donc possible de modifier certains éléments. Pour cela, nous allons prendre la déclaration de la méthode suivante en exemple :
Code Java : | Sélectionner tout |
protected Number getValue(int value) throws IOException;
Il est donc possible de modifier les éléments suivants :
- La portée de la méthode : il est en effet possible de changer la portée de la méthode, à condition de l'élargir. Ainsi, une méthode protected peut devenir public alors qu'une méthode sans modificateur (visibilité limitée au package) pourra devenir protected ou public... Cela est possible, car le contrat de la méthode originale est respecté (on se contente d'étendre l'accès à la méthode).
Par contre, l'inverse reste interdit ! Il est impossible de passer une méthode public en protected ou private par exemple.
Ainsi, la méthode ci-dessous est tout à fait valable :Code Java : Sélectionner tout public Number getValue(int index) throws IOException;
- Les Exceptions retournées : il est possible de modifier la déclaration des exceptions renvoyées par la méthode, tant que l'on respecte celle de la méthode parente. Il est donc possible de :
- supprimer l'exception : en effet, en ne renvoyant pas d'exception, on respecte le contrat original, car le mot-clé throw signifie « la méthode peut renvoyer une exception », mais ce n'est pas une obligation ;
- spécialiser le type de l'exception : en indiquant par exemple une exception qui hérite de celle définie dans la signature de la méthode parente.
Ainsi, les deux méthodes suivantes sont valables (puisque FileNotFoundException et ZipException héritent de IOException) :Code Java : Sélectionner tout protected Number getValue(int value);
Code Java : Sélectionner tout protected Number getValue(int value) throws FileNotFoundException, ZipException;
- La covariance du type de retour : depuis la version 5.0, Java apporte une nouvelle possibilité : on peut désormais modifier le type de retour de la méthode. Toutefois, il faut que le nouveau type hérite du type de retour d'origine afin de ne pas rompre le contrat.
Ainsi, notre méthode pourrait retourner un Long (puisque Long hérite de Number) :Code Java : Sélectionner tout protected Long getValue(int value) throws IOException;
Finalement, on peut obtenir des méthodes très différentes alors qu'elles redéfinissent bien la même méthode :
Code Java : | Sélectionner tout |
1 2 3 4 5 | public Double getValue(int value); protected Long getValue(int value) throws FileNotFoundException, ZipException; [...] |
Attention : les changements de signature affecteront bien sûr les classes filles... De plus, il peut devenir difficile de voir qu'une méthode en redéfinit une autre avec tant de modifications. Dans ce cas il est fortement conseillé d'utiliser l'annotation @Override (si possible)...
- Que signifient les mots-clés public, private et protected ?
- Tutoriel : La covariance dans le JDK1.5 de Fabrice Sznajderman.
La surcharge (ou encore overload) est le fait de déclarer plusieurs méthodes avec le même nom, mais avec des paramètres et/ou types de retour différents.
Une méthode qui surcharge une autre doit obligatoirement :
- avoir le même nom de la méthode surchargée ;
- être déclarée dans la même classe ou dans une classe fille ;
- avoir une signature différente : paramètres différents en nombre, en types ou en ordre de ceux de la méthode surchargée.
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | public class Salutation { public void disBonjour() { System.out.println("Bonjour inconnu !"); } public void disBonjour(String nom) { System.out.println("Bonjour "+nom+" !"); } } |
Dans cet exemple, la méthode disBonjour() est surchargée dans le sens où elle peut être appelée de deux façons différentes :
Code Java : | Sélectionner tout |
1 2 3 | Salutation salutation = new Salutation(); salutation.disBonjour(); salutation.disBonjour("Développeur"); |
Sortie :
Code : | Sélectionner tout |
1 2 | Bonjour inconnu ! Bonjour Développeur ! |
Un getter (du verbe anglais to get signifiant obtenir) est le surnom d'une méthode en accès public qui permet de récupérer la valeur d’un membre ou d'une propriété private ou protected d'un objet. Le but d'un getter est d'encapsuler l’accès à ce membre ou propriété, par exemple en cachant le fait que le résultat peut provenir d'un appel à une autre méthode ou même d'un sous-objet plutôt que d'un membre ou en s'assurant qu'on retourne un objet en lecture seule ou une copie lorsque la valeur de retour est de type complexe de manière à ne pas pouvoir permettre les effets de bord.
Cette méthode n'a généralement pas de paramètre d'entrée et retourne une valeur du même type que le membre. Selon la convention, ces méthodes sont écrites sous la forme get<Nom de la propriété avec la première lettre en majuscule>() ou, pour les valeurs booléennes, is<Nom de la propriété avec la première lettre en majuscule>().
Par exemple :
- boolean isEnabled() - retourne le statut « activé » du contrôle ;
- int getChildCount() - retourne le nombre d’éléments enfants ;
- String getPhoneNumber() - retourne le numéro de téléphone ;
- etc.
Un setter (du verbe anglais to set signifiant définir) est le surnom d'une méthode en accès public qui permet de spécifier la valeur d’un membre ou d'une propriété private ou protected d'un objet. Le but d'un setter est d'encapsuler l’accès direct à ce membre ou propriété, et de permettre de faire, par exemple, des vérifications sur la validité des valeurs qui sont affectées à la propriété, de pouvoir positionner des valeurs par défaut lorsque des valeurs non viables sont passées en paramètre ou de pouvoir générer des exceptions si besoin. Cela permet également de lancer une action dans l'objet lui-même suite au changement de valeur ou même de lancer une notification de modification à tout observateur intéressé en utilisant l'API d’évènements.
Cette méthode n'a pas de valeur de sortie et prend en argument en général un seul paramètre du même type que le membre. Selon la convention, ces méthodes sont écrites sous la forme set<Nom de la propriété avec la première lettre en majuscule>().
Par exemple :
- void setEnabled(boolean value) - définit si le contrôle est activé ;
- void setChildCount(int value) - définit le nombre d’éléments enfants ;
- void setPhoneNumber(String valeur) - définit le numéro de téléphone ;
- etc.
Les Generics (ou types paramétrés) permettent de s'abstraire du type réel des objets lors de la conception d'une classe ou d'une méthode tout en conservant un code sécurisé. En effet, contrairement au polymorphisme tel qu'il était utilisé jusque là dans Java, les Generics permettent de définir le type réel des objets lors de leurs utilisations, et permettent ainsi de s'affranchir des multiples cast peu pratiques et dangereux en cas de mauvaise utilisation. Ainsi, les Generics augmentent la sécurité en reportant à la compilation des erreurs qui survenaient à l'exécution...
Prenons pour exemple l'utilisation des collections de Java, avec une méthode qui permet de calculer la moyenne d'une List de Float :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | public float moyenne (List listNote) { float moyenne = 0.f; Iterator iterator = listNote.iterator(); while (iterator.hasNext()) { Float note = (Float) iterator.next(); moyenne += note.floatValue(); } return moyenne/listNote.size(); } |
Cette méthode toute simple peut poser problème si on l'utilise mal. En effet, si un seul des éléments de la liste passée en paramètre n'est pas du type Float, on se retrouve avec une ClassCastException. Si ce type de problème est facilement décelable dans de petits programmes, il en est tout autrement dans de gros projets, et en particulier si la liste en question peut être remplie par différents modules...
En permettant de typer des objets, les Generics reportent ces problèmes à la compilation. Ainsi la méthode devient :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | public float moyenne (List<Float> listNote) { float moyenne = 0.f; Iterator<Float> iterator = listNote.iterator(); while (iterator.hasNext()) { Float note = iterator.next(); moyenne += note.floatValue(); } return moyenne/listNote.size(); } |
Le cast a disparu, car il a été remplacé par le typage de la liste et de son itérateur grâce au <Float> après le nom du type, qui permet d'indiquer que la List et l'Iterator sont tous les deux paramétrés avec des Float. Ainsi, la méthode moyenne() ne risque plus de provoquer de ClassCastException puisque tous les éléments de la liste sont forcément des Float...
Enfin, il est à noter que l'utilisation cumulée des Generics, de l'auto-boxing et de la boucle for étendue simplifie grandement la méthode :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 | public float moyenne (List<Float> listNote) { float moyenne = 0.f; for (float note : listNote) { moyenne += note; } return moyenne/listNote.size(); } |
- Tutoriel :J2SE 1.5 Tiger, Les Générics par Lionel Roux.
L'auto-boxing gère la transformation des types primitifs (boolean, byte, char, short, int, long, float, double) vers la classe wrapper correspondante (Boolean, Byte, Character, Short, Integer, Long, Float, Double) et inversement pour l'auto-unboxing.
Tout est transparent, il n'y a donc plus aucune conversion explicite. Ainsi, le code suivant est tout à fait correct :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | Integer integer = 0;// Integer integer = new Integer(0); int j = integer;// int j = integer.intValue(); Map map = new HashMap(); map.put ( 2.1, "Valeur" );// map.put (new Double(2.1), "Valeur"); List list = new ArrayList(); list.add (true);// list.add (new Boolean(true)); [...] |
Attention : si l'objet wrapper est à la valeur null, une exception de type NullPointerException sera générée lors de toute tentative d'auto-unboxing. Par exemple :
Code Java : | Sélectionner tout |
1 2 | Integer value = null; int i = value; // Génère une NullPointerException. |
- Tutoriel : J2SE 1.5 Tiger, L'autoboxing des types primitifs par Lionel Roux.
Les annotations permettent de poser des « marqueurs » sur divers éléments du langage. Elles peuvent ensuite être utilisées par le compilateur ou d'autres outils de gestions des sources afin d'automatiser certaines tâches, voire directement pendant l'exécution de l'application...
Dans le code source, les annotations se distinguent par la présence d'une arobase (@) devant leur nom (à l'instar des tags Javadoc), et elles se déclarent avec le mot-clé @interface :
Code Java : | Sélectionner tout |
1 2 | public @interface SimpleAnnotation { } |
Code Java : | Sélectionner tout |
1 2 3 4 | public @interface AttributAnnotation { String value(); int count(); } |
Elles s'utilisent en plaçant l'annotation devant l'élément à annoter, avec les valeurs des éventuels attributs entre parenthèses, par exemple :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @SimpleAnnotation public class MaClasse { @SimpleAnnotation protected String name; @SimpleAnnotation protected int value; @AttributAnnotation(value="info", count=3); public MaClasse () { } @SimpleAnnotation @AttributAnnotation(value="m", count=1); public void method () { } |
Les annotations de base fournies par le langage Java :
- @Deprecated - qui permet d'indiquer qu'un élément est déprécié et ne devrait plus être utilisé ;
- @Override - à placer devant une méthode permet d'indiquer qu'elle surcharge une méthode héritée de la classe parent ;
- @SuppressWarnings - permet d'ignorer certains warnings lors de la compilation.
Il existe également des méta-annotations conçues exclusivement pour être utilisées sur d'autres annotations :
- @Documented - permet d'indiquer que l'annotation doit figurer dans la documentation générée par javadoc ;
- @Inherited - indique que l'annotation doit être héritée par les classes filles ;
- @Retention - spécifie de quelle manière l'annotation doit être conservée par le compilateur et la JVM ;
- @Target - permet de limiter les éléments du langage qui peuvent prendre cette annotation.
De plus, l'outil javac introduit une option -processor permettant d'analyser le code à la recherche des annotations avant la compilation.
- Tutoriel : Les annotations de Java 5.0 par AdiGuba.
Une enum représente un type énuméré, c'est-à-dire un type qui n'accepte qu'un ensemble fini d'éléments. Ce type permet donc de créer simplement des énumérations.
Utilisation
Dans sa forme la plus basique, une enum contient simplement la liste des valeurs possibles qu'elle peut prendre. Elle se déclare comme une classe, mis à part que l'on utilise le mot-clé enum, par exemple :
Code Java : | Sélectionner tout |
1 2 3 | public enum Season { spring, summer, automn, winter; } |
Les différentes valeurs de l'enum correspondent à des constantes statiques et publiques et peuvent donc être accédées directement comme les champs public static (par exemple avec Season.winter).
Les enums sont type-safe, c'est-à-dire qu'une enum ne peut en aucun cas prendre une autre valeur que celle définie dans sa déclaration, c'est pourquoi on ne peut ni construire de nouvelle instance, ni hériter d'une enum... Ici, les seules valeurs légales pour un objet de type Season sont null, Season.spring, Season.summer, Season.automn et Season.winter.
Toutefois, une énumération reste une classe Java, elle accepte donc des champs, des méthodes et des constructeurs. Par exemple, on pourrait compléter l'énumération avec le nom français de la saison :
Code java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public enum Season { spring("Printemps"), summer("Eté"), automn("Automne"), winter("Hiver"); protected String label; /** Constructeur */ Season(String pLabel) { this.label = pLabel; } public String getLabel() { return this.label; } } |
Il est donc désormais possible d'invoquer la méthode getLabel() sur des objets de type Season :
Code Java : | Sélectionner tout |
1 2 | Season season = Season.winter; System.out.println(season.getLabel()); |
Les constructeurs des enum sont implicitement privés (même si l'accesseur private n'est pas spécifié). Ils n'acceptent donc pas de modificateurs d'accessibilité public ou protected. En effet, ils ne sont utilisés que pour initialiser les différentes valeurs de l'énumération.
Ainsi, le code suivant est incorrect :
Code Java : | Sélectionner tout |
Season s = new Season("Hiver"); // Code invalide, ne compile pas.
De plus, chaque enum possède deux méthodes statiques implicites permettant d'accéder aux différentes valeurs :
- values() - cette méthode retournera un tableau du type de l'enum avec toutes les valeurs définies dans cette dernière. Par exemple :
Code Java : Sélectionner tout Season[] seasons = Season.values();
- valueOf(String) - cette méthode retournera la valeur de l'enum dont le nom est passé en paramètre. Par exemple :
Code Java : Sélectionner tout Season season = Season.valueOf("spring") //Retournera Season.spring.
Attention : une exception de type IllegalArgumentException sera générée si la valeur passée en paramètre ne correspond pas à une valeur définie dans l'enum.
Chaque instance d'une enum possède deux méthodes implicites permettant d'accéder aux différentes valeurs :
- ordinal() - cette méthode retournera l'index auquel cette valeur de l'enum a été déclarée dans le code. Il s'agit également de l'index de cette valeur dans le tableau retourné par la méthode statique values(). Par exemple :
Code Java : Sélectionner tout 1
2
3
4int springIndex = Season.spring.ordinal(); // Vaut 0. int summerIndex = Season.summer.ordinal(); // Vaut 1. int automnIndex = Season.automn.ordinal(); // Vaut 2. int winterIndex = Season.winter.ordinal(); // Vaut 3.
- name() - cette méthode renvoie la valeur de l'enum sous forme de String. Par exemple :
Code Java : Sélectionner tout 1
2
3
4String springName = Season.spring.name(); // Vaut "spring". String summerName = Season.summer.name(); // Vaut "summer". String automnName = Season.automn.name(); // Vaut "automn". String winterName = Season.winter.name(); // Vaut "winter".
Enfin, les enums peuvent directement être utilisées dans un switch :
Code java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public void method (Season season) { switch (season) { case spring: System.out.println("Les arbres sont en fleurs !!!"); break; case summer: System.out.println("Il fait chaud !!!"); break; case automn: System.out.println("Les feuilles tombent..."); break; case winter: System.out.println("Il neige !!!"); break; } } |
Attention : une exception de type NullPointerException sera générée si season est à la valeur null.
- Tutoriel : J2SE 1.5 Tiger, Les types énumérés type-safe par Lionel Roux.
Une constante est un attribut static final qui respecte certaines conditions :
- il doit correspondre à un type primitif (boolean, byte, char, int, long, float ou double) ou au type spécial String ;
- sa valeur doit être directement affectée en ligne à la déclaration de l'attribut ;
- sa valeur doit pouvoir être évaluée par le compilateur, c'est-à-dire qu'elle ne doit pas comporter de code dynamique (appel de méthode ou utilisation de variables), mais de simples opérations sur des constantes.
À titre d'exemple, les attributs suivants sont des constantes :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 | public static final char CHAR = 'C'; public static final double RAYON = 15.0; public static final double AIRE = Math.PI * RAYON * RAYON; public static final String HELLO = "Hello"; public static final String HELLO_WORLD = HELLO + " World"; |
À l'inverse, les attributs suivants ne sont pas des constantes, mais de simples attributs statiques invariables :
Code java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | public static final char CHAR = Character.valueOf('C'); // Appel d'une méthode. public static final double RANDOM = Math.random(); // Appel d'une méthode. public static final Object OBJECT = "Object"; // Type incorrect (Object). public static final String HELLO = new String("Hello"); // Utilisation d'un constructeur. public static final String HELLO_WORLD = HELLO + " World"; // HELLO n'est pas une constante. public static final String VALUE; // Pas d'initialisation. static { VALUE = "Value"; } |
Les constantes sont traitées d'une manière particulière par le compilateur, et se rapprochent des # define du C/C++. En effet, comme le compilateur peut évaluer leur valeur, il ne génère pas le code d'accès à l'attribut, mais le remplacera directement par sa valeur.
Prenons par exemple le code suivant :
Code Java : | Sélectionner tout |
System.out.println(Constante.HELLO + " World");
Si Constante.HELLO est une constante, le compilateur remplacera directement l'accès à l'attribut par sa valeur, ce qui reviendrait à faire ceci :
Code Java : | Sélectionner tout |
System.out.println("Hello" + " World");
Et, puisque le compilateur se charge d'évaluer les expressions ou sous-expressions ne contenant que des constantes, on obtient directement le code suivant :
Code Java : | Sélectionner tout |
System.out.println("Hello World");
Il n'y a donc plus aucune concaténation à l'exécution.
Attention toutefois, car si le code source est modifié pour changer la valeur de la constante, cette modification ne sera pas répercutée sur les autres classes si elles n'ont pas été recompilées, puisqu'elles conserveront en dur l'ancienne valeur de la constante. Du fait ce cette spécificité, il est important de ne pas utiliser abusivement les constantes pour des valeurs qui pourraient être modifiées au cours du temps, en particulier dans le cadre du développement d'une bibliothèque ou d'une API.
Plutôt que d'utiliser des constantes, il est généralement préférable d'utiliser des enums ou des objets immuables. Les constantes sont utiles seulement pour des grandeurs fixes et amorphes.
Les membres d'une classe (c'est-à-dire ses attributs, constructeurs ou méthodes) peuvent être marqués en tant que « synthetic » par le compilateur pour indiquer qu'il s'agit d'un élément qui a été introduit par le compilateur et qui n'est donc pas directement présent dans le code source original de la classe.
La plupart du temps les membres synthetic sont générés lors de l'utilisation de classes internes, par exemple :
- une classe interne non static se verra ajouter un attribut référençant l'instance de la classe parente avec laquelle elle est liée ;
- lorsqu'une classe interne accède à une méthode ou un attribut privé de la classe parente, le compilateur ajoutera et utilisera en réalité une méthode d'accès de visibilité « package-only » dans la classe parente ;
- lorsqu'une classe interne accède à un constructeur privé de la classe parente, le compilateur génèrera en fait un autre constructeur de visibilité « package-only ».
Bref, tous les membres introduits par le compilateur sans correspondance dans le code source original sont marqués comme « synthetic », à l'exception toutefois des constructeurs par défaut (qui sont automatiquement rajoutés lorsqu'une classe ne définit aucun constructeur).
Les méthodes bridges sont des méthodes « synthetics » générées par le compilateur sous certaines conditions lors de l'implémentation ou la redéfinition de méthodes paramétrées par les Generics, et que l'on spécifie un type particulier.
Prenons l'exemple d'une classe qui implémente l'interface Comparator<T> en utilisant le type String :
Code Java : | Sélectionner tout |
1 2 3 4 5 | public class StringIgnoreCaseComparator implements Comparator<String> { public int compare(String o1, String o2) { return o1.compareToIgnoreCase(o2); } } |
Les paramètres de la méthode compare() sont bien de type String, or, puisque le type des Generics est perdu à l'exécution, la méthode compare(String, String) ne respecte plus la signature de base de l'interface Comparator qui correspond plutôt à compare(Object, Object) (c'est-à-dire sans les types paramétrés).
Pour pallier cela, le compilateur générera automatiquement une méthode supplémentaire correspondant à la signature de base de la méthode, qui se contentera d'appeler la bonne méthode après un cast de ses paramètres. Ce qui donnerait dans notre cas :
Code java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | public class StringIgnoreCaseComparator implements Comparator<String> { public int compare(String o1, String o2) { return o1.compareToIgnoreCase(o2); } // Méthode bridge créée par le compilateur : public int compare(Object o1, Object o2) { return compare((String)o1, (String)o2); } } |
Afin de pouvoir être facilement repérées par l'API de reflection, ces méthodes sont marquées en tant que « bridge » par le compilateur.
Dans le langage, on ne peut spécifier qu'un type pour un paramètre. Ainsi, si une méthode doit utiliser des objets qui implémentent deux interfaces, on ne peut en utiliser qu'un seul dans la définition de la méthode, ce qui oblige à un cast potentiellement dangereux à l'exécution :
Code Java : | Sélectionner tout |
1 2 3 4 5 | public void method(Serializable data) { Comparable cData = (Comparable) data; // throw ClassCastException // Traitement ici [...] } |
Ce type de code a le désavantage de provoquer une exception si le type passé en paramètre n'implémente pas la seconde interface.
Avec les Generics, il est possible de reporter ce problème à la compilation. En effet les Generics ne se limitent pas seulement à paramétrer des classes, ils peuvent également s'appliquer aux méthodes, et ainsi permettent de spécifier plusieurs contraintes grâce à la covariance :
Code Java : | Sélectionner tout |
1 2 3 4 5 | public <T extends Serializable & Comparable<T>> void method(T data) { // 'data' implémente les interfaces Serializable et Comparable // Traitement ici [...] } |
Dans cet exemple, la méthode est paramétrée par un type T qui implémente à la fois les interfaces Serializable et Comparable<T>. Ainsi, si la méthode est utilisée avec un objet qui n'implémente pas ces deux interfaces, le compilateur générera une erreur à la compilation.
Connues précédemment sous le nom de Single Abstract Method interfaces (SAM Interfaces), les interfaces fonctionnelles introduisent les interfaces qui possèdent uniquement une seule méthode d’instance abstraite. Les plus connues sont java.lang.Runnable, java.awt.event.ActionListener, java.util.Comparator. Dès lors qu'une interface possède une seule méthode d’instance abstraite, elle est désignée comme interface fonctionnelle.
Il est aussi possible d'annoter l'interface par @FunctionalInterface. Si une interface est annotée ainsi et possède plus d'une méthode d’instance abstraite, une erreur de compilation sera produite. C'est un peu le même principe qu'avec l'annotation @Override.
L'interface ci-dessous, Runnable possède une méthode et est annotée @FunctionalInterface :
Code Java : | Sélectionner tout |
1 2 3 4 | @FunctionalInterface public interface Runnable { void run(); } |
Le nouveau package java.util.function propose d’ailleurs un certain nombre d’interfaces fonctionnelles répondant à divers usages.
Les lambdas ont été introduits dans la version 8 de Java. Décrite depuis la JSR 335, cette fonctionnalité permet d'apporter la puissance de la programmation fonctionnelle dans Java. Une expression lambda peut être assimilée à une fonction anonyme, ayant potentiellement accès au contexte (variables locales et/ou d'instance) du code appelant. Ces « fonctions anonymes » peuvent être affectées dans une interface fonctionnelle. Le code de l’expression lambda servira ainsi d’implémentation pour la méthode abstraite de l’interface. On peut donc les utiliser avec n'importe quel code Java utilisant une telle interface, à condition que la signature de la méthode corresponde à celle de l’expression lambda.
La syntaxe utilisée est la suivante : (paramètres) -> code ou (paramètres) -> { code } quand il y a plus d'une instruction.
Prenons l'exemple du tri des éléments d'une collection.
Code Java : | Sélectionner tout |
1 2 3 4 5 6 | Arrays.sort(testStrings, new Comparator<String>() { @Override public int compare(String s1, String s2) { return(s1.length() - s2.length()); } }); |
En utilisant les lambdas, la nouvelle écriture sera :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | // Forme longue : Arrays.sort(testStrings, (String s1, String s2) -> { return s1.length() – s2.length(); }); // Forme courte (possible uniquement s’il n’y a qu’une instruction) : Arrays.sort(testStrings, (String s1, String s2) -> s1.length() – s2.length()); // Forme courte avec type implicite des paramètres // (le type est déduit par le compilateur via l’inférence) Arrays.sort(testStrings, (s1, s2) -> s1.length() – s2.length()); |
Les interfaces fonctionnelles servent aux lambdas, facilitant ainsi l'écriture puisqu'elles permettent d'écrire l'implémentation de façon plus concise.
Nous montrons ci-dessous un exemple d'implémentation de l'interface Runnable :
Code Java : | Sélectionner tout |
Runnable r1 = () -> System.out.println("My Runnable");
- Tutoriel sur les nouveautés du langage 8 : le projet Lambda par Yohan Beschi.
Une référence de méthode est utilisée pour définir une méthode en tant qu’implémentation de la méthode abstraite d’une interface fonctionnelle. La notation utilise le nom de la classe ou une instance de la classe, suivi de l'opérateur :: et du nom de la méthode à référencer. Le type des paramètres sera déduit du contexte selon l’interface fonctionnelle vers laquelle on affecte la référence.
On peut distinguer quatre types de méthodes références :
- les références vers une méthode statique, qui s’utilisent toujours avec le nom de la classe en préfixe. La signature de la référence correspond alors à la signature de la méthode.
Code Java : Sélectionner tout 1
2Supplier<Double> random = Math::random; double result = random.get(); // Math.random();
- les références vers une méthode d’instance, liées à une instance spécifique, qui s’utilisent toujours avec l’instance en préfixe. Ici également, la signature de la référence correspond à la signature de la méthode, et tous les appels s’appliqueront sur l’instance définie dans la référence de méthode :
Code Java : Sélectionner tout 1
2
3Random r = new Random(); Supplier<Double> random2 = r::nextDouble; double result2 = random2.get(); // r.nextDouble();
- les références vers une méthode d’instance, mais sans lien avec une instance précise. Comme pour les méthodes statiques, on utilisera comme préfixe le nom de la classe. La signature de la référence correspond alors à la signature de la méthode, précédée par un argument du type de la classe, qui correspondra à l’instance sur laquelle on appellera la méthode :
Code Java : Sélectionner tout 1
2
3
4
5
6
7Function<Random,Double> random3 = Random::nextDouble; Random r1 = new Random(); Random r2 = new Random(); Random r3 = new Random(); double result1 = random3.apply(r1); // r1.nextDouble(); double result2 = random3.apply(r2); // r2.nextDouble(); double result3 = random3.apply(r3); // r2.nextDouble();
- enfin, il est possible de référencer un constructeur en utilisant le mot-clé new comme nom de méthode. Très pratique pour créer une fabrique :
Code Java : Sélectionner tout 1
2Function<String, Thread> factory = Thread::new; Thread t = factory.apply("name"); // new Thread("name");
Les références de méthodes sont un substitut aux expressions lambdas, lorsqu’il n’y a qu’une seule et unique méthode à exécuter, pour une syntaxe encore plus claire :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 | Random r = new Random(); Supplier<Double> random = Math::random; Supplier<Double> random2 = r::nextDouble; Function<Random,Double> random3 = Random::nextDouble; Function<String, Thread> factory = Thread::new; Supplier<Double> random = () -> Math.random(); Supplier<Double> random2 = () -> r->nextDouble(); Function<Random,Double> random3 = (Random random) -> random.nextDouble(); Function<String, Thread> factory = (String name) -> new Thread(name); |
Cela peut également être un substitut intéressant à l’API de reflection, puisque cela permet un code sécurisé.
Une méthode par défaut (Defender Methods) permet de proposer une implémentation dite « par défaut » aux méthodes déclarées dans les interfaces. Par conséquent, depuis Java 8, une interface Java contient du code. L'avantage est de pouvoir faire évoluer les interfaces sans avoir à tout casser.
Dans l'exemple ci-dessous, une interface Person déclare deux méthodes. La méthode sayHello() est dite par défaut via le mot-clé default. Toute implémentation de Person imposera que la méthode sayGoodBye() soit implémentée. Pour sayHello(), l'implémentation ne sera pas obligatoire, même si elle reste bien sûr possible.
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 | interface Person { void sayGoodBye(); default void sayHello() { System.out.println("Hello there!"); } } |
Les méthodes par défaut permettent ainsi de faire évoluer l’API des interfaces sans provoquer de grosses incompatibilités dues à l’absence d’implémentation dans les classes qui les implémentent. L’API de base en profite grandement en enrichissant certaines de ses interfaces (en particulier dans l’API de Collections dont les interfaces s’enrichissent de plusieurs méthodes).
- Tutoriel sur Java 8 : du neuf dans les interfaces par Olivier Croisier.
Un objet immuable est un objet dont on ne peut plus modifier l'état une fois l'instance créée.
La classe String est l'exemple le plus utilisé d'objet immuable : il n'est pas possible de modifier une chaine de caractères. Toutes les méthodes de la classe ou même l'opérateur + qui lui sont appliqués retournent en fait une nouvelle chaine distincte de la première.
Prenons, par exemple, le code suivant :
Code Java : | Sélectionner tout |
1 2 3 | String maChaine = "Test par Moi"; maChaine.toUpperCase(); System.out.println(maChaine); |
Dans ce cas, la deuxième instruction ne modifie pas le contenu de la variable maChaine mais crée un nouvel objet en retour. C'est bien la valeur "Test par Moi" qui sera affichée au lieu de "TEST PAR MOI".
Si vous souhaitez que maChaine prenne pour valeur le résultat de la deuxième instruction, il faut réaffecter la nouvelle valeur dans la variable en procédant comme suit :
Code Java : | Sélectionner tout |
1 2 3 | String maChaine = "Test par Moi"; maChaine = maChaine.toUpperCase(); System.out.println(maChaine); |
À noter que les classes de type wrapper du package java.lang sont immuables : Boolean, Byte, Character, Short, Integer, Long, Float et Double.
En informatique, et en représentation binaire, on groupe les bits par paquet de 8 (un octet ou byte). On note traditionnellement les bits de poids fort (ceux avec la plus grande puissance de 2) sur la gauche et les bits de poids faible (ceux avec la plus faible puissance de 2) sur la droite.
Par exemple :
Code Console : | Sélectionner tout |
2726252423222120
Ce qui permet d’écrire :
Code Console : | Sélectionner tout |
00000011 ↔ 21 + 20 ↔ 2 + 1 ↔ 3
Les choses se compliquent lorsqu'on manipule des données qui font plusieurs octets, par exemple des mots, qui font 4 octets, soit 32 bits. Ces mots sont donc constitués de bits qui vont du bit0 au bit31, eux-mêmes groupés en 4 octets : octet31-24, octet23-16, octet15-8 et octet7-0. Chaque fabricant de microprocesseurs a eu sa petite idée sur la manière de stocker une telle structure en mémoire tout en essayant d'optimiser la chose pour son architecture à lui et pas celle des autres. Grosso modo deux principaux modes de stockage ont fini par émerger entraînant de nombreuses disputes et batailles rangées pour savoir laquelle des deux était la meilleure ; on parle d'endianness (le boutisme ou endianisme) :
- sur les architectures IBM, Cray, Sun, etc., on a tendance à agencer les bits de la manière suivante :
Code Console : Sélectionner tout 231230229228227226225224 223222221220219218217216 2152142132122112102928 2726252423222120
Soit :Code : Sélectionner tout Octet31-24 Octet23-16 Octet15-8 Octet7-0
- tandis que, sur les architectures Intel, on a tendance à agencer les bits de la manière suivante :
Code Console : Sélectionner tout 2726252423222120 2152142132122112102928 223222221220219218217216 231230229228227226225224
Soit :Code : Sélectionner tout Octet7-0 Octet15-8 Octet23-16 Octet31-24
En avril 1980, alors qu'Internet est en pleins balbutiements et qu'on commence donc à interconnecter les machines et donc les architectures, il faut définir des formats communs sur la manière de faire transiter des données. Alors que les débats font rage, Danny Cohen écrit le document On Holy Wars and a Plea for Peace (À propos des guerres saintes et un plaidoyer pour la paix) dans lequel il compare cette véritable guerre de religion aux querelles futiles, mais sanguinaires rencontrées par le héros Gulliver dans Les Voyages de Gulliver de l'auteur anglais Jonathan Swift à propos de la meilleure manière de casser un œuf à la coque : par le petit ou par le gros bout (Swift se moque, à demi-mot, des querelles et guerres de religion entre catholiques et protestants en Angleterre à l’époque).
Depuis, le terme est resté :
- Big-Endian (gros-boutiste ou grand-boutien) - les octets de poids fort sont à gauche : architecture IBM, CRAY, Sun, etc. ;
- Little-Endian (petit-boutiste ou petit-boutien) - les octets de poids faible sont à gauche : architecture Intel.
Certains langages ne gèrent pas vraiment le boutisme et se contentent d'appliquer directement le boutisme supporté par la plateforme sous-jacente. Ainsi, avec ces langages, si on n'y prend pas garde, le même code qui écrit dans un fichier ou sur le réseau peut donner des résultats complètement différents quand il est compilé et exécuté sur machines d'architecture différente. Et c'est très important d'y faire attention ; prenons, par exemple, la valeur 3 :
Code Console : | Sélectionner tout |
1 2 | 3 sur une machine gros-boutiste ↔ 00000000 00000000 00000000 00000011 ↔ 50331648 sur une machine petit-boutiste. 3 sur une machine petit-boutiste ↔ 00000011 00000000 00000000 00000000 ↔ 50331648 sur une machine gros-boutiste. |
Si on ne gère pas correctement le boutisme, on ne lit pas du tout la bonne valeur, ce qui est très gênant quand il faut allouer des structures, tailles de messages, etc.
Ce n'est pas le cas de Java : Java est Big-Endian ! Et il l'est de tout temps, et ce, quelle que soit sa plateforme d’exécution ; que ce soit pour la représentation binaire des nombres entiers, l’écriture et la lecture des fichiers ou encore les accès réseau. Donc, si vous êtes amené à produire ou lire des fichiers binaires ou des trames réseau dans lesquels les données doivent être sous une forme petit-boutiste, vous devrez faire l'effort d'inverser les octets de vos mots et autres paquets d'octets. Certaines bibliothèques récentes, telles que la bibliothèque NIO.2 introduite dans le JDK7, peuvent cependant disposer de classes et méthodes utilitaires qui permettent d’alléger votre charge de travail en effectuant cette inversion automatiquement.
Un singleton est un objet qui ne peut être instancié qu'une seule et unique fois dans le programme.
Pour transformer une classe en singleton, il faut passer les constructeurs en accès private et ajouter au code une instance et un accesseur statiques :
Code java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class Singleton { private static Singleton instance = new Singleton(); public static Singleton getInstance() { return instance; } private Singleton() { //constructeur } //reste de la classe } |
Code java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class Singleton { private static Singleton instance = null; public static Singleton getInstance() { if(instance == null) instance = new Singleton(); return instance; } private Singleton() { //constructeur } //reste de la classe } |
Code java : | Sélectionner tout |
Singleton s = Singleton.getInstance();
Il existe encore une dernière variante consistant à utiliser une classe interne statique comme conteneur du singleton :
Code java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | public class Something { private Something() { } private static class LazyHolder { private static final Something something = new Something(); } public static Something getInstance(){ return LazyHolder.something; } } |
La méthode equals() permet d'indiquer qu'un objet est égal à un autre. L'implémentation par défaut hérité de Object se contente de renvoyer true lorsqu'il s'agit du même objet en mémoire et ne vérifie pas les valeurs des attributs de la classe (donc x.equals(y) équivaut à x==y).
Dès lors que l'on a besoin de tester l'égalité des instances d'une classe, il faut que cette dernière redéfinisse la méthode equals(). C'est également une condition pour le bon fonctionnement de certaines méthodes des Collections de Java.
La méthode equals() doit donc renvoyer true lorsque deux objets sont identiques, et false dans le cas inverse. Enfin il faut noter que cette méthode ne devrait pas renvoyer d'exception, même si son paramètre vaut null.
En général la méthode equals() se décompose en trois étapes :
- vérification de l'égalité des références : il est inutile de comparer les valeurs si les références sont identiques ;
- vérification du type du paramètre : on ne peut pas comparer n'importe quel type ;
- vérification des valeurs des attributs utiles des deux objets (c'est-à-dire des attributs représentatifs de la valeur de l'objet).
Ainsi, cela pourrait donner :
Code java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | class TestClass { private int attribut1; private String attribut2; private boolean visible; // Attribut non représentatif et donc ignoré @Override public boolean equals(Object obj) { // Vérification de l'égalité des références if (obj==this) { return true; } // Vérification du type du paramètre if (obj instanceof TestClass) { // Vérification des valeurs des attributs TestClass other = (TestClass) obj; // Pour les attributs de type primitif // on compare directement les valeurs : if (this.attribut1 != other.attribut1) { return false; // les attributs sont différents } // Pour les attributs de type objet // on compare dans un premier temps les références if (this.attribut2 != other.attribut2) { // Si les références ne sont pas identiques // on doit en plus utiliser equals() if (this.attribut2 == null || !this.attribut2.equals(other.attribut2)) { return false; // les attributs sont différents } } // Si on arrive ici c'est que tous les attributs sont égaux : return true; } return false; } } |
Il faut prendre en compte les règles suivantes lors de la redéfinition de la méthode equals() :
- Réflection: x.equals(x) devrait toujours retourner true ;
- Symétrie: Si x.equals(y) retourne true,alors y.equals(x) retournera true. ;
- Transitivité: Si x.equals(y) retourne trueet y.equals(z) retourne true,alors x.equals(z) retourne true. ;
- Consistance: Pour deux références x ety, tous les appels à la méthode x.equals(y)devront toujours donner le même résultat.
Toutefois ces règles peuvent être difficilement réalisables en cas d'héritage lorsque de nouveaux attributs sont pris en compte.
Enfin, la bibliothèque Jakarta Common Lang propose une classe EqualsBuilder qui facilite et simplifie l'écriture des méthodes equals() en se chargeant de la comparaison des attributs, ce qui pourrait donner dans ce cas :
Code java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public boolean equals(Object obj) { // Vérification de l'égalité des références if (obj==this) { return true; } // Vérification du type du paramètre if (obj instanceof TestClass) { // Vérification des valeurs des attributs TestClass other = (TestClass) obj; return new EqualsBuilder() .append(this.attribut1, other.attribut1) .append(this.attribut2, other.attribut2) .isEquals(); } return false; } |
Enfin, il faut noter qu'il est conseillé de redéfinir également la méthode hashCode() afin de respecter le contrat de cette dernière (qui est fortement lié à equals()).
La méthode hashCode() a pour objectif de fournir un code de hashage, afin d'optimiser le traitement des collections de type Map, qui se basent sur ce hashCode pour classer les données. Il est ainsi nécessaire de redéfinir la méthode hashCode() dès que l'on souhaite utiliser un objet en tant que clé d'une Map.
Mais plus généralement, il est souhaitable de redéfinir la méthode hashCode() lorsque l'on redéfinit la méthode equals(), afin de conserver une certaine cohérence.
En effet, ces deux méthodes sont très liées, puisque les hashCode de deux objets égaux doivent être égaux, ainsi l'on doit vérifier la condition suivante :
Si x.equals(y), alors x.hashCode() == y.hashCode()
Par contre l'inverse n'est pas forcément vrai. Ainsi deux objets différents peuvent avoir le même hashCode(). Toutefois ceci est à éviter dans la mesure du possible, car cela peut détériorer les performances des Map.
Mais le calcul du hashCode() doit rester raisonnable et peu complexe, afin d'éviter des temps de calcul trop importants.
La solution la plus courante est de calculer un hashCode() selon la valeur des attributs utilisés dans la méthode equals() afin de conserver la cohérence entre les deux méthodes.
Pour le calcul du hashCode(), on peut raisonnablement suivre la règle suivante.
- On choisit deux nombres impairs différents de zéro. Un de ces nombres servira comme valeur de départ pour le calcul du hashCode. Le second servira de « multiplieur » à chaque « ajout » du hashCode d'un attribut. Il est préférable d'utiliser des nombres premiers par sécurité.
En effet, si à chaque clé x , correspond h(x) l'endroit où se trouve x dans la table de hachage. L'expression de la fonction de Hachage est :
Code : | Sélectionner tout |
h(x)=[x(1)*B^(l-1) + x(2)*B^(l-2)....+x(l)] mod N
Code : | Sélectionner tout |
h(x)=x(l) mod N
Une seule valeur a donc été rangée dans la table, voilà pourquoi il est très important de limiter au maximum les risques de diviseurs communs, car certaines valeurs disparaîtraient alors des tables.
N.B. Le choix de B comme d'une puissance de 2 au lieu d'un nombre premier peut s'avérer judicieux pour des raisons de vitesse d'exécution. En effet une multiplication par 2 est un simple décalage d'un bit à gauche pour l'ordinateur. Malgré tout, cela augmente les risques de diviseurs communs et donc de disparition de données.
- Calcul du hashCode de chaque attribut utile (ceux utilisés dans la méthode equals()), ce qui revient à faire (soit 'a' l'attribut) :
- Pour un boolean, renvoyer 0 ou 1
Code java : Sélectionner tout (a ? 0 : 1)
- Pour type entier (byte, char, short ou int) il suffit de prendre la valeur entière (avec un cast éventuel) :
Code java : Sélectionner tout (int)a;
- Pour un type long, le convertir en int en déplaçant les bits :
Code java : Sélectionner tout (int) (a^(a>>>32))
- Pour le type float, on le convertit en int avec la méthode Float.floatToIntBits(a).
- Pour le type double, on le convertit en long avec la méthode Double.doubleToLongBits(a), puis on effectue le même traitement que pour les long.
- Pour les objets, on se contentera d'utiliser la méthode hashCode(), ou d'utiliser zéro si la référence est null :
Code java : Sélectionner tout (object==null ? 0, object.hashCode())
- Pour un tableau, on traitera tous les éléments de ce dernier en respectant les règles ci-dessus.
Et enfin, on ajoute chacun des hashCodes calculés au résultat, après l'avoir multiplié par le nombre « multiplieur ».
Ce qui pourrait donner (pour la classe en exemple dans la question précédente) :
Code java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public int hashCode() { // On choisit les deux nombres impairs int result = 7; final int multiplier = 17; // Pour chaque attribut, on calcule le hashcode // que l'on ajoute au résultat après l'avoir multiplié // par le nombre "multiplieur" : result = multiplier*result + attribut1; result = multiplier*result + (attribut2==null ? 0 : attribut2.hashCode()); // On retourne le résultat : return result; } |
Enfin, la bibliothèque Jakarta Common Lang propose également une classe HashCodeBuilder qui facilite et simplifie l'écriture des méthodes hashCode() en se chargeant du calcul des hashCode des attributs selon leur type, ce qui pourrait donner dans ce cas :
Code java : | Sélectionner tout |
1 2 3 4 5 6 | public int hashCode() { return new HashCodeBuilder(17, 37) .append(this.attribut1) .append(this.attribut2) .toHashCode(); } |
Un POJO ou Plain Old Java Object (un bon vieil objet Java) est un objet Java tout simple qui ne suit (presque) aucun concept de design ou framework particulier dans sa conception. Ce terme a été inventé par Martin Fowler, Rebecca Parsons et Josh MacKenzie en septembre 2000 alors qu'ils s’étonnaient de l'opposition apparente à l'utilisation d'objets à la conception très simple dans le code et en concluaient, avec humour, que cela venait probablement du fait que de tels objets ne disposaient pas d'une désignation « cool ». Ils sont à mettre en opposition à des objets « lourds » et complexes tels que les Entrerprise Java Beans par exemple.
Donc, pour résumer, un POJO :
- n’hérite pas d'objet préspécifié ;
- n’implémente pas des interfaces préspécifiées ;
- n'utilise pas d'annotation préspécifiée.
Cependant, on considère généralement qu'un bean simple (avec des getters et des setters) est un POJO, même s'il hérite de l'interface java.lang.Serializable. De fait, à cause de l’encapsulation des données et des conventions de nommage des méthodes, de nombreux POJO sont aussi des beans sans le savoir.
De plus, on considère généralement que si un objet était un POJO avant qu'on lui ajoute des annotations, il le reste après coup.
Pour copier un objet, deux manières de faire s'offrent à vous.
Copie manuelle
Vous pouvez effectuer une copie manuelle de l'objet dans une nouvelle instance, par exemple en le passant dans un constructeur spécialisé (aussi appelé constructeur de copie) de cette classe ou via une méthode de fabrique chargée de dupliquer l'objet. Par exemple ;
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public class Article { private int prix; private int quantite; private String nom; /** * Constructeur normal. */ public Article(String nom, int prix, int quantite) { this.prix = prix; this.quantite = quantite; this.nom = nom; } /** * Constructeur de copie. */ public Article(Article source) { Objects.requiresNonNull(source); this.prix = source.prix; this.quantite = source.quantite; this.nom = source.nom; } } |
Ce qui permet de faire :
Code Java : | Sélectionner tout |
1 2 | Article source = [...] Article copie = new Article(source); |
Cette façon de faire à un gros désavantage : elle s'adapte mal à l’héritage et au fait de pouvoir étendre les capacités des classes. Par exemple, ici si nous créons une classe ArticleDeMarque qui hérite de Article et qui contient un membre marque supplémentaire, alors notre constructeur de copie est totalement incapable de copier ce nouveau membre.
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | public class ArticleDeMarque extends Article { private String marque; public ArticleDeMarque(String nom, int prix, int quantite, String marque) { super(nom, prix, quantite); this.marque = marque; } } |
Il faudrait créer puis invoquer un nouveau constructeur de copie spécialisé qui serait défini dans ArticleDeMarque ce qui suppose d’écrire du code qui teste le type de l'objet à copier.
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 | Article source = [...] Article copie = null; if (source instanceof ArticleDeMarque) { copie = new ArticleDeMarque((ArticleDeMarque) source); } else { copie = new Article(source); } |
Bref, ce n'est pas vraiment viable sur le long terme. Cette façon de faire est surtout à réserver pour les classes déclarées avec le modificateur final, celles qu'on ne peut plus étendre ou, au contraire, des classes faisant partie d'une API complexe définissant un socle de méthodes communes pour accéder au contenu des instances (ex. : les collections).
Clonage
Java offre un mécanisme permettant de cloner les objets, c'est-à-dire d'invoquer la méthode clone() définie sur une instance de l'objet. C'est cette méthode qui se chargera de fournir une copie de l'instance en résultat.
La méthode clone() est définie directement dans la classe java.lang.Object et est donc accessible depuis n'importe quel objet en Java. Cependant, elle est définie en visibilité protected. De plus, si vous tentez de l'invoquer directement, votre code provoquera une exception de type java.lang.CloneNotSupportedException.
Lorsque vous créez une nouvelle classe que vous désirez pouvoir cloner, vous devez lui faire étendre l'interface de marquage java.lang.Cloneable ; cela permet d’éviter la génération de l'exception. Ensuite vous pouvez surcharger la méthode clone() pour lui donner une visibilité public ce qui permettra de l'invoquer depuis du code externe. Enfin, la covariance permet également de spécifier un type de retour approprié pour la méthode. Par exemple :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class Article implements Cloneable { private int prix; private int quantite; private String nom; public Article(String nom, int prix, int quantite) { this.prix = prix; this.quantite = quantite; this.nom = nom; } @Override public Article clone() throws CloneNotSupportedException { // Cet appel créera l'instance de la copie, // et recopiera les valeurs de tous les membres dans cette nouvelle instance. Article copie = (Article) super.clone(); return copie; } } |
Ce qui nous permet de faire :
Code Java : | Sélectionner tout |
1 2 | Article source = [...]; Article copie = source.clone(); |
Ce qui permet de faire des copies sans se soucier du type concret des objets :
Code Java : | Sélectionner tout |
1 2 | Article source = new ArticleDeMarque([...]); Article copie = source.clone(); |
En effet, le clonage du membre marque de notre classe ArticleDeMarque est assuré par la définition de clone() que nous avons écrite dans sa classe mère Article.
Par défaut, le clonage se contente de recopier les valeurs des membres des types primitifs et des références. Ainsi si notre classe contient une référence vers un objet complexe, c'est la référence de l'objet qui est copiée et non pas l'objet lui-même et donc le clone obtenu détiendra une référence vers la même instance de cet objet. Par exemple :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 | public class Article implements Cloneable { [...] private List<Integer> encheres = new LinkedList(); } |
Désormais notre article contient une liste des enchères effectuées. Si nous faisons :
Code Java : | Sélectionner tout |
1 2 | Article source = [...] article copie = source.clone(); |
Nos deux objets source et copie se partagent la même liste encheres ce qui fait que toute modification dans l'un des deux sera également répercutée dans l'autre.
Pour corriger ce problème, nous allons reprendre la définition de la méthode clone() et nous allons nous assurer que nous créons bien une copie de la liste ; c'est ce qu'on appelle le deep cloning ou le clonage en profondeur :
Code Java : | Sélectionner tout |
1 2 3 4 5 6 7 8 | @Override public Article clone() throws CloneNotSupportedException { Article copie = (Article) super.clone(); // On crée une nouvelle liste qui est la copie de la liste source. // Noter que LinkedList utilise un constructeur de copie. copie.encheres = new LinkedList(this.encheres); return copie; } |
Avertissements : ici, notre liste contient de simples entiers et nous pouvons nous contenter d'un seul niveau de profondeur de clonage. Si notre liste avait contenu des objets complexes mutables, il aurait également fallu copier/cloner chacun des objets de la liste pour éviter les effets de bord.
De plus, notre membre enchères ne peut pas être déclaré final, car alors la méthode clone() ne pourrait pas modifier la valeur du membre dans la copie.
Proposer une nouvelle réponse sur la FAQ
Ce n'est pas l'endroit pour poser des questions, allez plutôt sur le forum de la rubrique pour çaLes sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2024 Developpez Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.