Тестовое собеседование №24 (Разбор)

Ссылка на видео:

Ответы на вопросы

Что мы знаем о методе main?

Метод main в Java является точкой входа в приложение и является обязательным для любой программы на языке Java. Этот метод должен быть объявлен как public static void main(String[] args) и находиться в классе, который содержит точку входа в приложение.

Аргументы, переданные методу main, представляют собой массив строк, и они могут быть использованы для передачи параметров приложению. Метод main должен содержать код, который будет выполняться при запуске программы.

Важно отметить, что метод main не может быть перегружен, т.е. в одном классе может быть только один метод main. Кроме того, метод main должен быть объявлен как public, чтобы был доступ к нему из других классов, как static, чтобы он мог быть вызван без создания экземпляра класса, и void, чтобы он не возвращал никакого значения.

  • public – модификатор доступа, который указывает, что метод является общедоступным и может быть вызван из любого места в программе.
  • static – модификатор, который указывает, что метод принадлежит классу, а не объекту, и может быть вызван без создания экземпляра класса.
  • void – ключевое слово, которое указывает, что метод не возвращает значения.
  • main – имя метода, которое является стандартным и указывает на то, что это точка входа в приложение.
  • String[] – тип и имя параметра метода, который представляет собой массив строк. Этот параметр содержит аргументы командной строки, переданные при запуске приложения.

Стоит отметить, что сигнатура метода main строго определена в языке Java, и должна соответствовать вышеприведенному формату, чтобы компилятор мог правильно определить точку входа в приложение.

Что такое массивы в Java?

Массивы в Java – это упорядоченные коллекции элементов одного типа данных, которые хранятся в памяти в виде последовательности ячеек памяти, доступных по индексу. Это означает, что каждый элемент массива имеет свой индекс, начиная с 0, и можно получить доступ к элементам массива, обращаясь к ним по индексу.

В Java массивы могут быть одномерными, многомерными и переменной длины. Для создания массива в Java необходимо указать тип элементов, количество элементов и имя массива. Например, для создания одномерного массива типа int с 5 элементами можно написать следующий код:

int[] myArray = new int[5];

Это создаст массив myArray, который содержит 5 элементов типа int, все инициализированные значением по умолчанию (0 для типа int).

Чтобы получить доступ к элементам массива, нужно указать индекс элемента в квадратных скобках. Например, чтобы получить доступ к первому элементу массива, нужно написать:

int firstElement = myArray[0];

Массивы в Java часто используются для хранения больших объемов данных, таких как результаты опросов, массивы пикселей в изображениях или матрицы в математических вычислениях.

Какой класс реализует динамический массив в Java, и что мы можем про него рассказать?

В Java динамический массив реализуется классом ArrayList. ArrayList является классом, реализующим интерфейс List, который представляет собой упорядоченный список объектов с возможностью динамического изменения размера.

ArrayList в Java может хранить объекты любого типа, включая объекты других классов. В отличие от обычных массивов, размер ArrayList может динамически изменяться во время выполнения программы. Это делает ArrayList очень гибким и удобным для работы с данными, которые могут изменяться во время выполнения программы.

Некоторые из основных методов ArrayList, которые можно использовать для добавления, удаления, доступа и изменения элементов в списке, включают:

  • add() – добавляет элемент в конец списка
  • remove() – удаляет элемент из списка
  • get() – получает элемент по указанному индексу
  • set() – заменяет элемент по указанному индексу новым элементом

ArrayList также предоставляет ряд других методов, таких как size() (возвращает текущее количество элементов в списке), isEmpty() (возвращает true, если список пуст), и contains() (возвращает true, если список содержит указанный элемент).

Хотя ArrayList является гибким и удобным классом для работы с динамическими массивами в Java, он не всегда является наилучшим выбором для работы с большими объемами данных или для производительных приложений, где требуется быстрый доступ к элементам. В таких случаях могут использоваться другие классы, такие как LinkedList или ArrayDeque, которые предоставляют другие методы доступа и более эффективные способы работы с данными.

Алгоритмическая сложность базовых операций в ArrayList в Java зависит от конкретной операции и может быть выражена в терминах нотации большого “O”. Обычно сложность операций ArrayList составляет:

  • Добавление элемента в конец списка (add()) – O(1) в лучшем случае, когда есть еще свободное место в списке, O(n) в худшем случае, когда нужно расширить массив, где n – количество элементов в списке.
  • Удаление элемента из списка (remove()) – O(n), где n – количество элементов в списке.
  • Доступ к элементу списка (get()) – O(1), т.к. элементы списка хранятся в массиве, и доступ к элементу осуществляется по индексу, который известен заранее.
  • Замена элемента списка (set()) – O(1), т.к. замена элемента происходит по индексу, который известен заранее.

Общая сложность операций ArrayList в Java зависит от конкретной ситуации и может варьироваться в зависимости от размера списка, расположения элементов в списке и т.д. Например, поиск элемента в ArrayList может иметь сложность O(n), если элемент находится в конце списка, но может иметь сложность O(1), если элемент находится в начале списка. Также, если требуется часто вставлять или удалять элементы в середине списка, может быть более эффективно использовать LinkedList, который имеет другие алгоритмические сложности для этих операций.

За счет чего NIO обеспечивает неблокируемый доступ к ресурсам?

NIO (New IO) в Java обеспечивает неблокируемый доступ к ресурсам за счет использования таких функций, как буферизация, каналы и селекторы.

Буферизация позволяет собирать данные в буфер и работать с ними в пакетном режиме, вместо обращения к ресурсу для каждого отдельного запроса. Это позволяет сократить количество операций ввода-вывода и улучшить производительность приложения.

Каналы в NIO представляют собой абстракцию для ввода-вывода данных, которая позволяет работать с несколькими соединениями одновременно и асинхронно, т.е. не ждать завершения одного запроса перед переходом к следующему. Каналы в NIO могут использоваться для чтения и записи данных, а также для обработки сетевых соединений.

Селекторы позволяют ожидать нескольких операций ввода-вывода на нескольких каналах одновременно, что позволяет использовать один поток для обработки нескольких соединений. Это сокращает количество потоков, необходимых для работы приложения, что может улучшить производительность и снизить нагрузку на систему.

Все эти функции в NIO позволяют использовать неблокирующий ввод-вывод и работать с ресурсами асинхронно, что может улучшить производительность и снизить нагрузку на систему в целом.

Как работает CopyOnWriteArrayList

CopyOnWriteArrayList в Java – это потокобезопасный класс списка, который обеспечивает свою потокобезопасность путем создания новой копии списка при каждой операции изменения списка (например, добавлении или удалении элемента). Это означает, что при изменении списка создается копия исходного списка, которая затем изменяется, в то время как оригинальный список остается неизменным.

Таким образом, при выполнении операций чтения списка (таких, как get()) могут использоваться существующие данные без необходимости блокировки списка. Однако при выполнении операций записи (таких, как add() или remove()) происходит блокировка, чтобы гарантировать, что только один поток может изменять список в данный момент времени. Когда операция записи завершена, новая копия списка становится основным списком.

Такой подход может быть полезным в ситуациях, когда частые операции чтения списка являются более критичными, чем операции записи, или когда требуется обеспечить безопасный доступ к списку в многопоточной среде. Однако он может привести к дополнительным затратам на память и производительность в случае большого числа операций записи или при работе с большими списками.

Важно отметить, что CopyOnWriteArrayList не гарантирует согласованность данных между потоками, т.е. при изменении списка одним потоком может возникнуть задержка в обновлении других потоков, работающих с тем же списком. Это может привести к неожиданным результатам и несогласованным данным, поэтому CopyOnWriteArrayList рекомендуется использовать только в тех случаях, когда частые операции чтения списка являются более важными, чем операции записи.

Что такое Stream в контексте Stream API?

Stream в Java Stream API – это последовательность элементов, которые могут быть обработаны в потоковой манере. Stream представляет собой абстракцию, которая может использоваться для обработки коллекций данных, таких как списки, множества, массивы и другие источники данных.

Stream API предоставляет различные методы для создания и обработки потоков. Например, можно создать поток из списка с помощью метода stream():

List myList = Arrays.asList("apple", "banana", "orange");
Stream myStream = myList.stream();

Методы Stream API могут быть использованы для обработки потоков и преобразования данных. Например, можно использовать метод map() для преобразования каждого элемента в потоке:

Stream<String> myStream = myList.stream().map(String::toUpperCase);

Метод map() создает новый поток, в котором каждый элемент исходного потока преобразуется в новый элемент, используя переданную функцию (в данном случае метод toUpperCase()).

Stream API также предоставляет ряд других методов, таких как filter() (для фильтрации элементов в потоке), reduce() (для агрегации элементов в потоке) и т.д.

Stream API может быть использован для обработки больших объемов данных в потоковой манере, что может улучшить производительность и упростить код. Кроме того, Stream API может быть использован для работы с параллельными потоками, что может еще более улучшить производительность приложения.

Какие отличия между методами map и flatMap?

Методы map() и flatMap() в Java Stream API позволяют преобразовывать элементы потока в другие элементы, но они работают по-разному.

Метод map() позволяет применить функцию к каждому элементу потока и создать новый поток, в котором каждый элемент преобразован согласно заданной функции. Этот метод позволяет выполнить преобразование каждого элемента потока независимо друг от друга.

List<String> words = Arrays.asList("Hello", "World");
Stream<String> wordStream = words.stream();
Stream<String> upperCaseStream = wordStream.map(word -> word.toUpperCase());

В примере выше метод map() применяет функцию toUpperCase() к каждому элементу потока wordStream и создает новый поток upperCaseStream, в котором каждый элемент преобразован в верхний регистр.

Метод flatMap(), с другой стороны, позволяет преобразовать каждый элемент потока в другой поток, а затем объединить все эти потоки в один поток. Этот метод используется, когда каждый элемент потока может преобразоваться в несколько элементов другого потока.

List<List<String>> listOfLists = Arrays.asList(Arrays.asList("Hello", "World"), Arrays.asList("Stream", "API"));
Stream<List<String>> listStream = listOfLists.stream();
Stream<String> flatMapStream = listStream.flatMap(list -> list.stream());

В примере выше метод flatMap() принимает каждый элемент потока listStream, который представляет список строк, и создает новый поток, который объединяет все списки строк в один поток строк flatMapStream.

Таким образом, основное отличие между методами map() и flatMap() заключается в том, что map() преобразует каждый элемент потока в другой элемент, а flatMap() преобразует каждый элемент потока в другой поток и затем объединяет все потоки в один поток.

Что такое функциональный интерфейс?

Функциональный интерфейс в Java – это интерфейс, который содержит только один абстрактный метод. Они используются в функциональном программировании для передачи функций как параметров методов и для создания лямбда-выражений.

Функциональные интерфейсы в Java имеют аннотацию @FunctionalInterface, которая используется для обозначения того, что интерфейс является функциональным, и компилятор должен проверить, что в интерфейсе есть только один абстрактный метод.

Например, следующий интерфейс Calculator является функциональным интерфейсом, потому что он содержит только один абстрактный метод calculate():

@FunctionalInterface
public interface Calculator {
    double calculate(double x, double y);
}

Функциональные интерфейсы могут использоваться для создания лямбда-выражений, которые являются анонимными функциями. Их можно передавать как параметры в методы. Например, следующее лямбда-выражение реализует метод calculate() интерфейса Calculator:

Calculator add = (x, y) -> x + y;

Лямбда-выражения могут быть переданы в методы, которые принимают функциональные интерфейсы в качестве параметров, например:

public double process(Calculator calculator, double x, double y) {
    return calculator.calculate(x, y);
}

double result = process(add, 10.0, 20.0); // result = 30.0

Использование функциональных интерфейсов и лямбда-выражений может упростить код и улучшить его читаемость и гибкость.

Что такое лямбда?

Лямбда-выражение в Java – это анонимная функция, которая может быть использована в качестве параметра метода или возвращаемого значения. Она позволяет передавать функцию как параметр и использовать ее как функциональный интерфейс.

Лямбда-выражение начинается с символа -> и состоит из списка аргументов (в круглых скобках), разделенных запятой, и тела функции (в фигурных скобках). В зависимости от контекста, лямбда-выражения могут быть преобразованы в функциональные интерфейсы, что позволяет использовать их для передачи функций как параметров.

Например, следующее лямбда-выражение реализует функциональный интерфейс Runnable, который используется для создания потоков в Java:

Runnable runnable = () -> {
    System.out.println("This is a lambda expression");
};

В этом примере лямбда-выражение не имеет аргументов и содержит только одну инструкцию для вывода сообщения на консоль.

Лямбда-выражения могут использоваться в качестве параметров методов, которые принимают функциональные интерфейсы. Например, следующий метод принимает функциональный интерфейс Predicate, который используется для проверки условия:

public static void process(Predicate<Integer> predicate, int number) {
    if (predicate.test(number)) {
        System.out.println("The number satisfies the condition");
    } else {
        System.out.println("The number does not satisfy the condition");
    }
}

Этот метод может быть вызван с использованием лямбда-выражения для реализации функции Predicate:

process(n -> n % 2 == 0, 10); // выводит "The number satisfies the condition"

В этом примере лямбда-выражение используется для проверки, является ли число четным (n % 2 == 0). Лямбда-выражение передается в метод process() в качестве параметра Predicate<Integer> predicate.

Использование лямбда-выражений может упростить код и улучшить его читаемость и гибкость, особенно при работе с функциональными интерфейсами.

Что такое ExecutorService, для чего он нужен и какие реализации у нас есть?

ExecutorService в Java – это интерфейс, который предоставляет управление потоками и позволяет выполнить задачи в асинхронном режиме. Он предоставляет методы для запуска, остановки и управления потоками, а также управления пулом потоков.

ExecutorService позволяет создавать пул потоков и планировать выполнение задач в этом пуле потоков. Он упрощает работу с потоками, скрывая детали управления потоками и обеспечивая удобный интерфейс для выполнения асинхронных задач.

Некоторые реализации ExecutorService включают:

  1. ThreadPoolExecutor: это реализация ExecutorService, которая предоставляет пул потоков для выполнения задач. Этот пул потоков может быть настроен для изменения его размера и других параметров.
  2. ScheduledThreadPoolExecutor: это реализация ExecutorService, которая предоставляет планировщик задач для запуска задач в будущем. Он также предоставляет пул потоков для выполнения задач.
  3. ForkJoinPool: это реализация ExecutorService, которая используется для параллельного выполнения задач. Они могут быть разбиты на более мелкие задачи. Он оптимизирован для выполнения рекурсивных задач.

Для использования ExecutorService необходимо создать экземпляр реализации интерфейса и использовать его методы для управления потоками и выполнения задач. Например, для запуска задач в пуле потоков можно использовать метод execute():

ExecutorService executorService = Executors.newFixedThreadPool(10);

executorService.execute(() -> {
    // выполнение задачи в потоке
});

executorService.shutdown();

В этом примере мы создаем пул потоков с 10 потоками, запускаем задачу в пуле потоков и затем закрываем пул потоков, после завершения задач. Метод execute() принимает объект типа Runnable, содержащий код задачи, которую нужно выполнить.

Что “скрывается под” буквой O в SOLID?

Буква O в принципах SOLID (SOLID principles) обозначает принцип Open-Closed (открытость-закрытость). Этот принцип заключается в том, что программные сущности, такие как классы, модули и функции, должны быть открыты для расширения, но закрыты для модификации.

