4. Las funciones lambda de Java 8
4.1. Ejemplo-01: Interfaces funcionales y lambda
![]() |
Consideremos el siguiente código:
package dvp.java8.lambdas;
public class Exemple01 {
public static void main(String[] args) {
// clases anónimas
I1 ia1 = new I1() {
@Override
public void doSomething() {
System.out.println("ia1.doSomething");
}
};
I2 ia2 = new I2() {
@Override
public String getSomething(double value) {
return String.format("ia2.getSomething(%s)", value);
}
};
// lambdas
I1 ib1 = () -> System.out.println("ib1.lambda");
I2 ib2 = (value) -> String.format("ib2.lambda(%s)", value);
I1 ib3 = () -> {
System.out.println("ib3.lambda");
};
I2 ib4 = (double value) -> {
return String.format("ib4.lambda(%s)", value);
};
// aplicación
ia1.doSomething();
System.out.println(ia2.getSomething(4.3));
ib1.doSomething();
System.out.println(ib2.getSomething(5.8));
ib3.doSomething();
System.out.println(ib4.getSomething(10.1));
}
}
@FunctionalInterface
interface I1 {
void doSomething();
}
@FunctionalInterface
interface I2 {
String getSomething(double value);
}
- líneas 41-44: definen una interfaz funcional I1. Una interfaz funcional es una interfaz que solo tiene un método. No está vinculada a la presencia de la anotación [@FunctionalInterface] de la línea 41, que es opcional;
- líneas 46-49: una segunda interfaz funcional I2;
- líneas 6-19: las interfaces I1 y I2 se implementan con clases anónimas, la solución más habitual antes de la introducción de las funciones lambda;
- líneas 21-29: las interfaces I1 y I2 se implementan con funciones lambda;
- líneas 7-12: implementación de la interfaz I1 con una clase anónima. La sintaxis para implementar una interfaz I con una clase anónima es la siguiente:
donde m1, m2, ... son los métodos definidos por la interfaz I.
- líneas 14-19: implementación de la interfaz I2 con una clase anónima;
- línea 22: implementación de la interfaz I1 con una función lambda. Aquí se aprovecha el hecho de que la interfaz funcional solo tiene un método. La función lambda implementa entonces este único método M. Su sintaxis es la siguiente:
Los tipos T1, T2 y Tn pueden omitirse si el compilador puede deducirlos del contexto (inferencia de tipos).
- línea 22: implementación del método [I1.doSomething] con la siguiente firma:
void doSomething();
[doSomething] es un método que no tiene parámetros y que no devuelve ningún resultado. Su implementación lambda puede escribirse como en la línea 22 o bien como en las líneas 24-26, es decir, se pueden poner llaves alrededor del código de la función lambda. Si este código solo contiene una instrucción, como aquí, estas llaves pueden omitirse;
- línea 23: implementación del método [I1.getSomething] con la siguiente firma:
String getSomething(double value);
[getSomething] admite un parámetro de tipo [double] y devuelve un resultado de tipo [String]. Su implementación lambda puede ser la de la línea 23 o la de las líneas 27-29. En la implementación de la línea 23:
- se omite el tipo del parámetro [value]. En ese caso, se utilizará el tipo [double] que se encuentra en la firma de [getSomething];
- el código lambda no está entre paréntesis. El resultado del lambda es, por tanto, el valor de la única expresión de este código, en este caso: String.format("ib2.lambda(%s)", value);
En la implementación de las líneas 27-29:
- Se declara explícitamente el tipo del parámetro [value];
- se utiliza un [return] para devolver el resultado de la lambda. En este caso, hay que poner llaves;
- líneas 32-37: se invocan las diversas funciones anónimas y lambda;
El resultado obtenido es el siguiente:
4.2. Ejemplo-02: la interfaz funcional Predicate<T>
![]() |
La mayoría de las veces, nos encontramos con interfaces funcionales de bibliotecas en lugar de interfaces funcionales que definimos nosotros mismos. En este caso, nos interesa la interfaz funcional [Predicate] definida en el paquete [java.util.function], que reúne la mayoría de las interfaces funcionales de Java 8. Esta se define de la siguiente manera:

Hemos dicho que una interfaz funcional solo tiene un método. Sin embargo, aquí hay cuatro. Otra innovación aportada por Java 8 fue la introducción del concepto de método por defecto de una interfaz, caracterizado por la palabra clave [default]. Aquí tenemos tres métodos de este tipo. Estos métodos tienen la particularidad de contar con una implementación por defecto. Por lo tanto, una clase que implemente una interfaz con métodos por defecto no tiene la obligación de implementar estos últimos. Así, una clase que desee implementar la interfaz [Predicate] solo tiene un método que debe implementar obligatoriamente: el método [test]. La interfaz [Predicate] es, por tanto, una interfaz funcional. Se dirá, pues, que una interfaz funcional es aquella cuya implementación solo incluye un método obligatorio. Si tiene más de un método, los demás deben llevar la palabra clave [default].
El método [Predicate<T>.test] recibe un parámetro de tipo T y devuelve un valor booleano. Esta interfaz se utiliza generalmente para filtrar colecciones. Para ilustrar su uso, utilizaremos los siguientes datos:
package dvp.data;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Personne {
public enum Sexe {HOMME,FEMME};
// datos
private String nom;
private int age;
private double poids;
private Sexe sexe;
// constructores
public Personne() {
}
public Personne(String nom, Sexe sexe, int age, double poids) {
this.nom = nom;
this.sexe=sexe;
this.age = age;
this.poids = poids;
}
// getters y setters
...
// toString
@Override
public String toString(){
try {
return new ObjectMapper().writeValueAsString(this);
} catch (JsonProcessingException e) {
return e.getMessage();
}
}
}
- líneas 32-38: el método [toString] devuelve la cadena jSON de la persona;
La clase [Personnes] define una lista de 3 personas:
package dvp.data;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Arrays;
import java.util.List;
public class Personnes {
private static List<Personne> personnes = Arrays.asList(new Personne("jean", Personne.Sexe.HOMME, 20, 70),
new Personne("marie", Personne.Sexe.FEMME, 10, 30), new Personne("camille", Personne.Sexe.FEMME, 30, 55));
public static List<Personne> get() {
return personnes;
}
public static String toString(List<Personne> liste) {
try {
return new ObjectMapper().writeValueAsString(liste);
} catch (JsonProcessingException e) {
return e.getMessage();
}
}
}
- líneas 10-11: la lista de 3 personas;
- líneas 13-15: un método estático para obtener esta lista;
- líneas 17-23: un método estático que permite obtener la cadena jSON a partir de una lista de personas pasada como parámetro;
Estos datos serán procesados por el siguiente código:
package dvp.java8.lambdas;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import dvp.data.Personne;
import dvp.data.Personnes;
public class Exemple02 {
public static void main(String[] args) {
// predicado implementado por una clase anónima
Predicate<Personne> filterPoids=new Predicate<Personne>() {
@Override
public boolean test(Personne personne) {
return personne.getPoids()<50;
}
};
// predicado implementado por un lambda
Predicate<Personne> filterAge = p -> p.getAge() < 28;
// lista de personas
List<Personne> personnes = Personnes.get();
// visualizaciones
System.out.println(Personnes.toString(filterPersonnes(personnes, filterAge)));
System.out.println(Personnes.toString(filterPersonnes(personnes, filterPoids)));
}
private static List<Personne> filterPersonnes(List<Personne> personnes, Predicate<Personne> filter) {
// [filter] filtra la lista [personnes]
List<Personne> personnesFiltrées = new ArrayList<>();
for (Personne p : personnes) {
if (filter.test(p)) {
personnesFiltrées.add(p);
}
}
return personnesFiltrées; }
}
- líneas 13-18: implementación de la interfaz Predicate<Persona> mediante una clase anónima. Se trata de un filtro sobre el peso de la persona;
- línea 20: implementación de la interfaz Predicate<Persona> mediante una función lambda. Se trata de un filtro sobre la edad de la persona. Según lo dicho, también se podría haber escrito de la siguiente manera:
Predicate<Personne> filterAge = (Personne p) -> {
return (p.getAge() < 28);
};
pero version de la línea 20 es más conciso. El tipo del parámetro p se deduce del contexto. Aquí se construye un tipo [Predicate<Personne>]. El método implementado tiene entonces como firma [boolean test(Personne param)]. Por lo tanto, el tipo implícito de p, línea 20, es el tipo [Personne];
- línea 22: se recupera una lista de personas predefinida;
- línea 24: se filtran según su edad;
- línea 25: se filtran según su peso. En ambos casos, se muestra la cadena jSON de la lista así filtrada;
- líneas 28-37: un método estático que
- admite como parámetros: una lista de personas a filtrar y el filtro. Este último es una instancia de la interfaz [Predicate<Personne>]. Por conveniencia, se denomina aquí y en el resto del documento «instancia de una interfaz I» a una instancia de una clase C que implementa I;
- devuelve como resultado la lista así filtrada;
- línea 32: se utiliza el método [test] de la interfaz [Predicate]. Según el filtro pasado al método, el método [test] será:
return personne.getPoids()<50;
o bien
return p.getAge() < 28
La ejecución de la clase [Exemple02] da el siguiente resultado:
4.3. Ejemplo-03: la interfaz funcional Function<T,R>
![]() |
La interfaz funcional Function<T,R> se define de la siguiente manera:

El único método de la interfaz tiene la firma R apply(T t). Se utiliza generalmente para crear, a partir de un tipo Collection<T>, un nuevo tipo Collection<R>. Para ilustrar esta interfaz, utilizaremos el siguiente código:
package dvp.java8.lambdas;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import dvp.data.Personne;
import dvp.data.Personnes;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
public class Exemple03 {
public static void main(String[] args) throws JsonProcessingException {
// implementación con clase anónima
Function<Personne, String> mapToName = new Function<Personne, String>() {
@Override
public String apply(Personne personne) {
return personne.getNom();
}
};
// implementación con lambda
Function<Personne, Integer> mapToAge = p -> p.getAge();
// lista de personas
List<Personne> personnes = Personnes.get();
// jSON
ObjectMapper jsonMapper = new ObjectMapper();
// visualizaciones
System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToName)));
System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToAge)));
}
// transformación List<Persona> --> List<T>
private static <T> List<T> mapPersonnes(List<Personne> personnes, Function<Personne, T> mapper) {
List<T> maps = new ArrayList<>();
for (Personne p : personnes) {
maps.add(mapper.apply(p));
}
return maps;
}
}
- líneas 15-20: se implementa la interfaz [Function<Personne, String>] con una clase anónima que realizará la transformación Persona --> String;
- línea 22: se implementa la interfaz [Function<Personne, Integer>] con una función lambda que realizará la transformación Persona --> Integer;
- líneas 33-39: un método estático que
- admite dos parámetros: el primero, de tipo List<Personne>, es una lista de personas que se van a transformar. El segundo, de tipo Function<Persona, T>, es una función que, a partir de cada persona de la lista, crea un objeto de tipo T;
- devuelve como resultado un tipo List<T> en el que cada elemento procede de la transformación Persona -> T;
- líneas 35-37: se aplica la transformación Persona -> T. Si el segundo parámetro del método es el objeto [mapToName], se realizará una transformación Persona -> String. Si se trata del objeto [mapToAge], se realizará una transformación Persona -> Integer;
El resultado obtenido es el siguiente:
4.4. Ejemplo-04: la interfaz funcional Consumer<T>
![]() |
La interfaz funcional Consumer<T> se define de la siguiente manera:

El único método de la interfaz tiene la firma: void accept(T t). Este método utiliza (consume) su parámetro y no devuelve ningún resultado. Para ilustrarlo, utilizaremos el siguiente código:
package dvp.java8.lambdas;
import java.util.List;
import java.util.function.Consumer;
import dvp.data.Personne;
import dvp.data.Personnes;
public class Exemple04 {
public static void main(String[] args) {
// lista de personas
List<Personne> personnes = Personnes.get();
// implementación anónima
Consumer<Personne> consumerAge = new Consumer<Personne>() {
@Override
public void accept(Personne personne) {
System.out.printf(" age de %s = %s%n", personne.getNom(), personne.getAge());
}
};
// implementación lambda
Consumer<Personne> consumerPoids = p -> System.out.printf(" poids de %s = %s%n", p.getNom(), p.getPoids());
// visualizaciones
for (Personne p : personnes) {
consumerAge.accept(p);
}
System.out.println("--------");
for (Personne p : personnes) {
consumerPoids.accept(p);
}
}
}
- líneas 14-19: la interfaz [Consumer<Personne>] se implementa mediante una clase anónima cuyo método [accept] muestra el nombre y la edad de la persona;
- línea 21: la interfaz [Consumer<Personne>] se implementa mediante una función lambda cuyo método implícito [accept] muestra el nombre y el peso de la persona;
- líneas 23-25: la lista de personas es consumida por la implementación [consumerAge];
- líneas 27-29: la lista de personas es procesada por la implementación [consumerPoids];
Los resultados son los siguientes:
4.5. Ejemplo-05: la interfaz funcional BiConsumer<T,U>
![]() |
La interfaz funcional BiConsumer<T,U> se define de la siguiente manera:

El único método de la interfaz tiene la firma: void accept(T t, U u). Consume el tipo T con la información adicional U u. Ilustraremos su uso con el siguiente código:
package dvp.java8.lambdas;
import dvp.data.Personne;
import dvp.data.Personnes;
import java.util.List;
import java.util.function.BiConsumer;
public class Exemple05 {
public static void main(String[] args) {
// lista de personas
List<Personne> personnes = Personnes.get();
// implementación anónima
BiConsumer<Personne, Integer> biconsumerAge = new BiConsumer<Personne, Integer>() {
@Override
public void accept(Personne personne, Integer integer) {
personne.setAge(personne.getAge() + integer);
System.out.printf("age de %s = %s%n", personne.getNom(), personne.getAge());
}
};
// implementación lambda
BiConsumer<Personne, Integer> biconsumerPoids = (p, i) -> {
p.setPoids(p.getPoids() + i);
System.out.printf("poids de %s = %s%n", p.getNom(), p.getPoids());
};
// visualizaciones
for (Personne p : personnes) {
biconsumerAge.accept(p, 100);
}
System.out.println("--------");
for (Personne p : personnes) {
biconsumerPoids.accept(p, 200);
}
}
}
- líneas 14-20: implementación de la interfaz BiConsumer<T,U> con una clase anónima. El método [apply] utiliza su segundo parámetro para actualizar la edad de la persona pasada como primer parámetro. A continuación, muestra el resultado;
- líneas 22-25: implementación de la interfaz BiConsumer<T,U> con una función lambda. El método implícito [apply] utiliza su segundo parámetro para actualizar el peso de la persona pasada como primer parámetro. A continuación, muestra el resultado;
- líneas 27-29: la lista de personas se procesa con la implementación [biconsumerAge];
- líneas 31-33: la lista de personas se procesa con la implementación [biconsumerPoids];
Los resultados obtenidos son los siguientes:
4.6. Ejemplo-06: la interfaz funcional BiFunction<T,U,R>
![]() |
La interfaz funcional BiFunction<T,U,R> se define de la siguiente manera:

El único método de la interfaz tiene la firma: R apply(T t, U u). Este método es similar al método [BiConsumer.apply], pero mientras que este último no devuelve ningún resultado, el método [BiFunction.apply] devuelve un resultado. Ilustraremos su uso con el siguiente código:
package dvp.java8.lambdas;
import java.util.List;
import java.util.function.BiFunction;
import dvp.data.Personne;
import dvp.data.Personnes;
public class Exemple06 {
public static void main(String[] args) {
// lista de personas
List<Personne> personnes = Personnes.get();
// implementación anónima
BiFunction<Personne, Integer, Integer> biFunctionAge = new BiFunction<Personne, Integer, Integer>() {
@Override
public Integer apply(Personne personne, Integer integer) {
return personne.getAge() + integer;
}
};
// implementación lambda
BiFunction<Personne, Integer, Double> biFunctionPoids = (p, i) -> {
return p.getPoids() + i;
};
// visualizaciones
for (Personne p : personnes) {
System.out.printf("age de %s = %s%n", p.getNom(), biFunctionAge.apply(p, 100));
}
System.out.println("--------");
for (Personne p : personnes) {
System.out.printf("poids de %s = %s%n", p.getNom(), biFunctionPoids.apply(p, 200));
}
}
}
- líneas 14-19: la interfaz BiFunction<Persona, Integer, Integer> se implementa con una clase anónima. El método [apply] de esta devuelve la edad de la persona pasada como primer parámetro incrementada en el valor del segundo parámetro;
- líneas 21-23: la interfaz BiFunction<Persona, Entero, Doble> se implementa con una función lambda. El método [apply] devuelve el peso de la persona pasada como primer parámetro incrementado en el valor del segundo parámetro;
- líneas 25-27: se aplica la implementación [biFunctionAge] a las personas;
- líneas 29-31: se aplica la implementación [biFunctionPoids] a las personas;
Los resultados obtenidos son los siguientes:
age de jean = 120
age de marie = 110
age de camille = 130
--------
poids de jean = 270.0
poids de marie = 230.0
poids de camille = 255.0
Además de las funciones lambda, Java 8 introdujo el tipo Stream<T>, que modela un flujo de elementos de tipo T. Estos elementos pueden sufrir transformaciones sucesivas implementadas por funciones lambda. Cuando es posible, y hay varios procesadores, estas transformaciones pueden realizarse en paralelo.
4.7. Ejemplo-07: la interfaz funcional Supplier<T>
![]() |
La interfaz funcional Supplier<T> se define de la siguiente manera:

El único método de la interfaz tiene la firma: T get(), cuya función es proporcionar un objeto de tipo T.
Ilustraremos esta interfaz funcional con el siguiente código:
package dvp.java8.lambdas;
import java.util.List;
import java.util.Random;
import java.util.function.Supplier;
import dvp.data.Personne;
import dvp.data.Personnes;
public class Exemple07 {
public static void main(String[] args) {
// implementación anónima
Supplier<Personne> supplier = new Supplier<Personne>() {
// lista de personas
List<Personne> personnes = Personnes.get();
// implementación de interfaz
@Override
public Personne get() {
int i = new Random().nextInt(personnes.size());
return personnes.get(i);
}
};
// explotación
for (int i = 0; i < 5; i++) {
affiche(supplier);
}
}
// visualización de persona
public static void affiche(Supplier<Personne> supplier) {
System.out.println(supplier.get());
}
}
- líneas 13-28: implementación de un tipo Supplier<Personne>;
- líneas 31-33: el método estático [affiche] espera un parámetro de tipo Supplier<Personne>;
- líneas 25-27: utilización de la instancia Supplier<Personne>;
Se obtienen los siguientes resultados:






