JavaRush /Blog Java /Random-ES /FindBugs te ayuda a aprender Java mejor
articles
Nivel 15

FindBugs te ayuda a aprender Java mejor

Publicado en el grupo Random-ES
Los analizadores de código estático son populares porque ayudan a encontrar errores cometidos por descuido. Pero lo que es mucho más interesante es que ayudan a corregir errores cometidos por ignorancia. Incluso si todo está escrito en la documentación oficial del lenguaje, no es un hecho que todos los programadores lo hayan leído atentamente. Y los programadores pueden entenderlo: te cansarás de leer toda la documentación. En este sentido, un analizador estático es como un amigo experimentado que se sienta a tu lado y te observa escribir código. No sólo te dice: “Aquí te equivocaste al copiar y pegar”, sino que también te dice: “No, no puedes escribir así, mira tú mismo la documentación”. Un amigo así es más útil que la documentación misma, porque solo sugiere aquellas cosas que realmente encuentras en tu trabajo y guarda silencio sobre aquellas que nunca te serán útiles. En esta publicación, hablaré sobre algunas de las complejidades de Java que aprendí al usar el analizador estático FindBugs. Quizás algunas cosas también te resulten inesperadas. Es importante que todos los ejemplos no sean especulativos, sino que estén basados ​​en código real.

¿Operador ternario?:

Parecería que no hay nada más sencillo que el operador ternario, pero tiene sus inconvenientes. Creí que no había una diferencia fundamental entre los diseños Type var = condition ? valTrue : valFalse; y Type var; if(condition) var = valTrue; else var = valFalse; resultó que aquí había una sutileza. Dado que el operador ternario puede ser parte de una expresión compleja, su resultado debe ser un tipo concreto determinado en el momento de la compilación. Por lo tanto, digamos, con una condición verdadera en el formulario if, el compilador lleva valTrue directamente al tipo Type, y en forma de operador ternario, primero conduce a los tipos comunes valTrue y valFalse (a pesar de que valFalse no es evaluado), y luego el resultado conduce al tipo Tipo. Las reglas de conversión no son del todo triviales si la expresión involucra tipos primitivos y envoltorios sobre ellos (Entero, Doble, etc.). Todas las reglas se describen en detalle en JLS 15.25. Veamos algunos ejemplos. Number n = flag ? new Integer(1) : new Double(2.0); ¿Qué pasará con n si se establece la bandera? Un objeto Doble con un valor de 1,0. El compilador encuentra divertidos nuestros torpes intentos de crear un objeto. Dado que el segundo y tercer argumento son envoltorios de diferentes tipos primitivos, el compilador los desenvuelve y da como resultado un tipo más preciso (en este caso, doble). Y luego de ejecutar el operador ternario para la asignación, se vuelve a realizar el boxeo. Básicamente, el código es equivalente a esto: Number n; if( flag ) n = Double.valueOf((double) ( new Integer(1).intValue() )); else n = Double.valueOf(new Double(2.0).doubleValue()); desde el punto de vista del compilador, el código no contiene problemas y se compila perfectamente. Pero FindBugs da una advertencia:
BX_UNBOXED_AND_COERCED_FOR_TERNARY_OPERATOR: El valor primitivo se desempaqueta y se fuerza para el operador ternario en TestTernary.main(String[]) Un valor primitivo envuelto se desempaqueta y se convierte a otro tipo primitivo como parte de la evaluación de un operador ternario condicional (el operador b? e1: e2 ). La semántica de Java exige que si e1 y e2 son valores numéricos envueltos, los valores se desempaquetan y se convierten/se coaccionan a su tipo común (por ejemplo, si e1 es de tipo Integer y e2 es de tipo Float, entonces e1 no se desempaqueta, convertido a un valor de coma flotante y encuadrado. Consulte la sección 15.25 de JLS. Por supuesto, FindBugs también advierte que Integer.valueOf(1) es más eficiente que new Integer(1), pero todo el mundo ya lo sabe.
O este ejemplo: Integer n = flag ? 1 : null; el autor quiere poner nulo en n si la bandera no está configurada. ¿Crees que funcionará? Sí. Pero compliquemos las cosas: Integer n = flag1 ? 1 : flag2 ? 2 : null; parecería que no hay mucha diferencia. Sin embargo, ahora, si ambas banderas están claras, esta línea arroja una NullPointerException. Las opciones para el operador ternario derecho son int y null, por lo que el tipo de resultado es Entero. Las opciones para la izquierda son int e Integer, por lo que, según las reglas de Java, el resultado es int. Para hacer esto, necesita realizar unboxing llamando a intValue, lo que genera una excepción. El código es equivalente a este: Integer n; if( flag1 ) n = Integer.valueOf(1); else { if( flag2 ) n = Integer.valueOf(Integer.valueOf(2).intValue()); else n = Integer.valueOf(((Integer)null).intValue()); } Aquí FindBugs genera dos mensajes, que son suficientes para sospechar un error:
BX_UNBOXING_IMMEDIATELY_REBOXED: El valor encuadrado no está encajonado y luego se reencajona inmediatamente en TestTernary.main(String[]) NP_NULL_ON_SOME_PATH: Posible desreferencia del puntero nulo de nulo en TestTernary.main(String[]) Hay una rama de la declaración que, si se ejecuta, garantiza que Se eliminará la referencia al valor nulo, lo que generaría una NullPointerException cuando se ejecute el código.
Bueno, un último ejemplo sobre este tema: double[] vals = new double[] {1.0, 2.0, 3.0}; double getVal(int idx) { return (idx < 0 || idx >= vals.length) ? null : vals[idx]; } no es sorprendente que este código no funcione: ¿cómo puede una función que devuelve un tipo primitivo devolver nulo? Sorprendentemente, se compila sin problemas. Bueno, ya entiendes por qué se compila.

Formato de fecha

Para formatear fechas y horas en Java, se recomienda utilizar clases que implementen la interfaz DateFormat. Por ejemplo, se ve así: public String getDate() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); } A menudo una clase utilizará el mismo formato una y otra vez. A mucha gente se le ocurrirá la idea de optimización: ¿por qué crear un objeto de formato cada vez que se puede utilizar una instancia común? private static final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public String getDate() { return format.format(new Date()); } Es tan hermoso y genial, pero desafortunadamente no funciona. Más precisamente, funciona, pero a veces se estropea. El hecho es que la documentación de DateFormat dice:
Los formatos de fecha no están sincronizados. Se recomienda crear instancias de formato separadas para cada hilo. Si varios subprocesos acceden a un formato al mismo tiempo, se debe sincronizar externamente.
Y esto es cierto si nos fijamos en la implementación interna de SimpleDateFormat. Durante la ejecución del método format(), el objeto escribe en los campos de la clase, por lo que el uso simultáneo de SimpleDateFormat desde dos subprocesos conducirá a un resultado incorrecto con cierta probabilidad. Esto es lo que FindBugs escribe sobre esto:
STCAL_INVOKE_ON_STATIC_DATE_FORMAT_INSTANCE: Llamada al método estático java.text.DateFormat en TestDate.getDate() Como indica JavaDoc, los DateFormats son inherentemente inseguros para uso multiproceso. El detector ha encontrado una llamada a una instancia de DateFormat que se obtuvo a través de un campo estático. Esto parece sospechoso. Para obtener más información sobre esto, consulte el error de Sun n.° 6231579 y el error de Sun n.° 6178997.

Errores del BigDecimal

Habiendo aprendido que la clase BigDecimal te permite almacenar números fraccionarios de precisión arbitraria, y viendo que tiene un constructor para doble, algunos decidirán que todo está claro y puedes hacerlo así: System.out.println(new BigDecimal( 1.1)); En realidad, nadie prohíbe hacer esto, pero el resultado puede parecer inesperado: 1.100000000000000088817841970012523233890533447265625. Esto sucede porque el doble primitivo se almacena en el formato IEEE754, en el que es imposible representar 1,1 con perfecta precisión (en el sistema numérico binario, se obtiene una fracción periódica infinita). Por lo tanto, allí se almacena el valor más cercano a 1,1. Por el contrario, el constructor BigDecimal(double) funciona exactamente: convierte perfectamente un número dado en IEEE754 a forma decimal (una fracción binaria final siempre se puede representar como un decimal final). Si desea representar exactamente 1,1 como BigDecimal, puede escribir new BigDecimal("1.1") o BigDecimal.valueOf(1.1). Si no muestra el número de inmediato, pero realiza algunas operaciones con él, es posible que no comprenda de dónde proviene el error. FindBugs emite una advertencia DMI_BIGDECIMAL_CONSTRUCTED_FROM_DOUBLE, que ofrece el mismo consejo. Aquí hay otra cosa: BigDecimal d1 = new BigDecimal("1.1"); BigDecimal d2 = new BigDecimal("1.10"); System.out.println(d1.equals(d2)); de hecho, d1 y d2 representan el mismo número, pero igual devuelve falso porque compara no solo el valor de los números, sino también el orden actual (el número de decimales). Esto está escrito en la documentación, pero pocas personas leerán la documentación de un método tan familiar como iguales. Es posible que un problema así no surja de inmediato. Desafortunadamente, FindBugs no advierte sobre esto, pero existe una extensión popular para ello: fb-contrib, que tiene en cuenta este error:
Se llama a MDM_BIGDECIMAL_EQUALS igual a() para comparar dos números java.math.BigDecimal. Esto normalmente es un error, ya que dos objetos BigDecimal sólo son iguales si son iguales tanto en valor como en escala, por lo que 2,0 no es igual a 2,00. Para comparar objetos BigDecimal en busca de igualdad matemática, utilice compareTo() en su lugar.

Saltos de línea e impresión

A menudo, los programadores que cambian a Java después de C están felices de descubrir PrintStream.printf (así como PrintWriter.printf , etc.). Genial, sé que, al igual que en C, no necesitas aprender nada nuevo. En realidad hay diferencias. Uno de ellos radica en las traducciones de líneas. El lenguaje C tiene una división en flujos de texto y binarios. Al enviar el carácter '\n' a una secuencia de texto por cualquier medio, se convertirá automáticamente en una nueva línea dependiente del sistema ("\r\n" en Windows). No existe tal separación en Java: se debe pasar la secuencia correcta de caracteres al flujo de salida. Esto se hace automáticamente, por ejemplo, mediante métodos de la familia PrintStream.println. Pero cuando se usa printf, pasar '\n' en la cadena de formato es solo '\n', no una nueva línea dependiente del sistema. Por ejemplo, escribamos el siguiente código: System.out.printf("%s\n", "str#1"); System.out.println("str#2"); Después de redirigir el resultado a un archivo, veremos: FindBugs te ayuda a aprender Java mejor - 1 Por lo tanto, puede obtener una combinación extraña de saltos de línea en un hilo, que parece descuidada y puede sorprender a algún analizador. El error puede pasar desapercibido durante mucho tiempo, especialmente si trabaja principalmente con sistemas Unix. Para insertar una nueva línea válida usando printf, se usa un carácter de formato especial "%n". Esto es lo que FindBugs escribe sobre esto:
VA_FORMAT_STRING_USES_NEWLINE: La cadena de formato debe usar %n en lugar de \n en TestNewline.main(String[]) Esta cadena de formato incluye un carácter de nueva línea (\n). En cadenas de formato, generalmente es preferible usar %n, lo que producirá el separador de línea específico de la plataforma.
Quizás, para algunos lectores, todo lo anterior se sabía desde hacía mucho tiempo. Pero estoy casi seguro de que para ellos habrá una interesante advertencia del analizador estático, que les revelará nuevas características del lenguaje de programación utilizado.
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION