5. Constructeur, instanciation

Nous avons vu que l’instanciation d’un objet recouvrait deux choses : la réservation d’un espace mémoire sur lequel la machine Java nous donne une référence, et l'initialisation de cet espace mémoire. Ensuite, la machine Java appelle les éléments d'initialisation qui ont été définis dans la classe, ou la hiérarchie de classes, tels que les blocs statiques et non statiques, l'initialisation des champs et les constructeurs. Voyons en détails comment tous ces éléments sont appelés et dans quel ordre.

5.1. Chargement d'une classe

La première étape de l'instanciation d'un objet consiste à charger la classe à laquelle il appartient. Cette classe n'est chargée qu'une seule fois, et reste ensuite présente dans la machine Java. Certaines machines Java sont capables d'effacer des classes de leur mémoire, ce qui est possible en théorie, mais ce fonctionnement n'est pas la règle générale. En général une classe n'est chargée que lorsqu'elle est référencée par un objet. On peut aussi déclencher explicitement le chargement d'une classe. Lors du chargement d'une classe, la machine Java charge toutes les classes dont cette classe hérite, si elle ne l'ont pas déjà été. L'ordre d'exécution des blocs statiques varie ensuite suivant que l'on utilise une machine Java version 4 ou version 5 et suivantes. Pour les JVM version 4 et antérieures, les blocs statiques sont exécutés au chargement des classes. Donc, en partant de la classe Object, et en redescendant toute la hiérarchie jusqu'à la classe qu'elle charge, et pour toutes les classes nouvellement chargées :
  • la JVM exécute les initialiseurs des champs statiques ;
  • elle exécute ensuite les blocs statiques.
Pour les JVM version 5 et postérieures, ces deux opérations sont exécutées lors de la première instanciation d'un objet de la classe. Si un champ publique statique est référencé, alors il est initialisé, mais cela ne déclenche pas l'exécution des blocs statiques. Ces deux opérations ne sont effectuées qu'une seule fois, puisqu'une classe n'est chargée qu'une seule fois.

Exemple 32. Exécution des blocs statiques lors du chargement d'une classe

public  class Marin {  // dans le fichier Marin.java  
      
     public  static  long dateDeChargement = System.currentTimeMillis() ;  // 1  
  
     static {  
        System.out.println("Chargement de la classe Marin") ;  // 2  
    }  
}  
  
 public  class Capitaine  extends Marin {  // dans le fichier Capitaine.java  
      
     public  static  long dateDeChargement = System.currentTimeMillis() ;  // 3  
  
     static {  
        System.out.println("Chargement de la classe Capitaine") ;  // 4  
    }  
}

En Java 4 et avant, le chargement de la classe Capitaine déclenche les opérations statiques dans l'ordre de leur numérotation. À partir de la version 5 de Java ces blocs sont exécutés lors de la première instanciation de la classe Capitaine.

5.2. Constructeurs d'une classe

Un constructeur d'une classe est une méthode particulière. Elle n'a pas de type de retour, et porte le même nom que la classe dans laquelle elle se trouve. Un constructeur peut jeter des exceptions. Une classe peut avoir autant de constructeurs que l'on a le courage de lui en créer, dès l'instant qu'ils ont des signatures différentes, c'est-à-dire des paramètres différents. Il n'est toutefois pas conseillé de multiplier les constructeurs, ni de créer des constructeurs prenant des listes interminables de paramètres. Une classe qui ne déclare aucun constructeur explicitement en possède en fait toujours un : le constructeur vide par défaut, qui ne prend aucun paramètre. Si l'on définit un constructeur explicite, alors ce constructeur vide par défaut n'existe plus ; on doit le déclarer explicitement si l'on veut encore l'utiliser. Écrire ce constructeur systématiquement est une bonne habitude de programmation : le bon fonctionnement de nombreux framework repose sur l'existence systématique de ce constructeur. Prenons l'exemple de la classe Marin ci-dessous.

Exemple 33. Constructeur vide par défaut - 1

public  class Marin {   
      
     private String nom ;  
      
     public String getNom() {  
         return  this.nom ;  
    }  
      
     public  void setNom(String nom) {  
         this.nom = nom ;  
    }  
}

La machine Java a créé un constructeur vide par défaut dans cette classe, on peut donc l'instancier de la façon suivante :
 Marin marin = new Marin() ;
Si l'on spécifie un constructeur dans cette classe, alors le constructeur vide par défaut n'est plus créé. Si l'on reprend notre exemple :

Exemple 34. Constructeur vide par défaut - 2

public  class Marin {   
      
     private String nom ;  
      
     public Marin(String nom) {  
         this.nom ;  
    }  
      
     public String getNom() {  
         return  this.nom ;  
    }  
}

On ne peut plus instancier cette classe comme précédemment. On ne peut l'instancier que de la façon suivante :
 Marin marin = new Marin("Surcouf") ;
Considérons l'exemple suivant.

Exemple 35. Constructeur vide par défaut - 3

public  class Marin {  // dans le fichier Marin.java  
      
     private String nom ;  
      
     // constructeur vide de la classe Marin  
     public Marin() {  
        nom =  "indéfini" ;  
    }  
}  
  
 public  class Capitaine  extends Marin {  // dans le fichier Capitaine.java  
      
     private String grade  
      
     public Capitaine(String grade) {  
         this.grade = grade ;  
    }  
}

Sur ce code, instancions un objet de type Capitaine :
 Capitaine capitaine = new Capitaine("Capitaine de vaisseau") ;
L'instanciation de l'objet capitaine déclenche les opérations suivantes :
  • le constructeur de la classe Capitaine est appelé ;
  • ce constructeur appelle tout d'abord implicitement le constructeur vide de la classe Marin, qui initialise le champ nom ;
  • le constructeur de Capitaine initialise le champ grade.
Lors qu'un objet instance d'une classe qui en étend une autre est construit, au moins un constructeur de cette super classe doit être appelé. Si aucun appel explicite n'est écrit, alors la JVM exécute le constructeur vide par défaut. S'il n'existe pas, elle génère une erreur à la compilation. Dans notre exemple, remplacer le constructeur vide par un constructeur prenant en paramètre le nom du marin (par exemple), génèrera une erreur de compilation, puisque son existence supprime celle du constructeur vide par défaut. Il faudra donc conserver un constructeur vide, ou appeler explicitement ce nouveau constructeur de la classe Capitaine. Il est également possible pour un constructeur d'appeler explicitement un unique constructeur. Cet appel ne peut être que la première instruction de ce constructeur. Voyons cela sur un exemple.

Exemple 36. Appels explicites de constructeurs

public  class Marin {  // dans le fichier Marin.java  
      
     private String nom ;  
      
     public Marin(String nom) {  
         this.nom = nom ;  
    }  
}  
  
 public  class Capitaine  extends Marin {  // dans le fichier Capitaine.java  
      
     private String grade  
      
     public Capitaine(String nom, String grade) {  
         super(nom) ;  // appel du constructeur de la super classe  
         this.grade = grade ;  
    }  
      
     public Capitaine(String grade) {  
         this("indéfini", grade) ;  // appel du constructeur de même classe  
    }  
}

Rappelons que l'appel d'un autre constructeur, qu'il soit dans la même classe ou dans la super classe doit toujours être la première instruction d'un constructeur. On peut bien sûr ajouter autant d'instructions que l'on souhaite une fois cet appel effectué.

5.3. Instanciation d'un objet

Voyons à présent l'ensemble des opérations effectuées lors de la création d'un objet. Les différentes étapes se déroulent dans cet ordre :
  • la machine Java réserve de la mémoire pour stocker l'objet à créer ;
  • cette mémoire est effacée de toute ce qu'elle pouvait contenir auparavant : les champs sont mis à 0, false ou null suivant leur type ;
  • le constructeur invoqué est appelé ;
  • si ce constructeur appelle un autre constructeur de la même classe, alors il est appelé ;
  • si ce constructeur appelle un autre constructeur d'une super classe, alors il est appelé ;
  • une fois que la chaîne d'appel des constructeurs a été épuisée, alors les initialiseurs de champs sont appelés ;
  • les blocs non statiques sont exécutés ;
  • le constructeur de la super classe dans laquelle on se trouve est exécuté ;
  • on passe à la sous-classe suivante, en répétant les mêmes opérations dans le même ordre, jusqu'à la classe dont on construit finalement une instance.
Voyons ceci sur un exemple.

Exemple 37. Exemple d'une instanciation complexe

public  class Marin {  // dans le fichier Marin.java  
      
     private  long dateCreation = System.currentTimeMillis() ;  
  
    {  
         // ceci est un bloc non statique  
        System.out.println(i) ;  
    }  
  
     private String nom ;  
  
     public Marin() {  
         this.nom =  "indéfini" ;  
    }  
  
     public Marin(String nom) {  
         this.nom = nom ;  
    }  
  
     public String getNom() {  
         return nom;  
    }  
  
     public  void setNom(String nom) {  
         this.nom = nom;  
    }  
}  
  
 public  class Capitaine  extends Marin {  // dans le fichier Capitaine.java  
  
     private  int grade ;  
  
     private  long dateCreation = System.currentTimeMillis() ;  
  
    {  
         // ceci est un bloc non statique  
        System.out.println(i) ;  
    }  
  
     public Capitaine(String nom) {  
         super(nom) ;  
    }  
  
     public Capitaine(String nom,  int grade) {  
         this(nom) ;  
         this.grade = grade ;  
    }  
}

Créons un objet Capitaine avec l'instruction suivante :
 Capitaine m = new Capitaine("Surcouf", 2) ;
Les opérations s'enchaînent de la façon suivante :
  • appel du constructeur (String, int) de Capitaine ;
  • appel du constructeur (String) de Capitaine ;
  • appel du constructeur (String) de Marin ;
  • initialisation de la variable dateCreation de Marin ;
  • exécution du bloc non statique de Marin ;
  • exécution du constructeur (String) de Marin ;
  • initialisation de la variable dateCreation de Capitaine ;
  • exécution du bloc non statique de Capitaine ;
  • exécution du constructeur (String) de Capitaine ;
  • exécution du constructeur (String, int) de Capitaine.
Donc, lorsque l'exécution d'un constructeur s'achève, l'objet, ou le morceau d'objet dans lequel on se trouve est entièrement initialisé. Nous verrons que cela a un impact sur la définition des champs final.

5.4. Destruction d'objets

Rappelons que l'utilisateur n'a pas à se préoccuper de la destruction de ses objets : la notion de destructeur n'existe pas en Java. Il existe une méthode finalize() dans la classe Object, détaillée ici, qui joue le rôle de callback avant que le garbage collector ne détruise un objet.

5.5. Le mot-clé final

