Сложные вопросы на собеседовании для тех, кто 7 лет работал с Java. Часть 1

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.

Во второй расширим знания и навыки, попробуем повысить свои шансы на успех.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи AlishaS: Tricky Java interview questions for 7 years of Experience

Предыдущая статьяРеализация параллакс-карусели из SwiftUI в Jetpack Compose
Следующая статьяНе бойтесь генераторов JavaScript