Introducción
Este post tiene como objetivo mostrar una serie de trucos o tips sobre Java que no son más que nuevan funcionalidades que han ido saliendo con el paso de los años y que hoy recojo aquí con la intención de mostrar las más útiles y ejemplos de uso. Pertenece a una serie de post que siguen el mismo objetivo, puedes buscar en blog los demás post.
Try with resources
Desde Java 7 existe la fórmula try-with-resources que permite vincular el cerrado de recursos a la conclusión del try, de modo que no se nos olvide hacerlo manualmente.
| |
Como se puede observar, definimos los recursos que deben ser cerrados automáticamente después del try y entre paréntesis. Podemos incluir varios recursos separándolos por punto y coma. Al escribirse de esta forma se llamará al método close del BufferedReader al acabar la ejecución del bloque, se produzcan errores o no.
Todos los recursos que se utilicen dentro de un try-with-resources deben implementar la interfaz AutoCloseable, la cual tiene un único método close que define cómo se debe cerrar el recurso.
Fechas
LocalDateTime, LocalTime, LocalDate, TimePoint
| |
Formateado de fechas
| |
Genéricos
El término “Generic” viene a ser como un tipo parametrizado, es un tipo de dato especial del lenguaje que permite centrarnos en el algoritmo sin importar el tipo de dato específico que finalmente se utilice en él. Muchos algoritmos son los mismos, independientemente del tipo de dato que maneje. Por ejemplo, un algoritmo de ordenación.
Se llaman parametrizados porque el tipo de dato con el que opera la funcionalidad se pasa como parámetro. Pueden usarse en clases, interfaces y métodos, denominándose clases, interfaces o métodos genéricos respectivamente. En Java, la declaración de un tipo genérico se hace entre símbolos <>, pudiendo definir uno o más parámetros, por ejemplo:
- E – Element (usado bastante por Java Collections Framework).
- K – Key (usado en mapas).
- N – Number (para números).
- T – Type (representa un tipo, es decir, una clase).
- V – Value (representa el valor, también se usa en mapas).
- S, U, V etc. – usado para representar otros tipos
Ejemplo de clase genérica
| |
Ejemplo de método genérico
| |
Interfaces funcionales
En Java, se considera interfaz funcional a toda interfaz que contenga un único método abstracto. Es decir, interfaces que tienen métodos estáticos o por defecto (default) seguirán siendo funcionales si solo tienen un único método abstracto.
Ejemplo:
| |
La anotación @FunctionalInterface denota que es una interfaz funcional, pero es opcional y, aunque no estuviese, la interfaz seguiría siendo funcional. Sería buena práctica mantenerla para recordar que se trata de una interfaz funcional (no de una clase abstracta), y que en caso de añadir más métodos nos lanzaría un error de compilación.
Lambdas
Las lambdas fueron introducidas a partir de Java 8. No son más que funciones anónimas que nos permiten programar en Java con un estilo más funcional y, en ocasiones, declarativo.
La sintaxis de una lambda es la siguiente:
| |
El operador flecha (->), es característico de las lambda y separa los parámetros del cuerpo de la función. No es necesario incluir el tipo ya que este puede ser inferido. El paréntesis de los parámetros puede omitirse cuando sólo existe un parámetro y no incluimos el tipo. Si no hay parámetros los paréntesis son necesarios.
| |
En el caso del cuerpo, si solo tenemos una sentencia, podremos omitir las llaves y el return, por ejemplo:
| |
Si tenemos más de una, las llaves serán necesarias:
| |
¿Donde usar las lambdas?
Las lambdas se pueden usar en cualquier parte que acepte una interfaz funcional. La lambda tendrá que corresponder con la firma del método abstracto de la interfaz funcional.
Se pueden asignar a variables tipadas:
| |
Pueden ser parte del return de un método:
| |
En llamadas a métodos:
| |
Referencias a métodos
Cuando un método coincida con la firma de una interfaz funcional, podremos usar una referencia al método en vez de la sintaxis habitual de las lambdas
| |
Para usar referencias a métodos, ponemos (::) justo antes del método, en vez de un punto, e ignoramos los paréntesis. Así pues, estas podrían ser referencias válidas a métodos:
| |
Data processing Streams
Desde Java 8 podemos hacer uso de estas herramientas para simplificar la forma en la que interactuamos con las colecciones, evitando que tengamos que realizar bucles complejos. Nos permiten hacer operaciones paralelizables, concatenando instrucciones con un estilo declarativo.
Para utilizarlas sera necesario llamar a stream() o parallelStream(), en función de si queremos paralelizar las operaciones o no.
Un stream simplemente recibe los datos de una colección y genera un resultado tras el procesado de las operaciones intermedias. Estas operaciones intermedias devuelven un stream, por lo que será necesario ejecutar una operación terminal para que las intermedias se ejecuten y poder obtener un resultado.
Un ejemplo para verlo más claro:
| |
En el ejemplo anterior hemos visto algunas operaciones intermedias como son filter(), entre otras, y a demás el bloque de instrucciones termina con un collect(), que se trata de una operación terminal que nos permite pasarle un parámetro de tipo Collector, y que en este caso con el toList() conseguimos que se nos devuelva una lista como teníamos al principio.
Las operaciones intermedias se pueden clasificar en:
- FIltrado
- Búsqueda
- Mapeado
- Matching
- Reducción
- Iteración
Los streams pueden ser utilizados para más propósitos, como pueda ser un array:
| |
O para convertir un fichero en un stream de líneas:
| |
También se pueden crear Stream a partir de valores usando Stream.of
Optional
A partir de Java 8 tenemos implementado en Java el patrón option. Se basa en indicar que se puede devolver o no el valor esperado obligando a que tengamos contemplados ambos escenarios.
La clase Optional en Java nos dispone de constructor, en su lugar usaremos sus métodos de factoría estáticos para crear los optional. Veamos:
| |
La clase Optional nos proporciona una serie de métodos:
| |