JavaRush /Blog Java /Random-ES /Comparación de objetos: práctica.
articles
Nivel 15

Comparación de objetos: práctica.

Publicado en el grupo Random-ES
Este es el segundo de los artículos dedicados a comparar objetos. El primero de ellos discutió la base teórica de la comparación: cómo se hace, por qué y dónde se utiliza. En este artículo hablaremos directamente sobre comparar números, objetos, casos especiales, sutilezas y puntos no obvios. Más precisamente, esto es de lo que hablaremos:
Comparación de objetos: práctica - 1
  • Comparación de cadenas: ' ==' yequals
  • MétodoString.intern
  • Comparación de primitivas reales.
  • +0.0Y-0.0
  • SignificadoNaN
  • Java 5.0. Generando métodos y comparación vía ' =='
  • Java 5.0. Autoboxing/Unboxing: ' ==', ' >=' y ' <=' para envoltorios de objetos.
  • Java 5.0. comparación de elementos de enumeración (tipo enum)
¡Entonces empecemos!

Comparación de cadenas: ' ==' yequals

Ah, estas líneas... Uno de los tipos más utilizados, que causa muchos problemas. En principio, hay un artículo aparte sobre ellos . Y aquí tocaré cuestiones de comparación. Por supuesto, las cadenas se pueden comparar usando equals. Además, DEBEN compararse mediante equals. Sin embargo, hay sutilezas que vale la pena conocer. En primer lugar, cadenas idénticas son en realidad un único objeto. Esto se puede verificar fácilmente ejecutando el siguiente código:
String str1 = "string";
String str2 = "string";
System.out.println(str1==str2 ? "the same" : "not the same");
El resultado será el mismo" . Lo que significa que las referencias de cadenas son iguales. Esto se hace a nivel del compilador, obviamente para ahorrar memoria. El compilador crea UNA instancia de la cadena y asigna str1una str2referencia a esta instancia. Sin embargo, esto sólo se aplica a cadenas declaradas como literales en el código. Si compones una cadena a partir de piezas, el enlace a ella será diferente. Confirmación: este ejemplo:
String str1 = "string";
String str2 = "str";
String str3 = "ing";
System.out.println(str1==(str2+str3) ? "the same" : "not the same");
El resultado será "no el mismo" . También puedes crear un nuevo objeto usando el constructor de copia:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1==str2 ? "the same" : "not the same");
El resultado también será "no el mismo" . Por lo tanto, a veces las cadenas se pueden comparar mediante comparación de referencias. Pero es mejor no confiar en esto. Me gustaría mencionar un método muy interesante que le permite obtener la llamada representación canónica de una cadena: String.intern. Hablemos de ello con más detalle.

Método String.interno

Comencemos con el hecho de que la clase Stringadmite un grupo de cadenas. Todos los literales de cadena definidos en clases, y no solo ellos, se agregan a este grupo. Entonces, el método internle permite obtener una cadena de este grupo que es igual a la existente (aquel en la que se llama el método intern) desde el punto de vista de equals. Si dicha fila no existe en el grupo, la existente se coloca allí y se devuelve un enlace a ella. Por lo tanto, incluso si las referencias a dos cadenas iguales son diferentes (como en los dos ejemplos anteriores), las llamadas a estas cadenas interndevolverán una referencia al mismo objeto:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1.intern()==str2.intern() ? "the same" : "not the same");
El resultado de ejecutar este fragmento de código será "el mismo" . No puedo decir exactamente por qué se hizo de esta manera. El método internes nativo y, para ser honesto, no quiero adentrarme en la naturaleza del código C. Lo más probable es que esto se haga para optimizar el consumo de memoria y el rendimiento. En cualquier caso, vale la pena conocer esta característica de implementación. Pasemos a la siguiente parte.

Comparación de primitivas reales.

Para empezar quiero hacer una pregunta. Muy simple. ¿Cuál es la siguiente suma: 0,3f + 0,4f? ¿Por qué? 0,7f? Vamos a revisar:
float f1 = 0.7f;
float f2 = 0.3f + 0.4f;
System.out.println("f1==f2: "+(f1==f2));
¿Como resultado? ¿Como? Yo también. Para aquellos que no completaron este fragmento, les diré que el resultado será...
f1==f2: false
¿Por qué sucede esto?... Realicemos otra prueba:
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);
Tenga en cuenta la conversión a double. Esto se hace para generar más decimales. Resultado:
f1=0.30000001192092896
f2=0.4000000059604645
f3=0.7000000476837158
f4=0.699999988079071
En rigor, el resultado es predecible. La representación de la parte fraccionaria se realiza mediante una serie finita 2-n, por lo que no es necesario hablar de la representación exacta de un número elegido arbitrariamente. Como puede verse en el ejemplo, la precisión de la representación floates de 7 decimales. Estrictamente hablando, la representación float asigna 24 bits a la mantisa. Así, el número absoluto mínimo que se puede representar usando float (sin tener en cuenta el grado, porque estamos hablando de precisión) es 2-24≈6*10-8. Es con este paso que realmente van los valores en la representación float. Y como hay cuantización, también hay error. De ahí la conclusión: los números en una representación floatsólo pueden compararse con cierta precisión. Yo recomendaría redondearlos al sexto decimal (10-6), o, preferiblemente, comprobar el valor absoluto de la diferencia entre ellos:
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 ));
En este caso, el resultado es alentador:
|f3-f4|<1e-6: true
Por supuesto, la imagen es exactamente la misma que el tipo double. La única diferencia es que se asignan 53 bits para la mantisa, por lo tanto, la precisión de la representación es 2-53≈10-16. Sí, el valor de cuantificación es mucho menor, pero está ahí. Y puede ser una broma cruel. Por cierto, en la biblioteca de pruebas JUnit , en los métodos para comparar números reales, la precisión se especifica explícitamente. Aquellos. el método de comparación contiene tres parámetros: el número, a qué debería ser igual y la precisión de la comparación. Por cierto, me gustaría mencionar las sutilezas asociadas con la escritura de números en formato científico, indicando el grado. Pregunta. ¿Cómo escribir 10-6? La práctica demuestra que más del 80% responde: 10e-6. Mientras tanto, ¡la respuesta correcta es 1e-6! ¡Y 10e-6 es 10-5! En uno de los proyectos pisamos este rastrillo de forma bastante inesperada. Buscaron el error durante mucho tiempo, miraron las constantes 20 veces y nadie tuvo la menor duda sobre su exactitud, hasta que un día, en gran parte por accidente, se imprimió la constante 10e-3 y encontraron dos dígitos después del punto decimal en lugar de los tres esperados. Por tanto, ¡ten cuidado! Vamonos.

+0,0 y -0,0

En la representación de números reales, el bit más significativo tiene signo. ¿Qué pasa si todos los demás bits son 0? A diferencia de los números enteros, donde en tal situación el resultado es un número negativo ubicado en el límite inferior del rango de representación, un número real con solo el bit más significativo establecido en 1 también significa 0, solo que con un signo menos. Por tanto, tenemos dos ceros: +0,0 y -0,0. Surge una pregunta lógica: ¿deben considerarse iguales estos números? La máquina virtual piensa exactamente de esta manera. Sin embargo, estos son dos números diferentes , porque como resultado de las operaciones con ellos se obtienen 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);
... y el resultado:
f1=0.0
f2=-0.0
f1==f2: true
f3=Infinity
f4=-Infinity
Entonces, en algunos casos tiene sentido tratar +0,0 y -0,0 como dos números diferentes. Y si tenemos dos objetos, en uno de los cuales el campo es +0,0 y en el otro -0,0, estos objetos también pueden considerarse desiguales. Surge la pregunta: ¿cómo entender que los números son desiguales si se comparan directamente con una máquina virtual true? La respuesta es esta. Aunque la máquina virtual considera que estos números son iguales, sus representaciones siguen siendo diferentes. Por tanto, lo único que se puede hacer es comparar las opiniones. Y para conseguirlo existen métodos int Float.floatToIntBits(float)y long Double.doubleToLongBits(double), que devuelven una representación de bits en la forma inty longrespectivamente (continuación del ejemplo 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));
El resultado será
i1 (+0.0):0
i2 (-0.0):10000000000000000000000000000000
i1==i2: false
Por lo tanto, si +0,0 y -0,0 son números diferentes, entonces debes comparar las variables reales a través de su representación en bits. Parece que hemos resuelto +0,0 y -0,0. -0,0, sin embargo, no es la única sorpresa. También existe tal cosa como...

valor NaN

NaNrepresenta Not-a-Number. Este valor aparece como resultado de operaciones matemáticas incorrectas, por ejemplo, dividir 0,0 entre 0,0, infinito entre infinito, etc. La peculiaridad de este valor es que no es igual a sí mismo. Aquellos.:
float x = 0.0f/0.0f;
System.out.println("x="+x);
System.out.println("x==x: "+(x==x));
...resultará...
x=NaN
x==x: false
¿Cómo puede resultar esto al comparar objetos? Si el campo del objeto es igual a NaN, entonces la comparación dará false, es decir Se garantiza que los objetos se considerarán desiguales. Aunque, lógicamente, es posible que queramos justo lo contrario. Puede lograr el resultado deseado utilizando el método Float.isNaN(float). Devuelve truesi el argumento es NaN. En este caso, no me basaría en comparar representaciones de bits, porque no está estandarizado. Quizás esto sea suficiente sobre los primitivos. Pasemos ahora a las sutilezas que han aparecido en Java desde la versión 5.0. Y el primer punto que me gustaría tocar es

Java 5.0. Generando métodos y comparación vía ' =='

Hay un patrón en el diseño llamado método de producción.. En ocasiones su uso resulta mucho más rentable que utilizar un constructor. Dejame darte un ejemplo. Creo que conozco bien el objeto shell Boolean. Esta clase es inmutable y solo puede contener dos valores. Es decir, de hecho, para cualquier necesidad, solo dos copias son suficientes. Y si los crea con anticipación y luego simplemente los devuelve, será mucho más rápido que usar un constructor. Existe tal método Boolean: valueOf(boolean). Apareció en la versión 1.4. Se introdujeron métodos de producción similares en la versión 5.0 en las clases Byte, Character, y . Cuando se cargan estas clases, se crean matrices de sus instancias correspondientes a ciertos rangos de valores primitivos. Estos rangos son los siguientes: ShortIntegerLong
Comparación de objetos: práctica - 2
Esto significa que cuando se utiliza el método, valueOf(...)si el argumento cae dentro del rango especificado, siempre se devolverá el mismo objeto. Quizás esto dé algún aumento en la velocidad. Pero al mismo tiempo surgen problemas de tal naturaleza que puede resultar bastante difícil llegar al fondo de ellos. Lea más sobre esto. En teoría, el método de producción valueOfse ha agregado tanto a las clases Floatcomo a Double. Su descripción dice que si no necesita una copia nueva, entonces es mejor usar este método, porque puede dar un aumento de velocidad, etc. etcétera. Sin embargo, en la implementación actual (Java 5.0), se crea una nueva instancia en este método, es decir. No se garantiza que su uso dé un aumento en la velocidad. Además, me resulta difícil imaginar cómo se puede acelerar este método, porque debido a la continuidad de los valores, no se puede organizar un caché allí. Excepto los números enteros. Es decir, sin la parte fraccionaria.

Java 5.0. Autoboxing/Unboxing: ' ==', ' >=' y ' <=' para envoltorios de objetos.

Sospecho que los métodos de producción y el caché de instancias se agregaron a los contenedores de primitivas enteras para optimizar las operaciones autoboxing/unboxing. Déjame recordarte de qué se trata. Si un objeto debe estar involucrado en una operación, pero hay una primitiva involucrada, entonces esta primitiva se envuelve automáticamente en un contenedor de objetos. Este autoboxing. Y viceversa: si una primitiva debe estar involucrada en una operación, entonces puede sustituir un objeto shell allí y el valor se expandirá automáticamente a partir de él. Este unboxing. Naturalmente, tendrás que pagar por tal comodidad. Las operaciones de conversión automática ralentizan un poco el rendimiento de la aplicación. Sin embargo, esto no es relevante para el tema actual, así que dejemos esta pregunta. Todo está bien siempre que estemos tratando con operaciones que estén claramente relacionadas con primitivas o shells. ¿Qué pasará con la ' ==' operación? Digamos que tenemos dos objetos Integercon el mismo valor dentro. ¿Cómo se compararán?
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1==i2: "+(i1==i2));
Resultado:
i1==i2: false

Кто бы сомневался... Сравниваются они Cómo un objetoы. А если так:Integer i1 = 1;
Integer i2 = 1;
System.out.println("i1==i2: "+(i1==i2));
Resultado:
i1==i2: true
¡Ahora esto es más interesante! ¡ Si autoboxing-e se devuelven los mismos objetos! Aquí es donde está la trampa. Una vez que descubramos que se devuelven los mismos objetos, comenzaremos a experimentar para ver si este es siempre el caso. ¿Y cuántos valores comprobaremos? ¿Uno? ¿Diez? ¿Cien? Lo más probable es que nos limitemos a cien en cada dirección alrededor de cero. Y conseguimos igualdad en todas partes. Parecería que todo está bien. Sin embargo, mire un poco atrás, aquí . ¿Has adivinado cuál es el problema? Sí, las instancias de shells de objetos durante el autoboxing se crean utilizando métodos de producción. Esto queda bien ilustrado con la siguiente prueba:
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));
        }
    }
}
El resultado será así:
number=-129: false
number=-128: true
number=127: true
number=128: false
Para los valores que se encuentran dentro del rango de almacenamiento en caché , se devuelven objetos idénticos, para los que están fuera de él, se devuelven objetos diferentes. Y por lo tanto, si en algún lugar de los shells de la aplicación se comparan en lugar de primitivos, existe la posibilidad de obtener el error más terrible: uno flotante. Porque lo más probable es que el código también se pruebe en un rango limitado de valores en los que no aparecerá este error. Pero en el trabajo real, aparecerá o desaparecerá, dependiendo de los resultados de algunos cálculos. Es más fácil volverse loco que encontrar un error así. Por lo tanto, le aconsejaría que evite el autoboxing siempre que sea posible. Y eso no es todo. Recordemos las matemáticas, no más allá del 5º grado. Dejemos que las desigualdades A>=By А<=B. ¿Qué se puede decir sobre la relación Ay B? Sólo hay una cosa: son iguales. ¿Estás de acuerdo? Creo que sí. Ejecutemos la prueba:
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
Y esto es lo más extraño para mí. No entiendo en absoluto por qué se introdujo esta característica en el lenguaje si introduce tales contradicciones. En general, lo repetiré una vez más: si es posible prescindir de autoboxing/unboxing, entonces vale la pena aprovechar esta oportunidad al máximo. El último tema que me gustaría tocar es... Java 5.0. comparación de elementos de enumeración (tipo enum) Como sabe, desde la versión 5.0 Java ha introducido un tipo como enum - enumeración. Sus instancias contienen de forma predeterminada el nombre y el número de secuencia en la declaración de instancia de la clase. En consecuencia, cuando cambia el orden de los anuncios, los números cambian. Sin embargo, como dije en el artículo 'Serialización tal como es' , esto no causa problemas. Todos los elementos de enumeración existen en una sola copia, esto se controla a nivel de máquina virtual. Por tanto, se pueden comparar directamente, mediante enlaces. * * * Quizás eso sea todo por hoy sobre el lado práctico de implementar la comparación de objetos. Quizás me estoy perdiendo algo. Como siempre, espero tus comentarios! Por ahora, déjame irme. ¡Gracias a todos por su atención! Enlace a la fuente: Comparación de objetos: práctica
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION