Héritage et Polymorphisme


Surcharge de Méthodes/Constructeur

Surcharge de Méthodes

Il est possible de définir plusieurs méthodes qui portent le même nom. Dans ce cas on dit qu’il y a une surcharge de méthode. Les méthodes diffère malgré tout

  • Sur le nombre d’arguments
  • Sur le type (et/ou l’ordre) des arguments

Si c’est le cas elles peuvent avoir des types de retour différents mais ça n’a pas vraiment de sens de faire ça.

Surcharge de Méthodes

Lorsqu’il y a une surcharge de méthode, Java tente de satisfaire l’appelle de la méthode.

public class Main {
    static public void hello(String name) {
        System.out.println("Hello "+name+"!!!");
    }
    static public void hello(String name, int age) {
        System.out.println("Hello "+name+", you're "+age+" years old!!");
    }
    static public void hello(int age, String name) {
        System.out.println("Hello "+name+", you're "+age+" years old!!");
    }
    public static void main(String[] args) {
      hello("Alice");  // retourne "Hello Alice!!!"
      hello("Bob", 38);// retourne "Hello Bob, you're 38 years old!!"
      hello(38, "Bob");// retourne "Hello Bob, you're 38 years old!!"
    }
}

Run it!

Surcharge de Méthodes

Autre exemple: javafx.geometry.Rectangle2D

Une méthode peut appeler la methode qu’il surcharge pour évité de réécrire deux fois la même chose. Dans la classe Rectangle2D, on peut supposer que contains(Point2D p)fait appelle à contains(double x, double y).
(vérifier ça!!)

Surcharge du Constructeur

Comme les autres méthodes, le constructeur peut-être surchargé (et c’est souvent le cas)

Notamment, c’est très pratique de proposer un constructeur par defaut (sans aucun paramètre) ou bien un constructeur de copy (prenant un objet de même type en paramètre).

Surcharge du Constructeur

public class Student {
    private final int id;
    private String name;

    static private int lastStudentId;

    public Student() {
        this(null);
    }
    public Student(String name) {
        this.id = lastStudentId + 1;
        ++lastStudentId;

        if(name == null) {
            name = "No Name";
        }
        this.name = name;
    }
    public void setName(String n) {
        name = n;
    }
    public String getName() {
        return name;
    }
}

Dans un constructeur, on peut appeler un autre avec l’instruction this(...). Dans ce case ça doit être la première instruction du constructeur.

Constructeur de Copie


class Complex {

    private double re, im;

    public Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }
    Complex(Complex c) {
        re = c.re;
        im = c.im;
    }
    public void add(Complex other) {
        re += other.re;
        im += other.im;
    }
    public void add(double re, double im) {
        this.re += re;
        this.im += im;
    }
    public String toString() {
        return "(" + re + " + " + im + "i)";
    }
}

public class Main {

    public static void main(String[] args) {
        Complex c1 = new Complex(2, 2);
        Complex c2 = c1;
        Complex c3 = new Complex(c1);

        c1.add(3,3);

        System.out.println(c2);
        System.out.println(c3);
    }
}
                        

affiche : (run it!)

(5.0 + 5.0i)
(2.0 + 2.0i)

Constructeur de Copie

D’ailleurs cette classe aurait intérêt à être immuable. Et dans ce cas un constructeur de copie n’est pas utile :


final class Complex {

    private final double re, im;

    public Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }
    public Complex add(Complex other) {
        return add(other.re, other.im);
    }
    public Complex add(double re, double im) {
        return new Complex(this.re + re, this.im + im);
    }
    public String toString() {
        return "(" + re + " + " + im + "i)";
    }
}

public class Main {

    public static void main(String[] args) {
        Complex c1 = new Complex(2, 2);
        Complex c2 = c1;

        c1 = c1.add(3,3);

        System.out.println(c2);
    }
}
                        

run it!

Héritage

Relation entre Objets (et Classes)

 

  • La composition/agrégation d’objets :
    l’objet de type A “A UN” objet de type B
  • L’utilisation d’objets :
    l’objet de type A “UTILISE UN” objet de type B

L’héritage introduit un nouveau type de relation :
l’objet de type B “EST UN” objet de type A

Héritage

