Aller au contenu

Pièges classiques

Les erreurs qui piègent le plus souvent quelqu'un qui vient de Rust, C++ ou Python, classées par symptôme. Chaque entrée renvoie au chapitre qui l'explique en détail.

Le bon réflexe

Quand ça ne marche pas : lis le premier Caused by: de la pile d'appels (pas la dernière ligne), puis les logs de démarrage. Avec Spring, la cause réelle est souvent tout en bas d'une longue trace.

Java : égalité et valeurs

== renvoie false sur deux chaînes ou deux Integer identiques
== compare les références pour les objets. Utilise a.equals(b), ou Objects.equals(a, b) si a peut être null. Avec les Integer, == « marche » entre -128 et 127 (cache) puis échoue au-delà : un bug qui passe les tests. → Mémoire et références
Un objet ajouté à un HashSet ou utilisé comme clé de HashMap est introuvable
equals redéfini sans hashCode (les deux vont ensemble). Un record les génère pour toi. → Classes et objets
Un calcul d'entier donne un résultat négatif absurde
Le dépassement est silencieux (pas de panique comme en Rust en debug), et il n'existe pas d'entiers non signés. Utilise long, ou Math.addExact / multiplyExact pour lever une exception. → Syntaxe et types
NullPointerException sur une ligne sans appel de méthode
Un Integer, Long ou Boolean valant null est déballé automatiquement vers un primitif (int x = map.get(cle);). → Génériques et collections

Java : mutabilité et collections

Un objet change « tout seul »
Deux variables pointent vers le même objet (pas de borrow checker pour l'interdire). final fige la variable, pas l'objet. Un record qui reçoit une List garde la même référence : fais List.copyOf(...) dans son constructeur compact. → Mémoire et références, Records, enums, sealed
UnsupportedOperationException sur un add
List.of(...), Map.of(...) et stream.toList() renvoient des collections immuables. Copie dans une new ArrayList<>(...) pour modifier. → Génériques et collections, Lambdas et streams
ConcurrentModificationException dans une boucle
Tu modifies une collection pendant que tu la parcours avec un for (x : liste). Utilise liste.removeIf(...) ou un itérateur explicite. → Génériques et collections
liste.remove(1) retire le mauvais élément
Sur une List<Integer>, remove(1) retire l'élément d'indice 1. Pour retirer la valeur 1 : remove(Integer.valueOf(1)). → Génériques et collections
Une liste accepte n'importe quel type
var l = new ArrayList<>(); donne une ArrayList<Object>. Avec var, écris le type dans les chevrons. → Génériques et collections

Java : méthodes, classes, exceptions

Impossible de donner une valeur par défaut à un paramètre
Java n'a ni valeurs par défaut ni arguments nommés : on écrit des surcharges (une méthode qui appelle l'autre), ou un builder quand il y a beaucoup d'options. → Syntaxe et types
Un champ vaut null alors que le constructeur l'initialise
Le constructeur parent appelle une méthode redéfinie par la sous-classe, avant que les champs de celle-ci soient initialisés. N'appelle pas de méthode redéfinissable dans un constructeur. → Classes et objets
Une classe d'un autre fichier accède à un champ « protégé »
Sans mot-clé, la visibilité est « paquet » (pas private), et protected inclut tout le paquet. Mets les champs en private. → Classes et objets
Une méthode de secours est exécutée alors que l'Optional est rempli
orElse(calcul()) évalue toujours son argument. Utilise orElseGet(() -> calcul()). → Exceptions et Optional
Une exception disparaît sans laisser de trace
Un catch vide, ou un return dans un finally, qui écrase l'exception. Relance-la (en l'enveloppant avec sa cause) ou journalise-la. → Exceptions et Optional
Un compteur partagé entre threads perd des incréments
Les data races compilent en Java (pas de Send/Sync). Utilise AtomicInteger, ConcurrentHashMap ou synchronized, ou mieux : pas d'état mutable partagé. → Threads et concurrence
getAnnotation(...) renvoie null sur ta propre annotation
Il manque @Retention(RetentionPolicy.RUNTIME) : par défaut l'annotation n'existe plus à l'exécution. → Annotations et réflexion

Outillage

java -version et javac -version ne donnent pas la même version
Plusieurs JDK installés et un JAVA_HOME ou un PATH qui pointe vers le mauvais. Maven utilise JAVA_HOME. → Installer et lancer
./mvnw: Permission denied
Le bit d'exécution a été perdu (archive extraite sans les droits) : chmod +x mvnw. → Maven
Le code marche avec java Fichier.java mais pas avec Maven
Le lanceur de fichier source est plus tolérant que javac sur l'organisation des fichiers et des paquets. → Installer et lancer

Spring Boot

Un contrôleur ou un service est ignoré (route en 404, bean introuvable)
La classe n'est pas dans le paquet de la classe @SpringBootApplication ni dans un sous-paquet : le scan des composants ne la voit pas. → Premier projet
404 alors que la route existe, logs qui parlent d'une vue
@Controller au lieu de @RestController : Spring cherche une page HTML portant le nom de ta valeur de retour. → API REST
Les contraintes @NotBlank, @Size... sont ignorées
Il manque @Valid devant le @RequestBody. Sans lui, personne ne lit ces annotations. → API REST
Des utilisateurs voient les données d'autres utilisateurs
Un champ mutable dans un bean : les beans sont des singletons partagés par tous les threads de requêtes. Garde l'état dans les variables locales, la base, ou des objets dédiés à la requête. → Injection de dépendances
@Transactional (ou @Cacheable, @Async...) n'a aucun effet
La méthode est appelée depuis la même classe (this.methode()), ce qui contourne le proxy ; ou elle est private. Appelle-la depuis un autre bean. → Annotations et réflexion, Injection de dépendances
Une erreur n'annule pas la transaction
Par défaut, seul un rollback sur exception unchecked (RuntimeException) est fait. Pour une exception checked : @Transactional(rollbackFor = ...), ou utilise des exceptions unchecked. → Base de données (JPA)
LazyInitializationException: ... (no session)
Une relation chargée paresseusement est lue après la fin de la transaction (souvent en sérialisant une entité en JSON). Charge ce qu'il faut dans le service et renvoie des DTO. → Base de données (JPA)
Le schéma de prod a dérivé, ou Flyway refuse de démarrer
ddl-auto=update en production, ou une migration déjà appliquée a été modifiée (somme de contrôle différente). Une modification de schéma = un nouveau fichier V<n>__...sql. → Base de données (JPA)
Hibernate refuse un record comme entité
Une entité doit être une classe avec un constructeur sans argument et des champs modifiables. Les record servent aux DTO. → Base de données (JPA)
Après avoir déclaré ton propre bean, un comportement par défaut a changé
Déclarer un bean (un JsonMapper, par exemple) désactive celui de Boot et ses réglages. Préfère les customizers ou les propriétés. → Configuration
500 avec « parameter name information not available via reflection »
Le code n'a pas été compilé avec -parameters (IDE qui n'utilise pas le pom.xml). Compile avec Maven, ou nomme le paramètre : @RequestParam("name"). → Premier projet
@MockBean ou javax.persistence introuvables
Exemple écrit pour une ancienne version : @MockitoBean depuis Boot 3.4 (et seul disponible en Boot 4), jakarta.* depuis Boot 3. → Tests, Ressources
UnnecessaryStubbingException dans un test
Un when(...) n'est jamais utilisé : Mockito est strict par défaut. Retire-le. → Tests
L'appli ne se connecte pas à PostgreSQL en production
L'URL fournie par Dokploy (postgresql://...) n'est pas une URL JDBC : il faut jdbc:postgresql://hôte:5432/base, avec l'utilisateur et le mot de passe dans des variables séparées. → Déployer
Tout /actuator est visible publiquement
N'expose que health et info : env, beans ou heapdump révèlent la configuration, voire des secrets. → Déployer