Chapitre 6: Pièges Java Fondamentaux
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érateur | Compare 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
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)) { ... }
"== 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
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 !
"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
| Primitif | Wrapper | Taille | Valeur par défaut |
|---|---|---|---|
int | Integer | 4 bytes | 0 |
long | Long | 8 bytes | 0L |
double | Double | 8 bytes | 0.0 |
boolean | Boolean | 1 bit | false |
char | Character | 2 bytes | '\u0000' |
Quand utiliser quoi ?
| Situation | Utiliser | Pourquoi |
|---|---|---|
| Variables locales, calculs | int, double | Plus rapide, pas de boxing |
| Attributs d'entités JPA | Long, Integer | Peut être null (important pour id non encore persisté) |
Collections (List, Map) | Integer, Long | Les collections n'acceptent pas les primitifs |
| Paramètres optionnels | Integer | Peut ê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
Integer a = null;
if (a == 0) { ... } // 💥 NullPointerException (unboxing de null)
Toujours vérifier null avant de comparer un Wrapper avec un primitif.
"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
| Contexte | Signification | Exemple |
|---|---|---|
| Variable | La référence ne peut pas changer | final List<String> list = new ArrayList<>() |
| Méthode | Ne peut pas être surchargée (override) | public final void process() |
| Classe | Ne peut pas être héritée | public 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) └─────────────────┘
"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
| Usage | Exemple | Bonne pratique ? |
|---|---|---|
| Constantes | static final int MAX = 100 | Oui |
| Méthodes utilitaires | Math.max(), Collections.sort() | Oui |
| Variables partagées | static int count | Attention (thread safety) |
| Classes internes | static class Builder | Oui (Builder pattern) |
É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éthode | Usage |
|---|---|
.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) |
"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) { ... }
"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"] │
└──────────────┘
"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
| Aspect | Interface | Classe abstraite |
|---|---|---|
| Mot-clé | implements | extends |
| Héritage multiple | Oui (plusieurs interfaces) | Non (une seule classe) |
| Constructeur | Non | Oui |
| Champs d'instance | Non (que static final) | Oui |
| Méthodes concrètes | Oui (depuis Java 8, default) | Oui |
| Quand utiliser | Contrat / 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!"); }
}
"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
| Question | Ré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/hashCode | Si a.equals(b) alors a.hashCode() == b.hashCode() |
final | Référence non réassignable (pas immuabilité de l'objet) |
static | Appartient à la classe, pas à l'instance |
| String immuable | Chaque modification crée un nouvel objet |
Optional | Conteneur qui peut être vide, évite les null checks |
| Passage par valeur | Java copie la valeur (ou la référence pour les objets) |
| Interface vs Abstraite | Interface = contrat, Abstraite = base commune |