Capítulo 1. Lo básico

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

El mayor cambio de Java 8 es la incorporación al lenguaje de conceptos de la programación funcional. En concreto, el lenguaje ha añadido expresiones lambda, referencias a métodos y flujos.

Si aún no has utilizado las nuevas características funcionales, probablemente te sorprenderá lo diferente que será tu código respecto a versiones anteriores de Java. Los cambios en Java 8 representan los mayores cambios en el lenguaje de todos los tiempos. En muchos sentidos, parece como si estuvieras aprendiendo un lenguaje completamente nuevo.

La pregunta entonces es: ¿Por qué hacer esto? ¿Por qué hacer cambios tan drásticos en un lenguaje que ya tiene veinte años y que planea mantener la compatibilidad con versiones anteriores? ¿Por qué hacer revisiones tan drásticas a un lenguaje que ha sido, según todos los indicios, extremadamente exitoso? ¿Por qué cambiar a un paradigma funcional después de todos estos años de ser uno de los lenguajes orientados a objetos con más éxito de la historia?

La respuesta es que el mundo del desarrollo de software ha cambiado, por lo que los lenguajes que quieran tener éxito en el futuro tienen que adaptarse también. A mediados de los 90, cuando Java era brillante y nuevo, la ley de Moore1 seguía plenamente vigente. Todo lo que tenías que hacer era esperar un par de años y tu ordenador duplicaría su velocidad.

El hardware actual ya no se basa en aumentar la densidad de los chips para conseguir velocidad. En su lugar, incluso la mayoría de los teléfonos tienen múltiples núcleos, lo que significa que el software debe escribirse esperando ejecutarse en un entorno multiprocesador. La programación funcional, con su énfasis en las funciones "puras" (que devuelven el mismo resultado dadas las mismas entradas, sin efectos secundarios) y la inmutabilidad simplifica la programación en entornos paralelos. Si no tienes ningún estado mutable compartido, y tu programa puede descomponerse en colecciones de funciones simples, es más fácil comprender y predecir su comportamiento.

Sin embargo, éste no es un libro sobre Haskell, ni Erlang, ni Frege, ni ninguno de los demás lenguajes de programación funcional. Este libro trata de Java y de los cambios introducidos en el lenguaje para añadir conceptos funcionales a lo que sigue siendo fundamentalmente un lenguaje orientado a objetos.

Java admite ahora expresiones lambda, que son esencialmente métodos tratados como si fueran objetos de primera clase. El lenguaje también tiene referencias a métodos, que te permiten utilizar un método existente allí donde se espera una expresión lambda. Para aprovechar las expresiones lambda y las referencias a métodos, el lenguaje también ha añadido un modelo de flujo, que produce elementos y los hace pasar por una cadena de transformaciones y filtros sin modificar la fuente original.

Las recetas de este capítulo describen la sintaxis básica de las expresiones lambda, las referencias a métodos y las interfaces funcionales, así como el nuevo soporte para métodos estáticos y por defecto en las interfaces. Los flujos se tratan en detalle en el Capítulo 3.

1.1 Expresiones lambda

Problema

Quieres utilizar expresiones lambda en tu código.

Solución

Utiliza una de las variedades de la sintaxis de la expresión lambda y asigna el resultado a una referencia de tipo interfaz funcional.

Debate

Una interfaz funcional es una interfaz con un único método abstracto (SAM). Una clase implementa cualquier interfaz proporcionando implementaciones para todos los métodos que contiene. Esto puede hacerse con una clase de nivel superior, una clase interna o incluso una clase interna anónima.

Por ejemplo, considera la interfaz Runnable, que existe en Java desde la versión 1.0. Contiene un único método abstracto llamado run, que no toma argumentos y devuelve void. El constructor de la clase Thread toma un Runnable como argumento, por lo que en el Ejemplo 1-1 se muestra una implementación de clase interna anónima.

Ejemplo 1-1. Implementación de clase interna anónima de Runnable
public class RunnableDemo {
    public static void main(String[] args) {
        new Thread(new Runnable() {  1
            @Override
            public void run() {
                System.out.println(
                    "inside runnable using an anonymous inner class");
            }
        }).start();
    }
}
1

Clase interna anónima

La sintaxis de clase interna anónima consiste en la palabra new seguida del nombre de la interfaz Runnable y paréntesis, lo que implica que estás definiendo una clase sin nombre explícito que implementa esa interfaz. El código entre llaves ({}) anula el método run, que simplemente imprime una cadena en la consola.

El código del Ejemplo 1-2 muestra el mismo ejemplo utilizando una expresión lambda.

Ejemplo 1-2. Utilización de una expresión lambda en un constructor Thread
new Thread(() -> System.out.println(
    "inside Thread constructor using lambda")).start();

La sintaxis utiliza una flecha para separar los argumentos (como aquí no hay argumentos, sólo se utiliza un par de paréntesis vacíos) del cuerpo. En este caso, el cuerpo consta de una sola línea, por lo que no se necesitan llaves. Esto se conoce como expresión lambda. El valor al que se evalúe la expresión se devuelve automáticamente. En este caso, como println devuelve void, el retorno de la expresión es también void, que coincide con el tipo de retorno del método run.

Una expresión lambda debe coincidir con los tipos de argumento y de retorno de la firma del único método abstracto de la interfaz. A esto se le llama ser compatible con la firma del método. La expresión lambda es, por tanto, la implementación del método de la interfaz, y también puede asignarse a una referencia de ese tipo de interfaz.

Como demostración, el Ejemplo 1-3 muestra la lambda asignada a una variable.

Ejemplo 1-3. Asignar una expresión lambda a una variable
Runnable r = () -> System.out.println(
    "lambda expression implementing the run method");
new Thread(r).start();
Nota

No existe ninguna clase en la biblioteca Java llamada Lambda. Las expresiones lambda sólo pueden asignarse a referencias de interfaces funcionales.

Asignar una lambda a la interfaz funcional es lo mismo que decir que la lambda es la implementación del único método abstracto que contiene. Puedes pensar en la lambda como el cuerpo de una clase interna anónima que implementa la interfaz. Por eso la lambda debe ser compatible con el método abstracto; sus tipos de argumento y de retorno deben coincidir con la firma de ese método. Sin embargo, el nombre del método implementado no es importante. No aparece en ninguna parte como parte de la sintaxis de la expresión lambda.

Este ejemplo era especialmente sencillo porque el método run no toma argumentos y devuelve void. Considera en su lugar la interfaz funcional java.io.Filename​Filter, que también forma parte de la biblioteca estándar de Java desde la versión 1.0. Las instancias de Filename​Filter se utilizan como argumentos del método File.list para restringir los archivos devueltos a sólo aquellos que satisfagan el método.

Según los Javadocs, la clase FilenameFilter contiene el único método abstracto accept, con la siguiente firma:

boolean accept(File dir, String name)

El argumento File es el directorio en el que se encuentra el archivo, y el nombre String es el nombre del archivo.

El código del Ejemplo 1-4 implementa FilenameFilter utilizando una clase interna anónima para devolver sólo los archivos fuente Java.

Ejemplo 1-4. Implementación anónima de FilenameFilter en una clase interna
File directory = new File("./src/main/java");

String[] names = directory.list(new FilenameFilter() {  1
    @Override
    public boolean accept(File dir, String name) {
        return name.endsWith(".java");
    }
});
System.out.println(Arrays.asList(names));
1

Clase interna anónima

En este caso, el método accept devuelve verdadero si el nombre del archivo termina en .java y falso en caso contrario.

La versión de la expresión lambda se muestra en el Ejemplo 1-5.

Ejemplo 1-5. Expresión lambda que implementa FilenameFilter
File directory = new File("./src/main/java");

String[] names = directory.list((dir, name) -> name.endsWith(".java")); 1
    System.out.println(Arrays.asList(names));
}
1

Expresión lambda

El código resultante es mucho más sencillo. Esta vez los argumentos están contenidos entre paréntesis, pero no tienen tipos declarados. En tiempo de compilación, el compilador sabe que el método list toma un argumento del tipo FilenameFilter, y por tanto conoce la firma de su único método abstracto (accept). Por tanto, sabe que los argumentos de accept son un File y un String, de modo que los argumentos compatibles de la expresión lambda deben coincidir con esos tipos. El tipo de retorno de accept es un booleano, por lo que la expresión a la derecha de la flecha también debe devolver un booleano.

Si deseas especificar los tipos de datos en el código, puedes hacerlo libremente, como en el Ejemplo 1-6.

Ejemplo 1-6. Expresión lambda con tipos de datos explícitos
File directory = new File("./src/main/java");

String[] names = directory.list((File dir, String name) -> 1
    name.endsWith(".java"));
1

Tipos de datos explícitos

Por último, si la implementación de la lambda requiere más de una línea, debes utilizar llaves y una sentencia return explícita, como se muestra en el Ejemplo 1-7.

Ejemplo 1-7. Un bloque lambda
File directory = new File("./src/main/java");

String[] names = directory.list((File dir, String name) -> {  1
    return name.endsWith(".java");
});
System.out.println(Arrays.asList(names));
1

Sintaxis de bloque

Esto se conoce como bloque lambda. En este caso, el cuerpo sigue consistiendo en una sola línea, pero las llaves permiten ahora varias sentencias. Ahora es necesaria la palabra clave return.

Las expresiones lambda nunca existen solas. Siempre hay un contexto para la expresión, que indica la interfaz funcional a la que se asigna la expresión. Una lambda puede ser un argumento de un método, un tipo de retorno de un método o asignarse a una referencia. En cada caso, el tipo de la asignación debe ser una interfaz funcional.

1.2 Referencias del método

Problema

Quieres utilizar una referencia de método para acceder a un método existente y tratarlo como una expresión lambda.

Solución

Utiliza la notación de doble punto para separar una referencia de instancia o nombre de clase del método.((("

(dos puntos dobles) en las referencias a métodos"))

Debate

Si una expresión lambda es esencialmente tratar un método como si fuera un objeto, entonces una referencia a un método trata un método existente como si fuera una lambda.

Por ejemplo, el método forEach de Iterable toma como argumento un Consumer. El ejemplo 1-8 muestra que Consumer puede implementarse como una expresión lambda o como una referencia a un método.

Ejemplo 1-8. Utilizar una referencia a un método para acceder a println
Stream.of(3, 1, 4, 1, 5, 9)
        .forEach(x -> System.out.println(x));     1

Stream.of(3, 1, 4, 1, 5, 9)
        .forEach(System.out::println);            2

Consumer<Integer> printer = System.out::println;  3
Stream.of(3, 1, 4, 1, 5, 9)
        .forEach(printer);
1

Utilizar una expresión lambda

2

Utilizar una referencia de método

3

Asignar la referencia del método a una interfaz funcional

La notación de doble punto proporciona la referencia al método println en la instancia Sys⁠tem.out, que es una referencia de tipo PrintStream. No se colocan paréntesis al final de la referencia al método. En el ejemplo mostrado, cada elemento del flujo se imprime en la salida estándar.2

Consejo

Si escribes una expresión lambda que consta de una sola línea que invoca a un método, considera la posibilidad de utilizar en su lugar la referencia al método equivalente.

La referencia a un método ofrece un par de ventajas (menores) sobre la sintaxis lambda. En primer lugar, suele ser más corta y, en segundo lugar, suele incluir el nombre de la clase que contiene el método. Ambas cosas facilitan la lectura del código.

Las referencias a métodos también pueden utilizarse con métodos estáticos, como se muestra en el Ejemplo 1-9.

Ejemplo 1-9. Utilizar una referencia a un método estático
Stream.generate(Math::random)          1
        .limit(10)
        .forEach(System.out::println); 2
1

Método estático

2

Método de instancia

El método generate de Stream toma como argumento un Supplier, que es una interfaz funcional cuyo único método abstracto no toma argumentos y produce un único resultado. El método random de la clase Math es compatible con esa firma, porque tampoco toma argumentos y produce un único doble pseudoaleatorio uniformemente distribuido entre 0 y 1. La referencia del método Math::random se refiere a ese método como la implementación de la interfaz Supplier.

Como Stream.generate produce un flujo infinito, se utiliza el método limit para garantizar que sólo se producen 10 valores, que luego se imprimen en la salida estándar utilizando la referencia del método System.out::println como implementación de Consumer.

Sintaxis

Existen tres formas de la sintaxis del método de referencia, y una de ellas es un poco engañosa:

object::instanceMethod

Hacer referencia a un método de instancia utilizando una referencia al objeto suministrado, como en Syste⁠m.out::println

Class::staticMethod

Remite al método estático, como en Math::max

Class::instanceMethod

Invoca el método de instancia sobre una referencia a un objeto proporcionada por el contexto, como en String::length

Este último ejemplo es el más confuso, porque como desarrolladores Java estamos acostumbrados a que sólo se invoquen métodos estáticos a través del nombre de una clase. Recuerda que las expresiones lambda y las referencias a métodos nunca existen en el vacío: siempre hay un contexto. En el caso de una referencia a un objeto, el contexto proporcionará el argumento o argumentos al método. En el caso de la impresión, la expresión lambda equivalente es (como se muestra en el contexto del Ejemplo 1-8):

// equivalent to System.out::println
x -> System.out.println(x)

El contexto proporciona el valor de x, que se utiliza como argumento del método.

La situación es similar para el método estático max:

// equivalent to Math::max
(x,y) -> Math.max(x,y)

Ahora el contexto debe proporcionar dos argumentos, y la lambda devuelve el mayor.

La sintaxis "método de instancia a través del nombre de la clase" se interpreta de forma diferente. La lambda equivalente es

// equivalent to String::length
x -> x.length()

Esta vez, cuando el contexto proporciona x, se utiliza como objetivo del método, en lugar de como argumento.

Consejo

Si haces referencia a un método que toma varios argumentos a través del nombre de la clase, el primer elemento suministrado por el contexto se convierte en el objetivo y los elementos restantes son argumentos del método.

El Ejemplo 1-10 muestra el código de ejemplo.

Ejemplo 1-10. Invocar un método de instancia con múltiples argumentos desde una referencia de clase
List<String> strings =
    Arrays.asList("this", "is", "a", "list", "of", "strings");
List<String> sorted = strings.stream()
        .sorted((s1, s2) -> s1.compareTo(s2))  1
        .collect(Collectors.toList());

List<String> sorted = strings.stream()
        .sorted(String::compareTo)             1
        .collect(Collectors.toList());
1

Método de referencia y lambda equivalente

El método sorted de Stream toma como argumento un Comparator<T>, cuyo único método abstracto es int compare(String other). El método sorted suministra cada par de cadenas al comparador y las ordena en función del signo del entero devuelto. En este caso, el contexto es un par de cadenas. La sintaxis de referencia al método, utilizando el nombre de clase String, invoca al método compareTo sobre el primer elemento (s1 en la expresión lambda) y utiliza el segundo elemento s2 como argumento del método.

En el procesamiento de flujos, con frecuencia accedes a un método de instancia utilizando el nombre de la clase en una referencia de método si estás procesando una serie de entradas. El código del Ejemplo 1-11 muestra la invocación del método length en cada String individual del flujo.

Ejemplo 1-11. Invocación del método longitud sobre String utilizando una referencia de método
Stream.of("this", "is", "a", "stream", "of", "strings")
        .map(String::length)            1
        .forEach(System.out::println);  2
1

Método de instancia a través del nombre de la clase

2

Método de instancia mediante referencia a objeto

Este ejemplo transforma cada cadena en un número entero invocando al método length, y luego imprime cada resultado.

Una referencia a un método es esencialmente una sintaxis abreviada de una lambda. Las expresiones lambda son más generales, en el sentido de que cada referencia de método tiene una expresión lambda equivalente, pero no viceversa. Las lambdas equivalentes para las referencias a métodos del Ejemplo 1-11 se muestran en el Ejemplo 1-12.

Ejemplo 1-12. Equivalencias de expresiones lambda para referencias a métodos
Stream.of("this", "is", "a", "stream", "of", "strings")
        .map(s -> s.length())
        .forEach(x -> System.out.println(x));

Como con cualquier expresión lambda, el contexto importa. También puedes utilizar this o super como lado izquierdo de una referencia a un método si hay alguna ambigüedad.

Ver también

También puedes invocar constructores utilizando la sintaxis de referencia a métodos. Las referencias a constructores se muestran en la Receta 1.3. El paquete de interfaces funcionales, incluida la interfaz Supplier que se trata en esta receta, se trata en el Capítulo 2.

1.3 Referencias del Constructor

Problema

Quieres instanciar un objeto utilizando un método de referencia como parte de una cadena de flujo.

Solución

Utiliza la palabra clave new como parte de una referencia a un método.

Debate

Cuando la gente habla de la nueva sintaxis añadida a Java 8, menciona las expresiones lambda, las referencias a métodos y los flujos. Por ejemplo, supongamos que tienes una lista de personas y quieres convertirla en una lista de nombres. Una forma de hacerlo sería el fragmento que se muestra en el Ejemplo 1-13.

Ejemplo 1-13. Convertir una lista de personas en una lista de nombres
List<String> names = people.stream()
    .map(person -> person.getName()) 1
    .collect(Collectors.toList());

// or, alternatively,

List<String> names = people.stream()
    .map(Person::getName)           2
    .collect(Collectors.toList());
1

Expresión lambda

2

Método de referencia

¿Y si quieres hacer el camino inverso? ¿Y si tienes una lista de cadenas y quieres crear a partir de ella una lista de referencias Person? En ese caso puedes utilizar una referencia de método, pero esta vez utilizando la palabra clave new. Esa sintaxis se llama referencia de constructor.

Para mostrar cómo se utiliza, empieza con una clase Person, que es casi el Plain Old Java Object (POJO) más simple que se pueda imaginar. Todo lo que hace es envolver un simple atributo de cadena llamado name en el Ejemplo 1-14.

Ejemplo 1-14. Una clase Persona
public class Person {
    private String name;

    public Person() {}

    public Person(String name) {
        this.name = name;
    }

    // getters and setters ...

    // equals, hashCode, and toString methods ...
}

Dada una colección de cadenas, puedes mapear cada una de ellas en un Person utilizando una expresión lambda o la referencia al constructor del Ejemplo 1-15.

Ejemplo 1-15. Transformar cadenas en instancias Persona
List<String> names =
    Arrays.asList("Grace Hopper", "Barbara Liskov", "Ada Lovelace",
        "Karen Spärck Jones");

List<Person> people = names.stream()
    .map(name -> new Person(name)) 1
    .collect(Collectors.toList());

// or, alternatively,

List<Person> people = names.stream()
    .map(Person::new)              2
    .collect(Collectors.toList());
1

Utilizar una expresión lambda para invocar al constructor

2

Utilizar un constructor de referencia instanciando Person

La sintaxis Person::new hace referencia al constructor de la clase Person. Como ocurre con todas las expresiones lambda, el contexto determina qué constructor se ejecuta. Como el contexto proporciona una cadena, se utiliza el constructor String de un argumento.

Copiar constructor

Un constructor de copia toma un argumento Person y devuelve un nuevo Person con los mismos atributos, como se muestra en el Ejemplo 1-16.

Ejemplo 1-16. Un constructor de copia para Persona
public Person(Person p) {
    this.name = p.name;
}

Esto es útil si quieres aislar el código de flujo de las instancias originales. Por ejemplo, si ya tienes una lista de personas, conviertes la lista en un flujo y luego vuelves a convertirla en una lista, las referencias son las mismas (ver Ejemplo 1-17).

Ejemplo 1-17. Convertir una lista en un flujo y viceversa
Person before = new Person("Grace Hopper");

List<Person> people = Stream.of(before)
    .collect(Collectors.toList());
Person after = people.get(0);

assertTrue(before == after);                          1

before.setName("Grace Murray Hopper");                2
assertEquals("Grace Murray Hopper", after.getName()); 3
1

Mismo objeto

2

Cambia el nombre utilizando la referencia before

3

El nombre ha cambiado en la referencia after

Utilizando un constructor de copia, puedes romper esa conexión, como en el Ejemplo 1-18.

Ejemplo 1-18. Utilizar el constructor de copia
people = Stream.of(before)
      .map(Person::new)            1
      .collect(Collectors.toList());
after = people.get(0);
assertFalse(before == after);      2
assertEquals(before, after);       3

before.setName("Rear Admiral Dr. Grace Murray Hopper");
assertFalse(before.equals(after));
1

Utiliza el constructor de copia

2

Objetos diferentes

3

Pero equivalente

Esta vez, al invocar el método map, el contexto es un flujo de instancias Person. Por tanto, la sintaxis Person::new invoca al constructor que toma una Person y devuelve una instancia nueva, pero equivalente, y ha roto la conexión entre la referencia anterior y la referencia posterior.3

Constructor Varargs

Considera ahora un constructor varargs añadido al POJO Person, que se muestra en el Ejemplo 1-19.

Ejemplo 1-19. Un constructor Persona que toma como argumento una lista variable de String
public Person(String... names) {
    this.name = Arrays.stream(names)
                      .collect(Collectors.joining(" "));
}

Este constructor toma cero o más argumentos de cadena y los concatena con un único espacio como delimitador.

¿Cómo se puede invocar a ese constructor? Cualquier cliente que pase cero o más argumentos de cadena separados por comas lo invocará. Una forma de hacerlo es aprovechar el método split de String, que toma un delimitador y devuelve una matriz String:

String[] split(String delimiter)

Por tanto, el código del Ejemplo 1-20 divide cada cadena de la lista en palabras individuales e invoca al constructor varargs.

Ejemplo 1-20. Utilizar el constructor varargs
names.stream()                     1
    .map(name -> name.split(" "))  2
    .map(Person::new)              3
    .collect(Collectors.toList()); 4
1

Crear un flujo de cadenas

2

Mapa a un flujo de matrices de cadenas

3

Mapa a una corriente de Person

4

Recoger en una lista de Person

Esta vez, el contexto del método map que contiene la referencia al constructor Person::new es un flujo de matrices de cadenas, por lo que se llama al constructor varargs. Si añades una simple sentencia print a ese constructor

System.out.println("Varargs ctor, names=" + Arrays.asList(names));

entonces el resultado es

Varargs ctor, names=[Grace, Hopper]
Varargs ctor, names=[Barbara, Liskov]
Varargs ctor, names=[Ada, Lovelace]
Varargs ctor, names=[Karen, Spärck, Jones]

Matrices

Las referencias a constructores también pueden utilizarse con matrices. Si quieres una matriz de instancias de Person, Person[], en lugar de una lista, puedes utilizar el método toArray en Stream, cuya firma es:

<A> A[] toArray(IntFunction<A[]> generator)

Este método utiliza A para representar el tipo genérico de la matriz devuelta que contiene los elementos del flujo, que se crea utilizando la función generadora proporcionada. Lo bueno es que también se puede utilizar para ello una referencia al constructor, como en el Ejemplo 1-21.

Ejemplo 1-21. Crear una matriz de referencias Persona
Person[] people = names.stream()
    .map(Person::new)         1
    .toArray(Person[]::new);  2
1

Referencia del constructor para Person

2

Referencia del constructor para una matriz de Person

El argumento del método toArray crea una matriz de referencias Person del tamaño adecuado y la rellena con las instancias Person.

Las referencias a constructores no son más que referencias a métodos con otro nombre, utilizando la palabra new para invocar a un constructor. El constructor viene determinado por el contexto, como siempre. Esta técnica da mucha flexibilidad a la hora de procesar flujos.

Ver también

Las referencias de los métodos se tratan en la Receta 1.2.

1.4 Interfaces funcionales

Problema

Quieres utilizar una interfaz funcional existente, o escribir la tuya propia.

Solución

Crea una interfaz con un único método abstracto y añade la anotación @FunctionalInterface.

Debate

Una interfaz funcional en Java 8 es una interfaz con un único método abstracto. Como tal, puede ser el objetivo de una expresión lambda o de una referencia a un método.

El uso del término abstract aquí es significativo. Antes de Java 8, todos los métodos de las interfaces se consideraban abstractos por defecto; ni siquiera era necesario añadir la palabra clave.

Por ejemplo, aquí tienes la definición de una interfaz llamada PalindromeChecker, que se muestra en el Ejemplo 1-22.

Ejemplo 1-22. Interfaz de un comprobador de palíndromos
@FunctionalInterface
public interface PalindromeChecker {
    boolean isPalidrome(String s);
}

Todos los métodos de una interfaz son public,4 por lo que puedes omitir el modificador de acceso, igual que puedes omitir la palabra clave abstract.

Como esta interfaz sólo tiene un método abstracto, es una interfaz funcional. Java 8 proporciona una anotación llamada @FunctionalInterface en el paquete java.lang que puede aplicarse a la interfaz, como se muestra en el ejemplo.

Esta anotación no es necesaria, pero es una buena idea, por dos razones. En primer lugar, activa una comprobación en tiempo de compilación de que la interfaz satisface, de hecho, el requisito. Si la interfaz tiene cero métodos abstractos o más de uno, obtendrás un error del compilador.

La otra ventaja de añadir la anotación @FunctionalInterface es que genera una declaración en los Javadocs como la siguiente:

Functional Interface:
This is a functional interface and can therefore be used as the assignment
target for a lambda expression or method reference.

Las interfaces funcionales también pueden tener métodos default y static. Tanto los métodos por defecto como los estáticos tienen implementaciones, por lo que no cuentan para el requisito de un único método abstracto. El ejemplo 1-23 muestra el código de ejemplo.

Ejemplo 1-23. MiInterfaz es una interfaz funcional con métodos estáticos y por defecto
@FunctionalInterface
public interface MyInterface {
    int myMethod();          1
    // int myOtherMethod();  2

    default String sayHello() {
        return "Hello, World!";
    }

    static void myStaticMethod() {
        System.out.println("I'm a static method in an interface");
    }
}
1

Método abstracto único

2

Si se añade, dejaría de ser una interfaz funcional

Ten en cuenta que si se incluyera el método comentado myOtherMethod, la interfaz dejaría de satisfacer el requisito de interfaz funcional. La anotación generaría un error del tipo "se han encontrado varios métodos abstractos no sustitutivos".

Las interfaces pueden ampliar otras interfaces, incluso más de una. La anotación comprueba la interfaz actual. Por tanto, si una interfaz extiende una interfaz funcional existente y añade otro método abstracto, no es en sí misma una interfaz funcional. Véase el ejemplo 1-24.

Ejemplo 1-24. Ampliar una interfaz funcional que ya no es funcional
public interface MyChildInterface extends MyInterface {
    int anotherMethod(); 1
}
1

Método abstracto adicional

MyChildInterface no es una interfaz funcional, porque tiene dos métodos abstractos: myMethod MyInterface , que hereda de ; y anotherMethod, que declara. Sin la anotación @FunctionalInterface, esto compila, porque es una interfaz estándar. Sin embargo, no puede ser el objetivo de una expresión lambda.

También hay que señalar un perímetro. La interfaz Comparator se utiliza para la ordenación, de la que se habla en otras recetas. Si miras los Javadocs de esa interfaz y seleccionas la pestaña Métodos abstractos, verás los métodos que se muestran en la Figura 1-1.

mjr 0101
Figura 1-1. Métodos abstractos de la clase Comparador

Espera, ¿qué? ¿Cómo puede ser una interfaz funcional si hay dos métodos abstractos, sobre todo si uno de ellos está realmente implementado en java.lang.Object?

Lo que es especial aquí es que el método equals que se muestra es de Object, y por tanto ya tiene una implementación por defecto. La documentación detallada dice que, por razones de rendimiento, puedes proporcionar tu propio método equals que satisfaga el mismo contrato, pero que "siempre es seguro no (énfasis en el original) anular" este método.

Las reglas de las interfaces funcionales dicen que los métodos de Object no cuentan para el límite de un único método abstracto, por lo que Comparator sigue siendo una interfaz funcional.

Ver también

Los métodos por defecto de las interfaces se tratan en la Receta 1. 5, y los métodos estáticos de las interfaces se tratan en la Receta 1.6.

1.5 Métodos por defecto en las interfaces

Problema

Quieres proporcionar una implementación de un método dentro de una interfaz.

Solución

Utiliza la palabra clave default en el método de la interfaz, y añade la implementación de la forma normal.

Debate

La razón tradicional por la que Java nunca ha admitido la herencia múltiple es el llamado problema del diamante. Supongamos que tienes una jerarquía de herencia como la que se muestra en la Figura 1-2(vagamente parecida a UML).

mjr 0102
Figura 1-2. Herencia animal

La clase Animal tiene dos clases hijas, Bird y Horse, cada una de las cuales anula el método speak de Animal, en Horse para decir "relincho" y en Bird para decir "chirrido". Entonces, ¿qué dice Pegasus (que hereda múltiples veces tanto de Horse como de Bird)5 ¿dice? ¿Qué pasa si tienes una referencia de tipo Animal asignada a una instancia de Pegasus? ¿Qué debería devolver entonces el método speak?

Animal animal = new Pegaus();
animal.speak(); // whinny, chirp, or other?

Los distintos lenguajes adoptan enfoques diferentes ante este problema. En C++, por ejemplo, se permite la herencia múltiple, pero si una clase hereda implementaciones conflictivas, no compilará.6 En Eiffel7 el compilador te permite elegir qué implementación quieres.

El planteamiento de Java era prohibir la herencia múltiple, y las interfaces se introdujeron como solución para cuando una clase tiene una relación "es un tipo de" con más de un tipo. Como las interfaces sólo tenían métodos abstractos, no había implementaciones que entraran en conflicto. La herencia múltiple está permitida con las interfaces, pero de nuevo funciona porque sólo se heredan las firmas de los métodos.

El problema es que, si nunca puedes implementar un método en una interfaz, acabas con algunos diseños incómodos. Entre los métodos de la interfaz java.util.Collection, por ejemplo, están:

boolean isEmpty()
int     size()

El método isEmpty devuelve verdadero si no hay elementos en la colección, y falso en caso contrario. El método size devuelve el número de elementos de la colección. Independientemente de la implementación subyacente, puedes implementar inmediatamente el método isEmpty en términos de size, como en el Ejemplo 1-25.

Ejemplo 1-25. Implementación de isEmpty en función del tamaño
public boolean isEmpty() {
    return size() == 0;
}

Como Collection es una interfaz, no puedes hacer esto en la propia interfaz. En su lugar, la biblioteca estándar incluye una clase abstracta llamada java.util.AbstractCollection, que incluye, entre otro código, exactamente la implementación de isEmpty que se muestra aquí. Si estás creando tu propia implementación de la colección y aún no tienes una superclase, puedes extender AbstractCollection y obtendrás el método isEmpty de forma gratuita. Si ya tienes una superclase, tienes que implementar la interfaz Col⁠lection y acordarte de proporcionar tu propia implementación de isEmpty así como de size.

Todo esto es bastante familiar para los desarrolladores experimentados de Java, pero a partir de Java 8 la situación cambia. Ahora puedes añadir implementaciones a los métodos de la interfaz. Todo lo que tienes que hacer es añadir la palabra clave default a un método y proporcionar una implementación. El código del Ejemplo 1-26 muestra una interfaz con métodos abstractos y predeterminados.

Ejemplo 1-26. Una interfaz de Empleado con un método por defecto
public interface Employee {
    String getFirst();

    String getLast();

    void convertCaffeineToCodeForMoney();

    default String getName() {  1
        return String.format("%s %s", getFirst(), getLast());
    }
}
1

Método por defecto con una implementación

El método getName tiene la palabra clave default, y su implementación se realiza en términos de los otros métodos abstractos de la interfaz, getFirst y getLast.

Muchas de las interfaces existentes en Java se han mejorado con métodos por defecto para mantener la compatibilidad con versiones anteriores. Normalmente, cuando añades un nuevo método a una interfaz, rompes todas las implementaciones existentes. Al añadir un nuevo método por defecto, todas las implementaciones existentes heredan el nuevo método y siguen funcionando. Esto permitió a los responsables de la biblioteca añadir nuevos métodos por defecto en todo el JDK sin romper las implementaciones existentes.

Por ejemplo, java.util.Collection contiene ahora los siguientes métodos por defecto:

default boolean        removeIf(Predicate<? super E> filter)
default Stream<E>      stream()
default Stream<E>      parallelStream()
default Spliterator<E> spliterator()

El método removeIf elimina todos los elementos de la colección que cumplan el argumento Predicate8 y devuelve true si se ha eliminado algún elemento. Los métodos stream y parallelStream son métodos de fábrica para crear flujos. El método spliterator devuelve un objeto de una clase que implementa la interfaz Spliterator, que es un objeto para recorrer y particionar elementos de una fuente.

Los métodos por defecto se utilizan del mismo modo que cualquier otro método, como muestra el Ejemplo 1-27.

Ejemplo 1-27. Utilizar métodos por defecto
List<Integer> nums = new ArrayList<>();
nums.add(-3); nums.add(1); nums.add(4);
nums.add(-1); nums.add(5); nums.add(9);
boolean removed = nums.removeIf(n -> n <= 0);  1
System.out.println("Elements were " + (removed ? "" : "NOT") + " removed");
nums.forEach(System.out::println);             2
1

Utiliza el método default removeIf de Collection

2

Utiliza el método default forEach de Iterator

¿Qué ocurre cuando una clase implementa dos interfaces con el mismo método por defecto? Ése es el tema de la Receta 5.5, pero la respuesta breve es que si la clase implementa el propio método todo va bien. Para más detalles, consulta la Receta 5.5.

Ver también

La receta 5.5 muestra las reglas que se aplican cuando una clase implementa varias interfaces con métodos por defecto.

1.6 Métodos estáticos en interfaces

Problema

Quieres añadir un método de utilidad a nivel de clase a una interfaz, junto con una implementación.

Solución

Haz que el método sea static y proporciona la implementación de la forma habitual.

Debate

Los miembros estáticos de las clases Java son de nivel de clase, lo que significa que están asociados a la clase en su conjunto y no a una instancia concreta. Esto hace que su uso en interfaces sea problemático desde el punto de vista del diseño. Algunas preguntas son:

  • ¿Qué significa un miembro a nivel de clase cuando la interfaz es implementada por muchas clases diferentes?

  • ¿Es necesario que una clase implemente una interfaz para poder utilizar un método estático?

  • A los métodos estáticos de las clases se accede por el nombre de la clase. Si una clase implementa una interfaz, ¿se llama a un método estático desde el nombre de la clase o desde el nombre de la interfaz?

Los diseñadores de Java podrían haber decidido estas cuestiones de varias formas distintas. Antes de Java 8, la decisión era no permitir miembros estáticos en las interfaces.

Pero, por desgracia, eso llevó a la creación de clases de utilidad: clases que sólo contienen métodos estáticos. Un ejemplo típico es java.util.Collections, que contiene métodos para ordenar y buscar, envolver colecciones en tipos sincronizados o no modificables, y mucho más. En el paquete NIO, java.nio.file.Paths es otro ejemplo. Sólo contiene métodos estáticos que analizan instancias de Path a partir de cadenas o URIs.

Ahora, en Java 8, puedes añadir métodos estáticos a las interfaces siempre que quieras. Los requisitos son:

  • Añade la palabra clave static al método.

  • Proporcionan una implementación (que no se puede sobrescribir). De este modo, son como los métodos de default, y se incluyen en la ficha por defecto de los Javadocs.

  • Accede al método utilizando el nombre de la interfaz. Las clases no necesitan implementar una interfaz para utilizar sus métodos estáticos.

Un ejemplo de método estático conveniente en una interfaz es el método comparing de java.util.Comparator, junto con sus variantes primitivas, comparingInt, compa⁠ringLong y comparingDouble. La interfaz Comparator también tiene los métodos estáticos naturalOrder y reverseOrder. El ejemplo 1-28 muestra cómo se utilizan.

Ejemplo 1-28. Ordenar cadenas
List<String> bonds = Arrays.asList("Connery", "Lazenby", "Moore",
    "Dalton", "Brosnan", "Craig");

List<String> sorted = bonds.stream()
    .sorted(Comparator.naturalOrder())                 1
    .collect(Collectors.toList());
// [Brosnan, Connery, Craig, Dalton, Lazenby, Moore]

sorted = bonds.stream()
    .sorted(Comparator.reverseOrder())                 2
    .collect(Collectors.toList());
// [Moore, Lazenby, Dalton, Craig, Connery, Brosnan]

sorted = bonds.stream()
    .sorted(Comparator.comparing(String::toLowerCase)) 3
    .collect(Collectors.toList());
// [Brosnan, Connery, Craig, Dalton, Lazenby, Moore]

sorted = bonds.stream()
    .sorted(Comparator.comparingInt(String::length))   4
    .collect(Collectors.toList());
// [Moore, Craig, Dalton, Connery, Lazenby, Brosnan]

sorted = bonds.stream()
    .sorted(Comparator.comparingInt(String::length)    5
        .thenComparing(Comparator.naturalOrder()))
    .collect(Collectors.toList());
// [Craig, Moore, Dalton, Brosnan, Connery, Lazenby]
1

Orden natural (lexicográfico)

2

Lexicografía inversa

3

Ordenar por nombre en minúsculas

4

Ordenar por longitud del nombre

5

Ordena por longitud, luego iguala las longitudes lexicográficamente

El ejemplo muestra cómo utilizar varios métodos estáticos en Comparator para ordenar la lista de actores que han interpretado a James Bond a lo largo de los años.9 Los comparadores se tratan con más detalle en la Receta 4.1.

Los métodos estáticos de las interfaces eliminan la necesidad de crear clases de utilidad independientes, aunque esa opción sigue estando disponible si un diseño lo requiere.

Los puntos clave que debe recordar son:

  • Los métodos estáticos deben tener una implementación

  • No puedes anular un método estático

  • Llamar a métodos estáticos a partir del nombre de la interfaz

  • No necesitas implementar una interfaz para utilizar sus métodos estáticos

Ver también

Los métodos estáticos de las interfaces se utilizan en todo este libro, pero la Receta 4.1 trata de los métodos estáticos de Comparator utilizados aquí.

1 Acuñado por Gordon Moore, uno de los cofundadores de Fairchild Semiconductor e Intel, basándose en la observación de que el número de transistores que podían empaquetarse en un circuito integrado se duplicaba aproximadamente cada 18 meses. Para más detalles, véase la entrada de Wikipedia sobre la ley de Moore.

2 Es difícil hablar de las lambdas o de las referencias a métodos sin hablar de los flujos, que tienen su propio capítulo más adelante. Baste decir que un flujo produce una serie de elementos secuencialmente, no los almacena en ninguna parte y no modifica la fuente original.

3 No pretendo faltar al respeto al tratar al almirante Hopper como un objeto. No dudo de que aún podría patearme el trasero, y falleció en 1992.

4 Al menos hasta Java 9, cuando los métodos private también están permitidos en las interfaces. Consulta la Receta 10.2 para más detalles.

5 "Un caballo magnífico, con el cerebro de un pájaro". (Película Hércules de Disney, que es divertida si finges que no sabes nada de mitología griega y nunca has oído hablar de Hércules).

6 Esto puede solucionarse utilizando la herencia virtual, pero aún así.

7 Ahí tienes una referencia oscura, pero Eiffel fue uno de los lenguajes fundacionales de la programación orientada a objetos. Véase Construcción de software orientado a objetos, segunda edición, de Bertrand Meyer (Prentice Hall, 1997).

8 Predicate es una de las nuevas interfaces funcionales del paquete java.util.function, que se describe detalladamente en la Receta 2.3.

9 La tentación de añadir a Idris Elba a la lista es casi abrumadora, pero de momento no ha habido suerte.

Get Recetas Java modernas now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.