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.
// Con finally
String line = null;
BufferedReader br = new BufferedReader(new FileReader("myfile"));
try {
line = br.readLine();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (br != null) br.close();
}
// Con try-with-resources
String line = null;
try (BufferedReader br = new BufferedReader(new FileReader("myfile"))) {
line = br.readLine();
} catch (Exception e) {
e.printStackTrace();
}
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
LocalDateTime timepoint = LocalDateTime.now(); // Fecha y hora actual
LocalDate date = LocalDate.of(2020, Month.JULY, 27); // Obtenemos la fecha indicada
LocalTime.of(17, 30); // Obtenemos la hora indicada
LocalTime.parse("17:30:00"); // Otra forma para la hora
Month month = timepoint.getMonth(); //Obtener el mes actual
int day = timepoint.getDayOfMonth(); //Obtener el número de día de actual
// TimePoint es inmutable, así que cambiar el valor retorna un nuevo objeto y podemos realizar un desarrollo más funcional
LocalDateTime happyTwenties = timepoint.withYear(1920)
.withMonth(Month.January)
.withDayOfMonth(1)
.plusWeeks(3);
Formateado de fechas
Date date = new Date(); // Fecha actual
SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy");
String date = df.format(date); //Formato final de la fecha en String
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
// GenericClass.java file
public class GenericClass<T, K> {
private T g1;
private K g2;
public GenericClass(T g1, K g2) {
this.g1 = g1;
this.g2 = g2;
}
public T getGenericOne() {
return g1;
}
public K getGenericTwo() {
return g2;
}
}
// Main.java file
public class Main{
public static void main(String[] args) throws Exception {
GenericClass<Integer, String> clazz = new GenericClass<>(1, "generic");
Integer param1 = clazz.getGenericOne();
String param2 = clazz.getGenericTwo();
System.out.println(String.format("Param1 %d - Param2 %s", param1, param2));
}
}
Ejemplo de método genérico
// WhiteBoard.java file
public class WhiteBoard {
public <T> void draw(T figure ) {
...
}
}
// Main.java file
public static void main(String[] args) throws Exception {
WhiteBoard board = new WhiteBoard();
Figure circle = new Circle(1.5);
board.draw(circle);
}
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:
@FunctionalInterface
public interface SalaryToPrettyStringMapper {
default List<String> map(List<Salary> list) {
return list.stream()
.map(this::map)
.collect(Collectors.toList());
}
String map(Salary salary);
}
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:
( tipo1 param1, tipoN paramN) -> { cuerpo de la lambda }
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.
(param1, param2) -> { cuerpo }
param1 -> { cuerpo }
() -> { cuerpo }
En el caso del cuerpo, si solo tenemos una sentencia, podremos omitir las llaves y el return, por ejemplo:
numero -> String.valueOf(numero)
Si tenemos más de una, las llaves serán necesarias:
numero -> {
String cadena = String.valueOf(numero);
return cadena;
}
¿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:
Predicate<Integer> isOdd = n -> n % 2 != 0;
isOdd.test(2); // false
Pueden ser parte del return de un método:
private Predicate<Integer> isOddPredicate() {
return n -> n % 2 != 0;
}
En llamadas a métodos:
IntStream.range(0, 2)
.mapToObj(entero -> String.format("entero = %s", entero))
.forEach(cadena -> System.out.println(cadena));
// Salida:
// entero = 0
// entero = 1
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
IntStream.range(0, 2)
.mapToObj(entero -> String.format("entero = %s", entero))
.forEach(System.out::println); // <- Referencia a método
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:
System.out::println
this::miMetodo
super::metodoDeSuper
unObjeto::suMetodo
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:
List<Other> l2 = l1.stream()
.filter(elem -> elem.getAge() < 65)
.sorted() // Ordena según la implementación de Comparable
.map(elem -> new Other(elem.getName,() elem.getAge()))
.collect(toList());
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:
int[] array = {1, 2, 3, 4, 5};
int sum = Arrays.stream(array).sum();
O para convertir un fichero en un stream de líneas:
long numberOfLines = Files.lines(
Paths.get("yourFile.txt"),
Charset.defaultCharset()
).count();
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:
public static <T> Optional<T> empty() //Devuelve objeto opcional vacío
public static <T> Optional<T> ofNullable(T value) //Devuelve objeto con valor o si es null un objeto vacío
public static <T> Optional<T> of(T value) //Devuelve objeto con valor o si es null retorna NullPointerException
La clase Optional nos proporciona una serie de métodos:
public boolean isPresent() //Indica si el objeto tiene o no valor
public T get() //Retorna el valor almacenado. Si no hay valor retorna excepción
public Optional<T> filter(Function f) //Comportamiento como con los streams
public <U> Optional<U> map(Function f) //Comportamiento como con los streams
public <U> Optional<U> flatMap(Function f) //Comportamiento como con los streams
public T orElse(T other) //Nos retorna el valor original. Si es nulo, retorna el valor que pasamos por parámetro
public T orElseGet(Function f) //Igual que el anterior pero el parámetro es una función
public <X extends Throwable> T orElseThrow(Function f) //Retorna el valor original. Si es nulo, retorna la excepción que devuelva la función pasada como parámetro