Point

Cet exemple a pour but de montrer que les propriétés de l'égalité interdisent certaines relations d'héritage. Il est tiré de
Joshua Bloch, "Effective Java, Programming Language Guide", Addison-Wesely 2001.

equals(Object)

La méthode equals(Object) doit respecter les règles de l'égalité, c'est à dire celles d'une relation d'équivalence  : soient o1, o2 et o3 des instances de Object,
D'autre part, cette méthode doit être consistante, c'est à dire que si o1 et o2 ne changent pas, alors o1.equals(o2) ne doit pas changer non plus.

Les classes Point et ColorPoint

On se donne une classe Point :

public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public boolean equals(Object o) {
        if (!(o instanceof Point))
            return false;
        Point p = (Point) o;
        return p.x == x && p.y == y;
    }
    // Remainder omitted
}

et une classe ColorPoint :

public class ColorPoint extends Point {
    private Color color;

    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }
    // Remainder omitted
}

La méthode equals(Object o) de ColorPoint

Comment définir une méthode equals(Object o) pour ColorPoint ?
Voici une première proposition :

    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint) o;
        return super.equals(o) && cp.color == color;
    }


Cette solution viole la règle de symétrie. Le résultat du code suivant :

Point p = new Point(10,10);
ColorPoint cp = new ColorPoint(10, 10, Color.RED);
System.out.println(cp.equals(p));
System.out.println(p.equals(cp));

est :

false
true

Pour éviter ce problème, on propose le code suivant :

    public boolean equals(Object o) {
        if (!(o instanceof Point))
            return false;
        if (!(o instanceof ColorPoint))
            return o.equals(this);
        ColorPoint cp = (ColorPoint) o;
        return super.equals(o) && cp.color == color;
    }

Cette solution viole la règle de transitivité. Le résultat du code suivant :

        Point p = new Point(10, 10);
        ColorPoint cp1 = new ColorPoint(10, 10, Color.BLUE);
        ColorPoint cp2 = new ColorPoint(10, 10, Color.RED);
        System.out.println(cp1.equals(p));
        System.out.println(p.equals(cp2));
        System.out.println(cp1.equals(cp2));       

est :

true
true
false

Quelle solution ?

En fait, il n'y a pas de solution, car on cherche une égalité entre des objets de natures différentes ! L'erreur vient du fait qu'un point coloré n'est pas de même nature qu'un point sans couleur. Par conséquent, ColorPoint ne doit pas hériter de Point. On utilisera donc la délégation.

public class ColorPoint {
 
   private Point point;
    private Color color;
 
    public ColorPoint(int x, int y, Color color) {
        point = new Point(x, y);
        this.color = color;
    }

    // Returns the point-view of this color point.
    Point asPoint() {
        return point;
    }
   
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint) o;
        return cp.point.equals(point) && cp.color == color;
    }
    // Remainder omitted
}

Les vues

Pour pouvoir utiliser les méthodes prenant un Point en paramètre, il est judicieux de fournir une vue sur le Point contenu dans une instance de ColorPoint. Une vue est un objet qui "adapte" tout ou partie des données d'un objet à un autre type. Par exemple, la méthode Map.keySet() retourne une vue de type Set  sur les clés d'une table. Toute intervention sur l'ensemble des clés retourné aura un effet sur la table et inversement.
Dans le cas de ColorPoint, la vue est retournée par la méthode asPoint(). Le fait qu'une instance de la classe Point ne puisse être modifiée rend la solution très simple : il suffit de renvoyer la référence sur le point contenu dans le point coloré. Si les points sont modifiables, la solution sera plus complexe. L'exemple ci-dessous montre une solution dans le cas d'un point modifiable et où un point coloré doit signaler tout déplacement à l'aide d'un méthode privée showMove().

public class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public void moveTo(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public boolean equals(Object o) {
        if (!(o instanceof Point))
            return false;
        Point p = (Point) o;
        return p.x == x && p.y == y;
    }
    // Remainder omitted
}

public class ColorPoint {
    private Color color;
    private Point point;

    private class InternalPoint extends Point {
        InternalPoint(int x, int y) {
            super(x, y);
        }

        public void moveTo(int x, int y) {
            super.moveTo(x, y);
            showMove();
        }
    }

    public ColorPoint(int x, int y, Color color) {
        point = new InternalPoint(x, y);
        this.color = color;
    }

    public void moveTo(int x, int y) {
        point.moveTo(x,y);
    }

    public int getX() {
        return point.getX();
    }

    public int getY() {
        return point.getY();
    }

    private void showMove() {
        System.out.println("moving to (" + point.getX() + "," + point.getY() + ")");
    }
   
    // Returns the point-view of this color point.
    Point asPoint() {
        return point;
    }

    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint) o;
        return cp.point.equals(point) && cp.color == color;
    }
    // Remainder omitted
}