По сути, это означает, что код должен быть написан таким образом, чтобы его можно было легко расширять новыми функциональностями, не затрагивая уже написанный код. Это достигается путем использования абстракций и интерфейсов для определения общих концепций, которые могут быть реализованы в различных контекстах.

Классы и методы могут быть закрыты для модификации путем использования инкапсуляции и других механизмов, которые позволяют скрыть детали реализации и предоставить только интерфейс для использования.

Принцип Open-Closed имеет важное значение для разработки гибких и расширяемых приложений, поскольку он позволяет создавать модули, которые могут быть легко изменены или заменены, не затрагивая другие части приложения.

Какие отличия между шаблонами ООП Стратегия и Состояние?

Шаблоны проектирования Стратегия (Strategy) и Состояние (State) в объектно-ориентированном программировании имеют сходства, но также имеют и некоторые отличия.

Шаблон Стратегия используется для определения семейства алгоритмов, которые могут взаимозаменять друг друга, и использования интерфейса для их инкапсуляции. Этот шаблон позволяет изменять алгоритмы независимо от клиентского кода, что упрощает расширение функциональности программы.

Шаблон Состояние также используется для определения семейства алгоритмов, но он сосредоточен на изменении поведения объекта в зависимости от его внутреннего состояния. Этот шаблон использует различные классы, каждый из которых представляет определенное состояние объекта, и переходы между состояниями происходят путем изменения текущего состояния объекта.

Таким образом, ключевое отличие между шаблонами Стратегия и Состояние заключается в том, что Стратегия фокусируется на изменении алгоритма, в то время как Состояние фокусируется на изменении поведения объекта в зависимости от его внутреннего состояния.

Например, пусть у нас есть класс TrafficLight, который имеет три состояния: красный, желтый и зеленый. Мы можем использовать шаблон Состояние, чтобы изменить поведение светофора в зависимости от его текущего состояния. Каждое состояние будет иметь свою реализацию метода changeLight(), который определяет, какие лампочки будут включены на светофоре. Мы можем также использовать шаблон Стратегия для определения алгоритмов для определения времени, в течение которого каждый цвет светофора будет гореть. Каждый алгоритм будет иметь свою реализацию метода getDuration(), который определяет, как долго каждый цвет будет гореть на светофоре.

Решение алгоритмической задачи

Условия задачи:

A phrase is a palindrome if, after converting all uppercase letters into lowercase letters and removing all non-alphanumeric characters, it reads the same forward and backward. Alphanumeric characters include letters and numbers.

Given a string s, return true if it is a palindrome, or false otherwise.

 

Example 1:

Input: s = "A man, a plan, a canal: Panama"
Output: true
Explanation: "amanaplanacanalpanama" is a palindrome.
Example 2:

Input: s = "race a car"
Output: false
Explanation: "raceacar" is not a palindrome.
Example 3:

Input: s = " "
Output: true
Explanation: s is an empty string "" after removing non-alphanumeric characters.
Since an empty string reads the same forward and backward, it is a palindrome.
 

Constraints:

1 <= s.length <= 2 * 10^5
s consists only of printable ASCII characters.

Решение задачи:

Для решения этой задачи за O(1) по памяти, мы можем использовать два указателя, один начинается с начала строки, а другой с конца строки. Затем мы будем сравнивать символы, находящиеся на позициях, на которые указывают эти указатели, и смещать указатели к центру строки до тех пор, пока они не встретятся в середине строки или не станут на позиции, на которых символы не равны. Если все символы были сравнены и они все равны, то строка является палиндромом.

Этот подход не требует использования дополнительной памяти, поэтому он выполняется за O(1) по памяти.

public boolean isPalindrome(String s) {
    int left = 0, right = s.length() - 1;
    while (left < right) {
        while (left < right && !Character.isLetterOrDigit(s.charAt(left))) {
            left++;
        }
        while (left < right && !Character.isLetterOrDigit(s.charAt(right))) {
            right--;
        }
        if (Character.toLowerCase(s.charAt(left)) != Character.toLowerCase(s.charAt(right))) {
            return false;
        }
        left++;
        right--;
    }
    return true;
}

Что такое группировка в БД? Примеры.

Группировка (grouping) в базах данных – это процесс объединения строк в таблице на основе значения определенной колонки или колонок и получения сводной информации о группах строк. Результатом группировки является набор строк, в котором каждая строка представляет собой уникальное значение из указанных колонок и сводную информацию (например, среднее, минимальное или максимальное значение) для каждой группы.

Группировка часто используется в SQL-запросах, особенно при агрегировании данных, таких как вычисление среднего, максимального или минимального значения столбца, подсчет количества строк или выполнение других агрегатных функций.

Пример SQL-запроса с использованием группировки:

SELECT department, AVG(salary) as avg_salary, COUNT(*) as num_employees
FROM employees
GROUP BY department;

Этот запрос группирует данные таблицы employees по столбцу department, вычисляет среднее значение зарплаты (AVG(salary)) и количество сотрудников (COUNT(*)) для каждого отдела, и возвращает результаты в виде таблицы, в которой каждая строка соответствует отделу и содержит среднюю зарплату и количество сотрудников в этом отделе.

Группировка может быть полезна для анализа больших наборов данных и получения сводной информации о данных, что помогает принимать бизнес-решения и выявлять тенденции в данных.

Поиск групп, в которых более 8 студентов

SELECT SUM(score) as total_score
FROM students
GROUP BY group_id
HAVING group_id = 8;

Пример групп, где более 20 студентов:

SELECT group_id, COUNT(*) as num_students
FROM students
GROUP BY group_id
HAVING num_students > 20;

Что такое ORM и какие есть реализации?

ORM (Object-Relational Mapping) – это технология, которая позволяет связать объектно-ориентированный код с реляционной базой данных. Она предоставляет инструменты для преобразования данных из реляционной модели в объектно-ориентированную и обратно, что позволяет работать с базой данных, используя объекты и методы, как если бы она была объектной базой данных.

ORM упрощает работу с базой данных, уменьшает количество кода, необходимого для выполнения запросов к базе данных, и улучшает портативность приложений, позволяя использовать различные базы данных без необходимости изменения кода приложения.

Существует множество реализаций ORM-технологии на различных языках программирования. Некоторые из них наиболее популярные:

  1. Hibernate – это ORM-фреймворк для Java, который предоставляет широкий спектр функций, включая автоматическое создание таблиц, генерацию запросов, управление транзакциями и многие другие.
  2. Entity Framework – это ORM-фреймворк для .NET, который позволяет работать с базой данных, используя объекты .NET, и предоставляет широкий спектр функций для работы с базой данных.
  3. Django ORM – это ORM-фреймворк для Python, который предоставляет удобный и выразительный API для работы с базами данных, используя объекты Python.
  4. SQLAlchemy – это ORM-фреймворк для Python, который предоставляет мощный и гибкий API для работы с базами данных, включая поддержку различных СУБД и управление транзакциями.
  5. Ruby on Rails ActiveRecord – это ORM-фреймворк для Ruby on Rails, который предоставляет простой и интуитивно понятный API для работы с базами данных.
  6. Sequelize – это ORM-фреймворк для Node.js, который позволяет работать с различными СУБД и предоставляет множество функций для работы с базой данных.

Это только некоторые из многих ORM-реализаций, которые доступны на рынке. Каждый фреймворк имеет свои особенности и преимущества, поэтому выбор ORM-реализации зависит от конкретных потребностей проекта и языка программирования, на котором он написан.

Какие уровни кэширования есть в Hibernate?

Hibernate – это ORM-фреймворк для Java, который позволяет работать с базой данных через объекты Java. Hibernate предоставляет несколько уровней кэширования для улучшения производительности и уменьшения нагрузки на базу данных.

  1. Уровень первого уровня кэша (First-level cache) – это кэш, который хранит объекты сеанса Hibernate. Он предназначен для хранения объектов, которые были загружены из базы данных в рамках текущей транзакции. Кэш первого уровня ускоряет доступ к данным и уменьшает нагрузку на базу данных, так как объекты можно получить из кэша, а не выполнять запросы к базе данных.
  2. Уровень второго уровня кэша (Second-level cache) – это кэш, который хранит объекты, загруженные из базы данных. Он предназначен для хранения объектов, которые могут быть использованы в нескольких сессиях Hibernate. Кэш второго уровня позволяет избежать повторных запросов к базе данных, так как объекты могут быть получены из кэша.
  3. Уровень кэша запросов (Query cache) – это кэш, который хранит результаты выполнения запросов к базе данных. Он предназначен для хранения результатов запросов, которые могут быть использованы в нескольких сессиях Hibernate. Кэш запросов позволяет избежать повторного выполнения запросов к базе данных, так как результаты могут быть получены из кэша.

Кроме того, Hibernate предоставляет возможность настройки кэширования, так что каждый уровень кэша может быть настроен индивидуально, в зависимости от требований приложения и конкретных запросов к базе данных.

Как происходит запуск Spring Boot приложение?

Spring Boot – это фреймворк для разработки приложений на языке Java, который предоставляет множество инструментов и функций для упрощения создания, конфигурирования и запуска приложений.

При запуске Spring Boot приложения происходит следующее:

  1. Загружается конфигурация приложения. Spring Boot использует конфигурационные файлы, такие как application.properties или application.yml, для задания настроек приложения, таких как порт, на котором будет работать приложение, и другие параметры.
  2. Создается экземпляр ApplicationContext, который является центральной частью Spring Framework и управляет всеми компонентами приложения.
  3. Автоматическое конфигурирование. Spring Boot использует механизм автоматической конфигурации для обнаружения и настройки библиотек и инструментов, используемых в приложении. Например, если приложение использует базу данных, Spring Boot автоматически настроит соединение с базой данных на основе настроек в конфигурационных файлах.
  4. Запуск приложения. После того как приложение было настроено, Spring Boot запускает его, создавая и запуская контроллеры, сервисы, репозитории и другие компоненты.
  5. Взаимодействие с сервером приложений. Spring Boot может работать со многими серверами приложений, такими как Tomcat, Jetty и Undertow. При запуске приложения Spring Boot связывается с сервером приложений и управляет процессом его работы.

В результате всех этих шагов, Spring Boot приложение становится доступным для использования. Пользователи могут обращаться к нему через браузер, вызывать его API или использовать его другими способами в зависимости от его функциональности.

В чем разница между юнит и интеграционными тестами?

Юнит-тесты и интеграционные тесты – это два вида тестирования в разработке программного обеспечения, которые имеют разные цели и характеристики.

Юнит-тесты – это тесты, которые проверяют отдельные части кода, называемые юнитами, на корректность работы. Юнит-тесты обычно пишутся программистами и выполняются на каждом этапе разработки, чтобы убедиться в том, что отдельные компоненты программы работают правильно и соответствуют заданным требованиям.

Интеграционные тесты – это тесты, которые проверяют взаимодействие между различными частями системы в целом, а не отдельными ее частями. Интеграционные тесты могут проверять, как различные компоненты программы взаимодействуют друг с другом, как программа обрабатывает и сохраняет данные в базу данных, как программа работает в различных средах и т.д.

Основные различия между юнит-тестами и интеграционными тестами:

  1. Объекты тестирования: в юнит-тестах тестируется отдельный модуль или компонент, в интеграционных тестах тестируется взаимодействие между модулями и компонентами.
  2. Зависимости: юнит-тесты обычно выполняются в изолированном окружении, где зависимости заменяются заглушками (mocks) или поддельными объектами (stubs), в то время как интеграционные тесты требуют реальных зависимостей и среды выполнения.
  3. Скорость и стоимость: юнит-тесты быстрее выполняются и менее затратны в плане времени и ресурсов, чем интеграционные тесты.
  4. Цели: юнит-тесты ориентированы на тестирование корректности работы отдельных компонентов, в то время как интеграционные тесты ориентированы на тестирование корректности взаимодействия между компонентами и системой в целом.
  5. Количественные оценки: юнит-тесты измеряются в терминах покрытия кода, в то время как интеграционные тесты измеряются в терминах покрытия функциональности.

Юнит-тесты и интеграционные тесты – это два разных подхода к тестированию программного обеспечения, которые предназначены для разных целей и имеют свои преимущества и недостатки. Юнит-тесты используются для тестирования отдельных компонентов программы на корректность работы в изолированном окружении, в то время как интеграционные тесты используются для тестирования взаимодействия между различными компонентами программы. Оба подхода имеют свои преимущества и недостатки, и выбор подхода зависит от целей тестирования и требований к программе.

Что такое Docker?

Docker – это платформа для создания, развертывания и управления контейнерами, которые являются легковесными, переносимыми и автономными средами для запуска приложений.

Контейнер – это единица программного обеспечения, которая содержит все необходимое для запуска приложения, включая код, библиотеки, зависимости и настройки. Контейнеры позволяют разработчикам упаковывать приложения со всеми зависимостями и запускать их в любой среде, где установлен Docker.

Docker позволяет создавать образы контейнеров, которые могут быть использованы для развертывания приложений на любой платформе, где установлен Docker. Образы контейнеров создаются из Dockerfile – файла, в котором описывается, как должен быть создан контейнер.

Docker также предоставляет механизмы для управления контейнерами, включая возможность запуска, остановки и масштабирования контейнеров, а также мониторинга и управления ресурсами.

Одно из главных преимуществ использования Docker – это возможность создания изолированных сред для разработки и развертывания приложений, которые полностью автономны и могут быть легко перемещены между средами разработки и продакшена. Это позволяет ускорить процесс разработки и уменьшить нагрузку на администраторов систем.

В чем отличия между Docker и виртуальной машиной?

Docker и виртуальная машина – это две разные технологии, которые используются для развертывания и управления приложениями, но имеют ряд отличий.

  1. Архитектура: Виртуальные машины используют гипервизоры для виртуализации аппаратного обеспечения, в то время как Docker использует контейнеры для виртуализации операционной системы. Контейнеры Docker используют общие ядра операционных систем, что позволяет им быть легковесными и быстрее запускаться, чем виртуальные машины.
  2. Ресурсы: Виртуальные машины требуют значительных ресурсов, включая оперативную память, процессорное время и дисковое пространство, для создания виртуальной среды, в то время как контейнеры Docker используют общие ресурсы операционной системы и потребляют значительно меньше ресурсов.
  3. Изолированность: Виртуальные машины изолируются от других приложений и системы, в то время как контейнеры Docker используют общую операционную систему, но изолированы друг от друга при помощи технологий контейнеризации. Это означает, что контейнеры могут работать на одном сервере с другими контейнерами, приложениями и сервисами, не затрагивая их работу.
  4. Развертывание: Виртуальные машины обычно требуют настройки гостевой операционной системы и установки необходимого программного обеспечения, в то время как контейнеры Docker могут быть созданы из образов контейнеров и запущены в несколько секунд.
  5. Портабельность: Контейнеры Docker являются переносимыми и могут быть запущены на любой платформе, где установлен Docker, в то время как виртуальные машины требуют средств виртуализации и установленной операционной системы для каждой платформы.

В целом, контейнеры Docker являются более легковесными и быстрее запускаются, чем виртуальные машины, но могут быть менее изолированными. Выбор между Docker и виртуальными машинами зависит от требований конкретной задачи и среды, в которой будет запускаться приложение.

Рекомендованная литература и статьи: