Секреты Java
Відкрити в Telegram
Обсуждаем Java, архитектуру, фреймворки и всё, о чем должны говорить разработчики. Владелец: @Alexey_Morl Реклама на бирже: https://telega.in/c/java_secrets
Показати більше7 991
Підписники
Немає даних24 години
Немає даних7 днів
Немає даних30 день
Архів дописів
7 991
Maven. Фазы и команды
Maven использует жизненный цикл сборки, который делится на несколько фаз. Фазы определяют последовательность задач, которые Maven выполняет для сборки и управления проектом.
Основные фазы жизненного цикла Maven включают:
1️⃣ validate – Проверяет, что проект правильный и вся необходимая информация указана.
2️⃣ compile – Компилирует исходный код проекта.
3️⃣ test – Запускает тесты (обычно с использованием JUnit или TestNG) и проверяет, что они проходят успешно.
4️⃣ package – Собирает скомпилированный код и пакует его, например, в JAR или WAR-файл.
5️⃣ verify – Проверяет собранные артефакты и результаты тестов.
6️⃣ install – Устанавливает пакет в локальный репозиторий для использования как зависимость в других проектах.
7️⃣ deploy – Отправляет финальный пакет в удаленный репозиторий для использования в других проектах или на сервере.
Основные команды Maven:
✔️
mvn clean – Удаляет папку target, очищая проект от предыдущих сборок.
✔️ mvn compile – Компилирует исходный код проекта.
✔️ mvn test – Запускает тесты.
✔️ mvn package – Пакует скомпилированный код в конечный артефакт (обычно JAR или WAR).
✔️ mvn install – Устанавливает артефакт в локальный репозиторий.
✔️ mvn deploy – Деплоит артефакт в удаленный репозиторий.
✔️ mvn site – Генерирует документацию проекта на основе кода и зависимостей.
Фазы выполняются последовательно, то есть если вы запускаете команду mvn install, Maven автоматически пройдет через все предыдущие фазы – от validate до install.
Примеры команд:
✔️ mvn clean install – очищает проект, компилирует, тестирует и устанавливает артефакт в локальный репозиторий.
✔️ mvn package -DskipTests – собирает проект в артефакт, пропуская тесты.7 991
✅ Redis как брокер сообщений
Redis не является брокером сообщений, но всё-таки позволяет реализовать простую модель обмена сообщениями. В отличие от RabbitMQ и Kafka, с помощью Redis не получится настроить сложную маршрутизацию. Сообщения будут отправляться в определенные каналы, из которых подписчики будут получать данные.
Для реализации обмена сообщений необходимо настроить подключение к Redis. Как это сделать, было рассказано в предыдущих статьях.
Создадим класс для отправки сообщений в нужный топик(канал):
@Service
@RequiredArgsConstructor
public class Producer {
private final RedisTemplate<String, String> redisTemplate;
private final ChannelTopic channelTopic;
public void produce(String message) {
redisTemplate.convertAndSend(channelTopic.getTopic(), message);
}
}
Данный класс публикует сообщение, используя RedisTemplate. Метод convertAndSend принимает название топика и само сообщение. В этом примере используется класс ChannelTopic из пакета org.springframework.data.redis.listener. Вместо этого можно передавать сроковое значение, которое будет содержать название очереди.
Создадим класс, в котором будет содержаться логика обработки сообщений:
@Service
public class Receiver {
public void handleMessage(String message) {
System.out.println("Received: " + message);
}
}
В классе конфигурации добавим следующие бины:
@Bean
public ChannelTopic channelTopic() {
return new ChannelTopic("channel");
}
ChannelTopic - класс хранящий название топика для сообщений.
@Bean
public MessageListenerAdapter messageListenerAdapter(Receiver receiver) {
return new MessageListenerAdapter(receiver);
}
MessageListenerAdapter - класс, являющийся подписчиком для топика. Этот класс делегирует обработку сообщения пользовательскому классу, который должен реализовывать интерфейс MessageListener либо содержать метод с названием handleMessage. Название метода можно изменить, но тогда при определении MessageListenerAdapter необходимо указать название метода, который будет получать сообщения.
@Bean
public RedisMessageListenerContainer сontainer(RedisConnectionFactory redisConnectionFactory,MessageListenerAdapter messageListenerAdapter, ChannelTopic channelTopic) {
var container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
container.addMessageListener(messageListenerAdapter, channelTopic);
return container;
}
RedisMessageListenerContainer - это класс, который обрабатывает низкоуровневые детали получения и преобразования сообщений. С помощью метода addMessageListener() указывается слушатель сообщений и название топика, из которого будут читаться данные.
Redis может быть хорем выбор в качестве брокера сообщений из-за высокой скорости работы. Но если важна надежность, гарантия доставки сообщений и сложная маршрутизация, то лучше сделать выбор в пользу RabbitMQ или Kafka.7 991
Как устроен HashMap?
HashMap состоит из «корзин» (bucket). С технической точки зрения «корзины» — это элементы массива, которые хранят ссылки на списки элементов. При добавлении новой пары «ключ-значение», вычисляет хэш-код ключа, на основании которого вычисляется номер корзины (номер ячейки массива), в которую попадет новый элемент.
Если корзина пустая, то в нее сохраняется ссылка на вновь добавляемый элемент, если же там уже есть элемент, то происходит последовательный переход по ссылкам между элементами в цепочке, в поисках последнего элемента, от которого и ставится ссылка на вновь добавленный элемент. Если в списке был найден элемент с таким же ключом, то он заменяется.
7 991
✅ Приоритет логических операций.
В языке Java существует определенный порядок выполнения булевых операций, который регулируется приоритетами операторов. Правильное понимание этих приоритетов позволяет избежать ошибок при написании логических выражений и делает код более читаемым.
▫️Основные булевые операторы в Java:
1.
! – логическое отрицание (унарный оператор)
2. && – логическое И (конъюнкция)
3. || – логическое ИЛИ (дизъюнкция)
▫️Приоритеты операторов:
1. Логическое отрицание (!) имеет самый высокий приоритет.
2. Логическое И (&&) выполняется следующим.
3. Логическое ИЛИ (||) имеет наименьший приоритет.
Это означает, что операции с логическим отрицанием выполняются в первую очередь, затем выполняется логическое И, а логическое ИЛИ — последним. Однако, чтобы избежать неоднозначности или явно указать порядок операций, часто используют скобки.
Пример 1: Без скобок
boolean a = true;
boolean b = true;
boolean c = false;
System.out.println(a || b && c); // true
Здесь сначала выполняется операция b && c, так как оператор && имеет более высокий приоритет, чем ||. Результат выражения b && c равен false, после чего выполняется операция a || false, которая возвращает true.
Пример 2: С использованием скобок
boolean a = true;
boolean b = true;
boolean c = false;
System.out.println((a || b) && c); // false
В этом случае сначала выполняется операция a || b, которая возвращает true. Затем выполняется true && c, что вернет уже false.
Пример 3: Логическое отрицание
boolean a = true;
boolean b = false;
System.out.println(!a || b); // false
Здесь сначала выполняется !a, которая возвращает false. Затем выражение false || b возвращает false.
Рекомендации:
- Всегда используйте скобки, чтобы явно указывать порядок выполнения операций, особенно в сложных выражениях.
- Правильное использование операторов и скобок улучшает читаемость кода, даже если вы уверены в порядке выполнения операций.
Булевые выражения часто встречаются в условиях и логике программ, поэтому понимание их приоритетов — ключ к написанию корректного кода.7 991
Более эффективная работа с коллекциями
Java предоставляет множество мощных инструментов для работы с коллекциями. Одним из них является
Collectors.partitioningBy, который позволяет разделить элементы потока на две группы на основе заданного предиката.
Эта функция упрощает обработку и классификацию данных.7 991
Перегрузка метода
Перегрузка методов (Method Overloading) — это возможность создания нескольких методов с одинаковым именем, но разными типами или количеством параметров.
Основные принципы:
1. Различие в количестве параметров: перегруженные методы должны отличаться количеством параметров.
2. Различие в типах параметров: можно перегружать метод, изменяя типы параметров.
3. Порядок параметров: также можно перегрузить метод, изменив порядок параметров разного типа.
7 991
Может ли имя класса не совпадать с именем файла?
Компилятор требует, чтобы в .java файле был не больше чем один публичный класс верхнего уровня, и чтобы его название совпадало с названием файла. Все специальные символы также должны быть в имени файла.
Protected и private классов верхнего уровня не бывает в принципе, а вот на package-protected это ограничение не распространяется. Это значит, что класс без модификатора доступа может иметь любое имя. Также это значит, что рядом с основным публичным классом файла (или вместо него) можно объявить любое количество других классов без модификатора доступа, с произвольными именами. Они будут доступны внутри всего пакета.
Так что ответ – может.
7 991
Как реализовать метод hashCode?
Если вы переопределили equals(), то обязательно также переопределить и hashCode(). Это не просто теоретическое требование. Если класс нарушает это правило, хранение его экземпляров в качестве например ключей HashMap приводит к непредсказуемому поведению.
Результат hashCode() должен быть одинаковый для равных в смысле equals объектов. Обычно для этого значение хэш-кода вычисляется на основе значений полей, которые участвуют в equals(). Но и возвращение одной и той же константы 42 для любого экземпляра класса тоже будет валидной реализацией.
Результат hashCode() должен быть равномерно распределен. Это правило не такое строгое как остальные. Его нарушение не сломает программу, хотя может сильно ухудшить производительность. Поэтому константа 42 – допустимая, но не лучшая идея. Вместо этого все значения полей сначала приводятся к int: boolean превращается в любую пару констант, null в 0, для ссылочных типов берется их hashCode(). Затем все эти значения смешиваются с помощью бинарного оператора XOR (^). Дополнительно для лучшего распределения можно применять битовые сдвиги. Если вы владеете информацией о распределении значений полей в конкретно вашем случае, эту реализацию можно улучшить.
Результат hashCode() должен быть одинаковый на протяжении времени жизни объекта. Если вычисление хэш-кода зависит от переменных значений, сохраните его значение во внутреннее поле при первом вызове. При следующих вызовах сразу возвращайте это закэшированное значение.
7 991
⌨️ Усечение потока данных. Метод limit
Потоки данных поддерживают метод
limit(n). Он возвращает еще один поток, не превышающий заданную длину, переданную в limit в виде аргумента. Если поток данных упорядоченный, возвращаются первые элементы, но не более чем n.
Например, с помощью следующего кода можно создать List, выбрав первые три блюда, содержащие более 300 калорий:
List<Dish> dishes = specialMenu
.stream()
.filter(dish -> dish.getCalories() > 300)
.limit(3)
.collect(toList());
Метод limit работает и для неупорядоченных потоков данных (например, когда источник представляет собой объект типа Set). В этом случае не следует исходить из допущения о какой-либо упорядоченности результата, возвращенного методом limit.7 991
✅Метод Arrays.parallelSort()
Метод
parallelSort() из класса Arrays в Java — это мощный инструмент для сортировки массивов, появившийся в Java 8, который использует многопоточность для ускорения процесса сортировки. Этот метод особенно полезен при работе с большими массивами, где традиционная сортировка может занимать значительное время.
Как работает parallelSort()?
Метод parallelSort() использует стратегию разделяй и властвуй (divide and conquer). Массив делится на части, и каждая часть сортируется в отдельном потоке параллельно с другими частями. После этого результаты объединяются в отсортированный массив.
Пример использования parallelSort()
Для простого примера рассмотрим, как отсортировать массив целых чисел с помощью parallelSort().
import java.util.Arrays;
public class ParallelSortExample {
public static void main(String[] args) {
int[] numbers = {5, 8, 1, 6, 9, 2, 4, 7, 3};
Arrays.parallelSort(numbers);
System.out.println("Отсортированный массив: " + Arrays.toString(numbers));
}
}
Результат:
Отсортированный массив: [1, 2, 3, 4, 5, 6, 7, 8, 9]Чем отличается от обычного
sort()?
Основное отличие parallelSort() от обычного метода Arrays.sort() заключается в том, что parallelSort() использует несколько потоков для сортировки. Это означает, что для больших массивов parallelSort() может работать быстрее, так как сортировка выполняется параллельно.
Однако важно отметить, что для небольших массивов разница в производительности может быть незначительной или даже в пользу sort(), так как создание и управление потоками имеет свои накладные расходы.
Параметры для parallelSort()
Вы можете использовать перегруженные версии метода parallelSort(), которые принимают диапазоны индексов для сортировки только части массива:
import java.util.Arrays;
public class ParallelSortRangeExample {
public static void main(String[] args) {
int[] numbers = {5, 8, 1, 6, 9, 2, 4, 7, 3};
// Сортировка только части массива с 2-го по 6-й индекс
Arrays.parallelSort(numbers, 2, 7);
System.out.println("Частично отсортированный массив: " + Arrays.toString(numbers));
}
}
Результат:
Частично отсортированный массив: [5, 8, 1, 2, 4, 6, 9, 7, 3]Когда использовать
parallelSort()?
Например, когда:
▫️Размер массива достаточно велик, чтобы оправдать расходы на создание потоков.
▫️Вы работаете в многопоточной среде и хотите ускорить сортировку данных.
▫️Ваши данные требуют оптимальной производительности на многоядерных процессорах.
Заключение
Метод parallelSort() — это отличное решение для быстрого выполнения сортировки на больших массивах данных, особенно в системах с многоядерными процессорами. Однако, если массив небольшой, использование обычного метода sort() может быть более эффективным из-за отсутствия накладных расходов на управление потоками.
UPD: Как отметили в комментах про условия параллельного выполнения сортировки: метод выполняет параллельную сортировку только если ядер на машине больше одного и кол-во элементов в сортируемом массиве больше 4096, тогда используется ForkJoinPool для разделения.7 991
Класс Hashtable
Представляет собой структуру данных, которая используется для хранения и управления коллекциями пар «ключ-значение». Hashtable является одной из структур данных, предоставляемых Java Collections Framework, и работает по принципу хеш-таблицы. Он был представлен в ранних версиях Java и является устаревшим начиная с Java 5, так как более современные альтернативы, такие как HashMap, предпочтительнее.
7 991
Как выполнить две задачи параллельно?
Простейший, путь – явно создать два объекта типа Thread, передать им инстансы Runnable, с нужными задачами в реализации их методов run, и запустить вызвав thread.start(). Если в основном потоке нужно дождаться завершения задач – после start() вызывается метод thread.join(). Исполнение зависнет на вызове этого метода до тех пор, пока тред не закончит свою задачу и не умрет. Вся работа задач с внешними данными должна быть синхронизирована.
Такое ручное создание тредов полезно в учебных целях, но считается плохой практикой в промышленном коде: само создание – дорогостоящая операция, а большое количество случайно созданных потоков может приводить к проблеме голодания (starvation) потоков.
В качестве продвинутой альтернативы используются пуллы потоков – реализации интерфейса ExecutorService. Такие сервисы создаются статическими фабричными методами класса Executors. Они умеют принимать задачи в виде Runnable- или Callable-объектов на заранее созданном наборе потоков (собственно, пулле).
Кроме самого пулла, экземпляры ExecutorService содержат фабрику потоков («инструкцию» как создать тред при необходимости), и коллекцию-очередь задач на исполнение.
В ответ на передачу на исполнение Runnable или Callable, сервис возвращает связанный с ним объект типа Future – хранилище, которое будет заполнено результатом выполнения задачи в будущем. Даже если никакого результата не ожидается, Future поможет дождаться момента завершения обработки задачи.
В Android для асинхронного выполнения используется похожая сущность – Looper.
7 991
💡 Совет
show-sql - это один из самых простых (но не самый оптимальный) способ посмотреть SQL запросы, сгенерированные Hibernate.
Вы можете немного улучшить его, включив форматирование, показ JPQL-запроса, сгенерировавшего запрос, и включив подсветку, которая добавляет некоторый цвет к выводу.7 991
Как добавить элемент в середину массива?
Есть несколько способов добавить элемент в середину массива. Однако, поскольку массивы имеют фиксированную длину, напрямую добавить элемент в существующий массив нельзя — нужно создавать новый массив с увеличенной длиной и копировать данные. Рассмотрим несколько способов:
1️⃣ Копирование вручную
Суть в том что бы создать новый массив на 1 элемент больше оригинального и перенести элементы из старого, не забыв при этом вставить новый элемент.
int[] array = {1, 2, 4, 5};
int index = 2; // индекс, куда вставить новый элемент
int newElement = 3;
// Создаем новый массив на 1 больше
int[] newArray = new int[array.length + 1];
// Копируем элементы до позиции вставки
for (int i = 0; i < index; i++) {
newArray[i] = array[i];
}
// Вставляем новый элемент
newArray[index] = newElement;
// Копируем оставшиеся элементы
for (int i = index + 1; i < newArray.length; i++) {
newArray[i] = array[i - 1];
}
// Печатаем новый массив
System.out.println(Arrays.toString(newArray)); // [1, 2, 3, 4, 5]
2️⃣ Использование коллекций (например, ArrayList)
ArrayList динамический по размеру, и можно легко вставить элемент в любое место с помощью метода add(index, element).
Integer[] array = {1, 2, 4, 5};
int index = 2; // индекс, куда вставить новый элемент
int newElement = 3;
List<Integer> list = new ArrayList<>(Arrays.asList(array));
// Вставляем элемент
list.add(index, newElement);
// Создаем новый массив на 1 больше
Integer[] newArray = list.toArray(array);
// Печатаем новый массив
System.out.println(Arrays.toString(newArray)); // [1, 2, 3, 4, 5]
3️⃣ Использование метода System.arraycopy
Метод System.arraycopy позволяет эффективно копировать части массива.
int[] array = {1, 2, 4, 5};
int index = 2; // индекс, куда вставить новый элемент
int newElement = 3;
// Создаем новый массив на 1 больше
int[] newArray = new int[array.length + 1];
// Копируем элементы до позиции вставки
System.arraycopy(array, 0, newArray, 0, index);
// Вставляем новый элемент
newArray[index] = newElement;
// Копируем оставшиеся элементы
System.arraycopy(array, index, newArray, index + 1, array.length - index);
// Печатаем новый массив
System.out.println(Arrays.toString(newArray)); // [1, 2, 3, 4, 5]