7. Java moderno¶
Desde Java 8 el lenguaje ha incorporado herramientas que hacen el código más corto y claro. Las vas a encontrar continuamente en la documentación y en los ejemplos del módulo.
7.1 Lambdas e interfaces funcionales¶
Una interfaz funcional es una interfaz con un único método abstracto. Una lambda es una forma abreviada de escribir una implementación de ese método, sin crear una clase:
// Con una clase anónima (forma antigua)
Comparator<String> porLongitud = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
};
// Con una lambda: mismo significado
Comparator<String> porLongitud2 = (a, b) -> Integer.compare(a.length(), b.length());
Sintaxis¶
| Forma | Ejemplo |
|---|---|
| Sin parámetros | () -> System.out.println("Hola") |
| Un parámetro (paréntesis opcionales) | x -> x * 2 |
| Varios parámetros | (a, b) -> a + b |
| Varias instrucciones | (a, b) -> { int s = a + b; return s; } |
Interfaces funcionales más comunes¶
| Interfaz | Método | Recibe → devuelve | Ejemplo |
|---|---|---|---|
Runnable |
run() |
nada → nada | () -> System.out.println("tarea") |
Supplier<T> |
get() |
nada → T |
() -> new Random().nextInt(10) |
Consumer<T> |
accept(t) |
T → nada |
s -> System.out.println(s) |
Function<T,R> |
apply(t) |
T → R |
s -> s.length() |
Predicate<T> |
test(t) |
T → boolean |
n -> n % 2 == 0 |
BiFunction<T,U,R> |
apply(t, u) |
T, U → R |
(a, b) -> a * b |
Comparator<T> |
compare(a, b) |
T, T → int |
(a, b) -> a.compareTo(b) |
Predicate<Integer> esPar = n -> n % 2 == 0;
System.out.println(esPar.test(4)); // true
Predicate<Integer> esParYPositivo = esPar.and(n -> n > 0);
Runnable: el puente con el Tema 2
Un hilo ejecuta un Runnable. Con lambdas, crear y lanzar un hilo es una línea:
Referencias a métodos¶
Cuando la lambda solo llama a un método existente, se puede abreviar con ::
| Lambda | Referencia a método |
|---|---|
s -> System.out.println(s) |
System.out::println |
s -> s.toUpperCase() |
String::toUpperCase |
s -> Integer.parseInt(s) |
Integer::parseInt |
() -> new ArrayList<>() |
ArrayList::new |
Las variables locales que use una lambda deben ser efectivamente finales: no se pueden modificar después de asignarlas.
7.2 Streams¶
Un stream es una secuencia de elementos sobre la que se encadenan operaciones, como en una tubería. Se escribe qué quieres obtener, no cómo recorrer la colección:
List<String> nombres = List.of("Ana", "Luis", "Marta", "Alba", "Pedro");
List<String> resultado = nombres.stream() // 1. origen
.filter(n -> n.startsWith("A")) // 2. operaciones intermedias
.map(String::toUpperCase)
.sorted()
.toList(); // 3. operación final
// [ALBA, ANA]
Un stream tiene tres partes: un origen (lista.stream(), Arrays.stream(array), IntStream.range(0, 10), Files.lines(ruta)), operaciones intermedias, que devuelven otro stream y no se ejecutan hasta que hay una final, y una operación final, que produce el resultado.
| Intermedias | Hace |
|---|---|
filter(predicado) |
Deja pasar los que cumplen la condición. |
map(función) |
Transforma cada elemento. mapToInt, mapToDouble para números. |
sorted() / sorted(comparador) |
Ordena. |
distinct() |
Elimina duplicados. |
limit(n) / skip(n) |
Toma o salta los n primeros. |
| Finales | Devuelve |
|---|---|
toList() |
Una lista (inmutable). |
forEach(acción) |
Nada; ejecuta la acción con cada elemento. |
count() |
El número de elementos. |
sum(), average(), max(), min() |
En streams numéricos (IntStream, DoubleStream). |
anyMatch, allMatch, noneMatch |
boolean. |
findFirst() |
Un Optional con el primero. |
collect(Collectors...) |
Listas, mapas, textos, agrupaciones. |
double media = alumnos.stream().mapToDouble(Alumno::getNota).average().orElse(0);
long aprobados = alumnos.stream().filter(Alumno::estaAprobado).count();
String lista = nombres.stream().collect(Collectors.joining(", ")); // "Ana, Luis, ..."
Map<Boolean, List<Alumno>> partidos =
alumnos.stream().collect(Collectors.partitioningBy(Alumno::estaAprobado));
Map<String, Long> porCurso =
alumnos.stream().collect(Collectors.groupingBy(Alumno::getCurso, Collectors.counting()));
Un stream se usa una sola vez
Después de la operación final, el stream queda consumido. Si lo necesitas otra vez, crea uno nuevo desde la colección. Los streams no modifican la colección original.
Optional¶
Algunas operaciones pueden no tener resultado (el máximo de una lista vacía, buscar algo que no existe). En lugar de devolver null, devuelven un Optional:
Optional<Alumno> mejor = alumnos.stream().max(Comparator.comparingDouble(Alumno::getNota));
mejor.ifPresent(a -> System.out.println("Mejor nota: " + a));
String nombre = mejor.map(Alumno::getNombre).orElse("Nadie");
7.3 Tipos enumerados: enum¶
Un enum define un tipo con un conjunto cerrado de valores. Es más seguro que usar números o textos sueltos:
public enum Estado {
PENDIENTE, ENVIADO, ENTREGADO, CANCELADO
}
Estado e = Estado.ENVIADO;
e.name(); // "ENVIADO"
e.ordinal(); // 1
Estado.valueOf("CANCELADO"); // texto -> enum (IllegalArgumentException si no existe)
Estado.values(); // array con todos los valores
Un enum es una clase: puede tener atributos, constructor y métodos:
public enum Operacion {
SUMA("+"), RESTA("-"), MULTIPLICACION("*"), DIVISION("/");
private final String simbolo;
Operacion(String simbolo) { this.simbolo = simbolo; }
public String getSimbolo() { return simbolo; }
public double aplicar(double a, double b) {
return switch (this) {
case SUMA -> a + b;
case RESTA -> a - b;
case MULTIPLICACION -> a * b;
case DIVISION -> a / b;
};
}
}
En un switch sobre un enum que cubre todos los valores no hace falta default: el compilador comprueba que no falte ninguno.
7.4 Registros: record (Java 16+)¶
Un record es una clase pensada para guardar datos inmutables. En una línea, Java genera los atributos private final, el constructor, los métodos de acceso, equals, hashCode y toString:
public record Producto(String nombre, double precio, int stock) { }
Producto p = new Producto("Teclado", 25.5, 10);
p.nombre(); // "Teclado" (sin "get")
p.precio(); // 25.5
System.out.println(p); // Producto[nombre=Teclado, precio=25.5, stock=10]
Se pueden añadir métodos y un constructor compacto para validar:
public record Producto(String nombre, double precio, int stock) {
public Producto { // compacto: sin parámetros
if (precio < 0) throw new IllegalArgumentException("Precio negativo");
nombre = nombre.strip();
}
public double valorStock() { return precio * stock; }
}
¿Clase o record? |
|
|---|---|
| Datos que no cambian (un punto, un mensaje, una línea leída de un fichero) | record |
| Objeto con estado que cambia (una cuenta con saldo, un contador) | Clase normal |
Inmutabilidad y concurrencia
Un objeto inmutable se puede compartir entre varios hilos sin sincronización, porque nadie puede modificarlo. Por eso los record serán muy útiles en el Tema 2.
Preguntas de repaso¶
- ¿Qué es una interfaz funcional? Nombra tres.
- Escribe una lambda que reciba un
Stringy devuelva si tiene más de 5 caracteres. ¿Qué interfaz funcional implementa? - ¿Qué diferencia hay entre una operación intermedia y una final en un stream?
- ¿Qué ventaja tiene devolver un
Optionalen lugar denull? - ¿Qué genera automáticamente un
record?
Practica