A resposta de Kilian Foth é excelente. Gostaria apenas de adicionar o exemplo canônico * de por que isso é um problema. Imagine uma classe Point inteira:
class Point2D {
public int x;
public int y;
// constructor
public Point2D(int theX, int theY) { x = theX; y = theY; }
public int hashCode() { return x + y; }
public boolean equals(Object o) {
if (this == o) { return true; }
if ( !(o instanceof Point2D) ) { return false; }
Point2D that = (Point2D) o;
return (x == that.x) &&
(y == that.y);
}
}
Agora vamos subclassificar que seja um ponto 3D.
class Point3D extends Point2D {
public int z;
// constructor
public Point3D(int theX, int theY, int theZ) {
super(x, y); z = theZ;
}
public int hashCode() { return super.hashCode() + z; }
public boolean equals(Object o) {
if (this == o) { return true; }
if ( !(o instanceof Point3D) ) { return false; }
Point3D that = (Point3D) o;
return super.equals(that) &&
(z == that.z);
}
}
Super simples! Vamos usar nossos pontos:
Point2D p2a = new Point2D(3, 5);
Point2D p2b = new Point2D(3, 5);
Point2D p2c = new Point2D(3, 7);
p2a.equals(p2b); // true
p2b.equals(p2a); // true
p2a.equals(p2c); // false
Point3D p3a = new Point3D(3, 5, 7);
Point3D p3b = new Point3D(3, 5, 7);
Point3D p3c = new Point3D(3, 7, 11);
p3a.equals(p3b); // true
p3b.equals(p3a); // true
p3a.equals(p3c); // false
Você provavelmente está se perguntando por que estou postando um exemplo tão fácil. Aqui está o problema:
p2a.equals(p3a); // true
p3a.equals(p2a); // FALSE!
Quando comparamos o ponto 2D com o ponto 3D equivalente, nos tornamos verdadeiros, mas quando invertemos a comparação, nos tornamos falsos (porque p2a falha instanceof Point3D
).
Conclusão
Geralmente, é possível implementar um método em uma subclasse de forma que não seja mais compatível com o modo como a superclasse espera que ele funcione.
Geralmente é impossível implementar equals () em uma subclasse significativamente diferente de uma maneira que seja compatível com a classe pai.
Quando você escreve uma classe que pretende permitir que as pessoas subclasses, é uma boa idéia escrever uma contrato sobre como cada método deve se comportar. Melhor ainda seria um conjunto de testes de unidade que as pessoas poderiam executar contra suas implementações de métodos substituídos para provar que não violam o contrato. Quase ninguém faz isso porque é muito trabalho. Mas se você se importa, é o que deve ser feito.
Um ótimo exemplo de um contrato bem escrito é o Comparator . Apenas ignore o que diz .equals()
pelas razões descritas acima. Aqui está um exemplo de como o Comparator pode fazer as coisas .equals()
não .
Notas
O item 8 "Java efetivo" de Josh Bloch foi a fonte desse exemplo, mas Bloch usa um ColorPoint que adiciona uma cor ao invés de um terceiro eixo e usa duplas ao invés de ints. O exemplo de Java de Bloch é basicamente duplicado por Odersky / Spoon / Venners, que disponibilizou seu exemplo online.
Várias pessoas se opuseram a este exemplo porque, se você informar a classe pai sobre a subclasse, poderá corrigir esse problema. Isso é verdade se houver um número suficientemente pequeno de subclasses e se os pais souberem de todas elas. Mas a pergunta original era sobre como criar uma API para a qual alguém escreverá subclasses. Nesse caso, você geralmente não pode atualizar a implementação pai para ser compatível com as subclasses.
Bônus
O comparador também é interessante, pois funciona em torno da questão da implementação correta de equals (). Melhor ainda, segue um padrão para corrigir esse tipo de problema de herança: o padrão de design da Estratégia. As Typeclasses com as quais Haskell e Scala se empolgam também são o padrão da Estratégia. A herança não é ruim ou errada, é apenas complicada. Para uma leitura mais aprofundada, consulte o artigo de Philip Wadler Como tornar o polimorfismo ad-hoc menos ad hoc