Objets, Références et le mot-clé 'new' en Java
Fondamentaux JavaAnalyse approfondie de l'allocation sur le tas, l'aliasing de références et la mécanique du passage par valeur.
Dans cet article
Allocation et Nature des Références
En Java, il existe une distinction fondamentale entre une variable de référence et l’objet qu’elle désigne. Lorsque vous déclarez ShoppingBasket basket;, vous créez une variable de référence—un emplacement mémoire capable de stocker une référence à un objet ShoppingBasket. À ce stade, aucun objet n’existe sur le tas (heap).
L’utilisation du mot-clé new déclenche trois actions distinctes :
- Allocation Mémoire : La JVM alloue l’espace nécessaire sur le tas pour tous les champs d’instance de la classe.
- Initialisation : Les champs sont mis à leurs valeurs par défaut (0, false, ou null), puis le constructeur est exécuté pour définir l’état initial.
- Assignation de Référence : L’expression
newretourne la référence du nouvel objet, qui est ensuite stockée dans la variable.
Il est crucial de noter que les références Java ne sont pas des pointeurs comme en C++. Vous ne pouvez pas effectuer d’arithmétique de pointeurs ni voir l’adresse physique réelle. La référence est un handle opaque géré par la JVM.
Aliasing et Identité
L’aliasing se produit lorsque plusieurs variables de référence pointent vers le même objet sur le tas. Comme elles partagent la même valeur de référence, toute mutation effectuée via une variable est visible via toutes les autres.
L’identité est déterminée par le fait que deux références pointent vers le même objet sur le tas. On vérifie cela avec l’opérateur ==. À l’inverse, .equals() est destiné à vérifier l’égalité logique (équivalence de valeur), bien qu’il se comporte comme == par défaut s’il n’est pas redéfini.
Passage par Valeur : Le Piège de la Référence
Java utilise strictement le passage par valeur. Lorsque vous passez un objet à une méthode, vous ne passez pas l’objet lui-même, ni une référence à la variable. Vous passez une copie de la valeur de la référence.
Considérons ce scénario : deux variables référencent le même panier. Nous en passons une à une méthode qui modifie le panier puis tente de réassigner la référence.
Exemple concret : Mutation du Panier
import java.util.*;
public class BasketDemo {
static class ShoppingBasket {
List<String> items = new ArrayList<>();
void addItem(String item) {
items.add(item);
}
}
public static void main(String[] args) {
ShoppingBasket basketA = new ShoppingBasket();
ShoppingBasket basketB = basketA; // Aliasing : les deux pointent vers le même objet
System.out.println("Initial: basketA == basketB est " + (basketA == basketB));
processBasket(basketB);
System.out.println("Après méthode: basketA items: " + basketA.items);
System.out.println("Après méthode: basketA == basketB est " + (basketA == basketB));
}
static void processBasket(ShoppingBasket localBasket) {
// Mutation : affecte l'objet sur le tas
localBasket.addItem("Apple");
// Réassignation : change uniquement la copie locale de la référence
localBasket = new ShoppingBasket();
localBasket.addItem("Orange");
// L'orange est ajoutée à un nouvel objet qui sera ramassé par le GC
}
}
Analyse du résultat :
Initial: basketA == basketB est true: Les deux variables détiennent la même valeur de référence.Après méthode: basketA items: [Apple]: La mutationaddItem("Apple")a eu lieu sur l’objet dans le tas. CommebasketAetbasketBpointent vers cet objet,basketAvoit le changement.Après méthode: basketA == basketB est true: La réassignationlocalBasket = new ShoppingBasket()a seulement modifié la variable localelocalBasketà l’intérieur de la méthode. Elle n’a pas modifiébasketBdans la méthodemain.
Variables Locales Non Initialisées et Nulls
Les variables de champ (instance) sont initialisées automatiquement. Cependant, les variables locales (dans les méthodes) ne le sont pas. Tenter d’utiliser une variable locale non initialisée provoque une erreur de compilation.
null est une valeur de référence spéciale indiquant que la variable ne pointe vers aucun objet. Appeler une méthode sur une référence null déclenche une NullPointerException car il n’y a aucun objet sur le tas pour exécuter l’appel.
Exercice
Étant donné le code suivant, quel est l’état final de list1 et list2 ?
List<Integer> list1 = new ArrayList<>(List.of(1, 2));
List<Integer> list2 = list1;
modify(list1, list2);
void modify(List<Integer> a, List<Integer> b) {
a.add(3);
a = new ArrayList<>();
b.add(4);
}
Réponse :
list1 et list2 contiendront toutes deux [1, 2, 3, 4].
a.add(3)mute l’objet partagé.a = new ArrayList<>()réassigne seulement la copie localea; aucun effet surlist1.b.add(4)mute l’objet partagé carbpointe toujours vers la liste originale.