Este artículo fue publicado originalmente en mi blog.
También deberías leer mi Tutorial de Java 11 (que incluye nuevos lenguajes y funciones API de Java 9, 10 y 11).
Bienvenido a mi introducción a Java 8. Este tutorial lo guía paso a paso a través de todas las funciones nuevas del lenguaje. Con el respaldo de ejemplos de código breves y simples, aprenderá a utilizar métodos de interfaz predeterminados, expresiones lambda, referencias de métodos y anotaciones repetibles. Al final del artículo, estará familiarizado con los cambios de API más recientes, como transmisiones, interfaces funcionales, extensiones de mapas y la nueva API de fecha. Sin paredes de texto, solo un montón de fragmentos de código comentados. ¡Disfrutar!
★★★ ¿Te gusta este proyecto? Deja una estrella, síguenos en Twitter o haz una donación para apoyar mi trabajo. ¡Gracias! ★★★
Java 8 nos permite agregar implementaciones de métodos no abstractos a las interfaces utilizando la palabra clave default
. Esta característica también se conoce como métodos de extensión virtual.
Aquí está nuestro primer ejemplo:
interface Formula {
double calculate ( int a );
default double sqrt ( int a ) {
return Math . sqrt ( a );
}
}
Además del método abstracto, calculate
la interfaz. Formula
también define el método predeterminado sqrt
. Las clases concretas sólo tienen que implementar el método abstracto calculate
. El método predeterminado sqrt
se puede utilizar de forma inmediata.
Formula formula = new Formula () {
@ Override
public double calculate ( int a ) {
return sqrt ( a * 100 );
}
};
formula . calculate ( 100 ); // 100.0
formula . sqrt ( 16 ); // 4.0
La fórmula se implementa como un objeto anónimo. El código es bastante detallado: 6 líneas de código para un cálculo tan simple de sqrt(a * 100)
. Como veremos en la siguiente sección, existe una forma mucho mejor de implementar objetos de método único en Java 8.
Comencemos con un ejemplo sencillo de cómo ordenar una lista de cadenas en versiones anteriores de Java:
List < String > names = Arrays . asList ( "peter" , "anna" , "mike" , "xenia" );
Collections . sort ( names , new Comparator < String >() {
@ Override
public int compare ( String a , String b ) {
return b . compareTo ( a );
}
});
El método de utilidad estático Collections.sort
acepta una lista y un comparador para ordenar los elementos de la lista dada. A menudo te encuentras creando comparadores anónimos y los pasas al método de clasificación.
En lugar de crear objetos anónimos todo el día, Java 8 viene con una sintaxis mucho más corta, expresiones lambda :
Collections . sort ( names , ( String a , String b ) -> {
return b . compareTo ( a );
});
Como puede ver, el código es mucho más corto y más fácil de leer. Pero se hace aún más corto:
Collections . sort ( names , ( String a , String b ) -> b . compareTo ( a ));
Para cuerpos de métodos de una línea, puede omitir las llaves {}
y la palabra clave return
. Pero se hace aún más corto:
names . sort (( a , b ) -> b . compareTo ( a ));
La lista ahora tiene un método sort
. Además, el compilador de Java conoce los tipos de parámetros, por lo que también puede omitirlos. Profundicemos en cómo se pueden usar las expresiones lambda en la naturaleza.
¿Cómo encajan las expresiones lambda en el sistema de tipos de Java? Cada lambda corresponde a un tipo determinado, especificado por una interfaz. Una llamada interfaz funcional debe contener exactamente una declaración de método abstracto . Cada expresión lambda de ese tipo coincidirá con este método abstracto. Dado que los métodos predeterminados no son abstractos, puede agregar métodos predeterminados a su interfaz funcional.
Podemos usar interfaces arbitrarias como expresiones lambda siempre que la interfaz solo contenga un método abstracto. Para asegurarse de que su interfaz cumpla con los requisitos, debe agregar la anotación @FunctionalInterface
. El compilador es consciente de esta anotación y genera un error de compilación tan pronto como intenta agregar una segunda declaración de método abstracto a la interfaz.
Ejemplo:
@ FunctionalInterface
interface Converter < F , T > {
T convert ( F from );
}
Converter < String , Integer > converter = ( from ) -> Integer . valueOf ( from );
Integer converted = converter . convert ( "123" );
System . out . println ( converted ); // 123
Tenga en cuenta que el código también es válido si se omite la anotación @FunctionalInterface
.
El código de ejemplo anterior se puede simplificar aún más utilizando referencias de métodos estáticos:
Converter < String , Integer > converter = Integer :: valueOf ;
Integer converted = converter . convert ( "123" );
System . out . println ( converted ); // 123
Java 8 le permite pasar referencias de métodos o constructores mediante la palabra clave ::
. El ejemplo anterior muestra cómo hacer referencia a un método estático. Pero también podemos hacer referencia a métodos de objetos:
class Something {
String startsWith ( String s ) {
return String . valueOf ( s . charAt ( 0 ));
}
}
Something something = new Something ();
Converter < String , String > converter = something :: startsWith ;
String converted = converter . convert ( "Java" );
System . out . println ( converted ); // "J"
Veamos cómo funciona la palabra clave ::
para los constructores. Primero definimos una clase de ejemplo con diferentes constructores:
class Person {
String firstName ;
String lastName ;
Person () {}
Person ( String firstName , String lastName ) {
this . firstName = firstName ;
this . lastName = lastName ;
}
}
A continuación especificamos una interfaz de fábrica de personas que se utilizará para crear nuevas personas:
interface PersonFactory < P extends Person > {
P create ( String firstName , String lastName );
}
En lugar de implementar la fábrica manualmente, unimos todo mediante referencias del constructor:
PersonFactory < Person > personFactory = Person :: new ;
Person person = personFactory . create ( "Peter" , "Parker" );
Creamos una referencia al constructor Person a través de Person::new
. El compilador de Java elige automáticamente el constructor correcto haciendo coincidir la firma de PersonFactory.create
.
Acceder a variables de alcance externo desde expresiones lambda es muy similar a objetos anónimos. Puede acceder a las variables finales desde el ámbito externo local, así como a campos de instancia y variables estáticas.
Podemos leer variables locales finales desde el alcance externo de las expresiones lambda:
final int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
stringConverter . convert ( 2 ); // 3
Pero a diferencia de los objetos anónimos, la variable num
no tiene que declararse final. Este código también es válido:
int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
stringConverter . convert ( 2 ); // 3
Sin embargo, num
debe ser implícitamente final para que se pueda compilar el código. El siguiente código no se compila:
int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
num = 3 ;
También está prohibido escribir en num
desde la expresión lambda.
A diferencia de las variables locales, tenemos acceso de lectura y escritura a campos de instancia y variables estáticas desde expresiones lambda. Este comportamiento es bien conocido en objetos anónimos.
class Lambda4 {
static int outerStaticNum ;
int outerNum ;
void testScopes () {
Converter < Integer , String > stringConverter1 = ( from ) -> {
outerNum = 23 ;
return String . valueOf ( from );
};
Converter < Integer , String > stringConverter2 = ( from ) -> {
outerStaticNum = 72 ;
return String . valueOf ( from );
};
}
}
¿Recuerdas el ejemplo de fórmula de la primera sección? Formula
de interfaz define un método predeterminado sqrt
al que se puede acceder desde cada instancia de fórmula, incluidos los objetos anónimos. Esto no funciona con expresiones lambda.
No se puede acceder a los métodos predeterminados desde expresiones lambda. El siguiente código no se compila:
Formula formula = ( a ) -> sqrt ( a * 100 );
La API JDK 1.8 contiene muchas interfaces funcionales integradas. Algunos de ellos son bien conocidos por versiones anteriores de Java como Comparator
o Runnable
. Esas interfaces existentes se amplían para habilitar la compatibilidad con Lambda a través de la anotación @FunctionalInterface
.
Pero la API de Java 8 también está llena de nuevas interfaces funcionales para hacerle la vida más fácil. Algunas de esas nuevas interfaces son bien conocidas en la biblioteca de Google Guava. Incluso si está familiarizado con esta biblioteca, debe estar atento a cómo se amplían esas interfaces mediante algunas extensiones de métodos útiles.
Los predicados son funciones con valores booleanos de un argumento. La interfaz contiene varios métodos predeterminados para componer predicados en términos lógicos complejos (y, o, negar)
Predicate < String > predicate = ( s ) -> s . length () > 0 ;
predicate . test ( "foo" ); // true
predicate . negate (). test ( "foo" ); // false
Predicate < Boolean > nonNull = Objects :: nonNull ;
Predicate < Boolean > isNull = Objects :: isNull ;
Predicate < String > isEmpty = String :: isEmpty ;
Predicate < String > isNotEmpty = isEmpty . negate ();
Las funciones aceptan un argumento y producen un resultado. Se pueden utilizar métodos predeterminados para encadenar varias funciones (componer y luego).
Function < String , Integer > toInteger = Integer :: valueOf ;
Function < String , String > backToString = toInteger . andThen ( String :: valueOf );
backToString . apply ( "123" ); // "123"
Los proveedores producen un resultado de un tipo genérico determinado. A diferencia de las Funciones, los Proveedores no aceptan argumentos.
Supplier < Person > personSupplier = Person :: new ;
personSupplier . get (); // new Person
Los consumidores representan operaciones que se realizarán en un único argumento de entrada.
Consumer < Person > greeter = ( p ) -> System . out . println ( "Hello, " + p . firstName );
greeter . accept ( new Person ( "Luke" , "Skywalker" ));
Los comparadores son bien conocidos por las versiones anteriores de Java. Java 8 agrega varios métodos predeterminados a la interfaz.
Comparator < Person > comparator = ( p1 , p2 ) -> p1 . firstName . compareTo ( p2 . firstName );
Person p1 = new Person ( "John" , "Doe" );
Person p2 = new Person ( "Alice" , "Wonderland" );
comparator . compare ( p1 , p2 ); // > 0
comparator . reversed (). compare ( p1 , p2 ); // < 0
Los opcionales no son interfaces funcionales, sino utilidades ingeniosas para evitar NullPointerException
. Es un concepto importante para la siguiente sección, así que echemos un vistazo rápido a cómo funcionan los opcionales.
Opcional es un contenedor simple para un valor que puede ser nulo o no nulo. Piense en un método que puede devolver un resultado no nulo pero que a veces no devuelve nada. En lugar de devolver null
, devuelve un Optional
en Java 8.
Optional < String > optional = Optional . of ( "bam" );
optional . isPresent (); // true
optional . get (); // "bam"
optional . orElse ( "fallback" ); // "bam"
optional . ifPresent (( s ) -> System . out . println ( s . charAt ( 0 ))); // "b"
Un java.util.Stream
representa una secuencia de elementos en los que se pueden realizar una o más operaciones. Las operaciones de flujo son intermedias o terminales . Mientras que las operaciones de terminal devuelven un resultado de cierto tipo, las operaciones intermedias devuelven la secuencia misma para que pueda encadenar varias llamadas a métodos seguidas. Las secuencias se crean en una fuente, por ejemplo, una java.util.Collection
como listas o conjuntos (no se admiten mapas). Las operaciones de flujo se pueden ejecutar de forma secuencial o paralela.
Las transmisiones son extremadamente poderosas, por lo que escribí un tutorial de transmisiones de Java 8 por separado. También deberías consultar Sequency como una biblioteca similar para la web.
Primero veamos cómo funcionan las transmisiones secuenciales. Primero creamos una fuente de muestra en forma de una lista de cadenas:
List < String > stringCollection = new ArrayList <>();
stringCollection . add ( "ddd2" );
stringCollection . add ( "aaa2" );
stringCollection . add ( "bbb1" );
stringCollection . add ( "aaa1" );
stringCollection . add ( "bbb3" );
stringCollection . add ( "ccc" );
stringCollection . add ( "bbb2" );
stringCollection . add ( "ddd1" );
Las colecciones en Java 8 están ampliadas para que pueda crear secuencias simplemente llamando Collection.stream()
o Collection.parallelStream()
. Las siguientes secciones explican las operaciones de transmisión más comunes.
Filter acepta un predicado para filtrar todos los elementos de la secuencia. Esta operación es intermedia , lo que nos permite llamar a otra operación de flujo ( forEach
) en el resultado. ForEach acepta la ejecución de un consumidor para cada elemento del flujo filtrado. ForEach es una operación de terminal. Es void
, por lo que no podemos llamar a otra operación de transmisión.
stringCollection
. stream ()
. filter (( s ) -> s . startsWith ( "a" ))
. forEach ( System . out :: println );
// "aaa2", "aaa1"
Ordenado es una operación intermedia que devuelve una vista ordenada de la secuencia. Los elementos se ordenan en orden natural a menos que pase un Comparator
personalizado.
stringCollection
. stream ()
. sorted ()
. filter (( s ) -> s . startsWith ( "a" ))
. forEach ( System . out :: println );
// "aaa1", "aaa2"
Tenga en cuenta que sorted
solo crea una vista ordenada de la transmisión sin manipular el orden de la colección respaldada. El orden de stringCollection
no se modifica:
System . out . println ( stringCollection );
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
El map
de operación intermedia convierte cada elemento en otro objeto mediante la función dada. El siguiente ejemplo convierte cada cadena en una cadena en mayúsculas. Pero también puedes usar map
para transformar cada objeto en otro tipo. El tipo genérico de la secuencia resultante depende del tipo genérico de la función que pasa al map
.
stringCollection
. stream ()
. map ( String :: toUpperCase )
. sorted (( a , b ) -> b . compareTo ( a ))
. forEach ( System . out :: println );
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Se pueden utilizar varias operaciones de coincidencia para comprobar si un determinado predicado coincide con la secuencia. Todas esas operaciones son terminales y devuelven un resultado booleano.
boolean anyStartsWithA =
stringCollection
. stream ()
. anyMatch (( s ) -> s . startsWith ( "a" ));
System . out . println ( anyStartsWithA ); // true
boolean allStartsWithA =
stringCollection
. stream ()
. allMatch (( s ) -> s . startsWith ( "a" ));
System . out . println ( allStartsWithA ); // false
boolean noneStartsWithZ =
stringCollection
. stream ()
. noneMatch (( s ) -> s . startsWith ( "z" ));
System . out . println ( noneStartsWithZ ); // true
Count es una operación de terminal que devuelve el número de elementos de la secuencia como un long
.
long startsWithB =
stringCollection
. stream ()
. filter (( s ) -> s . startsWith ( "b" ))
. count ();
System . out . println ( startsWithB ); // 3
Esta operación terminal realiza una reducción de los elementos del flujo con la función dada. El resultado es un Optional
que mantiene el valor reducido.
Optional < String > reduced =
stringCollection
. stream ()
. sorted ()
. reduce (( s1 , s2 ) -> s1 + "#" + s2 );
reduced . ifPresent ( System . out :: println );
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
Como se mencionó anteriormente, las transmisiones pueden ser secuenciales o paralelas. Las operaciones en flujos secuenciales se realizan en un solo subproceso, mientras que las operaciones en flujos paralelos se realizan simultáneamente en varios subprocesos.
El siguiente ejemplo demuestra lo fácil que es aumentar el rendimiento mediante el uso de transmisiones paralelas.
Primero creamos una gran lista de elementos únicos:
int max = 1000000 ;
List < String > values = new ArrayList <>( max );
for ( int i = 0 ; i < max ; i ++) {
UUID uuid = UUID . randomUUID ();
values . add ( uuid . toString ());
}
Ahora medimos el tiempo que lleva ordenar un flujo de esta colección.
long t0 = System . nanoTime ();
long count = values . stream (). sorted (). count ();
System . out . println ( count );
long t1 = System . nanoTime ();
long millis = TimeUnit . NANOSECONDS . toMillis ( t1 - t0 );
System . out . println ( String . format ( "sequential sort took: %d ms" , millis ));
// sequential sort took: 899 ms
long t0 = System . nanoTime ();
long count = values . parallelStream (). sorted (). count ();
System . out . println ( count );
long t1 = System . nanoTime ();
long millis = TimeUnit . NANOSECONDS . toMillis ( t1 - t0 );
System . out . println ( String . format ( "parallel sort took: %d ms" , millis ));
// parallel sort took: 472 ms
Como puede ver, ambos fragmentos de código son casi idénticos, pero la clasificación en paralelo es aproximadamente un 50% más rápida. Todo lo que tienes que hacer es cambiar stream()
a parallelStream()
.
Como ya se mencionó, los mapas no admiten transmisiones directamente. No hay ningún método stream()
disponible en la interfaz Map
en sí, sin embargo, puede crear flujos especializados sobre las claves, valores o entradas de un mapa a través de map.keySet().stream()
, map.values().stream()
y map.entrySet().stream()
.
Además, los mapas admiten varios métodos nuevos y útiles para realizar tareas comunes.
Map < Integer , String > map = new HashMap <>();
for ( int i = 0 ; i < 10 ; i ++) {
map . putIfAbsent ( i , "val" + i );
}
map . forEach (( id , val ) -> System . out . println ( val ));
El código anterior debería explicarse por sí mismo: putIfAbsent
nos impide escribir comprobaciones adicionales si son nulas; forEach
acepta que un consumidor realice operaciones para cada valor del mapa.
Este ejemplo muestra cómo calcular código en el mapa utilizando funciones:
map . computeIfPresent ( 3 , ( num , val ) -> val + num );
map . get ( 3 ); // val33
map . computeIfPresent ( 9 , ( num , val ) -> null );
map . containsKey ( 9 ); // false
map . computeIfAbsent ( 23 , num -> "val" + num );
map . containsKey ( 23 ); // true
map . computeIfAbsent ( 3 , num -> "bam" );
map . get ( 3 ); // val33
A continuación, aprenderemos cómo eliminar entradas para una clave determinada, solo si actualmente está asignada a un valor determinado:
map . remove ( 3 , "val3" );
map . get ( 3 ); // val33
map . remove ( 3 , "val33" );
map . get ( 3 ); // null
Otro método útil:
map . getOrDefault ( 42 , "not found" ); // not found
Fusionar entradas de un mapa es bastante fácil:
map . merge ( 9 , "val9" , ( value , newValue ) -> value . concat ( newValue ));
map . get ( 9 ); // val9
map . merge ( 9 , "concat" , ( value , newValue ) -> value . concat ( newValue ));
map . get ( 9 ); // val9concat
Fusionar coloca la clave/valor en el mapa si no existe ninguna entrada para la clave, o se llamará a la función de fusión para cambiar el valor existente.
Java 8 contiene una nueva API de fecha y hora en el paquete java.time
. La nueva API Date es comparable con la biblioteca Joda-Time, sin embargo, no es la misma. Los siguientes ejemplos cubren las partes más importantes de esta nueva API.
Reloj proporciona acceso a la fecha y hora actuales. Los relojes reconocen una zona horaria y pueden usarse en lugar de System.currentTimeMillis()
para recuperar la hora actual en milisegundos desde Unix EPOCH. Un punto instantáneo de este tipo en la línea de tiempo también está representado por la clase Instant
. Los instantáneos se pueden utilizar para crear objetos java.util.Date
heredados.
Clock clock = Clock . systemDefaultZone ();
long millis = clock . millis ();
Instant instant = clock . instant ();
Date legacyDate = Date . from ( instant ); // legacy java.util.Date
Las zonas horarias están representadas por un ZoneId
. Se puede acceder fácilmente a ellos mediante métodos estáticos de fábrica. Las zonas horarias definen las compensaciones que son importantes para convertir entre instantes y fechas y horas locales.
System . out . println ( ZoneId . getAvailableZoneIds ());
// prints all available timezone ids
ZoneId zone1 = ZoneId . of ( "Europe/Berlin" );
ZoneId zone2 = ZoneId . of ( "Brazil/East" );
System . out . println ( zone1 . getRules ());
System . out . println ( zone2 . getRules ());
// ZoneRules[currentStandardOffset=+01:00]
// ZoneRules[currentStandardOffset=-03:00]
LocalTime representa una hora sin zona horaria, por ejemplo, 22:00 o 17:30:15. El siguiente ejemplo crea dos horas locales para las zonas horarias definidas anteriormente. Luego comparamos ambos tiempos y calculamos la diferencia en horas y minutos entre ambos tiempos.
LocalTime now1 = LocalTime . now ( zone1 );
LocalTime now2 = LocalTime . now ( zone2 );
System . out . println ( now1 . isBefore ( now2 )); // false
long hoursBetween = ChronoUnit . HOURS . between ( now1 , now2 );
long minutesBetween = ChronoUnit . MINUTES . between ( now1 , now2 );
System . out . println ( hoursBetween ); // -3
System . out . println ( minutesBetween ); // -239
LocalTime viene con varios métodos de fábrica para simplificar la creación de nuevas instancias, incluido el análisis de cadenas de tiempo.
LocalTime late = LocalTime . of ( 23 , 59 , 59 );
System . out . println ( late ); // 23:59:59
DateTimeFormatter germanFormatter =
DateTimeFormatter
. ofLocalizedTime ( FormatStyle . SHORT )
. withLocale ( Locale . GERMAN );
LocalTime leetTime = LocalTime . parse ( "13:37" , germanFormatter );
System . out . println ( leetTime ); // 13:37
LocalDate representa una fecha distinta, por ejemplo, 2014-03-11. Es inmutable y funciona exactamente de forma análoga a LocalTime. El ejemplo demuestra cómo calcular nuevas fechas sumando o restando días, meses o años. Tenga en cuenta que cada manipulación devuelve una nueva instancia.
LocalDate today = LocalDate . now ();
LocalDate tomorrow = today . plus ( 1 , ChronoUnit . DAYS );
LocalDate yesterday = tomorrow . minusDays ( 2 );
LocalDate independenceDay = LocalDate . of ( 2014 , Month . JULY , 4 );
DayOfWeek dayOfWeek = independenceDay . getDayOfWeek ();
System . out . println ( dayOfWeek ); // FRIDAY
Analizar LocalDate a partir de una cadena es tan simple como analizar LocalTime:
DateTimeFormatter germanFormatter =
DateTimeFormatter
. ofLocalizedDate ( FormatStyle . MEDIUM )
. withLocale ( Locale . GERMAN );
LocalDate xmas = LocalDate . parse ( "24.12.2014" , germanFormatter );
System . out . println ( xmas ); // 2014-12-24
LocalDateTime representa una fecha y hora. Combina fecha y hora como se ve en las secciones anteriores en una sola instancia. LocalDateTime
es inmutable y funciona de manera similar a LocalTime y LocalDate. Podemos utilizar métodos para recuperar ciertos campos de una fecha y hora:
LocalDateTime sylvester = LocalDateTime . of ( 2014 , Month . DECEMBER , 31 , 23 , 59 , 59 );
DayOfWeek dayOfWeek = sylvester . getDayOfWeek ();
System . out . println ( dayOfWeek ); // WEDNESDAY
Month month = sylvester . getMonth ();
System . out . println ( month ); // DECEMBER
long minuteOfDay = sylvester . getLong ( ChronoField . MINUTE_OF_DAY );
System . out . println ( minuteOfDay ); // 1439
Con la información adicional de una zona horaria se puede convertir a un instante. Los instantes se pueden convertir fácilmente a fechas heredadas de tipo java.util.Date
.
Instant instant = sylvester
. atZone ( ZoneId . systemDefault ())
. toInstant ();
Date legacyDate = Date . from ( instant );
System . out . println ( legacyDate ); // Wed Dec 31 23:59:59 CET 2014
Formatear fechas y horas funciona igual que formatear fechas u horas. En lugar de utilizar formatos predefinidos, podemos crear formateadores a partir de patrones personalizados.
DateTimeFormatter formatter =
DateTimeFormatter
. ofPattern ( "MMM dd, yyyy - HH:mm" );
LocalDateTime parsed = LocalDateTime . parse ( "Nov 03, 2014 - 07:13" , formatter );
String string = formatter . format ( parsed );
System . out . println ( string ); // Nov 03, 2014 - 07:13
A diferencia de java.text.NumberFormat
el nuevo DateTimeFormatter
es inmutable y seguro para subprocesos .
Para obtener detalles sobre la sintaxis del patrón, lea aquí.
Las anotaciones en Java 8 son repetibles. Profundicemos directamente en un ejemplo para resolverlo.
Primero, definimos una anotación contenedora que contiene una matriz de las anotaciones reales:
@interface Hints {
Hint [] value ();
}
@ Repeatable ( Hints . class )
@interface Hint {
String value ();
}
Java 8 nos permite utilizar múltiples anotaciones del mismo tipo declarando la anotación @Repeatable
.
@ Hints ({ @ Hint ( "hint1" ), @ Hint ( "hint2" )})
class Person {}
@ Hint ( "hint1" )
@ Hint ( "hint2" )
class Person {}
Usando la variante 2, el compilador de Java configura implícitamente la anotación @Hints
bajo el capó. Esto es importante para leer la información de las anotaciones mediante la reflexión.
Hint hint = Person . class . getAnnotation ( Hint . class );
System . out . println ( hint ); // null
Hints hints1 = Person . class . getAnnotation ( Hints . class );
System . out . println ( hints1 . value (). length ); // 2
Hint [] hints2 = Person . class . getAnnotationsByType ( Hint . class );
System . out . println ( hints2 . length ); // 2
Aunque nunca declaramos la anotación @Hints
en la clase Person
, aún se puede leer a través de getAnnotation(Hints.class)
. Sin embargo, el método más conveniente es getAnnotationsByType
, que otorga acceso directo a todas las anotaciones @Hint
.
Además, el uso de anotaciones en Java 8 se amplía a dos nuevos objetivos:
@ Target ({ ElementType . TYPE_PARAMETER , ElementType . TYPE_USE })
@interface MyAnnotation {}
Mi guía de programación para Java 8 termina aquí. Si desea obtener más información sobre todas las nuevas clases y características de la API JDK 8, consulte mi Explorador de API JDK8. Le ayuda a descubrir todas las nuevas clases y gemas ocultas de JDK 8, como Arrays.parallelSort
, StampedLock
y CompletableFuture
, solo por nombrar algunas.
También publiqué un montón de artículos de seguimiento en mi blog que podrían resultarle interesantes:
Deberías seguirme en Twitter. ¡Gracias por leer!