Lorsqu’un objet de type B “EST UN” objet de type A, on dit que B hérite des propriétés de A.

  • A est la super-classe (ou classe mère) de B
  • B est une classe dérivée (ou sous-class ou classe fille) de A

Héritage

Lorsqu’un objet de type B “EST UN” objet de type A, on dit que B hérite des propriétés de A.

Exercice : Héritage, Agrégation ou Utilisation ?

  • Cercle et Ellipse ?
  • Salle de bains et Baignoire ?
  • Piano et Joueur de piano ?
  • Entier et Réel ?
  • Personne, Enseignant et Etudiant ?
  • Appareil, Imprimante, Scanner et Copieur ?

Exemple

Problèmes pour implémenter cette spécification :
Elle est trop générale : manger( ) diffère suivant les animaux ;
Il manque les services spécifiques : voler( ), nager( ), …

Mauvaise Solutions

  • Faire autant de classe qu’il existe d’espèces animales :
    • => beaucoup de services similaires
    • => cout énorme pour maintenanir le code
  • Modifier la classe Animal pour représenter tous les animaux :
    • Ajouter des méthodes pour représenter toutes les choses que les animaux penvent faire
    • Ajouter un attribut pour indiquer la catégorie (isFish(), isBird())
    • => complexité accrue, et coût énorme pour étendre le code

Solution

Utiliser une hiérarchie de classe et faire de l’héritage

Héritage

L’héritage est un concept essentiel en programmation orientée objet.

Il permet la réutiliation des composants (les classes). Il permet la création d’une classe dérivée à partir d’une classe de base et ainsi l’enrichir.

Il permet aussi la création d’une hierarchie de types (la classe dérivée définie un sous-type de la classe de base).

Héritage en Java

  1. Une classe hérite toujours d’une seule classe
  2. Par défaut, une classe hérite de la classe Object
  3. Les cycles sont interdits

Héritage

Supposons qu’on propose une classe Point qui représente un point dans le plan :


class Point {
    private double x, y;
    public Point() {
        x = Math.random()*100;
        y = Math.random()*100;
    }
    public Point(double x, double y)
    {
        this.x = x; this.y = y;
    }
    public void add(int dx, double dy)
    {
        x += dx;
        y += dy;
    }
    public double getX() { return x; }
    public double getY() { return y; }
}

Héritage

Et que l’on souhaite créer une classe ColoredPoint qui représente un point coloré dans le plan.

Alors on peut utiliser la classe Point et lui ajouter des propriétés. On l’indique au compilateur avec le mot clé extends.


class ColoredPoint extends Point {
    private byte color;
    public void setColor(byte c) { color = c; }
    public byte getColor() { return color; }
}

    ColoredPoint p = new ColoredPoint();
    p.setColor((byte)50);
    System.out.println(p.toString() + " with color " + p.getColor());

run it!

Héritage

Une classe dérivée hérite des méthodes et attributs publics de sa classe de base. Un objet de type ColoredPoint étant aussi de type Point il peut donc utiliser les méthodes getX(), getY() en plus de getColor().

Attention: une classe dérivé n’a pas accés aux attributs et méthodes privés. Encore une fois, ce sont des détails d’implémentation qui ne doivent pas sortir de la classe.

Héritage et Constructeur

Dans l’exemple précédent, lorsqu’un objet de type ColoredPoint est crée, on peut se demander si un constructeur de la class Point est appelé.

La réponse est oui.

Juste avant que le constructeur de la classe ColoredPoint soit appelé, un constructeur de la classe de base est appelé.

Héritage et Constructeur

Par défaut, le constructeur par défaut est appelé

Mais on peut en choisir un autre avec l’instruction super(...). Dans ce cas ça doit être la première instruction du constructeur

Héritage et Constructeur


class ColoredPoint extends Point {
    //Choix 1
    public ColoredPoint() {
        color = (byte)(Math.random()*256);
    }
    //Choix 2
    public ColoredPoint() {
        super();
        color = (byte)(Math.random()*256);
    }
    //Choix 3
    public ColoredPoint() {
        super(0, 0);
        color = (byte)(Math.random()*256);
    }
}

Ici, les choix 1 et 2 sont équivalent (c’est inutile d’écrire super()). Par contre dans le choix 3, le constructeur de Point est appelé avec deux paramètres (les coordonnées ne sont donc plus aléatoire mais initialisé a 0).

