ru
Feedback
Секреты Java

Секреты Java

Открыть в Telegram

Обсуждаем Java, архитектуру, фреймворки и всё, о чем должны говорить разработчики. Владелец: @Alexey_Morl Реклама на бирже: https://telega.in/c/java_secrets

Больше
7 991
Подписчики
Нет данных24 часа
Нет данных7 дней
Нет данных30 день
Архив постов
Какое худшее время работы метода get(key) для ключа, который есть в HashMap? O(N). Худший случай - это поиск ключа в HashMap, вырожденного в список по причине совпадения ключей по hashCode() и для выяснения хранится ли элемент с определённым ключом может потребоваться перебор всего списка. Но начиная с Java 8, после определенного числа элементов в списке, связный список преобразовывается в красно-черное дерево и сложность выборки, даже в случае плохой хеш-функции, не хуже логарифмической O(log(N)).

Метод before() Метод before() класса Date проверяет, была ли наша дата раньше той, которую мы передаем в качестве аргумента.
Метод before() Метод before() класса Date проверяет, была ли наша дата раньше той, которую мы передаем в качестве аргумента.

Как прочитать файл из classpath? Часто в коде бывает необходимо прочесть файл из classpath, например с конфигами или какими-либо ресурсами. Предположим у нас есть файл demo.txt, который лежит в
src/main/resources/config.
Рассмотрим варианты, как можно прочитать этот файл. ▫️Загрузка через ClassLoader:
InputStream in = this.getClass().getClassLoader()
    .getResourceAsStream("config/demo.txt");
System.out.println(new String(in.readAllBytes()));
В ClassLoader все пути абсолютные, поэтому вам не нужен слеш в начале. ▫️Загрузка через класс:
InputStream in = this.getClass()
    .getResourceAsStream("/config/demo.txt");
System.out.println(new String(in.readAllBytes()));
А вот через класс путь будет относительным, поэтому потребуется слеш в начале для пути к файлу, лежащем в другой директории. ▫️Еще вариант - получить ClassLoader из текущего потока:
ClassLoader loader = Thread.currentThread()
    .getContextClassLoader();
InputStream in = loader
    .getResourceAsStream("config/demo.txt");
System.out.println(new String(in.readAllBytes()));
Так же путь абсолютный.

Как конвертировать ArrayList в массив на Java? Рассмотрим пример простой программы, которая преобразовывает ArrayList в масси
Как конвертировать ArrayList в массив на Java? Рассмотрим пример простой программы, которая преобразовывает ArrayList в массив в Java. Мы используем реализацию метода toArray(T[] a) класса ArrayList, чтобы конвертировать ArrayList в массив на Java. «Под капотом» этого метода используется метод Arrays.copyOf(), чтобы конвертировать ArrayList в Array.

Аннотация @Value в Spring Аннотация @Value в Spring Framework позволяет внедрять значения свойств прямо в поля, конструкторы или параметры методов Spring-компонентов. Это удобный способ управлять настройками приложения из внешних файлов конфигурации или переменных окружения. Примеры использования Допустим, у нас есть файл application.properties:
app.server.value=My value
И поле в нужном нам классе:
@Value("${app.server.value}")
private String value;
По умолчанию Spring будет искать это свойство в application.properties(yml), но можно указать другой файл с помощью аннотации @PropertySource:
@Configuration
@PropertySource("classpath:another.properties")
public class Config {

