Comparando Objetos

Objetos e Variáveis

Para entender como se comparam objetos é necessário entender a diferença entre comparar objetos e comparar o conteúdo de variáveis.

Variáveis contém referências a objetos. Objetos são intâncias de uma Classe. Todo o objeto pertence a uma Classe.

Existem formas de comparar as referências de duas variáveis - para saber se se referem ao mesmo objeto, ou comparar os objetos em si, para saber se se referem ao mesmo valor.

Formas de comparação

Existem várias formas de comparar dois objetos.

  • Equivalência — queremos saber se dois objetos podem ser utilizados no mesmo contexto indistintamente. Se dois objetos são equivalentes, podemos utilizar qualquer um deles, e descartar o outro.

  • Igualdade — queremos saber se os dois objetos representam, extamente, o mesmo valor. No caso geral, se eles têm o mesmo estado. Se dois elementos são iguais, não necessariamente são equivalentes, embora, na maioria dos casos, isso seja verdade.

  • Identidade — queremos saber se dois objetos são idênticos, ou seja, se são o mesmo objeto.

  • Ordem — queremos saber se um objetos é menor, ou maior, que o outro. Isto só é possivel se existir uma relação de ordem entre os objetos. Um caso particular é a relação de ordem entre um objeto e ele próprio. Neste caso o objetos não será menor, nem maior, que ele próprio, mas isso não significa que será igual a si mesmo. A Ordem não necessáriamente casa com a Igualdade.

  • Categorização - queremos saber se um objetos é de uma certa categoria, da mesma categoria que outro, ou se é de uma categoria compativel.

A relação de Identidade é a mais forte e resulta nos seguintes corolários:

  • Se dois objetos são idênticos, eles são equivalentes - Se são idênticos são na realidade o mesmo objetos, e um objetos é sempre equivalente a ele próprio.

  • Se dois objetos são idênticos, eles são iguais - Se são idênticos são na realidade o mesmo objetos, e um objetos é sempre igual a ele próprio.

Comparando Variáveis de Referência

A comparação de variáveis de referência nos informa se duas variáveis se referem ao mesmo objeto ou não. Pode acontecer que a variável não referencie objeto algum. Isto depende de linguagem para linguagem, mas para as linguagens que permitem que uma variável não referencie nada útil existe a uma palavra reservada que representa esse "lugar nenhum" - normalmente null ou nil.

Integer a = new Integer(1); // a variável `a` referencia um objeto
Integer b = new Integer(1); // a variável `b` referencia um objeto diferente da variável a
Integer c = null; // a variável `c` não referencia objeto algum

assertTrue(c == null); // sim, a variável `c` não referencia objeto algum
assertTrue(a != null); // sim, a variável `a` referencia um objeto e portanto não referencia "nada"
assertTrue(a == b); // a variável `a` referencia o mesmo objeto que ela mesma
assertTrue(b == b); // a variável `b` referencia o mesmo objeto que ela mesma
assertTrue(a != b); // a variável `a` não referencia o mesmo objeto que a variável `b`

O exemplo anterior mostra como variáveis podem referenciar objetos diferentes mesmo quando os objeto, em si mesmos, representam o mesmo valor.

No caso do Java, C# e outra linguagens, o operador == comparar a referência inscrita na variável. Em outras linguagens outros operadores farão este trabalho. Nem todas as linguagens permitem esta comparação.

Comparando Objetos

Comparando a identidade

A identidade de objetos pode ser testada pela identidade das referências das variáveis que a eles se referênciam. A ideia é que se as variáveis referenciam o mesmo objeto, então os objetos são, necessariamente, idênticos. Portanto, a identidade de objetos pode ser testada.

Comparando a equivalência

A equivalência de dois objetos depende do estado de cada um. Portanto, cabe ao objeto saber comparar o seu estado com o de outro objeto. Para que isto seja possível, o objeto deve conter um método, ou operador, que permita essa comparação.

Esse método normalmente é chamado equals e tem uma assinatura equals(Object): boolean. O método que recebe um objeto qualquer e retorna um valor logico dependendo se o objeto passado é equivalente a este objeto, ou não.

Se o objeto não tem uma ordem específica, então equivalência e igualdade são sinônimos. Este é o caso da maioria dos objetos com que lidamos.

Integer a = new Integer(1);
Integer b = new Integer(1);
Integer c = new Integer(2);

assertTrue(a.equals(b)); // O objeto em `a` é equivalente ao objeto em `b`.
assertFalse(a.equals(c)); // O objeto em `a` não é equivalente ao objeto em `c`.

O fato de equals ser um método presente em qualquer objeto permite que a lógica que compara o objeto seja modificada conforme o tipo de objeto. Este fato é importantíssimo porque assim podemos ter controlo sobre o tipo de comparação que fazemos, mesmo quando o objeto não foi codificado por nós. Esta forma de comparação de objetos é principalmente importante quando agrupamos os objetos em coleções já que algumas coleções se baseiam no conceito de equivalência.

Propriedades da Equivalência