Héritage et Constructeur

Remarque: Les contructeurs ne sont pas hérités au même titre que les méthodes publiques. Lors de la création d’un objet, un constructeur de la classe dérivée doit obligatoirement exister.


class ColoredPoint extends Point {
    private byte color;
    public ColoredPoint() {
        color = (byte)(Math.random()*256);
    }
    public void setColor(byte c) { color = c; }
    public byte getColor() { return color; }
}

On ne peut pas faire new ColoredPoint(10, 10) même si un contructeur de Point existe pour ces arguments.

bien sûr si aucun constructeur n’est défini, le compilateur en définit un par défaut qui ne fait rien.

Héritage et Constructeur

Pour être cohérent avec la classe Point, on peut faire :


class ColoredPoint extends Point {
    private byte color;
    public ColoredPoint() {
        color = (byte)(Math.random()*256);
    }
    public ColoredPoint(double x, double y) {
        super(x, y);
        color = (byte)(Math.random()*256);
    }
    public ColoredPoint(double x, double y, byte c) {
        super(x, y);
        color = c;
    }
    public void setColor(byte c) { color = c; }
    public byte getColor() { return color; }
}

Exemples


class A {
    public String a() { return "A"; }
}
class B {
    public String b() { return "B"; }
}
                        

B o = new B();
o.a();
o.b();
                        

B o = new B();
o.a(); //retourne "A"
o.b(); //retourne "B"
                        

Exemples


class A {
    public String a() { return "A"; }
}
class B {
    private int i;
    public B(int i) { this.i = i; } // Le constructeur par défaut de A est appelé
    public int b() { return i; }
}
                        

B o = new B(10);
o.a()
o.b()
o = new B();
                        

B o = new B(10);
o.a() //retourne "A"
o.b() //retourne 10
o = new B(); // Erreur de compilation
                        

Exemples


class A {
    private int j;
    public A(int j) { this.j = j; }
    public String a() { return "A"; }
}
class B {
    private int i;
    public B(int i) { this.i = i; }
    public int b() { return i; }
}
                        

Erreur de compilation, aucun constructeur par défaut n’existe pour A

Exemples


class A {
    public A() { System.out.println("A created"); }
    public String a() { return "A"; }
}
class B {
    public String b() { return "B"; }
}
                        

B o = new B();
                        

B o = new B(); // Affiche "A created"
                        

Héritage

On peut vérifier le type d’un objet avec le mot clé instanceof.


Point p = pointOrColoredPoint();
if(p instanceof ColoredPoint){
    ColoredPoint cp = (ColoredPoint)p;
    System.out.println(cp.getColor());
}
if(p instanceof Point){
    Point cp = (Point)p;
    System.out.println("just a point");
} else {
    System.out.println("just an object");
}
                        

Redéfinition de Méthodes

Redéfinition de Méthodes

Que ce passe-t’il lorsqu’un méthode d’une classe dérivée à exactement la même signature qu’une méthode de la classe de base?

C’est possible, et dans ce cas, c’est toujours la méthode de la classe dérivée qui est appelée.

remarque : si type de retour peut être un sous-type du type de retour de la classe de base, on parle toujours de redéfinition.

Redéfinition de Méthodes


class A {
    public String foo() { return "I'm A" }
}
class B extends A {
    @Override
    public String foo() { return "I'm B" }
}
                        

Le mot clé @Override est appelé une annotation. Ici elle indique au compilateur notre intention de redéfinir une méthode qui existe dans la super-classe.
L’annotation @Override n’est pas obligatoire mais elle permet d’être sûr qu’on n’a pas fait une faute de frappe lors de la redéfinition.


B b = new B();
b.foo(); // return "I'm B"
                        

Mot clé super

Dans une méthode d’une classe dérivé, on peut appeler une méthode de la super-classe avec le mot clé super


class A {
    public String foo() { return "I'm A" }
}
class B extends A {
    @Override
    public String foo() { return super.foo()+" and I'm B" }
}
                        

B b = new B();
b.foo(); // return "I'm A and I'm B"
                        

Surcharge de Méthodes

Si le type des arguments est différent de ceux de la classe de base, alors c’est une surcharge de méthode (comme c’est le cas au sein d’une classe).

