Este artigo foi postado originalmente em meu blog.
Você também deve ler meu tutorial Java 11 (incluindo nova linguagem e recursos de API do Java 9, 10 e 11).
Bem-vindo à minha introdução ao Java 8. Este tutorial orienta você passo a passo por todos os novos recursos da linguagem. Apoiado em exemplos de código curtos e simples, você aprenderá como usar métodos de interface padrão, expressões lambda, referências de métodos e anotações repetíveis. No final do artigo você estará familiarizado com as alterações mais recentes da API, como streams, interfaces funcionais, extensões de mapas e a nova API de data. Sem paredes de texto, apenas um monte de trechos de código comentados. Aproveitar!
★★★ Gostou deste projeto? Deixe uma estrela, siga no Twitter ou faça uma doação para apoiar meu trabalho. Obrigado! ★★★
Java 8 nos permite adicionar implementações de métodos não abstratos a interfaces utilizando a palavra-chave default
. Esse recurso também é conhecido como métodos de extensão virtual.
Aqui está nosso primeiro exemplo:
interface Formula {
double calculate ( int a );
default double sqrt ( int a ) {
return Math . sqrt ( a );
}
}
Além do método abstrato calculate
a interface Formula
também define o método padrão sqrt
. As classes concretas só precisam implementar o método abstrato calculate
. O método padrão sqrt
pode ser usado imediatamente.
Formula formula = new Formula () {
@ Override
public double calculate ( int a ) {
return sqrt ( a * 100 );
}
};
formula . calculate ( 100 ); // 100.0
formula . sqrt ( 16 ); // 4.0
A fórmula é implementada como um objeto anônimo. O código é bastante detalhado: 6 linhas de código para um cálculo tão simples de sqrt(a * 100)
. Como veremos na próxima seção, há uma maneira muito mais agradável de implementar objetos de método único em Java 8.
Vamos começar com um exemplo simples de como classificar uma lista de strings em versões anteriores do 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 );
}
});
O método utilitário estático Collections.sort
aceita uma lista e um comparador para classificar os elementos de uma determinada lista. Freqüentemente, você cria comparadores anônimos e os passa para o método de classificação.
Em vez de criar objetos anônimos o dia todo, o Java 8 vem com uma sintaxe muito mais curta, expressões lambda :
Collections . sort ( names , ( String a , String b ) -> {
return b . compareTo ( a );
});
Como você pode ver, o código é muito mais curto e fácil de ler. Mas fica ainda mais curto:
Collections . sort ( names , ( String a , String b ) -> b . compareTo ( a ));
Para corpos de métodos de uma linha, você pode pular as chaves {}
e a palavra-chave return
. Mas fica ainda mais curto:
names . sort (( a , b ) -> b . compareTo ( a ));
A lista agora possui um método sort
. Além disso, o compilador Java conhece os tipos de parâmetros, portanto você também pode ignorá-los. Vamos nos aprofundar em como as expressões lambda podem ser usadas em estado selvagem.
Como as expressões lambda se encaixam no sistema de tipos Java? Cada lambda corresponde a um determinado tipo, especificado por uma interface. Uma chamada interface funcional deve conter exatamente uma declaração de método abstrato . Cada expressão lambda desse tipo corresponderá a este método abstrato. Como os métodos padrão não são abstratos, você pode adicionar métodos padrão à sua interface funcional.
Podemos usar interfaces arbitrárias como expressões lambda, desde que a interface contenha apenas um método abstrato. Para garantir que sua interface atenda aos requisitos, você deve adicionar a anotação @FunctionalInterface
. O compilador está ciente dessa anotação e gera um erro de compilador assim que você tenta adicionar uma segunda declaração de método abstrato à interface.
Exemplo:
@ 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
Tenha em mente que o código também é válido se a anotação @FunctionalInterface
for omitida.
O código de exemplo acima pode ser ainda mais simplificado utilizando referências de métodos estáticos:
Converter < String , Integer > converter = Integer :: valueOf ;
Integer converted = converter . convert ( "123" );
System . out . println ( converted ); // 123
Java 8 permite passar referências de métodos ou construtores por meio da palavra-chave ::
. O exemplo acima mostra como fazer referência a um método estático. Mas também podemos fazer referência a métodos de objeto:
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"
Vamos ver como a palavra-chave ::
funciona para construtores. Primeiro definimos uma classe de exemplo com diferentes construtores:
class Person {
String firstName ;
String lastName ;
Person () {}
Person ( String firstName , String lastName ) {
this . firstName = firstName ;
this . lastName = lastName ;
}
}
A seguir, especificamos uma interface de fábrica de pessoas a ser usada para criar novas pessoas:
interface PersonFactory < P extends Person > {
P create ( String firstName , String lastName );
}
Em vez de implementar a fábrica manualmente, colamos tudo por meio de referências de construtor:
PersonFactory < Person > personFactory = Person :: new ;
Person person = personFactory . create ( "Peter" , "Parker" );
Criamos uma referência ao construtor Person via Person::new
. O compilador Java escolhe automaticamente o construtor correto combinando a assinatura de PersonFactory.create
.
Acessar variáveis de escopo externo a partir de expressões lambda é muito semelhante a objetos anônimos. Você pode acessar variáveis finais do escopo externo local, bem como campos de instância e variáveis estáticas.
Podemos ler variáveis locais finais do escopo externo das expressões lambda:
final int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
stringConverter . convert ( 2 ); // 3
Mas diferente dos objetos anônimos, a variável num
não precisa ser declarada final. Este código também é válido:
int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
stringConverter . convert ( 2 ); // 3
No entanto, num
deve ser implicitamente final para que o código seja compilado. O código a seguir não compila:
int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
num = 3 ;
Escrever em num
a partir da expressão lambda também é proibido.
Em contraste com as variáveis locais, temos acesso de leitura e gravação a campos de instância e variáveis estáticas de dentro de expressões lambda. Esse comportamento é bem conhecido em 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 );
};
}
}
Lembra do exemplo de fórmula da primeira seção? Interface Formula
define um método padrão sqrt
que pode ser acessado a partir de cada instância de fórmula, incluindo objetos anônimos. Isso não funciona com expressões lambda.
Os métodos padrão não podem ser acessados nas expressões lambda. O código a seguir não compila:
Formula formula = ( a ) -> sqrt ( a * 100 );
A API JDK 1.8 contém muitas interfaces funcionais integradas. Alguns deles são bem conhecidos em versões mais antigas do Java, como Comparator
ou Runnable
. Essas interfaces existentes são estendidas para habilitar o suporte Lambda por meio da anotação @FunctionalInterface
.
Mas a API Java 8 também está repleta de novas interfaces funcionais para facilitar sua vida. Algumas dessas novas interfaces são bem conhecidas na biblioteca Google Guava. Mesmo se você estiver familiarizado com esta biblioteca, você deve ficar atento em como essas interfaces são estendidas por algumas extensões de métodos úteis.
Predicados são funções com valor booleano de um argumento. A interface contém vários métodos padrão para compor predicados para termos lógicos complexos (e, ou, 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 ();
As funções aceitam um argumento e produzem um resultado. Os métodos padrão podem ser usados para encadear várias funções (compor e andThen).
Function < String , Integer > toInteger = Integer :: valueOf ;
Function < String , String > backToString = toInteger . andThen ( String :: valueOf );
backToString . apply ( "123" ); // "123"
Os fornecedores produzem um resultado de um determinado tipo genérico. Ao contrário das Funções, os Fornecedores não aceitam argumentos.
Supplier < Person > personSupplier = Person :: new ;
personSupplier . get (); // new Person
Os consumidores representam operações a serem executadas em um único argumento de entrada.
Consumer < Person > greeter = ( p ) -> System . out . println ( "Hello, " + p . firstName );
greeter . accept ( new Person ( "Luke" , "Skywalker" ));
Os comparadores são bem conhecidos em versões mais antigas do Java. Java 8 adiciona vários métodos padrão à interface.
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
Opcionais não são interfaces funcionais, mas utilitários bacanas para evitar NullPointerException
. É um conceito importante para a próxima seção, então vamos dar uma olhada rápida em como funcionam os Opcionais.
Opcional é um contêiner simples para um valor que pode ser nulo ou não nulo. Pense em um método que pode retornar um resultado não nulo, mas às vezes não retornar nada. Em vez de retornar null
você retorna um Optional
em 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"
Um java.util.Stream
representa uma sequência de elementos nos quais uma ou mais operações podem ser executadas. As operações de fluxo são intermediárias ou terminais . Embora as operações de terminal retornem um resultado de um determinado tipo, as operações intermediárias retornam o próprio fluxo para que você possa encadear várias chamadas de método seguidas. Os fluxos são criados em uma fonte, por exemplo, um java.util.Collection
como listas ou conjuntos (mapas não são suportados). As operações de fluxo podem ser executadas sequencialmente ou paralelamente.
Streams são extremamente poderosos, então escrevi um tutorial separado sobre Java 8 Streams. Você também deve verificar Sequency como uma biblioteca semelhante para a web.
Vejamos primeiro como funcionam os fluxos sequenciais. Primeiro criamos uma fonte de amostra na forma de uma lista de strings:
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" );
As coleções em Java 8 são estendidas para que você possa simplesmente criar fluxos chamando Collection.stream()
ou Collection.parallelStream()
. As seções a seguir explicam as operações de fluxo mais comuns.
Filter aceita um predicado para filtrar todos os elementos do fluxo. Esta operação é intermediária , o que nos permite chamar outra operação de fluxo ( forEach
) no resultado. ForEach aceita que um consumidor seja executado para cada elemento no fluxo filtrado. ForEach é uma operação de terminal. É void
, portanto não podemos chamar outra operação de stream.
stringCollection
. stream ()
. filter (( s ) -> s . startsWith ( "a" ))
. forEach ( System . out :: println );
// "aaa2", "aaa1"
Classificada é uma operação intermediária que retorna uma visualização classificada do fluxo. Os elementos são classificados em ordem natural, a menos que você passe um Comparator
personalizado.
stringCollection
. stream ()
. sorted ()
. filter (( s ) -> s . startsWith ( "a" ))
. forEach ( System . out :: println );
// "aaa1", "aaa2"
Lembre-se de que sorted
cria apenas uma visualização classificada do fluxo, sem manipular a ordem da coleção apoiada. A ordem de stringCollection
permanece intacta:
System . out . println ( stringCollection );
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
O map
de operação intermediária converte cada elemento em outro objeto por meio da função fornecida. O exemplo a seguir converte cada string em uma string maiúscula. Mas você também pode usar map
para transformar cada objeto em outro tipo. O tipo genérico do fluxo resultante depende do tipo genérico da função que você passa para map
.
stringCollection
. stream ()
. map ( String :: toUpperCase )
. sorted (( a , b ) -> b . compareTo ( a ))
. forEach ( System . out :: println );
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Várias operações de correspondência podem ser usadas para verificar se um determinado predicado corresponde ao fluxo. Todas essas operações são terminais e retornam um 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 é uma operação de terminal que retorna o número de elementos no fluxo como um long
.
long startsWithB =
stringCollection
. stream ()
. filter (( s ) -> s . startsWith ( "b" ))
. count ();
System . out . println ( startsWithB ); // 3
Esta operação terminal realiza uma redução nos elementos do fluxo com a função dada. O resultado é um Optional
contendo o valor reduzido.
Optional < String > reduced =
stringCollection
. stream ()
. sorted ()
. reduce (( s1 , s2 ) -> s1 + "#" + s2 );
reduced . ifPresent ( System . out :: println );
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
Conforme mencionado acima, os fluxos podem ser sequenciais ou paralelos. As operações em fluxos sequenciais são executadas em um único thread, enquanto as operações em fluxos paralelos são executadas simultaneamente em vários threads.
O exemplo a seguir demonstra como é fácil aumentar o desempenho usando fluxos paralelos.
Primeiro criamos uma grande lista de elementos exclusivos:
int max = 1000000 ;
List < String > values = new ArrayList <>( max );
for ( int i = 0 ; i < max ; i ++) {
UUID uuid = UUID . randomUUID ();
values . add ( uuid . toString ());
}
Agora medimos o tempo que leva para classificar um fluxo desta coleção.
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 você pode ver, os dois trechos de código são quase idênticos, mas a classificação paralela é aproximadamente 50% mais rápida. Tudo que você precisa fazer é mudar stream()
para parallelStream()
.
Como já mencionado, os mapas não suportam fluxos diretamente. Não há nenhum método stream()
disponível na própria interface Map
, no entanto, você pode criar fluxos especializados sobre as chaves, valores ou entradas de um mapa via map.keySet().stream()
, map.values().stream()
e map.entrySet().stream()
.
Além disso, os mapas suportam vários métodos novos e úteis para realizar tarefas comuns.
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 ));
O código acima deve ser autoexplicativo: putIfAbsent
nos impede de escrever verificações if null adicionais; forEach
aceita um consumidor para realizar operações para cada valor do mapa.
Este exemplo mostra como calcular código no mapa utilizando funções:
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 seguir, aprenderemos como remover entradas de uma determinada chave, apenas se ela estiver atualmente mapeada para um determinado valor:
map . remove ( 3 , "val3" );
map . get ( 3 ); // val33
map . remove ( 3 , "val33" );
map . get ( 3 ); // null
Outro método útil:
map . getOrDefault ( 42 , "not found" ); // not found
Mesclar entradas de um mapa é 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
Merge coloca a chave/valor no mapa se não existir nenhuma entrada para a chave, ou a função de mesclagem será chamada para alterar o valor existente.
Java 8 contém uma nova API de data e hora no pacote java.time
. A nova API Date é comparável à biblioteca Joda-Time, porém não é a mesma. Os exemplos a seguir cobrem as partes mais importantes desta nova API.
O relógio fornece acesso à data e hora atuais. Os relógios reconhecem um fuso horário e podem ser usados em vez de System.currentTimeMillis()
para recuperar a hora atual em milissegundos desde o Unix EPOCH. Esse ponto instantâneo na linha do tempo também é representado pela classe Instant
. Instantes podem ser usados para criar objetos java.util.Date
legados.
Clock clock = Clock . systemDefaultZone ();
long millis = clock . millis ();
Instant instant = clock . instant ();
Date legacyDate = Date . from ( instant ); // legacy java.util.Date
Os fusos horários são representados por um ZoneId
. Eles podem ser facilmente acessados por meio de métodos estáticos de fábrica. Os fusos horários definem os deslocamentos que são importantes para converter entre instantes e datas e horas locais.
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 um horário sem fuso horário, por exemplo, 22h ou 17h30min15s. O exemplo a seguir cria dois horários locais para os fusos horários definidos acima. Em seguida, comparamos os dois horários e calculamos a diferença em horas e minutos entre os dois horários.
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 vem com vários métodos de fábrica para simplificar a criação de novas instâncias, incluindo análise de strings de tempo.
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 uma data distinta, por exemplo, 11/03/2014. É imutável e funciona exatamente de forma análoga ao LocalTime. O exemplo demonstra como calcular novas datas adicionando ou subtraindo dias, meses ou anos. Lembre-se de que cada manipulação retorna uma nova instância.
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
Analisar um LocalDate a partir de uma string é tão simples quanto analisar um 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 uma data e hora. Ele combina data e hora conforme visto nas seções acima em uma instância. LocalDateTime
é imutável e funciona de forma semelhante a LocalTime e LocalDate. Podemos utilizar métodos para recuperar determinados campos de uma data e 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
Com a informação adicional de um fuso horário pode ser convertido para um instante. Os instantes podem ser facilmente convertidos em datas herdadas do 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
A formatação de data e hora funciona exatamente como a formatação de datas ou horas. Em vez de usar formatos predefinidos podemos criar formatadores a partir de padrões 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
Ao contrário de java.text.NumberFormat
o novo DateTimeFormatter
é imutável e seguro para threads .
Para obter detalhes sobre a sintaxe do padrão, leia aqui.
As anotações em Java 8 são repetíveis. Vamos mergulhar diretamente em um exemplo para descobrir isso.
Primeiro, definimos uma anotação wrapper que contém uma matriz das anotações reais:
@interface Hints {
Hint [] value ();
}
@ Repeatable ( Hints . class )
@interface Hint {
String value ();
}
Java 8 nos permite usar múltiplas anotações do mesmo tipo declarando a anotação @Repeatable
.
@ Hints ({ @ Hint ( "hint1" ), @ Hint ( "hint2" )})
class Person {}
@ Hint ( "hint1" )
@ Hint ( "hint2" )
class Person {}
Usando a variante 2, o compilador Java configura implicitamente a anotação @Hints
nos bastidores. Isso é importante para ler informações de anotação por meio de reflexão.
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
Embora nunca tenhamos declarado a anotação @Hints
na classe Person
, ela ainda pode ser lida por meio de getAnnotation(Hints.class)
. No entanto, o método mais conveniente é getAnnotationsByType
que concede acesso direto a todas as anotações @Hint
anotadas.
Além disso, o uso de anotações no Java 8 foi expandido para dois novos alvos:
@ Target ({ ElementType . TYPE_PARAMETER , ElementType . TYPE_USE })
@interface MyAnnotation {}
Meu guia de programação para Java 8 termina aqui. Se você quiser saber mais sobre todas as novas classes e recursos da API JDK 8, confira meu JDK8 API Explorer. Ele ajuda você a descobrir todas as novas classes e joias ocultas do JDK 8, como Arrays.parallelSort
, StampedLock
e CompletableFuture
- só para citar alguns.
Também publiquei vários artigos de acompanhamento em meu blog que podem ser interessantes para você:
Você deveria me seguir no Twitter. Obrigado por ler!