Java – Proyecto Coin (JSR 334) – Cambios en el lenguaje para aumentar la productividad

Uno de los cuatro JSRs (Java Especification Requests) que se incluyen en el nuevo JDK 7 es el JSR 334, conocido también como el Proyecto Coin.

Esta propuesta, titulada como “Small Enhancements to the JavaTM Programming Language” (en español: pequeñas mejoras en el lenguaje de programación JavaTM), es sucesora del antiguo JSR 201 (incluido en el JDK 5) el cual fue responsable de notables extensiones en el lenguaje Java al introducir Enumeraciones, Autoboxing, un bucle for mejorado (llamado también: “for each”) e importaciones estáticas, todas estas muy bien recordadas y agradecidas hasta el día de hoy.

El objetivo fue, sigue y seguirá siendo, realzar la productividad del programador con una nueva sintaxis simplificada (usualmente denominado “azúcar sintáctico”) que a grandes rasgos es capas de resolver los problemas del día a día evitando código innecesario o repetitivo mejorando la legibilidad y  así fomentar un desarrollo con menor margen de error. Todo esto gracias a extender el análisis en tiempo de compilación, y al menos por ahora sin romper la retrocompatibilidad.

La frase que resume el objetivo:

Making things programmers do everyday easier.

Proyecto Coin (JSR 334)


A continuación los pequeñas mejoras introducidas en el lenguaje.

Strings en estructuras de control switch

Ahora se pueden evaluar cadenas de caracteres (Strings) mediante estructuras de control switch. Un ejemplo:

...
String color = "celeste";

switch (color) {    //evaluación del string
    case "blanco":
        System.out.println("#FFFFFF");
        break;
    case "celeste":
        System.out.println("#00FFFF");
        break;
    case "amarillo":
        System.out.println("#FFFF00");
        break;
    default:
        System.out.println("Color desconocido");
}
...

Un inconveniente aquí es como ignorar las mayúsculas de ser necesario. Podríamos intentar aplicarlo así:

...
case "BLANCO":
case "blanco":
    System.out.println("#FFFFFF");
    break;
case "CELESTE":
case "celeste":
    System.out.println("#00FFFF");
    break;
case "AMARILLO":
case "amarillo":
    System.out.println("#FFFF00");
    break;
...

De esta forma las cadenas “celeste” y “CELESTE” serian validas pero no en el caso de una cadena “CeLeStE” o “cElEsTe”, y crear un case para todas las posibilidades de la cadena seria inadecuado por lo cual esta estructura switch no es aplicable en todos los casos.

Sin embargo, cuando su uso es posible, puede evitar el constante anidamiento de las típicas sentencias if-then-else para mejorar la legibilidad y potencialmente mejorar el rendimiento en este tipo de operaciones basadas en cadenas de caracteres.

Otros inconvenientes del switch es que no puede evaluar un caso null, y con esta nueva capacidad de manejar cadenas ah dejado más compleja la tarea del compilador.

Literales numéricos de tipo binario

Junto a las previamente posibles representaciones numéricas: “1” (Decimal), “01” (Octal), y “0x1” (hexadecimal), se añade la posibilidad de utilizar “0b1” para representar literalmente un número binario.

...
int binario = 0b00000001;    //binario=00000001 - decimal=1
System.out.println("El valor decimal de binario es: " + binario);
System.out.println("El valor decimal de 00000010 es: " + 0b00000010);
System.out.println("El valor decimal de 00000011 es: " + 0B00000011);    //se puede utilizar "B" como "b"
...

En algunos casos resulta más natural la expresión binaria y elimina la necesidad de transformar en hexadecimal.

Barra baja en literales numéricos

Las personas tienen que lidiar cotidianamente con números que están divididos por separadores, por ejemplo:

Numero telefónico:  123-123-123
Tarjeta de crédito: 1234-1234-1234-1234
Dinero: $1.000.000

Estos separadores simplifican el trabajo de visualización al ojo humano permitiendo recordar con facilidad.

Desde el JDK 7 se puede utilizar el símbolo de la barra baja “_” en el valor numérico de un dato declarado para mejorar su legibilidad. Solo se permite entre dígitos y no al principio o al final. Esta inclusión no afectará el valor, el compilador simplemente ignorará este símbolo.

...
int telNumber = 123_123_123;
long creditCard = 1234_1234_1234_1234L;
int money = 1_000_000;
...

Control de excepciones

El sistema de control de excepciones se ah visto mejorado por dos pequeñas pero importantes reformas:

  • Captura de múltiples excepciones

Se puede utilizar solo una clausula de catch para capturar varios tipos de excepciones evitando tener que crear cada catch para cada excepción como siempre fue usual de hacer.

Si se considera un caso típico, muchas veces cuando manejamos diferentes excepciones puede suceder que esperamos actuar en consecuencia para todas de la misma manera, por ejemplo:

...
try {
    //realiza una operación arriesgada
} catch (A ex1) {
    log.append(ex1);    //manejar la excepción A
} catch (B ex2) {
    log.append(ex2);    //manejar la excepción B
} catch (C ex3) {
    log.append(ex3);    //manejar la excepción C
}
...

Sin duda se ve repetitivo. Quizás a alguien se le podría ocurrir tratar de simplificar el código del ejemplo anterior capturando java.lang.Exception de la cual extienden las demás excepciones:

...
try {
    //realiza una operación arriesgada
} catch (Exception ex) {
    log.append(ex);    //manejar todas las excepciones
}
...

Aunque el código ahora es más claro, puede estar generando más problemas de los que debería de resolver porque esta capturando las excepciones que esperaba y además cualquier otra excepción que pudiera ocurrir. En general no es adecuado capturar más excepciones de las que se espera, a esto se le considera mala practica e incluso un “anti-patron” de diseño.

Pero desde el JDK7, la nueva sintaxis permite una mejor solución para estos casos:

...
try {
    //realiza una operación arriesgada
} catch (A | B | C ex) {
    log.append(ex);    //maneja la excepción A, B y C
}
...

Esto evita la presencia del código redundante mejorando el mantenimiento y legibilidad, sin tener que recurrir a malas practicas como la mencionada anteriormente.

  •  Precisión al regenerar excepciones

Permite regenerar sub-clases de la excepción capturada sin la necesidad de tener que añadir esta en la clausula throws de la declaración del método.

Aunque no es una situación típica, si se necesita capturar todas las excepciones posibles y realizar alguna tarea antes de regenerar la excepción capturada, se puede capturar la clase java.lang.Throwable de la cual extienden todos los errores y excepciones. Esto significaría tener que añadir también a Throwable en la clausula throws de la declaración del método y propagarla hacia arriba en la pila de llamadas. Esto desde el JDK7 ya no es necesario. Este es un ejemplo:

...
class FinalRethrow {

    //extiende Exception
    static class SomeException extends Exception {
    }

    //extiende Exception
    static class AnotherException extends Exception {
    }

    //punto de entrada
    public static void main(String[] args) {
        try {
            catchAll();
        } catch (SomeException | AnotherException ex) {
            ex.printStackTrace();
        }
    }

    //no estamos forzados a propagar Throwable, solo las sub-clases en las que estamos interesados
    static void catchAll() throws SomeException, AnotherException {
        try {
            //realizar operaciones arriesgadas
            doSomething();
            doAnotherThing();
        } catch (final Throwable ex) {    //capturamos Throwable
            //algo para hacer aquí...
            throw ex;    //re-genera el sub-tipo de excepción
        }
    }

    //propaga SomeException
    static void doSomething() throws SomeException {
        throw new SomeException();    //genera SomeException
    }

    //propaga AnotherException
    static void doAnotherThing() throws AnotherException {
        throw new AnotherException();    //genera AnotherException
    }
}
...

El resultado de la traza:

FinalRethrow$SomeException
at FinalRethrow.doSomething(FinalRethrow.java:45)
at FinalRethrow.catchAll(FinalRethrow.java:34)
at FinalRethrow.main(FinalRethrow.java:24)

El compilador es lo suficientemente inteligente para saber que excepciones necesitan ser capturadas, y permitirá regenerar la excepción que sea capturada aunque Throwable no este declarado con la clausula throws en el método.

El uso de la clave final en un bloque catch no es algo nuevo, se podía utilizar desde antes, y como lo sugiere su semántica significa que una excepción que es final no puede reasignarse. En este contexto de regeneración final de una excepción se sigue utilizando la clave final con el mismo objetivo, le permite al compilador saber que el tipo de excepción capturado no podrá ser alterado ya que de lo contrario se podría confundir al compilador en la inferencia del sub-tipo de excepción que debe de regenerar. Sin embargo no es obligatorio utilizar la clave final, posiblemente la excepción es implícitamente tratada como final por el compilador.

Resumiendo, con esta nueva posibilidad si se tiene un método que maneja de la misma forma varias excepciones que tienen todas una clase base en común y luego de realizar alguna operación (por ejemplo guardar logs) las necesita regenerar propagandolas, puede manejarlas todas capturando simplemente la clase base y propagando solo  las excepciones de las sub-clases.

Esto puede parecer en su utilidad algo similar al control de múltiples excepciones explicado antes.

Inferencia de tipos en la creación de instancias genéricas

Los Generics (tipos genéricos o parametrizados), introducidos en el JDK5 años atrás, aunque sin ser perfectos han tenido un efecto positivo en el lenguaje al mejorar la seguridad de tipos evitando potenciales errores en tiempo de ejecución. Una desventaja es que le agregan cierta complejidad a la sintaxis, y ese es el motivo aquí para tratar de corregir en parte su utilización.

Estos son ejemplos típicos de su uso:

...
List list = new ArrayList();
Map> map = new HashMap>();
...

Ahora gracias a la inferencia de tipos, podemos escribir lo anterior de la siguiente forma:

...
List lista = new ArrayList<>();
Map> tabla = new HashMap<>();
...

Se utiliza el operador diamante “<>” en la construcción para indicar que se infieren los mismos tipos de la declaración. Esto elimina código repetitivo y lo hace más agradable a la vista.

 Gestión automática de recursos

También conocido por su sigla en ingles ARM (Automatic Resource Management), se trata de un nuevo tipo de  sentencia comprendida por el compilador, se denomina try-with-resources y pretende simplificar el trabajo del la ya conocida try-catch-finally que se utiliza sobre flujo de datos hacia archivos, conexiones a bases de datos, etc. La idea consiste en que no se necesita escribir un bloque tipo finally para asegurarnos de cerrar los flujos de datos abiertos, ya que esto ocurre implícitamente sin este.

Tomará como ejemplo un pequeño código que escribe la mítica frase “HolaMundo” en un archivo test.txt en el directorio Home del usuario. Este es un sencillo caso de try-catch-finally:

...
String filePath = System.getProperty("user.home") + File.separator + "test.txt";
BufferedWriter bw = null;
try {
    //abrir flujo de datos hacia el destino
    bw = new BufferedWriter(new FileWriter(filePath));
    bw.write("Hola Mundo");
} catch (IOException ex) {    //en caso de excepcion
    ex.printStackTrace();
} finally {    //cerrar los recursos abiertos
    if (bw != null) {
        try {
            bw.close();
        } catch (IOException ex) {
            //nada que hacer...
        }
    }
}
...

Con el nuevo try-with-resources  se podría escribir de esta forma:

...
String filePath = System.getProperty("user.home") + File.separator + " test.txt";
try (BufferedWriter bw = new BufferedWriter(new FileWriter(filePath))) {    //abrir flujo de datos hacia el destino
    bw.write("Hola Mundo");
    bw.newLine();
} catch (IOException ex) {    //en caso de excepción
    ex.printStackTrace();
}
...

En este ejemplo el recurso en cuestión era unicamente bw, un objeto BufferedReader. Sin embargo se puede declarar más de un recurso en la sentencia try-with-resources  y todos estos serán cerrados automáticamente. La única condición es que deben implementar la interface java.lang.AutoCloseable.

Incluso se puede seguir utilizando el bloque finally al final de la sentencia try-with-resources, se ejecutará cuando los recursos creados en el try se hayan cerrado.

 Métodos con argumentos variables y parámetros formales no disponibles en tiempo de ejecución

Esto se trata de una mejora en el compilador para el mejor reconocimiento a través de advertencias sobre un problema que involucra dos extensiones incluidas en el lenguaje desde el JDK5: los Generics (tipos genéricos o parametrizados)  y los argumentos variables (varargs). Ambas muy útiles y con buena aceptación pero también con sus respectivos inconvenientes.

Para comenzar, los tipos genéricos como ArrayList<Sring> o HashMap<Integer> son tipos no cosificables (en ingles: non-reifiable), es decir no están completamente disponibles en tiempo de ejecución. En tiempo de compilación ocurre un proceso llamado “borrado de tipos” (en ingles: tipe erasure) en el cual el compilador borra la información relacionada con los tipos de parámetros y tipos de argumentos, esto es necesario para mantener la retrocompatibilidad con librerías antiguas a la introducción de los Generics.

Puede resultar posible que a una variable de un tipo genérico se le refiera otra variable que no lo es. Esta situación se denomina “polución del montículo” (heap pollution), y ocurre cuando el programa realiza una operación que pueda generar una advertencia no controlada (unchecked warning). Una advertencia no controlada es generada si en tiempo de compilación o en tiempo de ejecución la corrección de una operación que involucre un tipo generico, en un casting o la llamada a un método, no puede ser verificada. Un simple ejemplo:

...
List l = new ArrayList();
List ls = l;                  //warning: [unchecked] unchecked conversion
l.add(0, new Integer(42));    //warning: [unchecked] unchecked call to add(int,E) as a member of the raw type List
String s = ls.get(0);         //ClassCastException
...

Esta asignación de un tipo List<Number> con List<String>, genera una advertencia, pero por retrocompatibilidad el borrado de tipos convierte los dos tipos a List y entonces el compilador permite la operación. Esto es una situación de heap pollution.

El compilador también genera una advertencia en el método l.add(), pero no puede determinar si la variable l refiere al tipo List<String> o List<Number>, luego del borrado de tipos cualquier Object puede ser añadido a la lista,  por lo que ocurre otra situación de heap pollution. Y como trágico final, el ultimo método ls.get() es una sentencia valida en tiempo de compilación, pero en tiempo de ejecución genera una inesperada ClassCastException.

Justamente un caso de excepciones no controladas es cuando se invoca a un método que recibe argumentos variables de tipos genéricos, la información del tipo en el parámetro del método no esta completamente disponible en tiempo de ejecución. La extensión en el lenguaje para argumentos variables esta implementada a través de arreglos, es decir el compilador convierte los argumentos variables en arreglos, y en estos los tipos son guardados internamente para ser usados en tiempo de ejecución. Sin embargo el lenguaje Java no admite crear arreglos de tipos genéricos, y los arreglos no pueden guardar la información necesaria para representar un tipo generico. Existen varios métodos en la API de Java que generan estas advertencias, este es uno de ellos:

public static <T> boolean Collections.addAll(Collection<? super T> c, T... elements)

Un ejemplo de su uso:

...
List stringList1 = new ArrayList();
List stringList2 = new ArrayList();
List> allStringLists = new ArrayList>();

Collections.addAll(stringList1, "string1", "string2", "string3");
Collections.addAll(stringList1, "string4", "string4", "string6");
//warning: [unchecked] unchecked generic array creation of type java.util.List[] for varargs parameter
Collections.addAll(allStringLists, stringList1, stringList2);
...

Por supuesto que los métodos de la API son seguros, y estas advertencias son inútiles para estos casos. Por lo tanto el usuario puede legalmente suprimir las advertencias, en versiones anteriores al JDK7 se puede utilizar la ya conocida anotación @SuppressWarnings("unchecked").

Esencialmente estas advertencias indican la posibilidad de heap pollution en un método, y que consecuentemente se pueden obtener inesperadas excepciones ClassCastException. En los compiladores de Java SE 5 y 6 la llamada a un método conflictivo genera estas advertencias, pero no sucede lo mismo sobre la declaración del método. A todo esto, en el JDK7  se sigue el razonamiento de que la declaración un método conflictivo no es en realidad el causante de una situación de heap pollution, pero su existencia si contribuye a que esto pueda ocurrir, por lo que el compilador genera también una advertencia en la declaración de dichos métodos.

...
//JDK7 - warning: [unchecked] Possible heap pollution from parameterized vararg type List
public static void faultyMethod(List... l) {
    Object[] objectArray = l;
    objectArray[0] = Arrays.asList(new Integer(42));    //heap pollution
    String s = l[0].get(0);                             //ClassCastException
}
...
//JDK5-6 - warning: [unchecked] unchecked generic array creation for varargs parameter of type List[]
faultyMethod(Arrays.asList("Hola"), Arrays.asList("Mundo"));
...

En adición, también se introduce una nueva anotación especifica para suprimir estas advertencias. En un método conflictivo hay tres posibilidades para suprimir las advertencias generadas por el compilador:

  • Utilizar la anotación @SafeVarargs, en métodos estáticos, finales y no constructores. Esto suprime también las advertencias desde las llamadas al método, por lo que se asume que no se realizan operaciones inseguras con los parámetros. Métodos conflictivos de la API de Java como el visto anteriormente lo utilizan.
  • Utilizar la anotación @SuppressWarnings({"unchecked", "varargs"}). Al contrario que la anterior, esta no suprime las advertencias en las llamadas al método.
  • Utilizar la opción -Xlint:varargs en el compilador.

– – – – – – – – – – – – – – – – – –

Se llegaron a conocer también algunas mejoras que al final no fueron incluidas para la versión 7, por ejemplo: el operador seguro (null-safe) para no actuar sobre referencias nulas, o la construcción y asignación inmediata en colecciones al igual como se puede hacer con arreglos.

Para Java 8 se introducirá el proyecto Coin II, en el cual seguramente estarán las mejoras que no pudieron concretarse en Java 7 junto a otras nuevas.


Más información:
* Oracle – Enhancements in Java SE 7


, , , , , , , , ,

  1. Deja un comentario

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: