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

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

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

Ключевое слово final, назначение и варианты использования?

Ключевое слово “final” в Java используется для указания, что переменная, метод или класс не может быть изменен после их инициализации.

Варианты использования ключевого слова “final” в Java:

  • Переменные: Когда переменная объявлена как final, ее значение не может быть изменено после первоначального присвоения.

Например:

final int x = 5; // x = 10; // Ошибка компиляции, поскольку переменная x уже была инициализирована как final
  • Методы: Когда метод объявлен как final, его реализация не может быть переопределена в подклассах.

Например:

public class Parent {
    public final void method1() {
        System.out.println("Это метод, который нельзя переопределить");
    }
}

public class Child extends Parent {
    // Ошибка компиляции, так как метод1() объявлен как final и не может быть переопределен
    public void method1() { 
        System.out.println("Этот метод не будет вызван, так как он не может быть переопределен");
    }
}
  • Классы: Когда класс объявлен как final, он не может быть расширен (т.е. не может иметь подклассы).
public final class MyClass {
    // Тело класса
}
// Ошибка компиляции, так как MyClass объявлен как final и не может иметь подклассы
public class MySubclass extends MyClass { 
    // Тело подкласса
}

Использование ключевого слова “final” может повысить безопасность и производительность вашего кода, поскольку оно защищает значения, которые не должны быть изменены, и может помочь компилятору оптимизировать код.

Значения переменных по умолчанию – что это и как работает? 

Когда переменная объявляется в Java, она может быть проинициализирована начальным значением или не проинициализирована вообще. Если переменная не проинициализирована, то ей будет автоматически присвоено значение по умолчанию в зависимости от ее типа данных. Значения переменных по умолчанию в Java имеют следующие значения:
Для числовых типов данных (byte, short, int, long, float, double): 0

int num; // Переменная num проинициализирована значением 0

Для логического типа данных (boolean): false.

boolean flag; // Переменная flag проинициализирована значением false

Для символьного типа данных (char): null.

char ch; // Переменная ch проинициализирована значением '\u0000'

Для ссылочных типов данных (любой класс): null.

String str; // Переменная str проинициализирована значением null

Значение переменной по умолчанию может быть переопределено, если переменная объявляется с начальным значением.

Например:

int num = 5; // Переменная num проинициализирована значением 5
boolean flag = true; // Переменная flag проинициализирована значением true
char ch = 'a'; // Переменная ch проинициализирована значением 'a'
String str = "Hello"; // Переменная str проинициализирована значением "Hello"

Иерархия Collections API

Java Collections API представляет собой иерархию интерфейсов и классов, которые используются для хранения и обработки коллекций объектов в Java.

Ниже приведена иерархия интерфейсов и классов в Java Collections API:

  1. Collection – корневой интерфейс в иерархии, который определяет основные методы для работы с коллекциями.
  2. List – интерфейс, который представляет собой упорядоченную коллекцию неуникальных элементов.
  3. Set – интерфейс, который представляет собой неупорядоченную коллекцию уникальных элементов.
  4. SortedSet – интерфейс, который представляет собой неупорядоченную коллекцию уникальных элементов, отсортированных в порядке возрастания.
  5. NavigableSet – интерфейс, который расширяет SortedSet и добавляет методы для навигации по элементам в коллекции.
  6. Queue – интерфейс, который представляет собой коллекцию элементов, где элементы добавляются в конец и извлекаются из начала.
  7. Deque – интерфейс, который представляет собой двустороннюю очередь, где элементы могут быть добавлены и извлечены как из начала, так и из конца.
  8. Map – интерфейс, который представляет собой коллекцию пар ключ-значение, где ключи являются уникальными.
  9. SortedMap – интерфейс, который расширяет Map и представляет собой коллекцию пар ключ-значение, отсортированных в порядке возрастания ключей.
  10. NavigableMap – интерфейс, который расширяет SortedMap и добавляет методы для навигации по элементам в коллекции.
  11. AbstractCollection, AbstractList, AbstractSet, AbstractQueue и AbstractMap – абстрактные классы, которые предоставляют базовую реализацию для соответствующих интерфейсов.
  12. ArrayList, LinkedList, HashSet, TreeSet, PriorityQueue, ArrayDeque, HashMap, TreeMap и LinkedHashMap – конкретные классы, которые реализуют соответствующие интерфейсы и предоставляют конкретную реализацию методов.
Collection
??? List
?   ??? ArrayList
?   ??? LinkedList
?   ??? Vector
??? Queue
?   ??? Deque
?   ?   ??? ArrayDeque
?   ?   ??? LinkedList
?   ?   ??? PriorityQueue
?   ??? PriorityQueue
??? Set
?   ??? HashSet
?   ??? LinkedHashSet
?   ??? TreeSet
??? Map
    ??? HashMap
    ??? LinkedHashMap
    ??? TreeMap
    ??? Hashtable


