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
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
(
)
{
@Override
public
void
run
(
)
{
System
.
out
.
println
(
"inside runnable using an anonymous inner class"
)
;
}
}
)
.
start
(
)
;
}
}
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.FilenameFilter
, que también forma parte de la biblioteca estándar de Java desde la versión 1.0. Las instancias de FilenameFilter
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
(
)
{
@Override
public
boolean
accept
(
File
dir
,
String
name
)
{
return
name
.
endsWith
(
".java"
)
;
}
}
)
;
System
.
out
.
println
(
Arrays
.
asList
(
names
)
)
;
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"
)
)
;
System
.
out
.
println
(
Arrays
.
asList
(
names
)
)
;
}
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
)
-
>
name
.
endsWith
(
".java"
)
)
;
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
)
-
>
{
return
name
.
endsWith
(
".java"
)
;
}
)
;
System
.
out
.
println
(
Arrays
.
asList
(
names
)
)
;
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
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
)
)
;
Stream
.
of
(
3
,
1
,
4
,
1
,
5
,
9
)
.
forEach
(
System
.
out
:
:
println
)
;
Consumer
<
Integer
>
printer
=
System
.
out
:
:
println
;
Stream
.
of
(
3
,
1
,
4
,
1
,
5
,
9
)
.
forEach
(
printer
)
;
La notación de doble punto proporciona la referencia al método println
en la instancia System.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
)
.
limit
(
10
)
.
forEach
(
System
.
out
:
:
println
)
;
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
System.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
)
)
.
collect
(
Collectors
.
toList
(
)
)
;
List
<
String
>
sorted
=
strings
.
stream
(
)
.
sorted
(
String:
:
compareTo
)
.
collect
(
Collectors
.
toList
(
)
)
;
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
)
.
forEach
(
System
.
out
:
:
println
)
;
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
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
(
)
)
.
collect
(
Collectors
.
toList
(
)
)
;
// or, alternatively,
List
<
String
>
names
=
people
.
stream
(
)
.
map
(
Person:
:
getName
)
.
collect
(
Collectors
.
toList
(
)
)
;
¿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
)
)
.
collect
(
Collectors
.
toList
(
)
)
;
// or, alternatively,
List
<
Person
>
people
=
names
.
stream
(
)
.
map
(
Person:
:
new
)
.
collect
(
Collectors
.
toList
(
)
)
;
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
)
;
before
.
setName
(
"Grace Murray Hopper"
)
;
assertEquals
(
"Grace Murray Hopper"
,
after
.
getName
(
)
)
;
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
)
.
collect
(
Collectors
.
toList
(
)
)
;
after
=
people
.
get
(
0
)
;
assertFalse
(
before
=
=
after
)
;
assertEquals
(
before
,
after
)
;
before
.
setName
(
"Rear Admiral Dr. Grace Murray Hopper"
)
;
assertFalse
(
before
.
equals
(
after
)
)
;
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
(
)
.
map
(
name
-
>
name
.
split
(
" "
)
)
.
map
(
Person:
:
new
)
.
collect
(
Collectors
.
toList
(
)
)
;
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
)
.
toArray
(
Person
[
]
:
:
new
)
;
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
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
(
)
;
// int myOtherMethod();
default
String
sayHello
(
)
{
return
"Hello, World!"
;
}
static
void
myStaticMethod
(
)
{
System
.
out
.
println
(
"I'm a static method in an interface"
)
;
}
}
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
(
)
;
}
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.
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
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).
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 Collection
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
(
)
{
return
String
.
format
(
"%s %s"
,
getFirst
(
)
,
getLast
(
)
)
;
}
}
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 Predicate
8 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
)
;
System
.
out
.
println
(
"Elements were "
+
(
removed
?
""
:
"NOT"
)
+
" removed"
)
;
nums
.
forEach
(
System
.
out
:
:
println
)
;
¿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
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
, comparingLong
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
(
)
)
.
collect
(
Collectors
.
toList
(
)
)
;
// [Brosnan, Connery, Craig, Dalton, Lazenby, Moore]
sorted
=
bonds
.
stream
(
)
.
sorted
(
Comparator
.
reverseOrder
(
)
)
.
collect
(
Collectors
.
toList
(
)
)
;
// [Moore, Lazenby, Dalton, Craig, Connery, Brosnan]
sorted
=
bonds
.
stream
(
)
.
sorted
(
Comparator
.
comparing
(
String:
:
toLowerCase
)
)
.
collect
(
Collectors
.
toList
(
)
)
;
// [Brosnan, Connery, Craig, Dalton, Lazenby, Moore]
sorted
=
bonds
.
stream
(
)
.
sorted
(
Comparator
.
comparingInt
(
String:
:
length
)
)
.
collect
(
Collectors
.
toList
(
)
)
;
// [Moore, Craig, Dalton, Connery, Lazenby, Brosnan]
sorted
=
bonds
.
stream
(
)
.
sorted
(
Comparator
.
comparingInt
(
String:
:
length
)
.
thenComparing
(
Comparator
.
naturalOrder
(
)
)
)
.
collect
(
Collectors
.
toList
(
)
)
;
// [Craig, Moore, Dalton, Brosnan, Connery, Lazenby]
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.