Nous avons déjà utilisé ce mot-clé dans quelques exemples, sans définir précisément à quoi il correspond, il est donc temps de le faire. Ce mot-clé peut être utilisé comme modificateur de plusieurs choses. Tout d'abord, il peut être utilisé sur une classe. Si une classe est déclarée final, alors il n'est pas possible de l'étendre. De nombreuses classes sont final dans l'API standard : c'est le cas de String, et de toutes les classes enveloppes des classes de base. Il peut être utilisé de façon analogue sur une méthode. Une méthode déclarée final ne peut pas être surchargée par une méthode d'une classe qui étendrait la classe dans laquelle cette méthode est définie. Par exemple, les méthodes wait() de la classe Object sont finales, elles ne peuvent donc pas être surchargées. Il peut être utilisé sur le champ d'une classe, statique ou non. Dans ce cas, une fois intialisé, ce champ ne pourra plus être modifié. Se pose alors la question, à quel moment peut-on, et doit-ont initialiser un champ final ? Un champ static final, doit être initialisé par un initialiseur de champ ou un bloc statique. Un champ final (non statique) doit être initialisé par un initialiseur de champ, un bloc non statique, ou dans le constructeur. Un champ final, statique ou non, ne peut pas être initialisé deux fois, une fois initialisé, il n'est plus possible de changer sa valeur. Si un objet possède plusieurs constructeurs, et qu'il possède un champ final, alors l'initialisation de ce champ doit suivre le même processus, quel que soit le constructeur appelé. En particulier, si ce champ est initialisé dans un constructeur, alors tous les constructeurs doivent l'initialiser, y compris le constructeur par défaut. Le mot-clé final peut être posé sur un paramètre reçu par une méthode. Dans ce cas, ce paramètre ne pourra être modifié. Enfin, le mot-clé final peut être posé sur une variable définie dans une méthode. Dans ce cas, la valeur de cette variable ne pourra être modifiée. Notons qu'une classe locale, anonyme ou non, peut accéder aux variables et paramètres définis dans la méthode dans laquelle elle-même a été définie, que si ceux-ci sont final. Les règles d'utilisation des champs final sont complexes et subtiles. Heureusement les environnements de développement intégré sont là pour nous aider. Ils nous marquent les erreurs d'accès ou d'initialisation, et nous rappellent les règles à appliquer pour corriger nos erreurs.
Java langage & API
Retour au blog Java le soir
Cours & Tutoriaux
Table des matières
Introduction : un peu d'histoire
1. Java : genèse d'un nouveau langage
Programmer en Java
1. Un premier exemple
1.1. Aperçu général, cycle de vie
1.2. Un premier programme
1.3. Programmes, applets, servlets, etc...
2. Une première classe
2.1. Écriture d'une classe
2.2. Instanciation de cette classe
3. Types de base, classes et objets
3.1. Types de base
3.2. Classes et objets
Classes Object et String
1. Introduction
2. La classe Object
2.1. La méthode toString()
2.2. La méthode clone()
2.3. La méthode equals()
2.4. La méthode hashCode()
2.5. La méthode finalize()
2.6. La méthode getClass()
3. La classe String
3.1. Introduction
3.2. Construction d'un objet de type String
3.3. Concaténation, StringBuffer et StringBuilder
3.4. Concaténations de chaînes de caractères depuis Java 8
3.5. Concaténations de chaînes de caractères depuis Java 11
3.6. Extraction d'une sous-chaîne de caractères
3.7. Comparaison de deux chaînes de caractères
3.8. Méthodes de comparaisons lexicographiques
3.9. Méthode de recherche de caractères
3.10. Méthode de modification de chaîne
3.11. Méthode de duplication
3.12. Support de l'unicode, internationalisation
Structure d'une classe
1. Introduction
2. Classes
2.1. Classes publiques
2.2. Classes internes
2.3. Classe membre
2.4. Classes locales
2.5. Classes anonymes
2.6. Le mot-clé this
3. Éléments statiques
3.1. Champ statique
3.2. Cas des constantes
3.3. Bloc statique
3.4. Classe membre statique
4. Membres d'une classe, visibilité
4.1. Bloc non statique
4.2. Accès à un membre, visibilité
4.3. Les champs
4.4. Signature d'une méthode
4.5. Les méthodes
4.6. Getters et Setters
5. Constructeur, instanciation
5.1. Chargement d'une classe
5.2. Constructeurs d'une classe
5.3. Instanciation d'un objet
5.4. Destruction d'objets
5.5. Le mot-clé final
6. Énumérations
6.1. Déclaration d'une énumération
6.2. Classe énumération
6.3. Méthode toString()
6.4. Méthode valueOf()
6.5. Méthode values()
6.6. Méthode ordinal()
6.7. Méthode compareTo()
6.8. Constructeurs privés
6.9. Classe utilitaire : EnumSet
Noms, opérateurs, tableaux
1. Introduction
2. Identificateurs, noms et expressions
2.1. Identificateurs et noms
2.2. Expressions
3. Opérateurs, ordre d'exécution
3.1. Ordre d'exécution
3.2. Les opérateurs
3.3. Les opérateurs ++ et --
3.4. Les opérateurs % et /
3.5. Les opérateurs <<, >> et >>>
3.6. L'opérateur instanceof
3.7. Les opérateurs &, | et ^
3.8. Les opérateurs && et ||
3.9. L'opérateur ? ... :
3.10. Les opérateurs d'affectation
4. Tableaux
4.1. Création d'un tableau
4.2. Initialisation d'un tableau
4.3. Utilisation d'un tableau comme un Object
4.4. Tableaux de tableaux
4.5. Copie de tableaux
5. Blocs, boucles et contrôles
5.1. Blocs
5.2. Mots-clés réservés
5.3. Tests : if et switch
5.4. Boucles : for, while, do ... while
5.5. Commandes continue et break
5.6. Commandes return et goto
Nombres, précision, calculs
1. Introduction
2. Calculs
2.1. Précision
2.2. Codage des nombres flottants
2.3. Le mot-clé strictfp
2.4. Conversion de types
3. Dépassements de capacité
3.1. Cas des entiers
3.2. Cas des flottants
3.3. Bibliothèques BigInteger et BigDecimal
4. Fonctions mathématiques
4.1. Fonctions usuelles
4.2. Générateurs aléatoires
5. Classes enveloppe
5.1. Associer les types de base à des objets
5.2. Auto-boxing
Héritage, abstraction, interfaces
1. Introduction
2. Abstraction et encapsulation
2.1. Abstraction
2.2. Encapsulation
3. Héritage
3.1. Définition de l'héritage
3.2. Conséquences pour les membres
3.3. Polymorphisme
3.4. Empêcher l'héritage
4. Classes abstraites
5. Interfaces
5.1. Introduction
5.2. Définition
5.3. Java 8 et les interfaces
5.4. Utilisation des interfaces
5.5. Définition de constantes dans les interfaces
5.6. Utilité des interfaces
Packages
1. Introduction
2. Notion de paquet
2.1. Déclaration d’appartenance à un paquet
2.2. Chargement d’une classe
2.3. Choix de nom
3. Archives, chemin de recherche, classpath
3.1. Archives
3.2. Variable CLASSPATH
3.3. Notion de classloader
3.4. Bilan sur les classes chargées
3.5. Visibilité
3.6. Conseils d'écriture, bibliothèque standard
Exceptions
1. Introduction
2. Erreurs et Exceptions
2.1. Classe Throwable, notion de stack trace
2.2. Classe Error
2.3. Classe RuntimException
2.4. Classe Exception
3. Déclenchement d'une exception
3.1. Exceptions déclenchées par la JVM
3.2. Exceptions déclenchées par l'application
4. Capter une exception
4.1. Traiter une exception localement
4.2. Code de captage
5. Créer ses propres exceptions
Entrées / sorties
1. Introduction
2. Notion de fichier
2.1. Introduction
2.2. La classe File
2.3. Construction d'une instance de File
2.4. Méthodes exposées
3. Flux de sortie
3.1. Introduction, notion de flux
3.2. Écriture de caractères, classe Writer
3.3. Bufferisation, construction d'un flux sur un autre
3.4. Utilisation de PrintWriter
3.5. Écriture d'octets, OuputStream
3.6. Écriture de types primitifs : DataOutputStream
3.7. Écriture d'objets : ObjectOutputStream
4. Flux d'entrée
4.1. Introduction
4.2. Lecture de caractères, classe Reader
4.3. Bufferisation, lecture ligne par ligne
4.4. Lecture d'octets : InputStream
4.5. Lecture de types primitifs : DataInputStream
4.6. Lecture d'objets : ObjectInputStream
5. Lecture et écriture de flux croisés
5.1. Introduction
5.2. Lire des caractères sur un flux binaire : classe InputStreamReader
5.3. Écrire des caractères sur un flux binaire : classe OutputStreamWriter
5.4. Remarque sur les jeux de caractères
6. Serialization d'objets
6.1. Enjeu de la sérialization d'objets
6.2. Serialization d'un objet
6.3. Sérialization d'une grappe d'objets
6.4. Première surcharge : méthode writeObject() readObject()
6.5. Deuxième surcharge : utilisation d'un externalizer
6.6. Troisième surcharge : utilisation d'un objet proxy
7. Flux compressés
7.1. Introduction
7.2. Flux de type gzip
7.3. Flux de type zip