Java
Tutorial Java: Funciones lambda
Aprende la sintaxis, uso y scope de funciones lambda en Java para programación funcional y manejo eficiente de colecciones y concurrencia.
Aprende Java y certifícateSintaxis y estructura básica de expresiones lambda
Las expresiones lambda representan una de las características más importantes introducidas en Java 8, permitiendo implementar el paradigma de programación funcional. Una expresión lambda es, esencialmente, un bloque de código que puede ser pasado como argumento a métodos o almacenado en variables, similar a lo que podríamos considerar una función anónima.
La sintaxis básica de una expresión lambda en Java sigue este patrón:
(parámetros) -> expresión
o para bloques de código más extensos:
(parámetros) -> {
// Cuerpo con múltiples instrucciones
return resultado;
}
Componentes de una expresión lambda
Una expresión lambda consta de tres partes principales:
- Lista de parámetros: Encerrados entre paréntesis y separados por comas
- Operador flecha (
->
) - Cuerpo de la lambda: Una expresión o un bloque de código entre llaves
Variaciones en la sintaxis
La sintaxis de las expresiones lambda puede variar según diferentes situaciones:
- Sin parámetros:
() -> System.out.println("Lambda sin parámetros")
- Un solo parámetro (los paréntesis son opcionales):
x -> x * x
o con paréntesis:
(x) -> x * x
- Múltiples parámetros:
(x, y) -> x + y
- Con tipos de datos explícitos:
(int x, int y) -> x + y
- Expresión de una sola línea (sin llaves ni return):
(String s) -> s.length()
- Bloque de código con múltiples líneas:
(String s) -> {
int length = s.length();
return length > 5;
}
Ejemplos prácticos
Veamos algunos ejemplos prácticos de expresiones lambda en diferentes contextos:
- Ordenar una lista de cadenas por longitud:
List<String> nombres = Arrays.asList("Ana", "Juan", "Alejandro", "Eva");
// Antes de Java 8 (clase anónima)
Collections.sort(nombres, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
// Con expresión lambda
Collections.sort(nombres, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
- Filtrar elementos de una colección:
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> pares = new ArrayList<>();
// Filtrar números pares
numeros.forEach(n -> {
if (n % 2 == 0) {
pares.add(n);
}
});
- Ejecutar código en un hilo:
// Antes de Java 8
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Ejecutando en otro hilo");
}
}).start();
// Con expresión lambda
new Thread(() -> System.out.println("Ejecutando en otro hilo con lambda")).start();
Reglas de sintaxis importantes
Al trabajar con expresiones lambda, es importante tener en cuenta estas reglas:
- Inferencia de tipos: Java puede inferir los tipos de los parámetros basándose en el contexto.
// El compilador infiere que 'n' es un Integer
List<Integer> numeros = Arrays.asList(1, 2, 3);
numeros.forEach(n -> System.out.println(n));
- Return implícito: En expresiones de una sola línea sin llaves, el valor se retorna automáticamente.
// Estas dos expresiones son equivalentes
Function<Integer, Integer> cuadrado1 = n -> n * n;
Function<Integer, Integer> cuadrado2 = n -> { return n * n; };
- Paréntesis obligatorios: Si no hay parámetros o hay más de uno, los paréntesis son obligatorios.
Runnable tarea = () -> System.out.println("Tarea sin parámetros");
BiFunction<Integer, Integer, Integer> suma = (a, b) -> a + b;
- Llaves obligatorias: Si el cuerpo tiene más de una instrucción, las llaves son obligatorias.
Consumer<String> procesador = s -> {
String resultado = s.toUpperCase();
System.out.println(resultado);
// Más instrucciones...
};
Limitaciones de las expresiones lambda
Las expresiones lambda tienen algunas limitaciones en su sintaxis:
- No pueden declarar variables con el mismo nombre que variables en el ámbito externo.
- No pueden usar la palabra clave
this
para referirse a la instancia de la lambda (se refiere a la clase contenedora). - No pueden usar
break
ocontinue
para salir de la lambda.
// Esto NO compila
int contador = 0;
Runnable r = () -> {
int contador = 1; // Error: Variable 'contador' ya está definida
System.out.println(contador);
};
Las expresiones lambda proporcionan una forma concisa y expresiva de implementar interfaces funcionales, lo que permite escribir código más limpio y legible, especialmente cuando se trabaja con colecciones y operaciones de procesamiento de datos.
Contextos de uso y target typing
Las expresiones lambda en Java no existen de forma aislada, sino que siempre se utilizan en un contexto específico. A diferencia de lenguajes como JavaScript o Python, donde las funciones son ciudadanos de primera clase, en Java las lambdas están vinculadas al concepto de interfaces funcionales.
El compilador de Java utiliza un mecanismo llamado target typing (o tipado objetivo) para determinar qué interfaz funcional está implementando una expresión lambda. Este mecanismo permite que el compilador infiera los tipos de los parámetros y el tipo de retorno basándose en el contexto donde se utiliza la lambda.
Interfaces funcionales como objetivos
Para que una expresión lambda pueda ser utilizada, debe existir un "objetivo" que sea una interfaz funcional - una interfaz que contiene exactamente un método abstracto. Algunos ejemplos comunes incluyen:
// Contextos comunes donde se utilizan lambdas
Runnable tarea = () -> System.out.println("Ejecutando tarea");
Comparator<String> comparador = (s1, s2) -> s1.length() - s2.length();
ActionListener listener = e -> System.out.println("Botón pulsado");
En cada caso, el compilador determina qué interfaz funcional está implementando la lambda basándose en el contexto de asignación.
Asignación a variables
El contexto más directo es la asignación a una variable cuyo tipo es una interfaz funcional:
// El tipo de la variable define el "target type"
Predicate<String> estaVacio = s -> s.isEmpty();
Consumer<Integer> mostrar = n -> System.out.println(n);
Supplier<Double> numeroAleatorio = () -> Math.random();
En estos ejemplos, el compilador sabe exactamente qué método abstracto debe implementar la lambda porque está definido por el tipo de la variable.
Parámetros de métodos
Otro contexto común es cuando una lambda se pasa como argumento a un método:
List<String> nombres = Arrays.asList("Ana", "Carlos", "Beatriz");
// La lambda implementa Consumer<String> porque forEach espera ese tipo
nombres.forEach(nombre -> System.out.println(nombre));
// La lambda implementa Predicate<String> porque removeIf espera ese tipo
nombres.removeIf(nombre -> nombre.length() > 5);
El compilador determina el tipo de la lambda basándose en la firma del método al que se pasa como argumento.
Valores de retorno
Las lambdas también pueden ser valores de retorno de métodos:
public Comparator<String> obtenerComparadorPorLongitud() {
// Retorna una lambda que implementa Comparator<String>
return (s1, s2) -> Integer.compare(s1.length(), s2.length());
}
Target typing en expresiones
El target typing también funciona en expresiones más complejas:
// En una expresión condicional
Predicate<String> filtro = condicion
? s -> s.startsWith("A")
: s -> s.endsWith("Z");
// En constructores
new Thread(() -> System.out.println("Nuevo hilo")).start();
Inferencia de tipos
Gracias al target typing, el compilador puede inferir los tipos de los parámetros de la lambda:
// El compilador infiere que 'n' es Integer
List<Integer> numeros = Arrays.asList(1, 2, 3);
numeros.forEach(n -> System.out.println(n * n));
// Comparación con tipos explícitos
numeros.forEach((Integer n) -> System.out.println(n * n));
Ambas versiones son equivalentes, pero la primera es más concisa gracias a la inferencia de tipos.
Ambigüedad y resolución de tipos
En algunos casos, puede haber ambigüedad en el target type:
// Método sobrecargado
public void procesar(Consumer<String> consumidor) { /* ... */ }
public void procesar(Predicate<String> predicado) { /* ... */ }
// ¿Qué método se llama? ¡Ambigüedad!
procesar(s -> System.out.println(s)); // Error de compilación
En estos casos, es necesario proporcionar información adicional para resolver la ambigüedad:
// Soluciones a la ambigüedad
procesar((Consumer<String>) s -> System.out.println(s));
// O usar una referencia de método
procesar(System.out::println);
Contextos prácticos de uso
Las expresiones lambda se utilizan en diversos contextos prácticos:
- Operaciones con colecciones:
List<String> nombres = Arrays.asList("Ana", "Juan", "Carlos");
// Filtrado
List<String> nombresFiltrados = nombres.stream()
.filter(nombre -> nombre.length() > 3)
.collect(Collectors.toList());
- Eventos en interfaces gráficas:
JButton boton = new JButton("Pulsar");
boton.addActionListener(e -> System.out.println("Botón pulsado"));
- Programación concurrente:
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
// Código a ejecutar en otro hilo
return "Tarea completada";
});
- Operaciones de ordenación:
List<Persona> personas = obtenerPersonas();
// Ordenar por edad
Collections.sort(personas, (p1, p2) -> Integer.compare(p1.getEdad(), p2.getEdad()));
Conversión de contexto
El contexto de conversión es otro aspecto importante del target typing:
// Método que acepta un Runnable
void ejecutarEnHilo(Runnable tarea) {
new Thread(tarea).start();
}
// Llamada al método con una lambda
ejecutarEnHilo(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Iteración " + i);
}
});
En este ejemplo, la lambda se convierte automáticamente al tipo Runnable
porque ese es el tipo esperado por el método.
El target typing es un mecanismo fundamental que permite que las expresiones lambda en Java sean concisas y expresivas, al tiempo que mantiene la seguridad de tipos característica del lenguaje. Entender cómo funciona este mecanismo es esencial para utilizar lambdas de manera efectiva en diferentes contextos.
Variables capturadas y scope
Las expresiones lambda en Java no solo pueden utilizar parámetros que reciben, sino que también pueden acceder a variables definidas en el contexto donde se declaran. Esta capacidad de "capturar" variables del entorno circundante es una característica fundamental que amplía significativamente la utilidad de las lambdas.
Captura de variables
Cuando una expresión lambda utiliza variables definidas fuera de su cuerpo, se dice que captura esas variables. Existen dos tipos de variables que pueden ser capturadas:
- Variables de instancia y estáticas: Accesibles sin restricciones
- Variables locales: Deben ser efectivamente finales
class EjemploCaptura {
// Variable de instancia
private int contador = 0;
public void incrementarContador() {
// La lambda captura la variable de instancia 'contador'
Runnable incrementador = () -> {
contador++; // Acceso permitido a variable de instancia
System.out.println("Contador: " + contador);
};
new Thread(incrementador).start();
}
}
Restricción de efectivamente final
Una característica importante de las lambdas en Java es la restricción de que las variables locales capturadas deben ser efectivamente finales. Una variable es efectivamente final cuando su valor no cambia después de su inicialización, aunque no esté explícitamente declarada como final
.
public void procesarDatos() {
String prefijo = "ID-"; // Variable local efectivamente final
// Correcto: la variable 'prefijo' no se modifica después de su inicialización
Consumer<String> procesador = dato -> System.out.println(prefijo + dato);
procesarItems(procesador);
// Si intentáramos esto, obtendríamos un error de compilación:
// prefijo = "CÓDIGO-"; // Error: Variable used in lambda should be final or effectively final
}
Esta restricción existe porque:
- Las lambdas pueden ejecutarse en contextos asíncronos (como hilos separados)
- Las variables locales se almacenan en la pila y podrían dejar de existir cuando el método termine
- Para evitar condiciones de carrera y comportamientos impredecibles
Diferencia con variables de instancia
A diferencia de las variables locales, las variables de instancia y estáticas pueden modificarse dentro de las lambdas sin restricciones:
class Contador {
private int valor = 0; // Variable de instancia
public void incrementar() {
// Podemos modificar la variable de instancia 'valor' dentro de la lambda
Runnable tarea = () -> {
for (int i = 0; i < 10; i++) {
valor++; // Permitido: modificación de variable de instancia
}
System.out.println("Valor final: " + valor);
};
new Thread(tarea).start();
}
}
Esto es posible porque las variables de instancia se almacenan en el heap (montículo) y no en la pila, por lo que siguen existiendo mientras el objeto exista.
Soluciones para variables mutables
Cuando necesitamos modificar un contador o acumulador dentro de una lambda, podemos usar estas alternativas:
- Usar variables de instancia como vimos anteriormente
- Utilizar clases contenedoras como
AtomicInteger
o arrays de un solo elemento
public void contarElementos(List<String> elementos) {
// Usando un array de un solo elemento como contenedor mutable
final int[] contador = {0};
elementos.forEach(elemento -> {
if (elemento.length() > 5) {
contador[0]++; // Modificamos el contenido del array, no la referencia
}
});
System.out.println("Elementos largos: " + contador[0]);
}
- Usar clases atómicas para operaciones thread-safe:
public void procesarConcurrentemente(List<String> datos) {
AtomicInteger contador = new AtomicInteger(0);
datos.parallelStream().forEach(dato -> {
// Procesamiento del dato
contador.incrementAndGet(); // Incremento atómico y thread-safe
});
System.out.println("Elementos procesados: " + contador.get());
}
Scope de las variables en lambdas
El ámbito o scope de las variables dentro de una lambda sigue reglas específicas:
- Las lambdas crean su propio ámbito para variables locales
- No pueden redeclarar variables que existen en el ámbito contenedor
- Pueden ocultar variables de ámbitos externos si declaran nuevas con el mismo nombre
public void ejemploScope() {
String mensaje = "Mensaje original";
// Correcto: la lambda captura 'mensaje' del ámbito externo
Runnable r1 = () -> System.out.println(mensaje);
// Error: no se puede redeclarar 'mensaje' como parámetro
// Consumer<String> c = mensaje -> System.out.println(mensaje);
// Correcto: la lambda declara su propia variable local 'i'
for (int i = 0; i < 5; i++) {
int valorFinal = i; // Variable efectivamente final para captura
Runnable r2 = () -> {
// int i = 10; // Error: no se puede redeclarar 'i'
System.out.println("Valor capturado: " + valorFinal);
};
r2.run();
}
}
La palabra clave 'this' en lambdas
El comportamiento de la palabra clave this
dentro de las lambdas es diferente al de las clases anónimas:
- En una lambda,
this
se refiere a la instancia de la clase contenedora - En una clase anónima,
this
se refiere a la instancia de la clase anónima
class DemostradorThis {
private String nombre = "Instancia contenedora";
public void mostrarThis() {
// En la lambda, 'this' se refiere a la instancia de DemostradorThis
Runnable lambda = () -> {
System.out.println("En lambda, this.nombre = " + this.nombre);
};
// En la clase anónima, 'this' se refiere a la instancia de Runnable
Runnable anonima = new Runnable() {
private String nombre = "Clase anónima";
@Override
public void run() {
System.out.println("En anónima, this.nombre = " + this.nombre);
// Para acceder a la instancia externa:
System.out.println("Instancia externa: " + DemostradorThis.this.nombre);
}
};
lambda.run(); // Imprime: "En lambda, this.nombre = Instancia contenedora"
anonima.run(); // Imprime: "En anónima, this.nombre = Clase anónima"
}
}
Esta diferencia hace que las lambdas sean más intuitivas cuando se trabaja con el contexto de la clase contenedora.
Ejemplos prácticos de captura de variables
Veamos algunos ejemplos prácticos de cómo se utilizan las variables capturadas:
- Configuración de comportamiento:
public List<String> filtrarPorPrefijo(List<String> elementos, String prefijo) {
// La lambda captura la variable 'prefijo'
return elementos.stream()
.filter(elemento -> elemento.startsWith(prefijo))
.collect(Collectors.toList());
}
- Acumulación de resultados:
public Map<String, Integer> contarPalabras(List<String> textos) {
Map<String, Integer> frecuencias = new HashMap<>();
// La lambda captura la variable 'frecuencias'
textos.forEach(texto -> {
String[] palabras = texto.split("\\s+");
for (String palabra : palabras) {
frecuencias.merge(palabra.toLowerCase(), 1, Integer::sum);
}
});
return frecuencias;
}
- Personalización de comportamiento:
public void procesarConTimeout(Runnable tarea, long timeoutMs) {
// La lambda captura las variables 'tarea' y 'timeoutMs'
Thread hilo = new Thread(() -> {
long inicio = System.currentTimeMillis();
tarea.run();
long duracion = System.currentTimeMillis() - inicio;
if (duracion > timeoutMs) {
System.out.println("Advertencia: la tarea excedió el timeout de " + timeoutMs + "ms");
}
});
hilo.start();
}
La capacidad de capturar variables del entorno hace que las expresiones lambda sean extremadamente versátiles, permitiendo crear funciones que se adaptan dinámicamente al contexto donde se utilizan, lo que facilita la implementación de patrones de programación funcional en Java.
Otros ejercicios de programación de Java
Evalúa tus conocimientos de esta lección Funciones lambda con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Streams: match
Gestión de errores y excepciones
CRUD en Java de modelo Customer sobre un ArrayList
Clases abstractas
Listas
Métodos de la clase String
Streams: reduce()
API java.nio 2
Polimorfismo
Pattern Matching
Streams: flatMap()
Llamada y sobrecarga de funciones
Métodos referenciados
Métodos de la clase String
Representación de Fecha
Operadores lógicos
Inferencia de tipos con var
Tipos de datos
Estructuras de iteración
Streams: forEach()
Objetos
Funciones lambda
Uso de Scanner
Tipos de variables
Streams: collect()
Operadores aritméticos
Arrays y matrices
Clases y objetos
Interfaz funcional Consumer
CRUD en Java de modelo Customer sobre un HashMap
Interfaces
Enumeraciones Enums
API Optional
Interfaz funcional Function
Encapsulación
Interfaces
Uso de API Optional
Representación de Hora
Herencia básica
Clases y objetos
Interfaz funcional Supplier
HashMap
Sobrecarga de métodos
Polimorfismo de tiempo de ejecución
OOP en Java
Sobrecarga de métodos
CRUD de productos en Java
Clases sealed
Creación de Streams
Records
Encapsulación
Streams: min max
Herencia
Métodos avanzados de la clase String
Funciones
Polimorfismo de tiempo de compilación
Reto sintaxis Java
Conjuntos
Estructuras de control
Recursión
Excepciones
Herencia avanzada
Estructuras de selección
Uso de interfaces
Operadores
Variables
HashSet
Objeto Scanner
Streams: filter()
Operaciones de Streams
Interfaz funcional Predicate
Streams: sorted()
Configuración de entorno
Uso de variables
Clases
Streams: distinct()
Streams: count()
ArrayList
Mapas
Datos de referencia
Interfaces funcionales
Métodos básicos de la clase String
Tipos de datos
Clases abstractas
Instalación
Funciones
Excepciones
Estructuras de control
Herencia de clases
La clase Scanner
Generics
Streams: map()
Funciones y encapsulamiento
Todas las lecciones de Java
Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Instalación De Java
Introducción Y Entorno
Configuración De Entorno Java
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Recursión
Sintaxis
Arrays Y Matrices
Sintaxis
Excepciones
Programación Orientada A Objetos
Clases Y Objetos
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Clases Abstractas
Programación Orientada A Objetos
Interfaces
Programación Orientada A Objetos
Sobrecarga De Métodos
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
La Clase Scanner
Programación Orientada A Objetos
Métodos De La Clase String
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Records
Programación Orientada A Objetos
Pattern Matching
Programación Orientada A Objetos
Inferencia De Tipos Con Var
Programación Orientada A Objetos
Enumeraciones Enums
Programación Orientada A Objetos
Generics
Programación Orientada A Objetos
Clases Sealed
Programación Orientada A Objetos
Listas
Framework Collections
Conjuntos
Framework Collections
Mapas
Framework Collections
Funciones Lambda
Programación Funcional
Interfaz Funcional Consumer
Programación Funcional
Interfaz Funcional Predicate
Programación Funcional
Interfaz Funcional Supplier
Programación Funcional
Interfaz Funcional Function
Programación Funcional
Métodos Referenciados
Programación Funcional
Creación De Streams
Programación Funcional
Operaciones Intermedias Con Streams: Map()
Programación Funcional
Operaciones Intermedias Con Streams: Filter()
Programación Funcional
Operaciones Intermedias Con Streams: Distinct()
Programación Funcional
Operaciones Finales Con Streams: Collect()
Programación Funcional
Operaciones Finales Con Streams: Min Max
Programación Funcional
Operaciones Intermedias Con Streams: Flatmap()
Programación Funcional
Operaciones Intermedias Con Streams: Sorted()
Programación Funcional
Operaciones Finales Con Streams: Reduce()
Programación Funcional
Operaciones Finales Con Streams: Foreach()
Programación Funcional
Operaciones Finales Con Streams: Count()
Programación Funcional
Operaciones Finales Con Streams: Match
Programación Funcional
Api Optional
Programación Funcional
Transformación
Programación Funcional
Reducción Y Acumulación
Programación Funcional
Mapeo
Programación Funcional
Streams Paralelos
Programación Funcional
Agrupación Y Partición
Programación Funcional
Filtrado Y Búsqueda
Programación Funcional
Api Java.nio 2
Entrada Y Salida Io
Fundamentos De Io
Entrada Y Salida Io
Leer Y Escribir Archivos
Entrada Y Salida Io
Httpclient Moderno
Entrada Y Salida Io
Clases De Nio2
Entrada Y Salida Io
Api Java.time
Api Java.time
Localtime
Api Java.time
Localdatetime
Api Java.time
Localdate
Api Java.time
Executorservice
Concurrencia
Virtual Threads (Project Loom)
Concurrencia
Future Y Completablefuture
Concurrencia
Spring Framework
Frameworks Para Java
Micronaut
Frameworks Para Java
Maven
Frameworks Para Java
Gradle
Frameworks Para Java
Lombok Para Java
Frameworks Para Java
Quarkus
Frameworks Para Java
Ecosistema Jakarta Ee De Java
Frameworks Para Java
Introducción A Junit 5
Testing
Certificados de superación de Java
Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender la sintaxis básica y variaciones de las expresiones lambda en Java.
- Identificar los contextos de uso y el mecanismo de target typing para lambdas.
- Entender cómo las lambdas capturan variables externas y las restricciones asociadas.
- Diferenciar el scope de variables dentro de lambdas y el comportamiento de la palabra clave 'this'.
- Aplicar expresiones lambda para simplificar código en colecciones, concurrencia y eventos.