Skip to main content

Comment prévenir les NullPointerExceptions en Java

Écrit par
Headshot of Lucien Chemaly

Lucien Chemaly

feature java nullpointerexception

21 septembre 2023

0 minutes de lecture

En tant que développeur Java expérimenté, vous connaissez probablement les subtilités des exceptions Java. Ces événements signalent des conditions anormales pendant l’exécution d’un programme et, lorsqu’ils ne sont pas correctement gérés, peuvent perturber le fonctionnement normal de votre application et provoquer des plantages ou des comportements inattendus.

Parmi les différentes exceptions Java, vous devez prêter une attention particulière aux NullPointerExceptions. Elles sont particulièrement redoutées, car elles sont une source fréquente d’erreurs d’exécution et peuvent nuire considérablement à la stabilité et à la fiabilité de votre application.

Dans cet article, découvrez tout ce qu’il faut savoir sur les NullPointerExceptions en Java : leurs causes, leurs conséquences et, surtout, les techniques pour les prévenir.

Pourquoi éviter les NullPointerExceptions

Une NullPointerException est une anomalie d’exécution en Java qui se produit lorsqu’une personne tente d’accéder à une référence d’objet contenant la valeur null ou de la modifier. Cette référence null indique que l’objet concerné est absent ou n’a pas été instancié. Lorsque la machine virtuelle Java (JVM) rencontre une référence null lors de la manipulation d’un objet, elle génère une NullPointerException. Comprendre les causes de ces exceptions et y remédier permet de développer des applications plus solides et robustes.

Les NullPointerExceptions peuvent survenir dans différentes situations, notamment les suivantes :

  • Lorsqu’on accède à une variable d’instance ou qu’on appelle une méthode sur un objet null.

  • Lorsqu’on accède à un élément d’un tableau ou d’une collection à l’aide d’une référence null.

  • Lorsqu’on convertit automatiquement une référence null issue d’un objet wrapper vers le type primitif correspondant.

  • Lorsqu’on passe une référence null en argument à une méthode qui ne la prévoit pas.

Ces situations découlent généralement d’erreurs de programmation : un objet qui n’a pas été initialisé, des valeurs null mal gérées ou l’absence de vérification des valeurs null avant d’effectuer des opérations sur les objets.

Malheureusement, les NullPointerExceptions peuvent avoir de nombreuses conséquences néfastes dans un environnement de développement, notamment :

  • Plantage de l’application : lorsqu’une NullPointerException se produit, l’application s’arrête souvent brutalement, ce qui peut mécontenter les utilisateurs et entraîner la perte de données non enregistrées.

  • Comportement imprévisible : les NullPointerExceptions peuvent entraîner un comportement imprévisible de l’application, ce qui complique la reproduction et le diagnostic des problèmes.

  • Débogage difficile : trouver la cause profonde d’une NullPointerException peut prendre du temps et s’avérer complexe, surtout dans des bases de code volumineuses et complexes.

  • Vulnérabilités de sécurité : dans certains cas, les NullPointerExceptions peuvent révéler des vulnérabilités de sécurité, car des utilisateurs malveillants peuvent les exploiter pour contourner certains contrôles ou accéder sans autorisation à des données sensibles.

  • Baisse de la productivité des développeurs : les NullPointerException fréquentes peuvent nuire à la productivité, car les développeurs doivent consacrer davantage de temps à corriger et à tester ces problèmes.

Heureusement, en comprenant les causes et les conséquences des NullPointerExceptions, ainsi que les techniques pour les prévenir, vous pouvez en réduire la fréquence et créer des applications Java plus stables, plus efficaces et plus sécurisées.

Comment prévenir les NullPointerExceptions en Java

Pour prévenir les NullPointerExceptions en Java, les développeurs peuvent appliquer différentes techniques et bonnes pratiques, notamment utiliser l’opérateur ternaire et la bibliothèque Apache Commons StringUtils.

Utiliser l’opérateur ternaire

Au lieu d’utiliser une instruction if-else, vous pouvez utiliser l’opérateur ternaire pour vérifier les valeurs null et fournir une valeur par défaut, réduisant ainsi le risque de NullPointerExceptions. L’opérateur ternaire permet d’écrire des conditions concises et lisibles qui gèrent les valeurs null et fournissent des valeurs par défaut en Java :

String input = null;
String result = (input != null) ? input : "default";

Ce code attribue la valeur default à la variable result si la variable input est null. Si elle n’est pas null, il attribue la valeur de input à result. Il utilise l’opérateur ternaire pour vérifier les valeurs null et fournir une valeur par défaut.

Utiliser StringUtils d’Apache Commons

La bibliothèque Apache Commons StringUtils fournit des méthodes utilitaires pour gérer en toute sécurité les valeurs null dans les chaînes de caractères et réduire le risque de NullPointerException. Elle permet d’éviter les plantages inattendus de l’application et les vulnérabilités de sécurité qui peuvent découler des valeurs null :

import org.apache.commons.lang3.StringUtils;
String input = null;
boolean isBlank = StringUtils.isBlank(input); // Returns true if null, empty, or whitespace

Ce code utilise la classe StringUtils du package org.apache.commons.lang3 pour vérifier si une chaîne est null, vide ou composée uniquement d’espaces, puis affecte le résultat à la variable booléenne isBlank.

Vous pouvez réduire les risques de sécurité liés aux bibliothèques tierces comme Apache Commons StringUtils en utilisant Snyk pour détecter les vulnérabilités Java. De plus, il est essentiel d’utiliser la dernière version des bibliothèques tierces pour garantir la sécurité et la stabilité. Pour Apache Commons StringUtils, consultez la dernière version sur son site officiel et mettez à jour la dépendance correspondante dans le fichier pom.xml de votre projet.

Préférer les types primitifs aux objets

Dans la mesure du possible, utilisez des types primitifs plutôt que leurs classes wrapper correspondantes pour éviter les valeurs potentielles null, qui ne peuvent pas être null. Malheureusement, les classes wrapper correspondantes peuvent être null, ce qui peut entraîner une NullPointerException si elles ne sont pas correctement gérées :

int primitiveInt = 0; // A primitive int cannot be null
Integer objectInt = null; // An Integer object can be null, leading to a NullPointerException

Ce code déclare une variable entière primitiveInt de valeur 0, qui est de type primitif (et ne peut donc pas être null), ainsi qu’une variable objectInt de la classe Integer, qui peut être null et entraîner une NullPointerException si elle n’est pas correctement gérée.

Éviter de renvoyer null dans les méthodes

Au lieu de renvoyer des valeurs null depuis vos méthodes, renvoyez un objet vide :

public List<String> getStrings() {
    return Collections.emptyList(); // Return an empty list instead of null
}

Ce code définit une méthode getStrings() qui renvoie une liste vide de chaînes à l’aide de la méthode Collections.emptyList(), plutôt qu’une valeur null. Cette approche est préférable, car elle évite les NullPointerExceptions potentielles et favorise une gestion cohérente des valeurs de retour, ce qui facilite la lecture et la maintenance du code. Pour éviter de renvoyer null depuis les méthodes Java, une approche intéressante consiste à utiliser Optional. Nous approfondirons ce concept plus loin dans cet article.

Utiliser .equals() en toute sécurité

Appelez .equals() sur un objet dont vous savez qu’il n’est pas null, ou utilisez Objects.equals() pour éviter les NullPointerExceptions lors de la comparaison d’objets :

String str1 = "hello";
String str2 = null;
// Safe usage of .equals()
boolean isEqual = str1.equals(str2); // Returns false, no NullPointerException
// Using Objects.equals()
boolean isEqualObjects = Objects.equals(str1, str2); // Returns false, no NullPointerException

Ce code déclare deux variables de chaîne : str1 et str2. Il appelle la méthode .equals() sur l’objet non null str1 et utilise Objects.equals() pour comparer str1 et str2 en toute sécurité. Cette méthode vérifie les valeurs null afin d’éviter les NullPointerExceptions potentielles lors de la comparaison d’objets.

Éviter de passer null en argument à une méthode

Utilisez des annotations comme @NotNull pour indiquer qu’une méthode ne doit pas accepter d’arguments null, ce qui permet d’effectuer des vérifications à la compilation :

public void doSomething(@NotNull String input) {
    // Method implementation
}

Utiliser l’annotation @NonNull de Lombok

Lombok est une bibliothèque très utilisée qui simplifie le code Java. L’annotation @NonNull permet d’imposer que les paramètres ne soient pas null, en générant les vérifications null appropriées :

import lombok.NonNull;
public void doSomething(@NonNull String input) {
    // Method implementation
}

Ce code importe l’annotation @NonNull de la bibliothèque Lombok et l’utilise pour indiquer que le paramètre input de la méthode doSomething ne doit pas être null. Cela permet de prévenir les NullPointerExceptions potentielles en vérifiant les valeurs null à la compilation.

Utiliser le pattern null

Le pattern null est un patron de conception qui fournit un objet par défaut à la place des valeurs null, réduisant ainsi le risque de NullPointerExceptions :

public interface Animal {
    void makeSound();
}
public class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Woof!");
    }
}
public class NullAnimal implements Animal {
    @Override
    public void makeSound() {
        // Do nothing
    }
}
public class AnimalFactory {
    public Animal getAnimal(String type) {
        if ("Dog".equalsIgnoreCase(type)) {
            return new Dog();
        }
        return new NullAnimal();
    }
}

Ce code définit une interface Animal et deux classes qui l’implémentent : Dog et NullAnimal. La classe AnimalFactory contient une méthode, getAnimal(), qui prend un argument String et renvoie une instance de Dog ou de NullAnimal, selon l’argument.

Cette approche est utile pour gérer les valeurs null, car elle permet de créer un objet NullAnimal par défaut lorsqu’un argument invalide ou null est transmis à getAnimal(). Cela peut prévenir les NullPointerExceptions potentielles et garantir un comportement cohérent du programme.

Utiliser l’API Stream de Java pour gérer les valeurs null dans les collections

L’API Stream de Java fournit des méthodes permettant de gérer en toute sécurité les valeurs null dans les collections :

List<String> strings = Arrays.asList("Hello", null, "World");
strings = strings.stream().filter(Objects::nonNull).collect(Collectors.toList());

Ce code utilise un flux pour filtrer les éléments null d’une liste de chaînes et ainsi prévenir les NullPointerExceptions potentielles qui peuvent survenir lorsqu’on tente d’accéder à des valeurs null dans la liste.

Cette approche simplifie la vérification des valeurs null et aide à prévenir les plantages inattendus de l’application et les vulnérabilités de sécurité qui peuvent découler des valeurs null dans les collections.

Valider les arguments d’une méthode à l’aide d’une méthode utilitaire dédiée

Créez une méthode utilitaire pour valider les arguments et lever une exception personnalisée lorsque des valeurs null sont rencontrées. Cela peut aider à imposer que les paramètres des méthodes ne soient pas null :

public static <T> T checkNotNull(T reference, String errorMessage) {
    if (reference == null) {
        throw new IllegalArgumentException(errorMessage);
    }
    return reference;
}
public void doSomething(String input) {
    input = checkNotNull(input, "Input should not be null");
    // Method implementation
}

Ce code définit une méthode checkNotNull qui lève une IllegalArgumentException si le paramètre reference est null, ainsi qu’une méthode doSomething qui utilise checkNotNull pour s’assurer que le paramètre input n’est pas null avant de poursuivre l’implémentation de la méthode.

Cette approche est importante pour prévenir les NullPointerExceptions potentielles : elle vérifie les valeurs null à l’exécution et lève une exception si des valeurs null sont rencontrées.

Utiliser des méthodes par défaut dans les interfaces

Lorsque vous utilisez des interfaces, fournissez des méthodes par défaut pour gérer les objets potentiellement null et éviter ainsi les NullPointerExceptions :

public interface Processor {
    default void process(String input) {
        if (input == null) {
            handleNullInput();
        } else {
            doProcess(input);
        }
    }

    void doProcess(String input);

    default void handleNullInput() {
        // Handle null input or throw a custom exception
    }
}
public class MyProcessor implements Processor {
	@Override
	public void doProcess(String input) {
		System.out.println("Processing input: " + input);
	}
	// optionally override the handleNullInput method
	@Override
	public void handleNullInput() {
		System.out.println("Null input encountered.");
	}
}

Ce code définit une interface Processor dotée d’une méthode par défaut process(), qui vérifie si les valeurs d’entrée sont null et appelle soit doProcess(), soit handleNullInput(). La classe PMyProcessor implémente l’interface Processor et redéfinit les méthodes doProcess() et handleNullInput() pour définir un comportement personnalisé.

Cette approche aide à prévenir les NullPointerExceptions potentielles en fournissant une méthode cohérente pour gérer les valeurs d’entrée null et en permettant de mettre en place un comportement personnalisé ou une gestion des exceptions lorsque de telles valeurs null sont rencontrées.

Utiliser le pattern Builder

Le pattern Builder peut vous aider à créer des objets complexes avec des paramètres facultatifs (comme la couleur, la taille, la forme ou d’autres caractéristiques personnalisables), tout en garantissant que les paramètres obligatoires sont toujours définis et non null :

public class Person {
    private final String firstName;
    private final String lastName;

    private Person(Builder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
    }

    public static class Builder {
        private final String firstName;
        private String lastName = "";

        public Builder(String firstName) {
            this.firstName = Objects.requireNonNull(firstName, "First name must not be null");
        }

        public Builder lastName(String lastName) {
            this.lastName = Objects.requireNonNull(lastName, "Last name must not be null");
            return this;
        }

        public Person build() {
            return new Person(this);
        }
    }
}

Ce code définit une classe Person dotée d’un constructeur privé qui n’accepte qu’un objet Builder, lequel contient deux champs : firstName et lastName. La classe Builder garantit que le champ firstName n’est pas null, en le vérifiant lors de la construction à l’aide de Objects.requireNonNull(). Le champ lastName peut, lui, être null, mais doit être vérifié et défini à l’aide de la méthode lastName().

Cette approche aide à prévenir les NullPointerExceptions potentielles en garantissant que tous les champs obligatoires sont initialisés et que les valeurs null sont vérifiées lors de la construction. Elle favorise ainsi la cohérence et la fiabilité du programme.

Utiliser le verrouillage à double vérification avec le pattern singleton

Lors de la création de singletons, le verrouillage à double vérification peut contribuer à garantir que l’instance singleton n’est jamais null :

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Ce code définit une classe Singleton qui garantit qu’une seule instance de la classe est créée à l’exécution et utilise le verrouillage à double vérification pour éviter les éventuelles conditions de concurrence et les NullPointerExceptions.

Cette approche est importante pour éviter les NullPointerExceptions, car elle garantit que l’instance de la classe Singleton n’est créée qu’une seule fois et que les appels ultérieurs à la méthode getInstance() renvoient la même instance. Cela permet d’éviter que des valeurs null soient accidentellement affectées à la variable instance et provoquent d’éventuelles NullPointerExceptions.

Utiliser la classe Optional

La classe Optional est un outil puissant pour éviter les NullPointerExceptions en gérant explicitement les valeurs potentiellement null :

import java.util.Optional;
public class OptionalExample {
    private String someString;
    public OptionalExample(String someString) {
        this.someString = someString;
    }
    public Optional<String> getString() {
        return Optional.ofNullable(someString);
    }
}
//Usage
OptionalExample example1 = new OptionalExample("Hello, World!");
Optional<String> optString1 = example1.getString();
String value1 = optString1.orElse("default");
System.out.println(value1); // Output: Hello, World!
OptionalExample example2 = new OptionalExample(null);
Optional<String> optString2 = example2.getString();
String value2 = optString2.orElse("default");
System.out.println(value2); // Output: default

Ce code utilise la classe Optional pour gérer les valeurs null, en indiquant explicitement qu’une valeur peut être absente et en utilisant orElse() pour fournir une valeur par défaut, ce qui évite d’éventuelles NullPointerExceptions. Il est important de ne pas appeler directement get() sur Optional. Utilisez plutôt des méthodes comme orElse() pour fournir une valeur par défaut lorsque l’objet Optional est vide, évitant ainsi une NoSuchElementException.

Cette approche indique clairement qu’une valeur peut être présente ou non et impose également de gérer correctement ces cas, ce qui contribue à éviter des erreurs potentielles telles qu’une NullPointerException ou une NoSuchElementException.

Tester souvent et tôt

Veillez à écrire des tests unitaires complets afin de détecter les NullPointerExceptions pendant le développement, avant qu’elles ne posent problème en production :

@Test
public void testGetStrings() {
    MyClass obj = new MyClass();
    List<String> result = obj.getStrings();
    assertNotNull(result); // Ensure the result is not null
}

Ce code définit un test JUnit pour la méthode getStrings() de la classe MyClass. Il crée ensuite une instance de MyClass, appelle la méthode getStrings() et vérifie que le résultat n’est pas null à l’aide de la méthode assertNotNull().

Cette approche permet d’éviter les éventuelles NullPointerExceptions en vérifiant que le résultat de la méthode getStrings() est not null avant d’effectuer d’autres opérations. En s’assurant que le résultat est not null, le test peut aider à détecter tout problème potentiel lié aux valeurs null et à prévenir les plantages inattendus de l’application ou les vulnérabilités de sécurité.

Conclusion sur NullPoint

Éviter les NullPointerExceptions dans les applications Java est essentiel pour créer des logiciels robustes, fiables et sécurisés. Depuis Java 14, des améliorations facilitent la gestion des NullPointerExceptions, ainsi que leur prévention et leur diagnostic. Il peut donc être avantageux de passer à une version plus récente de Java pour faciliter le débogage.

En combinant différentes techniques, comme l’utilisation de l’opérateur ternaire, la validation des arguments de méthode et l’adoption de modèles de conception tels que Builder et le pattern objet nul, vous pouvez réduire efficacement le risque de rencontrer des NullPointerExceptions. Ces pratiques contribuent non seulement à améliorer la stabilité de l’application, mais aussi l’expérience globale de développement.

Snyk est un outil essentiel pour les développeurs qui souhaitent créer des applications sécurisées et fiables. Avec Snyk, vous avez accès à un éventail d’outils et de services conçus pour détecter et corriger les vulnérabilités de sécurité dans votre code. Snyk s’intègre également aux environnements et plateformes de développement les plus courants, ce qui facilite l’intégration de la sécurité à votre processus de développement.

Pour faire passer votre expérience de développement Java au niveau supérieur et contribuer à la sécurité et à la stabilité de l’écosystème logiciel, pensez à utiliser certains des outils puissants de Snyk, notamment les plugins IDE, le CLI et les cours pratiques gratuits sur la sécurité Java. Ces outils vous aident à détecter et à signaler les vulnérabilités de sécurité, y compris les NullPointerExceptions, afin de garantir la sécurité et la fiabilité de vos applications.

Détectez et corrigez les vulnérabilités Java

Sécurisez vos applications grâce à l’analyse des vulnérabilités de Snyk et à ses recommandations de correction. 

Aucune carte bancaire requise.

Ou inscrivez-vous avec Azure AD Docker ID Bitbucket

En utilisant Snyk, vous acceptez de respecter nos politiques, notamment nos Conditions d’utilisation et notre Politique de confidentialité.


Publié dans:

Lire la suite

blog feature snyk container custom base recommendations
Blog

Découvrez Evo Agentic AppSec : une sécurité au rythme du développement de l’IA

Le développement piloté par l’IA dépasse les workflows de sécurité traditionnels. L’AppSec agentique de Snyk s’appuie sur des agents de sécurité pour détecter, corriger et vérifier les vulnérabilités en continu, tandis que les équipes définissent les exigences.

feature insights context
Blog

Les attaques autonomes sont déjà là. La défense doit suivre leur rythme.

Les attaquants autonomes réduisent la fenêtre de défense. Découvrez comment la découverte, la correction, la validation et la prévention continues peuvent aider les équipes de sécurité à suivre le rythme.

illustration hero ai
Blog

L’ouragan de l’IA est là

L’IA accélère à la fois la création de logiciels et les cyberattaques. Les dirigeants doivent sécuriser les agents et le code dès leur conception, appliquer des contrôles à l’exécution et valider les défenses de manière indépendante.