Java: fill the gaps
12.8K subscribers
7 photos
216 links
Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк

🔥Тот самый курс по многопочке🔥
https://fillthegaps.ru/mt

Комплименты, вопросы, предложения: @utki_letyat
Download Telegram
Default методы: неудачный кейс

В чём ценность опытного разработчика? Способность видеть возможные проблемы. Интуиция, чуйка, "что-то мне здесь не нравится, давайте разберёмся".

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

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

C java 8 в интерфейсе Collection появились методы по умолчанию spliterator(), stream(), parallelStream(), removeIf(…).

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

От этих проблем пострадали пользователи SynchronizedCollection из библиотеки Apache Commons.

Что произошло?

В SynchronizedCollection каждый метод синхронизирован по объекту lock:
synchronized (lock) {
return coll.remove(object);
}

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

Что делает дефолтный метод removeIf? Берёт итератор, проверяет каждый элемент на соответствие условию и удаляет, если нужно.

Переложим на методы SynchronizedCollection. Синхронизация по lock берётся, отпускается, берётся, отпускается, и так несколько раз. При большой нагрузке управление перехватит другой поток, и произойдёт коллизия. Дефолтный метод не выполнит гарантий, заданных классом.

Ошибку легко исправить - переопределить метод removeIf:
synchronized (lock) {
return coll.removeIf(filter);
}

Проблема в том, что такие ошибки сложно обнаружить. Default метод появился в марте 2014, а класс обновили в июле 2019. 5 лет пользователи SynchronizedCollection пользовались ненадёжным методом.

5 лет! Может новым методом никто не пользовался. Может поток данных через коллекцию был небольшим. Может никаких последствий не было. А может были, неизвестно.

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

#core
2
IDEA: Code with me

Парное программирование - agile практика, когда два человека сидят за одним компьютером и работают над задачей. Один пишет код, другой - наблюдает.

Если партнёры хорошо сработаются, то код получится проще, а ошибок будет меньше.

Intellij IDEA недавно выпустила классный плагин Code with me, и теперь парным программированием можно заняться и на удалёнке.

Найти его легко: File → Settings → Plugins → Marketplace → Code with me

Новая плашка появится рядом с иконками Run/Debug. Можно отправить ссылку коллеге и вместе работать над задачей👯

#idea
По статистике 64% приложений сидят на java 8, а самая популярная фича java 8 - это Stream API.

Самая запутанная часть Stream API - коллекторы, о них я написала лонгрид в 3 частях.

Часть 1: обзор и простые коллекторы
Часть 2: сложные коллекторы
Часть 3: особенности дизайна

Рассмотрим почти все, что есть в JDK. Без внимания останутся collectingAndThen и teeing(Java 12). Не нашла ни одного кейса, где они полезны🤷
Коллекторы Stream API, часть 1: простые методы

В первой части повторим основы - из чего состоит стрим и что такое коллектор. Каждый код со Stream API состоит из 3х частей:
1️⃣ Получение стрима
2️⃣ Преобразования
3️⃣ Терминальная операция

1️⃣ list.stream()
2️⃣ .filter(e -> e != 3)
3️⃣ .count();

Терминальная операция collect собирает элементы стрима в другую структуру данных. Все подробности передаются через аргумент:
collect(Collector collector)

Коллекторы - статические методы класса Collectors, которые возвращают аргумент для метода collect. Я буду опускать основной класс и вместо Collectors.counting() будут писать counting(). Чтобы было короче.

Чаще всего элементы стрима собирают в обычную коллекцию:

▪️toCollection, toList, toSet
▪️toUnmodifiableSet, toUnmodifiableList

Set res=students.stream()
.filter(…).collect(toSet())

Ещё одна группа - коллекторы, которые возвращают одно значение:

▫️counting
▫️averagingToInt / Long / Double
▫️joining
▫️maxBy, minBy
▫️reducing
▫️summingInt / Long / Double
▫️summarizingInt / Long / Double

Интересны здесь только два метода:
🔸 joining
Соединяет элементы в одну строку:

chars.stream().collect(joining("-"));
// ['a','b','c'] → a-b-c

🔸summarizingInt
Возвращает объект IntSummaryStatistics, который содержит минимум, максимум, среднее, количество элементов и их сумму.

Остальные методы сами по себе бесполезны, так как есть простые аналоги:

list.stream().collect(counting())
// аналог
list.stream().count()

Коллекторы mapping, flatMapping и filtering применяют функцию к элементам перед отправкой в другой коллектор.

Set<Long> ids = …
collect(mapping(Student::id, toSet())

Использовать их напрямую тоже смысла нет. Проще применить к элементам map, flatmap или filter, а потом собрать результаты:

map(Student::id).collect(toSet())

Зачем нужны эти методы?

Они нужны в коллекторах groupingBy и partitioningBy, про них подробно поговорим завтра.

#core
👍51
Вы поручили джуниору задачу, в которой нужно написать пару строк на Stream API. Джуниор неопытный, но старательный, и принёс 5 вариантов решения. Все, кроме одного, приводят к одному результату. Какой код ошибочный?
Результат какого кода отличается от других?
Anonymous Poll
23%
1
27%
2
28%
3
7%
4
16%
5
Коллекторы Stream API, часть 2: сложные коллекторы

В этой части разберёмся в коллекторах toMap, groupingBy и partitioningBy. Уверена, года через 3 их будут спрашивать на занудных собеседованиях.

Будем тренироваться на классе Student из вопроса перед постом. У него есть id, имя, город и список оценок.

🔸toMap, toConcurrentMap
Цель понятна: уложить элементы стрима в map. Указываем функцию для ключа и для значения:
collect(toMap(Student::id, Student::name))

Если ключи повторяются, то вылетит IllegalStateException. Чтобы этого избежать, укажите третьим параметром, как объединять значения:

collect(toMap(Student::city, 
Student::name,
(a,b) -> a+" "+b))

["Москва":"Антон Аня Эдуард"]
Похоже на группировку по городу, но это не она. В результате получается одно значение, а не группа.

🔸groupingBy
Настоящую группировку делает коллектор groupingBy:

Map<String, List<Student>>=…
collect(groupingBy (Student::city))

По умолчанию сгруппированные элементы объединяются в список. Чтобы получить что-нибудь другое, передайте в метод другой коллектор:

groupingBy(Student::city, toSet())

Здесь нам наконец пригодятся коллекторы из предыдущего поста, которые подставим во второй аргумент.
▫️Города и количество студентов:
groupingBy(Student::city, counting())

▫️Города и имена студентов:
groupingBy(Student::city, 
mapping(Student::name, toSet()))

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

▫️ID студента и средняя оценка за экзамены:
...
Для этой задачи группировка не подходит. У одного студента только одна средняя оценка, количество итоговых элементов = количеству исходных. Группировать нечего, поэтому используем toMap:

toMap(Student::id,
s -> s.marks().stream().расчёт())

Вместо расчёт() - километр кода. Подсчёт среднего недоступен в интерфейсе Stream, а удобных методов перевода List<Integer> в IntStream нет.

🔸partitioningBy
Метод делит элементы на две группы по заданному условию. Результат - map с двумя ключами - true и false.
▫️Делим студентов на москвичей и жителей других городов:
Map<Boolean,Set<Student>>…
partitioningBy(s-> s.city().equals("Moscow"), toSet())

Вместо toSet можно подставить другой коллектор. Посчитаем количество студентов:

partitioningBy(s-> s.city().equals("Moscow"), counting())

Не очень понятно, зачем нужен partitioningBy. groupingBy даёт такой же результат, но на 2 символа короче:

groupingBy(s-> s.city().equals("Moscow"), counting())

Ответ на вопрос перед постом

Вы поручили джуниору посчитать среди студентов тех, кто не сдал ни одного экзамена. Вариант 3 предлагает применить к набору студентов метод Integer::longValue, что невозможно. Остальные варианты переводят каждого студента в число 1 и суммируют все значения. Ошибочный ответ - 3.

#core
👍3
Коллекторы Stream API, часть 3: дизайн

Вчера мы разбирали groupingBy и partitioningBy. У многих возникла мысль, что лучше держаться от группировок подальше. Коллекторы выглядят неважно относительно других методов Stream API:

▪️Многословные: collect(Collectors.toSet())
▪️Вложенные коллекторы
▪️Дублированные методы map, flatMap, max, min - 18 штук

Итог: плохая читаемость и желание написать всё через цикл for.

Почему так получилось? Разберёмся в этом посте.

Выделим три вопроса:
1️⃣ Почему вместо toSet() такой сложный collect(Collectors.toSet())?
2️⃣ Почему группировка - терминальная операция?
3️⃣ Зачем в классе 18 неполноценных методов и вложенные коллекторы?

Разберём по порядку.
1️⃣ Метод collect и класс Collectors.
Цель Stream API - удобная работа с данными. За интерфейсом Stream может быть любой источник данных: список, строка, коллекция или файл. Сторонние библиотеки могут реализовать свой источник данных и работать с ним стандартными средствами Stream API.

В обратную сторону это тоже работает. Пользователь может преобразовать стрим в свою структуру данных, для этого в интерфейсе Stream метод collect(Collector).

А ещё источник данных и конечная структура данных - разные сущности, поэтому в интерфейсе Stream нет методов toSet(), toList(). Single Responsibility.

2️⃣ Почему группировка - терминальная операция?

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

Дизайн Stream API для этого не подходит. Источник данных делится на части, элементы обрабатываются независимо, иногда в разных потоках.

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

Группировка работает со всеми элементами, и в терминах Stream API это терминальная операция, дальше работать со стримом нельзя.

3️⃣ Вложенные коллекторы и 18 методов для groupingBy

Кстати, разработчики JDK понимают, что группировка - это преобразование данных, а вовсе не финальная точка.

Что может понадобиться программисту?
🔸Указать итоговый тип данных.
🔸Преобразовать итоговые данные. Это сложно сделать напрямую, потому что универсального интерфейса для filter, map и average нет.

Для решения этих задач groupingBy получил аргумент-коллектор, а Collectors пополнился 18 методами-обёртками над map, flatMap, filter и average .

На мой взгляд получилось сложновато, но это спорный вопрос. А какие вам нравятся библиотеки?

💅 - Удобные методы для целевых кейсов, но с ограничениями
💪 - Максимум возможностей и кастомизации

#core
👍21
Сегодня начнётся онлайн конференция JLove❤️

Бесплатная!

Расписание уже готово, не забудьте подставить свой часовой пояс.

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

Зарегистрироваться: jlove.konfy.care
Паттерн проектирования, который в соответствии с принципом единственной обязанности передает другому объекту ответственность построения требуемых ему зависимостей внешнему, специально предназначенному для этого общему механизму
Anonymous Poll
40%
Dependency injection
9%
Dependency invertion
36%
Inversion of Control
16%
Factory Method
👍2
DI vs DI vs IoC

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

Сегодня разберём разницу между Dependency injection, Dependency invertion и Inversion of Control. Понимание пригодится на собеседованиях, при чтении статей по дизайну и архитектуре. Плюс поймёте, как хорошо вы программируете и какие проблемы решаете, даже не задумываясь.

Будем разбираться на простом примере.

Точка А: Сервис Service записывает логи в файл с помощью класса FileLogger:

class FileLogger {…}
class Service {
FileLogger logger=new FileLogger();
}

Сделаем код чуть лучше:

1️⃣ Dependency injection
это когда компоненты создаются не внутри класса, а передаются в конструкторах или сеттерах. Перенесём инициализацию логгера в конструктор:

class Service {
FileLogger logger;
Service (FileLogger logger) {
this.logger= logger;
}
}
Класс не занимается инициализацией логгера

2️⃣ Dependency invertion
Буква D в аббревиатуре SOLID, формулировка состоит из двух частей:

▫️Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
▫️Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракции.

Суть: пусть сервис работает не с конкретным логгером, а с интерфейсом

interface Logger {…}
class FileLogger implements Logger {…}
class Service {
Logger logger=new FileLogger();
}

В интерфейсе доступно меньше методов, поэтому его проще использовать
Реализацию легко заменить
Оба класса проще тестировать

Почему используется слово "абстракция"? Группу методов можно выделить в интерфейс, в абстрактный класс и даже в обычный класс. Но интерфейс самый подходящий вариант.

3️⃣ IoC - Inversion of Control
В маленьких программах жизнь начинается в методе main(). Программист создаёт объекты, вызывает методы, все шаги явно прописаны.

Inversion of Control - это когда ход выполнения программы задаёт фреймворк. Spring смотрит на классы и аннотации, а затем создаёт объекты, связывает их вместе и не даёт программе завершиться.

@Component class FileLogger {…}
@Component class Service {
@Autowired
FileLogger logger;
}

Меньше скучного кода
Низкая связность - код легко менять, тестировать и переиспользовать

Spring создаёт обёртки классов и работает через Dependency Injection. Можно и по-другому: через паттерн фабричный метод, стратегия или сервис локатор.

⚔️Историческая справка.
Сервис локатор иногда встречается в легаси проектах. Это когда компоненты создаются в классе ServiceLocator, а другие классы получают к ним доступ через статические методы.

class ServiceLocator {
private static Logger logger=…
public static Logger getLogger() {
return logger;
}
}

public class Service {
private Logger logger = ServiceLocator.getLogger();
}

Резюме:
🔸Dependency injection - класс не создаёт компоненты напрямую, они передаются через конструктор или сеттер
🔸Dependency invertion - класс работает с другими компонентами через интерфейс
🔸Inversion of Control - ход программы задаёт фреймворк. Соединять компоненты может Dependency injection, фабричный метод, стратегия или сервис локатор.

❗️Ответ на вопрос перед постом:
Это словоблудие относится к Dependency injection

#теория
👍4
IDEA: 4 метода для рефакторинга

IDEA - очень продвинутая IDE . Методов рефакторинга так много, что у них даже отдельная вкладка в меню. На каждом проекте точно пригодится:

1️⃣ Переименовать класс, метод, переменную или файл

Правой кнопкой по имени → Refactor → Rename
Имя изменится везде, где упоминается сущность.

2️⃣ Выделить константу

"Магические числа" в коде - плохая практика, лучше читаются именованные константы.

Правой кнопкой по числу → Refactor → Introduce Constant

 for(int i=0;i<100;i++)
for(int i=0;i<HLIMIT;i++)

3️⃣ Перенести код в отдельный метод

Выделяем нужные строки правый щелчок мышки Refactor → Extract Method...

И наоборот
4️⃣ Убрать лишние методы и переменные, "уплотнить" код

Правый щёлк → Refactor → Inline Method/Inline Variable
Switch: успеть до 30-ти

Сегодняшняя тема - история успеха оператора switch. Он появился в java 1.0, и с тех пор оставался в неизменном виде. В 2018 разработчики JDK смахнули пыль с кодовой базы switch, и теперь над ним идёт активная работа.

Почему о нём вспомнили, и как меняется switch? Рассмотрим по порядку.

1️⃣ Часть 1. Эпоха ООП

Java работает с объектами уже 25 лет. В этих условиях switch редко встречается в коде и часто считается плохой практикой.

Почему? Всё дело в сценарии использования:
switch (user.getState()) {
case NEW: …
case CONFIRMED: …
case BANNED: … }

Switch - это не просто набор нескольких if. У объекта user 3 статуса. Список чётко определен, статусы не пересекаются между собой.

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

Для объекта с понятным набором состояний switch лучше заменить на полиморфные методы. Это несложный рефакторинг - пример1, пример2.

Цель ООП - смоделировать реальный мир через объекты. Главное здесь - объекты взаимодействуют и меняют состояние друг друга. В таких условиях switch проигрывает полиморфным методам и редко используется.

2️⃣ Часть 2. Эра функциональности

Сегодня для бизнеса недостаточно простой автоматизации. Основной задачей становится работа с данными.

Они не меняются, приходят из разных источников в разных форматах. Потоки данных идут через множество сервисов. Каждый сервис берёт данные, которые понимает, а остальные игнорирует. Строить иерархии классов для такой задачи кажется лишним и сложным.

Поэтому внедряются подходы из фунциональных языков. Один из них - pattern matching, а switch идеально подходит для его реализации. Паттерн - некоторое условие для переменной:
▫️Равна заданной константе
▫️Имеет определённый тип
▫️Подходит под регулярное выражение

Если произошёл мэтч, то для переменной сразу доступна доп.информация. Например, она приводится к нужному типу:

switch (animal) {
case Cat c → c.putToBox();
case Dog d → d.train(); }

Итак, в чём разница между switch в 2000 и switch в 2021?

Switch 2000 работает с объектами, у которых меняется состояние. Вокруг этого строится бизнес-логика.

Switch 2021 работает с неизменными данными и помогает найти среди них подходящие. В следующем году выйдет java 17, и switch будет появляться в коде чаще.

Вот так один непопулярный оператор в JDK нашёл своё место в мире спустя 23 года⭐️
👍2
Спасибо за этот год!

Завтра новый год, это отличный повод сказать нечто важное.

Ребята, вы супер! Спасибо, что читаете мои нудные посты без картинок, помогаете найти ошибки и задаёте интересные вопросы. Благодаря вам блог ещё жив❤️

В 2020 году на канале вышло 85 постов, которые в сумме набрали 475к просмотров! Я в шоке и постараюсь в 2021 не сбавлять обороты.

Желаю всем в следующем году +1 грейд, интересные проекты и яркую жизнь вне работы🔥
Гороскоп на 2021

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

Вот что говорят звёзды про 2021 год:

♈️ Овен
Металлический Бык симпатизирует Овнам, поэтому все инициативы будут удачны, особенно зимой, весной, летом и осенью. Будьте активны на ретро, предлагайте новые фичи и подходы, возьмите под наставничество стажёров. В сентябре ожидайте наплыв писем от HR.

♉️ Телец
В год Быка Тельцы нацелены на быстрый карьерный взлёт. Подтяните пробелы и обсудите с тимлидом возможности роста. В этом году звёзды раскрутили ваш потенциал до максимума. В августе будьте осторожнее с git push --force.

♊️ Близнецы
Год будет спокойным и приятным. В прошлом году вы много работали, в 2021 выделяйте больше времени на отдых. Качество жизни и работы только улучшится. Самое время взяться за большие и фундаментальные книги, которые вы долго откладывали.

♋️ Рак
Откажитесь от лишней эмоциональности, она может помешать вашему развитию. Подтяните DevOps, в этом году он вам пригодится. Сложные задачи ждут вас в середине лета, но они дадут нужный стимул для дальнейшего роста.

♌️ Лев
В этом году удача не на вашей стороне, придётся много работать. Хотите успеха — начните сейчас, чтобы уже весной видеть первые результаты. Смотрите на вещи шире. Почитайте книжки по архитектуре, посмотрите видео с конференций HighLoad и ArchDays. В апреле высокий риск простудиться, одевайтесь теплее.

♍️ Дева
Год будет богат на свежие идеи и начинания. Начните то, что давно откладывали. Интересные идеи, вопросы и решения придут в самый неожиданный момент. Сохраняйте их сразу - запишите в блокнот, голосовое сообщение, как угодно, а то улетят. Обязательно делайте бэкапы и резервные копии.

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

♏️ Скорпион
В этом году вы будете на переднем фронте. Вас ждут горячие фиксы и спасение команды перед дедлайном. Будет сложно, но Сатурн вам поможет. Помните об отдыхе и набирайтесь сил в спокойное время.

♐️ Стрелец
Пересмотрите приоритеты в жизни, попробуйте смежные IT направления. Возможно позиция тимлида, менеджера или аналитика раскроют вас с новой стороны. В этом году особую важность приобретут межличностные отношения. Октябрь станет самым прибыльным месяцем в году.

♑️ Козерог
Для вас 2021 год — это борьба со своими слабостями. Уделяйте больше внимания тестированию и самопроверке. Разберитесь с NoSQL: книга для начинающих, для продолжающих. Хорошей идеей будет сходить на каток и покататься на ватрушках.

♒️ Водолей
Лучшее время для решительных шагов — начало весны. Много возможностей принесёт нетворкинг - поддерживайте тёплые отношения с коллегами, участвуйте в конференциях, митапах и корпоративных мероприятиях. Идите в ногу со временем - освойте Kotlin и Cloud computing.

♓️ Рыбы
Наступает период, когда пора применить все накопленные знания. А возможности для этого обязательно будут. Меркурий помешает сделать важные задачи в срок, поэтому закладывайте на выполнение в 2 раза больше времени. Лето подкинет массу интересных вариантов для отдыха.

Дружите со звёздами, они плохого не посоветуют💫
👍2
У аннотации Test определены все возможные Target. В какой строке будет ошибка компиляции (если будет)?
В какой строке будет ошибка компиляции (если будет)?
Anonymous Poll
16%
1
17%
2
4%
3
11%
4
35%
5
18%
Всё отлично скомпилируется
Аннотации, часть 1: обзор

Аннотации - дополнительная информация к исходному коду. @Override, @Deprecated, @SuppressWarnings - вот это всё.

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

Создать аннотацию легко:
public @interface MyAnnotation {}

Почему ключевое слово @интерфейс, а не @аннотейшн?

Во времена java 4 аннотаций не было и для дополнительной информации классу добавляли интерфейс-маркер. В интерфейсах Cloneable, Serializable, Remote нет методов, они используются только как дополнительный признак класса.

Подход рабочий, но похож на костыль. Цель интерфейса - показать контракт класса, поэтому для маркировки кода в java 5 ввели аннотации.

Вернёмся в наши дни. Посмотрим исходный код @Deprecated:

@Retention(RUNTIME)
@Target(value={FIELD,…})
public @interface Deprecated {

String since() default "";
boolean forRemoval() default false;

}

На этом примере видно, из чего состоит аннотация:

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

@Deprecated(since="14") 
// forRemoval по умолчанию false

🔸Список внутри @Target показывает элементы, для которых работает аннотация.
В java 7 аннотации доступны для классов, методов, параметров, полей и переменных.

В java 8 аннотации действуют везде, где указан тип. Можно писать даже такое:

▫️new @Test Account()
▫️throws @Test IOException
▫️implements @Test Comparable<@Test
T>

Такие аннотации называются type annotations и используются в IDE и компиляторах для анализа и строгого контроля типов.

Аннотации нельзя ставить для имён переменных. Правильный ответ на вопрос перед постом - ошибка в 5 строке:
@Test String doubled
String @Test out

🔸@Retention определяет, когда доступна аннотация и как её можно использовать. Все виды подробно рассмотрим во второй части.

@Target и @Retention- это мета аннотации, то есть аннотации для аннотаций.

Какие ещё бывают мета-аннотации:

🔸@Documented - аннотация появится в JavaDoc
🔸@Inherited - наследуется подклассами
🔸@Repeatable (Java 8) - можно использовать несколько раз для одного элемента. Иногда такое приятнее читать, чем один массив:

@Schedule(dayOfMonth="last")
@Schedule(dayOfWeek="Fri", hour="23")

Создать аннотацию легко, правильно применить - уже сложнее. С этим вопросом разберёмся во второй части.
1
Аннотации, часть 2: как использовать

Продолжим вчерашнюю тему. Рассмотрим Retention, и когда пригодится самодельная аннотация.

@Retention определяет, на каком этапе доступна аннотация:
🔹 SOURCE - аннотация видна только во время компиляции
🔹 CLASS - доступна также в байт-коде
🔹 RUNTIME - видна всегда, даже во время работы программы

Доступность выбирается исходя из цели. Поэтому перейдём к кейсам.

Что можно сделать через аннотации?

1️⃣ Объединить уже существующие.
Самый популярный и простой случай. Если в проекте несколько аннотаций часто идут вместе, объедините их в одну. Если у всех компонентов есть обработчики, то больше ничего делать не надо, всё заработает само.

Пример: @SpringBootApplication - это комбинация @Configuration, @EnableAutoConfiguration и @ComponentScan.

2️⃣ Генерация кода и файлов.
Происходит на этапе компиляции:
🔸 Отмечаем код аннотацией
🔸 Создаём класс-наследник от AbstractProcessor. Определяем, на какие аннотации реагировать
🔸 Вытаскиваем дополнительную информацию через .class

Deprecated d = Account.class. getAnnotation(Deprecated.class)

🔸 Делаем что-то полезное
🔸 Включаем процессор в компиляцию. В maven-compiler-plugin это секция annotationProcessors.

Для обработки подойдут аннотации с любой RetentionPolicy.

Что получаем:
Долгая компиляция
Специфичное тестирование. Пример
😐 Сложный и запутанный код
Не тратится время на старте приложений

Этот подход используется в библиотеке Lombok, микрофреймворках Quarkus и Micronaut, в Android фреймворке Dagger.
Библиотека Dekorate на основе аннотаций создаёт манифесты для Kubernetes и OpenShift.

3️⃣ Статический анализ.
Алгоритм такой же - создать наследник от AbstractProcessor, добавить в процесс компиляции.

Примеры: библиотека Google Error Prone ищет в коде ошибки, Hibernate Validator проверяет, что аннотации Hibernate корректно расставлены.

4️⃣ Работа с байт-кодом.
Редкий случай. В байт-коде остаются аннотации с RetentionPolicy.CLASS или RUNTIME.

5️⃣ Создать объекты и прокси-классы.
Доступно для аннотаций с RetentionPolicy.RUNTIME.

Основа работы многих фреймворков: Spring, Hibernate, Java EE. Под работу с аннотациями в рантайме заточены многие библиотеки: Reflections, Spring. Работать с ними удобно и приятно.

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

Теперь рассмотрим анти-кейсы, для чего аннотации НЕ нужны:

1️⃣ Отметить код для себя или команды.
Поставить аннотацию @RefactorASAP легко, но без дальнейших действий это бесполезно. Аннотации нужно обрабатывать, и делать это автоматически.

Чтобы запомнить место в коде, используйте TODO комментарии в Intellij IDEA.

2️⃣ Для бизнес-логики.
Аннотации легко добавить в обход ООП и основной логики. Так можно быстро решить проблему, но долгосрочно это неудачный вариант:

Нельзя контролировать процесс целиком
Сложно писать тесты
Сложно дебажить
Внезапные сайд-эффекты

Частая ситуация на Spring проектах: на старте запускаются десятки @PostConstruct. Если в процессе возникает ошибка, то найти и исправить её непросто.

Но вообще, чем меньше вы полагаетесь на аннотации, тем лучше.
👍31
Stream API: новые методы в Java 16

16 марта вышла java 16. Новые фичи входят во вторую превью стадию перед главным релизом 2021 - java 17 LTS.

Существующие классы тоже развиваются. В java 16 в Stream API появилось 4 новых метода, которые мы и рассмотрим в этом посте.

1️⃣ toList()

С 25 по 27 ноября была серия постов о коллекторах (часть 1, часть 2, часть 3). Там я писала, что вместо

collect(Collectors.toList())
было бы удобно писать просто
toList()

30 ноября разработчик Oracle добавил метод toList() в класс Stream. Вряд ли он читает этот канал, но совпадение интересное🙂

Новый метод не совсем равнозначный:
▫️Collectors.toList() возвращает экземпляр ArrayList.
▫️Новый метод toList() возвращает неизменяемый список.

2️⃣ mapMulti

Это оптизированный flatMap. Объясню суть на примере. Заказ - класс Order, товар - класс Item. Заказ состоит из нескольких товаров. Из списка заказов хотим получить список всех товаров.

orders.stream()
.flatMap(order->order.getItems().stream())
.toList();

flatMap переводит "список списков" в один список. Товары для каждого заказа превращаются в Stream, а метод flatMap объединяет эти стримы в один.

Минус: объект стрима создаётся всегда, даже для пустых списков.

mapMulti устраняет этот недостаток:

orders.stream()
.<Item>mapMulti((order, consumer) ->
order.getItems().forEach(item -> consumer.accept(item))
).toList();

Что происходит:
mapMulti принимает на вход (order, consumer):
▪️order - элемент стрима, в нашем случае - заказ
▪️consumer - следующий этап в стриме. Наша задача - передать этому этапу все будущие элементы. Берём у заказа товары и для каждого вызываем consumer.accept(item).

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

<Item>mapMulti(…)

Когда использовать mapMulti?

Небольшое количество элементов в списках.
Например, много заказов с 1-2 товарами

Элементы легко получить без Stream API
Основная фишка mapMulti - нет промежуточных стримов. Если внутри метода создаётся стрим, то вся выгода сходит на нет.
order.getItems().stream()…

Есть три вариации метода:
🔸IntStream mapMultiToInt
🔸LongStream mapMultiToLong
🔸DoubleStream mapMultiToDouble

Для них выходной тип не указывается.
1