viernes, 9 de septiembre de 2005

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


For/in

Este feature me parece interesante dado que facilita/optimiza la forma sobre la cual iterar por una collection o array, el código es más simple dado que elimina el uso de un iterator o variables índices pero esto genera algunas limitaciones que explicare mas adelante.
Aqui un ejemplo de for/in:
Collection<String> list = new ArrayList<String>();   // Generic.
list.add("for/in");
list.add("annotations");
list.add("autoboxing");
 
// Conventional for
for( Iterator<String> iter = list.iterator(); iter.hasNext(); ) {
   String value = iter.next();;   // no-cast
   System.out.println( value );
}
 
// Enhanced for
for( String value : list ) {
   System.out.println( value );
}

Este nuevo for/in no nos da nada nuevo ya que se puede hacer lo mismo utilizando un for convensional. for/in puede hacer uso de Generics para de esta forma tambien evitar el casting, aunque esto no es un obligatorio.
Sintaxis.
for( Type identifier : Expression ) Statement

La expression debe ser un array o una instancia de la nueva interface java.lang.Iterable (java.util.Collection implementa java.lang.Iterable desde la version 5 de Java) que tiene el método public Iterator<T> iterator();. Esto permite crear clases que puedan ser iteradas utilizando el nuevo enhanced for. Type debe ser un tipo valida para Java (un objeto o un primitivo).
Basicamente lo que hace el for/in es traducirlo en un for convensional de la siguiente forma o siguiendo estos pasos:
for( Iterator iterator = Expression.iterator(); iterator.hasNext(); ) {
   Type identifier = (Type)iterator.next();
   Statement
}

A pesar de todo este feature tiene algunas consecuencias en su uso y por lo tanto genera limitaciones:

- No se puede modificar los valores de los elementos de un array de tipos primitivos.
int[] i = {1, 2, 3, 4, 5, 6};
for( int x : i ) {
nbsp;  System.out.println(x);
   // No se modifica el valor del elemento en el array
   // aunque si se modifica dentro del bloque del for
   x++;
}

- No se puede eliminar elementos desde la collection, porque no se tiene el iterator.
- No se puede modificar la referencia de un objeto de la collecion dentro del loop, aunque se puede modificar el objeto en si mismo.
Collection list = new ArrayList();
list.add("for/in");
list.add("annotations");
list.add("autoboxing");
 
for( Object o : list ) {
   // Se modifica la referencia pero solo dentro del bloque for
   // No se modifica la collection
   o = new String("j2ee");
   System.out.println(o);
}

- No se puede utilizar para iterar simultaneamente varias collections o arrays en paralelo

lunes, 5 de septiembre de 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
}

¿Què sucede 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.

sábado, 3 de septiembre de 2005

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.