Java: cosas que pueden parecer curiosas para un desarrollador experimentado

Buen momento del día!

El artículo fue escrito a raíz de la publicación "Cosas que [quizás] no sabías sobre Java" por otro autor, que clasificaría como "para principiantes". Al leerlo y comentarlo, me di cuenta de que aprendí una serie de cosas bastante interesantes, que ya he programado en Java durante más de un año. Quizás estas cosas le parezcan curiosas a alguien más.

Los hechos que, desde mi punto de vista, pueden ser útiles para principiantes, los eliminé en los "spoilers". Algunas cosas aún pueden ser interesantes para los más experimentados. Por ejemplo, yo mismo no supe hasta el momento de escribir que Boolean.hashCode (true) == 1231 y Boolean.hashCode (false) == 1237.

para principiantes
  • Boolean.hashCode (true) == 1231
  • Boolean.hashCode (false) == 1237
  • Float.hashCode (value) == Float.floatToIntBits (value)
  • Double.hashCode (valor): xor de la primera y segunda media palabra de 32 bits Double.doubleToLongBits (valor)

Object.hashCode () ya no es la dirección de un objeto en la memoria


Descargo de responsabilidad: este es un detalle jvm de Oracle (HotSpot).

Érase una vez que esto era así.
Desde jdk1.2.1 / docs / api / java / lang / Object.html # hashCode ():
Por mucho que sea razonablemente práctico, el método hashCode definido por la clase Object devuelve enteros distintos para objetos distintos. (Esto generalmente se implementa convirtiendo la dirección interna del objeto en un número entero, pero el lenguaje de programación JavaTM no requiere esta técnica de implementación).

Luego lo rechazaron. Esto es lo que dice javadoc para jdk 12 .

vladimir_dolzhenko sugirió que el comportamiento anterior se puede restaurar usando -XX: hashCode = 4. Y el cambio de comportamiento en sí fue casi de la versión 1.2 de Java.

Integer.valueOf (15) == Integer.valueOf (15); Integer.valueOf (128)! = Integer.valueOf (128)


Descargo de responsabilidad: esto es parte de jls .

Está claro que cuando se comparan dos contenedores con el operador == (! =), No se produce el autoboxing. En términos generales, es la primera igualdad que confunde. El hecho es que para valores enteros i: -129 <i <128 Los objetos de contenedor enteros se almacenan en caché. Por lo tanto, para i de este rango, Integer.valueOf (i) no crea un objeto nuevo cada vez, sino que devuelve uno ya creado. Para i que no entran en este rango, Integer.valueOf (i) siempre crea un nuevo objeto. Por lo tanto, si no monitorea de cerca qué exactamente y cómo se compara exactamente, puede escribir código que parece funcionar e incluso está cubierto con pruebas, pero que al mismo tiempo contiene un "rastrillo".

En jvm de Oracle (HotSpot), el límite superior de almacenamiento en caché se puede cambiar a través de la propiedad "java.lang.Integer.IntegerCache.high" .

en algunos casos, los valores de los campos estáticos finales primitivos o de cadena de otra clase se resuelven en tiempo de compilación


Suena confuso, y la declaración es un poco larga. El significado es este. Si tenemos una clase que define constantes de tipos o cadenas primitivas como campos estáticos finales con inicialización inmediata,
class AnotherClass { public final static String CASE_1 = "case_1"; public final static String CASE_2 = "case_2"; } 

cuando se usa en otras clases,
 class TheClass { // ... public static int getCaseNumber(String caseName) { switch (caseName) { case AnotherClass.CASE_1: return 1; case AnotherClass.CASE_2: return 2; default: throw new IllegalArgumentException("value of the argument caseName does not belong to the allowed value set"); } } } 

Los valores de estas constantes ("case_1", "case_2") se resuelven en tiempo de compilación. Y se insertan en el código como valores, y no como enlaces. Es decir, si usamos tales constantes de la biblioteca, y luego obtenemos una nueva versión de la biblioteca en la que los valores de las constantes han cambiado, deberíamos volver a compilar el proyecto. De lo contrario, los valores constantes antiguos pueden continuar utilizándose en el código.

Este comportamiento se observa en todos los lugares donde se deben usar expresiones constantes (por ejemplo, switch / case), o el compilador puede convertir expresiones en constantes y él puede hacerlo.

Estos campos no se pueden usar en expresiones constantes tan pronto como eliminemos la inicialización inmediata al transferir la inicialización al bloque estático.

para principiantes

Bajo ciertas condiciones, el recolector de basura nunca puede funcionar.


Como resultado, no se lanzará finalize (). Por lo tanto, no debe escribir código que se base en el hecho de que finalize () siempre funcionará. Sí, y si el objeto se metió en la basura antes del final del programa, lo más probable es que el recolector no lo recoja.

El método finalize () para un objeto específico se puede llamar una vez y solo una vez.


En finalize (), podemos hacer que el objeto sea visible nuevamente, y el recolector de basura no lo "eliminará" esta vez. Cuando este objeto vuelva a caer en la basura, se "compilará" sin llamar a finalize (). Si se lanza una excepción en finalize () y el objeto aún no es visible para nadie, entonces será "ensamblado". Finalize () no se volverá a llamar.

la secuencia en la que se llamará finalize () no se conoce de antemano


Solo se garantiza que este hilo estará libre de bloqueos visibles por el programa principal.

la presencia de un método finalize () anulado en los objetos ralentiza el proceso de recolección de basura


Lo que se encuentra en la superficie es la necesidad de verificar la disponibilidad de los objetos, una vez antes de llamar a finalize (), una vez en una de las siguientes ejecuciones de recolección de basura.

es realmente difícil luchar contra puntos muertos en finalize ()


En finalize no trivial (), pueden ser necesarios bloqueos, lo cual, dados los detalles descritos anteriormente, es muy difícil de depurar.

Object.finalize () ya que la versión 9 de java está marcada como obsoleta!


Lo cual no es sorprendente, dados los detalles descritos anteriormente.

inicialización singleton perezosa clásica: requiere doble bloqueo


Existe una idea errónea sobre este tema de que el siguiente enfoque (expresión de verificación doble), que parece muy lógico, siempre funciona:
 public class UnsafeDCLFactory { private Singleton instance; public Singleton get() { if (instance == null) { // read 1, check 1 synchronized (this) { if (instance == null) { // read 2, check 2 instance = new Singleton(); } } } return instance; // read 3 } } 

Observamos si se crea el objeto (lea 1, verifique 1). Si es así, devuélvelo. De lo contrario, configure el bloqueo, asegúrese de que el objeto no se haya creado, cree el objeto (se elimina el bloqueo) y devuelva el objeto.

El enfoque no funciona por las siguientes razones. (leer 1, verificar 1) y (leer 3) no están sincronizados. Según el concepto del modelo de memoria de Java, los cambios realizados en otro subproceso pueden no ser visibles para nuestro subproceso hasta que sincronicemos. Gracias mk2 por el comentario, aquí está la descripción correcta del problema:
Sí, read1 y read3 no están sincronizados, pero el problema no está en otro hilo. Y el hecho de que las lecturas no sincronizadas se pueden reordenar, es decir read1! = nulo, pero read3 == nulo. Y al mismo tiempo, debido a "instance = new Singleton ();" podemos obtener una referencia al objeto antes de que se haya construido por completo, y esto es realmente un problema de sincronización con otro hilo, pero no read1 y read3, sino read3 y access a miembros de instancia.
Se trata agregando sincronización durante la lectura o marcando la variable en la que vive el enlace al singleton, volátil. (La solución con volátil solo funciona con java 5+. Antes de eso, java tenía un modelo de memoria con incertidumbre en esta situación). Aquí hay una versión funcional (con optimización adicional: se agregó la variable local 'res' para reducir el número de lecturas del campo volátil).
 public class SafeLocalDCLFactory { private volatile Singleton instance; public Singleton getInstance() { Singleton res = instance; // read 1 if (res == null) { // check 1 synchronized (this) { res = instance; // read 2 if (res == null) { // check 2 res = new Singleton(); instance = res; } } } return res; } } 

El código está tomado de aquí , del sitio de Alexei Shipilev. Se pueden encontrar más detalles sobre este problema en él.

"Idioma de titular de inicialización a pedido": una muy hermosa inicialización "perezosa" de singleton


Java inicializa las clases (objetos de clase) solo según sea necesario y, por supuesto, solo una vez. ¡Y puedes aprovechar esto! El mecanismo de idioma del titular de inicialización bajo demanda hace exactamente eso. (El código es de aquí ).
 public class Something { private Something() {} private static class LazyHolder { static final Something INSTANCE = new Something(); } public static Something getInstance() { return LazyHolder.INSTANCE; } } 

La clase LazyHolder solo se inicializará la primera vez que se llame a Something.getInstance (). Jvm se asegurará de que esto suceda solo una vez y, además, de manera muy eficiente: si la clase ya está inicializada, no habrá sobrecarga. En consecuencia, LazyHolder.INSTANCE también se inicializará una vez, "vago" y seguro para subprocesos.
pieza de especificaciones sobre gastos generales
Si este procedimiento de inicialización se completa normalmente y el objeto Class está completamente inicializado y listo para usar, entonces la invocación del procedimiento de inicialización ya no es necesaria y puede eliminarse del código, por ejemplo, parcheándolo o regenerando el código .
Fuente

En general, los singletones no se consideran la mejor práctica.

El material no ha terminado. Entonces, si las manos "alcanzan" y lo que ya se ha escrito tendrá demanda, escribiré de alguna manera más sobre este tema.

Gracias por los comentarios constructivos. Varios lugares en el artículo se ampliaron gracias a sergey-gornostaev , vladimir_dolzhenko , OlehKurpiak , mk2 .

Source: https://habr.com/ru/post/482950/


All Articles