  @Value("${app.server.value}")
  private String value;
  ...
}
Причем интересно, что Spring сначала все таки попробует найти значение в application.properties и если не найдет, пойдет искать в указанный файл. И это только начало! Внедряйте значение прямо из аннотации:
@Value("My valyue")
private String value;
Или установите дефолтное значение:
@Value("${app.server.value:some default value}")
private String value;
@Value может быть использована для внедрения значений переменных окружения или системных свойств. Если вы зададите системное свойство, то у него будет приоритет перед файлами свойств. Можно внедрять и массив значений:
app.server.value=first, second, third
Свойство выше будет установлено в массив:
@Value("${app.server.values}")
private String[] values;
Значения, внедряемые с помощью @Value, могут содержать выражения SpEL (Spring Expression Language), что позволяет проводить сложные преобразования и комбинировать значения в различных контекстах, но это тема отдельного поста. Напомню еще раз - все вышеуказанные варианты можно применять и для параметров конструкторов и методов. Аннотация @Value предоставляет удобный способ внедрять значения из внешних источников в Spring-компоненты, что делает конфигурацию приложения более гибкой и управляемой.

URL, URI и URN: чем они отличаются? URL, URI и URN - это основные термины для идентификации ресурсов в интернете и мы часто их встречали (особенно первые два). Давайте попробуем разобраться в них. ▫️URL (Uniform Resource Locator/Унифицированный указатель местоположения ресурса) Указывает расположение ресурса и схему доступа к нему. Из стандарта URL:
URL-адреса используются для «определения расположения» ресурсов путем предоставления абстрактной идентификации расположения ресурса. Обнаружив ресурс, система может выполнять с ним различные операции, которые можно охарактеризовать такими словами, как «доступ», «обновление», «замена», «поиск атрибутов».
Любой URL можно представить в виде
<scheme>:<scheme-specific-part>
Где <scheme> - это схема обращения к ресурсу (чаще всего это сетевой протокол), a <scheme-specific-part> - часть URL специфичная для этой схемы. Например для протокола http, URL будет вида:
http://<host>:<port>/<path>?<searchpart>
а для Telnet:
telnet://<user>:<password>@<host>:<port>/
▫️URN (Uniform Resource Name/Унифицированное имя ресурса) Из стандарта URN:
Унифицированные имена ресурсов (URN) предназначены для использования в качестве постоянных, независимых от местоположения, идентификаторов ресурсов и предназначены для облегчения сопоставления других пространств имен (которые разделяют свойства URN) с URN-пространством.
URN определяет уникальное имя ресурса, и в отличии от URL, не содержит данных о местоположении ресурса и способе доступа к нему. URN появилась, чтобы закрыть недостатки URL - ресурсы перемещаются по сети, а URL остаются, указывая на уже отсутствующие ресурсы. А URN постоянны и не зависят от систем и протоколов. Пример URN:
urn:isbn:9785389049949
Этот URN идентифицирует книгу по ее ISBN номеру, где isbn - идентификатор пространства имён, а 9785389049949 - строка из данного пространства имен(как идентификатор в пространстве).

Варианты конкатенации строк В Java существует несколько способов конкатенации строк, каждый из которых имеет свои особенности и области применения. Рассмотрим основные методы: ▫️Оператор "+" Самый простой и интуитивно понятный способ.
var result = "Hello" + " " + "World";
▫️StringBuilder Является более эффективным способом конкатенации строк, особенно в циклах или при частом изменении строк.
var sb = new StringBuilder("Hello");
sb.append(" ");
sb.append("World");
sb.toString(); // Hello World
Но такой вариант не потокобезопасен. ▫️StringBuffer Работает аналогично StringBuilder, но он синхронизирован и, следовательно, потокобезопасен.
var sb = new StringBuffer("Hello");
sb.append(" ");
sb.append("World");
sb.toString(); // Hello World
Но потокобезопасноть идет в ущерб производительности по сравнению со StringBuilder. ▫️String.concat()
var str = "Hello";
var result = str.concat(" ").concat("World"); // Hello World
Помните, что оригинальная строка не меняется! Производительнее оператора "+", принимает параметром только String. В отличии от "+" при аргументе null кидает NPE. ▫️String.join() Позволяет конкатенировать строки с использованием разделителя (в примере первый аргумент).
var result = String.join(" ", "Hello", "World"); // Hello World
Следующие аргументы - последовательность! Т.е. можете передать несколько параметров, или закинуть массив. Лучше использовать когда есть массив строк, чем конкатенировать их в цикле. ▫️String.format() Позволяет форматировать строки с помощью шаблонов, что может сделать код более читабельным.
var str1 = "Hello";
var str2 = "World";
var result = String.format("%s %s", str1, str2); // Hello World
Меньшая производительность по сравнению с другими методами и более сложный синтаксис для простых задач. Выбор метода, конечно, зависит от конкретных задач и требований.

Встроенные исключения Java определяет несколько классов исключений внутри стандартного пакета java.lang. Наиболее общие из эт
Встроенные исключения Java определяет несколько классов исключений внутри стандартного пакета java.lang. Наиболее общие из этих исключений являются подклассами стандартного типа RuntimeException. Поскольку java.lang неявно импортируется во все java-программы, то большинство исключений, полученных из RuntimeException, автоматические. Java определяет несколько других типов исключений, которые относятся к его различным библиотекам класса. Ниже приведен список неконтролируемых исключений на этапе выполнения (Unchecked RuntimeException).

Что такое mutable? Mutable называются объекты, чьи состояния и переменные можно изменить после создания. Например такие класс
Что такое mutable? Mutable называются объекты, чьи состояния и переменные можно изменить после создания. Например такие классы, как StringBuilder, StringBuffer.

Final класс Нельзя расширять класс, объявленный как final. Проще говоря, никакой класс не может наследоваться от данного. Пре
Final класс Нельзя расширять класс, объявленный как final. Проще говоря, никакой класс не может наследоваться от данного. Прекрасным примером final класса в JDK является String. Первый шаг к созданию неизменяемого класса — пометить его как final, и тогда нельзя будет его расширить.

Паттерны проектирования Шаблон (паттерн) проектирования (design pattern) — это проверенное и готовое к использованию решение. Это не класс и не библиотека, которую можно подключить к проекту, это нечто большее - он не зависит от языка программирования, не является законченным образцом, который может быть прямо преобразован в код и может быть реализован по-разному в разных языках программирования. ПОРОЖДАЮЩИЕ ПАТТЕРНЫ ✔️ Абстрактная фабрика (Abstract factory) - Класс, который представляет собой интерфейс для создания других классов. ✔️ Строитель (Builder) - Класс, который представляет собой интерфейс для создания сложного объекта. ✔️ Фабричный метод (Factory method) - Делегирует создание объектов наследникам родительского класса. Это позволяет использовать в коде программы не специфические классы, а манипулировать абстрактными объектами на более высоком уровне. ✔️ Прототип (Prototype) - Определяет интерфейс создания объекта через клонирование другого объекта вместо создания через конструктор. ✔️ Одиночка (Singleton) - Класс, который может иметь только один экземпляр. СТРУКТУРНЫЕ ПАТТЕРНЫ ✔️ Адаптер (Adapter) - Объект, обеспечивающий взаимодействие двух других объектов, один из которых использует, а другой предоставляет несовместимый с первым интерфейс. ✔️ Мост (Bridge) - Структура, позволяющая изменять интерфейс обращения и интерфейс реализации класса независимо. ✔️ Компоновщик (Composite) - Объект, который объединяет в себе объекты, подобные ему самому. ✔️ Декоратор (Decorator) - Класс, расширяющий функциональность другого класса без использования наследования. ✔️ Фасад (Facade) - Объект, который абстрагирует работу с несколькими классами, объединяя их в единое целое. ✔️ Приспособленец (Flyweight) - Это объект, представляющий себя как уникальный экземпляр в разных местах программы, но по факту не являющийся таковым. ✔️ Заместитель (Proxy) - Объект, который является посредником между двумя другими объектами, и который реализует/ограничивает доступ к объекту, к которому обращаются через него. ПОВЕДЕНЧЕСКИЕ ПАТТЕРНЫ ✔️ Цепочка обязанностей (Chain of responsibility) - Предназначен для организации в системе уровней ответственности. ✔️ Команда (Command) - Представляет действие. Объект команды заключает в себе само действие и его параметры. ✔️ Интерпретатор (Interpreter) - Решает часто встречающуюся, но подверженную изменениям, задачу. ✔️ Итератор (Iterator) - Представляет собой объект, позволяющий получить последовательный доступ к элементам объекта-агрегата без использования описаний каждого из объектов, входящих в состав агрегации. ✔️ Посредник (Mediator) - Обеспечивает взаимодействие множества объектов, формируя при этом слабую связанность и избавляя объекты от необходимости явно ссылаться друг на друга. ✔️ Хранитель (Memento) - Позволяет, не нарушая инкапсуляцию зафиксировать и сохранить внутренние состояния объекта так, чтобы позднее восстановить его в этих состояниях. ✔️ Наблюдатель (Observer) - Определяет зависимость типа «один ко многим» между объектами таким образом, что при изменении состояния одного объекта все зависящие от него оповещаются об этом событии. ✔️ Состояние (State) - Используется в тех случаях, когда во время выполнения программы объект должен менять своё поведение в зависимости от своего состояния. ✔️ Стратегия (Strategy) - Предназначен для определения семейства алгоритмов, инкапсуляции каждого из них и обеспечения их взаимозаменяемости. ✔️ Шаблонный метод (Template method) - Определяет основу алгоритма и позволяет наследникам переопределять некоторые шаги алгоритма, не изменяя его структуру в целом. ✔️ Посетитель (Visitor) - Описывает операцию, которая выполняется над объектами других классов. При изменении класса Visitor нет необходимости изменять обслуживаемые классы.

