20 março, 2013

Resumo Effective Java - Parte 2 (Métodos comuns a todos os objetos)



Capítulo 3 – Métodos comuns a todos os objetos

Item 8 – Obedecendo o contrato do equals

·         Sempre que instâncias de uma classe precisarem ser comparadas entre si e houver a noção de identidade que difere da mera identidade entre objetos (ou seja, aquela que retorna verdadeiro meramente se dois objetos forem iguais na memória), o método equals deve ser implementado. 
·         Deve-se obedecer as seguintes regras (contrato) ao sobreescrever o equals:

o   Reflexividade: Dado x, x.equals(x) deve retornar true.
o   Simetria: Dado x e y, x.equals(y) deve retornar true se e somente se y.equals(x) retornar true.
o   Transitividade: Dado x,y e z. Se x.equals(y) é true e y.equals(z) é true, então x.equals(z) deve ser true.

·         Transitividade: Essa característica merece uma atenção especial. A discussão que segue é longa e complicada, mas achei importante acrescentar aqui.

Numa relação de herança, nada impede que uma subclasse acrescente atributos que devem ser considerados na execução do equals. Isso gera inúmeros problemas! Vejamos o código abaixo:






public class Point {

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

@Override 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 extends Point {

private final Color color;

public ColorPoint(int x, int y, Color color) {

super(x, y);
this.color = color;

}
... // Remainder omitted
}

 

 
O funcionamento que esperamos é que consigamos comparar intercaladamente pontos e pontos coloridos através do método equals. Afinal, são todos pontos.  Mas como ColorPoint acrescentou uma informação a mais à sua identidade, esta deve ser considerada. Como implementar o equals para a classe ColorPoint? Vejamos algumas soluções.

1 - Comparar apenas objetos da mesma classe:

// Broken - violates symmetry!
@Override public boolean equals(Object o) {

if (!(o instanceof ColorPoint))
return false;

return super.equals(o) && ((ColorPoint) o).color == color;

}

Esse código viola a simetria. Point.equals(ColorPoint) retornará true se as coordenadas forem as mesmas mas ColorPoint.equals(Point) sempre retornará false.

2 – Não considerar informação de cor em comparações misturadas

Resolve o problema da simetria mas ao custo da transitividade

// Broken - violates transitivity!
@Override public boolean equals(Object o) {

if (!(o instanceof Point))
return false;
    
if (!(o instanceof ColorPoint))
return o.equals(this); //nao considera cor se Object for Point
     
return super.equals(o) && ((ColorPoint) o).color == color;

}

ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);

P1.equals(P2) retorna true = > chama o equals do ColorPoint na linha “return o.equals(this)”

P2.equals(P3) retorna true => chama o equals do Point

P1.equals(P3) retorna false => retorna falso pois são ColorPoint de cores diferentes

3 – Usando o getClass  ao invés do instanceOf

// Broken - violates Liskov substitution principl
@Override public boolean equals(Object o) {

  if (o == null || o.getClass() != getClass())
    return false;
  Point p = (Point) o;
  return p.x == x && p.y == y;

}

O código acima compara apenas objetos da mesma classe.  O princípio da substituição de Liskov diz que qualquer propriedade importante para um tipo deve valer também para o subtipo. Assim, qualquer método escrito para um tipo, deve funcionar corretamente nos seus subtipos. A implementação do equals, na classe Point, na forma acima, viola esta propriedade. Qualquer subclasse criada a partir de Point e que precisar (e puder) usar o equals da classe Point, não vai funcionar. Sempre que esta nova classe for comparada com a classe Point irá retornar false, independentemente se a comparação dos atributos estiver válida. 

Não há um meio satisfatório de estender uma classe, adicionar um atributo e ainda a relação de identidade continuar funcionando.
A melhor solução para isso é: ao invés de fazer herança, optar por composição. Concluímos que, toda vez que uma classe é estendida e um novo atributo adicionado, isso vai quebrar o método equals implementado na superclasse. Quando uma comparação for feita entre um objeto da sub.equals(super), o método da subclasse vai querer utilizar para comparação um atributo que só ela tem.

Evitamos isso utilizando composição ao invés de herança:

// Adds a value component without violating the equals contract
public class ColorPoint {

 private final Point point;
 private final Color color;

 public ColorPoint(int x, int y, Color color) {

  if (color == null)
   throw new NullPointerException();
  point = new Point(x, y);
  this.color = color;
 }


@Override public boolean equals(Object o) {

 if (!(o instanceof ColorPoint))//Como herança não está envolvida, não é
  return false;                 //preciso permitir a comparação de Point com
                                //ColorPoint                               
 ColorPoint cp = (ColorPoint) o;
 return cp.point.equals(point) && cp.color.equals(color);
}
... // Remainder omitted

}

OBS.: Sempre comparar double com Double.compare e float com Float.compare

Item 9 – Hashcode

·         Algoritmo para gerar hashcode:

1. Store some constant nonzero value, say, 17, in an int variable called result.
2. For each significant field f in your object (each field taken into account by the equals method, that is), do the following:

a. Compute an int hash code c for the field:
i. If the field is a boolean, compute (f ? 1 : 0).
ii. If the field is a byte, char, short, or int, compute (int) f.
iii. If the field is a long, compute (int) (f ^ (f >>> 32)).
iv. If the field is a float, compute Float.floatToIntBits(f).
v. If the field is a double, compute Double.doubleToLongBits(f), and
then hash the resulting long as in step 2.a.iii.
vi. If the field is an object reference and this class’s equals method compares the field by recursively invoking equals, recursively invoke hashCode on the field. If a more complex comparison is required, compute a “canonical representation” for this field and invoke hashCode on the canonical representation. If the value of the field is null, return 0 (or some other constant, but 0 is traditional).
vii. If the field is an array, treat it as if each element were a separate field. That is, compute a hash code for each significant element by applying these rules recursively, and combine these values per step 2.b. If every element in an array field is significant, you can use one of the Arrays.hashCode methods added in release 1.5.

b. Combine the hash code c computed in step 2.a into result as follows: result = 31 * result + c;

3. Return result.

4. When you are finished writing the hashCode method, ask yourself whether equal instances have equal hash codes. Write unit tests to verify your intuition!

Ex:
// Lazily initialized, cached hashCode
private volatile int hashCode;  // (See Item 71)

@Override public int hashCode() {
  int result = hashCode;
  if (result == 0) {
    result = 17;
    result = 31 * result + areaCode;
    result = 31 * result + prefix;
    result = 31 * result + lineNumber;
    hashCode = result;
  }
 return result;
}

Item 13 – Classes e Interfaces

·         Atributos devem ser PRIVADOS.
·         Uma exceção pode ser as constantes. Campos public static final SEMPRE devem apontar para primitivos ou objetos imutáveis. Se não apontarem para objetos imutáveis, apesar da referencia não poder ser alterada, o objeto poderá! Um array é sempre mutável e assim nunca deve ser criado como constante.
·         Exemplo de como disponibilizar um array privado de forma que continue imutável:

private static final Thing[] PRIVATE_VALUES = { ... };
public static final List VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

ou

private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {  return PRIVATE_VALUES.clone();  }

Item 10 – Sempre sobreescreva toString
Item 11 – Sobreescreva o método clone com cuidado
Item 12 – Considere implementar a interface Comparable

Nenhum comentário: