Сложные вопросы на собеседовании для тех, кто 7 лет работал с Java. Часть 1
https://t.me/javatgJava — популярный язык программирования, активно применяемый для разработки настольных, мобильных, веб-приложений и корпоративного ПО. Семи лет в 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.
Во второй расширим знания и навыки, попробуем повысить свои шансы на успех.