JavaRush /Blogue Java /Random-PT /Comparação de objetos: prática
articles
Nível 15

Comparação de objetos: prática

Publicado no grupo Random-PT
Este é o segundo dos artigos dedicados à comparação de objetos. O primeiro deles discutiu a base teórica da comparação – como é feita, por que e onde é utilizada. Neste artigo falaremos diretamente sobre comparação de números, objetos, casos especiais, sutilezas e pontos não óbvios. Mais precisamente, aqui está o que falaremos:
Comparação de objetos: prática - 1
  • Comparação de strings: ' ==' eequals
  • MétodoString.intern
  • Comparação de primitivas reais
  • +0.0E-0.0
  • SignificadoNaN
  • Java 5.0. Gerando métodos e comparação via ' =='
  • Java 5.0. Autoboxing/Unboxing: ' ==', ' >=' e ' <=' para wrappers de objetos.
  • Java 5.0. comparação de elementos enum (tipo enum)
Então vamos começar!

Comparação de strings: ' ==' eequals

Ah, essas linhas... Um dos tipos mais usados, que causa muitos problemas. Em princípio, há um artigo separado sobre eles . E aqui abordarei questões de comparação. Claro, strings podem ser comparadas usando equals. Além disso, eles DEVEM ser comparados via equals. No entanto, existem sutilezas que vale a pena conhecer. Primeiro de tudo, strings idênticas são na verdade um único objeto. Isso pode ser facilmente verificado executando o seguinte código:
String str1 = "string";
String str2 = "string";
System.out.println(str1==str2 ? "the same" : "not the same");
O resultado será o mesmo" . O que significa que as referências de string são iguais. Isso é feito no nível do compilador, obviamente para economizar memória. O compilador cria UMA instância da string e atribui str1uma str2referência a esta instância. No entanto, isso se aplica apenas a strings declaradas como literais no código. Se você compor um barbante a partir de pedaços, o link para ele será diferente. Confirmação - este exemplo:
String str1 = "string";
String str2 = "str";
String str3 = "ing";
System.out.println(str1==(str2+str3) ? "the same" : "not the same");
O resultado será "não o mesmo" . Você também pode criar um novo objeto usando o construtor de cópia:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1==str2 ? "the same" : "not the same");
O resultado também será "não o mesmo" . Assim, às vezes as strings podem ser comparadas por meio de comparação de referências. Mas é melhor não confiar nisso. Gostaria de abordar um método muito interessante que permite obter a chamada representação canônica de uma string - String.intern. Vamos falar sobre isso com mais detalhes.

Método String.intern

Vamos começar com o fato de que a classe Stringsuporta um pool de strings. Todos os literais de string definidos nas classes, e não apenas eles, são adicionados a este pool. Assim, o método internpermite obter uma string deste pool igual à existente (aquela na qual o método é chamado intern) do ponto de vista de equals. Se tal linha não existir no pool, a existente será colocada lá e um link para ela será retornado. Assim, mesmo que as referências a duas strings iguais sejam diferentes (como nos dois exemplos acima), as chamadas a essas strings internretornarão uma referência ao mesmo objeto:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1.intern()==str2.intern() ? "the same" : "not the same");
O resultado da execução deste trecho de código será "o mesmo" . Não posso dizer exatamente por que foi feito dessa maneira. O método interné nativo e, para ser honesto, não quero entrar na selva do código C. Muito provavelmente isso é feito para otimizar o consumo de memória e o desempenho. De qualquer forma, vale a pena conhecer esse recurso de implementação. Vamos para a próxima parte.

Comparação de primitivas reais

Para começar, quero fazer uma pergunta. Muito simples. Qual é a seguinte soma – 0,3f + 0,4f? Por que? 0,7f? Vamos checar:
float f1 = 0.7f;
float f2 = 0.3f + 0.4f;
System.out.println("f1==f2: "+(f1==f2));
Como resultado? Como? Eu também. Para quem não completou este fragmento, direi que o resultado será...
f1==f2: false
Por que isso está acontecendo?.. Vamos realizar outro teste:
float f1 = 0.3f;
float f2 = 0.4f;
float f3 = f1 + f2;
float f4 = 0.7f;
System.out.println("f1="+(double)f1);
System.out.println("f2="+(double)f2);
System.out.println("f3="+(double)f3);
System.out.println("f4="+(double)f4);
Observe a conversão para double. Isso é feito para gerar mais casas decimais. Resultado:
f1=0.30000001192092896
f2=0.4000000059604645
f3=0.7000000476837158
f4=0.699999988079071
A rigor, o resultado é previsível. A representação da parte fracionária é realizada por meio de uma série finita 2-n e, portanto, não há necessidade de falar sobre a representação exata de um número escolhido arbitrariamente. Como pode ser visto no exemplo, a precisão da representação floaté de 7 casas decimais. A rigor, a representação float aloca 24 bits para a mantissa. Assim, o número absoluto mínimo que pode ser representado usando float (sem levar em conta o grau, porque estamos falando de precisão) é 2-24≈6*10-8. É com esta etapa que os valores na representação realmente vão float. E como existe quantização, também existe um erro. Daí a conclusão: os números em uma representação floatsó podem ser comparados com certa precisão. Eu recomendaria arredondá-los para a 6ª casa decimal (10-6), ou, preferencialmente, verificar o valor absoluto da diferença entre eles:
float f1 = 0.3f;
float f2 = 0.4f;
float f3 = f1 + f2;
float f4 = 0.7f;
System.out.println("|f3-f4|<1e-6: "+( Math.abs(f3-f4) < 1e-6 ));
Neste caso, o resultado é encorajador:
|f3-f4|<1e-6: true
Claro, a imagem é exatamente a mesma com o tipo double. A única diferença é que 53 bits são alocados para a mantissa, portanto, a precisão da representação é 2-53≈10-16. Sim, o valor de quantização é muito menor, mas existe. E pode ser uma piada cruel. A propósito, na biblioteca de testes JUnit , nos métodos de comparação de números reais, a precisão é especificada explicitamente. Aqueles. o método de comparação contém três parâmetros - o número, a que deve ser igual e a precisão da comparação. A propósito, gostaria de mencionar as sutilezas associadas à escrita de números em formato científico, indicando o grau. Pergunta. Como escrever 10-6? A prática mostra que mais de 80% respondem – 10e-6. Enquanto isso, a resposta correta é 1e-6! E 10e-6 é 10-5! Pisamos nesse ancinho em um dos projetos, de forma bastante inesperada. Eles procuraram o erro por muito tempo, olharam as constantes 20 vezes, e ninguém teve a menor dúvida sobre sua correção, até que um dia, em grande parte por acidente, a constante 10e-3 foi impressa e eles encontraram dois dígitos após a vírgula decimal em vez dos três esperados. Portanto, tenha cuidado! Vamos continuar.

+0,0 e -0,0

Na representação de números reais, o bit mais significativo é assinado. O que acontece se todos os outros bits forem 0? Ao contrário dos números inteiros, onde em tal situação o resultado é um número negativo localizado no limite inferior do intervalo de representação, um número real com apenas o bit mais significativo definido como 1 também significa 0, apenas com um sinal de menos. Assim, temos dois zeros - +0,0 e -0,0. Surge uma questão lógica: esses números devem ser considerados iguais? A máquina virtual pensa exatamente assim. Porém, são dois números diferentes , pois como resultado das operações com eles são obtidos valores diferentes:
float f1 = 0.0f/1.0f;
float f2 = 0.0f/-1.0f;
System.out.println("f1="+f1);
System.out.println("f2="+f2);
System.out.println("f1==f2: "+(f1==f2));
float f3 = 1.0f / f1;
float f4 = 1.0f / f2;
System.out.println("f3="+f3);
System.out.println("f4="+f4);
... e o resultado:
f1=0.0
f2=-0.0
f1==f2: true
f3=Infinity
f4=-Infinity
Portanto, em alguns casos, faz sentido tratar +0,0 e -0,0 como dois números diferentes. E se tivermos dois objetos, em um dos quais o campo é +0,0 e no outro -0,0, esses objetos também podem ser considerados desiguais. Surge a pergunta - como você pode entender que os números são desiguais se sua comparação direta com uma máquina virtual dá true? A resposta é esta. Embora a máquina virtual considere esses números iguais, suas representações ainda são diferentes. Portanto, a única coisa que pode ser feita é comparar as opiniões. E para obtê-lo existem métodos int Float.floatToIntBits(float)e long Double.doubleToLongBits(double), que retornam uma representação de bits na forma inte longrespectivamente (continuação do exemplo anterior):
int i1 = Float.floatToIntBits(f1);
int i2 = Float.floatToIntBits(f2);
System.out.println("i1 (+0.0):"+ Integer.toBinaryString(i1));
System.out.println("i2 (-0.0):"+ Integer.toBinaryString(i2));
System.out.println("i1==i2: "+(i1 == i2));
O resultado será
i1 (+0.0):0
i2 (-0.0):10000000000000000000000000000000
i1==i2: false
Assim, se você tem +0,0 e -0,0 são números diferentes, então você deve comparar variáveis ​​​​reais por meio de sua representação em bits. Parece que resolvemos +0,0 e -0,0. -0,0, entretanto, não é a única surpresa. Também existe algo como...

