Diego's weblog - Developer's notes

Todo | General | Design | Java
Main | Next day (sep 4, 2005) »

20050903 sábado septiembre 03, 2005

Java 5. ¿Que tiene de bueno y de malo?. Parte III

Continuando con la serie de notas sobre los features de Tiger, hoy voy a hablar de static imports.
Static imports permite importar miembros estáticos de clases o interfaces de forma tal de no hacer uso del antipattern "Constant Interface".
Static imports no me gusta. No me parece algo que brinde facilidades, sino todo lo contrario, creo que molesta y hace poco legible el código haciendo que este se vea mas como un conjunto de funciones en lugar declases con métodos. En el único caso en el que veo que static imports puede ser útil es al utilizar enums o constantes.

¿Trabajando con funciones?
Aquí daré unos ejemplos de como puede verse el código de una clase como si fueran funciones:

import static java.util.Arrays.sort;
import static java.lang.System.out;
 
public class StaticImportTest {
    public static void main(String... args) {
        sort(args);       // llamo a la función
        out.println( deepToString(args) );       // llamo a otra función
    }
}

Aquí vemos como una clase en Java se parece a un archivo de código en C. Los static imports actúan como la directiva include y la llamada al método sort parece como si estuviéramos llamando a una función. Por otro lado, además de parece código estructurado, el código pierde legibilidad. Imaginemos una clase con muchos métodos, también imaginemos que tenemos que modificar esa clase y nos encontramos con algo así en nuestro código.

public void execute() {
   Integer[] array = {5,8,2};
 
   sort(array);
   out.println( deepToString(array) );
}

Surgen las siguientes dudas: ¿El método sort es un método estático importado o es un método privado de la clase?. A primera vista no lo sabemos. ¿Que sucede si nosotros creamos un método para la clase llamado sort(Integer a[]) en un escenario como este?. Los métodos que realicen las llamadas al método estático sort de Arrays se verán modificados en su comportamiento dado que ahora llamarán al nuevo método private creado, con lo cual estamos introduciendo un error sin darnos cuenta. Esto es porque en runtime tratará primero de ejecutar métodos de la clase y luego los métodos estáticos importados.

public void execute() {
   Integer[] array = {5,8,2};
   sort(array);    // Llama al método de instancia sort y no Arrays.sort()
   out.println( deepToString(array) );
}
 
private void sort(Integer[] a) {
   // nothing
}

¿Que pasa si tenemos varios static imports que tienen los mismos métodos?. Bueno aquí el compilador se da cuenta que la llamada al método sort es ambigua y nos da un error.

import static java.lang.System.out;
import static java.util.Arrays.sort;
import static staticimport.array.MyArrays.sort;
 
public class StaticImportError {
 
   public static void main(String[] args) {
      int[] array = {5,8,2};
      sort(array);   // ERROR en tiempo de compilación
 
      for(int i: array) {
         out.println( i );
      }
   }
}
 
...
public class MyArrays {
   public static void sort(int[] a) {
      // nothing
   }
}

Como se puede ser en estos casos (al menos desde mi punto de vista) el código es confuso y poco claro. De hecho en puede leerse en http://java.sun.com/j2se/1.5.0/docs/guide/language/static-import.html algo así:
Usar static import con moderación. Este es usado en situaciones cuando se necesita acceder con frecuencia a unos pocos objetos estáticos desde una o dos clases. El abuso de static import puede resultar en un código difícil de leer y mantener (como hemos visto ;)). Usado correctamente hace que el código sea más fácil de leer dada la eliminación de varios classnames repetidos. ( sep 03 2005, 10:14:42 AM ART ) Permalink Comentarios [0]

Java 5. ¿Que tiene de bueno y de malo?. Parte II

Otros de los problemas que puede acarrear el mal uso de autoboxing son:

Problema con el == y el equals
Este no es un problema en si, de echo el uso del metodo equals o de == no produce ningun problema en la ejecución de código java. Sino que el problema esta en que se puede producir una confusión y un uso incorrecto.
La confusión viene por el echo de que en el rango [-128, 127], la VM mantienen los wrapper como constantes cuando realiza autoboxing, es decir que:

Byte b1 = 10;
Byte b2 = 10;

Se cumple la condición (b1 == b2), pero si hacemos:

Byte b1 = new Byte((byte)10);
Byte b2 = new Byte((byte)10);

No se cumple que (b1 == b2) dado que al realizar el autoboxing del tipo primitivo 10 al objeto Byte se ejecuta el método estático valueOf(...) que verifica si el primitivo esta dentro del rango mencionado, si lo esta devuelve una wrapper constante, sino crea un nuevo objeto con el valor.

Veamos un ejemplo:

public static void testEquals(Number n1, Number n2) {
    if( n1 == n2 ) {      // comparacion de referencias de objetos, no hay auto-boxing
        System.out.println("Las referencias son iguales");
    }

    if( n1.equals(n2) ) {
        System.out.println("Tienen el mismo valor");
    }
}
public static void main(String[] args) {
        Integer i = 120;
        Integer b = 120;
        testEquals(i, b);
}

El resultado es el siguiente:
- Las referencias son iguales
- Tienen el mismo valor

Ahora si hacemos:

public static void main(String[] args) {
        Integer i = 129;
        Integer b = 129;
        testEquals(i, b);
}

El resultado es distinto ya que las dos referencias no apuntan a los mismos objetos Integer aunque tengan el mismo valor.
Por lo tanto es siempre recomendable utilizar el método equals al comparar wrappers

Problema con pasaje de parámetros a métodos sobrecargados.
Este problema lo mostrare con otro ejemplo. Supongamos tener la sigueinte clase:

public class Dummy {
    public void doIt(int x) {
        System.out.println("doIt(int x)");
    }
    public void doIt(Integer x) {
        System.out.println("doIt(Integer x)");
    }
    public void doIt(double x) {
        System.out.println("doIt(double x)");
    }
    public void doIt(Object x) {
        System.out.println("doIt(Object x)");
    }
    public static void main(String args...) {
        Dummy dummy = new Dummy();
        dummy.doIt(10);        
    }
}

Este código llama al metodo doIt(int x) y no hay ninguna duda de eso, pero que pasa ahora si este metodo no existe, a que metodo se llama?
Uno pensaria que se hace un autoboxing y se llama el método doIt(Integer x) dado que es el que corresponde por su tipo, pero no, se llama al metodo doIt(double x).
Porque pasa esto, bueno es por conpatibilidad con versiones anteriores a Tiger, por ejemplo, en java 1.4, al no existir autoboxing, las llamadas a los metodos se resolvian por compatibilidad de tipos. En este caso se llamaria a doIt(double x) y no a doIt(Integer x).
Ahora si tampoco existiera el metodo doIt(double x) si se realiza un autoboxing y se llama al metodo doIt(Integer x).
Por lo tanto podemos concluir con que la llamada a metodos se resuelve siguiendo el siguiente algoritmo:
1. El compilador intenta localizar el metodo sin utilizar boxing, unboxing o varargs.
2. Si el primer paso falla, el compilador intenta resolver el metodo permitiendo boxing y unboxing. varargs no es considerado.
3. Si el segundo paso fallo, el compilador intenta resolver el metodo una vez mas permitiendo boxing, unboxing y considerando los metodos con varargs.

Sin embargo el autoboxing es un feature que facilita mucho la programación en java y si es bien utilizado (y no se hace abuso de el) puede ser muy util a la hora de escribir codigo limpio y eficiente.
Una de las principales ventajas de autoboxing es en la utilización de collections, donde, junto al uso de generics, permite trabajar con tipos primitivos (agregar, sacar, etc) sin la necesidad de crear explicitamente objetos wrappers o realizar casteos de tipos, por ejemplo:

List<Long> longs = new LinkedList<Long>();
longs.add(12L);            // auto-boxing y luego coloca el objeto en la lista
longs.add(Long.MAX_VALUE);

Las coleciones no soportan tipos primitivos, pero con el uso de autoboxing pareciera que si. En realidad lo que aqui se hace es:

- Hacer autoboxing de 12 a un Long con el value = 12
- Poner en la lista longs el objeto wrapper creado.

Para obtener los valores desde la collection:

long lValue = longs.get(0);        //
Obtiene de la lista un objeto (Integer) y luego hace auto-unboxing
Long oValue = longs.get(1);

Lo que en realidad obtenemos de la lista (generics) es un objeto del tipo Long, y nuevamente con el uso de autoboxing, lo podemos trabajar como un tipo primitivo long sin realizar la llamada a los metodos del wrapper.
Como vemos aqui, no se escribio ningun casteo de tipos, esto es dado al uso de generic, que proximamente hablare de el.

( sep 03 2005, 10:12:46 AM ART ) Permalink Comentarios [1]