Использование ScheduledExecutorService ScheduledExecutorService - это интерфейс, который предоставляет удобный способ для выполнения задач с определенной задержкой или периодом. Он является частью пакета java.util.concurrent и предоставляет мощные возможности для управления планированием выполнения задач. Рассмотрим как его можно использовать: ▫️Создаем пул потоков и указываем его размер (сколько потоков будет в пуле):
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
▫️Запуск задачи с определенной задержкой:
executor.schedule(() -> {
    System.out.println("Task executed after 5 seconds");
}, 5, TimeUnit.SECONDS);
Этот код выведет в консоль текст через 5 секунд. ▫️Периодическое выполнение задачи с фиксированной задержкой:
executor.scheduleAtFixedRate(() -> {
    System.out.println("Task executed at fixed rate");
}, 3, 1, TimeUnit.SECONDS);
Этот код будет выводить в консоль текст, каждую 1 секунду, начиная через 3 секунды после запуска. Т.е. каждую секунду executor будет запускать выполнение задачи, а если выполнение задачи идет дольше, чем заданная задержка, следующее выполнение начнется сразу же после завершения предыдущего. ▫️Периодическое выполнение задачи с фиксированной задержкой между выполнениями:
executor.scheduleWithFixedDelay(() -> {
    System.out.println("Task executed with fixed delay");
}, 3, 1, TimeUnit.SECONDS);
Этот код будет выполнять то же самое, что и предыдущий пример, но с одним важным отличием - 1 секунда задержки будет применятся после завершения предыдущего выполнения, что означает, что интервал между окончанием одного выполнения и началом следующего всегда одинаковый. ScheduledExecutorService - полезный инструментом для создания планировщиков, таймеров и других приложений, требующих управления временем.

Switch Expressions В Java 12 появился новый синтаксис для switch, который позволяет возвращать значение используя стрелки ->, что делает код более компактным и удобным. Теперь switch может использоваться как выражение, а не только как оператор, что упрощает его применение в логике. Ранее switch использовался как оператор, и код мог выглядеть громоздко:

String day = "MONDAY";
int numLetters;
switch (day) {
    case "MONDAY":
    case "FRIDAY":
    case "SUNDAY":
        numLetters = 6;
        break;
    case "TUESDAY":
        numLetters = 7;
        break;
    default:
        numLetters = 8;
}
System.out.println(numLetters); // Вывод: 6
Со Switch Expressions можно переписать этот код более лаконично:

