Comment utiliser le mot-clé static en Java ?

Lorsque le mot-clé static est appliqué à une variable, une méthode ou une classe, cela signifie qu’il appartient à la classe plutôt qu’à une instance spécifique de la classe. Dans cette section, vous verrez que le mot-clé static peut également être appliqué aux instructions d’importation.

Conception des méthodes et variables static

À l’exception de la méthode main(), nous avons examiné jusqu’à présent des méthodes d’instance. Les méthodes et variables déclarées static ne nécessitent pas d’instance de la classe. Elles sont partagées entre tous les utilisateurs de la classe. Par exemple, examinons la classe Pingouin suivante :

public class Pingouin {
    String nom;
    static String nomDuPingouinLePlusGrand;
}

Dans cette classe, chaque instance de Pingouin a son propre nom comme “Lili” ou “Guillaume”, mais un seul Pingouin parmi toutes les instances est le plus grand. Vous pouvez considérer une variable static comme un membre de l’objet de classe unique qui existe indépendamment de toutes les instances de cette classe. Considérez l’exemple suivant :

public static void main(String[] inutilisé) {
    var p1 = new Pingouin();
    p1.nom = "Lili";
    p1.nomDuPingouinLePlusGrand = "Lili";
    var p2 = new Pingouin();
    p2.nom = "Guillaume";
    p2.nomDuPingouinLePlusGrand = "Guillaume";
    System.out.println(p1.nom);                   // Lili
    System.out.println(p1.nomDuPingouinLePlusGrand); // Guillaume
    System.out.println(p2.nom);                   // Guillaume
    System.out.println(p2.nomDuPingouinLePlusGrand); // Guillaume
}

Nous voyons que chaque instance de pingouin est mise à jour avec son propre nom unique. Le champ nomDuPingouinLePlusGrand est static et donc partagé, de sorte que chaque fois qu’il est mis à jour, cela affecte toutes les instances de la classe.

Vous avez vu une méthode static . La méthode main() est une méthode static. Cela signifie que vous pouvez l’appeler en utilisant le nom de la classe :

public class Koala {
    public static int compteur = 0; // variable static
    public static void main(String[] args) { // méthode static
        System.out.print(compteur);
    }
}

Ici, la JVM appelle essentiellement Koala.main() pour démarrer le programme. Vous pouvez faire de même. Nous pouvons avoir un TestKoala qui ne fait rien d’autre qu’appeler la méthode main() :

public class TestKoala {
    public static void main(String[] args) {
        Koala.main(new String[0]);      // appel de méthode static
    }
}

C’est une façon assez compliquée d’imprimer 0, n’est-ce pas ? Lorsque nous exécutons TestKoala, il fait un appel à la méthode main() de Koala, qui imprime la valeur de compteur. Le but de tous ces exemples est de montrer que main() peut être appelé comme n’importe quelle autre méthode static.

En plus des méthodes main(), les méthodes static ont deux objectifs principaux :

  • Pour les méthodes utilitaires ou auxiliaires qui ne nécessitent pas d’état d’objet. Puisqu’il n’y a pas besoin d’accéder aux variables d’instance, avoir des méthodes static élimine le besoin pour l’appelant d’instancier un objet juste pour appeler la méthode.
  • Pour un état qui est partagé par toutes les instances d’une classe, comme un compteur. Toutes les instances doivent partager le même état. Les méthodes qui utilisent simplement cet état devraient également être static.

Dans les sections suivantes, nous examinerons quelques exemples couvrant d’autres concepts static.

Accès à une variable ou méthode static

Habituellement, l’accès à un membre static est facile.

public class Serpent {
    public static long sifflement = 2;
}

Il suffit de mettre le nom de la classe avant la méthode ou la variable, et c’est fait. Voici un exemple :

System.out.println(Serpent.sifflement);

Simple et facile. Il y a une règle qui est plus délicate. Vous pouvez utiliser une instance de l’objet pour appeler une méthode static. Le compilateur vérifie le type de la référence et l’utilise à la place de l’objet — ce qui est rusé de la part de Java. Ce code est parfaitement légal :

Serpent s = new Serpent();
System.out.println(s.sifflement); // s est un Serpent
s = null;
System.out.println(s.sifflement); // s est toujours un Serpent