Valor NaN

NaNapoia Not-a-Number. Este valor aparece como resultado de operações matemáticas incorretas, digamos, divisão de 0,0 por 0,0, infinito por infinito, etc. A peculiaridade desse valor é que ele não é igual a si mesmo. Aqueles.:
float x = 0.0f/0.0f;
System.out.println("x="+x);
System.out.println("x==x: "+(x==x));
...resultará...
x=NaN
x==x: false
Como isso pode acontecer ao comparar objetos? Se o campo do objeto for igual a NaN, então a comparação dará false, ou seja, é garantido que os objetos sejam considerados desiguais. Embora, logicamente, possamos querer exatamente o oposto. Você pode alcançar o resultado desejado usando o método Float.isNaN(float). Ele retorna truese o argumento for NaN. Neste caso, eu não confiaria na comparação de representações de bits, porque não é padronizado. Talvez isso seja o suficiente sobre os primitivos. Passemos agora às sutilezas que apareceram em Java desde a versão 5.0. E o primeiro ponto que gostaria de abordar é

Java 5.0. Gerando métodos e comparação via ' =='

Existe um padrão de design chamado método de produção. Às vezes seu uso é muito mais lucrativo do que usar um construtor. Deixe-me lhe dar um exemplo. Acho que conheço bem o shell do objeto Boolean. Esta classe é imutável e pode conter apenas dois valores. Ou seja, para qualquer necessidade bastam apenas duas cópias. E se você criá-los com antecedência e simplesmente devolvê-los, será muito mais rápido do que usar um construtor. Existe um tal método Boolean: valueOf(boolean). Apareceu na versão 1.4. Métodos de produção semelhantes foram introduzidos na versão 5.0 nas classes Byte, Character, e . Quando essas classes são carregadas, são criados arrays de suas instâncias correspondentes a determinados intervalos de valores primitivos. Esses intervalos são os seguintes: ShortIntegerLong
Comparação de objetos: prática - 2
Isso significa que ao usar o método, valueOf(...)se o argumento estiver dentro do intervalo especificado, o mesmo objeto sempre será retornado. Talvez isso proporcione algum aumento na velocidade. Mas, ao mesmo tempo, surgem problemas de tal natureza que pode ser muito difícil chegar ao fundo da questão. Leia mais sobre isso. Em teoria, o método de produção valueOffoi adicionado às classes Floate Double. A descrição deles diz que se você não precisa de uma nova cópia, então é melhor usar este método, porque pode dar um aumento na velocidade, etc. e assim por diante. No entanto, na implementação atual (Java 5.0), uma nova instância é criada neste método, ou seja, Não é garantido que seu uso proporcione aumento de velocidade. Além disso, é difícil para mim imaginar como esse método pode ser acelerado, pois devido à continuidade dos valores, um cache não pode ser organizado ali. Exceto para números inteiros. Quero dizer, sem a parte fracionária.

Java 5.0. Autoboxing/Unboxing: ' ==', ' >=' e ' <=' para wrappers de objetos.

