Всем привет! Хотелось бы поделиться своим недавним опытом прохождения собеседования в одной из финтех-компаний. Там очень сложный процесс, рассчитанный на разработчиков Java с опытом от 3 до 8 лет. Надеюсь, эта расшифровка вам пригодится. Переходим к самому собеседованию.
Вопрос на основе сценария
Можно ли добавить в TreeSet три объекта одного класса Employee: e1, e2, e3 и два объекта класса Department: d1, d2? Что произойдет, если попробовать распечатать эти объекты с помощью System.out.println?
Вот программа:
package com.test.springdataflowtest;
import java.util.*;
public class TreeSetExample {
public static void main(String[] args) {
TreeSet<Object> treeSet = new TreeSet<>();
Employee e1 = new Employee(101, "John");
Employee e2 = new Employee(102, "Alice");
Employee e3 = new Employee(103, "Bob");
Department d1 = new Department(201, "HR");
Department d2 = new Department(202, "IT");
treeSet.add(e1);
treeSet.add(e2);
treeSet.add(e3);
treeSet.add(d1);
treeSet.add(d2);
System.out.println("Elements in TreeSet:");
for (Object obj : treeSet) {
System.out.println(obj);
}
}
}
class Employee implements Comparable<Employee> {
private int id;
private String name;
public Employee(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public int compareTo(Employee other) {
return Integer.compare(this.id, other.id);
}
@Override
public String toString() {
return "Employee{id=" + id + ", name='" + name + "'}";
}
}
class Department {
private int deptId;
private String deptName;
public Department(int deptId, String deptName) {
this.deptId = deptId;
this.deptName = deptName;
}
@Override
public String toString() {
return "Department{deptId=" + deptId + ", deptName='" + deptName + "'}";
}
}
Будет выброшено исключение:
Exception in thread “main” java.lang.ClassCastException: class Java8Features.Department cannot be cast to class java.lang.Comparable (Java8Features.Department is in unnamed module of loader ‘app’; java.lang.Comparable is in module java.base of loader ‘bootstrap’)
at java.base/java.util.TreeMap.put(TreeMap.java:811)
at java.base/java.util.TreeMap.put(TreeMap.java:534)
at java.base/java.util.TreeSet.add(TreeSet.java:255)
at Java8Features.OptionalClassDemoTest.main(OptionalClassDemoTest.java:20)
Программа в таком виде не выполнится, основная причина связана со смешиванием объектов Employee и Department в одном и том же TreeSet. Объекты Employee сопоставимы — так с английского переводится реализуемый ими интерфейс Comparable, — поэтому они без проблем добавляются в TreeSet, где его элементы сохраняются в отсортированном порядке на основе логики сравнения, указанной в методе compareTo класса Employee.
Однако объектами Department интерфейс Comparable не реализуется, следовательно, в TreeSet для них не определяется естественный порядок. При добавлении объекта Department в TreeSet, где уже содержатся объекты Employee, или наоборот, для сохранения порядка эти объекты в TreeSet сравниваются.
В Department нет метода compareTo или другого средства сравнения вроде Comparator, предоставляемого в TreeSet при создании, поэтому эта операция не выполнится, и появится ClassCastException, то есть объекты нельзя сопоставить для упорядочения.
Эта проблема устраняется по-разному.
Department делается Comparable, для этого в классе Department реализуется также интерфейс Comparable с логикой сравнения, аналогичной той, что имеется в классе Employee.
Применяется Comparator, для этого создается TreeSet с пользовательским Comparator, который «умеет» сравнивать как объекты Employee, так и Department. Таким подходом обеспечивается больше гибкости, особенно когда сами классы неизменяемы или взяты из внешних библиотек.
Экземпляры TreeSet отделяются, для этого поддерживаются отдельные коллекции для объектов Employee и Department, если нет логического способа сравнить их или если их не нужно отсортировывать в общей коллекции.
Когда в Java используются Comparable и Comparator?
Интерфейсами Comparable и Comparator в Java сортируются элементы в коллекциях вроде TreeSet, TreeMap, Collections.sort() и других. Но применяются они в разных целях и сценариях.
Интерфейс Comparable
Comparable применяется для определения естественного порядка сортировки объектов класса. То есть самим классом определяется, как сравнивать его экземпляры.
Единый порядок сортировки, то есть у Comparable имеется только один способ сравнения объектов. Логика сортировки включается в сам класс с помощью метода compareTo().
Внутренняя реализация: Comparable реализуется в классе, экземпляры которого нужно отсортировать. Этим обеспечивается согласованное поведение при сортировке во всех коллекциях, где применяется естественный порядок.
Пример: сортировка строк, целых чисел или пользовательских классов наподобие Employee по их идентификатору, названию или любому другому присущему им свойству.
Интерфейс Comparator
Пользовательский порядок сортировки: Comparator применяется при определении нескольких порядков сортировки для объектов класса или при отсутствии доступа к исходному коду класса для реализации Comparable.
С Comparator определяется внешняя по отношению к сортируемому классу логика сортировки, применяется для сортировки объектов по различным критериям без изменения их исходного класса.
Несколько порядков сортировки: когда требуется неодинаковая сортировка объектов без изменения самого класса, определяется несколько реализаций Comparator.
Пример: сортировка объектов Employee по зарплате, возрасту или любым другим критериям, которые варьируются в зависимости от контекста.
Когда и что использовать?
Comparable при естественном порядке сортировки, присущем самому классу, когда все экземпляры класса сортируются одинаково.
Comparator, когда нужна пользовательская логика сортировки, или когда объекты сортируются несколькими способами без изменения их исходного класса, или когда имеем дело с неизменяемыми классами из внешних библиотек.
Как проверить временную сложность программы при просмотре кода?
Под оценкой временной сложности программы при просмотре кода подразумевается понимание используемых алгоритмов, структур данных и того, как они масштабируются в зависимости от размера входных данных.
Напрямую измерить временную сложность нельзя: запустив код, получим время выполнения, которое зависит от аппаратного обеспечения, входных данных и т. д. Зато можно проанализировать код и определить его теоретическую временную сложность.
Вот как это делается.
1. Выявляются базовые операции. Определяем, что такое базовая операция в алгоритме — она максимально сказывается на производительности: сравнение в алгоритме сортировки, сложение в числовых расчетах или операция выборки данных.
2. Анализируется сложность цикла. Учитываем, сколько итераций выполняется циклом с точки зрения размера входных данных N, например у цикла, запускаемого от 1 до N, сложность O(N).
Для вложенных циклов перемножаем сложности внутреннего и внешнего циклов. У цикла сложности N внутри цикла сложности N — сложность O(N²).
Циклы while: определяем, какая часть входных данных обрабатывается условиями цикла и сколько итераций выполнится в наихудшем случае.
3. Учитывается рекурсия. Для рекурсивных функций определяем рекуррентное соотношение. Сложность выявляем по количеству рекурсивных вызовов и по тому, как с каждым вызовом уменьшается размер задачи. Затем эти соотношения решаются инструментами вроде основной теоремы.
4. Учитываются структуры данных. Со сложностью операций над структурами данных, таких как добавление или удаление элементов из массивов, деревьев или связанных списков, начинаются нюансы.
Например: сложность индексирования массива O(1), но у поиска она O(N) или O(log N), если массив отсортирован и применяется двоичный поиск. Такие операции с деревом, как вставка и поиск, зависят от типа дерева, например двоичное дерево поиска, сбалансированное дерево вроде АВЛ или красно-черное дерево.
5. Учитываются библиотечные функции и API-вызовы. Определяем сложность любой библиотечной функции или внешнего API-вызова, которые применяются в коде. Например, сложность функций сортировки Arrays.sort и Collections.sort обычно O(N log N).
6. Худший, лучший и средний случаи. Сложность может варьироваться:
- Сложность в худшем случае Big O, «O» большое — то, что обычно и оценивается, это верхняя граница времени.
- Сложность в лучшем случае Big Omega Ω, «Ω» большое относится к алгоритмам, поведение которых существенно отличается при наилучших входных данных.
- Сложности в среднем случае Big Theta Θ, «Θ» большое требуется вероятностный анализ входных данных.
7. Пространственная сложность. Хотя акцент делается на временной сложности, пространственная тоже учитывается — сколько дополнительной памяти требуется программе при масштабировании. В некоторых алгоритмах обеспечивается экономия времени за счет памяти, например при кэшировании или динамическом программировании.
При просмотре кода
Требуются обоснования сложности: в идеале разработчик документирует ожидаемые временную и пространственную сложности функций или алгоритмов или оставляет комментарии о них.
Сложность проверяется на соответствие требованиям производительности или ограничениям проекта.
Возможности оптимизации: выявляются любые очевидные недостатки вроде лишних вложенных циклов, повторных вычислений или неоптимального использования структур данных.
Как определить, включены ли в текущий код агрегация, композиция и наследование, и каково практическое назначение этих концепций?
Чтобы ответить на этот вопрос, изучаются отношения между объектами и классами в кодовой базе. Подход здесь такой:
1. Выявляется наследование. Наследованием называется отношение is-a («это») между базовым-родительским и производным-дочерним классами, последним наследуются, а также добавляются или переопределяются свойства и поведения-методы родительского класса.
Как проверить: разыскиваются определения классов и выясняется, расширяется ли каким-либо классом другой класс.
Ключевые слова поиска: в Java и подобных языках extends — для наследования классов, implements — для реализации интерфейсов.
Практическое применение: наследование задействуется для переиспользования кода и полиморфизма. Производными классами повторно используется код из базовых классов, отчего сокращается избыточность кода. Например, класс Vehicle — базовый, а Car и Motorcycle — производные классы, которыми наследуются такие свойства, как speed, и такие методы, как move().
2. Выявляется агрегация. Под агрегацией подразумевается отношение has-a («имеет»), при котором в одном классе содержатся ссылки на другие, но их жизненные циклы не контролируются им монопольно.
Как проверить: разыскиваются классы, в которых содержатся ссылки на экземпляры других классов как переменных-членов, но экземпляры классов в своих конструкторах ими не создаются.
Имеющиеся объекты передаются в конструктор объекта агрегации или задаются посредством сеттеров.
Практическое применение: агрегация полезна для представления отношений, в которых компоненты принадлежат нескольким контейнерам. Например, классами Professor и Student агрегируется класс Department, чем обозначается, что оба связаны с одним и тем же факультетом.
3. Определяется композиция. Композиция — более сильная форма агрегации с владением, под которой подразумевается отношение part-of («целое-часть»). Жизненный цикл имеющихся объектов зависит от жизненного цикла объекта контейнера.
Как проверить: в классах, которыми в собственных конструкторах или методах создаются экземпляры других классов и контролируется их время жизни, обнаруживается композиция.
Если класс контейнера уничтожен, уничтожаются и имеющиеся объекты.
Практическое применение: композицией моделируются отношения, в которых компонентов без контейнера быть не должно. Например, класс Page — компонент класса Book; если книга уничтожена, уничтожаются и ее страницы.
Что такое «утечка памяти» в Java?
Утечка памяти в Java — это когда объекты больше не нужны приложению, но еще занимают память, так как сборщик мусора не может удалить их из рабочей памяти. Такая ситуация обычно случается, когда ссылки на эти объекты остаются в коде, мешая сборщику мусора освободить занимаемую ими память, хотя для дальнейших операций приложения они фактически бесполезны.
Что такое «подстрока», как она используется, какие проблемы с ней связаны?
Подстрокой в программировании называется меньшая часть или сегмент строки. Во многих языках, в том числе Java, имеются методы для извлечения подстроки из заданной строки. В этих методах обычно указывается начальная позиция подстроки и дополнительно — конечная. В результате получается новая строка с символами из указанного диапазона:
public class SubstringExample {
public static void main(String[] args) {
String originalString = "Hello, World!";
// Извлечение подстроки от индекса 7 до конца
String substring1 = originalString.substring(7);
System.out.println("Substring 1: " + substring1); // Вывод: «World!»
// Извлечение подстроки исключительно от индекса 0 до 5
String substring2 = originalString.substring(0, 5);
System.out.println("Substring 2: " + substring2); // Вывод: «Hello»
// Извлечение подстроки исключительно от индекса 2 до 6
String substring3 = originalString.substring(2, 6);
System.out.println("Substring 3: " + substring3); // Вывод: «llo»
}
}
Использование подстрок. Синтаксический анализ и обработка текста: подстроки применяются для парсинга и обработки частей строки покрупнее, например для извлечения доменного имени из URL-адреса.
Проверка допустимости данных: подстроками проверяются конкретные части пользовательского ввода или полей данных, подтверждается их допустимость, например проверка префикса или кода зоны в телефонном номере.
Операции над строками: подстроковые методы необходимы для различных задач обработки строк, например удаление префиксов или суффиксов, реализация алгоритмов, которым требуется проверка или изменение частей строки.
Форматирование: подстроками форматируются выходные данные. Например, чтобы не загромождать пользовательский интерфейс, отображаются только первые символы длинной строки.
Проблемы подстроки. Утечки памяти — это давняя проблема Java, в версиях до Java 7, обновления 6 метод substring был чреват утечками памяти. Когда создавалась подстрока, в ней содержался тот же базовый массив символов, что и в исходной строке, отличались только начальный и конечный индексы. Если исходная строка большая, а подстрока маленькая, ссылка на подстроку сохранялась. Так предотвращалось автоматическое удаление сборщиком мусора большого массива символов, даже если ссылка на исходную строку отбрасывалась. В последующих версиях Java это поведение изменили: методом substring теперь копируется соответствующая часть массива, чем предотвращается этот конкретный вид утечки памяти.
Индекс за пределами допустимого диапазона: если начальный или конечный индекс некорректный, например отрицательный, находится дальше длины строки или начальный индекс больше конечного, тогда выбрасывается IndexOutOfBoundsException. Чтобы избежать этого, требуются правильная обработка ошибок и валидация индексов.
Производительность: будучи в целом эффективными, операции над подстроками чреваты проблемами производительности в конкретных контекстах, особенно при многократном использовании в цикле с очень большими строками или в приложениях, где важна производительность. Когда создается много объектов substring, рабочая нагрузка на сборщик мусора увеличивается.
Неизменяемость строк: в Java строки неизменяемы. При каждой выполняемой с подстроками операции создается новый объект string, что чревато увеличением расхода памяти в приложениях, занятых обработкой обширных строк. В StringBuilder или StringBuffer имеются более эффективные альтернативы для частых изменений.
Что такое OutOfMemoryError и как ее устранить?
Симптомы
Внезапный сбой или значительное замедление приложения:
- Обнаружена ошибка в журнале и консоли
-java.lang.OutOfMemoryError: Java heap space
. - Интенсивное использование процессора из-за повышенной активности сборщика мусора Java.
Возможные первопричины:
- Неадекватные параметры пространства в «куче», например xms, xmx.
- Утечки памяти в программах Java.
- Чрезмерный объем памяти, занимаемой приложением, и избыточная рабочая нагрузка при обработке.
Диагностические инструменты:
- Jvm verbose:gc logs;
- Java visual vm, oracle mision control;
- Apm technologies.
Решение:
- увеличиваем максимальную емкость «кучи» через параметр xmx;
- отслеживаем профиль и устраняем утечки памяти в приложениях;
- оптимизируем и сокращаем объем памяти, занимаемой приложением Java;
- разделяем рабочую нагрузку приложения Java на несколько процессов;
- чтобы сгенерировать дамп «кучи», запрашиваем hotspot jvm;
- заглядываем в журнал сборщика мусора.
Как создать неизменяемый класс, такой как String?
Не указываем методы-модификаторы, которыми изменяется состояние объекта. Убеждаемся, что класс не расширяется. Этим не допускается компрометирование неизменяемого поведения класса неаккуратными или вредоносными подклассами, которые ведут себя так, будто состояние объекта изменилось. Создание подклассов обычно предотвращается превращением класса в терминальный или предоставлением закрытого конструктора.
Делаем все поля final. Таким образом четко выражается намерение обеспечить применение этого в системе. Необходимо также обеспечить корректное поведение, если ссылка на вновь созданный экземпляр передается из одного потока в другой без синхронизации, как указано в модели памяти.
Делаем все поля private. Этим предотвращаются получение клиентами доступа к изменяемым объектам, на которые ссылаются поля, и изменение этих объектов напрямую. Наличие в неизменяемых классах общедоступных полей final с примитивными значениями или ссылками на неизменяемые объекты теоретически возможно, но не рекомендуется из-за невозможности в этом случае изменения внутреннего представления в более поздней версии.
Обеспечиваем эксклюзивный доступ к любым изменяемым компонентам. Если в классе имеются какие-либо поля, которые ссылаются на изменяемые объекты, предотвращаем получение клиентами класса ссылок на эти объекты. Никогда не инициализируем такое поле ссылкой на объект клиента и не возвращаем поле от средства доступа. Создаем защитные копии (пункт 50) в конструкторах, средствах доступа и методах readObject.
Читайте также:
- Map-ориентированное программирование в Java
- Расшифровка собеседования разработчика в Cisco 2024: Java, Spring Boot, Hibernate
- Реализация конвейера CI/CD «от и до»
Читайте нас в Telegram, VK и Дзен
Перевод статьи Ajay Rathod: Fiserv Java Developer Interview Question for Experienced Professionals 2024