String day = "MONDAY";
int numLetters = switch (day) {
    case "MONDAY", "FRIDAY", "SUNDAY" -> 6;
    case "TUESDAY" -> 7;
    default -> 8;
};
System.out.println(numLetters); // Вывод: 6
Теперь switch может вернуть значение, и нет необходимости в break.

LocalDateTime: форматируем и парсим В Java 8 в JSR 310 пришел достаточно удобный класс для работы с датой и временем без учета часовых поясов LocalDateTime. Давайте рассмотрим как мы можем спарсить произвольную строку с датой и временем в объект LocalDateTime и как можем вывести LocalDateTime в нужном нам формате. Чтобы создать объект LocalDateTime из строки с временем и датой в помщь идет класс DateTimeFormatter в котором задается формат:
String dateStr = "31-01-2022 10:15:30";
DateTimeFormatter formatter = 
    DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss");
LocalDateTime dateTime = 
    LocalDateTime.parse(dateStr, formatter);
System.out.println(dateTime);
Вывод будет:
2022-01-31T10:15:30
А теперь посмотрим как превраить LocalDateTime в строку в нужном нам формате. Для этого воспользуемся методом format() класса LocalDateTime. Например:
LocalDateTime dateTime = LocalDateTime.now();
DateTimeFormatter formatter = 
    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedDateTime = dateTime.format(formatter);
System.out.println(formattedDateTime);
Вывод будет (смотря когда вызываете) что-то вроде:
2024-01-21 12:43:29
Конечно, вы можете комбинировать символы в паттерне как вам угодно, чтобы получить нужный формат. Полный список символов используемых в паттернах (а их большое количество) можно посмотреть в Java доках (ссылка). Какие-то наиболее часто используемые форматы предопределены в DateTimeFormatter. Например DateTimeFormatter.ISO_DATE_TIME выведет дату и время в виде:
2024-01-31T10:54:39.3918743

Что такое Optional и как его применять? Как часто вы ловили NullPointerException? Есть элегантное решение, как уменьшить количество таких ошибок. И это Optional. Optional - это обертка над другими объектами, которые могут иметь значение или быть null. При использовании такого подхода вы сделаете свой код более безопасным и чистым. Вот простой пример создания Optional:
var optionalStr = Optional.of("Hello, World!");
var emptyOptional = Optional.empty();
Optional содержит множество методов: isEmpty(), isPresent(), ifPresent(), map() и тд. Их нужно использовать с умом, чтобы код был лаконичным. Вместо не очень элегантных проверок на null:
if(optionalStr.isPresent()) {
  return someMethod(optionalStr.get());
} else {
  return "str";
}
или делать какие-либо действия над значением ЕСЛИ оно присутствует:
return optionalStr.map(this::someMethod)
     .orElse("str");
Зная нужные методы класса Optional, мы сделали код более коротким и красивым. В этом примере .map() принимает ссылку на метод и возвращает результат выполнения этого метода, по сути, преобразуя значение. В случае если optionalString будет пустым, то .orElse() вернет значение, указанное в круглых скобках. Используя Optional можно выстраивать цепочку операций в функциональном стиле:
optionalStr.map(String::toUpperCase)
              .filter(s -> s.startsWith("H"))
              .ifPresent(System.out::println);
Данный код преобразует строку в верхний регистр, проверит полученное значение на соответствие условию, и, если это условие будет истинно, то выведет строку в консоль. Не бойтесь использовать Optional в своем коде, делайте его более устойчивым и читабельным.

Как получить текущий stack trace? Stack trace представляет собой последовательность методов, от последнего к первому, которые были вызваны выполняемой программой (потоком). С помощью него можно отследить где было выброшено исключение или через какую цепочку методов программа дошла до какой-либо места в коде. Удобно для дебага. Чтобы получить текущий stack trace можно использовать вызов метода getStackTrace() у настоящего потока, например:
public class StackTraceExample {
    public static void main(String[] args) {
        method1();
    }

    public static void method1() {
        method2();
    }

    public static void method2() {
        StackTraceElement[] stackTrace = 
            Thread.currentThread().getStackTrace();
        
        for (StackTraceElement element : stackTrace) {
            System.out.println(element.getMethodName());
        }
    }
}
В данном примере метод method2 выводит все вызванные методы в стеке вызовов. Результат выполнения программы будет следующим:
getStackTrace
method2
method1
main
Объекты класса StackTraceElement представляют элементы стек стрейса:) Можно узнать различную информацию у данного элемента, такую как имя метода в данном месте стека (getMethodName()), имя класса (getClassName()) и другую. Для получения stack trace с использованием Throwable можно вызвать метод getStackTrace(), который также возвращает массив StackTraceElement. Пример использования Throwable для получения stack trace:
try {
    // some code that may throw an exception
    throw new Exception("Error occurred");
} catch (Exception e) {
    StackTraceElement[] stackTrace = e.getStackTrace();
    
    for (StackTraceElement element : stackTrace) {
        System.out.println(element.toString());
    }
}
Оба эти способа позволяют получить информацию о стеке вызовов для отслеживания ошибок и исключений в Java.

Варианты конфигурации контекста Spring приложения. Рассмотрим какие есть варианты на примере Spring Boot приложения. ▫️XML-конфигурация: Мы имеем файл конфигурации в classpath, допустим beans.xml:
<beans <!-- some schema data--->>
       
    <bean id="userService" class="com.example.UserService">
        <property name="userRepository" ref="userRepository"/>
    </bean>
    
    <bean id="userRepository" class="com.example.UserRepository"/>
</beans>
Для поднятия контекста на xml добавляем аннотацию @ImportResource("classpath:beans.xml") к нашему классу помеченному @SpringBootApplication (либо @Configuration). Но стоит отметить, что XML-конфигурация почти изжила себя и мало используется. ▫️Annotation-based конфигурация: При помощи аннотаций @Component, @Service, @Repository, @Controller мы можем указывать Spring'у какие бины нужно создавать в контексте, а связь между ними задать через @Autowired:
@Repository
public class UserRepository {
    //...
}

@Service
public class UserService {
    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    //...
}
▫️Java-based конфигурация: Позволяет определять бины и их зависимости непосредственно в Java-коде с использованием аннотаций @Configuration и @Bean:
@Configuration
public class AppConfig {

    @Bean
    public UserService userService(UserRepository userRepository) {
        return new UserService(userRepository);
    }

    @Bean
    public UserRepository userRepository() {
        return new UserRepository();
    }
}
В каждом примере мы определили два бина - UserService и UserRepository, а так же связь между ними (инжект userRepository в userService). Выбор способа конфигурации зависит от ваших предпочтений и требований вашего проекта. Справедливости ради - есть еще groovy конфигурация, но в своей практике я ее не встречал. Напишите в комментах, если работали с groovy и в каких кейсах это было полезно.

Нормализация SQL баз данных: Первая нормальная форма (1NF) Нормализация баз данных нужна для того, чтобы сделать данные в них более организованными, структурированными и легко управляемыми. Это помогает избежать избыточности информации и дублирования данных, что делает базу данных более эффективной, улучшает производительность и уменьшает вероятность возникновения ошибок. Нормализация также способствует обеспечению целостности данных, что означает, что данные в базе остаются точными, актуальными и надежными во всех условиях. Существует несколько нормальных форм и мы рассмотрим их все в серии постов. Первая нормальная форма базы данных требует, чтобы каждая ячейка таблицы содержала только одно значение одного типа данных и чтобы каждая запись была уникальной. Пример без нормализации: Предположим, у нас есть таблица students, содержащая информацию о студентах и курсах, на которых они учатся.
| name     | courses          |
|----------|------------------|
| Алиса    | Математика, Физика |
| Боб      | Физика, Химия    |
В этом примере информация о курсах хранится в одной колонке courses, что является нарушением нормализации. С нормализацией: Чтобы привести таблицу к 1NF, мы разделим колонку courses на отдельные строки, каждая из которых содержит только один предмет:

| name   | courses   |
|--------|-----------|
| Алиса  | Математика|
| Алиса  | Физика    |
| Боб    | Физика    |
| Боб    | Химия     |
Теперь каждая колонка содержит только одно значение, и таблица соответствует требованиям 1NF.

Spring Profiles Spring Profiles - это мощный механизм Spring Framework, который позволяет легко настраивать контекст и использовать разные конфигурации для разных сред. ▫️Как определить профили? Профили определяются с использованием аннотации @Profile. Каждый профиль может иметь свои собственные настройки и бины:
@Configuration
@Profile("dev")
public class DevConfig {
}
Так же можно повесить эту аннотацию на класс компонента:
@Component
@Profile("dev")
public class DevComponent {
}
или метод создания бина в классе конфигурации:
@Configuration
public class Config {

  @Bean
  @Profile("dev")
  public MyBean myBean() {
    return new MyBean();
}
Для xml конфигурации:
<beans profile="dev">
    <bean id="myBean" class="org.demo.MyBean" />
</beans>
▫️Как установить активный профиль? Чтобы установить активный профиль, так же есть несколько различных вариантов: - Через JVM параметры
-Dspring.profiles.active=dev
- Профили можно активировать через переменные окружения (для Unix/Windows соответственно):
export SPRING_PROFILES_ACTIVE=dev
set SPRING_PROFILES_ACTIVE=dev
- Через файл application.properties (yaml):
spring.profiles.active=dev
- Программно через ConfigurableEnvironment:
@Autowired
private ConfigurableEnvironment env;
...
env.setActiveProfiles("dev");
▫️Рассмотрим простой пример для понимания: Есть два класса конфигурации для дева и прода:
@Configuration
@Profile("dev")
public class DevConfig {
   // Конфигурация для дев
}

@Configuration
@Profile("prod")
public class ProdConfig {
   // Конфигурация для прода
}
В файле application.yml:
spring:
    profiles:
       active: dev
В этом примере Spring будет использовать конфигурацию из DevConfig , а ProdConfig - только при активации профиля prod. Профили могут использоваться для конфигурации различных аспектов приложения, таких как базы данных, настройки безопасности, внешние сервисы и многое другое. Например, можно настроить разные подключения к базам данных для разных сред выполнения (тестирование, разработка, продакшн).

TreeSet TreeSet - это множество уникальных и упорядоченных (в отличии от HashSet) элементов. TreeSet наследуется от AbstractSet и реализует интерфейс NavigableSet. Эта коллекция под капотом имеет TreeMap, которая использует для хранения бинарное красно-черное дерево, из-за чего элементы в этой структуре данных хранятся в отсортированном порядке. Так как при вставке и поиске значений TreeSet использует сравнение через метод compare/compateTo, то добавляемые элементы должны реализовывать интерфейс Comparable, либо при создании коллекции TreeSet через конструктор необходимо указать Comparator. Передавая через конструктор реализацию Comparator можно изменить способ сортировки элементов в коллекции.
Set<Integer> tree = new TreeSet<>(Comparator.reverseOrder());
При указании Comparator нужно быть аккуратным, так как TreeSet гарантирует уникальность элементов, сравнивая их через метод compare/compateTo !
Set<String> tree = new TreeSet<>(Comparator.comparing(String::length));
tree.add("Hello");
tree.add("World");
В данном примере TreeSet сравнивает строки по их длине. Так как "Hello" и "World" имеют одинаковую длину, то в коллекцию будет добавлена только первая строка. Данную коллекцию имеет смысл использовать если: ▫️нужна автоматическая сортировка уникальных элементов ▫️необходимо быстро находить элементы в определенном диапазоне