[Совет по Java #006]
Тема: Используйте Enum вместо строковых констант для ограниченного набора значений.
Проблема: Использование строковых констант (String constants) для представления фиксированного набора допустимых значений является антипаттерном, известным как "Stringly Typed".
Такой подход лишен типобезопасности: компилятор не может предотвратить передачу в метод произвольной строки, не входящей в допустимый набор. Это приводит к ошибкам, обнаруживаемым только на этапе выполнения.
Кроме того, строковые константы не обеспечивают автодополнения в IDE (если только не используются статические импорты, но и они не решают проблему проверки на этапе компиляции), а рефакторинг (например, переименование значения) требует глобального поиска и замены по всему проекту, что чревато ошибками.
Решение: Использование перечислений (enum), которые являются полноценными классами в Java. enum предоставляет типобезопасность — компилятор гарантирует, что в метод, принимающий enum, может быть передан только один из предопределенных элементов перечисления или null.
Это делает код самодокументируемым, улучшает читаемость и полностью исключает класс ошибок, связанных с опечатками в строковых литералах. Более того, enum может содержать поля, методы и реализовывать интерфейсы, что позволяет инкапсулировать поведение, специфичное для каждого элемента, прямо внутри перечисления.
#Java #советы
Тема: Используйте Enum вместо строковых констант для ограниченного набора значений.
Проблема: Использование строковых констант (String constants) для представления фиксированного набора допустимых значений является антипаттерном, известным как "Stringly Typed".
Такой подход лишен типобезопасности: компилятор не может предотвратить передачу в метод произвольной строки, не входящей в допустимый набор. Это приводит к ошибкам, обнаруживаемым только на этапе выполнения.
Кроме того, строковые константы не обеспечивают автодополнения в IDE (если только не используются статические импорты, но и они не решают проблему проверки на этапе компиляции), а рефакторинг (например, переименование значения) требует глобального поиска и замены по всему проекту, что чревато ошибками.
Решение: Использование перечислений (enum), которые являются полноценными классами в Java. enum предоставляет типобезопасность — компилятор гарантирует, что в метод, принимающий enum, может быть передан только один из предопределенных элементов перечисления или null.
Это делает код самодокументируемым, улучшает читаемость и полностью исключает класс ошибок, связанных с опечатками в строковых литералах. Более того, enum может содержать поля, методы и реализовывать интерфейсы, что позволяет инкапсулировать поведение, специфичное для каждого элемента, прямо внутри перечисления.
package com.example.model;
public enum OrderStatus {
NEW("Новый") {
@Override
public void handle() {
System.out.println("Проверка наличия товара для нового заказа");
}
},
PROCESSING("В обработке") {
@Override
public void handle() {
System.out.println("Передача в службу доставки");
}
},
COMPLETED("Завершен") {
@Override
public void handle() {
System.out.println("Отправка опроса удовлетворенности");
}
};
private final String displayName;
OrderStatus(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
public abstract void handle();
}
#Java #советы
👍5
// OrderService.java - Сервисный слой
package com.example.service;
import com.example.model.OrderStatus;
public class OrderService {
public void processOrder(OrderStatus status) {
if (status == null) {
throw new IllegalArgumentException("Статус не может быть null");
}
status.handle();
displayStatusInfo(status);
}
private void displayStatusInfo(OrderStatus status) {
switch (status) {
case NEW -> System.out.println("Отображение как: " + status.getDisplayName());
case PROCESSING -> System.out.println("Отображение как: " + status.getDisplayName());
case COMPLETED -> System.out.println("Отображение как: " + status.getDisplayName());
}
}
}
Запуск кода.
// Application.java - Точка входа
package com.example;
import com.example.model.OrderStatus;
import com.example.service.OrderService;
public class Application {
public static void main(String[] args) {
OrderService orderService = new OrderService();
orderService.processOrder(OrderStatus.NEW);
}
}
Объяснение: Каждое перечисление в Java — это класс, который неявно наследуется от java.lang.Enum.
Элементы перечисления являются статическими финализированными экземплярами этого класса. Это позволяет добавлять в enum поля (например, для хранения описания), конструкторы (которые вызываются для создания каждого элемента), методы и даже реализовывать интерфейсы.
Особую мощь предоставляет возможность объявлять абстрактные методы и переопределять их для каждого элемента (как в примере с handle()). Это реализует паттерн "Стратегия" на уровне языка и позволяет избежать громоздких конструкций if-else или switch при обработке значений.
Разработчик должен рассматривать enum не просто как типизированный набор именованных констант, а как полноценный инструмент для моделирования предметной области с инкапсуляцией данных и поведения.
#Java #советы
👍5
Что выведет код?
#Tasks
public class Task170226 {
enum Color {
RED, GREEN, BLUE;
static {
System.out.println("Static block");
}
Color() {
System.out.println("Constructor: " + this);
}
}
public static void main(String[] args) {
System.out.println("Start");
Color c = Color.RED;
System.out.println("End");
}
}#Tasks
👍3
👍1😱1
Объясните паттерн Singleton. Как его реализовать в Java? 🤓
Ответ:
Singleton гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему.
Базовая небезопасная для потоков реализация через ленивую инициализацию.
Потокобезопасные реализации:
1) Eager Initialization: создание экземпляра в статическом поле при загрузке класса.
2) Double-Checked Locking: проверка в synchronized блоке с volatile переменной.
3) Static Nested Class (Holder): самый популярный и безопасный способ. Внутренний статический класс содержит экземпляр, который создается при первом обращении.
4) enum: наилучший способ по Джошуа Блоху, защищает от рефлексии и сериализации.
#собеседование
Ответ:
Базовая небезопасная для потоков реализация через ленивую инициализацию.
Потокобезопасные реализации:
1) Eager Initialization: создание экземпляра в статическом поле при загрузке класса.
2) Double-Checked Locking: проверка в synchronized блоке с volatile переменной.
3) Static Nested Class (Holder): самый популярный и безопасный способ. Внутренний статический класс содержит экземпляр, который создается при первом обращении.
4) enum: наилучший способ по Джошуа Блоху, защищает от рефлексии и сериализации.
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3🔥1
История IT-технологий сегодня — 18 Февраля
ℹ️ Кто родился в этот день
Алесса́ндро Джузе́ппе Анто́нио Анаста́сио Во́льта (итал. Alessandro Giuseppe Antonio Anastasio Volta; 18 февраля 1745, Комо — 5 марта 1827, там же) — итальянский физик, химик и физиолог, один из основоположников учения об электричестве; граф (1810). Создатель вольтова столба, первого устойчивого химического источника тока. Его работа фактически запустила эпоху управляемого электричества; единица напряжения «вольт» названа в его честь.
🌐 Знаковые события
2021 – Марсоход « Персеверанс» , предназначенный для исследования кратера Езеро на Марсе в рамках миссии НАСА «Марс 2020» , успешно совершил посадку.
#Biography #Birth_Date #Events #18февраля
Алесса́ндро Джузе́ппе Анто́нио Анаста́сио Во́льта (итал. Alessandro Giuseppe Antonio Anastasio Volta; 18 февраля 1745, Комо — 5 марта 1827, там же) — итальянский физик, химик и физиолог, один из основоположников учения об электричестве; граф (1810). Создатель вольтова столба, первого устойчивого химического источника тока. Его работа фактически запустила эпоху управляемого электричества; единица напряжения «вольт» названа в его честь.
2021 – Марсоход « Персеверанс» , предназначенный для исследования кратера Езеро на Марсе в рамках миссии НАСА «Марс 2020» , успешно совершил посадку.
#Biography #Birth_Date #Events #18февраля
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Раздел 8. Stream API и функциональный стиль в Java
Глава 2: Анатомия Stream API. Ленивость и стоимость операций
(Практика): Диссекция конвейера в «Библиотеке»
Убедитесь, что в проекте «Библиотека» настроены базовые классы. Для этого урока достаточно простого main метода в отдельном тестовом классе или в существующем Main.
Построение конвейера с наблюдением
Шаг 1. Создание источника с отслеживанием
Создайте метод demonstrateLazyEvaluation() в классе Main или отдельном тестовом классе.
Начните с простого источника данных и операции peek, которая позволяет наблюдать за элементами без изменения потока:
Что происходит: IntStream.range(0, 10) создаёт поток чисел от 0 до 9. peek — промежуточная операция, которая выполняет действие при прохождении элемента. На этом этапе ничего не выводится, потому что нет терминальной операции.
Шаг 2. Добавление фильтрации
Добавьте фильтр чётных чисел с ещё одним peek:
Что происходит: filter — промежуточная операция, которая пропускает только элементы, удовлетворяющие условию. Обе операции peek остаются промежуточными. Поток всё ещё не выполняется.
Шаг 3. Добавление преобразования
Добавьте возведение в квадрат с третьим peek:
Что происходит: map — промежуточная операция, преобразующая каждый элемент. Конвейер построен, но не запущен.
Часть 2. Запуск конвейера разными способами
Вариант А. Короткозамкнутая операция findFirst()
Добавьте терминальную операцию findFirst():
Наблюдайте за выводом в консоли. Запишите, в каком порядке появляются сообщения.
Ожидаемый вывод:
Вариант Б. Полная обработка forEach()
Замените findFirst() на forEach():
Наблюдайте за выводом. Запишите порядок сообщений.
Ожидаемый вывод:
Часть 3. Анализ «вертикальной» vs «горизонтальной» обработки
Вертикальная обработка (фактическая)
Stream API обрабатывает элементы по одному через весь конвейер:
Это ленивая оценка: каждый элемент проходит через все промежуточные операции до терминальной, прежде чем берётся следующий.
Горизонтальная обработка (гипотетическая)
Можно было бы ожидать, что сначала все элементы проходят source, потом все проходят filter, потом все проходят map:
Но Stream API так не работает. Почему?
#Java #для_новичков #beginner #Практика #stream_api #lazy
Глава 2: Анатомия Stream API. Ленивость и стоимость операций
(Практика): Диссекция конвейера в «Библиотеке»
Убедитесь, что в проекте «Библиотека» настроены базовые классы. Для этого урока достаточно простого main метода в отдельном тестовом классе или в существующем Main.
Построение конвейера с наблюдением
Шаг 1. Создание источника с отслеживанием
Создайте метод demonstrateLazyEvaluation() в классе Main или отдельном тестовом классе.
Начните с простого источника данных и операции peek, которая позволяет наблюдать за элементами без изменения потока:
IntStream.range(0, 10)
.peek(i -> System.out.println("source: " + i))
Что происходит: IntStream.range(0, 10) создаёт поток чисел от 0 до 9. peek — промежуточная операция, которая выполняет действие при прохождении элемента. На этом этапе ничего не выводится, потому что нет терминальной операции.
Шаг 2. Добавление фильтрации
Добавьте фильтр чётных чисел с ещё одним peek:
IntStream.range(0, 10)
.peek(i -> System.out.println("source: " + i))
.filter(i -> i % 2 == 0)
.peek(i -> System.out.println("filtered: " + i))
Что происходит: filter — промежуточная операция, которая пропускает только элементы, удовлетворяющие условию. Обе операции peek остаются промежуточными. Поток всё ещё не выполняется.
Шаг 3. Добавление преобразования
Добавьте возведение в квадрат с третьим peek:
IntStream.range(0, 10)
.peek(i -> System.out.println("source: " + i))
.filter(i -> i % 2 == 0)
.peek(i -> System.out.println("filtered: " + i))
.map(i -> i * i)
.peek(i -> System.out.println("mapped: " + i))
Что происходит: map — промежуточная операция, преобразующая каждый элемент. Конвейер построен, но не запущен.
Часть 2. Запуск конвейера разными способами
Вариант А. Короткозамкнутая операция findFirst()
Добавьте терминальную операцию findFirst():
OptionalInt result = IntStream.range(0, 10)
.peek(i -> System.out.println("source: " + i))
.filter(i -> i % 2 == 0)
.peek(i -> System.out.println("filtered: " + i))
.map(i -> i * i)
.peek(i -> System.out.println("mapped: " + i))
.findFirst();
Наблюдайте за выводом в консоли. Запишите, в каком порядке появляются сообщения.
Ожидаемый вывод:
source: 0
filtered: 0
mapped: 0
Почему только одна цепочка? Потому что findFirst() — короткозамкнутая операция. Как только найден первый подходящий элемент (0), выполнение останавливается. Остальные числа от 1 до 9 даже не достигают источника.
Вариант Б. Полная обработка forEach()
Замените findFirst() на forEach():
IntStream.range(0, 10)
.peek(i -> System.out.println("source: " + i))
.filter(i -> i % 2 == 0)
.peek(i -> System.out.println("filtered: " + i))
.map(i -> i * i)
.peek(i -> System.out.println("mapped: " + i))
.forEach(i -> System.out.println("result: " + i));
Наблюдайте за выводом. Запишите порядок сообщений.
Ожидаемый вывод:
source: 0
filtered: 0
mapped: 0
result: 0
source: 1
source: 2
filtered: 2
mapped: 4
result: 4
source: 3
source: 4
filtered: 4
mapped: 16
result: 16
...
Часть 3. Анализ «вертикальной» vs «горизонтальной» обработки
Вертикальная обработка (фактическая)
Stream API обрабатывает элементы по одному через весь конвейер:
Элемент 0: source → filter → map → result
Элемент 1: source → (отфильтрован) → ...
Элемент 2: source → filter → map → result
Это ленивая оценка: каждый элемент проходит через все промежуточные операции до терминальной, прежде чем берётся следующий.
Горизонтальная обработка (гипотетическая)
Можно было бы ожидать, что сначала все элементы проходят source, потом все проходят filter, потом все проходят map:
Все source: 0,1,2,3,4,5,6,7,8,9
Все filtered: 0,2,4,6,8
Все mapped: 0,4,16,36,64
Но Stream API так не работает. Почему?
#Java #для_новичков #beginner #Практика #stream_api #lazy
👍4
Часть 4. Почему вертикальная обработка?
Преимущества вертикального подхода
Эффективность с короткозамкнутыми операциями
При findFirst() или anyMatch() обработка останавливается при первом успехе. Если бы обработка была горизонтальной, пришлось бы прогнать все элементы через все операции, даже когда ответ найден сразу.
Экономия памяти
Не нужно хранить промежуточные коллекции между этапами. Каждый элемент обрабатывается «на лету».
Возможность бесконечных потоков
IntStream.iterate(0, i -> i + 1) — бесконечный поток. Горизонтальная обработка невозможна (нельзя собрать все элементы), вертикальная — работает с короткозамкнутыми операциями.
Часть 5. Применение к проекту «Библиотека»
Задача: найти первую книгу автора «X» с минимальными затратами
Императивный подход (неэффективный):
Stream с вертикальной обработкой (эффективный):
Вопрос для размышления: Почему sorted в этом примере всё-таки требует полного прохода? Когда Stream API может избежать полной сортировки?
Часть 6. Эксперименты
Эксперимент 1: Порядок операций
Поменяйте местами filter и map в конвейере:
Наблюдение: Нечётные числа тоже проходят map (становятся нечётными квадратами), потом отфильтровываются. Это менее эффективно — лишние вычисления.
Вывод: Порядок промежуточных операций влияет на производительность. Фильтруйте как можно раньше.
Эксперимент 2: limit как короткозамкнутая операция
Добавьте limit(3) перед forEach:
Наблюдение: Обработка останавливается после трёх отфильтрованных элементов. Источник даже не доходит до 1000.
Часть 7. Ключевые термины
Промежуточная операция (intermediate)
Возвращает новый Stream. Не выполняется до терминальной. Примеры: filter, map, sorted, peek, limit.
Терминальная операция (terminal)
Запускает выполнение конвейера, возвращает результат или побочный эффект. Примеры: forEach, findFirst, collect, reduce, anyMatch.
Короткозамкнутая операция (short-circuiting)
Может завершиться до обработки всех элементов. Примеры: findFirst, findAny, anyMatch, allMatch, noneMatch, limit.
Ленивая оценка (lazy evaluation)
Вычисление откладывается до момента, когда результат действительно нужен.
Практическое задание
Задача 1: Оптимизация порядка
В проекте «Библиотека» создайте метод findRecentBooksByAuthor(String author, int year, int limit), который находит limit книг автора, изданных после year, отсортированных по убыванию года.
Постройте конвейер и обоснуйте порядок операций:
Почему filter по автору стоит поставить первым?
Почему filter по году лучше объединить с фильтром автора или оставить отдельным?
Где разместить sorted относительно limit?
Задача 2: Сравнение подходов
Реализуйте ту же задачу императивно (циклами).
Сравните:
Объём кода
Читаемость намерения
Поведение при пустой библиотеке (Optional vs null)
Задача 3: Отладка конвейера
Добавьте peek в разные точки конвейера из Задачи 1. Убедитесь, что при limit(3) обработка действительно останавливается рано.
#Java #для_новичков #beginner #Практика #stream_api #lazy
Преимущества вертикального подхода
Эффективность с короткозамкнутыми операциями
При findFirst() или anyMatch() обработка останавливается при первом успехе. Если бы обработка была горизонтальной, пришлось бы прогнать все элементы через все операции, даже когда ответ найден сразу.
Экономия памяти
Не нужно хранить промежуточные коллекции между этапами. Каждый элемент обрабатывается «на лету».
Возможность бесконечных потоков
IntStream.iterate(0, i -> i + 1) — бесконечный поток. Горизонтальная обработка невозможна (нельзя собрать все элементы), вертикальная — работает с короткозамкнутыми операциями.
Часть 5. Применение к проекту «Библиотека»
Задача: найти первую книгу автора «X» с минимальными затратами
Императивный подход (неэффективный):
// Создаём полный список, сортируем, берём первый
List<Book> filtered = new ArrayList<>();
for (Book b : books) {
if (b.getAuthor().equals("X")) {
filtered.add(b);
}
}
filtered.sort(Comparator.comparingInt(Book::getYear));
Book first = filtered.get(0); // Всё равно обработали все книги
Stream с вертикальной обработкой (эффективный):
Optional<Book> first = books.stream()
.filter(b -> b.getAuthor().equals("X"))
.sorted(Comparator.comparingInt(Book::getYear))
.findFirst(); // Остановится при первой найденной книге
Вопрос для размышления: Почему sorted в этом примере всё-таки требует полного прохода? Когда Stream API может избежать полной сортировки?
Часть 6. Эксперименты
Эксперимент 1: Порядок операций
Поменяйте местами filter и map в конвейере:
IntStream.range(0, 10)
.peek(i -> System.out.println("source: " + i))
.map(i -> i * i) // теперь раньше
.peek(i -> System.out.println("mapped: " + i))
.filter(i -> i % 2 == 0) // теперь позже
.peek(i -> System.out.println("filtered: " + i))
.forEach(i -> System.out.println("result: " + i));
Наблюдение: Нечётные числа тоже проходят map (становятся нечётными квадратами), потом отфильтровываются. Это менее эффективно — лишние вычисления.
Вывод: Порядок промежуточных операций влияет на производительность. Фильтруйте как можно раньше.
Эксперимент 2: limit как короткозамкнутая операция
Добавьте limit(3) перед forEach:
IntStream.range(0, 1000) // большой диапазон
.peek(i -> System.out.println("source: " + i))
.filter(i -> i % 2 == 0)
.peek(i -> System.out.println("filtered: " + i))
.limit(3)
.forEach(i -> System.out.println("result: " + i));
Наблюдение: Обработка останавливается после трёх отфильтрованных элементов. Источник даже не доходит до 1000.
Часть 7. Ключевые термины
Промежуточная операция (intermediate)
Возвращает новый Stream. Не выполняется до терминальной. Примеры: filter, map, sorted, peek, limit.
Терминальная операция (terminal)
Запускает выполнение конвейера, возвращает результат или побочный эффект. Примеры: forEach, findFirst, collect, reduce, anyMatch.
Короткозамкнутая операция (short-circuiting)
Может завершиться до обработки всех элементов. Примеры: findFirst, findAny, anyMatch, allMatch, noneMatch, limit.
Ленивая оценка (lazy evaluation)
Вычисление откладывается до момента, когда результат действительно нужен.
Практическое задание
Задача 1: Оптимизация порядка
В проекте «Библиотека» создайте метод findRecentBooksByAuthor(String author, int year, int limit), который находит limit книг автора, изданных после year, отсортированных по убыванию года.
Постройте конвейер и обоснуйте порядок операций:
Почему filter по автору стоит поставить первым?
Почему filter по году лучше объединить с фильтром автора или оставить отдельным?
Где разместить sorted относительно limit?
Задача 2: Сравнение подходов
Реализуйте ту же задачу императивно (циклами).
Сравните:
Объём кода
Читаемость намерения
Поведение при пустой библиотеке (Optional vs null)
Задача 3: Отладка конвейера
Добавьте peek в разные точки конвейера из Задачи 1. Убедитесь, что при limit(3) обработка действительно останавливается рано.
#Java #для_новичков #beginner #Практика #stream_api #lazy
👍3
Что выведет код?
#Tasks
import java.util.Arrays;
public class Task180226 {
public static void main(String[] args) {
int[] arr1 = {1, 2, 3};
int[] arr2 = {1, 2, 3};
int[] arr3 = arr1;
arr3[0] = 10;
System.out.println(arr1 == arr2);
System.out.println(arr1.equals(arr2));
System.out.println(Arrays.equals(arr1, arr2));
System.out.println(arr1[0] + " " + arr2[0] + " " + arr3[0]);
}
}
#Tasks
👍3🗿1
Варианты ответа:
Anonymous Quiz
42%
false false false 10 1 10
21%
false false true 10 1 10
38%
false true true 10 1 10
0%
true false true 10 10 10
👍3
Что такое массив (array) и как с ним работать? 🤓
Ответ:
Массив — это структура данных, которая хранит фиксированное количество элементов одного типа.
Доступ к элементам осуществляется по индексу (начиная с 0).
Массивы в Java являются объектами. Длина массива задается при создании и не может быть изменена. Массивы могут быть одномерными и многомерными. Для работы с массивами есть удобный класс Arrays с методами: sort(), binarySearch(), toString(), equals(), fill().
При создании массива все элементы получают значения по умолчанию (0, false, null).
#собеседование
Ответ:
Доступ к элементам осуществляется по индексу (начиная с 0).
Массивы в Java являются объектами. Длина массива задается при создании и не может быть изменена. Массивы могут быть одномерными и многомерными. Для работы с массивами есть удобный класс Arrays с методами: sort(), binarySearch(), toString(), equals(), fill().
При создании массива все элементы получают значения по умолчанию (0, false, null).
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
История IT-технологий сегодня — 19 Февраля
ℹ️ Кто родился в этот день
Никола́й Копе́рник (пол. Mikołaj Kopernik, нем. Niklas Koppernigk[5]; 19 февраля 1473, Торунь — 24 мая 1543, Фромборк) — польский и немецкий астроном, математик, механик, экономист, каноник эпохи Возрождения. Наиболее известен как автор гелиоцентрической системы мира, положившей начало первой научной революции.
🌐 Знаковые события
1946 — доклад Алана Тьюринга об ACE. На заседании исполнительного комитета Национальной физической лаборатории (NPL) представил первый детальный дизайн хранимо-программного компьютера ACE (Automatic Computing Engine). Это был первый проект компьютера с хранимой программой, вдохновлённый идеей машины Тьюринга; ACE стал важным шагом к архитектуре фон Неймана и стал прототипом для многих последующих систем.
#Biography #Birth_Date #Events #19февраля
Никола́й Копе́рник (пол. Mikołaj Kopernik, нем. Niklas Koppernigk[5]; 19 февраля 1473, Торунь — 24 мая 1543, Фромборк) — польский и немецкий астроном, математик, механик, экономист, каноник эпохи Возрождения. Наиболее известен как автор гелиоцентрической системы мира, положившей начало первой научной революции.
1946 — доклад Алана Тьюринга об ACE. На заседании исполнительного комитета Национальной физической лаборатории (NPL) представил первый детальный дизайн хранимо-программного компьютера ACE (Automatic Computing Engine). Это был первый проект компьютера с хранимой программой, вдохновлённый идеей машины Тьюринга; ACE стал важным шагом к архитектуре фон Неймана и стал прототипом для многих последующих систем.
#Biography #Birth_Date #Events #19февраля
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
[Совет по Java #007]
Тема: System.currentTimeMillis() для замеров производительности ненадежен. Всегда используйте System.nanoTime().
Проблема: Использование System.currentTimeMillis() для измерения времени выполнения участков кода (профилирования, бенчмаркинга) является распространенной, но методологически неверной практикой.
Этот метод возвращает текущее время в миллисекундах, отсчитываемое от эпохи Unix (1 января 1970 года). Он подвержен влиянию корректировок системного времени, как ручных, так и автоматических (например, синхронизация через NTP — Network Time Protocol). Если во время замера системные часы будут переведены назад, результат может оказаться отрицательным, а если вперед — существенно завышенным. Кроме того, точности в миллисекундах может не хватать для измерения очень быстрых операций, длительность которых составляет микросекунды или наносекунды.
Решение: Для измерения интервалов времени предназначен метод System.nanoTime(). Он использует высокоточный таймер (high-resolution timer) с наносекундной точностью, если это поддерживается платформой. Важно понимать, что nanoTime() измеряет относительное время — некоторый произвольный, но фиксированный момент в прошлом (например, момент загрузки JVM).
Его нельзя использовать для получения текущего времени суток, но он идеально подходит для вычисления разницы (дельты) между двумя событиями. Метод не подвержен корректировкам системных часов и монотонно увеличивается, гарантируя корректность измерения промежутков времени.
#Java #советы
Тема: System.currentTimeMillis() для замеров производительности ненадежен. Всегда используйте System.nanoTime().
Проблема: Использование System.currentTimeMillis() для измерения времени выполнения участков кода (профилирования, бенчмаркинга) является распространенной, но методологически неверной практикой.
Этот метод возвращает текущее время в миллисекундах, отсчитываемое от эпохи Unix (1 января 1970 года). Он подвержен влиянию корректировок системного времени, как ручных, так и автоматических (например, синхронизация через NTP — Network Time Protocol). Если во время замера системные часы будут переведены назад, результат может оказаться отрицательным, а если вперед — существенно завышенным. Кроме того, точности в миллисекундах может не хватать для измерения очень быстрых операций, длительность которых составляет микросекунды или наносекунды.
Решение: Для измерения интервалов времени предназначен метод System.nanoTime(). Он использует высокоточный таймер (high-resolution timer) с наносекундной точностью, если это поддерживается платформой. Важно понимать, что nanoTime() измеряет относительное время — некоторый произвольный, но фиксированный момент в прошлом (например, момент загрузки JVM).
Его нельзя использовать для получения текущего времени суток, но он идеально подходит для вычисления разницы (дельты) между двумя событиями. Метод не подвержен корректировкам системных часов и монотонно увеличивается, гарантируя корректность измерения промежутков времени.
import java.util.concurrent.TimeUnit;
public class PerformanceMeasurement {
public static void main(String[] args) throws InterruptedException {
//Антипаттерн: Измерение через currentTimeMillis()
long startMillis = System.currentTimeMillis();
// Имитация работы
Thread.sleep(100);
long endMillis = System.currentTimeMillis();
long durationMillis = endMillis - startMillis;
System.out.println("currentTimeMillis() duration: " + durationMillis + " ms");
// Проблема: если во время замера часы переведятся назад,
// duration может быть отрицательным или неточным.
//Решение: Измерение через nanoTime()
long startNanos = System.nanoTime();
// Имитация работы
Thread.sleep(100);
long endNanos = System.nanoTime();
long durationNanos = endNanos - startNanos;
// Конвертация в миллисекунды для читаемости
long durationMs = TimeUnit.NANOSECONDS.toMillis(durationNanos);
double preciseMs = durationNanos / 1_000_000.0;
System.out.println("nanoTime() duration: " + durationNanos + " ns");
System.out.println("nanoTime() duration (ms): " + durationMs + " ms");
System.out.println("nanoTime() duration (precise ms): " + preciseMs + " ms");
// Демонстрация для сверхбыстрых операций
long fastStart = System.nanoTime();
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i;
}
long fastEnd = System.nanoTime();
System.out.println("Loop duration: " + (fastEnd - fastStart) + " ns");
// Важно: nanoTime() нельзя использовать для получения времени суток
// System.out.println(System.nanoTime()); // Бессмысленное огромное число
}
// Правильный способ измерения среднего времени выполнения
public static double measureAverageTime(Runnable task, int iterations) {
// Прогрев JVM (warm-up) для компиляции и оптимизации кода
for (int i = 0; i < 10_000; i++) {
task.run();
}
long totalNanos = 0;
for (int i = 0; i < iterations; i++) {
long start = System.nanoTime();
task.run();
long end = System.nanoTime();
totalNanos += (end - start);
}
return (double) totalNanos / iterations;
}
}
#Java #советы
👍3🔥1
Объяснение: Метод System.nanoTime() использует наиболее точный таймер, доступный на платформе (например, TSC — Time Stamp Counter на процессорах x86).
Его точность и стабильность зависят от конкретной реализации JVM и операционной системы, но гарантированно не хуже, чем у currentTimeMillis(), а обычно на несколько порядков выше. Важно отметить, что на некоторых системах (например, виртуальные машины) nanoTime() может использовать тот же таймер, что и currentTimeMillis(), но без корректировок системного времени. При проведении микро-бенчмарков необходимо также учитывать необходимость прогрева JVM (как показано в методе measureAverageTime), чтобы код был скомпилирован JIT-компилятором, и результаты отражали реальную производительность, а не работу в режиме интерпретации.
Для серьезного бенчмаркинга следует использовать специализированные фреймворки, такие как JMH (Java Microbenchmark Harness), которые автоматически решают эти и многие другие проблемы.
#Java #советы
Его точность и стабильность зависят от конкретной реализации JVM и операционной системы, но гарантированно не хуже, чем у currentTimeMillis(), а обычно на несколько порядков выше. Важно отметить, что на некоторых системах (например, виртуальные машины) nanoTime() может использовать тот же таймер, что и currentTimeMillis(), но без корректировок системного времени. При проведении микро-бенчмарков необходимо также учитывать необходимость прогрева JVM (как показано в методе measureAverageTime), чтобы код был скомпилирован JIT-компилятором, и результаты отражали реальную производительность, а не работу в режиме интерпретации.
Для серьезного бенчмаркинга следует использовать специализированные фреймворки, такие как JMH (Java Microbenchmark Harness), которые автоматически решают эти и многие другие проблемы.
#Java #советы
👍3 3
Что выведет код?
#Tasks
package oleborn.taskswithspring.tasks.year2026;
public class Task190226 {
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
Thread.sleep(5);
long end = System.currentTimeMillis();
long startNano = System.nanoTime();
Thread.sleep(5);
long endNano = System.nanoTime();
System.out.println("Millis diff: " + (end - start));
System.out.println("Nanos diff in ms: " + (endNano - startNano) / 1_000_000);
}
}
#Tasks
👍2
Что такое switch и как он работает? 🤓
Ответ:
switch — это условный оператор, который позволяет выбирать один из нескольких блоков кода для выполнения.
Он работает с типами: byte, short, char, int, их обертками, enum, String (начиная с Java 7). Оператор сравнивает значение выражения со значениями в case. При совпадении выполняется соответствующий блок до оператора break (если break нет, выполнение проваливается в следующий case — это называется fall-through).
Начиная с Java 12, появился более лаконичный switch-выражение с -> и yield.
#собеседование
Ответ:
Он работает с типами: byte, short, char, int, их обертками, enum, String (начиная с Java 7). Оператор сравнивает значение выражения со значениями в case. При совпадении выполняется соответствующий блок до оператора break (если break нет, выполнение проваливается в следующий case — это называется fall-through).
Начиная с Java 12, появился более лаконичный switch-выражение с -> и yield.
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
История IT-технологий сегодня — 20 Февраля
ℹ️ Кто родился в этот день
Лю́двиг Бо́льцман (нем. Ludwig Eduard Boltzmann; 20 февраля 1844, Вена — 5 сентября 1906, Дуино, Цислейтания) — австрийский физик-теоретик, основатель статистической механики и молекулярно-кинетической теории. Его работы по статистической механике и энтропии напрямую предвосхитили формулу Шеннона для количества информации. Теория информации — математический фундамент сжатия данных, криптографии и передачи сигналов.
Джон Уи́ллард Ми́лнор (англ. John Willard Milnor; род. 20 февраля 1931, Ориндж[англ.], Нью-Джерси, США) — американский математик. Его работы по дифференциальной топологии и алгебраической топологии используются в компьютерной графике, геометрическом моделировании и теории сложных систем.
🌐 Знаковые события
1986 — выведен на орбиту базовый блок орбитальной станции «Мир».
#Biography #Birth_Date #Events #20февраля
Лю́двиг Бо́льцман (нем. Ludwig Eduard Boltzmann; 20 февраля 1844, Вена — 5 сентября 1906, Дуино, Цислейтания) — австрийский физик-теоретик, основатель статистической механики и молекулярно-кинетической теории. Его работы по статистической механике и энтропии напрямую предвосхитили формулу Шеннона для количества информации. Теория информации — математический фундамент сжатия данных, криптографии и передачи сигналов.
Джон Уи́ллард Ми́лнор (англ. John Willard Milnor; род. 20 февраля 1931, Ориндж[англ.], Нью-Джерси, США) — американский математик. Его работы по дифференциальной топологии и алгебраической топологии используются в компьютерной графике, геометрическом моделировании и теории сложных систем.
1986 — выведен на орбиту базовый блок орбитальной станции «Мир».
#Biography #Birth_Date #Events #20февраля
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
Раздел 8. Stream API и функциональный стиль в Java
Глава 3: Чистые преобразования и борьба с исключениями
filter, map, flatMap — кирпичи декларативности
Stream API предоставляет минимальный, но выразительный набор операций для декларативной обработки данных. Если императивный код строится из циклов, условий и временных переменных, то потоковый код компонуется из трёх фундаментальных примитивов: фильтрации, отображения и свёртки. Рассмотрим первые два и их мощное обобщение — flatMap.
filter: селекция по предикату
Операция filter принимает Predicate<T> — функцию, возвращающую boolean — и возвращает поток, содержащий только элементы, удовлетворяющие условию. Это декларативный аналог условного оператора в цикле, но без видимого управления состоянием.
Важно понимать, что filter — операция без состояния (stateless). Она не требует памяти, пропорциональной количеству элементов, и не изменяет порядок. Каждый элемент проверяется независимо, в изоляции от других. Это делает filter идеальным для распараллеливания: элементы можно проверять в любом порядке и в любых потоках без синхронизации.
Стоимость filter — O(1) на элемент плюс стоимость выполнения предиката. Предикат должен быть чистой функцией: детерминированной, без побочных эффектов, зависящей только от входного элемента.
Нарушение этого правила — распространённый источник багов:
При параллельном выполнении счётчик будет инкрементироваться из нескольких потоков без синхронизации, давая непредсказуемый результат.
#Java #для_новичков #beginner #stream_api #filter
Глава 3: Чистые преобразования и борьба с исключениями
filter, map, flatMap — кирпичи декларативности
Stream API предоставляет минимальный, но выразительный набор операций для декларативной обработки данных. Если императивный код строится из циклов, условий и временных переменных, то потоковый код компонуется из трёх фундаментальных примитивов: фильтрации, отображения и свёртки. Рассмотрим первые два и их мощное обобщение — flatMap.
filter: селекция по предикату
Операция filter принимает Predicate<T> — функцию, возвращающую boolean — и возвращает поток, содержащий только элементы, удовлетворяющие условию. Это декларативный аналог условного оператора в цикле, но без видимого управления состоянием.
Stream<Book> classics = library.stream()
.filter(book -> book.year() < 1980);
Важно понимать, что filter — операция без состояния (stateless). Она не требует памяти, пропорциональной количеству элементов, и не изменяет порядок. Каждый элемент проверяется независимо, в изоляции от других. Это делает filter идеальным для распараллеливания: элементы можно проверять в любом порядке и в любых потоках без синхронизации.
Стоимость filter — O(1) на элемент плюс стоимость выполнения предиката. Предикат должен быть чистой функцией: детерминированной, без побочных эффектов, зависящей только от входного элемента.
Нарушение этого правила — распространённый источник багов:
// Антипаттерн: предикат с побочным эффектом
AtomicInteger counter = new AtomicInteger(0);
Stream<Book> everyThird = library.stream()
.filter(b -> counter.incrementAndGet() % 3 == 0); // Недетерминировано при parallel()
При параллельном выполнении счётчик будет инкрементироваться из нескольких потоков без синхронизации, давая непредсказуемый результат.
#Java #для_новичков #beginner #stream_api #filter
👍4
map: структурная трансформация
Операция map принимает Function<T, R> и применяет её к каждому элементу, заменяя его результатом функции. Это преобразование типа: из потока книг делаем поток названий, из потока строк — поток их длин, из потока заказов — поток сумм.
map также stateless и ленив. Функция вызывается только при запросе элемента терминальной операцией. Стоимость — O(1) на элемент плюс стоимость функции. Функция должна быть чистой: один и тот же вход всегда даёт один и тот же выход, без модификации внешнего состояния.
Композиция map позволяет выстраивать цепочки преобразований, читаемые справа налево или через method reference:
Каждый map — отдельный узел конвейера, отдельный объект в памяти. Для простых преобразований это приемлемо, но для тяжёлых операций композиция нескольких map может быть менее эффективна, чем одно комплексное преобразование, из-за накладных расходов на вызовы лямбд и промежуточные объекты.
flatMap: разворачивание иерархий
flatMap — наиболее мощный и наиболее сложный из трёх примитивов. Он решает проблему, которую map решить не может: когда один элемент источника порождает ноль, один или множество элементов результата.
Сигнатура flatMap требует функцию, возвращающую Stream<R> вместо R. Операция применяет эту функцию к каждому элементу, получает потоки результатов и сливает их в единый выходной поток. Это аналог вложенных циклов или операции "расплющивания" (flattening) в функциональных языках.
Классический сценарий: коллекция внутри коллекции
Предположим, у нас есть список заказов, каждый содержит список позиций.
Нужно получить поток всех позиций всех заказов:
Здесь order -> order.getItems().stream() — функция, превращающая один Order в Stream<OrderItem>. flatMap выполняет слияние этих потоков, устраняя уровень вложенности. Результат — плоский поток всех элементов.
Нулевое и единичное отображение
flatMap универсален: он обрабатывает все случаи количества выходных элементов на входной:
Ноль элементов: возвращаем Stream.empty(), элемент исключается из результата
Один элемент: возвращаем Stream.of(element), эквивалентно map с последующим filter(Objects::nonNull)
Множество элементов: возвращаем collection.stream(), все элементы включаются в результат
#Java #для_новичков #beginner #stream_api #filter #flatMap #map
Операция map принимает Function<T, R> и применяет её к каждому элементу, заменяя его результатом функции. Это преобразование типа: из потока книг делаем поток названий, из потока строк — поток их длин, из потока заказов — поток сумм.
Stream<String> titles = library.stream()
.map(Book::title);
Stream<Integer> lengths = titles.map(String::length);
map также stateless и ленив. Функция вызывается только при запросе элемента терминальной операцией. Стоимость — O(1) на элемент плюс стоимость функции. Функция должна быть чистой: один и тот же вход всегда даёт один и тот же выход, без модификации внешнего состояния.
Композиция map позволяет выстраивать цепочки преобразований, читаемые справа налево или через method reference:
// Цепочка преобразований: Book -> String -> Integer -> String
String summary = book.stream()
.map(Book::author) // Book -> String
.map(String::toUpperCase) // String -> String
.map(author -> author.substring(0, 3)) // String -> String
.map(prefix -> "Author: " + prefix) // String -> String
.findFirst()
.orElse("Unknown");
Каждый map — отдельный узел конвейера, отдельный объект в памяти. Для простых преобразований это приемлемо, но для тяжёлых операций композиция нескольких map может быть менее эффективна, чем одно комплексное преобразование, из-за накладных расходов на вызовы лямбд и промежуточные объекты.
flatMap: разворачивание иерархий
flatMap — наиболее мощный и наиболее сложный из трёх примитивов. Он решает проблему, которую map решить не может: когда один элемент источника порождает ноль, один или множество элементов результата.
Сигнатура flatMap требует функцию, возвращающую Stream<R> вместо R. Операция применяет эту функцию к каждому элементу, получает потоки результатов и сливает их в единый выходной поток. Это аналог вложенных циклов или операции "расплющивания" (flattening) в функциональных языках.
Классический сценарий: коллекция внутри коллекции
Предположим, у нас есть список заказов, каждый содержит список позиций.
Нужно получить поток всех позиций всех заказов:
List<Order> orders = fetchOrders();
// Императивный подход: вложенные циклы, временная коллекция
List<OrderItem> allItems = new ArrayList<>();
for (Order order : orders) {
for (OrderItem item : order.getItems()) {
allItems.add(item);
}
}
// Декларативный подход: flatMap
Stream<OrderItem> itemStream = orders.stream()
.flatMap(order -> order.getItems().stream());
Здесь order -> order.getItems().stream() — функция, превращающая один Order в Stream<OrderItem>. flatMap выполняет слияние этих потоков, устраняя уровень вложенности. Результат — плоский поток всех элементов.
Нулевое и единичное отображение
flatMap универсален: он обрабатывает все случаи количества выходных элементов на входной:
Ноль элементов: возвращаем Stream.empty(), элемент исключается из результата
Один элемент: возвращаем Stream.of(element), эквивалентно map с последующим filter(Objects::nonNull)
Множество элементов: возвращаем collection.stream(), все элементы включаются в результат
// Фильтрация через flatMap: ноль или один элемент
Stream<Book> validBooks = rawData.stream()
.flatMap(data -> {
try {
Book book = parseBook(data); // Может бросить исключение
return Stream.of(book); // Один элемент при успехе
} catch (ParseException e) {
return Stream.empty(); // Ноль элементов при ошибке
}
});
#Java #для_новичков #beginner #stream_api #filter #flatMap #map
👍5
Сложность и читаемость
Мощь flatMap оборачивается риском чрезмерной сложности.
Вложенные flatMap создают "лестницу" преобразований, сложную для восприятия:
Этот код эквивалентен четырём вложенным циклам. Он работает, но его трудно читать и отлаживать. Каждый уровень вложенности добавляет лексический контекст, и переменные верхних уровней (order, shipment) доступны во вложенных лямбдах, создавая неявные зависимости.
Рефакторинг через промежуточные методы улучшает читаемость:
Или использование mapMulti (Java 16+) — операции, комбинирующей map и flatMap с более эффективной реализацией для множественного вывода:
mapMulti избегает создания промежуточных объектов Stream для каждого элемента, что снижает нагрузку на GC при глубокой вложенности.
Анализ стоимости
filter: O(1) на элемент. Память: O(1). Параллелизм: отличный, нет синхронизации.
map: O(1) на элемент плюс стоимость функции. Память: O(1) для примитивов, O(1) для ссылок (создаётся новый объект результата). Параллелизм: отличный.
flatMap: O(1) на элемент плюс стоимость создания выходного потока плюс стоимость его обхода. Память: зависит от размера выходных потоков. Параллелизм: сложный — слияние множественных потоков требует координации.
Скрытая стоимость flatMap — в аллокациях. Каждый вызов функции создаёт новый объект Stream (обычно через Collection.stream() или Stream.of()). Для больших коллекций это давит на Young Generation. Оптимизация через mapMulti или специализированные сплитераторы уместна в критичных секциях.
Композиция как язык запросов
Комбинация filter, map, flatMap образует внутренний DSL (domain-specific language) для преобразования данных.
Хорошо спроектированный конвейер читается как предложение на естественном языке:
Каждая операция — глагол, каждый метод reference или лямбда — дополнение. Порядок операций отражает логику обработки: сначала сужаем (filter), затем расплющиваем (flatMap), затем нормализуем (distinct, sorted).
#Java #для_новичков #beginner #stream_api #filter #flatMap #map
Мощь flatMap оборачивается риском чрезмерной сложности.
Вложенные flatMap создают "лестницу" преобразований, сложную для восприятия:
// Антипаттерн: глубокая вложенность flatMap
Stream<String> deepStream = orders.stream()
.flatMap(order -> order.getShipments().stream()
.flatMap(shipment -> shipment.getPackages().stream()
.flatMap(pkg -> pkg.getItems().stream()
.flatMap(item -> item.getTags().stream()))));
Этот код эквивалентен четырём вложенным циклам. Он работает, но его трудно читать и отлаживать. Каждый уровень вложенности добавляет лексический контекст, и переменные верхних уровней (order, shipment) доступны во вложенных лямбдах, создавая неявные зависимости.
Рефакторинг через промежуточные методы улучшает читаемость:
// Лучше: именованные методы с ясной семантикой
Stream<String> tags = orders.stream()
.flatMap(this::extractShipments)
.flatMap(this::extractPackages)
.flatMap(this::extractItems)
.flatMap(this::extractTags);
private Stream<Shipment> extractShipments(Order order) {
return order.getShipments().stream();
}
// ... и так далее
Или использование mapMulti (Java 16+) — операции, комбинирующей map и flatMap с более эффективной реализацией для множественного вывода:
// mapMulti: более эффективен, чем flatMap, для множественных элементов
Stream<String> tags = orders.stream()
.<String>mapMulti((order, consumer) -> {
for (Shipment s : order.getShipments()) {
for (Package p : s.getPackages()) {
for (Item i : p.getItems()) {
for (String tag : i.getTags()) {
consumer.accept(tag); // Прямая передача без создания Stream
}
}
}
}
});
mapMulti избегает создания промежуточных объектов Stream для каждого элемента, что снижает нагрузку на GC при глубокой вложенности.
Анализ стоимости
filter: O(1) на элемент. Память: O(1). Параллелизм: отличный, нет синхронизации.
map: O(1) на элемент плюс стоимость функции. Память: O(1) для примитивов, O(1) для ссылок (создаётся новый объект результата). Параллелизм: отличный.
flatMap: O(1) на элемент плюс стоимость создания выходного потока плюс стоимость его обхода. Память: зависит от размера выходных потоков. Параллелизм: сложный — слияние множественных потоков требует координации.
Скрытая стоимость flatMap — в аллокациях. Каждый вызов функции создаёт новый объект Stream (обычно через Collection.stream() или Stream.of()). Для больших коллекций это давит на Young Generation. Оптимизация через mapMulti или специализированные сплитераторы уместна в критичных секциях.
Композиция как язык запросов
Комбинация filter, map, flatMap образует внутренний DSL (domain-specific language) для преобразования данных.
Хорошо спроектированный конвейер читается как предложение на естественном языке:
// "Найти все уникальные теги из активных заказов, отсортированные по алфавиту"
List<String> sortedUniqueTags = orders.stream()
.filter(Order::isActive) // которые активны
.flatMap(o -> o.getTags().stream()) // извлечь все теги
.distinct() // уникальные
.sorted() // отсортированные
.collect(toList()); // собрать в список
Каждая операция — глагол, каждый метод reference или лямбда — дополнение. Порядок операций отражает логику обработки: сначала сужаем (filter), затем расплющиваем (flatMap), затем нормализуем (distinct, sorted).
#Java #для_новичков #beginner #stream_api #filter #flatMap #map
👍3