ru
Feedback
Java | Фишки и трюки

Java | Фишки и трюки

Открыть в Telegram

Java: примеры кода, интересные фишки и полезные трюки Купить рекламу: https://telega.in/c/java_tips_and_tricks ✍️По всем вопросам: @Pascal4eg Менеджер по рекламе: @shmyzna

Больше
6 954
Подписчики
-724 часа
+117 дней
+830 день
Архив постов
🗑Внутренности JVM — кто убирает мусор и как JVM — это не просто виртуальная машина. Это механика управления памятью, где каждый сборщик мусора решает свои задачи и влияет на производительность. Разберёмся👇Антипаттерн: полагаться на default GC без понимания
// просто запускаем JVM и забываем
⚠️ Разные приложения — разные потребности: latency vs throughput. ➡️ Выбор GC = контроль над паузами и скоростью. ✅ Мини-примеры для понимания GC 👍 Создание большого массива объектов
public class GCDemo {
    public static void main(String[] args) {
        for (int i = 0; i < 1_000_000; i++) {
            byte[] block = new byte[1024 * 1024]; // 1MB
        }
        System.out.println("Создано много объектов");
    }
}
➡️Позволяет увидеть работу сборщика мусора, если включить лог GC:
-XX:+PrintGCDetails
👍Проверка поведения G1
public class G1Demo {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            int[] arr = new int[10_000_000]; // ~40MB
            Thread.sleep(50); // медленная генерация объектов
        }
        System.out.println("Демонстрация работы G1");
    }
}
➡️ JVM с G1 будет постепенно очищать регионы, минимизируя паузы. 👍 Принудительный вызов GC (для экспериментов, не для продакшена)
System.gc();
⚠️Позволяет проверить, как JVM реагирует, но не гарантирует моментальный сбор. 🗣 Запомни: Сборщик мусора — это механика JVM, а не магия. Выбор GC, размер heap и режим работы напрямую влияют на паузы и производительность. Этот код поможет увидеть GC в действии и понять разницу между Serial, Parallel и G1.

💻 Java Memory Model (JMM) - не просто volatile, а гарантии видимости между потоками Многие воспринимают volatile и synchronized как «какие-то ключевые слова», но на самом деле это контракт между потоками и процессором. Контракт простой: ➡️Изменения одного потока должны быть видимы другим потокам, иначе код ломается на кэшах и reorder’ах. Разберёмся👇 ❌ Антипаттерн: обычная переменная между потоками
class Counter {
    int count = 0;
    
    void increment() { count++; }
}

Counter counter = new Counter();

Runnable r = () -> {
    for(int i = 0; i < 1000; i++) counter.increment();
};
new Thread(r).start();
new Thread(r).start();
⚠️ Без volatile или synchronized значения могут теряться, потому что потоки кэшируют count локально. ➡️Даже ++ не атомарен - CPU может reorder инструкции. ✅ Как правильно 👍 volatile - видимость изменений
class Counter {
    volatile int count = 0;

    void set(int value) { count = value; } // запись видна другим потокам
    int get() { return count; }           // чтение гарантированно актуально
}
👍 synchronized - атомарность + видимость
class Counter {
    private int count = 0;

    synchronized void increment() { count++; }
    synchronized int get() { return count; }
}
➡️ Любая синхронизированная операция создаёт happens-before между потоками. 👍happens-before - фундамент JMM 😱 volatile write → volatile read Гарантирует, что запись видна всем потокам после прочтения 😱synchronized unlock → lock Выход из synchronized у одного потока → вход в synchronized у другого 😱 Thread start / join start() → run() run() → join() ⚠️ Без этих правил видимость не гарантируется, даже если код компилируется и выглядит «правильно». ❌ Пример ломающегося кода без volatile (CPU кэши)
class Flag {
    boolean running = true;
}

Flag flag = new Flag();

new Thread(() -> {
    while(flag.running) {
        // ждем события
    }
    System.out.println("Stopped");
}).start();

// другой поток через 1 секунду
flag.running = false;
⚠️ Поток в while может никогда не увидеть изменение из-за кэширования процессора. ➡️ Решение — volatile boolean running. 🗣️ Запомни: JMM — это не про синтаксис, это про гарантии между потоками и CPU. 👍volatile → видимость 👍synchronized → атомарность + видимость 👍happens-before → контракт, гарантирующий порядок Используй эти инструменты не ради ключевых слов, а чтобы код был предсказуемым и безопасным в многопоточности.

Почему при зарплате 400к денег всё равно нет? Замечал, как люди с доходом 80к умудряются копить на отпуск и покупать айфоны? А у многих айтишников при 300-400к — в конце месяца ноль. Знакомо? Есть 5 конкретных причин, почему так происходит при высоком доходе. Снял видео — разбираю каждую. 5 минут, без воды. Посмотри, возможно узнаешь себя или коллег! Смотреть #реклама 16+ sbsite.pro О рекламодателе

💻ThreadLocalRandom — не просто Random, а быстрый для многопоточки Используешь new Random() или Math.random() в многопоточном коде? Это не только медленно, но и создаёт конфликты потоков, снижая производительность. Разберёмся👇 ❌ Антипаттерн: общий Random для всех потоков
// Общий инстанс Random — синхронизация под капотом!
static Random globalRandom = new Random();

int randomNum = globalRandom.nextInt(100); // Потоки дерутся за монитор
⚠️ Под капотом Random синхронизирован ➡️ В многопотоке — contestion, падение скорости. ✅ Правильный способ (Java 7+)
int randomNum = ThreadLocalRandom.current().nextInt(100);
😀 Что происходит: 👍Каждый поток получает свой генератор 👍Нет синхронизации → нет contention 👍Скорость в разы выше в многопоточной среде 👍API такой же, есть удобные методы: nextInt(origin, bound), nextLong() и др. 👾 Для ForkJoinPool и параллельных стримов
int[] numbers = ThreadLocalRandom.current()
    .ints(1000, 0, 100)
    .parallel()
    .toArray();
➡️ Быстро, параллельно, без блокировок. ⚠️Ловушки и советы 👍Не создавай новый инстанс каждый раз — используй current() 👍Не передавай между потоками — потеряется смысл 👍Для криптографии только SecureRandom 🗣️ Запомни: ThreadLocalRandom — это дефолтный выбор для многопоточной генерации. Меньше блокировок, больше скорость. Просто работает.

💻 CompletableFuture в Java - не просто get(), а другой уровень асинхронности Многие воспринимают CompletableFuture как «асинхронный Future» и используют только get(). На самом деле это мощный инструмент для построения цепочек и обработки ошибок. Контракт простой: ➡️ Операции выполняются асинхронно — и ты обязан обрабатывать результат, ошибки и таймауты. Разберёмся👇 ❌ Антипаттерн: get() ради get()
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello");
String result = future.get(); // блокирует поток
⚠️ Это тот же синхронный вызов, только с обёрткой. ➡️ Если ты пишешь get() без нужды - CompletableFuture теряет смысл. ✅ Как правильно 👍 thenApply() - трансформация результата
future.thenApply(String::toUpperCase)
      .thenAccept(System.out::println);
👍 thenCompose() - для вложенных CompletableFuture
future.thenCompose(this::callAnotherService);
👍 allOf() / anyOf() - комбинаторы
CompletableFuture.allOf(f1, f2, f3)
                 .thenRun(() -> System.out.println("Все завершены"));
👍 handle() / exceptionally() - обработка ошибок
future.handle((res, ex) -> res != null ? res : "fallback");
future.exceptionally(ex -> "ошибка: " + ex.getMessage());
👍 complete() / obtrudeValue() - ручное завершение
future.complete("готово");
future.obtrudeValue("принудительно");
👍 Кастомный Executor - контроль потоков
ExecutorService executor = Executors.newFixedThreadPool(5);
CompletableFuture.supplyAsync(() -> "task", executor);
⚠️ Особенности и осторожностиget() - блокирует поток, может выбросить ExecutionException ⚠️ join() - не требует checked исключений, но может бросить CompletionException ➡️ Используй then*() цепочки для неблокирующей работы. ➡️ Таймауты обрабатывай через orTimeout() или completeOnTimeout(). 🗣️ Запомни: CompletableFuture - это не просто Future. Это способ сказать коду: «результат будет, ошибки будут, таймауты будут - разберись с этим прямо сейчас и не блокируй поток».

⌨️ Helpful NullPointerException: Java наконец-то «заговорила» Раньше NullPointerException (NPE) был похож на игру в «Сапёра». Вы получали ошибку: NullPointerException at com.app.Service.process(Service.java:45). Смотрим в код на строку 45:

person.getAddress().getCountry().getIsoCode().toLowerCase();
👀 Кто здесь null? person? Address? Country? IsoCode? Приходилось разбивать строку на части или запускать дебаггер, чтобы понять, где именно произошел взрыв. 💡 Как это работает сейчас (Java 14+): Java научилась анализировать байт-код в момент падения и формировать информативное сообщение. Теперь в логах вы увидите:
Exception in thread "main" java.lang.NullPointerException: 
Cannot invoke "Country.getIsoCode()" because the return value of "Address.getCountry()" is null
🗣 Java прямым текстом говорит: «Я не могу вызвать getIsoCode(), потому что метод getCountry() вернул null». Эта фича включена по умолчанию в современных версиях Java. Мелочь, а как приятно!

⌨️ Compact Strings: Как String похудел в 2 раза Строки занимают огромное количество места в куче (Heap) любого приложения. До Java 9 устройство строки выглядело примерно так:

public final class String {
    private final char[] value; // Массив символов
}
В чем была проблема? Тип char в Java занимает 2 байта (UTF-16). Это нужно, чтобы хранить иероглифы, эмодзи и сложные символы. Но статистика показала, что 90% строк в западных приложениях — это обычная латиница, цифры и спецсимволы (ASCII / ISO-8859-1). Для хранения буквы 'A' (которая весит 1 байт) мы тратили 2 байта: 00 41. Один байт просто заполнялся нулями. Это чудовищная трата памяти. 🛠 Что сделали в Java 9 (Compact Strings): Разработчики JDK переписали класс String. Теперь он выглядит так:

public final class String {
    private final byte[] value; // ❗️ Теперь это массив байтов
    private final byte coder;   // Флаг кодировки
}
Как это работает? Когда вы создаете строку, Java проверяет её содержимое: 1️⃣ Режим LATIN1: Если в строке только символы, помещающиеся в 1 байт (английский, цифры, стандартные знаки), флаг coder ставится в 0, и строка занимает 1 байт на символ. 2️⃣ Режим UTF16: Если встречается хоть одна кириллица, иероглиф или эмодзи, флаг coder меняется, и Java использует 2 байта на символ (как раньше). Результат: Если ваше приложение гоняет JSON-ы, HTTP-заголовки или технические логи (а они обычно на английском), потребление памяти под строки сокращается ровно в 2 раза автоматически. Без переписывания кода.

🔴 Завтра тестовое собеседование с Java-разработчиком 24 декабря(уже завтра!) в 19:00 по мск приходи онлайн на открытое собес
🔴 Завтра тестовое собеседование с Java-разработчиком 24 декабря(уже завтра!) в 19:00 по мск приходи онлайн на открытое собеседование, чтобы посмотреть на настоящее интервью на Middle Java-разработчика. Как это будет: 📂 Сергей Чамкин, старший разработчик из Uzum, ex-WildBerries, будет задавать реальные вопросы и задачи разработчику-добровольцу 📂 Cергей будет комментировать каждый ответ респондента, чтобы дать понять чего от вас ожидает собеседующий на интервью 📂 В конце можно будет задать любой вопрос Сергею Это бесплатно. Эфир проходит в рамках менторской программы от ШОРТКАТ для Java-разработчиков, которые хотят повысить свой грейд, ЗП и прокачать скиллы. Переходи в нашего бота, чтобы получить ссылку на эфир → @shortcut_sh_bot Реклама. О рекламодателе.

⌨️ Внутренности HashMap: Когда список превращается в дерево Все знают, что HashMap работает на основе хеширования. Мы кладем ключ, вычисляется хеш, и элемент попадает в определенную ячейку (bucket) массива. Но что происходит, если у многих ключей совпадает хеш (коллизия)? 🐢 До Java 8 (Эпоха LinkedList): Все элементы с одинаковым хешем выстраивались в простой связный список (LinkedList) внутри одной ячейки. Если у вас плохой алгоритм хеширования, поиск элемента в такой мапе превращался из мгновенного O(1) в медленный перебор списка O(n). Это могло «повесить» сервер. ⚡️С Java 8 (Эпоха Red-Black Tree): Разработчики JDK внедрили механизм Treeification (одеревенение). Как только в одной ячейке (bucket) скапливается слишком много элементов, Java автоматически превращает этот простой список в Красно-Черное дерево (Red-Black Tree). Магия чисел: Порог 8 (TREEIFY_THRESHOLD): Если в цепочке становится 8 или больше элементов - список превращается в дерево. Порог 6 (UNTREEIFY_THRESHOLD): Если при удалении элементов их становится меньше 6 - дерево разжалуется обратно в список (чтобы экономить память, так как дерево занимает больше места). 🌳 Как HashMap сравнивает ключи в дереве (Tree Bin) Когда корзина превращается в Красно-Черное дерево, Java нужна сортировка, чтобы работала навигация (бинарный поиск). Но ключи могут быть любыми объектами. Вот иерархия проверок, которую использует HashMap, чтобы понять, «больше» ключ или «меньше» текущего узла: 1️⃣ Сравнение хешей (hash): Даже если ключи попали в одну корзину (индекс массива), их полные 32-битные хеши могут отличаться. Пример: У нас корзина №5. Туда попал ключ с хешем 5 и ключ с хешем 21 (при размере массива 16: 5 % 16 = 5, 21 % 16 = 5). HashMap видит: 5 < 21. Ага, значит, идем влево или вправо. Это самая частая проверка. 2️⃣ Интерфейс Comparable: Если полные хеши абсолютно одинаковы (это редкая коллизия), Java проверяет: «А реализует ли ключ интерфейс Comparable?». Если да (например, ключи это String или Integer), то используется их метод compareTo(). Это позволяет идеально рассортировать элементы внутри дерева. 3️⃣ Последний шанс (tieBreakOrder): Если хеши одинаковы и ключи НЕ реализуют Comparable (или сравнение не удалось), Java использует System.identityHashCode() (адрес в памяти) или имя класса, чтобы хоть как-то их упорядочить. Это «аварийный» метод, просто чтобы дерево оставалось деревом и не ломалась структура. 🚀 Почему это быстрее? В обычном списке (LinkedList) вы вызываете equals() для каждого элемента (100 элементов = 100 проверок). В дереве (Red-Black Tree), используя правила выше (хеш > comparable > identity), мы на каждом шаге отсекаем половину вариантов. Мы смотрим на корень: наш хеш меньше? Идем влево. (Правая ветка со всеми ее элементами сразу отбрасывается). Смотрим следующий узел: больше? Идем вправо. В итоге, даже если в корзине 1000 элементов, в списке мы бы сделали 1000 вызовов equals. А в дереве мы спустимся по веткам примерно за 10 шагов (log2(1000) ≈ 10). Итог: equals() всё равно вызывается, но только когда мы уже нашли кандидата (или идем по пути с абсолютно идентичными хешами). Мы не "опрашиваем" всех соседей подряд.

Привет. Вот тебе самые топовые каналы по IT! ⚙️ Free Znanija (IT) — Самая огромная коллекция платных курсов, которые можно скачать бесплатно; 👩‍💻 IT Books — Самая огромная библиотека книг; 💻 Hacking & InfoSec Base — Крутой блог белого хакера; 🛡 CyberGuard — Всё про ИБ; 🤔 ИБ Вакансии — Всё, чтобы найти работу в ИБ; 👩‍💻 linux administration — Всё про Линукс; 👩‍💻 Программистика — Python, python и ещё раз python; 👩‍💻 GameDev Base — Всё про GameDev; 😆 //code — Самые топовые мемы по IT: Подпишись, чтобы не потерять!

⌨️ Java Unnamed Variables: Сила символа _ Все мы сталкивались с ситуацией, когда синтаксис требует объявить переменную, но она нам совершенно не нужна. 😒 Как мы выкручивались раньше: Приходилось придумывать имя переменной, чтобы компилятор был доволен, а потом IDE ругалась: "Variable 'e' is never used".

try {
    int number = Integer.parseInt(input);
} catch (NumberFormatException e) { // <-- Зачем нам 'e'?
    // Мы и так знаем, что это не число, детали ошибки нам не важны
    System.out.println("Это не число!");
}
Или в циклах:

for (var s : list) { // <-- Нам нужно просто посчитать количество, 's' не нужна
    count++;
}
Как стало с Java 22 (Unnamed Variables): Теперь можно использовать символ подчеркивания _. Это сигнал компилятору: "Здесь должна быть переменная, но я не собираюсь её использовать".

try {
    int number = Integer.parseInt(input);
} catch (NumberFormatException _) { // Красота!
    System.out.println("Это не число!");
}
Или в паттерн-матчинге (для instanceof и switch), если нам важен только тип, а не само значение:

