Aller au contenu principal

Chapitre 6: Pièges Java Fondamentaux

Objectif

Ce chapitre couvre les questions Java pures les plus fréquentes en entrevue. Ce sont les bases que tout développeur Java doit maîtriser. Un intervieweur les pose pour vérifier que vous comprenez comment Java fonctionne en mémoire.


1. == vs .equals() — LA question la plus posée​

La règle d'or​

OpérateurCompare quoi ?Utiliser pour
==Les adresses mémoire (références)Types primitifs (int, boolean, char...)
.equals()Le contenu (valeurs)Objets (String, Integer, List...)

Exemples avec String​

String a = "hello";
String b = "hello";
String c = new String("hello");

System.out.println(a == b); // true ← même référence (String Pool)
System.out.println(a == c); // false ← références différentes (new crée un nouvel objet)
System.out.println(a.equals(c)); // true ← même contenu

Pourquoi a == b est true ?​

Java maintient un String Pool (cache) en mémoire. Quand vous écrivez "hello" en dur dans le code, Java réutilise le même objet :

Mémoire:
┌──────────────────────────────────┐
│ String Pool │
│ ┌─────────┐ │
│ │ "hello" │ ← a et b pointent ici
│ └─────────┘ │
└──────────────────────────────────┘

┌──────────────────────────────────┐
│ Heap (tas) │
│ ┌─────────┐ │
│ │ "hello" │ ← c pointe ici (new = toujours un nouvel objet)
│ └─────────┘ │
└──────────────────────────────────┘

Exemples avec Integer (piège favori !)​

Integer x = 127;
Integer y = 127;
System.out.println(x == y); // true ← cache Integer [-128, 127]

Integer a = 128;
Integer b = 128;
System.out.println(a == b); // false ← hors du cache !
System.out.println(a.equals(b)); // true ← même valeur
Piège d'entrevue

Java cache les Integer de -128 à 127. Au-delà, == retourne false même si les valeurs sont identiques. C'est pourquoi il faut toujours utiliser .equals() pour comparer des objets.

La bonne pratique : "CONSTANT".equals(variable)​

String status = getStatus(); // peut retourner null

// DANGEREUX — NullPointerException si status est null
if (status.equals("ACTIVE")) { ... }

// SÉCURITAIRE — ne lance jamais de NPE
if ("ACTIVE".equals(status)) { ... }
Phrase d'entrevue

"== compares references (memory addresses), .equals() compares content. I always use .equals() for objects. For strings, I put the constant on the left to avoid NullPointerException."


2. hashCode() — C'est quoi et pourquoi c'est important ?​

Définition simple​

hashCode() retourne un nombre entier qui représente un objet. C'est comme une empreinte digitale : rapide à comparer, mais pas unique à 100%.

Le contrat hashCode/equals​

Règle absolue

Si deux objets sont .equals(), ils DOIVENT avoir le même hashCode().

Si vous redéfinissez .equals(), vous devez aussi redéfinir hashCode().

// MAUVAIS — redéfinit equals sans hashCode
public class User {
private String email;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User other)) return false;
return Objects.equals(email, other.email);
}
// PAS de hashCode() → BUG avec HashMap, HashSet !
}

// BON — redéfinit les deux
public class User {
private String email;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User other)) return false;
return Objects.equals(email, other.email);
}

@Override
public int hashCode() {
return Objects.hash(email);
}
}

Pourquoi c'est critique ? HashMap et HashSet en dépendent !​

User user1 = new User("alice@mail.com");
User user2 = new User("alice@mail.com");

// Sans hashCode correct :
Set<User> set = new HashSet<>();
set.add(user1);
set.add(user2);
System.out.println(set.size()); // 2 ← MAUVAIS ! Devrait être 1

// Avec hashCode correct :
set.add(user1);
set.add(user2);
System.out.println(set.size()); // 1 ← CORRECT

Comment HashMap utilise hashCode​

hashCode("alice@mail.com") → 12345

HashMap interne :
┌──────┬──────────────────┐
│ slot │ contenu │
├──────┼──────────────────┤
│ ... │ │
│ 12345│ → User("alice") │ ← hashCode détermine le slot
│ ... │ │
└──────┴──────────────────┘

1. hashCode() → trouve le bon slot (rapide, O(1))
2. equals() → vérifie que c'est bien le même objet

Avec Lombok (le plus simple)​

@Data  // génère equals(), hashCode(), toString(), getters, setters
public class User {
private String email;
private String name;
}

// Ou pour contrôler quels champs utiliser :
@EqualsAndHashCode(of = {"email"})
public class User {
private String email;
private String name;
}

Avec Java Records (le plus moderne)​

public record User(String email, String name) {}
// equals(), hashCode(), toString() sont générés automatiquement !
Phrase d'entrevue

"hashCode is like a fingerprint for objects. HashMap uses it to find the right bucket in O(1). The contract says: if two objects are equal, they must have the same hashCode. That's why I always override both together — or I use Lombok @Data or Java Records."


3. toString() — Pour le débogage​

Sans toString​

User user = new User("Alice", "alice@mail.com");
System.out.println(user);
// → com.example.User@1a2b3c4d ← inutile !

Avec toString​

@Override
public String toString() {
return "User{name='%s', email='%s'}".formatted(name, email);
}

System.out.println(user);
// → User{name='Alice', email='alice@mail.com'} ← utile !

Avec Lombok ou Record​

@Data       // génère toString() automatiquement
public class User { ... }

// Ou
public record User(String name, String email) {}
// toString() → User[name=Alice, email=alice@mail.com]

4. Types primitifs vs Wrapper classes​

PrimitifWrapperTailleValeur par défaut
intInteger4 bytes0
longLong8 bytes0L
doubleDouble8 bytes0.0
booleanBoolean1 bitfalse
charCharacter2 bytes'\u0000'

Quand utiliser quoi ?​

SituationUtiliserPourquoi
Variables locales, calculsint, doublePlus rapide, pas de boxing
Attributs d'entités JPALong, IntegerPeut être null (important pour id non encore persisté)
Collections (List, Map)Integer, LongLes collections n'acceptent pas les primitifs
Paramètres optionnelsIntegerPeut être null pour indiquer "non fourni"

Autoboxing / Unboxing​

// Autoboxing : int → Integer (automatique)
Integer a = 42;

// Unboxing : Integer → int (automatique)
int b = a;

// PIÈGE : unboxing de null → NullPointerException !
Integer c = null;
int d = c; // 💥 NullPointerException
Piège d'entrevue
Integer a = null;
if (a == 0) { ... } // 💥 NullPointerException (unboxing de null)

Toujours vérifier null avant de comparer un Wrapper avec un primitif.

Phrase d'entrevue

"I use primitives for local variables and calculations, and wrappers for entity IDs and collections. The key pitfall is unboxing null, which throws NullPointerException."


5. final — Trois usages différents​

ContexteSignificationExemple
VariableLa référence ne peut pas changerfinal List<String> list = new ArrayList<>()
MéthodeNe peut pas être surchargée (override)public final void process()
ClasseNe peut pas être héritéepublic final class String

Piège : final ne rend PAS l'objet immuable​

final List<String> list = new ArrayList<>();
list.add("hello"); // ✅ OK — on modifie le CONTENU
list = new ArrayList<>(); // ❌ Erreur — on essaie de changer la RÉFÉRENCE
Mémoire :
┌─────────────────┐
list (final) ──→ │ ArrayList │
(référence │ ["hello"] │ ← le contenu peut changer
ne bouge pas) └─────────────────┘
Phrase d'entrevue

"final means the reference can't be reassigned, but the object itself can still be mutated. For true immutability, I use List.of() or Collections.unmodifiableList()."


6. static — Appartient à la classe, pas à l'instance​

public class Counter {
static int totalCount = 0; // partagé entre TOUTES les instances
int instanceCount = 0; // propre à chaque instance

public Counter() {
totalCount++;
instanceCount++;
}
}

Counter c1 = new Counter();
Counter c2 = new Counter();
Counter c3 = new Counter();

System.out.println(Counter.totalCount); // 3 (partagé)
System.out.println(c3.instanceCount); // 1 (propre à c3)

static en pratique​

UsageExempleBonne pratique ?
Constantesstatic final int MAX = 100Oui
Méthodes utilitairesMath.max(), Collections.sort()Oui
Variables partagéesstatic int countAttention (thread safety)
Classes internesstatic class BuilderOui (Builder pattern)
Dans Spring Boot

Évitez les variables static mutables dans les beans Spring. Les beans sont déjà singleton par défaut — utilisez des champs d'instance normaux.


7. String — Immuable par design​

Les String sont immuables​

String s = "hello";
s.toUpperCase();
System.out.println(s); // "hello" ← PAS modifié !

s = s.toUpperCase();
System.out.println(s); // "HELLO" ← nouvelle String assignée

Chaque modification crée un nouvel objet String.

Concaténation en boucle : StringBuilder​

// MAUVAIS — crée N objets String intermédiaires → O(n²)
String result = "";
for (int i = 0; i < 10000; i++) {
result += i + ",";
}

// BON — un seul objet, modifications en place → O(n)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i).append(",");
}
String result = sb.toString();

String Pool (cache)​

String a = "hello";          // va dans le String Pool
String b = "hello"; // réutilise le même objet du Pool
String c = new String("hello"); // crée un NOUVEL objet dans le Heap

System.out.println(a == b); // true (même référence dans le Pool)
System.out.println(a == c); // false (c est dans le Heap, pas le Pool)

Comparaison de Strings​

MéthodeUsage
.equals()Comparaison sensible à la casse
.equalsIgnoreCase()Comparaison insensible à la casse
.compareTo()Ordre alphabétique (retourne int)
.contains()Vérifier si contient une sous-chaîne
.startsWith() / .endsWith()Vérifier début/fin
.isBlank()Vide ou espaces seulement (Java 11+)
.isEmpty()Vide (longueur 0)
Phrase d'entrevue

"Strings are immutable in Java. Every modification creates a new object. For concatenation in loops, I use StringBuilder. For comparison, I always use .equals(), never ==."


8. Optional — Éviter les NullPointerException​

Le problème​

// NullPointerException si findById retourne null
User user = userRepository.findById(1L);
String email = user.getEmail(); // 💥 NPE si user est null

La solution : Optional​

Optional<User> optional = userRepository.findById(1L);

// Approche 1 : isPresent
if (optional.isPresent()) {
User user = optional.get();
}

// Approche 2 : orElse (valeur par défaut)
User user = optional.orElse(new User("anonymous"));

// Approche 3 : orElseThrow (exception)
User user = optional.orElseThrow(() ->
new ResourceNotFoundException("User not found with id: 1"));

// Approche 4 : map (transformer)
String email = optional.map(User::getEmail).orElse("N/A");

Les anti-patterns à éviter​

// ❌ MAUVAIS — revient au même que null check
if (optional.isPresent()) {
return optional.get();
} else {
return null;
}

// ✅ BON
return optional.orElse(null);

// ❌ MAUVAIS — Optional en paramètre
public void process(Optional<String> name) { ... }

// ✅ BON — Optional uniquement en retour
public Optional<User> findByEmail(String email) { ... }
Phrase d'entrevue

"Optional represents a value that may or may not be present. I use it as a return type, never as a parameter. I prefer orElseThrow() for required values and map() for transformations."


9. Passage par valeur vs passage par référence​

Java passe TOUT par valeur​

// Primitifs : la VALEUR est copiée
void increment(int x) {
x++; // modifie la copie locale
}
int a = 5;
increment(a);
System.out.println(a); // 5 ← pas modifié

// Objets : la RÉFÉRENCE est copiée (pas l'objet)
void addItem(List<String> list) {
list.add("new"); // modifie l'objet pointé par la copie de référence
}
List<String> myList = new ArrayList<>();
addItem(myList);
System.out.println(myList); // [new] ← modifié !

// Mais réassigner la référence ne change rien
void replace(List<String> list) {
list = new ArrayList<>(); // change la copie locale, pas l'original
}
replace(myList);
System.out.println(myList); // [new] ← toujours l'ancienne liste

Schéma mémoire​

Appel : addItem(myList)

main() addItem()
┌──────┐ ┌──────┐
│myList │──┐ ┌────│ list │ ← copie de la référence
└──────┘ │ │ └──────┘
↓ ↓
┌──────────────┐
│ ArrayList │ ← les deux pointent vers le MÊME objet
│ ["new"] │
└──────────────┘
Phrase d'entrevue

"Java is always pass-by-value. For primitives, the value is copied. For objects, the reference is copied — so you can modify the object through the copy, but you can't make the original variable point to a different object."


10. Interfaces vs Classes abstraites​

AspectInterfaceClasse abstraite
Mot-cléimplementsextends
Héritage multipleOui (plusieurs interfaces)Non (une seule classe)
ConstructeurNonOui
Champs d'instanceNon (que static final)Oui
Méthodes concrètesOui (depuis Java 8, default)Oui
Quand utiliserContrat / capacité ("can do")Base commune ("is a")
// Interface = contrat (ce que l'objet PEUT faire)
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}

class Duck implements Flyable, Swimmable {
public void fly() { ... }
public void swim() { ... }
}

// Classe abstraite = base commune (ce que l'objet EST)
abstract class Animal {
protected String name;

Animal(String name) { this.name = name; }

abstract void makeSound();

void breathe() { System.out.println(name + " breathes"); }
}

class Dog extends Animal {
Dog(String name) { super(name); }
void makeSound() { System.out.println("Woof!"); }
}
Phrase d'entrevue

"I use interfaces to define contracts (what an object can do) and abstract classes to share common behavior (what an object is). Since Java 8, interfaces can have default methods, which blurred the line — but I still prefer interfaces for flexibility and multiple inheritance."


Résumé rapide pour l'entrevue​

QuestionRéponse en 1 phrase
== vs .equals()== compare les références, .equals() compare le contenu
hashCode()Empreinte numérique d'un objet, utilisée par HashMap/HashSet
Contrat equals/hashCodeSi a.equals(b) alors a.hashCode() == b.hashCode()
finalRéférence non réassignable (pas immuabilité de l'objet)
staticAppartient à la classe, pas à l'instance
String immuableChaque modification crée un nouvel objet
OptionalConteneur qui peut être vide, évite les null checks
Passage par valeurJava copie la valeur (ou la référence pour les objets)
Interface vs AbstraiteInterface = contrat, Abstraite = base commune

⬅️ Chapitre 5: Débogage | Chapitre 7: Collections Java →