A equivalência é:

  • Reflecxiva - um objeto é equivalente a si mesmo

  • Simétrica - o resultado de x.equals(y) é o mesmo que y.equal(x)

  • Transitiva - se é verdade que x.equals(y) e que y.equals(z) então é verdade que x.equals(z)

  • Consistente - o resultado de equals só deve se modificar se uma propriedade contida no cálculo de equals for modificada. Ou seja, não é permitida aleatoriedade na resposta de equals.

Estas são propriedades matemáticas que as implementações do método devem obedecer. Para linguagens em que é possível que uma variável não referencie nada, a simetria não é perfeita. Para estas linguagens a simetri é definida como:

  • Simétrica - o resultado de x.equals(y) é o mesmo que y.equal(x) se y != null. Se y == null então x.equals(null) é false e y.equals(x) resulta no lançamento de uma exceção.

O hashCode

É comum que as linguagens tenham no seu SDK classes que representam coleções. Algumas coleções, especialmente Set e Map precisam de uma forma de comprar os objetos para saber se eles são iguais. Para isso é usado o método equals quando ele está disponível.

Mas mais do que comparar objetos Set e Map são otimizadas para encontrar objetos rapidamente. Para ajudar neste velocidade as linguagens com classes de coleções definem um segundo método - normalmente chamado hashCode - de tal forma que:

x.equals(y) ⇒ x.hashCode() == y.hashCode()

Isto significa que se x e y são iguais conforme equals então o seu hashCode é o mesmo. Repare que o inverso não é verdade. Objetos x e y diferentes conforme equals podem ter hashCode iguais.

É necessário, que ao definir a implementação do método equals de uma classe, também a implementação do método hashCode seja implementada coerentemente com a regra acima. Esta não é apenas uma boa prática, é uma necessidade para o bom funcionamento do software.

JavaScript e TypeScript

A plataforma JavaScript - utilizada pelas linguagens JavaScript e TypeScript - define classes de coleções Set`e `Map contudo a plataforma e as linguagens não definem os métodos equals e hashCode. Nesta plataforma a igualdade dos objetos tem regras um pouco mais complexas que dependem da definição do método toString esse sim, definido pela plataforma.

Comparando categorias

Para variáveis de referência é muitas vezes importante saber se o objeto referenciado por elas é de uma certa categoria, ou compatível com alguma categoria. A categoria é definida pelos tipos do objeto (classes e interfaces que ele herda ou implementa). Para verificar se um certo objeto é de uma certa categoria utilizamos um operador definido pela linguagem. Em Java é o operador instanceof, em C# e Dart é o operador is.

Integer a = new Integer(1);
Number b = new Integer(1);
Number c = new Double(1);

assertTrue(a instanceof Integer); // O objeto em `a` é um Integer
assertTrue(a instanceof Number); // O objeto em `a` é um Number porque todo o Integer é um Number

assertTrue(b instanceof Integer); // O objeto em `b`,também, é um Integer

É bom lembrar a esta altura que aquilo que é usado na comparação é o tipo do objeto referenciado pelo conteúdo da variável e não o tipo da variável em si. Por isso, mesmo b sendo definido como do tipo Number o objeto real referenciado é um Integer.

Comparando a Ordem

Alguns objetos têm uma ordem intrínseca para os seus valores. O exemplo clássico são os objetos que representam números, mas também datas e sequências de caracteres têm uma ordem intrínseca. Para estes tipos de objetos é comum termos que saber se um certo objeto tem valor menor ou maior que outro. Não existem operadores para comparar a ordem dos objetos diretamente. A principal razão para isto é que é necessário definir condições que testam esta igualdade. Portanto, é mais simples deixar essa responsabilidade para o objeto.

A responsabilidade de testar a ordem deve ser do próprio objeto, se existir uma ordem natural única para esse objeto. Neste caso ele deve implementar uma interface - normalmente chamada Comparable. Caso não exista uma ordem natural, ou exista mais do que uma ordem possível para o objeto, será necessário um objeto diferente deve implementar outra interface - normalmente chamada Comparator Cada comparador é usado para impor uma ordem específica.

Comparando a Ordem de Números

A comparação do valor da variável é feita utilizando operadores definidos pela própria linguagem. A identidade de valores primitivos é testada pelos operadores == (idêntico) e != (diferente).

Para números a comparação da igualdade, equivalência e identidade é realizada pelo mesmo operador, já que para números elas são indistinguíveis. O operador de comparação de identidade é, portanto, utilizado também para comparar a igualdade dos valores e consequentemente a sua equivalência.

Para núemros é ainda possível utilizar os operadores de comparação de ordem: < (menor que), > (maior que), (menor ou igual) e >= (maior ou igual). As regras associadas à comparação são dependentes do tipo de variável em questão. (ver Números)

Algoritmo de Comparação para Objetos

Vimos que para números podemos utilizar os operadores de ordem para a comparação. Tal como em:

 int a = 2 ;
 int b = 3 ;

 assertTrue(a <= b) ;

Podemos pensar em outra forma de comparar os valores de a e b usando a operação de subtração.

 int a = 2 ;
 int b = 3 ;

 assertTrue ( a-b <= 0 ) ;

Com esta simples alteração matemática passamos a comparar com o núemro zero. Isto é muito prático porque zero é uma constante que não depende de a ou b. Assim, não importa o que está do lado esquerdo do operador, sempre podemos usar zero, do lado direito.

Agora podemos encapsular a operação num método, assim:

 int a = 2 ;
 int b = 3 ;

 assertTrue ( compare(a, b) <=0 ) ;

Desta forma, consideramos que compare irá realizar a subtração entre a e b e devolver um valor. Este valor será zero se a é igual a b. O valor será negativo se a é menor que b e será positivo se a é maior que b.

Com esta definição, podemos trocar a definição de a e b para objetos sem que a nossa forma de comparação seja alterada. O método irá retornar um número, mas a e b não têm que ser números.

 String a = "A" ;
 String b = "Z" ;

 assertTrue ( compare(a, b) <=0 ) ;

A forma de comparação poderia ser utiliza para qualquer tipo de objeto. O algoritmo é simples:

  1. Um método - normalmente chamado compareTo ou compare - é utilizado para comparar os objetos

  2. Se o valor é zero, os objetos são considerados iguais, ou seja, nenhum é maior, ou menor que o outro.

  3. Se o valor é positivo, o objeto do primeiro argumento é maior que o do segundo

  4. Se o valor é negativo, o objeto do primeiro argumento é menor que o do segundo

A implementação de compare() é baseada no conceito de que: estamos logicamente subtraindo os valores dos objetos como se eles fossem números.

O algoritmo de comparação é encapsulado pelos objetos que implementam Comparable ou Comparator.

Quando Igualdade não significa Equivalência

Vimos como a equivalência se relaciona com a igualdade na ausência de ordem. Vejamos agora como se relaciona se a ordem estiver presente.

Pensemos no caso mais simples de um objeto que implemente Comparable temos agora duas formas de saber se dois objetos são iguais:

 // podemos usar equals()
 assertTrue ( a.equals(b)) ;

 // ou usar compareTo()
 assertTrue ( a.compareTo(b) == 0 ) ;

Acontece que estes métodos não significam o mesmo, porque os objetos podem ser equivalentes sem serem iguais. Imaginemos uma classe Rational definida assim:

class Rational {
  int numerator;
  int denominator;

  Rational(int numerator, int denominator){
    this.numerator = numerator;
    this.denominator = denominator;

...
  }

...
}

Vamos assumir que esta classe têm outros métodos para realizar operações e que deteta erros e lança exceções adquadamente. Assim, podemos escrever

Rational a = Rational(2, 6); // dois sextos
Rational b = Rational(1, 3); // um terço

Se usamos a.equals(b) vamos obter false pois o numerador e denominador não são iguais. Já se usarmos a.compareTo(b) vamos obter 0 significando que são equivalentes.

Quando acontece isto, dizemos que a classe não implementa compareTo() de forma compatível com equals.

Portanto, como todos os objetos implementam equals mas apenas alguns implementam Comparable ao implementar compareTo é preciso especificar se ele é compatível, ou não, com equals . O método compareTo diz-se compatível com equals se , e apenas se, for verdade que:

se x.equals(y) então x.compareTo(y) == 0 e x.compareTo(y) == 0 então x.equals(y)

Em cada SDK de cada linguagem é necessário conhecer quais objetos têm implementações incompatíveis. Em Java, por exemplo, BigDecimal , Double e Float têm compareTo não compatível com equals.

Se queremos saber se um objeto é matematicamente igual a outro devemos sempre comparar usando compareTo e nunca equals.

Como boa prática tente sempre que as suas classes tenham a implementação de compareTo compatível com equals, isso remove confusões e bugs. Por exempplo, no caso do exemplo de Rational se reduzimo sempre à fração mais baixa teremos ambas as propriedades consistentes entre si.

Números de Ponto Flutuante

Tenha sempre atenção que implementações de números de ponto flutuante, em qualquer linguagem, tendem a ter compareTo não compatível com equals. Isto significa que não é uma boa prática comparar igualdade de duas variáveis deste tipo usando == ou equals. Igualdade sempre deve ser realizada usando x.compareTo(y) == 0.

Resumo

A tabela a seguir resume como operar sobre dois objetos para conhecer a relação entre eles:

Relação

Operadores

Método

Identidade

== e !=

Ordem

> , <, <=, >=

compareTo ou compare

Equivalência

equals()

Igualdade

compareTo() == 0 ou compare() == 0

Categorização

instanceof ou is

A seguir o resumo das implicações entre as propriedades

  • Identidade implica Equivalência - se a == b então é verdade que a.equals(b)

  • Identidade implica Igualdade - se a == b então é verdade que a.compareTo(b) == 0

  • Equivalência implica Igualdade - se a.equals(b) então é verdade que a.compareTo(b) == 0

  • Equivalência não implica Identidade - se a.equals(b) não implica que a == b

  • Igualdade não implica Identidade - se a.compareTo(b) == 0 não implica que a == b

  • Igualdade não implica Equivalência - se a.compareTo(b) == 0 não implica que a.equals(b) == 0

Scroll to Top