Java 8 es la próxima versión del lenguaje, y contendrá muchas mejoras. Su característica más conocida son los lambdas, que se pueden usar para instancias interfaces funcionales (interfaces con 1 solo método) con una sintaxis concisa. Sin embargo, Java 8 también incluye muchas otras novedades interesantes, que no tienen tanta prensa. ¡Veamos juntos que hay en Java 8 más allá de los lambdas!
Interfaces funcionales
Quizás el agregado más importante de Java 8 sean los lambdas. Muy relacionado a los lambdas se encuentra el concepto de "interfaces funcionales". Una interfaz es funcional si declara exactamente 1 método abstracto. Por ejemplo, java.lang.Runnable es una interfaz funcional porque define un único método abstracto.
Los métodos predeterminados (marcados con "default") no son abstractos, por lo cual una interfaz funcional puede definir varios métodos predeterminados.
Se agrega una nueva anotación: @FunctionalInterface. Esta anotación puede usarse para declarar la intención que la interfaz es funcional. Esta anotación la utiliza el compilador, y sirve para evitar error y agregar métodos a interfaces que se desean sean funcional. Es como la anotación @Override, que denota intención.
Lambdas
Una característica única de las interfaces funcionales es que pueden ser instanciadas usando "lambdas". Veamos algunos ejemplos de lambdas.
Una lista de entradas separada por comas, indicando el tipo de datos, y un bloque que retorna la suma de ambos parámetros:
(int x, int y) -> { return x + y; }
Una lista de entradas separada por comas, infiriendo el tipo de datos, y retorna la suma de ambos:
(x, y) -> x + y
Un único parámetro con el tipo de dato inferido, y retorna ese parámetro al cuadrado:
x -> x * x
Sin valores de entrada, retorna un valor:
() -> x
Un único parámetro de entrada con el tipo de datos inferido, y un bloque que retorna void a la derecha:
x -> { System.out.println(x); }
También hay algunas formas "cortas" de escribir lambdas comunes:
String::valueOf x -> String.valueOf(x) Object::toString x -> x.toString() x::toString () -> x.toString() ArrayList::new () -> new ArrayList<>()
Por supuesto, en Java existe la sobrecarga de métodos. Los operadores lambda intentan usar el método que mejor se adapte a la expresión: se tienen en cuenta los valores de entrada, de salida y las excepciones declaradas.
Por ejemplo:
Comparator c = (a, b) -> Integer.compare(a.length(), b.length());
El método "compare" de Comparator tiene dos parámetros de entrada , y retorna un int. Esto es consistente con el lambda de la derecha, por lo cual es una asignación válida.
Runnable r = () -> { System.out.println("Running!"); }
El método "run" de la interfaz Runnable no recibe parámetros de entrada, y retorna void. Esto es consistente con el lambda de la derecha, y resulta en una asginación válida.
También se tienen en cuenta las excepciones chequeadas declaradas en la firma del método. El lambda sólo puede lanzar excepciones chequeadas si están declaradas en la interfaz.
Ejemplos de uso de lambdas en Java 8
Actualmente, para agregarle un listener a un botón podemos hacer:btn.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { System.out.println("Hola, mundo!"); } });Con expresiones lambda de Java 8 podemos hacer exactametne lo mismo con la siguiente expresión:
btn.setOnAction( event -> System.out.println("Hola, mundo!") );También se puede usar el operador :: para hacer referencia a métodos estáticos de una clase:
public class Utils { public static int compareByLength(String in, String out){ return in.length() - out.length(); } } public class MyClass { public void doSomething() { String[] args = new String[] {"microsoft","apple","linux","oracle"} Arrays.sort(args, Utils::compareByLength); } }... o también podemos acceder a métodos no estáticos:
public class MyClass implements Comparable<MyObject> { @Override public int compareTo(MyObject obj) {return ...} public void doSomething() { MyObject myObject = new MyObject(); Arrays.sort(args, myObject::compareTo); } }
La página con la documentación de expresiones lambda tiene mucha más información.
Métodos predeterminados en las interfaces
Las interfaces ahora podrán definir métodos estáticos. En las bibliotecas de Java, es bastante común que si existe una interfaz Foo, exista también una clase utilitaria Foos que contiene métodos estáticos para realizar tareas con instancias de Foo (por ejemplo, la interfaz Collection y la clase Collections). Con esta nueva característica, todos estos métodos pueden ubicarse directamente en la interfaz.
Otro cambio más importante es que ahora se pueden definir implementaciones predeterminadas para los métodos, usando la palabra clave default. Por ejemplo, se agregó el método forEach a la interfaz java.lang.Iterable:
public default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } }
Hasta ahora, no se podía agregar métodos a una interfaz (sin que impacte en quienes implementaba dicha interfaz, ya que debían implementar los nuevos métodos). A partir de ahora, los proveedores de la interfaz la pueden extender y usar implementaciones predeterminadas, lo cual permite evolucionar una interfaz y a la vez mantener la compatibilidad con versiones anteriores. En el JDK 8 hay muchas interfaces clásicas que fueron extendidas con métodos predeterminado
Nuevo API de fechas
Hace rato que se mencionaba la necesidad de un nuevo API de fechas/horas/calendarios que tenga una mejor interfaz que el (viejo y odiado) java.util.Date. Para esto llega el nuevo paquete
java.time
que contendrá al nuevo API. Les va a resultar facil de usar, especialmente para los que conozcan
JodaTime.
Prácticamente todo lo que expone el API es inmutable, para evitar las confusiones que presente el API actual.
La integración con el API actual de fechas es muy simple a partir de nuevos métodos:
Las nuevas clases más importantes:
-
Instant
es, básicamente, un timestamp numérico. Es una clase útil para realizar logs y usar para frameworks de persistencia. -
LocalDate
sirve para almacenar una fecha sin hora. Por ejemplo, un cumpleaños como "16-12-1979". -
LocalTime
sirve para almacenar una hora sin fecha. Por ejemplo, un horario de apertura a las "9:30". -
LocalDateTime
sirve para almacenar una fecha con hora. Por ejmplo, puede almacenar "1979-12-16T12:30". -
ZonedDateTime
almacena hora y fecha con información de uso horario.
Algunos ejemplos de código:
//imprimir la fecha actual LocalDate date = LocalDate.now(); System.out.printf("%s-%s-%s",date.getYear(), date.getMonthValue(), date.getDayOfMonth()); //vamos a sumar 5 horas LocalTime time = LocalTime.now(); LocalTime newTime; newTime = time.plus(5, HOURS); // o también newTime = time.plusHours(5); // o también Period p = Period.of(5, HOURS); newTime = time.plus(p);
La página de la documentación de java.time y este artículo introductorio a java.time tienen más información al respecto.
java.util.stream
Este nuevo API provee utilidades para para permitir operaciones "estilo programación funcional" sobre flujo de valores. La forma más simple de obtener un Stream seguramente sea a partir de una colección:
Stream<T> stream = collection.stream();
Los stream son parecidos a los iteradores. Los valores van "fluyendo" (piensen en el agua que fluye en una canilla abierta), y se van. Los stream sólo pueden recorrerse 1 vez. Los stream también pueden ser infinitos.
Más interesante es que los stream pueden ser secuenciales o paralelos, característica que puede cambiarse usando los métodos stream.sequential() o stream.parallel(). Las acciones en un stream secuencial ocurren de forma secuencial en un único thread. En cambio, las acciones sobre un stream paralelo pueden ocurrir todas a la vez en múltiples threads.
Un ejemplo más concreto de uso de streams:
int sumOfWeights = blocks.stream().filter(b -> b.getColor() == RED) .mapToInt(b -> b.getWeight()) .sum();
Los stream tienen un API fluído que permite encadenar invocaciones. Hay dos tipos de invocaciones: "intermedias" y "finales". Las invocaciones intermedias permiten continuar con el flujo de las operaciones, mientras que las operaciones terminales "consumen" al stream y se tienen que invocar para finalizar la operacion. sum() es una operación terminal.
En general, el uso de streams involucra:
- Obtener un stream.
- Realizar una o más operaciones intermedias.
- Realizar una operación final.
Las operaciones intermedias no se ejecutan de forma inmediata, sino que son lazy. La operación final es quien desencadena el procesamiento de los elementos del stream.
Pueden leer más en la documentación sobre los streams.
Repetición de anotaciones
Este es uno de los cambios "menores" de Java 8, que sin embargo resulta muy práctico para algunas situaciones. Consiste en que será posible anotar un objeto muchas veces con la misma anotación.
Un ejemplo del tutorial:
@Alert(role = "Manager") @Alert(role = "Administrator") public class UnauthorizedAccessException { ... }
Para que esto funcione, la anotación @Alert tiene que estar anotada con java.lang.annotation.Repeatable.
Es posible que deba revisarse el código de algunas herramientas que dependían en el procesamiento de anotaciones. Se agregaron nuevos métodos al JDK como
AnnotatedElement.getAnnotationsByType(Class)
y
AnnotatedElement.getDeclaredAnnotationsByType(Class)
que permiten descubrir las anotaciones repetidas de un elemento.
Pueden leer más en la documentación de anotaciones repetibles.
Eliminación del espacio PermGen
Este es uno de los cambios grandes: el espacio de memoria PermGen (Permanent Generation) se elimina completamente, y se reemplaza por un espacio nuevo llamado Metaspace.
La consecuencia obvia es que se ignorarán los parámetros de la JVM "PermSize" y "MaxPermSize", y por otro lado dejaremos de ver el java.lang.OutOfMemoryError: PermGen error.
Obviamente, esto no significa que no tendremos que preocuparnos por el uso de memoria. El espacio Metaspace cambiará de tamaño dinámicamente dependiendo de la demanda de la aplicación en tiempo de ejecución. Aparecen nuevos parámetros para la JVM que nos permitirán limitar el tamaño del Metaspace, si así lo deseamos.
Les recomiendo leer el artículo From PermGen to Metaspace para entender los detalles de este cambio.
Nuevo método para ordenar arrays en paralelo
El método Arrays.parallelSort es nuevo, y utiliza el framework Fork/Join de Java 7 para asignar las tareas de ordenamiento a varios threads disponibles en el pool de threads.
Para pocos elementos seguirá siendo conveniente el algoritmo tradicional de ordenamiento, pero se puede lograr una diferencia muy importante en rendimiento cuando tratamos con Arrays de miles de elementos. Les recomiendo leer Arrays.sort versus Arrays.parallelSort para ver una comparativa de rendimiento de ambos métodos.
Nashorn, el nuevo engine JavaScript
Java 8 incluirá un nuevo engine JavaScript (denominado "Nashorn"), que tiene mucho mejor rendimiento que el actual Rhino. La presentación Project Nashorn in Java 8 contiene diapositivas que explican las características de este nuevo engine, y ejemplos de código de integración con Java.