Os conceitos de igualdade em Java são expandidos pelo conceito de Tipo Primitivo que em outras linguagens não existe.
Uma variável, em Java, é composta por um valor e um tipo. O fato de todas as variáveis terem um tipo torna a plataforma fortemente tipada e por consequência mais segura, pois não ha como, inadvertidamente, designar um valor de um tipo a uma variável de tipo diferente. Contudo, em alguns casos, é possível explicitamente fazer conversões usando operações de cast.
Em Java uma variável só contém um conjunto de bytes. Este conjunto de bytes pode representar um valor, ou uma referência. Saberemos que é um valor ou uma referência pelo tipo da variável.
Um valor pode representar um número ou um valor logico ( true
, ou false
), este tipo de variável chama-se Variável de valor primitivo ou simplesmente Variável primitiva.
Uma referência representa um endereço de localização de um objeto. Diz-se que a variável se refere a um objeto e a este tipo de variável chama-se Variável de Referência. Em particular, variáveis de referência podem não referir nenhum objeto e, nesse caso, o seu valor é valor especial conhecido como nulo ( null
). Quando o valor da variável é nulo diz que a variável está vazia.
Comparar variáveis primitivas ou comparar os seus valores primitivos, dá no mesmo, já que o conteúdo da variável é o próprio valor e todas as variáveis primitivas têm valores de inicialização (zero para números e false
para valores lógicos).
int um = 1; // variável inteira com conteúdo 1
int dois = 2; // variável inteira com conteúdo 2
int tres = 3; // variável inteira com conteúdo 3
assertTrue ( um == um); // 1 é identicamente igual a 1
assertTrue ( um != dois); // 1 é diferente de 2
assertFalse ( um != um); // 1 não é diferente de 1
assertTrue ( um < dois); // 1 é menor que 2
assertTrue ( tres > dois); // 3 é maior que 2
assertTrue ( tres >= dois); // 3 é maior ou igual a 2
assertTrue ( tres >= tres); // 3 é maior ou igual a 3
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 valores primitivos a comparação da igualdade, equivalência e identidade é realizada pelo mesmo operador.
Para as variáveis numéricas é 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 Trabalhando com Números)
A comparação de variáveis de referencia é mais limitada que a de variáveis primitivas. Basicamente estamos interessados em saber se a variável está, ou não, preenchida e se a referência é idêntica, ou não, à de outra variável.
Integer a = new Integer(1);
Integer b = new Integer(1);
Integer c;
assertTrue(c == null); // c não referencia objeto algum
assertTrue(a != null); // a referencia um objeto
assertTrue(a != b); // a não referencia o mesmo objeto que b
O exemplo anterior mostra como variáveis podem referenciar objetos diferentes mesmo quando os objeto, em si mesmos, representam o mesmo valor.
Tipos primitivos de valor não têm identidade. Para os tipos de referência a identidade é comparada com o operador ==
e a sua negação !=
, que comparam se a referência é a mesma. Se a referência é a mesma é garantido que o objeto é o mesmo. Se as variáveis referenciam o mesmo objeto, então os objetos são, necessariamente, idênticos.
A equivalência de dois objetos, em Java, é testada pelo método equals(Object)
, herdado de Object
, que recebe um objeto qualquer e retorna um valor logico dependendo se o objeto passado é equivalente a este objeto, ou não.
Integer a = new Integer(1);
Integer b = new Integer(1);
Integer c = new Integer(2);
assertTrue(a.equals(b)); // a é equivalente a b.
assertFalse(a.equals(c)); // a não é equivalente a 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 podemos ter controlo sobre o tipo de comparação que estamos fazendo 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 todas as coleções em Java se baseiam nesta operação para verificar se um certo objeto está na coleção.
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 o operador instanceof
.
Opcionalmente podemos utilizar o método isInstance()
da classe Class
para fazer as comparações. Estes métodos são particularmente uteis em circunstâncias em que não sabemos à priori qual a classe que está sendo comparada.
Integer a = new Integer(1);
Number b = new Integer(1);
Number c = new Double(1);
assertTrue(a instanceof Integer); // a é um Integer
assertTrue(a instanceof Number); // a é um Number porque todo o Integer é um Number
assertTrue(b instanceof Integer); // o que é comparado é o objeto
assertTrue(Integer.class.isInstance(a)); // forma alternativa para testa se a é um Integer
assertFalse(c.getClass().isInstance(a)); // a não é do mesmo tipo que c
É 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
.
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 em java. A principal razão para isto é que é necessário definir condições que testam esta igualdade. Portanto, é mais simples deixar essa responsabilidade para um objeto.
A responsabilidade de testar a ordem pode ser do próprio objeto, se existir uma ordem natural única para esse objeto. Neste caso ele deve implementar Comparable
. Caso não exista uma ordem natural, ou exista mais do que uma ordem possível para o objeto. Neste caso um objeto diferente que implemente Comparator
será necessário. Cada comparator é usado para impor uma ordem específica.
A forma de comparação poderia ser utiliza para qualquer tipo de objeto.O algoritmo é simples:
Um método ( compareTo
em Comparable
e compare
em Comparator
) é utilizado para comparar os objetos
Se o valor é zero, os objetos são considerados iguais, ou seja, nenhum é maior, ou menor que o outro.
Se o valor é positivo, o objeto do primeiro argumento é maior que o do segundo
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 subtraindo os valores dos objetos como se eles fossem números.
Um Comparator
permite construir qualquer forma de comparação de dois objetos. É especialmente indicado quando o objeto não tem uma ordem intrínseca ou tem mais do que uma ordem possível. Por exemplo: Produto pode ser ordenado por nome, for fabricante ou por preço.
public interface Comparator<T> {
public int compare ( T a, T b ) ;
}
Por outro lado, se o objeto tem apenas uma ordenação possível é melhor que o próprio objeto implemente a interface Comparable
:
public interface Comparable<T> {
public int compareTo ( T other) ;
}
Pensemos no caso mais simples de um objeto que implemente Comparable
temos agora duas formas parecidas de comparar objetos:
// 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 iguais sem serem equivalentes. Um exemplo simples são os objetos da classe BigDecimal
. Esta classe define um número como um conjunto de algarismos e uma escala que corresponde a uma potência de dez. Assim a quantidade 100 pode ser representada por 1 com escala 2, por 10 com escala 1 ou por 100 com escala 0. Utilizando compareTo()
sempre será comparada a quantidade independentemente da representação e da escala. Já se usarmos equals
a escala e a representação serão comparadas e não a quantidade representada. Quando acontece isto, dizemos que a classe não implementa tem 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)`
Não apenas BigDecimal
mas também 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
.
Tanto os primitivos double
e float
têm regras especiais no que toca a comparação. Isto porque os operadores de comparação atuam sobre a representação binário do número e essa pode ser diferente mesmo quando o número representado é o mesmo. Ou seja, porque um mesmo número de ponto flutuante pode ser representado de formas diferentes, a comparação não é compativel com a equivalência.
Por outro lado, devido à existência de valores especiais para estes tipos como NaN
(Not a Number-"não é um número"), -0
(zero negativo) e infinity
não ha garantia que as regras comuns para núemros se apliquem nesses casos. O caso de NaN
é típico do problema porque, por definição NaN
sempre é diferente de si mesmo, o que singifica que uma comparação x == x
pode ser falso se x
for NaN
. Isto é impensável pelas definições comuns de "número" e "igual". Escapa da lógica que utilizamos no dia a dia, embora matematicamente faça todo o sentido.
Para comparar double
e float
é necessário apelar para um método especial que se encontra no respetivo wrapper. O método compare()
de Double
ou Float
permite comparar realmente os valores e não os bits. Este tipo de subtileza é o que torna trabalhar com estes tipos numéricos perigoso, pois a maioria dos programadores irá utilizar o operador ==
sem pensar duas vezes. Quando o programa começar a apresentar erros de cálculo ou ordenação ninguém saberá por quê.
Repare que o problema é com o operador ==
os operadores de comparação <
, >
, >=
e ⇐
dão o resultado correto.
Para os wrapper que estendam Number
a comparação é simples já que Number
implementa Comparable
mas mesmo assim ha armadilhas. As implementações de compareTo
em Double e Float não são compativeis com equals
. Por exemplo, Double
implementa equals
como doubleValue() == doubleValue()
.
Então, mesmo usando wrappers é preciso usar o método especial compareTo
de Double
( ou Float
) para as comparações de igualdade.
// no good
public min(double a, double b){
if (b >= a) { // dangerous.
return a;
}
return b;
}
// good
public min(double a, double b){
if (Double.compate(b, a) >= 0) { // correct
return a;
}
return b;
}
Em Java, o operador ==
testa se os bytes contidos numa variável são os mesmos que os contidos em outra. Isto traz algumas surpresas.
Quando comparamos variáveis de valor de números de ponto flutuante um conjunto diferente de bytes pode representar o mesmo número tornando a comparação com ==
falha.
Quando comparamos variáveis de referência estamos apenas comparando se as variáveis têm a mesma referência, sem comparar nenhuma propriedade do objeto referido.
Para descobrir se dois objetos são equivalentes temos que usar o método equals
.
Vários tipos de objeto são ordenáveis. Classes de objetos com ordem intrínseca devem implementar Comparable
. Classes de objetos com várias ordens não devem implementar Comparable
. Em vez, uma, ou mais, implementações de Comparator
devem ser disponibilizadas à parte.
Quando a classe implementa Comparable
é preciso ter atenção se a implementação de compareTo
é compatível com equals
. Quanto não é, pode ser bem confuso trabalhar com esses objetos, então como regra, se um objeto é Comparable
sempre compare usando compareTo
e nunca compare com equals
. Assim sempre terá o resultado correto.