Buscar..


Introducción

Este tema describe algunos de los errores comunes que cometen los principiantes en Java.

Esto incluye cualquier error común en el uso del lenguaje Java o la comprensión del entorno de tiempo de ejecución.

Los errores asociados con API específicas se pueden describir en temas específicos de esas API. Las cuerdas son un caso especial; están cubiertos en la especificación del lenguaje Java. Los detalles que no sean errores comunes se pueden describir en este tema en Cadenas .

Pitfall: utilizando == para comparar objetos de envoltorios primitivos, como Integer

(Este escollo se aplica por igual a todos los tipos de envoltorios primitivos, pero lo ilustraremos para Integer e int .)

Cuando se trabaja con objetos Integer , es tentador usar == para comparar valores, porque eso es lo que haría con los valores int . Y en algunos casos esto parecerá funcionar:

Integer int1_1 = Integer.valueOf("1");
Integer int1_2 = Integer.valueOf(1);

System.out.println("int1_1 == int1_2: " + (int1_1 == int1_2));          // true
System.out.println("int1_1 equals int1_2: " + int1_1.equals(int1_2));   // true

Aquí creamos dos objetos Integer con el valor 1 y los comparamos (en este caso creamos uno de una String y uno de un literal int . Hay otras alternativas). Además, observamos que los dos métodos de comparación ( == y equals ) son ambos true .

Este comportamiento cambia cuando elegimos diferentes valores:

Integer int2_1 = Integer.valueOf("1000");
Integer int2_2 = Integer.valueOf(1000);

System.out.println("int2_1 == int2_2: " + (int2_1 == int2_2));          // false
System.out.println("int2_1 equals int2_2: " + int2_1.equals(int2_2));   // true

En este caso, solo la comparación de equals produce el resultado correcto.

La razón de esta diferencia en el comportamiento es que la JVM mantiene un caché de objetos Integer para el rango de -128 a 127. (El valor superior se puede anular con la propiedad del sistema "java.lang.Integer.IntegerCache.high" o la Argumento de JVM "-XX: AutoBoxCacheMax = tamaño"). Para los valores en este rango, Integer.valueOf() devolverá el valor almacenado en caché en lugar de crear uno nuevo.

Por lo tanto, en el primer ejemplo, las llamadas Integer.valueOf(1) y Integer.valueOf("1") devolvieron la misma instancia de Integer caché. Por el contrario, en el segundo ejemplo, Integer.valueOf(1000) y Integer.valueOf("1000") crearon y devolvieron nuevos objetos Integer .

El operador == para tipos de referencia prueba la igualdad de referencia (es decir, el mismo objeto). Por lo tanto, en el primer ejemplo int1_1 == int1_2 es true porque las referencias son las mismas. En el segundo ejemplo int2_1 == int2_2 es falso porque las referencias son diferentes.

Pitfall: olvidarse de liberar recursos

Cada vez que un programa abre un recurso, como un archivo o una conexión de red, es importante liberar el recurso una vez que haya terminado de usarlo. Se debe tener la misma precaución si se lanzara alguna excepción durante las operaciones con dichos recursos. Se podría argumentar que FileInputStream tiene un finalizador que invoca el método close() en un evento de recolección de basura; sin embargo, dado que no podemos estar seguros de cuándo se iniciará un ciclo de recolección de basura, la secuencia de entrada puede consumir recursos de computadora por un período de tiempo indefinido. El recurso se debe cerrar en una sección de finally de un bloque try-catch:

Java SE 7
private static void printFileJava6() throws IOException {
    FileInputStream input;
    try {
        input = new FileInputStream("file.txt");
        int data = input.read();
        while (data != -1){
            System.out.print((char) data);
            data = input.read();
        }
    } finally {
        if (input != null) {
            input.close();
        }
    }
}

Desde Java 7 hay una declaración realmente útil y ordenada introducida en Java 7 particularmente para este caso, llamada try-with-resources:

Java SE 7
private static void printFileJava7() throws IOException {
    try (FileInputStream input = new FileInputStream("file.txt")) {
        int data = input.read();
        while (data != -1){
            System.out.print((char) data);
            data = input.read();
        }
    }
}

La sentencia try-con-recursos se puede utilizar con cualquier objeto que implemente la Closeable o AutoCloseable interfaz. Asegura que cada recurso se cierre al final de la declaración. La diferencia entre las dos interfaces es que el método close() de Closeable lanza una IOException que debe manejarse de alguna manera.

En los casos en los que el recurso ya se ha abierto pero debe cerrarse de manera segura después de su uso, se puede asignar a una variable local dentro de try-with-resources

Java SE 7
private static void printFileJava7(InputStream extResource) throws IOException {
    try (InputStream input = extResource) {
        ... //access resource
    }
}

La variable de recurso local creada en el constructor try-with-resources es efectivamente final.

Trampa: fugas de memoria

Java gestiona la memoria automáticamente. No es necesario liberar la memoria manualmente. La memoria de un objeto en el montón puede ser liberada por un recolector de basura cuando el objeto ya no es accesible por un hilo vivo.

Sin embargo, puede evitar que se libere la memoria, permitiendo que se pueda acceder a objetos que ya no son necesarios. Ya sea que llame a esto una pérdida de memoria o una tasa de paquetes de memoria, el resultado es el mismo: un aumento innecesario en la memoria asignada.

Las fugas de memoria en Java pueden ocurrir de varias maneras, pero la razón más común son las referencias eternas de objetos, porque el recolector de basura no puede eliminar objetos del montón mientras todavía hay referencias a ellos.

Campos estáticos

Uno puede crear una referencia de este tipo definiendo la clase con un campo static contiene alguna colección de objetos, y olvidando establecer ese campo static en null después de que la colección ya no sea necesaria. static campos static se consideran raíces GC y nunca se recopilan. Otro problema son las fugas en la memoria no de pila cuando se utiliza JNI .

Fuga del cargador de clases

De lejos, sin embargo, el tipo más insidioso de pérdida de memoria es la pérdida del cargador de clases . Un cargador de clases contiene una referencia a cada clase que ha cargado, y cada clase tiene una referencia a su cargador de clases. Cada objeto tiene una referencia a su clase también. Por lo tanto, si incluso un solo objeto de una clase cargada por un cargador de clases no es basura, no se puede recopilar una sola clase que ese cargador de clases haya cargado. Como cada clase también hace referencia a sus campos estáticos, tampoco se pueden recopilar.

Fuga de acumulación El ejemplo de fuga de acumulación podría ser similar al siguiente:

final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
final Deque<BigDecimal> numbers = new LinkedBlockingDeque<>();
final BigDecimal divisor = new BigDecimal(51);

scheduledExecutorService.scheduleAtFixedRate(() -> {
    BigDecimal number = numbers.peekLast();
    if (number != null && number.remainder(divisor).byteValue() == 0) {
        System.out.println("Number: " + number);
        System.out.println("Deque size: " + numbers.size());
    }
}, 10, 10, TimeUnit.MILLISECONDS);

scheduledExecutorService.scheduleAtFixedRate(() -> {
    numbers.add(new BigDecimal(System.currentTimeMillis()));
}, 10, 10, TimeUnit.MILLISECONDS);

try {
    scheduledExecutorService.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
    e.printStackTrace();
}

Este ejemplo crea dos tareas programadas. La primera tarea toma el último número de un deque llamado numbers , y, si el número es divisible por 51, imprime el número y el tamaño del deque. La segunda tarea pone números en el deque. Ambas tareas se programan a una velocidad fija y se ejecutan cada 10 ms.

Si se ejecuta el código, verá que el tamaño del deque está aumentando permanentemente. Esto eventualmente hará que el deque se llene con objetos que consumen toda la memoria disponible del montón.

