Java — популярный язык программирования, активно применяемый для разработки настольных, мобильных, веб-приложений и корпоративного ПО. Семи лет в Java достаточно, чтобы разбираться в синтаксисе языка, структурах данных, концепциях программирования.
Но на собеседованиях даже опытные разработчики не справляются с каверзными вопросами, которыми проверяются их знания и навыки решения задач. Разберем некоторые их этих вопросов с подробными объяснениями и примерами.
1. Для чего и как в Java используется ключевое слово «transient
»?
При сериализации объекта его состояние преобразуется в последовательность байтов, записываемой в файл или отправляемой по сети.
Ключевым словом transient в Java указывается, что значение конкретного поля класса при этом не должно включаться в сериализованную форму объекта.
Примеры использования ключевого слова transient: когда имеется поле с временным значением, которое не нужно сохранять при сериализации объекта; или поле с конфиденциальными данными, не включаемое в сериализованную форму объекта по соображениям безопасности.
public class MyClass implements Serializable {
private int myInt;
private transient String myTransientString;
// Конструктор, геттеры, сеттеры ради простоты игнорируем
// Другие методы…
}
В этом примере поле myTransientString помечено как transient
: его значение не будет включено при сериализации экземпляра MyClass.
2. Чем наследование отличается от композиции? Приведите пример.
Наследование и композиция — два фундаментальных способа создания связей между классами в ООП. В обоих подходах возможны переиспользование кода и абстракция, но с разной реализацией и типами таких связей.
Вот краткое описание каждого подхода.
- Наследование. Это механизм, при котором новый класс, называемый подклассом или производным классом, создается наследованием свойств и характеристик — методов и полей — имеющегося класса, называемого суперклассом или базовым классом. Кроме того, методы суперкласса переопределяются подклассом в его собственной реализации. Наследованием между суперклассом и подклассом создаются отношения is-a («это»).
- Композиция. Это механизм, при котором в полях одного класса, называемого контейнером или целым классом, содержится минимум один экземпляр другого, называемого компонентным или составным классом. Композицией между классом-контейнером и компонентным классом создаются отношения has-a («имеет»).
На рисунке показано два класса: Vehicle («Автомобиль») и Engine («Двигатель»). Engine включается в Vehicle наследованием либо композицией.
- Пример наследования. Класс Engine расширяется классом Vehicle, которым наследуются все его поля и методы. Между классами Vehicle и Engine создаются отношения is-a («это»), где Vehicle is a («это») тип Engine.
public class Vehicle extends Engine {
// Специфичные для класса «Vehicle» поля и методы
}
- Пример композиции. В поле класса Vehicle содержится экземпляр класса Engine. Между классами Vehicle и Engine создаются отношения has-a («имеет»), то есть в Vehicle имеется Engine.
public class Vehicle {
private Engine engine;
public Vehicle(Engine engine) {
this.engine = engine;
}
// Методы, которыми применяется экземпляр «Engine»
}
В целом наследование применяется при наличии между классами четких отношений is-a («это»), когда подкласс является специализированной версией суперкласса.
Композиция применяется при наличии между классами отношений has-a («имеет»), когда классом-контейнером используется или контролируется минимум один экземпляр другого класса.
3. В чем разница между «HashSet» и «TreeSet» в Java? Как данные хранятся внутри?
Допустим, имеются такие целочисленные данные: {7, 3, 9, 4, 1, 8}.
- У HashSet данные хранятся в хеш-таблице. В ней методом hashCode() для каждого элемента определяется уникальный индекс, под которым этот элемент сохраняется:
На примере выше в хеш-таблице содержится восемь наборов элементов, помеченных индексами с 51 по 56, а в каждом наборе — элементы с хеш-кодами, сопоставляемыми с этим набором. Так, в наборе с индексом 53 содержатся элементы 3 и 4 с хеш-кодом [197]. В наборе с индексом 56 содержатся элементы 7, 8 и 9 с хеш-кодом [195].
- У TreeSet данные хранятся в красно-черном дереве, сортируемом согласно естественному упорядочению элементов или упорядочению, определяемому пользовательским компаратором, передаваемым конструктору TreeSet.
Вот пример хранения данных в красно-черном дереве:
Здесь у дерева шесть узлов с одним из элементов {1, 3, 4, 7, 8, 9}. Красными узлами указывается на нарушение свойств красно-черного дерева.
Элементы внутри дерева хранятся в отсортированном порядке: меньшие слева, бо́льшие справа. Например, наименьший элемент 1 хранится в самом левом конечном узле, а наибольший 9 — в самом правом.
4. Как в Java поступают с одновременными изменениями коллекции?
Одновременные изменения коллекции в Java чреваты неожиданным поведением, недетерминированными результатами или даже выбрасыванием ConcurrentModificationException
.
Во избежание этого в Java применяются:
- Синхронизированные коллекции. Это потокобезопасные коллекции, которыми обеспечивается единовременное изменение коллекции только одним потоком. Создается такая коллекция вызовом метода Collections.synchronizedCollection() с передачей синхронизируемой коллекции. Например:
List<String> list = new ArrayList<>();
List<String> synchronizedList = Collections.synchronizedList(list);
- Многопоточные коллекции. Это потокобезопасные коллекции с возможностью параллельного изменения коллекции несколькими потоками без внешней синхронизации. В пакете java.util.concurrent имеются такие классы многопоточных коллекций, как ConcurrentHashMap, ConcurrentLinkedDeque и ConcurrentSkipListSet.
- Явная блокировка. Изменямая коллекция блокируется с помощью ключевого слова
synchronized
или пакета java.util.concurrent.locks, например:
List<String> list = new ArrayList<>();
synchronized(list) {
list.add(“foo”);
}
- Правильные итераторы. При прохождении коллекции, чтобы избежать одновременные изменения, применяют интерфейс итератора. Если изменить коллекцию при прохождении ее с итератором, получим
ConcurrentModificationException
. При прохождении коллекции элементы удаляются из нее с помощью методаremove()
. Например:
List<String> list = new ArrayList<>();
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
if (someCondition) {
iterator.remove(); // Безопасный способ удалить элемент из списка
}
}
5. Как в Java реализуется взаимоблокировка?
Взаимоблокировка случается, когда минимум два потока блокируются в ожидании освобождения блокировки или удерживаемого ими ресурса. На Java взаимоблокировка реализуется созданием сценария с блокировкой как минимум двух потоков, которые находятся в ожидании друг друга и не способны продолжить работу.
Вот пример взаимоблокировки на Java:
public class Main {
// Блокировка объекта, требуемого потоку для выполнения.
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
// Создание одного потока и его реализованного анонимного метода.
Thread thread1 = new Thread(() -> {
// Синхронизированный блок, которым захватывается блокировка объекта
synchronized (lock1) {
System.out.println(“Thread 1 acquired lock 1”);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Захват синхронизированного блока, которым захватывается блокировка другого
// объекта для выполнения.
synchronized (lock2) {
System.out.println(“Thread 1 acquired lock 2”);
}
}
});
// Создание другого потока и его реализованного анонимного метода.
Thread thread2 = new Thread(() -> {
// Синхронизированный блок, которым захватывается блокировка объекта
synchronized (lock2) {
System.out.println(“Thread 2 acquired lock 2”);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Захват синхронизированного блока, которым захватывается блокировка другого
// объекта для выполнения.
synchronized (lock1) {
System.out.println(“Thread 2 acquired lock 1”);
}
}
});
// Запуск обоих потоков.
thread1.start();
thread2.start();
}
}
В этом примере двумя потоками, thread1
и thread2
, пытаются захватить две блокировки: lock1 и lock2.
- Сначала потоком
Thread1
захватывается lock1, после 100 миллисекунд ожидания предпринимается попытка захвата lock2. - Одновременно потоком
thread2
захватывается lock2, после 100 миллисекунд ожидания предпринимается попытка захвата lock1.
Оба потока пребывают в ожидании освобождения друг другом удерживаемых блокировок, поэтому создается ситуация взаимоблокировки и программа, не в состоянии продолжить работу, зависает.
Заключение
Это была первая часть каверзных вопросов на собеседованиях для имеющих семь лет опыта в Java.
Во второй расширим знания и навыки, попробуем повысить свои шансы на успех.
Читайте также:
- Глубокое погружение в Java: рефлексия и загрузчик классов. Часть 3
- Java 21: новый подход к созданию строк
- Заменят ли потоки данных циклы в Java?
Читайте нас в Telegram, VK и Дзен
Перевод статьи AlishaS: Tricky Java interview questions for 7 years of Experience