Surcharge et Redéfinition de Méthodes

Lorsqu’une méthode est appelé sur un objet, il y a une étape de résolution pour savoir qu’elle implémentation de la méthode est utilisé :

 

  • On considère la classe la plus dérivée dans la hiérarchie définissant l’objet (celle qui a créer l’objet avec new).
  • On regarde si la méthode existe, avec les bon arguments.
  • Si c’est le cas, on utilise cette méthode, sinon on considère la classe parente (la super-classe) et on recommence au point précédent.
  • Si il n’y a pas de classe parente, il y a une erreur.

 

Surcharge et Redéfinition de Méthodes


class A {
    String foo() { return "I'm A" }
}
class B extends A {
    String foo(int i) { return "I'm B" }
}
class C extends A {
    String foo() { return "I'm C" }
}
class D extends C {
    String foo() { return "I'm D" }
}
class E extends C {
    String foo(int i) { return "I'm E" }
}
                        

B b = new B(); C c = new C(); D d = new D(); E e = new E();
b.foo();
b.foo(1);
c.foo();
c.foo(1);
d.foo();
e.foo();
e.foo(1);
                        

B b = new B(); C c = new C(); D d = new D(); E e = new E();
b.foo();  // return "I'm A"
b.foo(1);
c.foo();
c.foo(1);
d.foo();
e.foo();
e.foo(1);
                        

B b = new B(); C c = new C(); D d = new D(); E e = new E();
b.foo();  // return "I'm A"
b.foo(1); // return "I'm B"
c.foo();
c.foo(1);
d.foo();
e.foo();
e.foo(1);
                        

B b = new B(); C c = new C(); D d = new D(); E e = new E();
b.foo();  // return "I'm A"
b.foo(1); // return "I'm B"
c.foo();  // return "I'm C"
c.foo(1);
d.foo();
e.foo();
e.foo(1);
                        

B b = new B(); C c = new C(); D d = new D(); E e = new E();
b.foo();  // return "I'm A"
b.foo(1); // return "I'm B"
c.foo();  // return "I'm C"
c.foo(1); // erreur
d.foo();
e.foo();
e.foo(1);
                        

B b = new B(); C c = new C(); D d = new D(); E e = new E();
b.foo();  // return "I'm A"
b.foo(1); // return "I'm B"
c.foo();  // return "I'm C"
c.foo(1); // erreur
d.foo();  // return "I'm D"
e.foo();
e.foo(1);
                        

B b = new B(); C c = new C(); D d = new D(); E e = new E();
b.foo();  // return "I'm A"
b.foo(1); // return "I'm B"
c.foo();  // return "I'm C"
c.foo(1); // erreur
d.foo();  // return "I'm D"
e.foo();  // return "I'm C"
e.foo(1);
                        

B b = new B(); C c = new C(); D d = new D(); E e = new E();
b.foo();  // return "I'm A"
b.foo(1); // return "I'm B"
c.foo();  // return "I'm C"
c.foo(1); // erreur
d.foo();  // return "I'm D"
e.foo();  // return "I'm C"
e.foo(1); // return "I'm E"
                        

Polymorphisme

Polymorphisme


class A {
    String foo() { return "I'm A" }
}
                        

A a = generateSomething();
System.out.println(a.foo());
                        

Considérons cet exemple, qu’affiche notre programme?

Il semblerait que ce soit “I’m A” , mais

on ne peut pas en être sûr!!

Polymorphisme


class A {
    String foo() { return "I'm A" }
}
class B extends A {
    String foo() { return "I'm B" }
}
A generateSomethin() { return new B(); }
                        

A a = generateSomethin();
System.out.println(a.foo());
                        

Dans ce cas, notre programme affiche “I’m B”. Car l’objet a a été défini par la classe B même s’il est stocké dans une variable de type A. Donc lors de l’éxecution de la méthode foo, la classe la plus dérivée (ici B) qui possède la méthode et donc utilisée.

C’est ce qu’on appelle le polymorphisme.

Polymorphisme

Le polymorphisme est le concept consistant à fournir une interface unique à des entités pouvant avoir différents types (wikipedia).

On pourra remarquer que la surcharge de méthode est aussi considéré comme du polymorphisme.

