Перейти к содержимому

Java 8 что нового

  • автор:

Новое в Java 8

Добро пожаловать в ведение по Java 8. Этот материал шаг за шагом познакомит вас со всеми новыми фичами языка. Вы научитесь использовать методы интерфейсов по умолчанию (default interface methods), лямбда-выражения (lambda expressions), ссылки на методы (method references) и повторяемые аннотации (repeatable annotations). Все это будет сопровождаться короткими и простыми примерами кода. В конце статьи вы познакомитесь с наиболее свежими изменениями в API, касающихся потоков, функциональных интерфейсов, расширений для ассоциативных массивов, а также с изменениями в API работы с датами.

Методы интерфейсов по умолчанию

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 

formula реализован как анонимный объект. Этот код довольно избыточен: целых 6 строчек для такого простого вычисления как sqrt(a * 100) . В следующем разделе мы увидим, что в Java 8 есть куда более изящный способ реализации объектов с одним методом.

Лямбда-выражения

Давайте начнем с простого примера: сортировка массива строк в предыдущих версиях языка.

List names = Arrays.asList("peter", "anna", "mike", "xenia"); Collections.sort(names, new Comparator() < @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 . Так еще короче:

Collections.sort(names, (a, b) -> b.compareTo(a)); 

Компилятору известны типы параметров, поэтому их можно тоже опустить. Давайте посмотрим, как еще могут использовать лямбда-выражения.

Функциональные интерфейсы

Как лямбда-выражения соответствуют системе типов языка Java? Каждой лямбде соответствует тип, представленный интерфейсом. Так называемый функциональный интерфейс должен содержать ровно один абстрактный метод. Каждое лямбда-выражение этого типа будет сопоставлено объявленному методу. Также, поскольку методы по умолчанию не являются абстрактными, вы можете добавлять в функциональный интерфейс сколько угодно таких методов.

Мы можем использовать какие угодно интерфейсы для лямбда-выражений, содержащие ровно один абстрактный метод. Для того, чтобы гарантировать, что ваш интерфейс отвечает этому требованию, используется аннотация @FunctionalInterface . Компилятор осведомлен об этой аннотации, и выдаст ошибку компиляции, если вы добавите второй абстрактный метод в функциональный интерфейс.

@FunctionalInterface interface Converter

Converter converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123 

Имейте в виду, что этот код останется корректным даже если убрать аннотацию @FunctionalInterface .

Ссылки на методы и конструкторы

Предыдущий пример можно упростить, если использовать статические ссылки на методы:

Converter 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 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

Теперь вместо реализации интерфейса мы соединяем все вместе при помощи ссылки на конструктор:

PersonFactory personFactory = Person::new; Person person = personFactory.create("Peter", "Parker"); 

Мы создаем ссылку на конструктор с помощью Person::new . Компилятор автоматически выбирает подходящий конструктор, сигнатура которого совпадает с сигнатурой PersonFactory.create .

Области действия лямбд

Доступ к переменным внешней области действия из лямбда-выражения очень схож к доступу из анонимных объектов. Вы можете ссылаться на переменные, объявленные как final , на экземплярные поля класса и статические переменные.

final int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 

Но в отличии от анонимных объектов, переменная num не обязательно должна быть объявлена как final . Такой код тоже сработает:

int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 

Однако переменная num должна все равно оставаться неизменяемой. Следующий код не скомпилируется:

int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); num = 3; 

Запись в переменную num в пределах лямбда-выражения также запрещена.

Доступ к полям и статическим переменным

В отличии от локальных переменных, мы можем записывать значения в экземплярные поля класса и статические переменные внутри лямбда-выражений. Это поведение хорошо знакомо по анонимным объектам.

class Lambda4 < static int outerStaticNum; int outerNum; void testScopes() < ConverterstringConverter1 = (from) -> < outerNum = 23; return String.valueOf(from); >; Converter stringConverter2 = (from) -> < outerStaticNum = 72; return String.valueOf(from); >; > > 
Доступ к методам интерфейсов по умолчанию

Помните пример с формулой из первого раздела? В интерфейсе Formula определен метод по умолчанию sqrt , который доступен из каждой имплементации интерфейса, включая анонимные объекты. Однако это не сработает в лямбда-выражениях.

Внутри лямбда-выражений запрещено обращаться к методам по умолчанию. Следующий код не скомпилируется:

Formula formula = (a) -> sqrt( a * 100); 
Встроенные функциональные интерфейсы

JDK 1.8 содержит множество встроенных функциональных интерфейсов. Некоторые из них хорошо известны по предыдущим версиям языка, например, Comparator или Runnable . Все эти интерфейсы были поддержаны в лямбдах добавлением аннотации @FunctionalInterface .

Однако в Java 8 также появилось много новых функциональных интерфейсов, которые призваны облегчить вам жизнь. Некоторые интерфейсы хорошо известны по библиотеке Google Guava. Даже если вы незнакомы с этой библиотекой, вам стоить взглянуть, как эти интерфейсы были дополнены некоторыми полезными методами расширений.

Предикаты

Предикаты — это функции, принимающие один аргумент, и возвращающие значение типа boolean. Интерфейс содержит различные методы по умолчанию, позволяющие строить сложные условия ( and , or , negate ).

Predicate predicate = (s) -> s.length() > 0; predicate.test("foo"); // true predicate.negate().test("foo"); // false Predicate nonNull = Objects::nonNull; Predicate isNull = Objects::isNull; Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = isEmpty.negate(); 
Функции

Функции принимают один аргумент и возвращают некоторый результат. Методы по умолчанию могут использоваться для построения цепочек вызовов ( compose , andThen ).

Function toInteger = Integer::valueOf; Function backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123" 
Поставщики

Поставщики (suppliers) предоставляют результат заданного типа. В отличии от функций, поставщики не принимают аргументов.

Supplier personSupplier = Person::new; personSupplier.get(); // new Person 
Потребители

Потребители (consumers) представляют собой операции, которые производятся на одним входным аргументом.

Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker")); 
Компараторы

Компараторы хорошо известны по предыдущим версиям Java. Java 8 добавляет в интерфейс различные методы по умолчанию.

Comparator 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 
Опциональные значения

Опциональные значения (optionals) не являются функциональными интерфейсами, однако являются удобным средством предотвращения NullPointerException. Это важная концепция, которая понадобится нам в следующем разделе, поэтому давайте взглянем, как работают опциональные значения.

Опциональные значение — это по сути контейнер для значения, которое может быть равно null. Например, вам нужен метод, который возвращает какое-то значение, но иногда он должен возвращать пустое значение. Вместо того, чтобы возвращать null, в Java 8 вы можете вернуть опциональное значение.

Optional 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 представляет собой последовательность элементов, над которой можно производить различные операции. Операции над потоками бывают или промежуточными (intermediate) или конечными (terminal). Конечные операции возвращают результат определенного типа, а промежуточные операции возвращают тот же поток. Таким образом вы можете строить цепочки из несколько операций над одним и тем же потоком. Поток создаются на основе источников, например типов, реализующих java.util.Collection , такие как списки или множества (ассоциативные массивы не поддерживаются). Операции над потоками могут выполняться как последовательно, так и параллельно.

Сначала давайте посмотрим, как работать с потоком последовательно. Сперва создадим источник в виде списка строк:

List 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

Операция Filter принимает предикат, который фильтрует все элементы потока. Эта операция является промежуточной, т.е. позволяет нам вызвать другую операцию (например, forEach ) над результатом. ForEach принимает функцию, которая вызывается для каждого элемента в (уже отфильтрованном) поток. ForEach является конечной операцией. Она не возращает никакого значения, поэтому дальнейший вызов потоковых операций невозможен.

stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1" 
Sorted

Операция Sorted является промежуточной операцией, которая возвращает отсортированное представление потока. Элементы сортируются в обычном порядке, если вы не предоставили свой компаратор:

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 для преобразования каждого объекта в объект другого типа. Тип результирующего потока зависит от типа функции, которую вы передаете при вызове map .

stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1" 
Match

Для проверки, удовлетворяет ли поток заданному предикату, используются различные операции сопоставления (match). Все операции сопоставления являются конечными и возвращают результат типа boolean.

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

Операция Count является конечной операцией и возвращает количество элементов в потоке. Типом возвращаемого значения является long .

long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3 
Reduce

Эта конечная операция производит свертку элементов потока по заданной функции. Результатом является опциональное значение.

Optional 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 values = new ArrayList<>(max); for (int i = 0; i

Теперь измерим время сортировки этого списка.

Последовательная сортировка
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 

Как вы можете видеть, оба куска кода практически идентичны, однако параллельная сортировка почти в два раза быстрее. Все, что вам нужно сделать, это заменить вызов stream() на parallelStream() .

Ассоциативные массивы

Как уже упоминалось, ассоциативные массивы (maps) не поддерживают потоки. Вместо этого ассоциативные массивы теперь поддерживают различные полезные методы, которые решают часто встречаемые задачи.

Map map = new HashMap<>(); for (int i = 0; i < 10; i++) < map.putIfAbsent(i, "val" + i); >map.forEach((id, val) -> System.out.println(val)); 

Этот код в особых комментариях не нуждается: putIfAbsent позволяет нам не писать дополнительные проверки на 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 

В случае отсутствия ключа Merge создает новую пару ключ-значение. В противном случае — вызывает функцию объединения для существующего значения.

API для работы с датами

Java 8 содержит совершенно новый API для работы с датами и временем, расположенный в пакете java.time . Новый API сравним с библиотекой Joda-Time, однако имеются отличия. Следующие разделы рассказывают о наиболее важных частях нового API.

Clock

Тип Clock предоставляет доступ к текущей дате и времени. Этот тип знает о часовых поясах и может использоваться вместо вызова System.currentTimeMillis() для возвращения миллисекунд. Такая точная дата также может быть представлена классом 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

Тип LocalTime представляет собой время с учетом часового пояса, например, 10pm или 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

Тип LocalDate представляет конкретную дату, например, 2014-03-11. Объекты LocalDate неизменяемы и являются аналогом 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 путем парсинга строки:

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 представляет собой комбинацию даты и времени. Объекты 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 

Путем добавления информации о часовом поясе мы можем получить Instant .

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

Java 8 позволяет нам использовать множество аннотаций одного типа путем указания аннотации @Repeatable .

Вариант 1: использовать аннотацию-контейнер (старый способ)

@Hints() class Person <> 

Вариант 2: использовать повторяемую аннотацию (новый способ)

@Hint("hint1") @Hint("hint2") class Person <> 

При использовании варианта 2 компилятор автоматически подставляет аннотацию @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() @interface MyAnnotation <> 
Вот и все

На этом введение в программирование на Java 8 завершено. Вам остается самостоятельно изучить другие новинки JDK 1.8, например, Arrays.parallelSort , StampedLock , CompletableFuture и другие.

Полный исходный код статьи доступен на GitHub.

Возможности Java — от Java 8 до Java 17

С момента появления в 1995 году до сегодняшнего дня в Java многое изменилось. Java 8 была революционным выпуском, вернувшим Java на пьедестал лучших языков программирования.

Мы рассмотрим большинство изменений в языке Java, которые произошли с Java 8 в 2014 году до сегодняшнего дня. Мы постараемся быть как можно более краткими по каждой функции. Намерение состоит в том, чтобы иметь ссылку на все новые фичи языка Java версий 8 - 17 включительно.

Эта статья сопровождается примером рабочего кода на GitHub.

Эта статья переведена по просьбе одного из читателей Хабр. Надеюсь она будет полезна как краткий справочник по новым фичам языка Java 8 - 17.

Java 8

Основные изменения в выпуске Java 8:

  • Лямбда-выражения и Stream API
  • Ссылка на метод
  • Методы по умолчанию
  • Аннотации типов
  • Повторяющиеся аннотации

Лямбда-выражения и Stream API

Java всегда была известна наличием большого количества шаблонного кода. С выпуском Java 8 это утверждение стало немного менее актуальным. Stream API и лямбда-выражения - это новые возможности, которые приближают нас к функциональному программированию.

В наших примерах мы увидим, как мы используем лямбды и потоки в различных сценариях.

Мир до лямбда-выражений

Допустим у нас есть автосалон. Чтобы избавиться от всей бумажной работы, мы хотим создать программу, которая находит все доступные в настоящее время автомобили с пробегом менее 50 000 км.

Давайте посмотрим, как наивно реализовать функцию для чего-то вроде этого:

public class LambdaExpressions < public static ListfindCarsOldWay(List cars) < ListselectedCars = new ArrayList<>(); for (Car car : cars) < if (car.kilometers < 50000) < selectedCars.add(car); >> return selectedCars; > >

Чтобы реализовать ее, мы создаем статическую функцию, которая принимает список автомобилей List . Он должен возвращать отфильтрованный список в соответствии с указанным условием.

Использование потока и лямбда-выражения

У нас та же проблема, что и в предыдущем примере.

Наш клиент хочет найти все автомобили по одинаковым критериям.

Давайте посмотрим на решение, в котором мы использовали API потока и лямбда-выражение:

public class LambdaExpressions < public static ListfindCarsUsingLambda(List cars) < return cars.stream().filter(car ->car.kilometers < 50000) .collect(Collectors.toList()); >>

Нам нужно передать список машин в поток, вызвав метод stream() . Внутри метода filter() мы устанавливаем наше условие. Мы сравниваем каждую запись с желаемым условием. Мы сохраняем только те записи, которые имеют пробег менее 50 000 километров. Последнее, что нам нужно сделать, это свернуть результат в список.

Подробнее о лямбда-выражениях можно найти в документации.

Ссылка на метод

Без ссылки на метод

У нас все еще есть автосалон, и мы хотим распечатать все автомобили в магазине. Для этого мы будем использовать ссылку на метод.

Ссылка на метод позволяет нам вызывать функции в классах, используя особый синтаксис :: . Есть четыре вида ссылок на методы:

  • Ссылка на статический метод
  • Ссылка на метод экземпляра объекта
  • Ссылка на метод экземпляра для типа
  • Ссылка на конструктор

Давайте посмотрим, как это сделать с помощью стандартного вызова метода:

public class MethodReference < ListwithoutMethodReference = cars.stream().map(car -> car.toString()) .collect(Collectors.toList()); >

Мы используем лямбда-выражение для вызова метода toString() на каждого объекта car.

Использование ссылки на метод

Теперь давайте посмотрим, как в той же ситуации использовать ссылку на метод:

public class MethodReference < ListmethodReference = cars.stream().map(Car::toString) .collect(Collectors.toList()); >

Мы снова используем лямбда-выражение, но теперь мы вызываем метод toString() по ссылке на метод. Мы видим, насколько он лаконичнее и легче читается.

Чтобы узнать больше о справочнике по методам, посмотрите документацию.

Методы по умолчанию

Представим, что у нас есть простой метод, log(String message) , который при вызове выводит сообщения журнала. Мы поняли, что хотим предоставлять сообщениям отметки времени, чтобы в журналах было легко найти их. Мы не хотим, чтобы наши клиенты перестали работать после того, как мы внесем это изменение. Мы сделаем это, используя реализацию метода по умолчанию в интерфейсе.

Реализация метода по умолчанию - это функция, которая позволяет нам создать резервную реализацию метода интерфейса.

Сценарий использования

Посмотрим, как выглядит наш контракт:

public class DefaultMethods < public interface Logging < void log(String message); >public class LoggingImplementation implements Logging < @Override public void log(String message) < System.out.println(message); >> >

Мы создаем простой интерфейс с помощью всего одного метода и реализуем его в классе LoggingImplementation .

Добавление нового метода

Мы добавим новый метод внутрь интерфейса. Метод принимает второй аргумент с именем date, который представляет отметку времени.

public class DefaultMethods < public interface Logging < void log(String message); void log(String message, Date date); >>

Мы добавляем новый метод, но не реализуем его во всех клиентских классах. Компилятор выдаст исключение:

Class 'LoggingImplementation' must either be declared abstract or implement abstract method 'log(String, Date)' in 'Logging'`.

Использование методов по умолчанию

После добавления нового метода внутри интерфейса наш компилятор выдавал исключения. Мы собираемся решить эту проблему, используя реализацию метода по умолчанию для нового метода.

Давайте посмотрим, как создать реализацию метода по умолчанию:

public class DefaultMethods < public interface Logging < void log(String message); default void log(String message, Date date) < System.out.println(date.toString() + ": " + message); >> >

Ввод ключевого слова default позволяет нам добавить реализацию метода внутри интерфейса. Теперь наш класс LoggingImplementation не завершается с ошибкой компилятора, даже если мы не реализовали новый метод внутри него.

Чтобы узнать больше о методах по умолчанию, обратитесь к документации.

Аннотации типов

Аннотации типов - еще одна функция, представленная в Java 8. Несмотря на то, что и раньше у нас были аннотации, теперь мы можем использовать их везде, где используется тип. Это означает, что мы можем использовать их при:

  • определении локальной переменной
  • вызове конструктора
  • приведении типов
  • в дженериках
  • в throw и многое другое

Такие инструменты, как IDE, могут затем читать эти аннотации и выдавать предупреждения или ошибки на основе аннотаций.

Определение локальной переменной

Давайте посмотрим, как сделать так, чтобы наша локальная переменная не получала значение null :

public class TypeAnnotations < public static void main(String[] args) < @NotNull String userName = args[0]; >>

Здесь мы используем аннотацию в определении локальной переменной. Обработчик аннотаций времени компиляции теперь может читать аннотацию @NotNull и выдавать ошибку, если переменной присваивается значение NULL.

Вызов конструктора

Мы хотим убедиться, что мы не можем создать пустой ArrayList :

public class TypeAnnotations < public static void main(String[] args) < Listrequest = new @NotEmpty ArrayList<>(Arrays.stream(args).collect( Collectors.toList())); > >

Это прекрасный пример того, как использовать аннотации типов в конструкторе. Опять же, обработчик аннотаций может определить аннотацию и проверить, не пуст ли список массивов.

Generic тип

Пусть одно из наших требований - каждый электронный адрес должен быть в формате @.com . Если мы используем аннотации типов, мы можем сделать это легко:

public class TypeAnnotations < public static void main(String[] args) < Listemails; > >

Это определение списка адресов электронной почты. Мы используем аннотацию @Email , которая гарантирует, что каждая запись внутри этого списка находится в желаемом формате.

Инструмент может использовать рефлексию для определения аннотации и проверки того, что каждый из элементов в списке является допустимым адресом электронной почты.

Дополнительные сведения об аннотациях типов см. в документации.

Повторяющиеся аннотации

Представим, что у нас есть приложение с полностью реализованной системой безопасности. Оно имеет разные уровни авторизации. Несмотря на то, что мы все реализовали тщательно, мы хотим убедиться, что мы регистрируем каждое несанкционированное действие. При каждом несанкционированном действии мы отправляем электронное письмо владельцу компании и нашей группе администраторов безопасности. Повторяющиеся аннотации - это наш способ продолжить этот пример.

Повторяющиеся аннотации позволяют нам размещать несколько аннотаций в одном классе.

Создание повторяющейся аннотации

В этом примере мы собираемся создать повторяющуюся аннотацию под названием @Notify :

public class RepeatingAnnotations < @Repeatable(Notifications.class) public @interface Notify < String email(); >public @interface Notifications < Notify[] value(); >>

Мы создаем обычную аннотацию @Notify , но добавляем к ней (мета) аннотацию @Repeatable . Кроме того, мы должны создать аннотацию «контейнер» Notifications , содержащую массив объектов Notify . Обработчик аннотаций теперь может получить доступ ко всем повторяющимся аннотациям Notify через аннотацию контейнера Noifications .

Обратите внимание, что это фиктивная аннотация только для демонстрационных целей. Эта аннотация не будет отправлять электронные письма без процессора аннотаций, который читает ее, а затем отправляет электронные письма.

Использование повторяющихся аннотаций

Мы можем добавить повторяющуюся аннотацию несколько раз к одной и той же конструкции:

@Notify(email = "admin@company.com") @Notify(email = "owner@company.com") public class UserNotAllowedForThisActionException extends RuntimeException < final String user; public UserNotAllowedForThisActionException(String user) < this.user = user; >>

У нас есть собственный класс исключений, которые мы будем генерировать всякий раз, когда пользователь пытается сделать что-то, что ему не разрешено. Наши аннотации к этому классу говорят, что мы хотим высылать уведомления в двух электронных письмах, когда код генерирует это исключение.

Чтобы узнать больше о повторяющихся аннотациях, обратитесь к документации.

Java 9

В Java 9 представлены следующие основные функции:

  • Система модулей Java
  • Try-with-resources
  • Diamond оператор для анонимных внутренних классов
  • Private методы в интерфейсах

Система модулей Java

Модуль - это группа пакетов, их зависимости и ресурсы. Он предоставляет более широкий набор функций, чем пакеты.

При создании нового модуля нам необходимо предоставить несколько атрибутов:

  • Имя
  • Зависимости
  • Публичные пакеты - по умолчанию все пакеты являются private модулями.
  • Предлагаемые сервисы
  • Потребляемые сервисы
  • Разрешения на рефлексию

Не вдаваясь в подробности, давайте создадим наш первый модуль. В нашем примере мы покажем несколько параметров и ключевых слов, которые можно использовать при создании модуля.

Создание модулей внутри IntelliJ IDEA

Сначала рассмотрим простой пример. Мы создадим приложение Hello World, в котором мы напечатаем «Hello» из одного модуля и вызываем второй модуль, чтобы вывести «World!».

Поскольку я работаю в IntelliJ IDEA, нам нужно кое-что понять в первую очередь. IntelliJ IDEA имеет концепцию модулей. Поэтому каждый модуль Java должен соответствовать одному модулю IntelliJ.

Структура пакета

У нас есть два Java модуля: hello.module и world.module . Они соответствуют IntelliJ модулям hello и world соответственно. Внутри каждого из них мы создали файл module-info.java . Этот файл определяет наш Java-модуль. Внутри мы объявляем, какие пакеты нам нужно экспортировать и от каких модулей мы зависим.

Определение нашего первого модуля

Мы используем модуль hello для печати слова: «Hello». Внутри мы вызываем метод внутри модуля world , который выведет «World!». Первое, что нам нужно сделать, это объявить внутри module-info.java экспорт пакета, содержащего наш класс World.class :

module world.module

Мы используем ключевое слово module с именем модуля для ссылки на модуль.

Следующее ключевое слово, которое мы используем, - это exports . Он сообщает модульной системе, что мы делаем наш пакет com.reflectoring.io.app.world видимым за пределами нашего модуля.

Можно использовать еще несколько ключевых слов:

  • requires
  • requires transitive
  • exports to
  • uses
  • provides with
  • open
  • opens
  • opens to

Из них мы покажем только декларацию requires . Остальные можно найти в документации.

Определение нашего второго модуля

После того, как мы создали и экспортировали модуль world , мы можем приступить к созданию модуля hello :

module hello.module

Мы определяем зависимости с помощью ключевого слова requires . Мы ссылаемся наш вновь созданный модуль hello.module . Пакеты, которые не экспортируются, по умолчанию являются private для модуля и не могут быть видимы извне модуля.

Чтобы узнать больше о модульной системе Java, обратитесь к документации.

Try-with-resources

Try-with-resources - это фича, которая позволяет нам объявлять новые автоматически закрываемые ресурсы в блоке try-catch . Объявление их внутри блока try-catch указывает JVM освободить их после выполнения кода. Единственное условие - декларированный ресурс реализует интерфейс Autoclosable .

Закрытие ресурса вручную

Мы хотим читать текст, используя BufferedReader . BufferedReader является закрываемым ресурсом, поэтому нам нужно убедиться, что он правильно закрыт после использования. До Java 8 мы делали это так:

public class TryWithResources < public static void main(String[] args) < BufferedReader br = new BufferedReader( new StringReader("Hello world example!")); try < System.out.println(br.readLine()); >catch (IOException e) < e.printStackTrace(); >finally < try < br.close(); >catch (IOException e) < e.printStackTrace(); >> > >

В блоке finally мы бы вызывали close() . Блок finally гарантирует, что reader всегда правильно закрыт.

Закрытие ресурса с помощью try-with-resources

В Java 8 появилась фича try-with-resource, которая позволяет нам декларировать наш ресурс внутри блока try . Это гарантирует, что наш закрываемый объект будет закрыт без использования finally .

Примечание переводчика. В оригинале ошибка. Оператор try-with-resources появился в Java 7. См. Java SE 7 Features and Enhancements.

В Java 9 разрешено использовать final переменные в качестве ресурсов в операторе try-with-resources . См. What’s New for the Java Language in JDK 9

Давайте посмотрим на пример использования BufferedReader для чтения строки:

public class TryWithResources < public static void main(String[] args) < final BufferedReader br3 = new BufferedReader( new StringReader("Hello world example3!")); try (BufferedReader reader = br3) < System.out.println(reader.readLine()); >catch (IOException e) < System.out.println("Error happened!"); >> >

Внутри блока try мы назначаем наш ранее созданной BufferedReader новой переменной. Теперь мы уверены, что наш reader всегда закрывается.

Чтобы узнать больше о try-with-resources, обратитесь к документации.

Diamond оператор для анонимных внутренних классов

До Java 9 мы не могли использовать ромбовидный оператор (<>) во внутреннем анонимном классе.

В нашем примере мы создадим абстрактный класс StringAppender . У класса есть только один метод, который добавляет две строки с разделителем - между ними. Мы будем использовать анонимный класс для реализации метода append() :

public class DiamondOperator < StringAppenderappending = new StringAppender<>() < @Override public String append(String a, String b) < return new StringBuilder(a).append("-").append(b).toString(); >>; public abstract static class StringAppender  < public abstract T append(String a, String b); >>

Мы используем ромбовидный оператор, чтобы опустить тип при вызове конструктора new StringAppender<>() . Если мы используем Java 8, в этом примере мы получим ошибку компилятора:

java: cannot infer type arguments for com.reflectoring.io.java9.DiamondOperator.StringAppender reason: '<>' with anonymous inner classes is not supported in -source 8 (use -source 9 or higher to enable '<>' with anonymous inner classes)

В Java 9 эта ошибка компилятора больше не возникает.

Private методы в интерфейсах

Мы уже упоминали, как мы используем методы по умолчанию в интерфейсах.

Как разделить реализацию на несколько методов? При работе с классами мы можем добиться этого с помощью private методов. Может ли это быть выходом в нашем случае?

Что касается Java 9, да. Мы можем создавать private методы внутри интерфейсов.

Использование private метода в интерфейсе

В нашем примере мы хотим напечатать набор имен.

Для интерфейса, содержащего эту функцию, определен метод по умолчанию. Мы решили, что должны, если клиент не предоставляет реализацию, предоставить набор предопределенных имен, которые мы читаем из папки ресурсов:

public class PrivateInterfaceMethods < public static void main(String[] args) < TestingNames names = new TestingNames(); System.out.println(names.fetchInitialData()); >public static class TestingNames implements NamesInterface < public TestingNames() < >> public interface NamesInterface < default ListfetchInitialData() < try (BufferedReader br = new BufferedReader( new InputStreamReader(this.getClass() .getResourceAsStream("/names.txt")))) < return readNames(br); >catch (IOException e) < e.printStackTrace(); return null; >> private List readNames(BufferedReader br) throws IOException < ArrayListnames = new ArrayList<>(); String name; while ((name = br.readLine()) != null) < names.add(name); >return names; > > > 

Мы используем BufferedReader для чтения файла, содержащего имена по умолчанию, которыми мы делимся с клиентом. Чтобы инкапсулировать наш код и, возможно, сделать его повторно используемым в других методах, мы решили переместить код для чтения и сохранения имен в List в отдельный метод. Этот метод является private, и теперь мы можем использовать его где угодно в нашем интерфейсе.

Как уже упоминалось, основным преимуществом этой функции внутри Java 9 является лучшая инкапсуляция и возможность повторного использования кода.

Java 10

Вывод типа локальной переменной

Java всегда требовала явное указание типа локальных переменных.

При написании и чтении кода мы всегда знаем, какой тип ожидаем. С другой стороны, большая часть кода - это просто указания типа, что не очень удобно.

Тип var позволяет нам опускать тип в левой части наших операторов.

Старый способ

Давайте рассмотрим пример. Мы хотим создать небольшую группу людей, поместить все в один список, а затем просмотреть этот список в цикле for и распечатать имя и фамилию:

public class LocalTypeVar < public void explicitTypes() < Person Roland = new Person("Roland", "Deschain"); Person Susan = new Person("Susan", "Delgado"); Person Eddie = new Person("Eddie", "Dean"); Person Detta = new Person("Detta", "Walker"); Person Jake = new Person("Jake", "Chambers"); Listpersons = List.of(Roland, Susan, Eddie, Detta, Jake); for (Person person : persons) < System.out.println(person.name + " - " + person.lastname); >> >

Это код, который мы можем видеть в большинстве случаев в Java. Мы используем явные типы, чтобы убедиться, что мы знаем, чего ожидает метод.

Неявный тип с var

Теперь мы рассмотрим тот же пример, но с использованием ключевого слова var , введенного в Java 10. Мы по-прежнему хотим создать несколько объектов Person и поместить их в список. После этого мы просмотрим этот список и распечатаем имя каждого человека:

public class LocalTypeVar < public void varTypes() < var Roland = new Person("Roland", "Deschain"); var Susan = new Person("Susan", "Delgado"); var Eddie = new Person("Eddie", "Dean"); var Detta = new Person("Detta", "Walker"); var Jake = new Person("Jake", "Chambers"); var persons = List.of(Roland, Susan, Eddie, Detta, Jake); for (var person : persons) < System.out.println(person.name + " - " + person.lastname); >> >

Мы видим несколько наиболее типичных примеров использования типа var для локальных переменных. Во-первых, мы используем их для определения локальных переменных. Это может быть отдельный объект или даже список с ромбовидным оператором.

Дополнительные сведения о выводе типа локальной переменной см. в документации.

Java 11

Вывод типа локальной переменной в лямбда-выражениях

В Java 11 внесены улучшения в ранее упомянутый вывод типа локальной переменной. Они позволяют нам использовать тип var внутри лямбда-выражения.

Мы снова создадим несколько Person, соберем их в список и отфильтруем записи, в имени которых нет буквы «а»:

public class LocalTypeVarLambda < public void explicitTypes() < var Roland = new Person("Roland", "Deschain"); var Susan = new Person("Susan", "Delgado"); var Eddie = new Person("Eddie", "Dean"); var Detta = new Person("Detta", "Walker"); var Jake = new Person("Jake", "Chambers"); var filteredPersons = List.of(Roland, Susan, Eddie, Detta, Jake) .stream() .filter((var x) ->x.name.contains("a")) .collect(Collectors.toList()); System.out.println(filteredPersons); > >

Внутри метода filter() мы используем var для вывода типа переменной вместо явного упоминания типа.

Обратите внимание, что не имеет значения, используем ли мы var или нет, вывод типа локальной переменной в лямбда-выражениях будет работать одинаково для обоих случаев.

Java 14

Switch выражения

Switch выражения позволили нам опускать break внутри каждого case блока. Это помогает улучшить читаемость и понимание кода.

В этом разделе мы увидим несколько способов использования Switch выражений.

Старый способ c оператором Switch

У нас есть метод, в котором клиент указывает желаемый месяц, а мы возвращаем количество дней в этом месяце.

Первое, что приходит в голову, - это построить его с помощью оператора switch-case :

public class SwitchExpression < public static void main(String[] args) < int days = 0; Month month = Month.APRIL; switch (month) < case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER : days = 31; break; case FEBRUARY : days = 28; break; case APRIL, JUNE, SEPTEMBER, NOVEMBER : days = 30; break; default: throw new IllegalStateException(); >> >

Нам нужно убедиться, что мы поместили оператор break внутри каждого case блока кода. В противном случае будут проверяться другие условия после совпадения с первым.

Использование Switch выражений

Мы рассмотрим тот же пример, что и раньше. Пользователь хочет отправить месяц и получить количество дней в этом месяце:

public class SwitchExpression < public static void main(String[] args) < int days = 0; Month month = Month.APRIL; days = switch (month) < case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER ->31; case FEBRUARY -> 28; case APRIL, JUNE, SEPTEMBER, NOVEMBER -> 30; default -> throw new IllegalStateException(); >; > > 

В блоке case мы используем немного другие обозначения. Мы используем -> вместо двоеточия. Несмотря на то, что мы не указываем оператор break , мы все равно выйдем из оператора switch при первом выполненном условии.

Этот код будет делать то же самое, что и код, показанный в предыдущем примере.

Ключевое слово yield

Логика внутри case блока может быть немного сложнее, чем просто возврат значения. Например, мы хотим записать в лог, какой месяц нам отправил пользователь:

public class SwitchExpression < public static void main(String[] args) < int days = 0; Month month = Month.APRIL; days = switch (month) < case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER -> < System.out.println(month); yield 31; >case FEBRUARY -> < System.out.println(month); yield 28; >case APRIL, JUNE, SEPTEMBER, NOVEMBER -> < System.out.println(month); yield 30; >default -> throw new IllegalStateException(); >; > >

В многострочном блоке кода мы должны использовать ключевое слово yield для возврата значения из case блока.

Чтобы узнать больше об использовании Switch выражений, обратитесь к документации.

Java 15

Текстовые блоки

Текстовый блок усовершенствует форматирование строковых переменных. Начиная с Java 15, мы можем написать String, занимающую несколько строк, как обычный текст.

Пример без использования текстовых блоков

Мы хотим отправить HTML-документ по электронной почте. Мы сохраняем шаблон электронного письма в переменной:

public class TextBlocks < public static void main(String[] args) < System.out.println( "\n" + "\n" + " \n" + " Example\n" + " \n" + " \n" + " 

This is an example of a simple HTML " + "page with one paragraph.

\n" + " \n" + "\n"); > >

Мы форматируем нашу строку, как в примере выше. Нам нужно позаботиться о переходе текста на новую строку и соединить все строки в одну строку.

Пример использования текстовых блоков

Давайте рассмотрим тот же пример HTML-шаблона для электронной почты. Мы хотим отправить пример электронного письма с простым форматированием HTML. На этот раз мы будем использовать текстовый блок:

public class TextBlocks < public static void main(String[] args) < System.out.println( """   Example  

This is an example of a simple HTML page with one paragraph.

""" ); > >

Мы использовали специальный синтаксис для открытия и закрытия кавычек: """ . Это позволяет нам трактовать нашу строку как будто мы записываем ее в файл .txt.

Есть некоторые правила, которые мы должны соблюдать при использовании текстового блока. Нам нужно убедиться, что мы поместили текст на новую строку после открывающих кавычек, иначе наш компилятор выдаст ошибку:

Illegal text block start: missing new line after opening quotes.

Если мы хотим закончить нашу строку символом \n мы можем сделать это, поместив новую строку перед закрывающими кавычками """ , как в примере выше.

Чтобы узнать больше о текстовых блоках, обратитесь к документации.

Java 16

Сопоставление с образцом instanceof

Сопоставление с образцом instanceof позволяет нам преобразовать нашу переменную в строку и использовать ее внутри желаемого if-else блока без явного преобразования.

Пример без сопоставления с образцом

У нас есть вызываемый базовый класс Vehicle и два расширяющих его класса: Car и Bicycle . Мы опустили код для них - вы можете найти его в репозитории GitHub.

Наш алгоритм расчета цен зависит от экземпляра автомобиля:

public class PatternMatching < public static double priceOld(Vehicle v) < if (v instanceof Car) < Car c = (Car) v; return 10000 - c.kilomenters * 0.01 - (Calendar.getInstance().get(Calendar.YEAR) - c.year) * 100; >else if (v instanceof Bicycle) < Bicycle b = (Bicycle) v; return 1000 + b.wheelSize * 10; >else throw new IllegalArgumentException(); > >

Поскольку мы не используем сопоставление с образцом, нам нужно преобразовать транспортное средство в правильный тип внутри каждого if-else блока. Как мы видим, это типичный пример шаблонного кода, которым славится Java.

Использование сопоставления с образцом

Давайте посмотрим, как мы можем избежать шаблонного кода в приведенном выше примере:

public class PatternMatching < public static double price(Vehicle v) < if (v instanceof Car c) < return 10000 - c.kilomenters * 0.01 - (Calendar.getInstance().get(Calendar.YEAR) - c.year) * 100; >else if (v instanceof Bicycle b) < return 1000 + b.wheelSize * 10; >else throw new IllegalArgumentException(); > >

Следует отметить область видимости приведенной переменной. Ее видно только внутри оператора if.

Дополнительные сведения о сопоставлении с образцом в методе instanceof см. в документации.

Записи

Сколько POJO (Plain Old Java Objects) вы написали?

Что ж, могу ответить за себя: «Слишком много!».

У Java плохая репутация написания шаблонного кода. Lombok позволил нам перестать беспокоиться о геттерах, сеттерах и т. д. В Java 16 наконец-то появились Records (записи) для удаления большого количества шаблонного кода.

Класс записи - это не что иное, как обычный POJO, для которого большая часть кода генерируется из определения.

Обычное определение POJO

Давайте посмотрим на пример POJO класса до того, как в Java 16 были введены записи:

public class Vehicle < String code; String engineType; public String getCode() < return code; >public void setCode(String code) < this.code = code; >public String getEngineType() < return engineType; >public void setEngineType(String engineType) < this.engineType = engineType; >public Vehicle(String code, String engineType) < this.code = code; this.engineType = engineType; >@Override public boolean equals(Object o) . @Override public int hashCode() . @Override public String toString() . >

Он включает почти 50 строк кода для объекта, который содержит только два свойства. IDE сгенерировала этот код, но, тем не менее, он существует и должен поддерживаться.

Определение Record

Определение записи (Record) vehicle с теми же двумя свойствами может быть выполнено всего в одной строке:

public record VehicleRecord(String code, String engineType) <>

В этой строке есть все те же геттеры, сеттеры, конструкторы и т. д., что и в приведенном выше примере. Следует отметить, что класс Record по умолчанию является final, и мы должны этому подчиняться. Это означает, что мы не можем расширить класс Record, но для нас доступно большинство других вещей.

Чтобы узнать больше о классах записи, обратитесь к документации.

Java 17

Sealed классы

Модификатор final в классе не позволяет никому расширить его. А как насчет того, чтобы расширить класс, но разрешить это только для некоторых классов?

Вернемся в автосалон. Мы так гордимся нашим алгоритмом расчета цен, что хотим его опубликовать. Однако мы не хотим, чтобы кто-либо использовал наше представление Vehicle. Оно справедливо только для нашего бизнеса. Здесь мы видим небольшую проблему. Нам нужно раскрыть класс, но также ограничить его.

Именно здесь в игру вступает Java 17 с запечатанными (Sealed) классами. Запечатанный класс позволяет нам сделать класс final для всех, кроме явно упомянутых классов.

public sealed class Vehicle permits Bicycle, Car

Мы добавили модификатор sealed к нашему классу Vehicle , и нам пришлось добавить ключевое слово permits со списком классов, которым мы разрешаем расширять его. После этого изменения мы по-прежнему получаем ошибки от компилятора.

Здесь нам нужно сделать еще одну вещь.

Нам нужно добавить модификаторы final , sealed или non-sealed в классы, которые расширяют наш класс.

public final class Bicycle extends Vehicle

Ограничения

Для работы запечатанного класса необходимо выполнить несколько ограничений:

  • Разрешенные подклассы должны быть доступны запечатанному классу во время компиляции.
  • Разрешенные подклассы должны напрямую расширять запечатанный класс
  • Разрешенные подклассы должны иметь один из следующих модификаторов:
    • final
    • sealed
    • non-sealed

    Более подробную информацию о запечатанных классах можно найти в документации.

    Java 8. Новые возможности

    Java SE 8 представляет самые глубокие изменения в языке Java и библиотеках с момента возникновения. Хорошей новостью является то, что эти изменения позволят Вам писать программы более легко. Java 8 предлагает новый интерфейс прикладного программирования - API (Streams API), который поддерживает множество параллельных операций обработки данных и помогает вам думать о них в представлениях похожих на запросы к базе данных. Изменение программирования в Java на функциональный стиль поможет Вам писать код, который является более кратким и выразительным, и (во многих случаях) готовым к использованию в параллельной среде. В этом курсе, вы узнаете, как, казалось бы, незначительные изменения, которые вводят лямбда-выражения делают это возможным.

    После изучения курса вы сможете:

    • Применять лямбда-выражения (lambda expressions).
    • Использовать методы интерфейсов по умолчанию (default interface methods).
    • Включать в код ссылки на методы (method references) и повторяемые аннотации (repeatable annotations).
    • Встраивать новые конкурентные функции лямбда-выражений.
    • Работать с новыми возможностями API для работы с датами: Java Date и Time API.
    • Использовать среду Nashorn JavaScript.

    Содержание курса

    Модуль 1: Введение в лямбда-выражения.

    • Цель использования анонимного внутреннего класса.
    • Недостатки анонимных внутренних классов.
    • Компоненты лямбда-выражения.
    • Функциональный интерфейс.
    • Создание программ, использующих лямбда-выражения.

    Модуль 2: Рефакторинг кода для использования лямбда-выражений.

    • Причины добавления лямбда-выражений в язык Java.
    • Рефакторинг кода для уменьшения избыточности.
    • Рефакторинг кода при использовании внутренних классов.
    • Рефакторинг кода при использовании лямбда-выражений.
    • Список преимуществ лямбда-выражений.

    Модуль 3: Фильтрация коллекций с помощью лямбда-выражений.

    • Итерации по коллекции с помощью forEach.
    • Итерации по коллекции с помощью лямбда-выражений.
    • Описание интерфейса Stream.
    • Фильтрация коллекции с использованием лямбда-выражения.
    • Вызов существующего метода с использованием ссылки на метод.
    • Сцепление нескольких методов вместе.
    • Сравнение функционального и императивного программирования.
    • Определение конвейеров в терминах лямбда-выражений и коллекций.

    Модуль 4: Использование встроенных интерфейсов в лямбда-выражениях.

    • Список встроенных интерфейсов, включенных в java.util.function.
    • Использование интерфейса Predicate.
    • Обработка объекта с помощью интерфейса Consumer.
    • Обработка одного объекта и возврат другого с помощью интерфейса Function.
    • Создание нового объекта с помощью интерфейса Supplier.
    • Использование примитивных версий базовых интерфейсов.
    • Использование бинарных версий базовых интерфейсов.

    Модуль 5: Операции с коллекциями в лямбда-выражениях.

    • Извлечение данных из объекта с использованием отображения (map).
    • Обнаружение данных с использованием методов поиска.
    • Описание типов потоковых операций.
    • Определение класса Optional.
    • Выполнение расчетов с использованием методов.
    • Описание отложенной обработки.
    • Сортировка в потоке.
    • Сохранение результатов в коллекцию с помощью метода collect.

    Модуль 6: Параллельные потоки.

    • Обзор ключевых характеристик потоков.
    • Сравнение старого стиля циклических операции с потоками.
    • Описание параллельного выполнения потокового процесса.
    • Список основных условий использования параллельных потоков.
    • Определение редукции (reduction).
    • Ассоциативная функция при редукции.
    • Вычисление значения при редукции.
    • Описание процесса декомпзиции и объединения при завершении.

    Модуль 7: Примеры применения лямбда-выражений.

    • Изменение списка с использованием removeIf.
    • Обновление списка с использованием ReplaceAll.
    • Обновление map (отображение) с помощью computeIfAbsent, computeIfPresent и merge.
    • Отправка ключей и значений из map в stream.
    • Чтение файла в поток.
    • Чтение текстового файла в ArrayList.
    • Список, проход и поиск по структуре каталогов при помощи потока.
    • Сведение потока с помощью flatMap.

    Модуль 8: Расширения методов.

    • Построение качественных библиотек.
    • Использование статических методов в интерфейсах.
    • Использование методов по умолчанию.
    • Правила наследования для методов по умолчанию.

    Модуль 9: Использование DateTime API.

    • Список целей Date/Time API (JSR-310).
    • Создание и управление событиями на основе даты.
    • Создание и управление событиями на основе времени .
    • Комбинирование даты и времени в одном объекте.

    Модуль 10: JavaScript на Java с использованием Nashorn.

    • Создание и выполнение сценариев shell.
    • Написание приложений JavaScript.
    • Написание JavaFX приложений с помощью JavaScript.

    Разработчики java приложений, использующие Java SE 8.

    Особенности Java 8 – максимальное руководство (часть 1)

    Java-университет

    Особенности Java 8 – максимальное руководство (часть 1) - 1

    Первая часть перевода статьи Java 8 Features – The ULTIMATE Guide. Вторая часть тут (ссылка может поменяться). От редакции: статья опубликована в то время как Java 8 была доступна общественности и все указывает на то, что это действительно major-версия. Здесь мы в изобилии предоставили руководства Java Code Geeks, такие как Играем с Java 8 – Лямбды и Параллелизм, Java 8 руководство по API даты и времени: LocalDateTime и Абстрактный Класс против Интерфейса в эру Java 8. Мы также ссылаемся на 15 необходимых к прочтению руководств по Java 8 из других источников. Конечно мы рассматриваем некоторые из недостатков, например, Темная сторона Java 8. Итак, пришло время собрать все основные особенности Java 8 в одном месте для вашего удобства. Наслаждайтесь!

    1. Введение

    • Язык
    • Компилятор
    • Библиотеки
    • Инструменты
    • Среда выполнения (JVM)

    2. Новые особенности в языке Java 8

    В любом случае Java 8 – это крупный релиз. Можно сказать, что это заняло так много времени из-за реализации возможностей, которые искал каждый Java-разработчик. В этом разделе мы собираемся охватить большинство из них.

    2.1. Лямбды и Функциональные интерфейсы

    Лямбды (также известные как закрытые или анонимные методы) наиболее большое и наиболее ожидаемое изменение языка во всем релизе Java 8. Они позволяют нам задавать функциональность как аргумент метода (объявляя функцию вокруг), или задавать код как данные: понятия с которыми знаком каждый разработчик функционального программирования. Много языков на платформе JVM (Groovy, Scala, …) имели лямбды с первого дня, но у разработчиков Java не было выбора кроме как представлять лямбды через анонимные классы. Обсуждение дизайна лямбд заняли много времени и усилий общественности. Но в конце концов компромиссы были найдены, что привело к появлению новых кратких конструкций. В своей простейшей форме лямбда может быть представлена в виде разделенных запятыми списка параметров, символа –> и тела. Например:

     Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) ) 

    Обратите внимание, что тип аргумента e определен компилятором. Кроме того вы можете явно указать тип параметра обернув параметр в скобки. Например:

     Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.println( e ) ); 

    В случае, если тело лямбды более сложное, оно может быть обернуто в фигурные скобки подобно определению обычной функции в Java. Например:

     Arrays.asList( "a", "b", "d" ).forEach( e - < < System.out.print( e ); System.out.print( e ); >); 

    Лямбда может ссылаться на члены класса и локальные переменные (неявно делает обращение эффективным независимо от того обращается к final полю или нет). Например, эти 2 фрагмента эквиваленты:

     String separator = ","; Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.print( e + separator ) ); 
     final String separator = ","; Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.print( e + separator ) ); 

    Лямбды могут возвращать значение. Тип возвращаемого значения будет определен компилятором. Объявление return не требуется, если тело лямбды состоит из одной строки. Два фрагмента кода ниже эквивалентны:

     Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> e1.compareTo( e2 ) ); 
     Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> < int result = e1.compareTo( e2 ); return result; >); 

    Разработчики языка долго думали как сделать уже существующие функции лямбда-дружелюбными. В результате появилось понятие функционального интерфейса. Функциональный интерфейс – это интерфейс с только одним методом. В результате он может быть неявно преобразован в лямбда-выражение. java.lang.Runnable и java.util.concurrent.Callable два замечательных примера функциональных интерфейсов. На практике функциональные интерфейсы очень хрупки: если кто-то добавит хотя бы один другой метод в определение интерфейса, он не будет больше функциональным и процесс компиляции не завершится. Чтобы избежать этой хрупкости и явно определить намерения интерфейса как функционального в Java 8 была добавлена специальная аннотация @FunctionalInterface (все существующие интерфейсы в библиотеке Java получили аннотацию @FunctionalInterface). Давайте посмотрим на это простое определение функционального интерфейса:

     @FunctionalInterface public interface Functional

    Есть одна вещь, которую нужно иметь в виду: методы по умолчанию и статические методы не нарушают принцип функционального интерфейса и могут быть объявлены:

     @FunctionalInterface public interface FunctionalDefaultMethods < void method(); default void defaultMethod() < >> 

    Лямбды наиболее популярный пункт Java 8. Они имеют весь потенциал для привлечения большего количества разработчиков к этой прекрасной платформе и обеспечить умелую поддержку функциональным особенностям в чистой Java. Для более детальной информации обратитесь к официальной документации.

    2.2. Интерфейсы по умолчанию и статические методы

    В Java 8 было расширено определение интерфейсов двумя новыми концепциями: метод по умолчанию и статический метод. Методы по умолчанию делают интерфейсы несколько похожими на трейты, но служат немного другой цели. Они позволяют добавлять новые методы к существующим интерфейсам не нарушая обратную совместимость для ранее написанных версий этих интерфейсов. Разница между методами по умолчанию и абстрактными методами в том, что абстрактные методы должны быть реализованы, а методы по умолчанию нет. Вместо этого каждый интерфейс должен предоставить так называемую реализацию по умолчанию, и все наследники будут получать ее по умолчанию (с возможностью переопределить эту реализацию по умолчанию при необходимости). Давайте посмотрим на пример ниже.

     private interface Defaulable < // Интерфейсы теперь разрешают методы по умолчанию, // клиент может реализовывать (переопределять) // или не реализовывать его default String notRequired() < return "Default implementation"; >> private static class DefaultableImpl implements Defaulable < >private static class OverridableImpl implements Defaulable < @Override public String notRequired() < return "Overridden implementation"; >> 

    Интерфейс Defaulable объявил метод по умолчанию notRequired() используя ключевое слово default как часть определения метода. Один из классов, DefaultableImpl , реализует этот интерфейс оставляя метод по умолчанию как есть. Другой класс, OverridableImpl , переопределяет реализацию по умолчанию и предоставляет свою собственную. Другая интересная особенность, представленная в Java 8 – это то, что интерфейсы могут объявить (и предложить реализацию) статических методов. Вот пример:

     private interface DefaulableFactory < // Interfaces now allow static methods static Defaulable create( Suppliersupplier ) < return supplier.get(); >> 

    Небольшой фрагмент кода объединяет метод по умолчанию и статический метод из примера выше:

     public static void main( String[] args )

    Вывод на консоль этой программы выглядит так:

     Default implementation Overridden implementation 

    Реализация методов по умолчанию в JVM является очень эффективной и вызов метода поддерживается инструкциями байт-кода. Методы по умолчанию позволили существующим Java интерфейсам развиваться не нарушая процесс компиляции. Хорошие примеры – множество добавленных методов в интерфейс java.util.Collection : stream() , parallelStream() , forEach() , removeIf() , … Хотя будучи мощными, методы по умолчанию следует использовать с осторожностью: прежде, чем объявить метод по умолчанию стоит подумать дважды действительно ли это необходимо так как это может привести к неоднозначности компиляции и ошибках в сложных иерархиях. Для получения более детальной информации обращайтесь к документации.

    2.3. Ссылочные методы

    Ссылочные методы внедряют полезный синтаксис, чтобы ссылаться на существующие методы или конструкторы Java-классов или объектов (экземпляров). Совместно с лямбда-выражениями, ссылочные методы делают языковые конструкции компактными и лаконичными, делая его шаблонным. Ниже представлен класс Car как пример различных определений методов, давайте выделим четыре поддерживаемых типа ссылочных методов:

     public static class Car < public static Car create( final Suppliersupplier ) < return supplier.get(); >public static void collide( final Car car ) < System.out.println( "Collided " + car.toString() ); >public void follow( final Car another ) < System.out.println( "Following the " + another.toString() ); >public void repair() < System.out.println( "Repaired " + this.toString() ); >> 

    Первый ссылочный метод – ссылка на конструктор с синтаксисом Class::new или альтернативный для дженериков (generics) Class< T >::new . Обратите внимание, что конструктор не имеет аргументов.

     final Car car = Car.create( Car::new ); final List cars = Arrays.asList( car ); 

    Второй вариант это ссылка на статический метод с синтаксисом Class::static_method . Обратите внимание, что метод принимает ровно один параметр типа Car .

     cars.forEach( Car::collide ); 

    Третий тип – ссылка на метод экземпляра произвольного объекта определенного типа с синтаксисом Class::method . Обратите внимание, что никакие аргументы не принимаются методом.

     cars.forEach( Car::repair ); 

    И последний, четвертый тип – ссылка на метод экземпляра определенного класса с синтаксисом instance::method . Обратите внимание, что метод принимает только один параметр типа Car .

     final Car police = Car.create( Car::new ); cars.forEach( police::follow ); 

    Запуск всех этих примеров как Java-программы производит следующий вывод на консоль (ссылка на класс Car может отличаться):

     Collided com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d Repaired com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d Following the com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d 

    Для более детальной информации и деталей ссылочных методов обращайтесь к официальной документации.

    2.4. Повторяющиеся аннотации

    С тех пор как в Java 5 была введена поддержка аннотаций, эта возможность стала очень популярной и очень широко используемой. Тем не менее, одним из ограничений использования аннотаций был тот факт, что одна и та же аннотация не может быть объявлена более одного раза в одном месте. Java 8 нарушает это правило и представляет повторяющиеся аннотации. Это позволяет тем же аннотациям повторяться несколько раз в том месте где они объявлены. Повторяющиеся аннотации следует аннотировать себя с использованием аннотации @Repeatable . На самом деле, это не сколько изменение языка сколько трюк компилятора, в то время как техника остается той же. Давайте посмотрим на простой пример:

     package com.javacodegeeks.java8.repeatable.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; public class RepeatingAnnotations < @Target( ElementType.TYPE ) @Retention( RetentionPolicy.RUNTIME ) public @interface Filters < Filter[] value(); >@Target( ElementType.TYPE ) @Retention( RetentionPolicy.RUNTIME ) @Repeatable( Filters.class ) public @interface Filter < String value(); >; @Filter( "filter1" ) @Filter( "filter2" ) public interface Filterable < >public static void main(String[] args) < for( Filter filter: Filterable.class.getAnnotationsByType( Filter.class ) ) < System.out.println( filter.value() ); >> > 

    Как мы можем видеть, класс Filter аннотирован с помощью @Repeatable( Filters. class ). Filters просто владелец аннотаций Filter , но компилятор Java старается скрыть их присутствие от разработчиков. Таким образом, интерфейс Filterable содержит аннотации Filter , которые объявлены дважды (без упоминания Filters ). Также Reflection API предоставляет новый метод getAnnotationsByType() для возвращения повторяющихся аннотаций некоторого типа (помните, что Filterable. class .getAnnotation( Filters. class ) вернет экземпляр Filters введенный компилятором). Вывод программы будет выглядеть следующим образом:

     filter1 filter2 

    За более детальной информацией обратитесь к официальной документации.

    2.5. Улучшенное выведение типов

    Компилятор Java 8 получил много улучшений выведения типов. Во многих случаях явные параметры типов могут быть определены компилятором, тем самым делая код чище. Давайте взглянем на один из примеров:

     package com.javacodegeeks.java8.type.inference; public class Value  < public staticT defaultValue() < return null; >public T getOrDefault( T value, T defaultValue ) < return ( value != null ) ? value : defaultValue; >> 

    А вот использование с типом Value :

     package com.javacodegeeks.java8.type.inference; public class TypeInference < public static void main(String[] args) < final Valuevalue = new Value<>(); value.getOrDefault( "22", Value.defaultValue() ); > > 

    Параметр типа Value.defaultValue() определеляется автоматически и не должен быть представлен явно. В Java 7 тот же пример не будет компилироваться и должен быть переписан в виде Value.defaultValue().

    2.6. Расширенная поддержка аннотаций

    Java 8 расширяет контекст где могут быть использованы аннотации. Сейчас аннотацию может иметь почти все: локальные переменные, обобщенные типы, суперклассы и реализуемые интерфейсы, даже исключения методов. Несколько примеров представлено ниже:

     package com.javacodegeeks.java8.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.ArrayList; import java.util.Collection; public class Annotations < @Retention( RetentionPolicy.RUNTIME ) @Target( < ElementType.TYPE_USE, ElementType.TYPE_PARAMETER >) public @interface NonEmpty < >public static class Holder extends @NonEmpty Object < public void method() throws @NonEmpty Exception < >> @SuppressWarnings( "unused" ) public static void main(String[] args) < final Holderholder = new @NonEmpty Holder(); @NonEmpty Collection strings = new ArrayList<>(); > > 

    ElementType.TYPE_USE и ElementType.TYPE_PARAMETER два новых типа элементов, чтобы описать соответствующий контекст аннотаций. Annotation Processing API также потерпело незначительные изменения, чтобы распознать новые типы аннотаций в Java.

    3. Новые возможности в компиляторе Java

    3.1. Имена параметров

    На протяжении всего времени разработчики Java изобретали разнообразные способы сохранения имен параметров метода в Java байт-коде, чтобы сделать их доступными во время выполнения (например, библиотека Paranamer). И наконец, Java 8 создает эту нелегкую функцию в языке (используя Reflection API и метод Parameter.getName() ) и байт-коде (с помощью нового аргумента компилятора javac : –parameters ).

     package com.javacodegeeks.java8.parameter.names; import java.lang.reflect.Method; import java.lang.reflect.Parameter; public class ParameterNames < public static void main(String[] args) throws Exception < Method method = ParameterNames.class.getMethod( "main", String[].class ); for( final Parameter parameter: method.getParameters() ) < System.out.println( "Parameter: " + parameter.getName() ); >> > 

    Если вы скомпилируете этот класс без использования аргумента –parameters и затем запустите программу, вы увидите что-то типа этого:

     Parameter: arg0 

    С параметром –parameters переданным компилятору, вывод программы будет отличаться (будет показано фактическое имя параметра):

     Parameter: args 

    Для опытных пользователей Maven аргумент –parameters может быть добавлен в компиляцию используя секцию maven-compiler-plugin :

      org.apache.maven.plugins maven-compiler-plugin 3.1 -parameters 1.8 1.8   

    Для проверки доступности имен параметров есть удобный isNamePresent() метод предоставленный классом Parameter .

    4. Новые инструменты Java

    Java 8 поставляется с новым набором инструментов командной строки. В этом разделе мы рассмотрим самые интересные из них.

    4.1. Движок Nashorn: jjs

    jjs – автономный движок Nashorn, который основан на командной строке. Он принимает список JavaScript файлов исходного кода и запускает их. Например, давайте создадим файл func.js следующего содержания:

     function f() < return 1; >; print( f() + 1 ); 

    Чтобы запустить этот файл давайте передадим его как аргумент в jjs:

     jjs func.js 

    Вывод на консоль будет таким:
    Для более подробной информации смотрите документацию.

    4.2. Анализатор зависимостей класса: jdeps

    jdeps действительно отличный инструмент командной строки. Он показывает зависимости на уровне пакета или класса для Java классов. Он принимает .class файл, папку или JAR-файл на вход. По умолчанию jdeps выводит зависимости в стандартную систему вывода (консоль). В качестве примера давайте посмотрим на отчет зависимостей популярной библиотеки Spring Framework. Чтоб сделать пример коротким давайте посмотрим зависимости только для JAR-файла org.springframework.core-3.0.5.RELEASE.jar .

     jdeps org.springframework.core-3.0.5.RELEASE.jar 

    Эта команда выводит довольно много, поэтому мы будем анализировать только часть вывода. Зависимости сгруппированы по пакетам. Если зависимостей нет – будет выведено not found.

     org.springframework.core-3.0.5.RELEASE.jar -> C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar) -> java.io -> java.lang -> java.lang.annotation -> java.lang.ref -> java.lang.reflect -> java.util -> java.util.concurrent -> org.apache.commons.logging not found -> org.springframework.asm not found -> org.springframework.asm.commons not found org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar) -> java.lang -> java.lang.annotation -> java.lang.reflect -> java.util 

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *