Programmation Java/Instanciation et cycle de vie

Un livre de Wikilivres.

Toute variable de type objet est une référence vers une instance de classe allouée en mémoire sur le tas. La référence particulière null pointe vers une instance non allouée, et constitue la valeur par défaut des références. Il faut donc allouer une instance et l'assigner à une variable de type référence avant de pouvoir utiliser un objet et ses membres par l'opérateur de déréférencement point : objet.membre. L'opérateur de déréférencement sur une référence nulle déclenche une exception de type NullPointerException.

MaClasse objet = null;
objet.toString(); // ---> NullPointerException

Un tableau est également un objet, pouvant stocker un nombre fixe d'éléments spécifié à l'instruction new.

int[] tableau = null;
tableau[0] = 1; // ---> NullPointerException

L'instruction new[modifier | modifier le wikicode]

L'instruction new permet d'instancier une classe en utilisant l'un des constructeurs de la classe.

Par exemple pour créer un objet de type MaClasse, on écrit :

MaClasse cl = new MaClasse("hello");

Cette instruction sert également à allouer les tableaux en spécifiant le nombre d'éléments entre crochets après leur type.

int[] entiers = new int[15]; // Alloue un tableau de 15 entiers initialisés à 0.
MaClasse[] mes_objets = new MaClasse[20]; // Alloue un tableau de 20 références de type MaClasse initialisées à null.

Les constructeurs[modifier | modifier le wikicode]

Un constructeur est une méthode particulière de la classe appelée lors de la création d'une instance de la classe. Son rôle est d'initialiser les membres de l'objet juste créé. Un constructeur a le même nom que sa classe et n'a pas de valeur de retour.

Dans l'exemple suivant la classe MaClasse dispose de deux constructeurs, l'un ne prenant aucun paramètre et l'autre prenant un paramètre de type String :

public class MaClasse
{
	// Attributs
	private String name;

	// Constructeurs
	public MaClasse()
	{
		name = "defaut";
	}

	public MaClasse(String s)
	{
		name = s;
	}
}

Toute classe possède au moins un constructeur. Cependant, il n'est pas obligatoire de déclarer un constructeur pour une classe. En effet, si aucun constructeur n'est déclaré dans une classe, un constructeur sans paramètre est ajouté de manière implicite. Celui-ci ne fait rien, en apparence.

Un constructeur d'objet en appelle toujours un autre :

  • Soit explicitement un autre constructeur de la même classe en utilisant this,
  • Soit explicitement un constructeur de la classe mère en utilisant super,
  • Soit implicitement le constructeur sans arguments de la classe mère.

Ces deux derniers points ne sont pas applicables à la classe java.lang.Object racine de toutes les autres classes.

Le code ci-dessous est strictement équivalent à l'exemple précédent, avec les appels implicites mis explicitement

public class MaClasse extends Object /* Implicite */
{
	// Attributs
	private String name;

	// Constructeurs
	public MaClasse()
	{
		super(); /* Implicite */
		name = "defaut";
	}

	public MaClasse(String s)
	{
		super(); /* Implicite */
		name = s;
	}
}

Quand une classe a plusieurs constructeurs qui définissent des valeurs par défaut, il vaut mieux que chaque constructeur appelle un autre constructeur de la classe. Cela permet de faciliter la maintenance en centralisant le comportement de construction et la définition des valeurs par défaut en un seul endroit du code.

public class MaClasse
{
	// Attributs
	private String name;
	private String description;

	// Constructeurs
	public MaClasse()
	{
		this("Pas de nom");
	}

	public MaClasse(String name)
	{
		this(name, "Pas de description");
	}

	public MaClasse(String name, String description)
	{
		// super(); /* Implicite */
		this.name = name;
		this.description = description;
	}
}

Initialisation d'un objet[modifier | modifier le wikicode]

L'appel à l'instruction new crée un nouvel objet de la classe spécifiée initialisé de la façon suivante :

  • Juste après l'allocation mémoire, tous les membres sont initialisés à leur valeur par défaut dépendant du type (null pour les références, false pour les booléens, et 0 pour les types numériques).
  • Le constructeur nommé en interne <init> correspondant aux types des arguments de l'instruction new est appelé.

Le code de chaque constructeur <init> est constitué de plusieurs parties concaténées :

  • l'appel explicite au constructeur de la classe de base dans le fichier source, ou un appel implicite au constructeur par défaut (sans arguments) ;
  • le code commun de construction définit par les initialisations de membres et les blocs de code non précédés du mot static
  • et le code définit explicitement dans le fichier source.

Exemple[modifier | modifier le wikicode]

public class MaClasse extends ClasseDeBase
{
	// Attributs : un nom et 4 lignes de description

	private String name;

	private DescriptionLine[] description = new String[4]; // (1)

	{
		description[0] = new DescriptionLine("Exemple d'objet"); // (2)
	}

	private DescriptionLine firstline = description[0]; // (3)

	// Constructeur
	public MaClasse(String name)
	{
		super("Exemple"); // (0)
		this.name = name; // (4)
	}
}

Le code du constructeur <init>(String name) contient alors cette séquence générée par le compilateur :

super("Exemple"); // (0)
description = new String[4]; // (1)
description[0] = new DescriptionLine("Exemple d'objet"); // (2)
firstline = description[0]; // (3)
this.name = name; // (4)

Les lignes d'initialisation (de (1) à (3) pour l'exemple précédent) sont donc insérées juste après l'appel au constructeur de la classe de base, dans tous les constructeurs de la classe.

L'ordre des initialisations est très important. Dans l'exemple précédent, si les lignes (1) et (3) sont interverties, une exception de type NullPointerException est lancée lors de la construction de l'objet.

Le bloc d’initialisation ne peut pas envoyer explicitement une exception, même s'il s'agit d'une sous-classe de RuntimeException. Dans le cas contraire, le compilateur retourne l'erreur Initializer does not complete normally.

Avertissement Ce code contient une erreur volontaire !
public class MaClasseNonImplementee extends ClasseDeBase
{
	{
		throw new RuntimeException("Non implémentée");
		// Erreur à la compilation : Initializer does not complete normally
	}
}

Par contre le constructeur peut en lancer :

public class MaClasseNonImplementee extends ClasseDeBase
{
	public MaClasseNonImplementee()
	{
		throw new RuntimeException("Non implémentée");
	}
}

Initialisation d'une classe[modifier | modifier le wikicode]

Avant la première instanciation, la classe est initialisée par la méthode statique spéciale nommée <clinit>. Le code de cette méthode est construit par le compilateur en concaténant les instructions d'initialisations des membres statiques et des blocs de code précédés du mot static.

Il s'agit donc du même principe que les constructeurs <init> vu dans la section précédente mais appliqué aux membres statiques de la classe. Toutefois aucun code de constructeur n'est ajouté à la fin (pas de constructeur statique), et le code appelle la méthode statique spéciale <clinit> de la classe de base plutôt qu'un constructeur.

Nettoyage[modifier | modifier le wikicode]

Ramasse-miettes (Garbage Collector)[modifier | modifier le wikicode]

Le ramasse-miettes garde un compteur du nombre de références pour chaque objet. Dès qu'un objet n'est plus référencé, celui-ci est marqué. Lorsque le ramasse-miettes s'exécute (en général quand l'application ne fait rien), les objets marqués sont libérés.

Son exécution se produit toujours à un moment qui ne peut être déterminé à l'avance. Il s'exécute lors des évènements suivants :

  • périodiquement, si le processeur n'est pas occupé,
  • quand la quantité de mémoire restante est insuffisante pour allouer un nouveau bloc de mémoire.

Il est donc recommandé de libérer toute référence (affecter null) à des objets encombrants (tableaux de grande taille, collection d'objets, ...) dès que possible, ou au plus tard juste avant l'allocation d'une grande quantité de mémoire.

Exemple : Pour le code suivant, il faut 49 152 octets disponibles, le ramasse-miettes ne pouvant rien libérer durant l'allocation du deuxième tableau.

byte[] buffer = new byte[16384];
// -> 1. allocation de 16384 octets
//    2. affecter la référence à la variable buffer
// ...
buffer = new byte[32768];
// -> 1. allocation de 32768 octets
//    2. affecter la référence à la variable buffer

Une fois le code corrigé, il ne faut plus que 32 768 octets disponibles, le ramasse-miettes pouvant libérer le premier tableau avant d'allouer le deuxième.

byte[] buffer = new byte[16384];
// -> 1. allocation de 16384 octets
//    2. affecter la référence à la variable buffer
// ...
buffer = null; // Le ramasse-miettes peut libérer les 16384 octets du tableau si besoin.
buffer = new byte[32768];
// -> 1. allocation de 32768 octets
//    2. affecter la référence à la variable buffer

Mettre à null les références devenues inutiles au plus tôt permet donc de réduire la quantité de mémoire libre nécessaire à l'allocation de tableaux ou d'objets.

Finalisation[modifier | modifier le wikicode]

Lors de la libération des objets par le ramasse-miettes, celui-ci appelle la méthode finalize() afin que l'objet libère les ressources qu'il utilise.

Cette méthode peut être redéfinie afin de libérer des ressources système non Java. Dans ce cas, la méthode doit se terminer en appelant la méthode de la classe parente :

super.finalize();

Comme l'exécution du ramasse-miettes se produit toujours à un moment qui ne peut être déterminé à l'avance, l'appel à cette méthode par le ramasse-miettes n'est pas garanti. C'est pourquoi cette méthode est rarement implémentée afin d'éviter des effets de bords.

Représentation des objets dans la mémoire[modifier | modifier le wikicode]

L'instruction new alloue la mémoire sur le tas pour les objets et les tableaux.

La création d'un objet de classe C qui hérite de la classe B, elle-même héritant de la classe A, se déroule à peu près de la manière suivante :

  • Pour chacune des classes de la hiérarchie en partant de la classe racine (ordre A, B, C pour l'exemple), si cela n'a pas déjà été fait auparavant :
    • Charger la classe (Class, code des méthodes, ...), en lui allouant également l'espace nécessaire pour ses variables membres statiques.
    • Initialiser les variables membres statiques de la classe en appelant la méthode spéciale <clinit> qui contient la concaténation des assignations des membres statiques et des blocs de code statiques.
  • Allocation de l'espace mémoire pour les membres d'instance de la classe C incluant les membres déclarés ainsi que ceux hérités des classes B et A.
  • Appel au constructeur (méthode spéciale <init>) de la classe C correspondant aux arguments fournis en lui passant la référence à la zone mémoire allouée dans this :
    • Sa première instruction est un appel explicite ou implicite à un constructeur de la classe B (lui-même appelant un constructeur de la classe A).
    • Les instructions suivantes sont la concaténation des assignations des membres d'instance et des blocs de code non statiques.
    • Enfin, les instructions définies explicitement dans le constructeur sont appelées.

La création d'un tableau avec l'instruction new se déroule de la même façon (sachant que tout type de tableau hérite de la classe java.lang.Object), en allouant la place nécessaire pour le type des éléments multipliée par le nombre d'éléments voulu.

Le tas et la pile[modifier | modifier le wikicode]

Le tas est la zone mémoire où sont alloués les éléments de tableaux et les membres des objets. C'est une zone mémoire très grande, partagée par tous les processus légers (threads) de l'application Java. C'est pourquoi l'accès à un même objet ou tableau par plusieurs threads nécessite des moyens de synchronisation.

La pile est une zone mémoire locale limitée, alloué pour chaque processus léger (thread). Cette zone sert de stockage temporaire aux appels de méthodes pour y stocker la valeur des arguments, des variables locales, et de manière interne la valeur de retour.

public class Test
{
	int a;
	int b = 50;
	String c;
	String d = "test";

	// Code inclus dans tous les constructeurs :
	{
		c = d.toUpperCase();
		System.out.println("C = "+c);
	}

	public static Object creerTest()
	{
		Object o; // Une référence nulle, allouée sur la pile
		o = new Test(); // Objet alloué sur le tas
		return o; // La méthode retourne la référence à l'objet alloué dans le tas
	}
}