Lorsqu’il provient d’une redéfinition de méthode, on l’appelle polymorphisme par sous-typage.

Polymorphisme

En clair, ça signifie que lorsqu’on appelle une méthode sur un objet, l’implémentation choisie dépend du type réel de l’objet (la classe qui a crée l’objet) et ne dépends pas du type de la variable qui stocke l’objet.

Cela permet d’appeler la même méthode sur des objets sans se soucier de leur type!!

Polymorphisme

Cela permet d’appeler la même méthode sur des objets sans se soucier de leur type!!


Animal animals = NoahsArk.listAnimals();
for(Animal a : animals) {
    a.speak();
}
                        

Ici, on fait parler tous les animaux quelque soit leur type.

Polymorphisme


class Animal {
    public void speak() { System.out.println("who am I?"); }
}
class Dog extends Animal{
    public void speak() { System.out.println("I'm a dog"); }
}
class Cat extends Animal{
    public void speak() { System.out.println("I'm a cat"); }
}
class Squirrel extends Animal{
    public void speak() { System.out.println("I'm a squirrel"); }
}
class Dolphin extends Animal{
    public void speak() { System.out.println("I'm a dolphin"); }
}
class Duck extends Animal{
    public void speak() { System.out.println("I'm a duck"); }
}
class Parrot extends Animal{
    public void speak() { System.out.println("I'm a parrot"); }
}

class NoahsArk {
    static Animal[] listAnimals() {
        Animal[] animals = new Animal[6];
        animals[0] = new Dog();
        animals[1] = new Squirrel();
        animals[2] = new Cat();
        animals[3] = new Dolphin();
        animals[4] = new Duck();
        animals[5] = new Parrot();
        return animals;
    }
}
class Main {
  public static void main(String[] args) {
    Animal[] animals = NoahsArk.listAnimals();
    for(Animal a : animals) {
        a.speak();
    }
  }
}
                        

run it!

Package et Visibilité

Package (Paquetage en français)

Un paquetage est une bibliothèque d’objets, un ensemble de classes (dans un même répertoire).

Les paquetages permettent :

  • de structurer le projet : les classes sont triées dans des répertoires.
  • d’évité les conflits de nom : des classes de même nom peuvent exister dans des packages différents.
  • d’affiner la visibilité.

 

Package

Pour créer un paquetage, il suffit de créer un répertoire dont le nom est en minuscule (c’est le nom du paquetage)

Pour faire partie du paquetage, une classe doit se trouver dans ce répertoire et la première ligne du fichier de la class contient le mot clé package suivit du nom du paquetage.

Exemple, dans un fichier /..../pim/Poum.java:


package pim;

public class Poum {
   ...
}

                        

Package

Un paquetage peut contenir d’autre paquetage. On peut ainsi crée une hiérarchie complète.

Exemple, dans un fichier /..../pim/pam/Poum.java:


package pim.pam;

public class Poum {
   ...
}
                        

Il est courant dans les bibliothèques disponibles sur internet, de trouver des noms de packages contenant une url inversée. Cela permet d’éviter les collisions entre bibliothèques.


package org.json.simple; // nom du package de la libraire json-simple qui permet de parser du json
                        

package org.json.simple.parser; // Cette bibliotèque contient un sous-package parser, ajouté à la suite de l'url inversée.
                        

Autre exemple de paquetage javafx.scene.control

Visibilité

On a vu les mots clé public et private pour indiquer qu’une méthode ou un attribut est visible en dehors de la classe ou non.

On a vu aussi qu’un attribut private est visible dans la classe mais pas dans les classes dérivées.

Visibilité – protected

Le mot clé protected étend la visibilité aux classes dérivées.

Visibilité – par défaut

Par défaut (aucun mot clé), un attribut ou une méthode n’est visible que dans le package.

Visibilité – par défaut

Attention : la visibilité protected étant la visibilité par défaut. Donc un élément protected est accessbile par toutes les classes du paquetage et à toutes les sous-classes (même dans d’autre package)

Visibilité

 

  • Elément private : visible uniquement au sein de la classe.
  • Elément par défaut : visible en plus au sein du même paquetage.
  • Elément protected : visible en plus au sein des classes dérivées.
  • Elément public : visible partout.

 

Visibilité

Les classes dans un même répertoire sont considérées comme appartenant au même paquetage, même si celui-ci n’est pas déclaré au début du fichier avec le mot clé package (c’est un paquetage sans nom).

Utilisation des Paquetages

Comment utiliser une classe qui se trouve dans un paquetage?


package pim.pam;

public class Poum {
   ...
}
                        

En indiquand devant le nom de la classe, le nom du paquetage


public class Main {
    public static void main(String[] args) {
        pim.pam.Poum p = new pim.pam.Poum();
        p.poum();
    }
}

                        

Utilisation des Paquetages

Ou bien, en important la classe du paquetage et ensuite utiliser la classe normalement (mot clé import) :


import pim.pam.Poum;

public class Main {
    public static void main(String[] args) {
        Poum p = new Poum();
        p.poum();
    }
}
                        

Import de Classes

Pour utiliser les fonctionnalités d’un package, il est souvent nécéssaire d’importer de nombreuses classes.

C’est, par exemple, le cas avec les entrées/sorties du package java.io


import java.io.InputStream;
import java.io.DataInputStream;
import java.io.BufferedInputStream;

public class Test {
    public static void main (String[] args) {
        InputStream is = new InputStream();
        DataInputStream dis = new DataInputStream(is);
        BufferedInputStream bdis = new BufferedInputStream(dis);
        ...
    }
}
                        

Import de Classes

Dans ce cas, on peut importer toutes les classes d’un paquetage avec l’astérisque.


                        import nomPackage.*
                        

import java.io.*;

public class Test {
    public static void main (String[] args) {
        InputStream is = new InputStream();
        DataInputStream dis = new DataInputStream(is);
        BufferedInputStream bdis = new BufferedInputStream(dis);
        ...
    }
}
                        

Attention : ceci n’importe pas les classes des sous-paquetages (ce n’est pas récursif).

La Classe Object

la Classe Object

On a vu que par défaut une classe hérite de la classe Object

Sinon, elle hérite d’une autre classe, qui elle hérite de la classe Object

Donc dans tous les cas, elle hérite de la classe Object, directement ou indirectement

l’Objet Object

On a vu que toutes les classes héritent de la classe Object.

Donc, toutes les classes héritent des méthodes publiques de cette classe. doc

Donc sur tous les objets, même sans connaitre leur type, on peut appeler ces méthodes.

Parmis ces méthodes, 3 sont particulièrement importantes.

Méthode toString()

Elle permet de représenter/décrire l’objet sous forme de chaine de charactères.

Par défaut elle affiche l’adresse mémoire de l’objet, il est important de la redéfinir au sein de chaque classe.


public class Person {
    private int birthYear;
    private String name;
    public Person(String name, int birthYear) {
        this.name = name; this.birthYear = birthYear;
    }
    @Override
    public String toString() {
        return "I'm "+name+", I'm "+getAge()+" years old.";
    }
    public int getAge() { return 2017 - birthYear; }
}
                        

Méthode toString()


Object alice = new Person("Alice", 1977);
System.out.println(alice.toString());
                        

System.out.println(alice);
                        

System.out.println(alice); // Java converti automatiquement un objet en String en utilisant la méthode toString()
                        

System.out.println("I've created an object: "+alice);
                        

System.out.println("I've created an object: "+alice); // idem
                        

System.out.println(alice.getAge());
                        

System.out.println(alice.getAge()); // Erreur de compilation, la classe Object n'a pas de méthode getAge()
                        

A la compilation, Java vérifie que la méthode appelée existe dans la classe contenant l’objet.


System.out.println(((Person)alice).getAge()); // ok
                        

On peut convertir un objet dans un type dérivé. La compilation se passe bien. Mais pendant l’exécution, il peut se produire une erreur si l’objet n’est pas du type donné.

Méthode equals()

Elle permet de comparer deux objets. Elle retourne true si les deux objets sont identiques.

Par défaut, cette méthode retourne true si et seulement si les objets correspondent exactement au même objet en mémoire (ob1 == ob2).

Lorsqu’on chosit de redéfinir cette méthode alors, intuitivement, elle doit être :

  • Réflexive : x.equals(x) retourne true.
  • Symétrique : si x.equals(y) alors y.equals(x).
  • Transitive : si x.equals(y) et y.equals(z) alors x.equals(z).
  • Cohérente : des appelles successifs retournent la même valeur.

 

Méthode equals()

Il est parfois intéressant de redéfinir cette méthode.


public class Person {
    private int birthYear;
    private String name;
    public Person(String name, int birthYear) {
        this.name = name; this.birthYear = birthYear;
    }
    @Override
    public boolean equals(Object other) {
        if(other == null || this.getClass() != other.getClass()) 
            return false;

        Person p = (Person)other;
        return name == p.name && birthYear == p.birthYear;
    }
}
                        

Ici on considère que deux objets de type Person sont egaux si et seulement si ils ont le même nom et la même année de naissance.

Méthode equals()


Object alice = new Person("Alice", 1977);
Object aliceBis = new Person("Alice", 1977);
Object bob = new Person("Bob", 1978);
Object bobBis = bob;
                        

alice == aliceBis
                        

alice == aliceBis // false
                        

alice.equals(aliceBis);
                        

alice.equals(aliceBis); // true
                        

alice.equals(bob);
                        

alice.equals(bob); // false
                        

bob == aliceBis
                        

bob == bobBis // true
                        

bob.equals(bobBis);
                        

bob.equals(bobBis); // true
                        

Exemple de la classe Arrays :

“Returns true if the two specified arrays of Objects are equal to one another. The two arrays are considered equal if both arrays contain the same number of elements, and all corresponding pairs of elements in the two arrays are equal.”

Méthode hashCode()

Elle permet de représenter l’objet sous la forme d’un entier.

C’est utile pour certaines bibliothèques, qui veulent répartir des objets quelconques dans des tableau par exemple.

Il est important de redéfinir cette méthode lorsque l’on a redéfinit la méthode equals()

Surtout pour la raison suivante : La méthode hashCode() doit retourner des valeurs identiques pour deux objets identiques (au sens de la méthode equals())

Méthode hashCode()


public class Person {
    private int birthYear;
    private String name;
    public Person(String name, int birthYear) {
        this.name = name; this.birthYear = birthYear;
    }
    @Override
    public boolean equals(Object other) {
        if(other == null || this.getClass() != other.getClass()) 
            return false;

        Person p = (Person)other;
        return name == p.name && birthYear == p.birthYear;
    }
    @Override
    public int hashcode() {
        return 1;
    }
}
                        

Erreur de compilation : error: method does not override or implement a method from a supertype. Sans l’annotation @Override, la compilation aurait fonctionné !!

Méthode hashCode()


public class Person {
    private int birthYear;
    private String name;
    public Person(String name, int birthYear) {
        this.name = name; this.birthYear = birthYear;
    }
    @Override
    public boolean equals(Object other) {
        if(other == null || this.getClass() != other.getClass()) 
            return false;

        Person p = (Person)other;
        return name == p.name && birthYear == p.birthYear;
    }
    @Override
    public int hashCode() {
        return 1;
    }
}

Cette redéfinition de hashCode() est correcte, mais le but est d’essayer dans la mesure du possible de renvoyer des valeurs différentes pour des objets différents.

Méthode hashCode()


public class Person {
    private int birthYear;
    private String name;
    public Person(String name, int birthYear) {
        this.name = name; this.birthYear = birthYear;
    }
    @Override
    public boolean equals(Object other) {
        if(other == null || this.getClass() != other.getClass()) 
            return false;

        Person p = (Person)other;
        return name == p.name && birthYear == p.birthYear;
    }
    @Override
    public int hashCode() {
        return 7 * name.hashCode() + 37 * birthYear;
    }
}
                        

Comme ça c’est mieux!!

Mot Clé final

Le mot clé final peut être utilisé pour une classe ou une méthode.

Dans ce cas, la classe ne peut pas être dérivée, resp. la méthode ne peux pas être redéfinie.

Objets Immuables

Une classe A qui ne contient que des attributs final et qui ne possède pas de mutateurs n’est pas forcément immuable.

En effet, lorsqu’on manipule un objet de type A, c’est peut-être en faite un objet de sous-type B, qui lui possède des attributs non final

> Pour construire des objets immuables, il faut donc une classe sans modificateurs et ne pouvant pas être dérivée (donc une classe finale)