Java Geek
2.45K subscribers
272 photos
1 file
24 links
Практичные советы, лайфхаки и код для Java-разработчиков. Каждый пост — реальная польза. Учим Java на примерах.

По всем вопросам @evgenycarter
Download Telegram
🕵️ Java var: Удобный сахар или скрытая угроза?

С выходом Java 10 ключевое слово var позволило нам писать меньше кода. var - это Local Variable Type Inference. Это значит, что Java осталась строго типизированным языком, просто теперь компилятор сам догадывается о типе переменной, глядя на то, что находится справа от знака равно (=).

Но иногда его догадки могут вас удивить. Вот 3 примера, где var работает неочевидно.

1. Ловушка "Diamond Operator" (<>)

Самая частая ошибка новичков.


// Без var (Классика)
List<String> list = new ArrayList<>();
// Компилятор видит слева List<String> и понимает, что справа тоже String.

// С var (Ошибка)
var list = new ArrayList<>();



В чем проблема?
У компилятора нет информации о типе. Он видит пустые скобки <> и решает, что это ArrayList<Object>.
В итоге вы теряете типизацию:


list.add("Hello");
list.add(123); // Это сработает, хотя вы, вероятно, хотели только строки!



Как исправить:
Если используете var с конструктором, всегда указывайте тип справа:


var list = new ArrayList<String>();



2. Магия "Пересечения типов" (Intersection Types)

А вот это уже высший пилотаж, который часто встречается при использовании List.of() или Map.of().

Что будет, если сложить в список данные разных типов?


var magicList = List.of(10, 20.5, "30");
// Какой тут тип списка? List<Object>?



На самом деле компилятор выведет наиболее специфичный общий тип.
Тип переменной magicList будет выглядеть примерно так:
List<? extends Serializable & Comparable<? extends Serializable & Comparable<?>>>

Java находит общие интерфейсы для Integer, Double и String (все они реализуют Serializable и Comparable) и создает этот ужасный тип-франкенштейн. Это работает, но может свести с ума вашу IDE или методы, принимающие конкретные типы.

3. Анонимные классы на стероидах

var позволяет делать трюк, невозможный ранее: сохранять тип анонимного класса.


var user = new Object() {
String name = "Alex";
int age = 25;
};

// Это работает!
System.out.println(user.name);
System.out.println(user.age);



Если бы мы написали Object user = ..., поля name и age были бы недоступны. А var "видит" реальную структуру анонимного объекта. Это иногда полезно для локальных промежуточных вычислений, заменяя DTO или кортежи.

🧠 Золотое правило использования

var хорош тогда, когда тип очевиден из правой части:

🔴👍 var users = Map.of("id", 1); (Понятно, что это Map)

🔴👍 var stream = list.stream(); (Понятно, что Stream)

🔴👎 var result = service.process(); (Что вернулось? boolean? User? null?)

Читаемость кода важнее краткости!

#Java #CleanCode #Var #Java10

👉 @java_geek
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥4👍21
💿 Java Records: Конец эпохи Lombok?

Долгие годы Project Lombok был нашим единственным спасением от бесконечных геттеров, сеттеров, equals() и hashCode(). Но начиная с Java 14 (и официально в Java 16), в языке появилась нативная альтернатива - Records.

Многие поспешили удалить Lombok и переписать всё на рекорды. И... столкнулись с проблемами. Давайте разберем, почему record - это не просто "короткий класс".

🏎 Скорость написания

Lombok (@Value для неизменяемости):


@Value
public class User {
String name;
int age;
}



Record:


public record User(String name, int age) {}



Тут победа за рекордами. Синтаксис максимально лаконичен. Мы объявляем состояние, а не поля.

🔍 Что под капотом? (Главные отличия)

Вот здесь начинаются нюансы, из-за которых Records нельзя назвать полной заменой Lombok.

1. Жесткая неизменяемость (Immutability)
Record - это всегда final класс с final полями. Вы не можете сделать "сеттер" в рекорде. Если вам нужен изменяемый DTO (например, для заполнения формы по шагам) - Record не подойдет. Lombok @Data всё еще нужен.

2. Запрет на наследование
Рекорды не могут наследоваться от других классов (они уже неявно наследуются от java.lang.Record).

• Если у вас есть BaseEntity с полем ID - вы не сможете унаследовать рекорд от него.

3. Имена геттеров
Это ломает многие старые библиотеки.

• Lombok/JavaBean: getName(), getAge()
• Record: name(), age()
Совет: Jackson и современные JSON-библиотеки уже умеют с этим работать, но легаси-фреймворки могут "не увидеть" ваши поля.

Killer Feature: Компактные конструкторы

То, чего нет в Lombok. Валидация данных в рекордах выглядит потрясающе чисто. Вам не нужно дублировать аргументы:


public record User(String name, int age) {
// Обратите внимание: нет скобок с аргументами!
public User {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
name = name.toUpperCase(); // Можно даже менять данные перед записью
}
}



🏆 Вердикт: Кто победил?

Никто. Это инструменты для разных целей.

Используйте record: Для DTO, ключей в Map, ответов API, записей в Event Log и промежуточных данных в Stream API. Это "именованные кортежи" данных.

Используйте Lombok / POJO: Для JPA сущностей (Hibernate не очень любит рекорды из-за прокси и отсутствия сеттеров), для классов с наследованием и там, где нужна мутабельность.

#Java #Lombok #CleanCode #Java17

👉 @java_geek
👍21🔥1
🔒 Java Sealed Classes: Диктатура в вашей иерархии

Раньше в Java у нас было всего два стула для классов:

1. Public: Наследуйся кто хочет (открытый проходной двор).
2. Final: Никто не пройдет (полная изоляция).

Но что, если я хочу разрешить наследование только моим классам Circle и Square, но запретить Васе из соседнего отдела создавать свой кривой Triangle?
Начиная с Java 17, у нас есть Sealed Classes.

🚧 Как это работает?

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


public sealed interface PaymentResult
permits Success, Failure, Pending {
}



Теперь компилятор гарантирует: в мире существуют только три вида PaymentResult. Четвертого не дано.

🤝 Идеальная пара: Sealed + Records

Чаще всего наследниками делают record, потому что они идеально подходят для хранения данных.


public record Success(String txId) implements PaymentResult {}
public record Failure(String error) implements PaymentResult {}
public record Pending(long timestamp) implements PaymentResult {}



🧠 Главная фишка: Умный Switch (Pattern Matching)

Зачем нам эти ограничения? Ради исчерпываемости (Exhaustiveness).
Когда вы используете sealed классы в новых switch (Java 21+), компилятор знает все возможные варианты.

Вам не нужно писать default ветку!


String message = switch (result) {
case Success s -> "Paid! ID: " + s.txId();
case Failure f -> "Error: " + f.error();
case Pending p -> "Wait...";
// Нет default! И это безопасно.
};



В чем магия? Если через полгода вы добавите в permits новый вариант Cancelled, ваш код перестанет компилироваться везде, где используется этот switch. Компилятор ткнет вас носом: "Ты забыл обработать новый статус!". Это спасает от сотен багов в сложной бизнес-логике.

📜 Три правила для наследников

Наследник sealed класса обязан выбрать одну из трех стратегий:

1. final: На мне иерархия заканчивается (как в Records).
2. sealed: Я продолжаю жесткий контроль, вот мои наследники.
3. non-sealed: Я открываю шлюзы - от меня может наследоваться кто угодно (возврат к старому поведению).

🚀 Итог: Используйте Sealed Classes, когда ваша модель данных представляет собой конечное множество вариантов:

🔴Статусы заказа
🔴Типы пользователей (Admin, User, Guest)
🔴Результаты операций (Success, Error)

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

#Java17 #Architecture #CleanCode #PatternMatching

👉 @java_geek
Please open Telegram to view this post
VIEW IN TELEGRAM
5👍2
🏗 Java: Структурная конкурентность. Прощайте, зомби-потоки!

Допустим, вы используете Виртуальные потоки (Project Loom), чтобы сделать два независимых запроса: получить данные пользователя (API 1) и его заказы (API 2).

Раньше мы использовали ExecutorService или CompletableFuture. Но у них есть огромная архитектурная дыра: они ничего не знают друг о друге.

🧟‍♂️ Проблема зомби-потоков (Unstructured Concurrency)

Если API 1 мгновенно падает с ошибкой 500, ваш метод все равно будет ждать, пока API 2 доработает (или упадет по таймауту). Поток, качающий заказы, становится "сиротой". Он делает бесполезную работу, тратит сеть и память, хотя результат уже никому не нужен.

А если ошибку выкинет родительский метод? Дочерние потоки продолжат жить своей жизнью в фоне. Это хаос.

🧩 Решение: StructuredTaskScope

В современной Java (начиная с 21 версии) потоки привязали к лексической области видимости - блоку кода. Если мы выходим из блока (из-за ошибки или успешного завершения), все запущенные внутри него дочерние потоки автоматически отменяются (получают interrupt).

Вот как выглядит "Запрос-Ответ", где должны выполниться оба действия (Стратегия *All or Nothing*):


// ShutdownOnFailure: если один упал, отменяем остальные
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {

// Запускаем подзадачи (Subtasks)
Subtask<User> user = scope.fork(() -> fetchUser(id));
Subtask<List<Order>> orders = scope.fork(() -> fetchOrders(id));

scope.join(); // Ждем завершения обеих задач...
scope.throwIfFailed(); // ...или первой же ошибки!

// Сюда дойдем, только если обе задачи успешны
return new UserProfile(user.get(), orders.get());
}
// При выходе из блока любые зависшие потоки будут убиты



🏎 Стратегия "Кто первый, тот и прав"

А что, если вам нужно получить курс валют, и у вас есть 3 разных провайдера? Вам нужен ответ от любого, кто ответит быстрее.


// ShutdownOnSuccess: первый успешный отменяет остальные
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<Quote>()) {

scope.fork(() -> getFromBankA());
scope.fork(() -> getFromBankB());
scope.fork(() -> getFromBankC());

scope.join(); // Ждем первого успешного

return scope.result(); // Возвращаем самый быстрый ответ
}



Как только Банк А ответит, запросы к Банкам B и C будут немедленно отменены. Никакого ручного управления Future.cancel(). Всё работает из коробки.

🧠 Почему это меняет всё?

1. Читаемость: Многопоточный код читается сверху вниз, как обычный синхронный.
2. Безопасность ресурсов: Утечки потоков физически невозможны. Структура гарантирует, что родитель не завершится, пока не разберется со всеми детьми.
3. Идеальные логи: Стек-трейс теперь показывает реальную иерархию (кто кого вызвал), а не обрывается на внутренностях пула потоков.

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

#Concurrency #ProjectLoom #CleanCode #Backend

👉 @java_geek
👍4🔥43