Introduction
Avant qu'un objet Java ne puisse être manipulé, il est impératif qu'il soit correctement initialisé. L'instanciation est le mécanisme par lequel une classe sert de modèle pour créer une instance concrète en mémoire. L'instruction courante Personne p = new Personne(); illustre précisément la création d'une nouvelle instance de la classe Personne et l'affectation d'une référence à celle-ci.
Concernant l'instanciation des classes, nous nous penchons sur deux aspects fondamentaux :
- Les différentes méthodes pour créer une instance.
- L'ordre et le moment de l'exécution des diverses phases d'initialisation.
Les Différentes Méthodes d'Instanciation
1. Utilisation du mot-clé new
C'est la méthode la plus courante et la plus directe pour créer un objet. Elle implique l'appel explicite d'un constructeur de la classe.
class EntiteBase { /* ... */ }
EntiteBase monObjet = new EntiteBase();
2. Utilisation des méthodes newInstance()
Il existe deux principales façons d'utiliser newInstance() pour instancier un objet :
a. Via Class.newInstance() (Obsolète depuis Java 9)
Cette méthode, dispoinble dans l'objet Class, permet de créer une instance en invoquant le constructeur sans arguments de la classe. Le constructeur doit être public.
try {
Class<?> classeCible = Class.forName("com.exemple.MaClasse");
MaClasse instance = (MaClasse) classeCible.newInstance(); // Nécessite un constructeur public sans arguments
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
b. Via java.lang.reflect.Constructor.newInstance()
Cette approche, plus puissante, utilise l'API de réflexion pour obtenir un objet Constructor spécifique. Elle permet d'appeler des constructeurs avec des arguments et même des constructeurs privés.
class Produit {
private String nom;
private int prix;
public Produit(String nom, int prix) {
this.nom = nom;
this.prix = prix;
System.out.println("Produit '" + nom + "' créé avec un prix de " + prix);
}
private Produit(String nom) { // Constructeur privé
this(nom, 0);
System.out.println("Produit privé '" + nom + "' créé.");
}
}
// ... dans une méthode main ou autre
try {
Class<Produit> classeProduit = (Class<Produit>) Class.forName("Produit");
// Appel d'un constructeur public avec arguments
Constructor<Produit> constructeurPublic = classeProduit.getConstructor(String.class, int.class);
Produit produit1 = constructeurPublic.newInstance("Livre", 25);
// Appel d'un constructeur privé
Constructor<Produit> constructeurPrive = classeProduit.getDeclaredConstructor(String.class);
constructeurPrive.setAccessible(true); // Rend le constructeur privé accessible
Produit produit2 = constructeurPrive.newInstance("Stylo secret");
} catch (Exception e) {
e.printStackTrace();
}
3. Utilisation de la méthode clone()
La méthode clone() crée une copie de l'objet existant. Un point crucial est qu'aucune méthode de constructeur n'est appelée lors de la création d'objets via clone(). Cette méthode est héritée de la classe Object et nécessite que la classe implémente l'interface Cloneable.
class Tache implements Cloneable {
String description;
public Tache(String desc) { this.description = desc; }
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone(); // Réalise une copie superficielle par défaut
}
}
// ...
try {
Tache tacheOriginale = new Tache("Préparer la réunion");
Tache tacheCopie = (Tache) tacheOriginale.clone();
System.out.println("Original: " + tacheOriginale.description);
System.out.println("Copie: " + tacheCopie.description);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
4. Désérialisation d'objets
Lorsqu'un objet est désérialisé (lu à partir d'un flux binaire), la JVM recrée l'objet en mémoire sans appeler ses constructeurs. La classe doit implémenter l'interface java.io.Serializable.
import java.io.*;
class Utilisateur implements Serializable {
private static final long serialVersionUID = 1L;
String nom;
transient String motDePasse; // Ne sera pas sérialisé
public Utilisateur(String nom, String motDePasse) {
this.nom = nom;
this.motDePasse = motDePasse;
System.out.println("Constructeur Utilisateur appelé pour " + nom);
}
@Override
public String toString() {
return "Utilisateur{nom='" + nom + "', motDePasse='" + (motDePasse == null ? "N/A" : motDePasse) + "'}";
}
}
// ... dans une méthode main ou autre
try {
// 1. Sérialisation de l'objet
Utilisateur user1 = new Utilisateur("Alice", "secret123");
FileOutputStream fos = new FileOutputStream("utilisateur.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(user1);
oos.close();
fos.close();
System.out.println("Objet sérialisé.");
// 2. Désérialisation de l'objet
FileInputStream fis = new FileInputStream("utilisateur.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
Utilisateur user2 = (Utilisateur) ois.readObject();
ois.close();
fis.close();
System.out.println("Objet désérialisé: " + user2);
// Notez que le constructeur n'est pas appelé lors de la désérialisation
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
Le Processus Détaillé d'Initialisation d'un Objet
L'instruction MaClasse obj = new MaClasse(); déclenche une série d'opérations au sein de la JVM. Globalement, on peut décomposer cela en :
- Allocation de l'espace mémoire pour l'objet
MaClassedans le tas (heap). - Exécution du processus d'initialisation de l'objet (qui inclut l'initialisation des membres, des blocs et des constructeurs).
- Création d'une variable de référence
obj. - Assignation de cette référence à l'objet nouvellement créé en mémoire.
Le cœur de ce processus réside dans l'initialisation de l'objet lui-même, qui se déroule en plusieurs étapes ordonnées :
- Initialisation de la classe (si ce n'est pas déjà fait).
- Initialisation des variables d'instance de l'objet.
- Exécution des blocs d'initialisation d'instance de l'objet.
- Exécution du constructeur de l'objet.
Ces étapes s'appliquent de manière récursive, de la super-classe la plus haute (Object) à la classe concrète instanciée.
1. Initialisation de la Classe
La toute première fois qu'une classe est référencée (par exemple, lors de la création de la première instance ou de l'accès à un membre statique), la JVM s'assure que la classe est chargée, vérifiée, préparée, et finalement initialisée. Cette phase d'initialisation de la classe comprend l'exécution de tous les blocs statiques et l'assignation des valeurs aux variables statiques.
Ceci est un processus unique par classe et se produit avant toute initialisation d'objet de cette classe.
2. Initialisation des Variables d'Instance
Après l'initialisation de la classe (si nécessaire), chaque fois qu'un nouvel objet est créé, la JVM initialise les variables d'instance. Si une variable d'instance est déclarée et directement assignée une valeur, cette assignation se produit avant l'exécution du constructeur.
class ElementParent {
int valA = 10;
int valB = valA + 2; // valB devient 12
public ElementParent() {
System.out.println("Constructeur ElementParent: valA=" + valA + ", valB=" + valB);
valB += 5; // valB devient 17
}
}
class ElementEnfant extends ElementParent {
int valC = 20;
int valD = valC - 3; // valD devient 17
public ElementEnfant() {
System.out.println("Constructeur ElementEnfant: valA=" + valA + ", valB=" + valB + ", valC=" + valC + ", valD=" + valD);
valD += 10; // valD devient 27
}
}
// Méthode main ou autre:
public static void main(String[] args) {
System.out.println("--- Début test d'initialisation des variables ---");
ElementEnfant enfant = new ElementEnfant();
System.out.println("--- Fin test d'initialisation des variables ---");
}
L'ordre d'exécution pour les variables d'instance est le suivant :
- Initialisation des variables d'instance de la super-classe (
valA=10, valB=12). - Exécution du constructeur de la super-classe (
valBpasse à17). - Initialisation des variables d'instance de la classe actuelle (
valC=20, valD=17). - Exécution du constructeur de la classe actuelle (
valDpasse à27).
3. Exécution des Blocs d'Initialisation d'Instance
Les blocs d'initialisation d'instance (code entre accolades {}, non statique et non rattaché à une méthode) s'exécutent pour chaque nouvelle instance de la classe. Ils s'exécutent après l'initialisation des variables d'instance mais avant le corps du constructeur.
class ClasseMere {
{
System.out.println("3. Bloc d'instance de ClasseMere");
}
public ClasseMere() {
System.out.println("4. Constructeur de ClasseMere");
}
}
class ClasseFille extends ClasseMere {
static {
System.out.println("2. Bloc statique de ClasseFille (si la classe est chargée)");
}
{
System.out.println("5. Bloc d'instance de ClasseFille");
}
public ClasseFille() {
System.out.println("6. Constructeur de ClasseFille");
}
}
// Pour le test:
public static void main(String[] args) {
System.out.println("--- Début de l'instanciation ---");
ClasseFille objetFille = new ClasseFille();
System.out.println("--- Fin de l'instanciation ---");
}
Si ClasseMere avait aussi un bloc statique, il s'exécuterait avant celui de ClasseFille (étape 1).
4. Exécution du Constructeur
Chaque objet Java possède au moins un constructeur. Si aucun n'est explicitement défini, le compilateur en fournit un par défaut et sans arguments. En bytecode, les constructeurs sont représentés par la méthode <init>().
Une exigence fnodamentale de Java est que la première instruction de tout constructeur (sauf celui de Object) doit être un appel à un constructeur de sa super-classe, via super() ou this(). Si ce n'est pas explicitement fait, le compilateur insère automatiquement super();. Ceci garantit que la chaîne d'initialisation des ancêtres est respectée, assurant que toutes les propriétés héritées sont correctement configurées.
Synthèse de l'Ordre d'Exécution Complet
En combinant toutes les étapes et en considérant l'héritage, le processus d'initialisation d'une classe et de son objet est un processus récursif. Lors de l'instanciation de ClasseFille qui hérite de ClasseMere, l'ordre d'exécution est le suivant :
- Blocs statiques de la super-classe (
ClasseMere). - Blocs statiques de la classe actuelle (
ClasseFille). - Initialisation des variables d'instance de la super-classe.
- Blocs d'initialisation d'instance de la super-classe.
- Constructeur de la super-classe.
- Initialisation des variables d'instance de la classe actuelle.
- Blocs d'initialisation d'instance de la classe actuelle.
- Constructeur de la classe actuelle.
Pour vérifier cet ordre :
class SuperClasseA {
static {
System.out.println("1. Bloc statique de SuperClasseA");
}
{
System.out.println("3. Bloc d'instance de SuperClasseA");
}
public SuperClasseA() {
System.out.println("4. Constructeur de SuperClasseA");
}
}
class SousClasseB extends SuperClasseA {
static {
System.out.println("2. Bloc statique de SousClasseB");
}
{
System.out.println("5. Bloc d'instance de SousClasseB");
}
public SousClasseB() {
System.out.println("6. Constructeur de SousClasseB");
}
}
public class TestInitialisation {
public static void main(String[] args) {
System.out.println("Début de l'instanciation de SousClasseB:");
SousClasseB monObjet = new SousClasseB();
System.out.println("Fin de l'instanciation.");
}
}
L'exécution de ce code produira la sortie suivante, confirmant l'ordre décrit :
Début de l'instanciation de SousClasseB:
1. Bloc statique de SuperClasseA
2. Bloc statique de SousClasseB
3. Bloc d'instance de SuperClasseA
4. Constructeur de SuperClasseA
5. Bloc d'instance de SousClasseB
6. Constructeur de SousClasseB
Fin de l'instanciation.