Para evitar esto y preservar la semántica de este programa, podemos usar un método diferente para tomar números del deque: pollLast . Al contrario del método peekLast , pollLast devuelve el elemento y lo elimina del deque, mientras que peekLast solo devuelve el último elemento.

Pitfall: usando == para comparar cadenas

Un error común para los principiantes de Java es usar el operador == para probar si dos cadenas son iguales. Por ejemplo:

public class Hello {
    public static void main(String[] args) {
        if (args.length > 0) {
            if (args[0] == "hello") {
                System.out.println("Hello back to you");
            } else {
                System.out.println("Are you feeling grumpy today?");
            }
        }
    }
}

Se supone que el programa anterior prueba el primer argumento de la línea de comando e imprime diferentes mensajes cuando no es la palabra "hola". Pero el problema es que no funcionará. Ese programa producirá "¿Te sientes malhumorado hoy?" no importa cuál sea el primer argumento de la línea de comando.

En este caso particular, la String "hola" se coloca en el grupo de cadenas mientras que la String args [0] reside en el montón. Esto significa que hay dos objetos que representan el mismo literal, cada uno con su referencia. Dado que == prueba las referencias, no la igualdad real, la comparación producirá un falso la mayoría de las veces. Esto no significa que siempre lo hará.

Cuando utiliza == para probar cadenas, lo que realmente está probando es si dos objetos de String son el mismo objeto de Java. Desafortunadamente, eso no es lo que significa la igualdad de cadenas en Java. De hecho, la forma correcta de probar cadenas es usar el método equals(Object) . Para un par de cadenas, generalmente queremos probar si están formadas por los mismos caracteres en el mismo orden.

public class Hello2 {
    public static void main(String[] args) {
        if (args.length > 0) {
            if (args[0].equals("hello")) {
                System.out.println("Hello back to you");
            } else {
                System.out.println("Are you feeling grumpy today?");
            }
        }
    }
}

Pero en realidad se pone peor. El problema es que == dará la respuesta esperada en algunas circunstancias. Por ejemplo

public class Test1 {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hello";
        if (s1 == s2) {
            System.out.println("same");
        } else {
            System.out.println("different");
        }
    }
}

Curiosamente, esto imprimirá "igual", aunque estamos probando las cadenas de manera incorrecta. ¿Porqué es eso? Debido a que la Especificación del lenguaje Java (Sección 3.10.5: Literales de cadenas) estipula que dos cadenas >> literales << consistentes en los mismos caracteres serán representadas por el mismo objeto Java. Por lo tanto, la prueba == dará verdadero para literales iguales. (Los literales de cadena se "internan" y se agregan a un "grupo de cadenas" compartido cuando se carga su código, pero eso es en realidad un detalle de implementación).

Para agregar a la confusión, la especificación del lenguaje Java también estipula que cuando se tiene una expresión constante en tiempo de compilación que concatena dos literales de cadena, es equivalente a un solo literal. Así:

    public class Test1 {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hel" + "lo";
        String s3 = " mum";
        if (s1 == s2) {
            System.out.println("1. same");
        } else {
            System.out.println("1. different");
        }
        if (s1 + s3 == "hello mum") {
            System.out.println("2. same");
        } else {
            System.out.println("2. different");
        }
    }
}

Esto dará salida a "1. igual" y "2. diferente". En el primer caso, la expresión + se evalúa en tiempo de compilación y comparamos un objeto String consigo mismo. En el segundo caso, se evalúa en tiempo de ejecución y comparamos dos objetos String diferentes

En resumen, usar == para probar cadenas en Java es casi siempre incorrecto, pero no se garantiza que dé la respuesta incorrecta.

Pitfall: probar un archivo antes de intentar abrirlo.

Algunas personas recomiendan que aplique varias pruebas a un archivo antes de intentar abrirlo para proporcionar un mejor diagnóstico o evitar tratar con excepciones. Por ejemplo, este método intenta verificar si la path corresponde a un archivo legible:

public static File getValidatedFile(String path) throws IOException {
    File f = new File(path);
    if (!f.exists()) throw new IOException("Error: not found: " + path);
    if (!f.isFile()) throw new IOException("Error: Is a directory: " + path);
    if (!f.canRead()) throw new IOException("Error: cannot read file: " + path);
    return f;
}

Puedes usar el método anterior como este:

File f = null;
try {
    f = getValidatedFile("somefile");
} catch (IOException ex) {
    System.err.println(ex.getMessage());
    return;
}
try (InputStream is = new FileInputStream(file)) {
    // Read data etc.
}

El primer problema está en la firma para FileInputStream(File) porque el compilador seguirá insistiendo en que IOException aquí, o más arriba en la pila.

El segundo problema es que las comprobaciones realizadas por getValidatedFile no garantizan que FileInputStream tendrá éxito.

  • Condiciones de la carrera: otro hilo o un proceso separado podría cambiar el nombre del archivo, eliminar el archivo o eliminar el acceso de lectura después de que getValidatedFile el getValidatedFile . Eso llevaría a una IOException "simple" sin el mensaje personalizado.

  • Hay casos de borde no cubiertos por esas pruebas. Por ejemplo, en un sistema con SELinux en modo "de cumplimiento", un intento de leer un archivo puede fallar a pesar de que canRead() devuelva true .

El tercer problema es que las pruebas son ineficientes. Por ejemplo, las llamadas exists , isFile y canRead harán cada una syscall para realizar la verificación requerida. Luego se hace otro syscall para abrir el archivo, que repite las mismas comprobaciones detrás de escena.

En resumen, los métodos como getValidatedFile son erróneos. Es mejor simplemente intentar abrir el archivo y manejar la excepción:

try (InputStream is = new FileInputStream("somefile")) {
    // Read data etc.
} catch (IOException ex) {
    System.err.println("IO Error processing 'somefile': " + ex.getMessage());
    return;
}

Si desea distinguir los errores de E / S que se producen al abrir y leer, puede usar un try / catch anidado. Si desea producir mejores diagnósticos para fallas abiertas, puede realizar las isFile exists , isFile y canRead en el controlador.

Pitfall: pensar las variables como objetos

Ninguna variable Java representa un objeto.

String foo;   // NOT AN OBJECT

Ninguna matriz de Java contiene objetos.

String bar[] = new String[100];  // No member is an object.

Si piensa erróneamente que las variables son objetos, el comportamiento real del lenguaje Java lo sorprenderá.

  • Para las variables de Java que tienen un tipo primitivo (como int o float ), la variable contiene una copia del valor. Todas las copias de un valor primitivo son indistinguibles; es decir, solo hay un valor int para el número uno. Los valores primitivos no son objetos y no se comportan como objetos.

  • Para las variables de Java que tienen un tipo de referencia (ya sea una clase o un tipo de matriz), la variable contiene una referencia. Todas las copias de una referencia son indistinguibles. Las referencias pueden apuntar a objetos, o pueden ser null que significa que no apuntan a ningún objeto. Sin embargo, no son objetos y no se comportan como objetos.

Las variables no son objetos en ningún caso, y no contienen objetos en ninguno de los casos. Pueden contener referencias a objetos , pero eso es decir algo diferente.

Clase de ejemplo

Los ejemplos que siguen utilizan esta clase, que representa un punto en el espacio 2D.

public final class MutableLocation {
   public int x;
   public int y;

   public MutableLocation(int x, int y) {
       this.x = x;
       this.y = y;
   }

   public boolean equals(Object other) {
       if (!(other instanceof MutableLocation) {
           return false;
       }
       MutableLocation that = (MutableLocation) other;
       return this.x == that.x && this.y == that.y;
   }
}

Una instancia de esta clase es un objeto que tiene dos campos x y y que tienen el tipo int .

Podemos tener muchas instancias de la clase MutableLocation . Algunos representarán las mismas ubicaciones en el espacio 2D; Es decir, los valores respectivos de x y y coincidirán. Otros representarán diferentes lugares.

Múltiples variables pueden apuntar al mismo objeto.

 MutableLocation here = new MutableLocation(1, 2);
 MutableLocation there = here;
 MutableLocation elsewhere = new MutableLocation(1, 2);

En lo anterior, hemos declarado tres variables here , there y en elsewhere que pueden contener referencias a objetos MutableLocation .

Si (incorrectamente) piensa que estas variables son objetos, entonces es probable que malinterprete las afirmaciones diciendo:

  1. Copie la ubicación "[1, 2]" here
  2. Copie la ubicación "[1, 2]" there
  3. Copie la ubicación "[1, 2]" a elsewhere

A partir de eso, es probable que deduzca que tenemos tres objetos independientes en las tres variables. De hecho solo hay dos objetos creados por el anterior. Las variables here y there realidad se refieren al mismo objeto.

Podemos demostrar esto. Suponiendo las declaraciones de variables como anteriormente:

System.out.println("BEFORE: here.x is " + here.x + ", there.x is " + there.x +
                   "elsewhere.x is " + elsewhere.x);
here.x = 42;
System.out.println("AFTER: here.x is " + here.x + ", there.x is " + there.x +
                   "elsewhere.x is " + elsewhere.x);

Esto dará salida a lo siguiente:

BEFORE: here.x is 1, there.x is 1, elsewhere.x is 1
AFTER: here.x is 42, there.x is 42, elsewhere.x is 1

Le asignamos un nuevo valor a here.x y cambió el valor que vemos a través de there.x . Se están refiriendo al mismo objeto. Pero el valor que vemos a través de elsewhere.x no ha cambiado, por lo que en elsewhere debe referirse a un objeto diferente.

Si una variable era un objeto, entonces la asignación here.x = 42 no cambiaría there.x .

El operador de igualdad NO prueba que dos objetos son iguales

La aplicación del operador de igualdad ( == ) a los valores de referencia comprueba si los valores se refieren al mismo objeto. No prueba si dos (diferentes) objetos son "iguales" en el sentido intuitivo.

 MutableLocation here = new MutableLocation(1, 2);
 MutableLocation there = here;
 MutableLocation elsewhere = new MutableLocation(1, 2);

 if (here == there) {
     System.out.println("here is there");
 }
 if (here == elsewhere) {
     System.out.println("here is elsewhere");
 }

Esto imprimirá "aquí está ahí", pero no imprimirá "aquí está en otra parte". (Las referencias here y en elsewhere son para dos objetos distintos).

Por el contrario, si llamamos al método equals(Object) que implementamos anteriormente, vamos a probar si dos instancias de MutableLocation tienen una ubicación igual.

 if (here.equals(there)) {
     System.out.println("here equals there");
 }
 if (here.equals(elsewhere)) {
     System.out.println("here equals elsewhere");
 }

Esto imprimirá ambos mensajes. En particular, here.equals(elsewhere) devuelve true porque los criterios semánticos que elegimos para la igualdad de dos objetos MutableLocation se han cumplido.

Las llamadas a métodos NO pasan objetos en absoluto

Las llamadas al método Java usan el paso por valor 1 para pasar argumentos y devolver un resultado.

Cuando pasa un valor de referencia a un método, en realidad está pasando una referencia a un objeto por valor , lo que significa que está creando una copia de la referencia del objeto.

Siempre que ambas referencias de objetos sigan apuntando al mismo objeto, puede modificar ese objeto de cualquiera de las dos referencias, y esto es lo que causa confusión para algunos.

Sin embargo, no está pasando un objeto por referencia 2. La distinción es que si la copia de referencia del objeto se modifica para apuntar a otro objeto, la referencia del objeto original seguirá apuntando al objeto original.

void f(MutableLocation foo) {  
    foo = new MutableLocation(3, 4);   // Point local foo at a different object.
}

void g() {
    MutableLocation foo = MutableLocation(1, 2);
    f(foo);
    System.out.println("foo.x is " + foo.x); // Prints "foo.x is 1".
}

Tampoco estás pasando una copia del objeto.

void f(MutableLocation foo) {  
    foo.x = 42;
}

void g() {
    MutableLocation foo = new MutableLocation(0, 0);
    f(foo);
    System.out.println("foo.x is " + foo.x); // Prints "foo.x is 42"
}

1 - En idiomas como Python y Ruby, el término "pasar compartiendo" se prefiere para "pasar por valor" de un objeto / referencia.

2 - El término "pasar por referencia" o "llamada por referencia" tiene un significado muy específico en la terminología del lenguaje de programación. En efecto, significa que pasa la dirección de una variable o un elemento de matriz , de modo que cuando el método llamado asigna un nuevo valor al argumento formal, cambia el valor en la variable original. Java no soporta esto. Para obtener una descripción más completa de los diferentes mecanismos para pasar parámetros, consulte https://en.wikipedia.org/wiki/Evaluation_strategy .

Pitfall: combinación de asignación y efectos secundarios

Ocasionalmente vemos preguntas de Java de StackOverflow (y preguntas de C o C ++) que preguntan algo como esto:

i += a[i++] + b[i--];

evalúa a ... para algunos estados iniciales conocidos de i , a y b .

Generalmente hablando:

  • para Java, la respuesta siempre se especifica 1 , pero no es obvia y, a menudo, difícil de entender
  • para C y C ++ la respuesta a menudo no se especifica.

Tales ejemplos se utilizan a menudo en exámenes o entrevistas de trabajo como un intento de ver si el estudiante o el entrevistado entiende cómo funciona realmente la evaluación de expresiones en el lenguaje de programación Java. Esto es posiblemente legítimo como una "prueba de conocimiento", pero eso no significa que debas hacer esto en un programa real.

Para ilustrar, el siguiente ejemplo aparentemente simple ha aparecido varias veces en preguntas de StackOverflow (como esta ). En algunos casos, aparece como un error genuino en el código de alguien.

int a = 1;
a = a++;
System.out.println(a);    // What does this print.

La mayoría de los programadores (incluidos los expertos en Java) que leen esas declaraciones rápidamente dirían que produce 2 . De hecho, produce 1 . Para una explicación detallada de por qué, lea esta Respuesta .

Sin embargo la comida para llevar real a partir de esto y ejemplos similares es que cualquier declaración de Java que tanto le asigna y los efectos secundarios de la misma variable va a ser en el mejor de difícil de entender, y en el peor francamente engañosa. Debes evitar escribir código como este.


1 - módulo problemas potenciales con el modelo de memoria de Java si las variables u objetos son visibles a otros hilos.

Pitfall: no entender que String es una clase inmutable

Los nuevos programadores de Java a menudo olvidan, o no comprenden completamente, que la clase String Java es inmutable. Esto conduce a problemas como el del siguiente ejemplo:

public class Shout {
    public static void main(String[] args) {
        for (String s : args) {
            s.toUpperCase();
            System.out.print(s);
            System.out.print(" ");
        }
        System.out.println();
    }
}

Se supone que el código anterior imprime los argumentos de la línea de comandos en mayúsculas. Desafortunadamente, no funciona, el caso de los argumentos no se cambia. El problema es esta afirmación:

s.toUpperCase();

Podría pensar que llamar a toUpperCase() cambiará s a una cadena en mayúsculas. No lo hace No se puede String objetos de String son inmutables. No se pueden cambiar.

En realidad, el método toUpperCase() devuelve un objeto String que es una versión en mayúsculas del String que lo llamas. Probablemente este será un nuevo objeto String , pero si s ya estaba todo en mayúsculas, el resultado podría ser la cadena existente.

Por lo tanto, para utilizar este método de manera efectiva, debe usar el objeto devuelto por la llamada al método; por ejemplo:

s = s.toUpperCase();

De hecho, la regla de "las cadenas nunca cambian" se aplica a todos los métodos de String . Si recuerdas eso, entonces puedes evitar toda una categoría de errores de principiantes.



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