if (obj instanceof String _) {
    System.out.println("Да, это строка (но читать её я не буду)");
}
🔥 Почему это круто? 1️⃣ Чистота намерений: Читая код, другой разработчик сразу понимает: эта переменная игнорируется намеренно, а не по ошибке. 2️⃣ Спокойствие IDE: Анализаторы кода больше не спамят предупреждениями "Unused variable". 3️⃣ Меньше когнитивной нагрузки: Не нужно придумывать имена вроде ignored, unused или dummy.

🔥 БЕСПЛАТНЫЙ КУРС ПО СОЗДАНИЮ НЕЙРО-СОТРУДНИКОВ НА GPT И ДРУГИХ LLM 🔥 Ищете практический и углубленный курс, чтобы освоить
🔥 БЕСПЛАТНЫЙ КУРС ПО СОЗДАНИЮ НЕЙРО-СОТРУДНИКОВ НА GPT И ДРУГИХ LLM 🔥 Ищете практический и углубленный курс, чтобы освоить создание нейро-сотрудников? Мы создали курс из 5 объемных занятий. Это именно то, что нужно, чтобы прокачать свои навыки абсолютно бесплатно! 📌 Темы занятий: 1. Введение в мир нейро-сотрудников 2. Как работают LLM и их аналоги 3. Создание базы знаний для нейро-сотрудника (RAG) 4. Тестирование и отладка нейро-сотрудников 5. Интеграция нейро-сотрудников в Production Вот 5 тем курса - он максимально простой и доступный, общеобразовательный, без какого-либо сложного программирования 📚Прохождение этого курса, скорее всего, займет у вас от 1 до 3 часов 🤖 Присоединяйтесь к нашему бесплатному курсу и разберитесь в этой увлекательной теме с нами!

⌨️ Sequenced Collections: Конец мучений с list.get(size - 1) Признайтесь, сколько раз за свою карьеру вы писали этот уродливый код, чтобы достать последний элемент списка? 😫 Старая школа (до Java 21):

var list = List.of("A", "B", "C");

// Чтобы взять последний элемент:
String last = list.get(list.size() - 1); 

// А если это Set? (LinkedHashSet)
// Приходилось использовать итератор или перегонять в список... 😱
String lastInSet = set.iterator()... // Ой, всё, лень писать.
Это было неудобно, нечитаемо и чревато ошибками (привет, -1). 🥳 Java 21 (Sequenced Collections): В Java наконец-то добавили общий интерфейс для всех коллекций, у которых есть порядок элементов — SequencedCollection. Теперь у List, Deque, SortedSet и LinkedHashSet появились единые методы:

list.getFirst();  // Взять первый
list.getLast();   // Взять последний (Наконец-то!)

list.addFirst("Z"); // Добавить в начало
list.addLast("X");  // Добавить в конец

list.reversed();    // Получить представление коллекции в обратном порядке
🔥 Почему это круто? 1️⃣ Единый стандарт. Раньше у Deque были методы getFirst, у Listget(0), у SortedSetfirst(). Теперь везде одинаково. 2️⃣ Работает с Set. Теперь можно легко взять первый или последний элемент из LinkedHashSet или TreeSet, не прибегая к итераторам. 3️⃣ Безопасность типов. Метод reversed() возвращает «живое» представление. Изменения в нем отразятся на оригинале (для мутабельных коллекций). Вроде мелочь, а код становится намного чище.

💻 Пишешь на Python или JavaScript и всё чаще ловишь себя на мысли: «А может, попробовать Go?» Такой момент рано или поздно н
💻 Пишешь на Python или JavaScript и всё чаще ловишь себя на мысли: «А может, попробовать Go?» Такой момент рано или поздно наступает у многих. Про Go говорят как о языке для серьёзных backend-задач, высоких нагрузок и стабильных проектов, но непонятно главное — как на него перейти без боли. В Kata Academy вышла подробная статья: — чем Go принципиально отличается от Python и JavaScript; — в каких моментах будет непривычно в начале; — какие навыки реально пригодятся при переходе; — кому Go подойдёт лучше всего. Кстати, это ещё и одна из статей новогоднего розыгрыша 🎄 В декабрьских материалах Kata Academy спрятаны слова. Соберёшь фразу — можно выиграть сертификат Ozon. Читать про переход на Go 👈🏻 #реклама О рекламодателе

⌨️ Sealed Classes: Фейсконтроль для наследования В ООП всегда была дилемма. Если вы делаете класс public, от него может наследоваться кто угодно. Если делаете final, от него не может наследоваться никто. А что, если я хочу, чтобы от моего класса Payment могли наследоваться только CashPayment и CardPayment, и больше никто? До Java 17 приходилось придумывать костыли с пакетами. Теперь у нас есть Sealed Classes (Запечатанные классы). ✔️ Как это работает: Вы используете ключевое слово sealed и через permits перечисляете, кому "можно".

public sealed interface Shape permits Circle, Square, Rectangle {
    // Общие методы
}
Теперь Java (и компилятор) гарантирует: никаких других фигур, кроме этих трех, в программе существовать не может. ➡️ Правила игры: Наследники (Circle, Square...) должны выбрать свою судьбу и указать один из модификаторов: 1️⃣ final — от меня наследовать нельзя (конец цепочки). 2️⃣ sealed — я тоже строгий, вот мой список наследников. 3️⃣ non-sealed — ладно, от меня можно наследовать всем (открываем шлюз). 🔥 Зачем это нужно? (Главная фишка) Это идеально работает в связке с новым Switch. Поскольку компилятор точно знает все возможные варианты наследников, он не потребует от вас ветку default!

String result = switch (shape) {
    case Circle c -> "Это круг радиусом " + c.radius();
    case Square s -> "Это квадрат";
    case Rectangle r -> "Это прямоугольник";
    // default не нужен! Java знает, что других фигур нет.
};
Это делает моделирование бизнес-логики (статусы заказов, типы ошибок) невероятно надежным. Если вы добавите новую фигуру, код перестанет компилироваться, пока вы не обработаете её в свитче.

🔍 Завтра тестовое собеседование с Java-разработчиком 17 декабря(уже завтра!) в 19:00 по мск приходи онлайн на открытое собес
🔍 Завтра тестовое собеседование с Java-разработчиком 17 декабря(уже завтра!) в 19:00 по мск приходи онлайн на открытое собеседование, чтобы посмотреть на настоящее интервью на Middle Java-разработчика. Как это будет: 📂 Сергей Чамкин, старший разработчик из Uzum, ex-WildBerries, будет задавать реальные вопросы и задачи разработчику-добровольцу 📂 Cергей будет комментировать каждый ответ респондента, чтобы дать понять чего от вас ожидает собеседующий на интервью 📂 В конце можно будет задать любой вопрос Сергею Это бесплатно. Эфир проходит в рамках менторской программы от ШОРТКАТ для Java-разработчиков, которые хотят повысить свой грейд, ЗП и прокачать скиллы. Переходи в нашего бота, чтобы получить ссылку на эфир → @shortcut_sh_bot Реклама. О рекламодателе.

⌨️ Java Collection Factories: List.of и Map.of Если вы пишете на Java дольше 5 лет, вы помните эту боль. Вам просто нужно создать Map с двумя значениями. Но Java требовала от вас целый ритуал. 🐢 Как это было раньше (Java 8 и старее):

// Для списка:
List<String> list = Arrays.asList("a", "b", "c"); 
// Вроде ок, но этот список можно менять (set), но нельзя менять размер (add/remove).

// Для Map — вообще ужас:
Map<String, Integer> map = new HashMap<>();
map.put("one", 1);
map.put("two", 2);
map = Collections.unmodifiableMap(map); // Если хотим защитить от изменений
🚀 Как это делается с Java 9:

List<String> list = List.of("a", "b", "c");

Map<String, Integer> map = Map.of("one", 1, "two", 2);

Set<String> set = Set.of("a", "b", "c");
Преимущества: 1️⃣ Неизменяемость (Immutability): Эти коллекции нельзя менять. Попытка сделать .add() или .put() сразу выбросит UnsupportedOperationException. Это делает код безопаснее (особенно в многопоточности). 2️⃣ Лаконичность: Map создается в одну строку (до 10 пар ключ-значение можно писать через запятую, дальше — через Map.ofEntries). 3️⃣ Никаких null: Если вы попытаетесь положить null в List.of или Map.of, вы сразу получите ошибку. Java приучает нас не использовать null в коллекциях. 💡 Лайфхак для Java 16+: Если вы работаете со стримами, забудьте про .collect(Collectors.toList()). Теперь можно писать просто:

List<String> result = stream.filter(s -> s.length() > 3).toList();
Обратите внимание: toList() возвращает неизменяемый список, в отличие от коллектора.

Напоминаем про наш канал с тестами по Java ⌨️ Самое время проверить свои знания и понять, что стоит подтянуть. ➡️ Java | Tests

⌨️ HashMap и TreeMap: Когда и как использовать В Java коллекции Map предоставляют возможность хранить пары "ключ-значение". Два популярных варианта — HashMap и TreeMap. Давайте разберем их ключевые особенности и когда их лучше использовать. 📚 Kраткие определения: - HashMap:   - Неупорядоченная коллекция.   - Основан на хэш-таблице. - TreeMap:   - Упорядоченная коллекция.   - Основан на красно-черном дереве.   - Поддерживает сортировку по ключам. ⚖️ Когда использовать: - HashMap:   - Если важна производительность и порядок хранения не имеет значения.   - Когда нужно быстро получать значения по ключу - TreeMap:   - Если необходимо хранить элементы в отсортированном порядке.   - Для использования функционала "примитивного" поиска (например, firstKey() или lastKey()). 📌 Подведение итогов: Выбор между HashMap и TreeMap зависит от ваших требований к производительности и порядку элементов. Помните, что HashMap лучше подходит для большинства случаев, когда необходим быстрый доступ, а TreeMap — для упорядоченного хранения данных. #java #HashMap #TreeMap

☕️ Spring AI: LLM прямо внутри твоего Java-сервиса Никаких отдельных пайплайнов, очередей и костылей. Spring AI даёт готовый слой для работы с LLM: модели, промпты, memory, embeddings — всё как в Python, только нативно для Java. ⚙️ 1. Подключаем Spring AI build.gradle
dependencies {
    implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
}
➡️ Starter тащит автоконфигурацию, модельный клиент и готовые бинды. 🔑 2. Настройка доступа к модели application.yml
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        model: gpt-4.1
➡️ Осталось только дернуть bean — Spring сам соберёт клиент. 💬 3. Генерация ответа из Java-кода
@RestController
@RequiredArgsConstructor
public class AiController {

    private final ChatClient chatClient;

    @GetMapping("/explain")
    public String explain(@RequestParam String code) {
        return chatClient
                .prompt("Объясни этот код кратко:\n" + code)
                .call()
                .content();
    }
}
➡️ ChatClient → prompt → call(). Минимум связующей логики. 🧩 4. Структурированный вывод (JSON → Java)
record Result(String summary, int score) {}

var response = chatClient
        .prompt("""
            Проанализируй текст. 
            Верни: summary, score (0-10).
        """)
        .responseAs(Result.class);
➡️ LLM сразу мапится в твой record-класс. Никаких JSON-ручек. 📚 5. Prompts как ресурсы
@AiClient
public interface ReviewAi {

    @SystemPrompt("prompts/system.txt")
    @UserPrompt("prompts/review.txt")
    String analyze(String text);
}
➡️ Промпты лежат в папке, модель вызывается как обычный Java-метод. 🔍 6. Embeddings для поиска
var embedding = embeddingClient.embed("Spring AI makes Java smart");

vectorStore.add("doc-1", embedding);
➡️ Vector store — часть Spring AI: Chroma, PGVector, Redis. Подходит для RAG в микросервисах. 🧠 7. RAG: ответ с учётом своих данных
var answer = ragClient
        .query("Как работает наш биллинг?")
        .call()
        .content();
➡️ ragClient сам:
1. берёт эмбеддинг вопроса, 2. ищет релевантные документы, 3. подаёт всё в LLM.
🪢 8. Streaming-ответы (chunk за chunk’ом)
chatClient
        .prompt("Сгенерируй длинное объяснение")
        .stream()
        .forEach(chunk -> System.out.print(chunk.content()));
➡️ Нужен, если делаешь real-time UI или аналитический сервис. 🔌 9. Используем несколько моделей одновременно
spring:
  ai:
    clients:
      local:
        base-url: http://localhost:1234/v1
        chat:
          model: llama3
      openai:
        api-key: ${KEY}
        chat:
          model: gpt-4.1
➡️ Можно запрашивать локальную LLaMA и облачную OpenAI в одном приложении. 🧵 10. Memory (контекст диалога)
chatClient
    .withConversation("session-42")
    .prompt("Продолжи диалог")
    .call();
➡️ Хранит состояние переписки без твоих ручных костылей. 🗣️ Запомни:Spring AI — это мост между классическим Java-стеком и LLM-миром: минимум клея, максимум интеграций, всё по-джавовски строго и предсказуемо.