5 errores que comete el 99% de los desarrolladores de Java

Fuente: Medio En esta publicación, conocerá los errores más comunes que cometen muchos desarrolladores de Java. Pausa para el café #146.  5 errores que comete el 99% de los desarrolladores de Java.  Cadenas en Java - vista interior - 1Como programador de Java, sé lo malo que es dedicar mucho tiempo a corregir errores en el código. A veces esto lleva varias horas. Sin embargo, muchos errores aparecen debido al hecho de que el desarrollador ignora las reglas básicas, es decir, se trata de errores de muy bajo nivel. Hoy veremos algunos errores de codificación comunes y luego explicaremos cómo solucionarlos. Espero que esto te ayude a evitar problemas en tu trabajo diario.

Comparar objetos usando Objects.equals

Supongo que estás familiarizado con este método. Muchos desarrolladores lo utilizan con frecuencia. Esta técnica, introducida en JDK 7, le ayuda a comparar objetos rápidamente y evitar eficazmente la molesta comprobación de puntero nulo. Pero este método a veces se utiliza incorrectamente. Esto es lo que quiero decir:
Long longValue = 123L;
System.out.println(longValue==123); //true
System.out.println(Objects.equals(longValue,123)); //false
¿Por qué reemplazar == con Objects.equals() produciría un resultado incorrecto? Esto se debe a que el compilador == obtendrá el tipo de datos subyacente correspondiente al tipo de empaquetado longValue y luego lo comparará con ese tipo de datos subyacente. Esto equivale a que el compilador convierta automáticamente constantes al tipo de datos de comparación subyacente. Después de usar el método Objects.equals() , el tipo de datos base predeterminado de la constante del compilador es int . A continuación se muestra el código fuente de Objects.equals() donde a.equals(b) usa Long.equals() y determina el tipo de objeto. Esto sucede porque el compilador asumió que la constante era de tipo int , por lo que el resultado de la comparación debe ser falso.
public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));
    }

  public boolean equals(Object obj) {
        if (obj instanceof Long) {
            return value == ((Long)obj).longValue();
        }
        return false;
    }
Conociendo el motivo, corregir el error es muy sencillo. Simplemente declare el tipo de datos de las constantes, como Objects.equals(longValue,123L) . Los problemas anteriores no surgirán si la lógica es estricta. Lo que debemos hacer es seguir reglas de programación claras.

Formato de fecha incorrecto

En el desarrollo diario, a menudo es necesario cambiar la fecha, pero muchas personas usan el formato incorrecto, lo que genera cosas inesperadas. He aquí un ejemplo:
Instant instant = Instant.parse("2021-12-31T00:00:00.00Z");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm:ss")
.withZone(ZoneId.systemDefault());
System.out.println(formatter.format(instant));//2022-12-31 08:00:00
Esto utiliza el formato AAAA-MM-dd para cambiar la fecha de 2021 a 2022. No deberías hacer eso. ¿Por qué? Esto se debe a que el patrón Java DateTimeFormatter “AAAA” se basa en el estándar ISO-8601, que define el año como el jueves de cada semana. Pero el 31 de diciembre de 2021 cayó en viernes, por lo que el programa indica incorrectamente 2022. Para evitar esto, debes utilizar el formato aaaa-MM-dd para formatear la fecha . Este error ocurre con poca frecuencia, sólo con la llegada del nuevo año. Pero en mi empresa provocó un fallo de producción.

Usando ThreadLocal en ThreadPool

Si crea una variable ThreadLocal , un hilo que acceda a esa variable creará una variable local de hilo. De esta manera puede evitar problemas de seguridad de subprocesos. Sin embargo, si utiliza ThreadLocal en un grupo de subprocesos , debe tener cuidado. Su código puede producir resultados inesperados. Para un ejemplo simple, digamos que tenemos una plataforma de comercio electrónico y los usuarios deben enviar un correo electrónico para confirmar la compra completa de productos.
private ThreadLocal<User> currentUser = ThreadLocal.withInitial(() -> null);

    private ExecutorService executorService = Executors.newFixedThreadPool(4);

    public void executor() {
        executorService.submit(()->{
            User user = currentUser.get();
            Integer userId = user.getId();
            sendEmail(userId);
        });
    }
Si utilizamos ThreadLocal para guardar información del usuario, aparecerá un error oculto. Debido a que se utiliza un grupo de subprocesos y los subprocesos se pueden reutilizar, cuando se utiliza ThreadLocal para obtener información del usuario, es posible que muestre erróneamente la información de otra persona. Para resolver este problema, debes usar sesiones.

Utilice HashSet para eliminar datos duplicados

Al codificar, a menudo tenemos la necesidad de deduplicar. Cuando piensas en deduplicación, lo primero que mucha gente piensa es en usar un HashSet . Sin embargo, el uso descuidado de HashSet puede provocar que falle la deduplicación.
User user1 = new User();
user1.setUsername("test");

User user2 = new User();
user2.setUsername("test");

List<User> users = Arrays.asList(user1, user2);
HashSet<User> sets = new HashSet<>(users);
System.out.println(sets.size());// the size is 2
Algunos lectores atentos deberían poder adivinar el motivo del fallo. HashSet usa un código hash para acceder a la tabla hash y usa el método igual para determinar si los objetos son iguales. Si el objeto definido por el usuario no anula el método de código hash y el método igual , entonces el método de código hash y el método igual del objeto principal se utilizarán de forma predeterminada. Esto hará que HashSet asuma que son dos objetos diferentes, lo que provocará que falle la deduplicación.

Eliminando un hilo de piscina "comido"

ExecutorService executorService = Executors.newFixedThreadPool(1);
        executorService.submit(()->{
            //do something
            double result = 10/0;
        });
El código anterior simula un escenario en el que se lanza una excepción en el grupo de subprocesos. El código comercial debe asumir varias situaciones, por lo que es muy probable que arroje una RuntimeException por algún motivo . Pero si no hay un manejo especial aquí, el grupo de subprocesos "devorará" esta excepción. Y ni siquiera tendrás forma de comprobar la causa de la excepción. Por lo tanto, es mejor detectar excepciones en el grupo de procesos.

Cadenas en Java - vista interior

Fuente: Medio El autor de este artículo decidió analizar detalladamente la creación, funcionalidad y características de las cadenas en Java. Pausa para el café #146.  5 errores que comete el 99% de los desarrolladores de Java.  Cadenas en Java - vista interior - 2

Creación

Una cadena en Java se puede crear de dos maneras diferentes: implícitamente, como una cadena literal, y explícitamente, usando la nueva palabra clave . Los literales de cadena son caracteres encerrados entre comillas dobles.
String literal   = "Michael Jordan";
String object    = new String("Michael Jordan");
Aunque ambas declaraciones crean un objeto de cadena, existe una diferencia en cómo se ubican ambos objetos en la memoria del montón.

Representación interna

Anteriormente, las cadenas se almacenaban en el formato char[] , lo que significaba que cada carácter era un elemento separado en la matriz de caracteres. Dado que estaban representados en el formato de codificación de caracteres UTF-16 , esto significaba que cada carácter ocupaba dos bytes de memoria. Esto no es muy correcto, ya que las estadísticas de uso muestran que la mayoría de los objetos de cadena constan únicamente de caracteres Latin-1 . Los caracteres Latin-1 se pueden representar usando un solo byte de memoria, lo que puede reducir significativamente el uso de la memoria, hasta en un 50%. Se implementó una nueva función de cadena interna como parte de la versión JDK 9 basada en JEP 254 llamada Compact Strings. En esta versión, char[] se cambió a byte[] y se agregó un campo de indicador de codificador para representar la codificación utilizada (Latin-1 o UTF-16). Después de esto, se produce la codificación según el contenido de la cadena. Si el valor contiene solo caracteres Latin-1, entonces se usa la codificación Latin-1 (la clase StringLatin1 ) o la codificación UTF-16 (la clase StringUTF16 ).

Asignación de memoria

Como se indicó anteriormente, existe una diferencia en la forma en que se asigna la memoria para estos objetos en el montón. Usar la nueva palabra clave explícita es bastante sencillo ya que la JVM crea y asigna memoria para la variable en el montón. Por lo tanto, el uso de una cadena literal sigue un proceso llamado pasantía. La pasantía de cuerdas es el proceso de poner cuerdas en un grupo. Utiliza un método para almacenar solo una copia de cada valor de cadena individual, que debe ser inmutable. Los valores individuales se almacenan en el grupo String Intern. Este grupo es un almacén Hashtable que almacena una referencia a cada objeto de cadena creado usando literales y su hash. Aunque el valor de la cadena está en el montón, su referencia se puede encontrar en el grupo interno. Esto se puede verificar fácilmente mediante el siguiente experimento. Aquí tenemos dos variables con el mismo valor:
String firstName1   = "Michael";
String firstName2   = "Michael";
System.out.println(firstName1 == firstName2);             //true
Durante la ejecución del código, cuando la JVM encuentra firstName1 , busca el valor de la cadena en el grupo de cadenas interno Michael . Si no puede encontrarlo, se crea una nueva entrada para el objeto en el grupo interno. Cuando la ejecución llega a firstName2 , el proceso se repite nuevamente y esta vez el valor se puede encontrar en el grupo en función de la variable firstName1 . De esta manera, en lugar de duplicar y crear una nueva entrada, se devuelve el mismo enlace. Por tanto, se cumple la condición de igualdad. Por otro lado, si se crea una variable con el valor Michael usando la nueva palabra clave, no se produce ninguna internación y no se cumple la condición de igualdad.
String firstName3 = new String("Michael");
System.out.println(firstName3 == firstName2);           //false
La pasantía se puede utilizar con el método intern() firstName3 , aunque normalmente no se prefiere.
firstName3 = firstName3.intern();                      //Interning
System.out.println(firstName3 == firstName2);          //true
La internación también puede ocurrir al concatenar dos cadenas literales usando el operador + .
String fullName = "Michael Jordan";
System.out.println(fullName == "Michael " + "Jordan");     //true
Aquí vemos que en tiempo de compilación, el compilador agrega ambos literales y elimina el operador + de la expresión para formar una sola cadena como se muestra a continuación. En tiempo de ejecución, tanto el nombre completo como el "literal agregado" se internan y se cumple la condición de igualdad.
//After Compilation
System.out.println(fullName == "Michael Jordan");

Igualdad

En los experimentos anteriores, puede ver que de forma predeterminada solo se internan cadenas literales. Sin embargo, una aplicación Java ciertamente no tendrá sólo cadenas literales, ya que puede recibir cadenas de diferentes fuentes. Por lo tanto, no se recomienda utilizar el operador de igualdad y puede producir resultados no deseados. Las pruebas de igualdad sólo deben realizarse mediante el método de iguales . Realiza igualdad basándose en el valor de la cadena en lugar de en la dirección de memoria donde está almacenada.
System.out.println(firstName1.equals(firstName2));       //true
System.out.println(firstName3.equals(firstName2));       //true
También hay una versión ligeramente modificada del método igual llamada equalsIgnoreCase . Puede resultar útil para fines que no distinguen entre mayúsculas y minúsculas.
String firstName4 = "miCHAEL";
System.out.println(firstName4.equalsIgnoreCase(firstName1));  //true

Inmutabilidad

Las cadenas son inmutables, lo que significa que su estado interno no se puede cambiar una vez creadas. Puede cambiar el valor de una variable, pero no el valor de la cadena en sí. Cada método de la clase String que se ocupa de manipular un objeto (por ejemplo, concat , substring ) devuelve una nueva copia del valor en lugar de actualizar el valor existente.
String firstName  = "Michael";
String lastName   = "Jordan";
firstName.concat(lastName);

System.out.println(firstName);                       //Michael
System.out.println(lastName);                        //Jordan
Como puede ver, no se producen cambios en ninguna de las variables: ni nombre ni apellido . Los métodos de clase String no cambian el estado interno, crean una nueva copia del resultado y devuelven el resultado como se muestra a continuación.
firstName = firstName.concat(lastName);

System.out.println(firstName);                      //MichaelJordan