Introducción a Java Lambda

    La programación funcional o también llamadas las expresiones lambda han supuesto un salto importante en la evolución del lenguaje de programación Java. Mostramos una introducción de cómo se usan y cómo escribir más con menos.

    ARTECO CONSULTING SL

    Java, Software, Webs, SEO y Formación

    Qué son las funciones lambdas de Java

    Las funciones lambdas es un término adoptado de la programación funcional y corresponden con funciones de Java que normalmente son anónimas y se escriben en línea allí donde se usan. Como cualquier función recibe cero o más argumentos y devuelven uno o ningún valor de retorno. Como cualquier función, puede consumir métodos de otras clases y objetos. Al declararse al mismo tiempo en donde se usa, puede acceder a las variables locales del ámbito al que pertenece, pero sólo podrá usar estos como valores de sólo lectura, impidiendo realizar alguna modificación.

    Las funciones lambdas se crearon a partir de la versión Java 8, por lo que no es posible usar su sintaxis en versiones anteriores. Sus entidades principales están contenidas en el package java.util.funcional. Sí hay que decir, que no aportan una funcionalidad que no pueda hacerse con Java pre 8, simplemente es una manera más compacta de escribir código Java. Se puede decir de manera resumida que una función lambda es como una clase con un único método público. Así que los que no dispongan de Java 8 podrían simular un comportamiento similar creando clases parecidas a las proporcionadas con el API de Java en el package java.util.functional.

    Entidades de java.util.functional

    Antes de crear una función de tipo lambda, conviene conocer las entidades básicas que componen esta manera de programar. Las principales entidades son interfaces con un único método que debe implementar el programador y que estas implementaciones pueden hacerse llegar como argumentos de métodos de otras muchas clases del API de Java. Hubo una gran modificación de las clases existentes para aceptar este tipo de implementaciones allí donde tuviera sentido, como ocurre en las colecciones.

    Las implementaciones de estas interfaces son del tipo, consume un valor y retorna otro tipo de valor, o produce un valor sin argumentos o produce un valor dados dos argumentos. A éstas se les llama unidades funcionales porque componen una lógica interna que a priori el consumidor de esta lógica no conoce, pero de la que sí se conoce su interfaz y por tanto la manera de relacionarse con el resto de los objetos, o lo que es lo mismo la manera de ser invocada. Aparece de nuevo el concepto de cajas negras en donde entran parámetros y salen resultados.

    Las interfaces funcionales más importantes contenidas en java.util.functional son:

    • Supplier<T>: esta función se debe utilizar cuando se necesiten generar objetos sin requerir argumentos. Por ejemplo para realizar una inicialización perezosa.
    • Consumer<T> esta en cambio es el opuesto de Supplier ya que consume, acepta como argumento el tipo T sin devolver ningún valor de retorno.
    • Function<T,R> esta interfaz permite definir una función que acepta un parámetro de tipo T y devuelve un resultado del tipo R pudiendo aplicarle alguna transformación u operación.
    • BiFunction<T,R,S> esta interfaz permite definir una función que acepta dos parámetros de tipo T y R, devolviendo un resultado del tipo S. Normalmente serán operaciones de agregación u operadores binarios como la suma, resta, etc..
    • Predicate<T> la interfaz predicado debe devolver forzosamente un boolean dado un objeto de tipo T, normalmente utilizado para filtrar elementos de una colección.

    El paquete incluye más interfaces que el programador puede usar, pero estas son las más básicas con las que ya es posible empezar a realizar algunos ejemplos útiles y frecuentes.

    Cómo se crea una función lambda

    La sintaxis cambia un poco respecto a Java tradicional, ya que se intentan no escribir los tipos de las variables siempre y cuando no se cree alguna ambigüedad. Veamos el primer ejemplo:

    Function<String,Integer> sizeOf = (String s) -> {
        return s.length();
    };
    

    O su equivalente y más compacta:

    Function<String,Integer> sizeOf =  s -> s.length();
    

    En ambos casos se está definiendo una función que dado un String devolverá la longitud de la cadena de caracteres que almacene. Fíjese que el tipo de la variable de s se infiere automáticamente de los tipos utilizados en sizeOf y que la palabra reservada return no es necesaria, siempre y cuando no haya más de una sentencia en la función.

    Por rara y compacta que pueda parecer la sintaxis, no es más que otra forma de escribir la siguiente clase, de hecho esto es lo que realmente genera el compilador:

    public class SizeOf implements Function<String,Integer>{
        public Integer apply(String s){
            return s.length();
        }
    }
    

    Así que para poder usar tanto la función sizeOf como la clase SizeOf en un bloque de código cualquiera, se realizaría de la siguiente forma:

    Integer r1 = sizeOf.apply("hola java 8");
    // o
    Integer r2 = new SizeOf().apply("hola java 8");
    

    La ventaja de hacerlo como función, en lugar de como clase, a parte de la reducción de los literales que acompañan a cada opción, es que el API del JDK de Java versión 8 y adelante tiene métodos que aceptan estas funciones, reduciendo aun más la cantidad de código que se debe escribir.

    Por ejemplo veamos como podemos aplicar una ordenación usando un comparador escrito en Java 8:

    class Persona{
        String nombre;
    
        Persona (String nombre){
            this.nombre = nombre;
        }
    }
    
    List<Persona> personas = new ArrayList<>();
    personas.add(new Persona("Pepe"));
    personas.add(new Persona("Andrés"));
    
    personas.sort( (l, r) -> l.nombre.compareTo(r.nombre));
    
    

    Esta última función lambda es una del tipo BiFunction que acepta dos objetos de tipo Persona y devuelve un int típico de cualquier comparador de java java.util.Comparator<T>. Tanto la función como el comparador son compatibles, así que la función lambda anónima, se podría haber guardado en una variable de tipo comparador para ser usado otra vez más adelante:

    Comparator<Persona> comp = (l, r) -> l.nombre.compareTo(r.nombre));
    personas.sort(comp);
    
    

    El uso de lambda en collecciones

    El uso más habitual de lambda es en las colecciones y se utiliza para definir una operación que será aplicada a los elementos contenidos en ésta, dejando que el Api de Java realice la iteración sobre los elementos por nosotros, sin que tengamos que escribir ninguna sentencia de control de iteración como for o while.

    Para poder aplicar funciones lambda en colecciones, en Java 8, se introduce una nueva entidad denominada Stream. Los Stream representan flujos de datos que pueden ser infinitos, pero en general el flujo estará asociado a una colección finita como un ArrayList. Así, en un Stream el programador puede «registrar» operaciones que se harán sobre una lista, por ejemplo. Para ello las colecciones incorporar un método .stream() con el cual acceder al correspondiente flujo.

    Stream<Persona> stream = personas.stream();
    
    List<String> nombres = stream
    
        // filtrado de los elementos que tienen nombre nullo
        .filter(p -> p.nombre!=null)
    
        // aplicar una conversión, de Persona a String
        // quedándonos con el nombre
        .map(p -> p.nombre)
        // a partir de aquí se tiene un Stream<String>
    
        // recolectar los elementos en una lista
        .collect(Collectors.toList());
    

    En el código anterior, se definen varias operaciones (filtrado, conversión y recolección) que dan como resultado la obtención de todos los nombres de una colección de Personas en una instancia del tipo List<String> sin necesidad de escribir un bucle.

    Este tipo de sintaxis es muy compacta y se utiliza intensivamente en grandes proyectos para reducir el número de líneas necesarias. Además obliga al programador a escribir unidades funcionales que son fácilmente testeables desde pruebas unitarias con jUnit.

    Desde luego hay muchas más opciones y funciones lambda que pueden usarse, este es sólo algunos ejemplos de cómo utilizar las más comunes. Si estás interesado en aprender más de Java y la sintaxis introducida en Java 8 te recomendamos que revises nuestro libro de java Guía Javañol.

    Deja una respuesta