Comment utiliser this et super en Java ?

Maintenant que nous avons établi comment l’héritage fonctionne en Java, nous pouvons l’utiliser pour définir et créer des relations complexes entre classes. Dans cette section, nous revoyons les bases pour créer et travailler avec des classes.

Extension d’une Classe

Créons deux fichiers dans le même package, Animal.java et Lion.java.

// Animal.java
public class Animal {
    private int age;
    protected String nom;
    public int getAge() {
        return age;
    }
    public void setAge(int nouvelAge) {
        age = nouvelAge;
    }
}

// Lion.java
public class Lion extends Animal {
    protected void definirProprietes(int age, String n) {
        setAge(age);
        nom = n;
    }
    public void rugir() {
        System.out.print(nom + ", age " + getAge() + ", dit : Roar!");
    }
    public static void main(String[] args) {
        var lion = new Lion();
        lion.definirProprietes(3, "kion");
        lion.rugir();
    }
}

Il y a beaucoup à comprendre ici ! La variable age existe dans la classe parent Animal et n’est pas directement accessible dans la classe enfant Lion. Elle est indirectement accessible via la méthode setAge(). La variable nom est protégée (protected), donc elle est héritée dans la classe Lion et directement accessible. Nous créons l’instance de Lion dans la méthode main() et utilisons definirProprietes() pour définir les variables d’instance. Enfin, nous appelons la méthode rugir(), qui imprime ce qui suit :

kion, age 3, dit : Roar!

Examinons les membres de la classe Lion. La variable d’instance age est marquée comme private et n’est pas directement accessible depuis la sous-classe Lion. Par conséquent, ce qui suit ne compilerait pas :

public class Lion extends Animal {
    public void rugir() {
        System.out.print("Age du lion : " + age); // NE COMPILE PAS 
    }
}

Souvenez-vous que lorsque vous travaillez avec des sous-classes, les membres privés ne sont jamais hérités, et les membres de package ne sont hérités que si les deux classes sont dans le même package. Si vous avez besoin d’un rappel sur les modificateurs d’accès, il peut être utile de relire le Chapitre 5.

Application des Modificateurs d’Accès aux Classes

Comme pour les variables et les méthodes, vous pouvez appliquer des modificateurs d’accès aux classes. Comme vous vous en souvenez peut-être du Chapitre 1, une classe de niveau supérieur est une classe qui n’est pas définie à l’intérieur d’une autre classe. Rappelez-vous également qu’un fichier .java peut contenir au maximum une classe de niveau supérieur.

Bien que vous ne puissiez avoir qu’une seule classe de niveau supérieur, vous pouvez avoir autant de classes (dans n’importe quel ordre) avec un accès de package que vous voulez. En fait, vous n’avez même pas besoin de déclarer une classe public ! L’exemple suivant déclare trois classes, chacune avec un accès de package :

// Ours.java
class Oiseau {}
class Ours {}
class Poisson {}

Essayer de déclarer une classe de niveau supérieur avec protected ou private entraînera une erreur de compilation :

// PoissonClown.java
protected class PoissonClown{} // NE COMPILE PAS

// ChirurgienBleu.java
private class ChirurgienBleu {} // NE COMPILE PAS

Cela signifie-t-il qu’une classe ne peut jamais être déclarée protected ou private ? Pas exactement. Dans le Chapitre 7, nous présentons les types imbriqués et montrons que lorsque vous définissez une classe à l’intérieur d’une autre, elle peut utiliser n’importe quel modificateur d’accès.

Accès à la Référence this

Que se passe-t-il lorsqu’un paramètre de méthode a le même nom qu’une variable d’instance existante ? Jetons un coup d’œil à un exemple. Selon vous, qu’imprime le programme suivant ?

public class Flamant {
    private String couleur = null;
    public void setCouleur(String couleur) {
        couleur = couleur;
    }
    public static void main(String... inutilise) {
        var f = new Flamant();
        f.setCouleur("ROSE");
        System.out.print(f.couleur);
    }
}

Si vous avez répondu null, alors vous avez raison. Java utilise la portée la plus granulaire, donc quand il voit couleur = couleur, il pense que vous assignez la valeur du paramètre de méthode à lui-même (et non à la variable d’instance). L’assignation s’effectue avec succès dans la méthode, mais la valeur de la variable d’instance couleur n’est jamais modifiée et reste null lorsqu’elle est imprimée dans la méthode main().

La solution, lorsque vous avez une variable locale avec le même nom qu’une variable d’instance, est d’utiliser la référence ou le mot-clé this. La référence this fait référence à l’instance courante de la classe et peut être utilisée pour accéder à n’importe quel membre de la classe, y compris les membres hérités. Elle peut être utilisée dans n’importe quelle méthode d’instance, constructeur ou bloc d’initialisation d’instance. Elle ne peut pas être utilisée lorsqu’il n’y a pas d’instance implicite de la classe, comme dans une méthode static ou un bloc d’initialisation static. Nous appliquons this à notre implémentation de méthode précédente comme suit :

public void setCouleur(String couleur) {
    this.couleur = couleur; // Définit la variable d'instance avec le paramètre de méthode
}

Le code corrigé imprimera maintenant ROSE comme prévu. Dans de nombreux cas, la référence this est facultative. Si Java rencontre une variable ou une méthode qu’il ne peut pas trouver, il vérifiera la hiérarchie de classes pour voir si elle est disponible.

Maintenant, examinons quelques exemples qui ne sont pas courants mais que vous pourriez rencontrer.

public class Canard {
    private String couleur;
    private int hauteur;
    private int longueur;

    public void setDonnees(int longueur, int laHauteur) {
        longueur = this.longueur; // À l'envers -- pas bon !
        hauteur = laHauteur; // Bien, car c'est un nom différent
        this.couleur = "blanc"; // Bien, mais la référence this. n'est pas nécessaire
    }

    public static void main(String[] args) {
        Canard b = new Canard();
        b.setDonnees(1,2);
        System.out.print(b.longueur + " " + b.hauteur + " " + b.couleur);
    }
}

Ce code compile et imprime ce qui suit :

0 2 blanc

Ce n’est peut-être pas ce à quoi vous vous attendiez. La ligne 7 est incorrecte. La variable d’instance longueur commence avec une valeur de 0. Ce 0 est assigné au paramètre de méthode longueur. La variable d’instance reste à 0. La ligne 8 est plus simple. Le paramètre laHauteur et la variable d’instance hauteur ont des noms différents. Puisqu’il n’y a pas de collision de noms, this n’est pas nécessaire. Enfin, la ligne 9 montre qu’une assignation de variable est autorisée à utiliser la référence this même lorsqu’il n’y a pas de duplication des noms de variables.

Appel à la Référence super

En Java, une variable ou une méthode peut être définie à la fois dans une classe parent et une classe enfant. Cela signifie que l’instance de l’objet contient en réalité deux copies de la même variable avec le même nom sous-jacent. Lorsque cela se produit, comment référençons-nous la version dans la classe parent au lieu de la classe actuelle ? Jetons un coup d’œil à un exemple.

// Reptile.java
public class Reptile {
    protected int vitesse = 10;
}

// Crocodile.java
public class Crocodile extends Reptile {
    protected int vitesse = 20;
    public int getVitesse() {
        return vitesse;
    }
    public static void main(String[] data) {
        var croc = new Crocodile();
        System.out.println(croc.getVitesse()); // 20
    }
}

L’une des choses les plus importantes à retenir à propos de ce code est qu’une instance de Crocodile stocke deux valeurs séparées pour vitesse : une au niveau Reptile et une au niveau Crocodile. À la ligne 4, Java vérifie d’abord s’il existe une variable locale ou un paramètre de méthode nommé vitesse. Puisqu’il n’y en a pas, il vérifie ensuite this.vitesse; et puisqu’il existe, le programme imprime 20.

Déclarer une variable avec le même nom qu’une variable héritée est appelé masquer une variable et est discuté plus loin dans ce chapitre.

Mais que se passe-t-il si nous voulons que le programme imprime la valeur dans la classe Reptile ? Dans la classe Crocodile, nous pouvons accéder à la valeur parent de vitesse, en utilisant plutôt la référence ou le mot-clé super. La référence super est similaire à la référence this, sauf qu’elle exclut tout membre trouvé dans la classe actuelle. En d’autres termes, le membre doit être accessible via l’héritage.

public int getVitesse() {
    return super.vitesse; // Fait en sorte que le programme imprime maintenant 10
}

Voyons si vous avez compris this et super. Qu’affiche le programme suivant ?

class Insecte {
    protected int nombreDePattes = 4;
    String etiquette = "insecte";
}

public class Scarabee extends Insecte {
    protected int nombreDePattes = 6;
    short age = 3;
    public void afficherDonnees() {
        System.out.println(this.etiquette);
        System.out.println(super.etiquette);
        System.out.println(this.age);
        System.out.println(super.age);
        System.out.println(nombreDePattes);
    }
    public static void main(String []n) {
        new Scarabee().afficherDonnees();
    }
}

C’était une question piège — ce code ne compilerait pas ! Examinons chaque ligne de la méthode afficherDonnees(). Puisque etiquette est définie dans la classe parent, elle est accessible via les références this et super. Pour cette raison, les lignes 10 et 11 compilent et imprimeraient toutes deux insecte si la classe compilait. D’autre part, la variable age est définie uniquement dans la classe actuelle, la rendant accessible via this mais pas super. Pour cette raison, la ligne 12 compile (et imprimerait 3), mais pas la ligne 13. Souvenez-vous, alors que this inclut les membres actuels et hérités, super n’inclut que les membres hérités.

Enfin, qu’imprimerait la ligne 14 si la ligne 13 était commentée ? Même si les deux variables nombreDePattes sont accessibles dans Scarabee, Java vérifie de l’extérieur, en commençant par la portée la plus étroite. Pour cette raison, la valeur de nombreDePattes dans la classe Scarabee est utilisée, et 6 est imprimé. Dans cet exemple, this.nombreDePattes et super.nombreDePattes font référence à différentes variables avec des valeurs distinctes.

Puisque this inclut les membres hérités, vous n’utilisez souvent super que lorsque vous avez un conflit de nommage via l’héritage. Par exemple, vous avez une méthode ou une variable définie dans la classe actuelle qui correspond à une méthode ou une variable dans une classe parent. Cela survient couramment dans la substitution de méthode et le masquage de variable, qui sont discutés plus loin dans ce chapitre.

Ouf, c’était beaucoup ! L’utilisation de this et super peut prendre un peu de temps à maîtriser. Puisque nous les utilisons souvent dans les sections à venir, assurez-vous de bien comprendre le dernier exemple avant d’avancer.