Как стать автором
Обновить
65.78
SSP SOFT
🔹 Более 15 лет занимаемся заказной разработкой ПО

Реальный кейс: 22 вопроса на техническом интервью на вакансию Java Lead в JP Morgan в 2025 году

Уровень сложностиСложный
Время на прочтение35 мин
Количество просмотров28K
Автор оригинала: Shivam Srivastava

Аудитории Хабра наверняка интересно, какие вопросы задают на техинтервью в мультинациональных корпорациях. Ведь ИТ-индустрия была и остается глобальной, несмотря на геополитику и разные другие сложности. Как минимум, такая информация об интервью «из первых рук» — это ориентир по уровню сложности собеседования на позицию Java Lead или Senior Java Developer в любых других компаниях, включая крупные российские банковские и промышленные структуры. Именно поэтому эта статья в нашем блоге SSP SOFT.

***

Дисклеймер: Статья очень-очень длинная (58К символов) и ее желательно рассматривать как туториал к техинтервью на Java Developer / Java Lead, и выделить время на проработку каждого вопроса — т.е. это не "чтиво по диагонали". В реальности, кандидат в той истории вряд ли отвечала столь подробно на интервью как описано в статье, т.к. время собеса обычно ограничено 1-1.5 часами. И 22 вопроса — это непривычно много для техинтервью даже на Java Lead, ведь обычное число вопросов общего характера и заданий с написанием кода на собесе не превышает 5-8.

Автор статьи сознательно дает больше материала, чтобы читатели могли глубже изучить тематику каждого ответа. Со своей стороны, при подготовке перевода в SSP SOFT, некоторые ответы были еще дополнены.

Для контекста этого технического интервью надо дать портрет кандидата: это женщина, сеньор с 7.5-летним опытом в Java, Spring Boot, MongoDB, Microservices и сопутствующих технологиях.

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

В день собеседования присутствовали HR и техлид интервьюер, все началось вовремя, и после обмена любезностями они перешли к техническим вопросам. По отзыву кандидата, и рекрутер, и интервьюер вели себя очень профессионально.

Далее в тексте пойдут вопросы, ответы и пояснения к ответам со стороны автора. В отличие от оригинала статьи, где вопросы идут «сплошной простыней», мы вынесли список вопросов в начало статьи и сделали его кликабельным для вашего удобства.

  1. Что такое согласование содержимого в микросервисах?

  2. Почему мы используем Java 8? В чем эта версия лучше Java 7?

  3. Что такое Dependency Injection и какие у него преимущества?

  4. Назовите преимущества MongoDB перед MySQL.

  5. Предположим, у вас есть два потока. Один из них выводит (1,2,3…), а другой — (A,B,C,…). Как обеспечить их выполнение в чередующейся последовательности (1,A,2,B…)?

  6. Что такое Bean? В чем разница между обычным Bean и Spring Bean?

  7. Как обеспечить безопасность работы микросервисов?

  8. В чем Разница между @Component vs @Service vs @Repository— в Java Spring?

  9. Что произойдет, если выполнять операции с БД прямо в контроллере?

  10. Назовите преимущества использования слоя DAO.

  11. Назовите методы измерения производительности базы данных.

  12. Опишите принцип проектирования масштабируемой БД, подход к задаче и сложности.

  13. Как происходит обработка исключений в Spring Boot?

  14. Как написать кастомный метод в MongoDB?

  15. Сравните Maven и Gradle по различным параметрам корпоративного применения.

  16. Что такое rebase в Git?

  17. Какие преимущества у лямбда-выражений?

  18. Каков контракт между методами hashCode() и equals() в Java?

  19. Что такое WeakHashMap?

  20. Объясните внутреннее устройство ConcurrentHashMap.

  21. Как преобразовать список имен студентов в набор данных?

  22. Объясните, что такое глубокое копирование (Deep Copy) с примерами

1. What is content negotiation in microservices?

(Что такое согласование содержимого в микросервисах?)

Согласование содержимого в микросервисах — это процесс, в котором клиент и сервер договариваются о формате данных, которыми они будут обмениваться во время HTTP-запроса/ответа. Это позволяет микросервису предоставлять ответы в различных форматах, таких как JSON, XML или обычный текст, в зависимости от предпочтений или возможностей клиента.

Как это работает:

  1. Client Preference (Предпочтения клиента): Клиент указывает предпочтительный формат (форматы) данных с помощью заголовка HTTP header:

    Accept header: Определяет типы медиа, которые может обрабатывать клиент, например, Accept: application/json, application/xml.
    Content-Type header: Определяет формат данных, отправляемых в теле запроса, например, Content-Type: application/json.

  2. Server Response (Ответ сервера): Сервер анализирует заголовок Accept header и определяет наилучший формат, который он может предоставить. Если поддерживается несколько форматов, сервер выбирает наиболее подходящий из них на основе приоритета.

  3. Response (Ответ): Сервер отправляет ответ в согласованном формате вместе с заголовком Content-Type, указывающим тип носителя,
    например Content-Type: application/json.

Types of Content Negotiation (Типы переговоров по содержимому)

  1. Server-Driven Negotiation (Переговоры, управляемые сервером): Сервер принимает решение о формате ответа на основе заголовка Accept от клиента.

  2. Client-Driven Negotiation (Переговоры, управляемые клиентом): Клиент запрашивает определенные форматы через параметры запроса, например,?format=json.

  3. Agent-Driven Negotiation: Посредническое программное обеспечение или прокси согласовывает формат содержимого.

Преимущества микросервисов

  1. Гибкость: Поддержка различных клиентских приложений за счет предоставления данных в понятных им форматах.

  2. Интероперабельность: Улучшает интеграцию с системами, использующими различные форматы данных.

  3. Масштабируемость: Позволяет сервисам развиваться без нарушения совместимости с клиентами за счет добавления или отказа от форматов.

Пример: Предположим, клиент отправляет микросервису следующий запрос:

GET /users HTTP/1.1
Accept: application/json

Если сервер поддерживает JSON, он отвечает на запрос:

HTTP/1.1 200 OK Content-Type: application/json
{   
"id": 1,   
"name": "Shivam Srivastava" 
}

В противном случае. сервер возвращает ответ:

HTTP/1.1 406 Not Acceptable

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

2. Why do we use Java 8? Why was it introduced over Java 7?

(Почему мы используем Java 8? В чем эта версия лучше Java 7?)

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

  • Функциональное программирование:
    В Java 8 появились парадигмы функционального программирования, которые упрощают и улучшают процесс написания кода. Это помогает писать более чистый, читабельный и лаконичный код.

  • Stream API:
    Мощная абстракция для обработки коллекций данных в функциональном стиле. Такие операции, как фильтрация, сопоставление и сокращение больших наборов данных, теперь выполняются проще и быстрее.

  • Улучшенный параллелизм:
    В Java 8 появились параллельные потоки, которые позволяют разработчикам обрабатывать данные параллельно с минимальными усилиями, повышая производительность в многоядерных системах.

  • Повышенная производительность:
    Такие функции, как лямбда-выражения (lambda expressions), ссылки на методы и методы по умолчанию, сокращают количество шаблонизированного кода и повышают производительность разработчиков.

  • API для работы с датой и временем:
    Новый пакет java.time предоставляет современный, неизменяемый и безопасный для потоков способ работы с датой и временем, заменяя старые, неудобные java.util.Date и java.util.Calendar.

  • Обратная совместимость:
    Все функции в Java 8 полностью обратно совместимы (backward-compatible), что обеспечивает бесперебойную работу унаследованных кодовых баз и при этом позволяет использовать новые возможности.

  • Улучшенная производительность:
    Благодаря усовершенствованиям в JVM и библиотеках Java 8 обеспечивает более высокую производительность как при функциональном программировании, так и при выполнении приложений в целом.

Почему язык Java 8 был представлен вместо Java 7:

Мнение автора статьи: Настоящая причина появления Java 8 заключалась в том, что Java начала терять долю рынка в пользу Python, который стал более эффективным и сводил шаблонный код к минимуму.

Но отвечать так, как в цитате выше, на интервью в международной компании м.б. чревато. Безопаснее назвать причины, которые сама Oracle определила как драйверы появления версии Java 8:

Модернизация API: API в Java 7 были многословными и недостаточно гибкими. В Java 8 это исправили за счет:

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

  • Обновления API, таких как Collections и Map, с поддержкой потоков (Streams) и лямбда-выражений.

Улучшенная работа с датами и временем: Классы Date и Calendar в Java 7 были изменяемыми, неудобными и подверженными ошибкам. В Java 8 появился пакет java.time, вдохновленный Joda-Time, который предложил:

  • Неизменяемость (immutability) для повышения безопасности.

  • Интуитивные API для работы с датами и временем.

Параллельность: В Java 7 был представлен Fork/Join Framework, но он оказался сложным для большинства разработчиков. В Java 8 параллельная обработка стала проще благодаря:

  • Параллельным потокам (Parallel Streams) — высокоуровневой абстракции для параллельной обработки данных.

Глобальные тренды в разработке ПО: С ростом big data, облачных вычислений и современных практик разработки в Java 8 появились инструменты для решения актуальных задач:

  • Эффективная обработка данных с помощью Streams.

  • Улучшенная масштабируемость и конкурентность благодаря параллельным потокам и оптимизациям производительности.

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

  • Лямбда-выражений для краткости.

  • Ссылок на методы для лучшей читаемости.

Обратная совместимость без ущерба для инноваций: Разработчикам нужны новые возможности без риска сломать существующий код. Java 8 обеспечила это с помощью:

  • Default-методов, позволяющих расширять интерфейсы без изменения старого кода.

  • Полной обратной совместимости, что упростило переход на новую версию.

3. What is Dependency Injection and what are it’s advantages?

(Что такое Dependency Injection и какие у него преимущества?)

Dependency Injection (DI) — это техника, используемая в объектно-ориентированном программировании для реализации Inversion of Control (IoC).

Она заключается в том, что зависимые объекты (dependencies) передаются извне, вместо того чтобы создаваться внутри зависимого объекта (dependent object).

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

Как работает Dependency Injection?

В основе DI лежат два ключевых понятия:

  • Зависимости (Dependencies) – объекты или ресурсы, которые необходимы классу для работы.

  • Инъекция (Injection) – процесс передачи этих зависимостей зависимому объекту.

Существует три основных способа внедрения зависимостей:

  1. Constructor Injection – зависимости передаются через конструктор.

  2. Setter Injection – зависимости устанавливаются с помощью сеттеров (public setter methods).

  3. Interface Injection – зависимость предоставляет метод-инъектор, который зависимый класс использует для получения зависимостей (используется реже).

Пример без Dependency Injection (жесткая связность):

public class Service {
    private Repository repository = new Repository(); // Жёсткая связность

    public void performService() {
        repository.save();
    }
}

В этом случае Service создаёт экземпляр Repository внутри себя, что затрудняет тестирование и изменение кода.

включая Dependency Injection (гибкость и тестируемость):

public class Service {
    private Repository repository;

    public Service(Repository repository) { // Constructor Injection
        this.repository = repository;
    }
    
    public void performService() {
        repository.save();
    }
}

Теперь объект Repository передается извне, а значит, Service не зависит от его создания и может легко работать с разными реализациями.

Преимущества Dependency Injection:

  1. Слабая связность (Loose Coupling)

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

  2. Улучшенная тестируемость

    • Зависимости можно легко мокать или подменять заглушками при юнит-тестировании.

  3. Простота сопровождения

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

  4. Повторное использование кода

    • Одна и та же зависимость может использоваться в разных классах, избегая дублирования кода.

  5. Лучшая масштабируемость

    • Добавление новых зависимостей или функций становится проще, так как код не жестко связан с конкретными реализациями.

  6. Соответствие принципам SOLID

    • Принцип единственной ответственности (SRP) – создание зависимостей отделено от их использования.

    • Принцип инверсии зависимостей (DIP) – классы зависят от абстракций (интерфейсов), а не от конкретных реализаций.

  7. Гибкость конфигурации

    • DI позволяет динамически управлять зависимостями в зависимости от среды (разработка, продакшн и т. д.).

  8. Лучшая читаемость кода

    • Основной код становится чище, так как управление зависимостями берёт на себя DI-контейнер или фреймворк (например, Spring).

Резюме: Dependency Injection – это полезная техника, которая делает код гибким, тестируемым и поддерживаемым, что особенно важно в крупных проектах.

4. What are advantages of MongoDB over MySQL?

(Назовите преимущества MongoDB перед MySQL)

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

Автор свел преимущества MongoDB перед MySQL в 3 таблицы:

Таблица 1. Основное различие

Характеристика 

MongoDB 

MySQL 

Проектирование схем 

Без схемы; JSON-подобные документы. 

Фиксированная схема, Требует миграции

Масштабируемость

Горизонтальное масштабирование с помощью шардинга. 

Вертикальное масштабирование с ограничениями.

Производительность

Оптимизирована для больших неструктурированных данных.

Медленно для больших наборов данных  

Модель данных 

Поддерживает массивы, вложенные объекты и т. д.

Плоская, табличная; нуждается в нормализации. 

Простота разработки 

JSON-подобный синтаксис: без предварительной схемы. 

Требуется предопределенная схема.

Таблица 2. Сравнение функций

Характеристика 

MongoDB 

MySQL 

Высокая доступность 

Встроенная репликация; автоматическое восстановление после отказа

Ручная репликация; менее автоматизирована. 

Поддержка больших данных 

Идеально подходит для работы с большими данными в режиме реального времени 

приложений. 

Лучше подходит для транзакционных рабочих нагрузок.

Эволюция схем 

Динамические изменения без простоев. 

Изменения требуют 

миграции/простоя. 

Нативный JSON и NoSQL 

Хранение данных в BSON (двоичный JSON). 

Поддержка SON менее эффективна. 

Геопространственные запросы

Расширенная поддержка геопространственных данных. 

Базовая поддержка; менее эффективна.

Таблица 3. Применимость для разных кейсов

Характеристика 

MongoDB 

MySQL 

Агрегация 

Мощные конвейеры для аналитики, 

SQL-запросы; менее гибкие. 

Гибкость

Работа с изменяющимися/гетерогенными данными. 

Жесткие ограничения схемы. 

Стоимость обслуживания 

Меньше благодаря масштабируемости и схеме. 

Более сложное обслуживание при изменении схемы. 

Пригодность для конкретных применений 

Идеально подходит для IoT, аналитики и реального времени. 

Лучше всего подходит для финансовых/транзакционных приложений.

Резюме: Если не хочется рассматривать авторские таблицы, то вот наш анализ их содержимого и преимущества MongoDB в виде списка:

Основные возможности (Core Features):

  • Гибкая схема данных (MongoDB не требует строгой структуры таблиц, как в MySQL).

  • Лучшая масштабируемость благодаря горизонтальному шардированию.

  • Встроенная поддержка репликации и отказоустойчивости.

Расширенные возможности (Advanced Capabilities):

  • Поддержка встроенных документов, что упрощает работу с вложенными структурами данных.

  • Более высокая производительность при обработке больших объёмов данных.

  • Поддержка сложных запросов с помощью Aggregation Framework.

Сценарии использования (Use Cases):

  • MongoDB отлично подходит для Big Data, IoT, аналитики в реальном времени, хранения JSON-документов.

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

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

5. Suppose you have 2 threads. One of them prints (1,2,3…) and the other one prints (A,B,C,..). How will you ensure that they run in a sequence, so that it prints (1,A,2,B…)?

(Предположим, у вас есть два потока. Один из них выводит (1,2,3…), а другой — (A,B,C,…). Как обеспечить их выполнение в чередующейся последовательности (1,A,2,B…)?

Для достижения такой чередующейся последовательности вывода как (1, A, 2, B, ...) можно использовать механизмы синхронизации, такие как общий объект-блокировка (synchronized или ReentrantLock) и переменная условия (wait/notify), чтобы координировать выполнение потоков.

Вот как можно реализовать это в Java:

class AlternatingPrinter {

    private final Object lock = new Object();
    private boolean numberTurn = true; // Указывает, чей ход: потока с числами или буквами

    public static void main(String[] args) {
        AlternatingPrinter printer = new AlternatingPrinter();
        Thread numberThread = new Thread(() -> printer.printNumbers());
        Thread letterThread = new Thread(() -> printer.printLetters());
        numberThread.start();
        letterThread.start();
    }

    public void printNumbers() {
        for (int i = 1; i <= 26; i++) { // Диапазон можно изменить
            synchronized (lock) {
                while (!numberTurn) {
                    try {
                        lock.wait(); // Ожидание своей очереди
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
                System.out.print(i + " ");
                numberTurn = false; // Передача хода потоку с буквами
                lock.notifyAll(); // Уведомление второго потока
            }
        }
    }

    public void printLetters() {
        for (char c = 'A'; c <= 'Z'; c++) { // Диапазон можно изменить
            synchronized (lock) {
                while (numberTurn) {
                    try {
                        lock.wait(); // Ожидание своей очереди
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
                System.out.print(c + " ");
                numberTurn = true; // Передача хода потоку с числами
                lock.notifyAll(); // Уведомление второго потока
            }
        }
    }
}

Вывод потоков:

1 A 2 B 3 C ...

Резюме: Как этот механизм синхронизации работает:
- Общий объект lock используется для синхронизации двух потоков.
- Переменная numberTurn определяет, чей ход: если true, то число, если false, то буква.
- Один поток ждет (wait()), пока не наступит его очередь, а затем выполняет свою задачу.
- После вывода числа или буквы ход передается другому потоку (notifyAll()).

Это гарантирует, что потоки работают поочередно, обеспечивая требуемый порядок вывода.

6. What is a Bean? What are the differences between normal Bean vs Spring Bean?

(Что такое Bean? В чем разница между обычным Bean и Spring Bean?)

Bean — это объект, который создается, настраивается и управляется контейнером. В контексте Java bean — это повторно используемый программный компонент, который следует определенным соглашениям (например, имеет конструктор без аргументов, поддерживает сериализацию и предоставляет геттеры и сеттеры).

В Spring bean — это любой объект, который управляется контейнером Inversion of Control (IoC).

Пример обычного Java Bean:

public class NormalBean {
    private String name;

    public NormalBean() {} // Конструктор без аргументов

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Обычный Java Bean – это просто объект с геттерами/сеттерами, который не управляется Spring и создается вручную.

Пример Spring Bean:

1. Использование аннотации @Component:

import org.springframework.stereotype.Component;

@Component
public class SpringBean {
    private String name;

    public SpringBean() {}

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Здесь SpringBean автоматически регистрируется в контейнере Spring благодаря аннотации @Component.

2. Определение в классе конфигурации:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public SpringBean springBean() {
        return new SpringBean();
    }
}

В этом случае Spring Bean создается вручную через @Bean в классе конфигурации @Configuration.

Основные отличия обычного Bean и Spring Bean:

Критерий

Обычный Java Bean

Spring Bean

Управление объектом

Создается вручную с new

Управляется Spring IoC

Связывание зависимостей

Ручное (в коде)

Автоматическое через Spring

Жизненный цикл

Определяется вручную

Контролируется Spring

Использование аннотаций

Нет

@Component, @Bean и др.

Где используется

В обычных Java-приложениях

В Spring-приложениях

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

7. How do you secure your microservices?

(Как обеспечить безопасность работы микросервисов?)

Вот основные способы обеспечения безопасности микросервисов:

1. Аутентификация и авторизация

Зачем?
Чтобы гарантировать, что только легитимные пользователи и системы получают доступ к сервисам.

Как?

  • Реализовать OAuth 2.0 с токенами для аутентификации пользователей и систем.

  • Централизовать управление идентификацией пользователей с помощью Keycloak, Okta или аналогичных решений.

2. API Gateway

API-шлюз выступает единым входным узлом для всех запросов и обеспечивает безопасность.

Используйте API-шлюзы, такие как Kong, AWS API Gateway, Spring Cloud Gateway, для:

  • Аутентификации входящих запросов.

  • Ограничения частоты запросов (rate limiting).

  • Применения политик контроля доступа.

3. Безопасное соединение

Шифруйте весь трафик между сервисами и клиентами.

Как?

  • Используйте TLS/SSL для защиты соединений.

  • Применяйте взаимный TLS (mTLS) для шифрования трафика между микросервисами.

4. Service Mesh

Service Mesh (e.g., Istio, Linkerd) помогает усилить безопасность:

  • Встроенная поддержка mTLS для безопасного взаимодействия сервисов.

  • Гибкое управление трафиком и доступами.

  • Мониторинг и логирование сетевых событий.

5. Централизованное логирование и мониторинг

Раннее выявление атак и аномалий с помощью логов и мониторинга.

Как?

  • Логируйте все важные события, включая неудачные попытки входа.

  • Используйте инструменты мониторинга:

    • Prometheus

    • ELK Stack (Elasticsearch, Logstash, Kibana)

    • Splunk

6. Безопасное управление секретами

Не храните пароли, ключи API и другие секретные данные в коде.

Как?

  • Используйте специализированные инструменты управления:

    • AWS Secrets Manager

    • Azure Key Vault

    • HashiCorp Vault

Резюме: Защита микросервисов требует комплексного подхода:
- Надежная аутентификация и API Gateway.
- Шифрование трафика (TLS/mTLS).
- Мониторинг и логирование.
- Безопасное хранение секретных данных.
Такой подход минимизирует риски атак и повышает устойчивость системы, основанной на микросервисах.

8. What are the differences between @Component vs @Service vs @Repository?

(Разница между @Component, @Service и @Repository— в Java Spring)

В Spring аннотации @Component, @Service и @Repository используются для автоматического обнаружения и управления бинами в контейнере IoC. Все три аннотации являются специализациями @Component, но предназначены для разных типов классов (Таблица 5).

Таблица 5. Аннотации

Annotation 

Описание 

Use Case

Расширенные функции

@Component 

Общая аннотация для обозначения класса как управляемого Spring bean

Используется, когда не требуется определенная роль для компонента.

Никакого дополнительного поведения. 

@Service

Специализированная аннотация, указывающая на компонент сервисного слоя. 

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

Никакого дополнительного поведения, но улучшает читабельность и семантическое понимание кода. 

@Repository 

Специализированная аннотация. указывающая на DAO (объект доступа к данным) 

Используется для компонентов взаимодействующих с базой данных (репозитории или классы доступа к данным). 

Для исключений: Преобразует специфические для базы данных исключения в иерархию Spring DataAccessException 

Резюме:
@Component – базовая аннотация для любого Spring Bean.
@Service – для сервисного слоя, обработки данных и бизнес-логики.
@Repository – для работы с базами данных и DAO-слоя.
Все три аннотации автоматически регистрируются в Spring-контейнере и могут использоваться с @Autowired для внедрения зависимостей.

9. Suppose you’ve a controller annotation and then you perform DB operation in it. What will happen in that case?

(Что произойдет, если выполнять операции с БД прямо в контроллере?)

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

Что произойдет?

Контроллер сможет обработать HTTP-запрос и выполнить операцию с базой данных, если в него внедрить репозиторий. Например:

@RestController
public class UserController {
    @Autowired
    private UserRepository userRepository;

    @GetMapping("/getUser/{id}")
    public User getUserById(@PathVariable Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

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

Почему это плохая практика?

  • Нарушение принципа разделения ответственности (SoC - Separation of Concerns)

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

  • Сложности в тестировании и поддержке

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

  • Плохая масштабируемость

Если приложение растет, контроллер начинает выполнять слишком много задач.
Код становится жестко связанным (tight coupling), что делает его сложным для изменений и рефакторинга.

  • Отсутствие управления транзакциями

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

Правильный подход

Лучший способ — разделить логику между слоями:

  1. Контроллер (обрабатывает запросы и передает их сервису):

@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/getUser/{id}")
    public User getUserById(@PathVariable Long id) {
        return userService.getUserById(id);
    }
}

Контроллер больше не обращается к базе данных напрямую!

  1. Сервисный слой (содержит бизнес-логику и вызывает репозиторий):

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public User getUserById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

Здесь можно добавить транзакции, обработку ошибок, кэширование и другие механики.

  1. Репозиторий (работает с базой данных через JPA/Hibernate):

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

Этот слой отвечает только за доступ к данным и ничего больше.

Резюме:
- Разделение ответственности – Контроллер обрабатывает HTTP-запросы, сервис выполняет бизнес-логику, а репозиторий управляет данными.
- Тестируемость – Можно тестировать сервисы отдельно от веб-слоя.
- Гибкость и масштабируемость – Можно легко изменять код без нарушения архитектуры.
- Транзакции и управление данными – Можно добавить @Transactional в сервисный слой.

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

10. What are the benefits of using DAO layer?

(Назовите преимущества использования слоя DAO).

DAO (Data Access Object) — это слой, который обеспечивает удобный и структурированный способ работы с источником данных (например, базой данных), абстрагируя механизмы доступа к данным. Использование слоя DAO приносит множество преимуществ, особенно в плане гибкости, поддержки и разделения логики.

Основные преимущества DAO:

  • Разделение ответственности (Separation of Concerns)

  • DAO изолирует логику доступа к данным от бизнес-логики.

Код становится чище: бизнес-логика не зависит от того, как данные извлекаются, сохраняются или обновляются.

Удобство поддержки и масштабируемость:

Вся логика работы с базой данных сосредоточена в одном слое, и изменения (например, смена базы MySQL → PostgreSQL) затронут только DAO.-

  • Легкость рефакторинга: изменения в БД не влияют на остальной код.

  • Упрощение поддержки: легче исправлять ошибки и обновлять код.

Повторное использование кода:

DAO инкапсулирует типовые операции (save, update, delete, find) в универсальные методы.

  • Минимизация дублирования кода.

  • Любая часть приложения может использовать DAO для работы с данными.

Независимость от источника данных:

DAO абстрагирует работу с БД. Например, можно заменить MySQL на MongoDB, изменив только DAO, а не всю логику приложения.

  • Гибкость в выборе и смене базы данных.

  • Уменьшение зависимости от конкретной технологии.

Упрощенное юнит-тестирование:

DAO можно мокировать (mock) или использовать заглушки (stub) в тестах.

  • Тестирование бизнес-логики без реального подключения к базе.

  • Быстрое моделирование различных сценариев работы с БД.

Управление транзакциями:

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

  • Централизованное управление транзакциями.

  • Возможность использовать @Transactional в сервисном слое.

Инкапсуляция и безопасность:

DAO скрывает сложность работы с БД, контролируя доступ к данным.

  • Улучшенная безопасность (минимизация риска SQL-инъекций).

  • Код не содержит сложных SQL-запросов — они инкапсулированы в DAO.

Улучшенная читаемость кода:

DAO абстрагирует SQL-запросы, что делает код более понятным.

  • Четкая организация кода.

  • Бизнес-логика не засорена SQL-запросами.

Гибкость работы с данными:

  • DAO позволяет создавать сложные запросы, включая кастомные SQL-запросы и преобразование данных.

  • Возможность адаптировать запросы под конкретные нужды бизнеса.

Пример использования DAO

public class UserDAO {
    private EntityManager entityManager;

    public UserDAO(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    // Сохранение пользователя
    public void saveUser(User user) {
        entityManager.persist(user);
    }

    // Поиск пользователя по ID
    public User findUserById(Long id) {
        return entityManager.find(User.class, id);
    }

    // Удаление пользователя
    public void deleteUser(Long id) {
        User user = findUserById(id);
        if (user != null) {
            entityManager.remove(user);
        }
    }
}

Этот код изолирует работу с базой данных и делает приложение гибким, поддерживаемым и тестируемым.

Резюме:
- Не пишите SQL-запросы в сервисах или контроллерах.
- Используйте слой DAO для удобного и безопасного взаимодействия с базой данных.
Это делает код гибким, модульным и легко поддерживаемым.

11. How do you measure DB performance?

(Назовите методы измерения производительности базы данных).

Автор в качестве ответа привел список из 14 критериев оценки производительности, но не вдавался в подробности каждого. Видимо, на интервью оценивалось именно количество параметров, про которые знает кандидат.
Ниже приведены эти 14 критериев оценки и оптимизации работы базы данных:

Производительность запросов (Query Performance)

  • Измеряйте время выполнения запросов.

  • Используйте EXPLAIN или EXPLAIN ANALYZE для анализа и оптимизации.

Время отклика (Response Time)

  • Отслеживайте время выполнения запросов (round-trip time).

  • Минимизируйте задержки.

Пропускная способность (Throughput)

  • Оценивайте, сколько запросов обрабатывает база в секунду/минуту.

Соединения с базой (Database Connections)

  • Контролируйте количество активных соединений.

  • Оптимизируйте connection pooling.

Дисковый ввод-вывод (Disk I/O)

  • Измеряйте скорость чтения/записи, очереди операций, нагрузку на диск.

Уровень загрузки процессора (CPU Usage)

  • Мониторьте загрузку CPU, чтобы база данных не была перегружена.

Использование памяти (Memory Usage)

  • Оценивайте потребление оперативной памяти.

  • Избегайте нехватки памяти, что ведет к замедлению работы.

Конкуренция за блокировки (Lock Contention)

  • Анализируйте конфликты блокировок и deadlocks, чтобы избежать задержек.

Коэффициент попаданий в кэш (Cache Hit Ratio)

  • Контролируйте, как часто данные загружаются из кэша вместо диска.

Сетевые задержки (Network Latency)

  • Измеряйте время передачи данных между приложением и базой данных.

Логирование медленных запросов (Slow Query Logs)

  • Включите логирование медленных SQL-запросов и оптимизируйте их.

Оптимизация индексов (Index Optimization)

  • Проверяйте эффективность индексов и устраняйте фрагментацию.

Профилирование запросов (Query Execution Plan)

  • Используйте инструменты профилирования SQL-запросов (EXPLAIN, SHOW PROFILE).

Управление схемой базы данных (Database Schema Design)

  • Оптимизируйте структуру таблиц и нормализацию/денормализацию для повышения производительности.

Резюме: Для мониторинга БД можно использовать инструменты:
- Prometheus, New Relic (общий мониторинг).
- Мониторинг метрик (pg_stat_statements, SHOW STATUS).
- MySQL EXPLAIN, PostgreSQL EXPLAIN ANALYZE (анализ запросов).
- Анализ индексов и кэша (SHOW INDEX, pg_stat_user_indexes).
- Системные инструменты (top, iostat, vmstat)
- Query Profiling, Slow Query Logs (профилирование и логирование).
Систематический анализ метрик помогает выявить узкие места и повысить производительность базы данных.

12. How would you design a scalable database? What challenges do you foresee, and how would you mitigate them?

(Опишите принцип проектирования масштабируемой БД, подход к задаче и сложности).

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

  • Проектирование схемы базы данных

    • Начните с нормализованной схемы, чтобы устранить избыточность и обеспечить целостность данных.

    • В критичных по производительности приложениях можно частично денормализовать данные для оптимизации чтения.

  • Горизонтальное масштабирование

    • Используйте шардинг для распределения данных между несколькими узлами (например, по ID пользователя или регионам).

    • Разбейте большие таблицы на партиции (например, по месяцам в системе логирования).

  • Репликация

    • Реализуйте репликацию master-slave, где master обрабатывает запись, а реплики — чтение, улучшая производительность и отказоустойчивость.

    • Для распределенных систем можно использовать multi-master репликацию для обработки записей из нескольких источников.

  • Кеширование

    • Внедрите кеширование (Redis, Memcached) для хранения часто запрашиваемых данных в памяти.

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

  • Балансировка нагрузки

    • Добавьте балансировщик нагрузки для распределения запросов между несколькими экземплярами базы данных.

  • Асинхронная обработка

    • Для интенсивных операций записи используйте очереди сообщений (Kafka, RabbitMQ) для асинхронной обработки, снижая нагрузку на базу.

  • Облачные и распределенные базы данных

    • Для масштабных приложений можно использовать Cassandra, CockroachDB, MongoDB, которые изначально рассчитаны на горизонтальное масштабирование.

    • Рассмотрите облачные сервисы (AWS RDS, Google Cloud SQL) с встроенными возможностями масштабирования и отказоустойчивости.

  • Мониторинг и оптимизация

    • Отслеживайте метрики БД (производительность запросов, загрузку CPU, использование памяти, I/O) с помощью Prometheus, Grafana.

    • Регулярно оптимизируйте медленные запросы и следите за актуальностью индексов.

  • Архивирование и управление данными

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

Вызовы и способы их решения

  • Согласованность данных

    • В распределенных базах данных поддержание строгой согласованности сложно. Используйте модели eventual consistency, если допустимо.

  • Управление шардингом

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

  • Оптимизация запросов

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

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

13. How do you handle exceptions in Spring Boot application?

(Как происходит обработка исключений в Spring Boot?)

Обработка исключений в Spring Boot может быть организована с помощью нескольких ключевых подходов.

  • Использование @ControllerAdvice и @ExceptionHandler

    • @ControllerAdvice определяет глобальный обработчик исключений для всего приложения.

    • @ExceptionHandler задает, как обрабатывать определенные исключения.

    • Пример:

      @RestControllerAdvice
      public class GlobalExceptionHandler {
          @ExceptionHandler(ResourceNotFoundException.class)
          public ResponseEntity<String> handleResourceNotFound(ResourceNotFoundException ex) {
              return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
          }
      
          @ExceptionHandler(Exception.class)
          public ResponseEntity<String> handleGenericException(Exception ex) {
              return new ResponseEntity<>("An unexpected error occurred: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
          }
      }
      
    • Такой подход обеспечивает единообразную обработку исключений и возвращает корректные HTTP-ответы.

  • Использование аннотации @ResponseStatus

    • Позволяет автоматически сопоставлять исключения с HTTP-статусами.

    • Пример:

      @ResponseStatus(HttpStatus.NOT_FOUND)
      public class ResourceNotFoundException extends RuntimeException {
          public ResourceNotFoundException(String message) {
              super(message);
          }
      }
      
    • При выбрасывании этого исключения Spring автоматически возвращает статус 404 с заданным сообщением.

  • Создание структуры кастомного ответа с ошибкой

    • Для детального ответа можно создать объект с полями timestamp, message, details.

    • Пример:

      public class ErrorResponse {
          private String timestamp;
          private String message;
          private String details;
          // Геттеры и сеттеры
      }
      
    • Использование в обработчике:

      @ExceptionHandler(Exception.class)
      public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex, WebRequest request) {
          ErrorResponse error = new ErrorResponse();
          error.setTimestamp(LocalDateTime.now().toString());
          error.setMessage(ex.getMessage());
          error.setDetails(request.getDescription(false));
      
          return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
      }
      
  • Обработка ошибок валидации

    • При использовании @Valid или @Validated, Spring выбрасывает MethodArgumentNotValidException, если валидация не пройдена.

    • Пример обработки:

      @ExceptionHandler(MethodArgumentNotValidException.class)
      public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException ex) {
          String errors = ex.getBindingResult().getAllErrors().stream()
                            .map(ObjectError::getDefaultMessage)
                            .collect(Collectors.joining(", "));
          return new ResponseEntity<>("Validation failed: " + errors, HttpStatus.BAD_REQUEST);
      }
      
  • Логирование исключений

    • Все исключения можно логировать с помощью SLF4J или другого фреймворка.

    • Пример:

      private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
      
      @ExceptionHandler(Exception.class)
      public ResponseEntity<String> handleGenericException(Exception ex) {
          logger.error("An error occurred: ", ex);
          return new ResponseEntity<>("Something went wrong", HttpStatus.INTERNAL_SERVER_ERROR);
      }
      
  • Обработка непредвиденных исключений

    • Fallback-механизм гарантирует, что необработанные исключения не приводят к критическим сбоям приложения.

Резюме: Перечисленные методы помогают централизованно и эффективно управлять исключениями в Spring Boot.

14. How to write a custom method in MongoDB?

(Как написать кастомный метод в MongoDB?)

Создание кастомного метода в MongoDB зависит от контекста его использования. Реализовать такие методы можно разными способами:

  • Использование MongoDB Shell или Compass

    • Можно писать кастомную логику прямо в MongoDB Shell (JavaScript). Пример:

      db.collectionName.findCustom = function(criteria) {
          return this.find({ $or: [criteria] }).sort({ createdAt: -1 });
      };
      
    • Добавляет метод findCustom в коллекцию, который можно повторно использовать. Обычно применяется для тестирования и прототипирования.

  • Использование агрегатных запросов (Aggregation Framework)

    • Для сложных запросов можно использовать Aggregation Framework.

    • Пример:

      db.orders.aggregate([
          { $match: { status: "Pending" } },
          { $group: { _id: "$customerId", total: { $sum: "$amount" } } },
          { $sort: { total: -1 } }
      ]);
      
    • Запрос находит все заказы со статусом "Pending", группирует их по customerId, суммирует сумму заказов и сортирует результат.

  • Кастомные методы в коде приложения (Custom Repository Methods)

    • Чаще всего кастомные методы реализуются на стороне приложения через Mongoose (Node.js) или Spring Data MongoDB (Java).

    • Пример: использование Spring Data MongoDB

      • Создание интерфейса репозитория:

        public interface CustomUserRepository {
            List<User> findUsersByCustomCriteria(String status, int minAge);
        }
        
      • Реализация репозитория:

        public class CustomUserRepositoryImpl implements CustomUserRepository {
            @Autowired
            private MongoTemplate mongoTemplate;
        
            @Override
            public List<User> findUsersByCustomCriteria(String status, int minAge) {
                Query query = new Query();
                query.addCriteria(Criteria.where("status").is(status).and("age").gte(minAge));
                return mongoTemplate.find(query, User.class);
            }
        }
        
      • Интеграция с сервисом:

        @Service
        public class UserService {
            @Autowired
            private CustomUserRepository customUserRepository;
        
            public List<User> getFilteredUsers(String status, int minAge) {
                return customUserRepository.findUsersByCustomCriteria(status, minAge);
            }
        }
        
    • Такой подход позволяет изолировать кастомную логику запросов и легко интегрировать их в приложение.

  • Кастомные JavaScript-функции в MongoDB

    • Можно сохранять и вызывать кастомные функции прямо в MongoDB.

    • Создание функции:

      db.system.js.save({
          _id: "findUsersByAge",
          value: function(age) {
              return db.users.find({ age: { $gte: age } });
          }
      });
      
    • Вызов функции:

      db.loadServerScripts();
      findUsersByAge(30);
      
  • Кастомные команды (Custom Commands)

    • В MongoDB можно определять кастомные команды с помощью серверных скриптов или плагинов.

Резюме: Перечисленные методы позволяют гибко расширять функциональность MongoDB в зависимости от требований приложения.

15. What are the differences between Maven and Gradle?

(Сравните Maven и Gradle по различным параметрам корпоративного применения).

Автор свел различия в Таблицу 6.
Со своей стороны, мы написали ниже небольшое дополнение об этих инструментах.

Maven: Инструмент для управления зависимостями и сборки проектов, используемый в основном в экосистеме Java.
Для чего используется при работе с БД:

  • Управление зависимостями драйверов для работы с БД (например, JDBC, Hibernate).

  • Автоматизация сборки проектов, использующих базы данных.

  • Настройка плагинов для миграции схемы БД (Flyway, Liquibase).

Gradle: Современный инструмент сборки, поддерживающий декларативный и императивный стиль конфигурации.
Для чего используется при работе с БД:

  • Гибкое управление зависимостями, включая JDBC-драйверы и ORM-фреймворки.

  • Автоматизация работы с БД (запуск скриптов, тестирование).

  • Поддержка миграции БД через плагины (Flyway, Liquibase).

  • Улучшенная производительность по сравнению с Maven при сборке проектов.

Характеристика

Maven

Gradle

Конфигурация

XML (pom.xml)

Groovy/Kotlin DSL (build.gradle)

Производительность

Медленнее из-за фазы валидации

Быстрее за счет инкрементной сборки

Гибкость

Жесткие стандарты, меньше кастомизации

Гибкий DSL, возможность кастомизации

Легкость в использовании

Прост в освоении, но XML громоздок

Требует изучения DSL, но удобнее в использовании

Управление зависимостями

Централизованное, стабильное

Гибкое, но требует более точных настроек

Поддержка нескольких проектов

Есть, но сложнее в настройке

Лучшая поддержка многомодульных проектов

Скорость сборки

Медленная, без кэширования

Быстрая за счет кэширования и инкрементной сборки

Популярность

Широко используется в Java-проектах, в т.ч. легаси

Популярен в Android и современных проектах

Резюме:
- Maven — стабильный, предсказуемый, простой в настройке, но медленный и менее гибкий.
- Gradle — более мощный, гибкий и быстрый, но сложнее в освоении.

16. What is rebasing in GIT?

(Что такое rebase в Git?)

Rebasing в Git — это способ переноса коммитов с одной ветки на другую в линейной последовательности.

Как работает rebase:

  • Git применяет коммиты текущей ветки к целевой ветке по одному.

  • Коммиты пересоздаются с новыми хешами, так как они применяются в новом контексте.

Команда для выполнения rebase:

git checkout feature-branch  
git rebase main

Эта команда переносит коммиты feature-branch так, чтобы они шли после коммитов из main.

Преимущества:

  • Чистая история — делает историю коммитов линейной и понятной.

  • Отсутствие merge-коммитов — в отличие от слияния (merge), rebase не создает дополнительных коммитов.

Когда использовать:

  • Для обновления feature-ветки перед слиянием с основной веткой.

  • Для очистки истории коммитов перед объединением.

Риски:

  • Изменение истории — rebase изменяет хеши коммитов, что может привести к проблемам при совместной работе.

  • Разрешение конфликтов — если есть конфликты, их нужно будет решать для каждого коммита.

Резюме:
- Rebase удобен для поддержания чистой и линейной истории проекта.
- Подходит для обновления feature-веток и подготовки к слиянию.
- Надо использовать с осторожностью, особенно в общих ветках, чтобы избежать проблем с измененной историей.

17. What are the advantages of Lambda expression?

(Какие преимущества у лямбда-выражений?)
Автор свел преимущества в таблицу 7, к которой мы в SSP SOFT добавили еще один столбец с выводами по каждому преимуществу. Enjoy!

Преимущество

Объяснение

Вывод

Лаконичный код

Убирает необходимость в анонимных классах и избыточном коде.

Код становится компактнее и удобнее в написании.

Улучшенная читаемость

Код становится более понятным и компактным, особенно при работе с коллекциями.

Упрощает понимание логики программы.

Поддержка функциональных интерфейсов

Позволяет использовать @FunctionalInterface, упрощая разработку.

Способствует более чистой архитектуре кода.

Облегчает функциональное программирование

Позволяет применять концепции функционального программирования, такие как map, filter, reduce.

Делает код более выразительным и декларативным.

Расширенная коллекция операций

Улучшает работу с Stream API, добавляя мощные методы обработки данных.

Позволяет писать более эффективный и понятный код.

Параллельная обработка

Позволяет легко использовать многопоточность и parallelStream для повышения производительности.

Ускоряет выполнение операций на больших данных.

Сокращает количество шаблонов кода

Избавляет от необходимости писать однотипные реализации интерфейсов.

Упрощает поддержку и уменьшает количество повторяющегося кода.

18. What is the contract between Hashcode and equals method in Java?

(Каков контракт между методами hashCode() и equals() в Java?)

Контракт между hashCode() и equals() описывается следующим образом:

  • Если два объекта равны (согласно equals()), то их hashCode() должен быть одинаковым.
    Это гарантирует предсказуемое поведение в коллекциях, использующих хеширование.

  • Если два объекта имеют одинаковый hashCode(), они не обязаны быть равными.
    Возможны коллизии, когда разные объекты получают одинаковый хеш-код.

  • Если переопределяется equals(), нужно переопределить и hashCode().
    В противном случае коллекции могут работать некорректно.

Пример

Без правильной реализации hashCode():

class Person {
    String name;

    Person(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof Person) {
            return ((Person) o).name.equals(this.name);
        }
        return false;
    }
}

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

С правильной реализацией hashCode():

class Person {
    String name;

    Person(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof Person) {
            return ((Person) o).name.equals(this.name);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return name.hashCode();
    }
}

Резюме: Объекты с одинаковым name будут иметь одинаковый hashCode(), обеспечивая корректное поведение в коллекциях, использующих хеширование.

19. What is a weak hashmap?

(Что такое WeakHashMap?)

WeakHashMap — это разновидность карты (Map) в Java, где ключи хранятся как слабые ссылки (weak references). Это означает, что если на ключевой объект больше нет сильных ссылок, он может быть удален сборщиком мусора, даже если он все еще присутствует в WeakHashMap.

В отличие от HashMap, где ключи хранятся как сильные ссылки, препятствующие их сборке сборщиком мусора, WeakHashMap автоматически удаляет элементы, если на ключ больше нет ссылок.

Основные особенности:

  • Слабые ссылки для ключей:
    Если ключ становится недоступным для активных потоков (т. е. больше нет сильных ссылок на него), соответствующая запись удаляется при следующем проходе сборщика мусора.

  • Автоматическая очистка:
    Полезно для кеширования, где нежелательно, чтобы устаревшие ключи продолжали занимать память.

  • Значения остаются сильными ссылками:
    Значения в WeakHashMap остаются в памяти, пока на них есть ссылки.

Использование:

  • Кеширование с учетом памяти:
    Позволяет автоматически очищать устаревшие записи без явного управления памятью.

  • Хранение метаданных:
    Полезно для временных сопоставлений, которые не должны препятствовать сборке мусора.

Пример:

import java.util.*;

public class WeakHashMapExample {
    public static void main(String[] args) {
        Map<String, String> map = new WeakHashMap<>();
        
        String key = new String("Key");
        map.put(key, "Value");

        System.out.println("Before GC: " + map);
        key = null;  // Удаляем сильную ссылку на ключ
        
        // Запрос на сборку мусора
        System.gc();

        System.out.println("After GC: " + map);  // Запись может быть удалена
    }
}

В этом примере, после установки key = null и вызова сборщика мусора, запись в WeakHashMap может быть удалена.

Резюме:
- Экономия памяти: WeakHashMap автоматически удаляет записи, если на ключ больше нет сильных ссылок, что помогает избежать утечек памяти.
- Полезен для кеширования: Позволяет хранить временные данные без необходимости явного удаления устаревших записей.
- Не гарантирует сохранность данных: Записи могут быть удалены в любой момент при работе сборщика мусора, поэтому WeakHashMap не подходит для критически важных данных.
- Отличается от HashMap: В отличие от HashMap, который хранит ключи как сильные ссылки, WeakHashMap позволяет сборщику мусора управлять их временем жизни.
- Значения остаются в памяти: Хотя ключи являются слабыми ссылками, значения остаются доступными, пока на них есть ссылки.
- Не подходит для всех задач: Если важна надежность хранения данных, лучше использовать HashMap или другие реализации Map.

20. Explain Internal working of concurrent hashmap?

(Объясните внутреннее устройство ConcurrentHashMap).

ConcurrentHashMap — это потокобезопасная карта, появившаяся в Java 5 и предназначенная для высокого параллелизма, которая позволяет нескольким потокам читать и записывать в карту одновременно, не блокируя друг друга.

В отличие от обычной карты HashMap, которая не синхронизирована и склонна к несогласованности данных при одновременном доступе, ConcurrentHashMap обеспечивает эффективный способ обработки одновременных операций.

Внутренний механизм работы

  1. Сегментированная блокировка (блокировка на уровне корзин)

    • ConcurrentHashMap делит данные на сегменты (по умолчанию 16).

    • Каждый сегмент работает как отдельная хеш-таблица с независимой блокировкой.

    • Это позволяет разным потокам изменять разные сегменты одновременно без конфликта.

  2. Корзины (Buckets)

    • Внутри сегментов данные хранятся в виде массива корзин, как в обычном HashMap.

    • При высокой загрузке корзины могут использовать связанный список или сбалансированное дерево (TreeBin).

  3. Уровень конкурентности (Concurrency Level)

    • Определяет количество сегментов. По умолчанию 16, что позволяет 16 потокам работать параллельно.

    • Этот параметр можно настроить при создании карты.

  4. Раздельная блокировка (Lock Striping)

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

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

  5. Неблокирующее чтение (Non-Blocking Reads)

    • Операции чтения (get) не требуют блокировки сегментов.

    • Это обеспечивает высокую производительность при многопоточном доступе.

  6. Операции записи (Locking on Writes)

    • При добавлении (put) или удалении (remove) блокируется только соответствующий сегмент.

    • Минимизируется конкуренция между потоками.

  7. Высокая масштабируемость (Scalability)

    • Разделение на сегменты снижает конфликты потоков.

    • Хорошо подходит для многопроцессорных систем, где потоки могут работать параллельно.

  8. Рехеширование (Rehashing)

    • Когда сегмент заполняется, происходит перераспределение (resize).

    • Блокируется только изменяемый сегмент, остальные остаются доступными.

Механизм работы операций

  • put()

    • Вычисляется хеш-значение ключа и определяется сегмент.

    • При необходимости сегмент блокируется, а затем значение добавляется в соответствующую корзину.

    • Если корзина переполнена, может произойти расширение или рехеширование.

  • get()

    • Вычисляется хеш ключа, определяется сегмент и корзина.

    • Чтение выполняется без блокировки, обеспечивая высокую скорость.

  • computeIfAbsent()

    • Если ключ отсутствует, вычисляется значение и добавляется в карту.

    • Гарантирует, что вычисление выполняется единожды даже при доступе из нескольких потоков.

21. You have a list of student names in a college. How can you convert this list into a set? What happens with the duplicate names?

(Как преобразовать список имен студентов в набор данных?)

Чтобы преобразовать список имен студентов в Set, можно передать список в конструктор реализации интерфейса Set, например, HashSet.

Пример кода:

import java.util.*;

public class ListToSetExample {
    public static void main(String[] args) {
        List<String> studentNames = Arrays.asList("Alice", "Bob", "Alice", "Charlie", "Bob");
        // Преобразование списка в множество
        Set<String> uniqueNames = new HashSet<>(studentNames);
        // Вывод множества
        System.out.println("Уникальные имена: " + uniqueNames);
    }
}

Что происходит с дубликатами?

При преобразовании списка в Set:

  • Дубликаты автоматически удаляются, так как Set не допускает повторяющихся элементов.

  • В множестве остаётся только один экземпляр каждого уникального имени.

Например, если список содержит:
["Alice", "Bob", "Alice", "Charlie", "Bob"],
то результирующее множество будет:
["Alice", "Bob", "Charlie"].

Резюме: Такое поведение полезно, когда нужно устранить дубликаты из набора данных.

22. Explain deep copy with examples

(Объясните, что такое глубокое копирование (Deep Copy) с примерами).

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

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

Пример

Рассмотрим класс Student, содержащий вложенный объект Address.

// Класс Address с методом глубокого копирования
class Address implements Cloneable {
    String city;
    String state;

    public Address(String city, String state) {
        this.city = city;
        this.state = state;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return new Address(this.city, this.state); // Создаём новый объект Address
    }
}

// Класс Student с методом глубокого копирования
class Student implements Cloneable {
    String name;
    Address address;

    public Student(String name, Address address) {
        this.name = name;
        this.address = address;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student clonedStudent = (Student) super.clone(); // Поверхностное копирование
        clonedStudent.address = (Address) address.clone(); // Глубокое копирование вложенного объекта
        return clonedStudent;
    }
}

public class DeepCopyExample {
    public static void main(String[] args) throws CloneNotSupportedException {
        // Исходный объект
        Address address = new Address("New York", "NY");
        Student originalStudent = new Student("John", address);

        // Создаём глубокую копию
        Student clonedStudent = (Student) originalStudent.clone();

        // Изменяем копию
        clonedStudent.name = "Alice";
        clonedStudent.address.city = "Los Angeles";

        // Выводим оба объекта
        System.out.println("Оригинальный студент: " + originalStudent.name + ", Адрес: " 
                + originalStudent.address.city + ", " + originalStudent.address.state);
        System.out.println("Клонированный студент: " + clonedStudent.name + ", Адрес: " 
                + clonedStudent.address.city + ", " + clonedStudent.address.state);
    }
}

Вывод:

Оригинальный студент: John, Адрес: New York, NY  
Клонированный студент: Alice, Адрес: Los Angeles, NY  

Резюме: Преимущества глубокого копирования:
- Полностью независимые объекты, изменения в одном не затрагивают другой.
- Идеально подходит для объектов с вложенными или изменяемыми (mutable) полями.

Заключение

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

Как поделилась кандидат, «Это было очень длинное и детальное собеседование, как и следовало ожидать от JP Morgan». В целом, она описала техинтервью исключительно как положительный опыт.

Немного HR-рекламы от нашего блога: мы занимаемся заказной разработкой ПО и будем рады резюме специалистов, готовых работать оффлайн в Москве и Томске, а также удаленно из любой точки России. Текущие вакансии на нашей странице на hh.ru. Если вашей специальности нет в списке вакансий, не стесняйтесь прислать нам резюме — в SSP SOFT новые позиции открываются регулярно. Резюме можно направить в Telegram или на почту job@ssp-soft.com.

Успехов на техническом собеседовании по вакансиям Java Developer / Java Lead!

Теги:
Хабы:
+37
Комментарии30

Публикации

Информация

Сайт
ssp-soft.com
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия
Представитель
Андрей