Suspeito que os métodos de produção e o cache da instância foram adicionados aos wrappers de primitivos inteiros para otimizar as operações autoboxing/unboxing. Deixe-me lembrá-lo do que é. Se um objeto deve estar envolvido em uma operação, mas um primitivo está envolvido, então esse primitivo é automaticamente encapsulado em um wrapper de objeto. Esse autoboxing. E vice-versa - se um primitivo deve estar envolvido na operação, então você pode substituir um shell de objeto ali, e o valor será automaticamente expandido a partir dele. Esse unboxing. Naturalmente, você terá que pagar por essa comodidade. As operações de conversão automática tornam o aplicativo um pouco lento. Porém, isso não é relevante para o tema atual, então vamos deixar essa questão. Tudo está bem, desde que estejamos lidando com operações claramente relacionadas a primitivos ou shells. O que acontecerá com a ==operação ''? Digamos que temos dois objetos Integercom o mesmo valor dentro. Como eles serão comparados?
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1==i2: "+(i1==i2));
Resultado:
i1==i2: false

Кто бы сомневался... Сравниваются они How an objectы. А если так:Integer i1 = 1;
Integer i2 = 1;
System.out.println("i1==i2: "+(i1==i2));
Resultado:
i1==i2: true
Agora isso é mais interessante! Se autoboxing-e os mesmos objetos são retornados! É aqui que reside a armadilha. Assim que descobrirmos que os mesmos objetos são retornados, começaremos a experimentar para ver se esse é sempre o caso. E quantos valores iremos verificar? Um? Dez? Cem? Muito provavelmente nos limitaremos a cem em cada direção em torno de zero. E obtemos igualdade em todos os lugares. Parece que está tudo bem. Porém, olhe um pouco para trás, aqui . Você adivinhou qual é o problema? Sim, instâncias de shells de objetos durante o autoboxing são criadas usando métodos de produção. Isto é bem ilustrado pelo seguinte teste:
public class AutoboxingTest {

    private static final int numbers[] = new int[]{-129,-128,127,128};

    public static void main(String[] args) {
        for (int number : numbers) {
            Integer i1 = number;
            Integer i2 = number;
            System.out.println("number=" + number + ": " + (i1 == i2));
        }
    }
}
O resultado será assim:
number=-129: false
number=-128: true
number=127: true
number=128: false
Para valores que estão dentro do intervalo de cache , objetos idênticos são retornados; para aqueles que estão fora dele, objetos diferentes são retornados. E, portanto, se em algum lugar do aplicativo os shells forem comparados em vez dos primitivos, há uma chance de obter o erro mais terrível: um erro flutuante. Porque o código provavelmente também será testado em um intervalo limitado de valores nos quais esse erro não aparecerá. Mas no trabalho real, ele aparecerá ou desaparecerá, dependendo dos resultados de alguns cálculos. É mais fácil enlouquecer do que encontrar tal erro. Portanto, aconselho você a evitar o autoboxing sempre que possível. E não é isso. Vamos lembrar da matemática, não além da 5ª série. Deixe as desigualdades A>=Be А<=B. O que pode ser dito sobre o relacionamento Ae B? Só existe uma coisa: eles são iguais. Você concorda? Acho que sim. Vamos fazer o teste:
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1>=i2: "+(i1>=i2));
System.out.println("i1<=i2: "+(i1<=i2));
System.out.println("i1==i2: "+(i1==i2));
Resultado:
i1>=i2: true
i1<=i2: true
i1==i2: false
E esta é a coisa mais estranha para mim. Não entendo por que esse recurso foi introduzido na linguagem se introduz tais contradições. Em geral, vou repetir mais uma vez - se for possível prescindir autoboxing/unboxing, então vale a pena aproveitar esta oportunidade ao máximo. O último tópico que gostaria de abordar é... Java 5.0. comparação de elementos de enumeração (tipo enum) Como você sabe, desde a versão 5.0 Java introduziu um tipo como enum - enumeração. Suas instâncias, por padrão, contêm o nome e o número de sequência na declaração da instância na classe. Conseqüentemente, quando a ordem do anúncio muda, os números mudam. Porém, como falei no artigo 'Serialization as it is' , isso não causa problemas. Todos os elementos de enumeração existem em uma única cópia, isso é controlado no nível da máquina virtual. Portanto, eles podem ser comparados diretamente, por meio de links. * * * Talvez isso seja tudo por hoje sobre o lado prático da implementação da comparação de objetos. Talvez esteja faltando alguma coisa. Como sempre, estou ansioso por seus comentários! Por enquanto, deixe-me ir embora. Obrigado a todos pela atenção! Link para a fonte: Comparando objetos: prática
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION