Эта статья изначально была опубликована в моем блоге.
Вам также следует прочитать мое руководство по Java 11 (включая новые функции языка и API из Java 9, 10 и 11).
Добро пожаловать в мое введение в Java 8. Это руководство шаг за шагом проведет вас через все новые возможности языка. С помощью коротких и простых примеров кода вы узнаете, как использовать методы интерфейса по умолчанию, лямбда-выражения, ссылки на методы и повторяемые аннотации. В конце статьи вы познакомитесь с самыми последними изменениями API, такими как потоки, функциональные интерфейсы, расширения карт и новый API даты. Никаких стен текста, только куча прокомментированных фрагментов кода. Наслаждаться!
★★★ Нравится этот проект? Оставьте звезду, подпишитесь на Twitter или сделайте пожертвование в поддержку моей работы. Спасибо! ★★★
Java 8 позволяет нам добавлять в интерфейсы реализации неабстрактных методов, используя ключевое слово default
. Эта функция также известна как методы виртуального расширения.
Вот наш первый пример:
interface Formula {
double calculate ( int a );
default double sqrt ( int a ) {
return Math . sqrt ( a );
}
}
Помимо абстрактного метода calculate
интерфейс Formula
также определяет метод по умолчанию sqrt
. Конкретные классы должны реализовать только абстрактный метод calculate
. Метод по умолчанию sqrt
можно использовать «из коробки».
Formula formula = new Formula () {
@ Override
public double calculate ( int a ) {
return sqrt ( a * 100 );
}
};
formula . calculate ( 100 ); // 100.0
formula . sqrt ( 16 ); // 4.0
Формула реализована как анонимный объект. Код довольно подробный: 6 строк кода для такого простого расчета sqrt(a * 100)
. Как мы увидим в следующем разделе, в Java 8 существует гораздо более удобный способ реализации объектов с одним методом.
Начнем с простого примера того, как сортировать список строк в предыдущих версиях 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 );
}
});
Статический служебный метод Collections.sort
принимает список и компаратор для сортировки элементов данного списка. Вы часто создаете анонимные компараторы и передаете их методу сортировки.
Вместо того, чтобы целый день создавать анонимные объекты, Java 8 имеет гораздо более короткий синтаксис — лямбда-выражения :
Collections . sort ( names , ( String a , String b ) -> {
return b . compareTo ( a );
});
Как видите, код намного короче и его легче читать. Но оно становится еще короче:
Collections . sort ( names , ( String a , String b ) -> b . compareTo ( a ));
Для тела однострочного метода вы можете пропустить как фигурные скобки {}
, так и ключевое слово return
. Но оно становится еще короче:
names . sort (( a , b ) -> b . compareTo ( a ));
В списке теперь есть метод sort
. Кроме того, компилятор Java знает о типах параметров, поэтому вы также можете их пропустить. Давайте углубимся в то, как можно использовать лямбда-выражения в реальных условиях.
Как лямбда-выражения вписываются в систему типов Java? Каждая лямбда соответствует данному типу, указанному в интерфейсе. Так называемый функциональный интерфейс должен содержать ровно одно объявление абстрактного метода . Каждое лямбда-выражение этого типа будет сопоставлено с этим абстрактным методом. Поскольку методы по умолчанию не являются абстрактными, вы можете добавлять методы по умолчанию в свой функциональный интерфейс.
Мы можем использовать произвольные интерфейсы в качестве лямбда-выражений, если интерфейс содержит только один абстрактный метод. Чтобы убедиться, что ваш интерфейс соответствует требованиям, вам следует добавить аннотацию @FunctionalInterface
. Компилятор знает об этой аннотации и выдает ошибку компилятора, как только вы пытаетесь добавить в интерфейс второе объявление абстрактного метода.
Пример:
@ 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
Имейте в виду, что код действителен и в том случае, если аннотация @FunctionalInterface
опущена.
Приведенный выше пример кода можно еще больше упростить, используя ссылки на статические методы:
Converter < String , Integer > converter = Integer :: valueOf ;
Integer converted = converter . convert ( "123" );
System . out . println ( converted ); // 123
Java 8 позволяет передавать ссылки на методы или конструкторы через ключевое слово ::
. В приведенном выше примере показано, как ссылаться на статический метод. Но мы также можем ссылаться на методы объекта:
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"
Давайте посмотрим, как ключевое слово ::
работает для конструкторов. Сначала мы определяем пример класса с разными конструкторами:
class Person {
String firstName ;
String lastName ;
Person () {}
Person ( String firstName , String lastName ) {
this . firstName = firstName ;
this . lastName = lastName ;
}
}
Далее мы указываем интерфейс фабрики людей, который будет использоваться для создания новых людей:
interface PersonFactory < P extends Person > {
P create ( String firstName , String lastName );
}
Вместо того, чтобы реализовывать фабрику вручную, мы склеиваем все вместе с помощью ссылок на конструктор:
PersonFactory < Person > personFactory = Person :: new ;
Person person = personFactory . create ( "Peter" , "Parker" );
Мы создаем ссылку на конструктор Person через Person::new
. Компилятор Java автоматически выбирает правильный конструктор, сопоставляя подпись PersonFactory.create
.
Доступ к переменным внешней области видимости из лямбда-выражений очень похож на анонимные объекты. Вы можете получить доступ к конечным переменным из локальной внешней области, а также к полям экземпляра и статическим переменным.
Мы можем прочитать конечные локальные переменные из внешней области лямбда-выражений:
final int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
stringConverter . convert ( 2 ); // 3
Но в отличие от анонимных объектов переменную num
не обязательно объявлять окончательной. Этот код также действителен:
int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
stringConverter . convert ( 2 ); // 3
Однако для компиляции кода num
должно быть неявно конечным. Следующий код не компилируется:
int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
num = 3 ;
Запись в num
из лямбда-выражения также запрещена.
В отличие от локальных переменных, у нас есть доступ как для чтения, так и для записи к полям экземпляра и статическим переменным из лямбда-выражений. Такое поведение хорошо известно по анонимным объектам.
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 );
};
}
}
Помните пример формулы из первого раздела? Formula
интерфейса определяет метод sqrt
по умолчанию, доступ к которому можно получить из каждого экземпляра формулы, включая анонимные объекты. Это не работает с лямбда-выражениями.
Доступ к методам по умолчанию невозможен из лямбда-выражений. Следующий код не компилируется:
Formula formula = ( a ) -> sqrt ( a * 100 );
API JDK 1.8 содержит множество встроенных функциональных интерфейсов. Некоторые из них хорошо известны из старых версий Java, таких как Comparator
или Runnable
. Эти существующие интерфейсы расширены для включения поддержки Lambda через аннотацию @FunctionalInterface
.
Но API Java 8 также полон новых функциональных интерфейсов, которые сделают вашу жизнь проще. Некоторые из этих новых интерфейсов хорошо известны из библиотеки Google Guava. Даже если вы знакомы с этой библиотекой, вам следует внимательно следить за тем, как эти интерфейсы расширяются некоторыми полезными расширениями методов.
Предикаты — это логические функции одного аргумента. Интерфейс содержит различные методы по умолчанию для составления предикатов в сложные логические термины (и/или отрицания).
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 ();
Функции принимают один аргумент и выдают результат. Методы по умолчанию можно использовать для объединения нескольких функций (compose и Then).
Function < String , Integer > toInteger = Integer :: valueOf ;
Function < String , String > backToString = toInteger . andThen ( String :: valueOf );
backToString . apply ( "123" ); // "123"
Поставщики выдают результат заданного общего типа. В отличие от функций, поставщики не принимают аргументы.
Supplier < Person > personSupplier = Person :: new ;
personSupplier . get (); // new Person
Потребители представляют собой операции, которые необходимо выполнить с одним входным аргументом.
Consumer < Person > greeter = ( p ) -> System . out . println ( "Hello, " + p . firstName );
greeter . accept ( new Person ( "Luke" , "Skywalker" ));
Компараторы хорошо известны из старых версий Java. Java 8 добавляет в интерфейс различные методы по умолчанию.
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
Опциональные возможности — это не функциональные интерфейсы, а изящные утилиты для предотвращения NullPointerException
. Это важная концепция для следующего раздела, поэтому давайте кратко рассмотрим, как работают опции.
Необязательный — это простой контейнер для значения, которое может быть нулевым или ненулевым. Подумайте о методе, который может возвращать ненулевой результат, но иногда ничего не возвращает. Вместо возврата значения null
вы возвращаете Optional
в 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"
java.util.Stream
представляет собой последовательность элементов, над которыми можно выполнить одну или несколько операций. Потоковые операции бывают промежуточными или терминальными . В то время как терминальные операции возвращают результат определенного типа, промежуточные операции возвращают сам поток, поэтому вы можете связать несколько вызовов методов подряд. Потоки создаются в источнике, например, в java.util.Collection
например списках или наборах (карты не поддерживаются). Потоковые операции могут выполняться последовательно или параллельно.
Потоки чрезвычайно мощны, поэтому я написал отдельное руководство по потокам Java 8. Вам также следует проверить Sequency как аналогичную библиотеку для Интернета.
Давайте сначала посмотрим, как работают последовательные потоки. Сначала мы создаем образец источника в виде списка строк:
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" );
Коллекции в Java 8 расширены, поэтому вы можете просто создавать потоки, вызывая Collection.stream()
или Collection.parallelStream()
. В следующих разделах описаны наиболее распространенные потоковые операции.
Filter принимает предикат для фильтрации всех элементов потока. Эта операция является промежуточной и позволяет нам вызвать другую потоковую операцию ( forEach
) для результата. ForEach принимает выполнение потребителя для каждого элемента в фильтрованном потоке. ForEach — это терминальная операция. Это void
, поэтому мы не можем вызвать другую потоковую операцию.
stringCollection
. stream ()
. filter (( s ) -> s . startsWith ( "a" ))
. forEach ( System . out :: println );
// "aaa2", "aaa1"
Sorted — это промежуточная операция, которая возвращает отсортированное представление потока. Элементы сортируются в естественном порядке, если вы не передадите собственный Comparator
.
stringCollection
. stream ()
. sorted ()
. filter (( s ) -> s . startsWith ( "a" ))
. forEach ( System . out :: println );
// "aaa1", "aaa2"
Имейте в виду, что sorted
создает только отсортированное представление потока без изменения порядка поддерживаемой коллекции. Порядок stringCollection
не изменен:
System . out . println ( stringCollection );
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
map
промежуточных операций преобразует каждый элемент в другой объект с помощью заданной функции. В следующем примере каждая строка преобразуется в строку верхнего регистра. Но вы также можете использовать map
для преобразования каждого объекта в другой тип. Общий тип результирующего потока зависит от универсального типа функции, которую вы передаете в map
.
stringCollection
. stream ()
. map ( String :: toUpperCase )
. sorted (( a , b ) -> b . compareTo ( a ))
. forEach ( System . out :: println );
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Чтобы проверить, соответствует ли определенный предикат потоку, можно использовать различные операции сопоставления. Все эти операции являются терминальными и возвращают логический результат.
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 — это терминальная операция, возвращающая количество элементов в потоке в формате long
.
long startsWithB =
stringCollection
. stream ()
. filter (( s ) -> s . startsWith ( "b" ))
. count ();
System . out . println ( startsWithB ); // 3
Эта терминальная операция выполняет сокращение элементов потока с заданной функцией. Результатом является Optional
параметр, содержащий уменьшенное значение.
Optional < String > reduced =
stringCollection
. stream ()
. sorted ()
. reduce (( s1 , s2 ) -> s1 + "#" + s2 );
reduced . ifPresent ( System . out :: println );
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
Как упоминалось выше, потоки могут быть как последовательными, так и параллельными. Операции с последовательными потоками выполняются в одном потоке, а операции с параллельными потоками выполняются одновременно в нескольких потоках.
Следующий пример демонстрирует, насколько легко повысить производительность с помощью параллельных потоков.
Сначала мы создаем большой список уникальных элементов:
int max = 1000000 ;
List < String > values = new ArrayList <>( max );
for ( int i = 0 ; i < max ; i ++) {
UUID uuid = UUID . randomUUID ();
values . add ( uuid . toString ());
}
Теперь мы измеряем время, необходимое для сортировки потока этой коллекции.
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
Как видите, оба фрагмента кода почти идентичны, но параллельная сортировка выполняется примерно на 50 % быстрее. Все, что вам нужно сделать, это изменить stream()
на parallelStream()
.
Как уже упоминалось, карты напрямую не поддерживают потоки. В самом интерфейсе Map
нет stream()
, однако вы можете создавать специализированные потоки для ключей, значений или записей карты с помощью map.keySet().stream()
, map.values().stream()
и map.entrySet().stream()
.
Кроме того, карты поддерживают различные новые и полезные методы выполнения обычных задач.
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 ));
Приведенный выше код не требует пояснений: putIfAbsent
не позволяет нам писать дополнительные проверки if null; forEach
принимает потребителя для выполнения операций для каждого значения карты.
В этом примере показано, как вычислить код на карте с помощью функций:
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
Далее мы узнаем, как удалять записи для данного ключа, только если он в данный момент сопоставлен с данным значением:
map . remove ( 3 , "val3" );
map . get ( 3 ); // val33
map . remove ( 3 , "val33" );
map . get ( 3 ); // null
Еще один полезный метод:
map . getOrDefault ( 42 , "not found" ); // not found
Объединить записи карты довольно просто:
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
Слияние либо помещает ключ/значение на карту, если запись для ключа не существует, либо функция слияния будет вызвана для изменения существующего значения.
Java 8 содержит совершенно новый API даты и времени в пакете java.time
. Новый Date API сравним с библиотекой Joda-Time, однако это не то же самое. Следующие примеры охватывают наиболее важные части этого нового API.
Часы обеспечивают доступ к текущей дате и времени. Часы знают часовой пояс и могут использоваться вместо System.currentTimeMillis()
для получения текущего времени в миллисекундах, начиная с Unix EPOCH. Такая мгновенная точка на временной шкале также представлена классом Instant
. Мгновения можно использовать для создания устаревших объектов java.util.Date
.
Clock clock = Clock . systemDefaultZone ();
long millis = clock . millis ();
Instant instant = clock . instant ();
Date legacyDate = Date . from ( instant ); // legacy java.util.Date
Часовые пояса представлены ZoneId
. Доступ к ним можно легко получить через статические фабричные методы. Часовые пояса определяют смещения, которые важны для преобразования мгновенных и местных дат и времени.
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 представляет время без часового пояса, например 22:00 или 17:30:15. В следующем примере создаются два локальных времени для часовых поясов, определенных выше. Затем мы сравниваем оба времени и вычисляем разницу в часах и минутах между обоими значениями времени.
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 поставляется с различными фабричными методами, упрощающими создание новых экземпляров, включая анализ строк времени.
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 представляет собой отдельную дату, например 11 марта 2014 г. Он неизменен и работает точно так же, как LocalTime. В примере показано, как рассчитать новые даты путем добавления или вычитания дней, месяцев или лет. Имейте в виду, что каждая манипуляция возвращает новый экземпляр.
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
Анализ LocalDate из строки так же прост, как анализ 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 представляет дату и время. Он объединяет дату и время, как показано в приведенных выше разделах, в один экземпляр. LocalDateTime
является неизменяемым и работает аналогично LocalTime и LocalDate. Мы можем использовать методы для получения определенных полей из даты и времени:
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
При наличии дополнительной информации о часовом поясе его можно преобразовать в мгновенное. Мгновенные значения можно легко преобразовать в устаревшие даты типа 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
Форматирование даты и времени работает так же, как форматирование даты или времени. Вместо использования предопределенных форматов мы можем создавать средства форматирования на основе пользовательских шаблонов.
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
В отличие от java.text.NumberFormat
новый DateTimeFormatter
является неизменяемым и потокобезопасным .
Подробности о синтаксисе шаблона читайте здесь.
Аннотации в Java 8 повторяются. Давайте углубимся непосредственно в пример, чтобы понять это.
Сначала мы определяем аннотацию-оболочку, которая содержит массив фактических аннотаций:
@interface Hints {
Hint [] value ();
}
@ Repeatable ( Hints . class )
@interface Hint {
String value ();
}
Java 8 позволяет нам использовать несколько аннотаций одного и того же типа, объявляя аннотацию @Repeatable
.
@ Hints ({ @ Hint ( "hint1" ), @ Hint ( "hint2" )})
class Person {}
@ Hint ( "hint1" )
@ Hint ( "hint2" )
class Person {}
Используя вариант 2, компилятор Java неявно устанавливает аннотацию @Hints
под капотом. Это важно для чтения информации аннотации посредством отражения.
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
Хотя мы никогда не объявляли аннотацию @Hints
в классе Person
, ее все равно можно прочитать с помощью getAnnotation(Hints.class)
. Однако более удобным методом является getAnnotationsByType
, который предоставляет прямой доступ ко всем аннотированным аннотациям @Hint
.
Кроме того, использование аннотаций в Java 8 расширено до двух новых целей:
@ Target ({ ElementType . TYPE_PARAMETER , ElementType . TYPE_USE })
@interface MyAnnotation {}
На этом мое руководство по программированию на Java 8 заканчивается. Если вы хотите узнать больше обо всех новых классах и функциях API JDK 8, ознакомьтесь с моим обозревателем API JDK8. Он поможет вам разобраться во всех новых классах и скрытых жемчужинах JDK 8, таких как Arrays.parallelSort
, StampedLock
и CompletableFuture
— и это лишь некоторые из них.
Я также опубликовал в своем блоге несколько последующих статей, которые могут быть вам интересны:
Вам следует подписаться на меня в Твиттере. Спасибо за чтение!