Иерархия исключения в Java, их типы и способы их обработки. 

Иерархия исключений в Java представлена классом Throwable, который имеет два основных подкласса: Exception и Error.

Exception подразделяется на два подкласса: RuntimeException и Checked Exception.

  1. RuntimeException: Этот тип исключений возникает во время выполнения программы и может быть обработан в коде программы. Он включает исключения, такие как ArithmeticException, NullPointerException, ArrayIndexOutOfBoundsException и другие.
  2. Checked Exception: Этот тип исключений обычно возникает из-за ошибок программирования или ошибок “ввода-вывода” и должен быть обработан при помощи блока try-catch. Он включает исключения, такие как FileNotFoundException, IOException, ClassNotFoundException и другие.
  3. Error: Этот тип исключений возникает в основном при серьезных проблемах, которые невозможно обработать в рамках программы. Он включает исключения, такие как OutOfMemoryError, StackOverflowError и другие.

Способы обработки исключений:

  1. Блок try-catch-finally: Этот блок позволяет обработать исключение внутри блока try и выполнить определенные действия в блоке catch. Блок finally выполняется в любом случае, независимо от того, возникло исключение или нет.
try {
    // Код, который может вызвать исключение
} catch (Exception e) {
    // Обработка исключения
} finally {
    // Код, который будет выполнен независимо от того, было исключение или нет
}

Блок try-with-resources: Этот блок позволяет автоматически закрывать ресурсы после их использования. Он доступен только в Java 7 и выше.

try (ресурс) {
    // Код, который использует ресурс
} catch (Exception e) {
    // Обработка исключения
}

“Пробрасывание” исключения: Это позволяет программисту явно бросить исключение, когда возникает какое-либо условие. Исключение должно быть обработано в блоке try-catch.

if (условие) {
    throw new Exception("Сообщение об ошибке");
}

Класс TreeMap – какая структура данных и алгоритмические сложности базовых операций

Класс TreeMap – это реализация интерфейса SortedMap в Java, которая использует структуру данных красно-черного дерева для хранения пар “ключ-значение”.

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

  1. Каждый узел содержит ключ и значение.
  2. Каждый лист дерева является NIL и имеет цвет черный.
  3. Каждый красный узел имеет черного родителя.
  4. Для каждого узла все пути от узла до NIL-узла содержат одинаковое количество черных узлов.

Эти свойства гарантируют, что дерево всегда сбалансировано, и что операции поиска, вставки и удаления выполняются за O(log n) времени в среднем и худшем случаях.

Операции, которые поддерживаются TreeMap, включают:

  1. put(K key, V value) – вставка пары “ключ-значение”.
  2. get(Object key) – получение значения по ключу.
  3. remove(Object key) – удаление элемента по ключу.
  4. firstKey() – получение первого ключа в дереве.
  5. lastKey() – получение последнего ключа в дереве.
  6. headMap(K toKey) – получение поддерева всех элементов, ключ которых меньше заданного ключа toKey.
  7. tailMap(K fromKey) – получение поддерева всех элементов, ключ которых больше или равен заданному ключу fromKey.
  8. subMap(K fromKey, K toKey) – получение поддерева всех элементов, ключ которых больше или равен fromKey, и меньше toKey.

Сложности базовых операций в TreeMap:

  1. put(K key, V value) – O(log n) в среднем и худшем случаях.
  2. get(Object key) – O(log n) в среднем и худшем случаях.
  3. remove(Object key) – O(log n) в среднем и худшем случаях.
  4. firstKey() и lastKey() – O(log n) в худшем случае.
  5. headMap(K toKey), tailMap(K fromKey) и subMap(K fromKey, K toKey) – O(log n) в худшем случае, где n – это количество элементов в дереве.

Что делает ключевое слово volatile?

Ключевое слово “volatile” обеспечивает видимость изменений между потоками, а не синхронизацию выполнения потоков. Изменения, сделанные в переменной, объявленной как volatile, будут видны всем потокам, использующим эту переменную. Это гарантирует, что каждый поток, использующий переменную, будет работать с ее последним значением.

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

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

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

Что такое Future? Что такое CompletableFuture? Какие задачи они решают? 

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

Future предоставляет следующие основные методы:

  • get() – ожидает завершения выполнения задачи и возвращает результат.
  • isDone() – проверяет, завершилась ли задача.
  • cancel() – отменяет выполнение задачи.

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

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

Некоторые из основных методов класса CompletableFuture:

  • thenApply() – преобразует результат выполнения задачи в новый результат.
  • thenCompose() – комбинирует результат выполнения нескольких задач и возвращает результат в виде объекта CompletableFuture.
  • thenCombine() – комбинирует результат выполнения двух задач и возвращает результат в виде объекта CompletableFuture.
  • exceptionally() – обрабатывает ошибку, которая может возникнуть при выполнении задачи.
  • complete() – устанавливает результат выполнения задачи.

CompletableFuture также поддерживает методы, которые позволяют выполнить задачу асинхронно, с задержкой или в фоновом режиме, а также методы, которые позволяют установить экземпляр Executor для выполнения задачи в отдельном потоке.

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

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

class Solution {
    public List<String> commonChars(String[] words) {
        List<String> result = new ArrayList<>();
        int[] charCount = new int[26];
        Arrays.fill(charCount, Integer.MAX_VALUE);

        for (String word : words) {
            int[] currCount = new int[26];
            for (char ch : word.toCharArray()) {
                currCount[ch - 'a']++;
            }

            for (int i = 0; i < 26; i++) {
                charCount[i] = Math.min(charCount[i], currCount[i]);
            }
        }

        for (int i = 0; i < 26; i++) {
            for (int j = 0; j < charCount[i]; j++) {
                result.add(Character.toString((char)('a' + i)));
            }
        }

        return result;
    }
}

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

Что такое нормальная форма БД? Виды и мотивировки приведения БД к нормальной форме?

Нормальная форма базы данных (НФБД) – это стандартный набор правил, которые помогают разработчикам баз данных создавать более эффективные и удобные базы данных.

Существует несколько уровней НФБД, каждый из которых является более строгим и более эффективным по сравнению с предыдущим.

  1. Первая нормальная форма (1НФ) – это базовый уровень, который требует, чтобы все значения в таблице были атомарными (неделимыми).
  2. Вторая нормальная форма (2НФ) – требует, чтобы каждый неключевой атрибут в таблице был полностью функционально зависим от первичного ключа.
  3. Третья нормальная форма (3НФ) – требует, чтобы каждый неключевой атрибут в таблице был функционально зависим только от первичного ключа, а не от других неключевых атрибутов.
  4. Нормальная форма Бойса-Кодда (НФБК) – это более строгий уровень, который требует, чтобы каждый неключевой атрибут в таблице был функционально зависим только от первичного ключа.

Мотивация для приведения БД к НФБД включает:

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

Приведение БД к НФБД – это итеративный процесс, который может потребовать многократной декомпозиции таблиц, чтобы достичь оптимальной структуры данных.

Что такое JDBC?

JDBC (Java Database Connectivity) – это API (Application Programming Interface) для языка Java, которое обеспечивает доступ к различным реляционным базам данных. JDBC предоставляет набор классов и интерфейсов для управления соединениями с базами данных, выполнения SQL-запросов и обработки результатов.

JDBC был разработан как стандартный интерфейс для взаимодействия между Java-приложениями и базами данных. С помощью JDBC можно установить соединение с базой данных, выполнить запросы и получить результаты в виде объектов Java.

Некоторые основные компоненты JDBC:

  • Драйвер JDBC: драйвер JDBC – это программное обеспечение, которое позволяет Java-приложению подключаться к базе данных. Существует несколько типов драйверов JDBC, включая тип 1, тип 2, тип 3 и тип 4.
  • Интерфейсы и классы JDBC: JDBC предоставляет набор интерфейсов и классов, которые позволяют Java-приложениям подключаться к базам данных, создавать и выполнять запросы, а также получать результаты.
  • Объект Connection: объект Connection представляет собой соединение между Java-приложением и базой данных. Он обеспечивает методы для выполнения запросов, управления транзакциями и т.д.
  • Объект Statement: объект Statement представляет собой SQL-запрос, который может быть выполнен на базе данных.
  • Объект ResultSet: объект ResultSet представляет собой набор результатов, полученных в результате выполнения запроса на базе данных.

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

Что такое statement в контексте JDBC? Виды и отличия.

В контексте JDBC, Statement – это интерфейс, который используется для выполнения SQL-запросов к базе данных. Statement предоставляет несколько методов для выполнения запросов, обработки результатов и управления параметрами.

Существует три типа Statement:

  1. Statement – это базовый интерфейс, который используется для выполнения статических SQL-запросов. Этот тип Statement не поддерживает передачу параметров, и запросы могут содержать только “хардкод” значения.
  2. PreparedStatement – это интерфейс, который используется для выполнения динамических SQL-запросов с параметрами. Запросы могут содержать параметры, которые могут быть установлены перед выполнением запроса. Это позволяет избежать вставки “хардкод” значений в запросы и обеспечивает более безопасное выполнение запросов.
  3. CallableStatement – это интерфейс, который используется для выполнения хранимых процедур в базе данных. Хранимые процедуры могут принимать параметры и возвращать результаты.

Отличия между этими тремя типами Statement заключаются в их возможностях и использовании. PreparedStatement более эффективен и безопасен, чем Statement, так как позволяет использовать параметры вместо закодированных значений в запросах. CallableStatement используется для вызова хранимых процедур, которые часто используются в базах данных.

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

Что такое Hibernate? Что такое JPA? Их отличия.

Hibernate и JPA (Java Persistence API) – это две технологии для работы с объектно-реляционными отображениями (ORM) в Java. ORM – это методика программирования, которая позволяет сохранять объекты Java в базе данных и извлекать их из нее, а также связывать объекты Java с записями в таблицах базы данных.

Hibernate – это конкретная реализация ORM, которая была разработана для упрощения работы с базами данных и сокращения объема кода, необходимого для взаимодействия с базой данных. Hibernate предоставляет набор классов и методов для выполнения операций CRUD (создание, чтение, обновление и удаление), а также для выполнения более сложных операций, таких как связывание объектов, кеширование, отложенная загрузка и т.д.

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

Основные отличия между Hibernate и JPA:

  1. Hibernate – это конкретная реализация ORM, тогда как JPA – это стандартный API для ORM в Java.
  2. Hibernate предоставляет более мощный и гибкий инструментарий для работы с базами данных, тогда как JPA – это стандартизированный API, который ограничивает функциональность и гибкость.
  3. Hibernate может быть более эффективным, чем JPA, поскольку он предоставляет более прямой доступ к функциональности ORM.
  4. Hibernate имеет большую общественную поддержку и сообщество, чем JPA, что может облегчить поиск ответов на вопросы и решение проблем.

В общем, Hibernate и JPA – это две технологии ORM, которые могут быть использованы в Java для работы с базами данных. Hibernate предоставляет более мощный и гибкий инструментарий, тогда как JPA предоставляет стандартизированный API для ORM в Java. Выбор между Hibernate и JPA зависит от конкретных потребностей проекта и уровня гибкости, необходимой для работы с базой данных.

Что такое N+1 SELECT проблема? 

N+1 SELECT проблема – это проблема, которая может возникнуть при работе с ORM приложениями, когда при загрузке коллекции объектов, если эти объекты имеют отношения Many-To-Many, выполняется N+1 SQL запросов к базе данных. Это означает, что для каждого элемента, который нужно загрузить, выполняется один SQL запрос для получения списка элементов и еще N SQL запросов для получения связанных с этим элементом данных.

Например, предположим, что у нас есть две таблицы в базе данных: users и orders, связанные отношением Many-To-Many. Если мы используем ORM приложение для загрузки всех пользователей и связанных с ними заказов, то при выполнении запроса к базе данных, ORM будет выполнять N+1 SQL запросов, где N – это количество пользователей. Сначала будет выполнен запрос для загрузки всех пользователей, а затем для каждого пользователя будет выполнен еще один запрос для загрузки связанных с ним заказов.

Проблема N+1 SELECT может привести к значительному увеличению времени выполнения запросов к базе данных, особенно когда связи между таблицами Many-To-Many достаточно сложны и данные могут быть извлечены по множеству путей.

Существуют различные способы решения проблемы N+1 SELECT, например, можно использовать методы предзагрузки данных (Eager Loading) или ленивую загрузку данных (Lazy Loading) для загрузки данных в одном запросе, а также использовать инструменты и библиотеки, такие как Hibernate Fetch и JPA Fetch.

Что такое REST API?

REST (Representational State Transfer) API – это архитектурный стиль веб-сервисов, который использует стандартные протоколы HTTP для создания, обновления и удаления ресурсов. REST API обеспечивает стандартизированный способ взаимодействия между клиентом и сервером через Интернет.

REST API использует понятие ресурсов (resources), которые могут быть представлены любыми объектами, данных или информацией в приложении. Ресурсы могут быть представлены в формате JSON, XML, HTML и других форматах данных.

Основные принципы REST API включают:

  1. Клиент-серверная архитектура: REST API разделяет приложение на две части – клиентскую и серверную, которые могут быть развернуты на разных машинах.
  2. Стандартные методы HTTP: REST API использует стандартные HTTP-методы, такие, как GET, POST, PUT и DELETE для управления ресурсами.
  3. Stateless (без состояния): REST API не сохраняет состояние клиента на сервере, что позволяет балансировать нагрузку и масштабировать приложение.
  4. Кэширование: REST API поддерживает кэширование, что позволяет уменьшить количество запросов к серверу.

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

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

  1. Java Concurrency In Practice
  2. Курс “Алгоритмы” часть первая
  3. Курс “Алгоритмы” часть вторая
  4. Как работает volatile в Java
  5. Java Persistence with Spring Data and Hibernate