Croyez-le ou non, ce code affiche 2 deux fois. La ligne 6 voit que s est un Serpent et sifflement est une variable static, donc elle lit cette variable static. La ligne 8 fait la même chose. Java ne se soucie pas que s soit null. Puisque nous recherchons une variable static, cela n’a pas d’importance.

N’oubliez pas de regarder le type de référence pour une variable lorsque vous voyez une méthode ou une variable static. Java ne jettera pas de NullPointerException car la variable est null. Ne vous y trompez pas !

Encore une fois, parce que c’est vraiment important : que produira le code suivant ?

Serpent.sifflement = 4;
Serpent serpent1 = new Serpent();
Serpent serpent2 = new Serpent();
serpent1.sifflement = 6;
serpent2.sifflement = 5;
System.out.println(Serpent.sifflement);

Nous espérons que vous avez répondu 5. Il n’y a qu’une seule variable sifflement puisqu’elle est static. Elle est définie à 4, puis à 6, et finit par être 5. Toutes les variables Serpent ne sont que des distractions.

Appartenance à la classe vs instance

Il y a une autre façon de tromper les développeurs concernant les membres static et d’instance. Un membre static ne peut pas appeler un membre d’instance sans référencer une instance de la classe. Cela ne devrait pas être une surprise puisque static ne nécessite pas d’instances de la classe pour exister.

Voici une erreur courante que les programmeurs débutants commettent :

public class RaieManta {
    private String nom = "Sammy";
    public static void premier() { }
    public static void deuxième() { }
    public void troisième() { System.out.print(nom); }
    public static void main(String args[]) {
        premier();
        deuxième();
        troisième(); // NE COMPILE PAS
    }
}

Le compilateur vous donnera une erreur concernant une référence static à une méthode d’instance. Si nous corrigeons cela en ajoutant static à troisième(), nous créons un nouveau problème. Pouvez-vous deviner lequel ?

public static void troisième() { System.out.print(nom); } // NE COMPILE PAS

Tout ce que cela fait, c’est déplacer le problème. Maintenant, troisième() fait référence à une variable d’instance nom. Il y a deux façons de résoudre ce problème. La première consiste à ajouter static à la variable nom également.

public class RaieManta {
    private static String nom = "Sammy";
    // ...
    public static void troisième() { System.out.print(nom); }
    // ...
}

La deuxième solution aurait été d’appeler troisième() comme une méthode d’instance et de ne pas utiliser static pour la méthode ou la variable.

public class RaieManta {
    private String nom = "Sammy";
    // ...
    public void troisième() { System.out.print(nom); }
    public static void main(String args[]) {
        // ...
        var raie = new RaieManta();
        raie.troisième();
    }
}

Une méthode static ou une méthode d’instance peut appeler une méthode static car les méthodes static ne nécessitent pas d’objet à utiliser. Seule une méthode d’instance peut appeler une autre méthode d’instance sur la même classe sans utiliser une variable de référence, car les méthodes d’instance nécessitent un objet. Une logique similaire s’applique aux variables d’instance et static.

Supposons que nous ayons une classe Girafe :

public class Girafe {
    public void manger(Girafe g) {}
    public void boire() {};
    public static void toutesLesGirafesRentrentÀLaMaison(Girafe g) {}
    public static void toutesLesGirafesSortent() {}
}

Assurez-vous de comprendre le tableau 5.5 avant de continuer.

MéthodeAppelantLégal?
toutesLesGirafesRentrentÀLaMaison()toutesLesGirafesSortent()Oui
toutesLesGirafesRentrentÀLaMaison()boire()Non
toutesLesGirafesRentrentÀLaMaison()g.manger()Oui
manger()toutesLesGirafesSortent()Oui
manger()boire()Oui
manger()g.manger()Oui

Essayons un exemple de plus pour que vous ayez plus de pratique à reconnaître ce scénario. Comprenez-vous pourquoi les lignes suivantes ne compilent pas ?

public class Gorille {
    public static int compteur;
    public static void ajouterGorille() { compteur++; }
    public void bebeGorille() { compteur++; }
    public void annoncerBebes() {
        ajouterGorille();
        bebeGorille();
    }
    public static void annoncerBebesATous() {
        ajouterGorille();
        bebeGorille(); // NE COMPILE PAS
    }
    public int total;
    public static double moyenne 
        = total / compteur; // NE COMPILE PAS
}

Les lignes 3 et 4 sont correctes car les méthodes static et d’instance peuvent faire référence à une variable static. Les lignes 5-8 sont correctes car une méthode d’instance peut appeler une méthode static. La ligne 11 ne compile pas car une méthode static ne peut pas appeler une méthode d’instance. De même, la ligne 15 ne compile pas car une variable static essaie d’utiliser une variable d’instance.

Une utilisation courante des variables static est le comptage du nombre d’instances :

public class Compteur {
    private static int compteur;
    public Compteur() { compteur++; }
    public static void main(String[] args) {
        Compteur c1 = new Compteur();
        Compteur c2 = new Compteur();
        Compteur c3 = new Compteur();
        System.out.println(compteur); // 3
    }
}

Chaque fois que le constructeur est appelé, il incrémente compteur de un. Cet exemple s’appuie sur le fait que les variables static (et d’instance) sont automatiquement initialisées à la valeur par défaut pour ce type, qui est 0 pour int.

Notez également que nous n’avons pas écrit Compteur.compteur. Nous aurions pu. Ce n’est pas nécessaire car nous sommes déjà dans cette classe, donc le compilateur peut le déduire.

Modificateurs de variables static

En se référant au tableau 5.3, les variables static peuvent être déclarées avec les mêmes modificateurs que les variables d’instance, tels que final, transient et volatile. Alors que certaines variables static sont destinées à changer au cours de l’exécution du programme, comme notre exemple compteur, d’autres ne sont jamais censées changer. Ce type de variable static est connu sous le nom de constante. Il utilise le modificateur final pour garantir que la variable ne change jamais.

Les constantes utilisent le modificateur static final et une convention de nommage différente des autres variables. Elles utilisent toutes les lettres majuscules avec des traits de soulignement entre les “mots”. Voici un exemple :

public class EnclosDuZoo {
    private static final int NOMBRE_DE_SEAUX = 45;
    public static void main(String[] args) {
        NOMBRE_DE_SEAUX = 5; // NE COMPILE PAS
    }
}

Le compilateur s’assurera que vous n’essayez pas accidentellement de mettre à jour une variable final. Cela peut devenir intéressant. Pensez-vous que le code suivant compile ?

import java.util.*;
public class GestionnaireInventaireZoo {
    private static final String[] friandises = new String[10];
    public static void main(String[] args) {
        friandises[0] = "pop-corn";
    }
}

Il compile effectivement puisque friandises est une variable de référence. Nous sommes autorisés à modifier le contenu de l’objet ou du tableau référencé. Tout ce que le compilateur peut faire est de vérifier que nous n’essayons pas de réassigner friandises pour pointer vers un objet différent.

Les règles pour les variables static final sont similaires aux variables final d’instance, sauf qu’elles n’utilisent pas de constructeurs static (il n’y a pas de telle chose !) et utilisent des initialiseurs static à la place des initialiseurs d’instance.

public class Panda {
    final static String nom = "Ronda";
    static final int bambou;
    static final double taille; // NE COMPILE PAS
    static { bambou = 5;}
}

La variable nom se voit attribuer une valeur lorsqu’elle est déclarée, tandis que la variable bambou se voit attribuer une valeur dans un initialiseur static. La variable taille ne se voit pas attribuer de valeur où que ce soit dans la définition de la classe, donc cette ligne ne compile pas. N’oubliez pas que les variables final doivent être initialisées avec une valeur. Ensuite, nous couvrons les initialiseurs static.

Initialiseurs static

Au Chapitre 1, nous avons couvert les initialiseurs d’instance qui ressemblaient à des méthodes sans nom — juste du code entre accolades. Les initialiseurs static sont similaires. Ils ajoutent le mot-clé static pour spécifier qu’ils doivent être exécutés lorsque la classe est chargée pour la première fois. Voici un exemple :

private static final int NOMBRE_DE_SECONDES_PAR_MINUTE;
private static final int NOMBRE_DE_MINUTES_PAR_HEURE;
private static final int NOMBRE_DE_SECONDES_PAR_HEURE;
static {
    NOMBRE_DE_SECONDES_PAR_MINUTE = 60;
    NOMBRE_DE_MINUTES_PAR_HEURE = 60;
}
static {
    NOMBRE_DE_SECONDES_PAR_HEURE
        = NOMBRE_DE_SECONDES_PAR_MINUTE * NOMBRE_DE_MINUTES_PAR_HEURE;
}

Tous les initialiseurs static s’exécutent lorsque la classe est utilisée pour la première fois, dans l’ordre où ils sont définis. Les instructions qu’ils contiennent s’exécutent et attribuent des valeurs aux variables static selon les besoins. Il y a quelque chose d’intéressant à propos de cet exemple. Nous venons de dire que les variables final ne sont pas autorisées à être réassignées. La clé ici est que l’initialiseur static est la première attribution. Et comme elle se produit au début, c’est acceptable.

Essayons un autre exemple pour vous assurer que vous comprenez la distinction :

private static int un;
private static final int deux;
private static final int trois = 3;
private static final int quatre; // NE COMPILE PAS
static {
    un = 1;
    deux = 2;
    trois = 3; // NE COMPILE PAS
    deux = 4; // NE COMPILE PAS
}

La ligne 14 déclare une variable static qui n’est pas final. Elle peut être assignée autant de fois que nous le souhaitons. La ligne 15 déclare une variable final sans l’initialiser. Cela signifie que nous pouvons l’initialiser exactement une fois dans un bloc static. La ligne 22 ne compile pas car c’est la deuxième tentative. La ligne 16 déclare une variable final et l’initialise en même temps. Nous ne sommes pas autorisés à l’assigner à nouveau, donc la ligne 21 ne compile pas. La ligne 17 déclare une variable final qui n’est jamais initialisée. Le compilateur donne une erreur car il sait que les blocs static sont le seul endroit où la variable pourrait être initialisée. Puisque le programmeur a oublié, c’est clairement une erreur.

Essayez d’éviter les initialiseurs static et d’instance

L’utilisation d’initialiseurs static et d’instance peut rendre votre code beaucoup plus difficile à lire. Tout ce qui pourrait être fait dans un initialiseur d’instance pourrait être fait dans un constructeur à la place. Beaucoup de gens trouvent l’approche du constructeur plus facile à lire.

Il y a un cas courant d’utilisation d’un initialiseur static : lorsque vous devez initialiser un champ static et que le code pour ce faire nécessite plus d’une ligne. Cela se produit souvent lorsque vous voulez initialiser une collection comme un ArrayList ou une HashMap. Lorsque vous avez besoin d’utiliser un initialiseur static, placez toute l’initialisation static dans le même bloc. De cette façon, l’ordre est évident.

Imports static

Au Chapitre 1, vous avez vu que vous pouvez importer une classe spécifique ou toutes les classes d’un package. Si vous n’avez jamais vu ArrayList ou List auparavant, ne vous inquiétez pas, car nous les couvrons en détail au Chapitre 9, “Collections et Generics”.

import java.util.ArrayList;
import java.util.*;

Nous pourrions utiliser cette technique pour importer deux classes :

import java.util.List;
import java.util.Arrays;
public class Imports {
    public static void main(String[] args) {
        List list = Arrays.asList("un", "deux");
    }
}

Les imports sont pratiques car vous n’avez pas besoin de spécifier d’où vient chaque classe chaque fois que vous l’utilisez. Il existe un autre type d’import appelé import static. Les imports réguliers sont pour importer des classes, tandis que les imports static sont pour importer des membres static de classes comme des variables et des méthodes.

Tout comme les imports réguliers, vous pouvez utiliser un joker ou importer un membre spécifique. L’idée est que vous ne devriez pas avoir à spécifier d’où vient chaque méthode ou variable static chaque fois que vous l’utilisez. Un exemple de cas où les imports static brillent est lorsque vous faites référence à beaucoup de constantes dans une autre classe.

Nous pouvons réécrire notre exemple précédent pour utiliser un import static. Cela donne ce qui suit :

import java.util.List;
import static java.util.Arrays.asList; // import static
public class StationnementZoo {
    public static void main(String[] args) {
        List list = asList("un", "deux"); // Pas de préfixe Arrays.
    }
}

Dans cet exemple, nous importons spécifiquement la méthode asList. Cela signifie que chaque fois que nous faisons référence à asList dans la classe, cela appelle Arrays.asList().

Un cas intéressant est ce qui se passerait si nous créions une méthode asList dans notre classe StationnementZoo. Java lui donnerait la préférence sur celle importée, et la méthode que nous avons codée serait utilisée.

Dans un grand programme, les imports static peuvent être surutilisés. Lors de l’importation depuis trop d’endroits, il peut être difficile de se souvenir d’où vient chaque membre static. Utilisez-les avec parcimonie !