
- Главная
- Каталог
- Интернет технологии
- Java | Фишки и трюки
Java | Фишки и трюки
Аудитория канала - начинающие или опытные Java программисты. Канал о разработке приложений на Java, в том числе написание бэкенд и web-приложений. Рассматриваются фишки и трюки при программировании на Java.
Статистика канала
String.format() StringBuilder — кто быстрее на самом деле?
Каждый день ты склеиваешь строки. Но если это происходит в горячем цикле или в высоконагруженном микроссервисе — разница в подходах может стоить миллисекунд, а то и секунд процессорного времени. Давай расставим всё по местам с цифрами в руках.
Вот бенчмарк-ликбез, после которого ты больше не будешь гадать
// 1. Конкатенация (+)
String result1 = "Привет, " + name + "! Тебе " + age + " лет.";
// 2. StringBuilder
String result2 = new StringBuilder()
.append("Привет, ")
.append(name)
.append("! Тебе ")
.append(age)
.append(" лет.")
.toString();
// 3. String.format()
String result3 = String.format("Привет, %s! Тебе %d лет.", name, age);
// 4. String.concat() (редко, но бывает)
String result4 = "Привет, ".concat(name).concat("! Тебе ").concat(String.valueOf(age)).concat(" лет.");{}
Конкатенация (+) ........... ~50 нс
StringBuilder .............. ~15 нс
String.format() ............ ~1200 нс
String.concat() ............ ~70 нс{}
String.format() проигрывает в скорости в десятки раз. Он делает парсинг шаблона, проверку типов — это дорого. StringBuilder — чемпион по скорости.
// Ты пишешь:
String s = a + b + c;
// Компилятор делает под капотом:
String s = new StringBuilder().append(a).append(b).append(c).toString();{}
// ❌ ПЛОХО (создаётся новый StringBuilder на КАЖДОЙ итерации)
String result = "";
for (String part : parts) {
result += part; // Скрытый new StringBuilder() каждый раз!
}
// ✅ ХОРОШО (один StringBuilder на весь цикл)
StringBuilder sb = new StringBuilder();
for (String part : parts) {
sb.append(part);
}
String result = sb.toString();{}
// Читаемо vs Нечитаемо
String.format("Сумма: %.2f руб., дата: %tD", sum, date); // ✅
// vs ручное склеивание с DecimalFormat и SimpleDateFormat — кошмар
{}
2. Логирование, где скорость не критична (но даже там лучше шаблонизатор).
3. Конфигурационные сообщения, выносимые в properties-файлы.
String.format() — это Rolls-Royce: удобно, красиво, но для поездки в магазин за хлебом — избыточно.
// Когда нужно собрать строку через разделитель
StringJoiner joiner = new StringJoiner(", ", "[", "]");
joiner.add("Java");
joiner.add("Kotlin");
joiner.add("Scala");
String result = joiner.toString(); // [Java, Kotlin, Scala]{}
StringBuilder, но даёт элегантный API для склейки с разделителями. Используй вместо ручных проверок «не первый ли элемент?».
java.util.Date должен умереть
В старых версиях Java работа со временем была настоящим квестом. Класс java.util.Date — это, пожалуй, самый неудачный дизайн в истории JDK.
🤦♂️ Зал славы проблем Date и Calendar:
1️⃣ Они изменяемые (Mutable): Вы передаете дату в метод, а он может тихо поменять ей год. Это ад для многопоточности.
2️⃣ Нумерация месяцев: Январь — это 0. Декабрь — 11. Сколько багов было написано из-за этого!
3️⃣ Годы: new Date(2023, ...) создаст дату в 3923 году (потому что отсчет идет с 1900).
4️⃣ Нейминг: java.sql.Date наследуется от java.util.Date, но не содержит времени. Путаница неизбежна.
✅ Спасение в Java 8+ (java.time)
Java переняла опыт библиотеки Joda-Time и внедрила JSR 310. Теперь у нас есть строгие, неизменяемые и понятные типы.
Шпаргалка, что выбрать:
1️⃣ Instant — Точка на временной шкале (Unix Timestamp).
Для кого: Для машин, логов и баз данных. Внутри это просто количество секунд с 1970 года (UTC).
Пример: Instant.now()
2️⃣ LocalDate / LocalTime — "Дата в календаре" и "Время на часах". Без часового пояса.
Для кого: Дни рождения, праздники ("Новый год всегда 1 января", неважно, где вы).
Пример: LocalDate.of(2023, Month.JANUARY, 1)
3️⃣ ZonedDateTime — Полный фарш: дата + время + часовой пояс.
Для кого: Для организации встреч звонков между странами. Учитывает переход на летнее время!
💎 Неизменяемость (Immutability):
Больше никаких сюрпризов. Методы изменения времени всегда возвращают новый объект.
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plusDays(1); // today остался прежним!
{}
💡 Лайфхак:
Если вам нужно посчитать разницу между датами, используйте Period (для дней/месяцев) или Duration (для секунд/наносекунд).
long days = ChronoUnit.DAYS.between(date1, date2);{}
java.util.Date должен умереть
В старых версиях Java работа со временем была настоящим квестом. Класс java.util.Date — это, пожалуй, самый неудачный дизайн в истории JDK.
🤦♂️ Зал славы проблем Date и Calendar:
1️⃣ Они изменяемые (Mutable): Вы передаете дату в метод, а он может тихо поменять ей год. Это ад для многопоточности.
2️⃣ Нумерация месяцев: Январь — это 0. Декабрь — 11. Сколько багов было написано из-за этого!
3️⃣ Годы: new Date(2023, ...) создаст дату в 3923 году (потому что отсчет идет с 1900).
4️⃣ Нейминг: java.sql.Date наследуется от java.util.Date, но не содержит времени. Путаница неизбежна.
✅ Спасение в Java 8+ (java.time)
Java переняла опыт библиотеки Joda-Time и внедрила JSR 310. Теперь у нас есть строгие, неизменяемые и понятные типы.
Шпаргалка, что выбрать:
1️⃣ Instant — Точка на временной шкале (Unix Timestamp).
Для кого: Для машин, логов и баз данных. Внутри это просто количество секунд с 1970 года (UTC).
Пример: Instant.now()
2️⃣ LocalDate / LocalTime — "Дата в календаре" и "Время на часах". Без часового пояса.
Для кого: Дни рождения, праздники ("Новый год всегда 1 января", неважно, где вы).
Пример: LocalDate.of(2023, Month.JANUARY, 1)
3️⃣ ZonedDateTime — Полный фарш: дата + время + часовой пояс.
Для кого: Для организации встреч звонков между странами. Учитывает переход на летнее время!
💎 Неизменяемость (Immutability):
Больше никаких сюрпризов. Методы изменения времени всегда возвращают новый объект.
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plusDays(1); // today остался прежним!
{}
💡 Лайфхак:
Если вам нужно посчитать разницу между датами, используйте Period (для дней/месяцев) или Duration (для секунд/наносекунд).
long days = ChronoUnit.DAYS.between(date1, date2);{}
Future из Java 5? Он был похож на чемодан без ручки. Вы могли отправить задачу в другой поток, но чтобы получить результат, приходилось делать future.get() и блокировать текущий поток, ожидая ответа. Смысл асинхронности терялся.
В Java 8 появился `CompletableFuture`. Это Future, который можно "программировать".
🔗 Цепочки вызовов (Pipelining)
Вместо того чтобы ждать результат, вы говорите Java: *"Когда результат будет готов, сделай с ним вот это, а потом вот это"*.
CompletableFuture.supplyAsync(() -> {
return fetchUser(123); // 1. Идем в базу (в другом потоке)
}).thenApply(user -> {
return user.getEmail(); // 2. Преобразуем (когда данные пришли)
}).thenAccept(email -> {
sendEmail(email); // 3. Отправляем письмо
});
System.out.println("Я не заблокирован!"); // Эта строка выполнится мгновенно
{}
💪 Суперсила: Комбинация задач
Представьте, что вам нужно получить данные пользователя И его последние заказы из разных сервисов одновременно.
var userFuture = CompletableFuture.supplyAsync(() -> getUser());
var ordersFuture = CompletableFuture.supplyAsync(() -> getOrders());
// Ждем оба, комбинируем результаты и возвращаем отчет
userFuture.thenCombine(ordersFuture, (user, orders) -> {
return new Report(user, orders);
}).thenAccept(report -> show(report));
{}
Оба запроса выполняются параллельно. Время выполнения равно времени самого медленного запроса, а не их сумме.
⚠️ Опасная ловушка
По умолчанию CompletableFuture использует общий пул потоков ForkJoinPool.commonPool().
Если вы запустите там "тяжелую" блокирующую задачу (например, долгий запрос к БД без асинхронного драйвера), вы можете забить все потоки и "повесить" всё приложение.
Совет: Всегда передавайте свой собственный Executor вторым аргументом:
CompletableFuture.supplyAsync(() -> heavyTask(), myCustomExecutor);{}
volatile?», и 80% ответят: «Чтобы потоки не конфликтовали».
Это не совсем так. volatile не спасает от конфликтов (race conditions). Он решает проблему видимости.
Давайте разберемся, почему ваш код может зависнуть навечно без этого слова.
🛑 Ситуация: Бесконечный цикл
Представьте простой код:
public class Worker {
private boolean running = true; // ❌ Забыли volatile
public void run() {
while (running) {
// Крутимся в цикле и ждем команды стоп
}
System.out.println("Stopped!");
}
public void stop() {
running = false;
}
}
{}
Вы запускаете run() в Потоке А, а через секунду вызываете stop() из Потока Б.
Ожидание: Цикл остановится.
Реальность: Программа может работать вечно.
Почему? (Кэши процессора)
Современные процессоры супер-быстрые, а оперативная память (RAM) — медленная.
Чтобы не ждать память, каждое ядро процессора имеет свой локальный кэш (L1/L2 Cache).
1. Поток А (Ядро 1) скачал переменную running = true в свой кэш. Он проверяет её там.
2. Поток Б (Ядро 2) меняет значение на false в *своем* кэше и даже записывает в RAM.
3. Но Поток А этого не видит! Он продолжает читать true из своего изолированного кэша. Это называется проблема видимости.
✨ Магия `volatile`
Если мы напишем:
private volatile boolean running = true;
{}
Мы говорим JVM и процессору: «Эту переменную нельзя кэшировать локально».
1. Любая запись в volatile переменную сразу пробрасывается (flush) в общую память (RAM).
2. Любое чтение volatile переменной идет напрямую из общей памяти.
Поток А сразу увидит изменение, и цикл остановится.
⚠️ Главная ловушка: Атомарность
Многие новички думают: «О, volatile синхронизирует данные! Значит можно делать счетчик».
volatile int count = 0;
count++; // ⛔️ ОПАСНО!
{}
НЕТ! Операция count++ — это три действия: считать, прибавить, записать.
volatile гарантирует, что вы считаете свежее значение, но он не блокирует других от изменения этой переменной в тот же момент. Два потока могут одновременно считать 5, оба добавят 1 и запишут 6. Мы потеряем данные.
Итог:
👉 Используйте volatile только для флагов состояния (вкл/выкл) или когда запись делает только один поток.
👉 Для счетчиков используйте AtomicInteger или synchronized.String.class.getClassLoader(), вы получите null - Java его просто не видит как объект).
Что грузит: Ядро Java (java.lang.*, java.util.* - всё из rt.jar или модулей java.base).
2️⃣ Platform ClassLoader (ранее Extension)
Наследник Bootstrap'а.
Что грузит: Расширения JDK (SQL, XML парсеры и прочие системные модули).
3️⃣ Application (System) ClassLoader
Тот самый, с которым мы работаем 99% времени.
Что грузит: Всё, что лежит в вашем CLASSPATH (`-cp`). Ваш код и ваши библиотеки.
🔄 Как работает поиск класса?
Допустим, в коде встретилось new User().
1. AppClassLoader не бросается искать файл User.class сам. Он звонит родителю: «Слушай, Platform, у тебя есть User?»
2. PlatformClassLoader тоже ленив и звонит своему родителю: «Bootstrap, у тебя есть User?»
3. Bootstrap смотрит в ядре Java. «Нет, это не стандартный класс».
4. Только тогда управление возвращается вниз. Platform смотрит у себя — нет.
5. И только в конце AppClassLoader ищет класс в ваших jar-файлах.
Если не найдет и он — привет, ClassNotFoundException.
👹 Ад зависимостей (Jar Hell) и Custom ClassLoaders
Зачем знать про ClassLoaders? Чтобы понимать сложные баги и архитектуру.
Представьте, что ваше приложение использует библиотеку Lib A версии 1.0, а другая ваша библиотека требует Lib A версии 2.0.
Стандартный загрузчик сломается: он не может загрузить два класса с одинаковым именем (`com.example.Lib`). Выиграет тот, кто первый попался в classpath, а второй упадет с ошибкой NoSuchMethodError.
Как это решают Tomcat или Maven?
Они пишут свои собственные ClassLoader'ы, нарушая стандартную иерархию.
В Tomcat каждое веб-приложение (`war`) имеет свой изолированный загрузчик. Поэтому два разных приложения могут использовать разные версии Spring на одном сервере, и они не подерутся.
Итог:
ClassLoader — это мост между файловой системой и памятью JVM. Понимание модели делегирования спасает часы при отладке странных ошибок, когда класс вроде бы есть, но Java его «не видит».
// просто запускаем JVM и забываем{}
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("Создано много объектов");
}
}{}
-XX:+PrintGCDetails{}
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");
}
}{}
System.gc();{}
Отзывы канала
всего 11 отзывов
- Добавлен: Сначала новые
- Добавлен: Сначала старые
- Оценка: По убыванию
- Оценка: По возрастанию
Каталог Телеграм-каналов для нативных размещений
Java | Фишки и трюки — это Telegam канал в категории «Интернет технологии», который предлагает эффективные форматы для размещения рекламных постов в Телеграмме. Количество подписчиков канала в 7.1K и качественный контент помогают брендам привлекать внимание аудитории и увеличивать охват. Рейтинг канала составляет 19.5, количество отзывов – 11, со средней оценкой 5.0.
Вы можете запустить рекламную кампанию через сервис Telega.in, выбрав удобный формат размещения. Платформа обеспечивает прозрачные условия сотрудничества и предоставляет детальную аналитику. Стоимость размещения составляет 2237.76 ₽, а за 85 выполненных заявок канал зарекомендовал себя как надежный партнер для рекламы в TG. Размещайте интеграции уже сегодня и привлекайте новых клиентов вместе с Telega.in!
Вы снова сможете добавить каналы в корзину из каталога
Комментарий