Buscar..


Introducción

Varios usos indebidos del lenguaje de programación Java pueden llevar a cabo un programa para generar resultados incorrectos a pesar de haber sido compilados correctamente. El propósito principal de este tema es enumerar las dificultades comunes con sus causas y proponer la forma correcta de evitar caer en tales problemas.

Observaciones

Este tema trata sobre aspectos específicos de la sintaxis del lenguaje Java que son propensos a errores o que no deben usarse de ciertas maneras.

Pitfall - Ignorar la visibilidad del método

Incluso los desarrolladores Java experimentados tienden a pensar que Java tiene solo tres modificadores de protección. ¡El lenguaje en realidad tiene cuatro! El nivel de visibilidad del paquete privado (también conocido como predeterminado) a menudo se olvida.

Debes prestar atención a los métodos que haces públicos. Los métodos públicos en una aplicación son la API visible de la aplicación. Esto debería ser lo más pequeño y compacto posible, especialmente si está escribiendo una biblioteca reutilizable (vea también el principio SOLID ). Es importante considerar de manera similar la visibilidad de todos los métodos, y usar solo el acceso privado protegido o en paquetes cuando sea apropiado.

Cuando declara métodos que deberían ser privados como públicos, expone los detalles de la implementación interna de la clase.

Un corolario de esto es que solo prueba por unidad los métodos públicos de su clase; de ​​hecho, solo puede probar métodos públicos. Es una mala práctica aumentar la visibilidad de los métodos privados solo para poder ejecutar pruebas unitarias contra esos métodos. La prueba de métodos públicos que llaman a los métodos con una visibilidad más restrictiva debería ser suficiente para probar una API completa. Nunca se debe ampliar su API con los métodos más comunes sólo para permitir las pruebas unitarias.

Pitfall - Falta un 'break' en un caso de 'cambio'

Estos problemas de Java pueden ser muy embarazosos y, a veces, no se han descubierto hasta que se ejecutan en producción. El comportamiento fallido en las declaraciones de cambio suele ser útil; sin embargo, faltar una palabra clave de "ruptura" cuando no se desea tal comportamiento puede llevar a resultados desastrosos. Si ha olvidado poner un "descanso" en el "caso 0" en el ejemplo de código a continuación, el programa escribirá "Cero" seguido de "Uno", ya que el flujo de control que se encuentra aquí pasará por toda la declaración del "interruptor" hasta que Alcanza un “descanso”. Por ejemplo:

public static void switchCasePrimer() {
        int caseIndex = 0;
        switch (caseIndex) {
            case 0:
                System.out.println("Zero");
            case 1:
                System.out.println("One");
                break;
            case 2:
                System.out.println("Two");
                break;
            default:
                System.out.println("Default");
        }
}

En la mayoría de los casos, la solución más limpia sería utilizar interfaces y mover el código con un comportamiento específico a implementaciones separadas ( composición sobre herencia )

Si una declaración de cambio es inevitable, se recomienda documentar los avances "esperados" si se producen. De esa manera, le demuestra a los demás desarrolladores que está al tanto del salto faltante y que este es el comportamiento esperado.

switch(caseIndex) {
    [...]
    case 2:
        System.out.println("Two");
        // fallthrough
    default:
        System.out.println("Default");

Pitfall - punto y coma mal colocados y llaves faltantes

Este es un error que causa confusión real para los principiantes de Java, al menos la primera vez que lo hacen. En lugar de escribir esto:

if (feeling == HAPPY)
    System.out.println("Smile");
else
    System.out.println("Frown");

accidentalmente escriben esto:

if (feeling == HAPPY);
    System.out.println("Smile");
else
    System.out.println("Frown");

y se desconciertan cuando el compilador de Java les dice que la else está fuera de lugar. El compilador de Java interpreta lo anterior de la siguiente manera:

if (feeling == HAPPY)
    /*empty statement*/ ;
System.out.println("Smile");   // This is unconditional
else                           // This is misplaced.  A statement cannot
                               // start with 'else'
System.out.println("Frown");

En otros casos, no habrá errores de compilación, pero el código no hará lo que pretende el programador. Por ejemplo:

for (int i = 0; i < 5; i++);
    System.out.println("Hello");

sólo imprime "Hola" una vez. Una vez más, el punto y coma falso significa que el cuerpo del bucle for es una declaración vacía. Eso significa que la llamada println que sigue es incondicional.

Otra variación:

for (int i = 0; i < 5; i++);
    System.out.println("The number is " + i);

Esto dará un error de "No se puede encontrar el símbolo" para i . La presencia del punto y coma espurio significa que la llamada de println está intentando usar i fuera de su alcance.

En esos ejemplos, hay una solución directa: simplemente elimine el punto y coma no esencial. Sin embargo, hay algunas lecciones más profundas que se pueden extraer de estos ejemplos:

  1. El punto y coma en Java no es "ruido sintáctico". La presencia o ausencia de un punto y coma puede cambiar el significado de su programa. No solo los agregue al final de cada línea.

  2. No confíes en la sangría de tu código. En el lenguaje Java, el compilador ignora los espacios en blanco adicionales al principio de una línea.

  3. Utilice un indentador automático. Todos los IDE y muchos editores de texto simples entienden cómo sangrar correctamente el código Java.

  4. Esta es la lección más importante. Siga las últimas pautas de estilo de Java y ponga llaves alrededor de las declaraciones "then" y "else" y la declaración del cuerpo de un bucle. La abrazadera abierta ( { ) no debe estar en una nueva línea.

Si el programador siguiera las reglas de estilo, el ejemplo if con puntos y coma fuera de lugar se vería así:

if (feeling == HAPPY); {
    System.out.println("Smile");
} else {
    System.out.println("Frown");
}

Eso se ve extraño para un ojo experimentado. Si autodentara ese código, probablemente se vería así:

if (feeling == HAPPY); {
                           System.out.println("Smile");
                       } else {
                           System.out.println("Frown");
                       }

Lo que debería destacarse como mal incluso para un principiante.

Trampa: omitir llaves: los problemas de "colgar si" y "colgar de otra manera"

La última versión de la guía de estilo Java de Oracle exige que las declaraciones "entonces" y "más" en una instrucción if siempre deben estar entre "llaves" o "llaves". Reglas similares se aplican a los cuerpos de varias declaraciones de bucle.

if (a) {           // <- open brace
    doSomething();
    doSomeMore();
}                  // <- close brace

Esto no es realmente requerido por la sintaxis del lenguaje Java. De hecho, si la parte "entonces" de una declaración if es una sola declaración, es legal omitir las llaves

if (a)
    doSomething();

o incluso

if (a) doSomething();

Sin embargo, hay peligros en ignorar las reglas de estilo de Java y omitir las llaves. Específicamente, aumenta significativamente el riesgo de que el código con sangría defectuosa se lea mal.

El problema "que cuelga si":

Considere el código de ejemplo de arriba, reescrito sin llaves.

if (a)
   doSomething();
   doSomeMore();

Este código parece decir que las llamadas a doSomething y doSomeMore se producen tanto si y sólo si a es true . De hecho, el código está sangrado incorrectamente. La especificación del lenguaje Java que la llamada doSomeMore() es una declaración separada que sigue a la instrucción if . La sangría correcta es la siguiente:

if (a)
   doSomething();
doSomeMore();

El problema de "colgar más"

Un segundo problema aparece cuando agregamos else a la mezcla. Considere el siguiente ejemplo con llaves faltantes.

if (a)
   if (b)
      doX();
   else if (c)
      doY(); 
else
   doZ();

El código anterior parece doZ que doZ se llamará cuando a sea false . De hecho, la sangría es incorrecta una vez más. La sangría correcta para el código es:

if (a)
   if (b)
      doX();
   else if (c)
      doY(); 
   else
      doZ();

Si el código se escribiera de acuerdo con las reglas de estilo de Java, en realidad se vería así:

if (a) {
   if (b) {
      doX();
   } else if (c) {
      doY(); 
   } else {
      doZ();
   }
}

Para ilustrar por qué es mejor eso, suponga que accidentalmente ha malgastado el código. Podría terminar con algo como esto:

if (a) {                         if (a) {
   if (b) {                          if (b) {
      doX();                            doX();
   } else if (c) {                   } else if (c) {
      doY();                            doY();
} else {                         } else {
   doZ();                            doZ();
}                                    }
}                                }

Pero en ambos casos, el código mal escrito "se ve mal" a los ojos de un programador Java experimentado.

Trampa: sobrecargar en lugar de anular

Considere el siguiente ejemplo:

public final class Person {
    private final String firstName;
    private final String lastName;
   
    public Person(String firstName, String lastName) {
        this.firstName = (firstName == null) ? "" : firstName;
        this.lastName = (lastName == null) ? "" : lastName;
    }

    public boolean equals(String other) {
        if (!(other instanceof Person)) {
            return false;
        }
        Person p = (Person) other;
        return firstName.equals(p.firstName) &&
                lastName.equals(p.lastName);
    }

    public int hashcode() {
        return firstName.hashCode() + 31 * lastName.hashCode();
    }
}

Este código no va a comportarse como se espera. El problema es que los métodos equals y hashcode para Person no anulan los métodos estándar definidos por Object .

  • El método equals tiene la firma incorrecta. Se debe declarar como equals(Object) no equals(String) .
  • El método hashcode tiene el nombre equivocado. Debe ser hashCode() (tenga en cuenta la C mayúscula).

Estos errores significan que hemos declarado sobrecargas accidentales, y no se utilizarán si Person se usa en un contexto polimórfico.

Sin embargo, hay una manera simple de lidiar con esto (desde Java 5 en adelante). Use la anotación @Override siempre que pretenda que su método sea una anulación:

Java SE 5
public final class Person {
    ...

    @Override
    public boolean equals(String other) {
        ....
    }

    @Override
    public hashcode() {
        ....
    }
}

Cuando añadimos un @Override anotación a una declaración de método, el compilador comprobará que el método no anula (o implementar) un método declarado en una superclase o interfaz. Entonces, en el ejemplo anterior, el compilador nos dará dos errores de compilación, que deberían ser suficientes para alertarnos sobre el error.

Pitfall - Octales literales

Considere el siguiente fragmento de código:

// Print the sum of the numbers 1 to 10
int count = 0;
for (int i = 1; i < 010; i++) {    // Mistake here ....
    count = count + i;
}
System.out.println("The sum of 1 to 10 is " + count);

Un principiante de Java podría sorprenderse al saber que el programa anterior imprime la respuesta incorrecta. En realidad imprime la suma de los números del 1 al 8.

El motivo es que el compilador de Java interpreta un literal entero que comienza con el dígito cero ('0') como un literal octal, no un literal decimal, como cabría esperar. Por lo tanto, 010 es el número octal 10, que es 8 en decimal.

Pitfall - Declarar clases con los mismos nombres que las clases estándar

A veces, los programadores que son nuevos en Java cometen el error de definir una clase con un nombre que es el mismo que una clase muy utilizada. Por ejemplo:

package com.example;

/**
 * My string utilities
 */
public class String {
    ....
}

Entonces se preguntan por qué reciben errores inesperados. Por ejemplo:

package com.example;

public class Test {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}

Si compilas y luego intentas ejecutar las clases anteriores, obtendrás un error:

$ javac com/example/*.java
$ java com.example.Test
Error: Main method not found in class test.Test, please define the main method as:
   public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application

Alguien que mire el código de la clase Test vería la declaración de main y miraría su firma y se preguntaría de qué se está quejando el comando java . Pero, de hecho, el comando java está diciendo la verdad.

Cuando declaramos una versión de String en el mismo paquete que Test , esta versión tiene prioridad sobre la importación automática de java.lang.String . Por lo tanto, la firma del método Test.main es en realidad

void main(com.example.String[] args) 

en lugar de

void main(java.lang.String[] args)

y el java comando no reconocerá que como un método de punto de entrada.

Lección: no defina clases que tengan el mismo nombre que las clases existentes en java.lang u otras clases comúnmente utilizadas en la biblioteca de Java SE. Si haces eso, te estás abriendo para todo tipo de errores oscuros.

Pitfall - Usar '==' para probar un booleano

A veces, un nuevo programador de Java escribirá código así:

public void check(boolean ok) {
    if (ok == true) {           // Note 'ok == true'
        System.out.println("It is OK");
    }
}

Un programador experimentado lo consideraría torpe y querría reescribirlo como:

public void check(boolean ok) {
    if (ok) {
       System.out.println("It is OK");
    }
}

Sin embargo, hay más errores con ok == true que simple torpeza. Considera esta variación:

public void check(boolean ok) {
    if (ok = true) {           // Oooops!
        System.out.println("It is OK");
    }
}

Aquí el programador ha escrito mal == como = ... y ahora el código tiene un error sutil. La expresión x = true asigna incondicionalmente true a x y luego se evalúa como true . En otras palabras, el método de check ahora imprimirá "Está bien", sin importar cuál fue el parámetro.

La lección aquí es dejar el hábito de usar == false y == true . Además de ser detallados, hacen que su codificación sea más propensa a errores.


Nota: Una posible alternativa a ok == true que evita el escollo es usar las condiciones de Yoda ; es decir, ponga el literal en el lado izquierdo del operador relacional, como en true == ok . Esto funciona, pero la mayoría de los programadores probablemente estarían de acuerdo en que las condiciones de Yoda son extrañas. Ciertamente, ok (o !ok ) es más conciso y más natural.

Pitfall: las importaciones de comodines pueden hacer que su código sea frágil

Considere el siguiente ejemplo parcial:

import com.example.somelib.*;
import com.acme.otherlib.*;

public class Test {
    private Context x = new Context();   // from com.example.somelib
    ...
}

Supongamos que la primera vez que desarrolló el código contra la versión 1.0 de somelib y la versión 1.0 de otherlib . Luego, en algún momento posterior, deberá actualizar sus dependencias a versiones posteriores, y decide utilizar la versión 2.0 de otherlib . Supongamos también que uno de los cambios que hicieron en otherlib entre 1.0 y 2.0 fue agregar una clase de Context .

Ahora, cuando recompiles la Test , obtendrás un error de compilación que te dice que el Context es una importación ambigua.

Si está familiarizado con el código base, esto probablemente sea solo un inconveniente menor. Si no es así, entonces tiene trabajo que hacer para solucionar este problema, aquí y potencialmente en otro lugar.

El problema aquí es la importación de comodines. Por un lado, el uso de comodines puede hacer que sus clases sean un poco más cortas. Por otra parte:

  • Los cambios compatibles hacia arriba en otras partes de su base de código, bibliotecas estándar de Java o bibliotecas de terceros pueden provocar errores de compilación.

  • La legibilidad sufre. A menos que esté utilizando un IDE, puede ser difícil determinar cuál de las importaciones de comodín está generando una clase con nombre.

La lección es que es una mala idea usar las importaciones de comodines en el código que debe ser de larga duración. Las importaciones específicas (sin comodines) no son mucho esfuerzo de mantener si usa un IDE, y el esfuerzo vale la pena.

Pitfall: Usar 'assert' para validar argumentos o entradas de usuario

Una pregunta que ocasionalmente en StackOverflow es si es apropiado usar assert para validar los argumentos proporcionados a un método, o incluso las entradas proporcionadas por el usuario.

La respuesta simple es que no es apropiado.

Mejores alternativas incluyen:

  • Lanzar una excepción IllegalArgument utilizando un código personalizado.
  • Usando los métodos de las condiciones Preconditions disponibles en la biblioteca de Google Guava.
  • Usando los métodos Validate disponibles en la biblioteca Lang3 de Apache Commons.

Esto es lo que la Especificación del lenguaje Java (JLS 14.10, para Java 8) aconseja sobre este asunto:

Normalmente, la verificación de afirmaciones se habilita durante el desarrollo y las pruebas del programa, y ​​se deshabilita para la implementación, para mejorar el rendimiento.

Debido a que las aserciones pueden estar deshabilitadas, los programas no deben asumir que las expresiones contenidas en las aserciones serán evaluadas. Por lo tanto, estas expresiones booleanas generalmente deben estar libres de efectos secundarios. La evaluación de tal expresión booleana no debería afectar ningún estado que sea visible después de que se complete la evaluación. No es ilegal que una expresión booleana contenida en una aserción tenga un efecto secundario, pero generalmente es inapropiado, ya que podría causar que el comportamiento del programa varíe dependiendo de si las aserciones estaban habilitadas o deshabilitadas.

A la luz de esto, las aserciones no deben usarse para la verificación de argumentos en métodos públicos. La verificación de argumentos generalmente es parte del contrato de un método, y este contrato debe ser confirmado si las aserciones están habilitadas o deshabilitadas.

Un problema secundario con el uso de aserciones para la verificación de argumentos es que los argumentos erróneos deberían resultar en una excepción de tiempo de ejecución apropiada (como IllegalArgumentException , ArrayIndexOutOfBoundsException o NullPointerException ). Un fallo de aserción no arrojará una excepción apropiada. Nuevamente, no es ilegal usar aserciones para la verificación de argumentos en métodos públicos, pero en general es inapropiado. Se pretende que AssertionError nunca se detecte, pero es posible hacerlo, por lo tanto, las reglas para las declaraciones de prueba deben tratar las aserciones que aparecen en un bloque de prueba de manera similar al tratamiento actual de las declaraciones de lanzamiento.

El escollo de los objetos nulos de autoenvasado en primitivos

public class Foobar {
    public static void main(String[] args) {

        // example: 
        Boolean ignore = null;
        if (ignore == false) {
            System.out.println("Do not ignore!");
        }
    }
}

El escollo aquí es que null se compara con false . Como estamos comparando un boolean primitivo con un Boolean , Java intenta desempaquetar el Object Boolean en un equivalente primitivo, listo para la comparación. Sin embargo, dado que ese valor es null , se lanza una NullPointerException .

Java es incapaz de comparar tipos primitivos contra valores null , lo que provoca una NullPointerException en tiempo de ejecución. Considere el caso primitivo de la condición false == null ; esto generaría un error de tiempo de compilación de incomparable types: int and <null> .



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow