Java: Senior. Тестирование hh.ru
Тестирование Java-приложений — неотъемлемая часть процесса разработки, особенно если речь идет о сложных, многопоточных или масштабируемых системах. Эта статья раскрывает ключевые аспекты продвинутого тестирования, которые помогут вам вывести ваш процесс проверки на новый уровень.
Эта статья станет вашим путеводителем в мире продвинутого тестирования Java, объединяя теорию, практические примеры и лучшие инструменты.
Это наш финальный разбор тестовых материалов за 2024 год. Мы тщательно проработали все темы и уровни — от английского языка до Java, разобрав в общей сложности 59 тестов.
👉🏻Навигацию по материалам вы найдете в Telegram-канале.
Вопрос 1
Обоснование
При вызове метода foo(10)
, аргумент 10
имеет тип int (примитивный тип данных). В Java при перегрузке методов применяется следующее правило: компилятор выбирает наиболее подходящий метод из доступных на основе типа аргументов.
Анализ методов:
foo(Integer i)
— принимает типInteger
. Не будет вызван, так как нет автоматического преобразования изint
вInteger
в первую очередь. Автобоксинг происходит только если нет подходящего метода дляint
.foo(short i)
— принимает типshort
. Не подходит напрямую, так какint
нельзя сузить (widening conversions не сужают тип).foo(long i)
— принимает типlong
. Это расширение типа (widening), и метод подходит дляint
.foo(Object i)
— подходит только в случае автобоксинга доInteger
, но это менее приоритетно, чем расширение типа.foo(int... i)
— метод с varargs. Он рассматривается последним, если другие перегрузки не подходят.
Выбор метода
Поскольку foo(long i)
является наиболее подходящей перегрузкой для типа int
(расширение), именно этот метод будет вызван.
Вопрос 2
Что будет выведено при исполнении данного участка кода?
Обоснование:
Переменная field
в классе SomeClass
является static, что означает, что она общая для всех экземпляров класса.
Начальное значение переменной field = 0
.
Выполнение шагов кода:
- Создание
someClass1
и инкремент:SomeClass someClass1 = new SomeClass(); someClass1.field++;
Переменнаяfield
увеличивается на 1:field = 1
- Создание
someClass2
и инкремент:SomeClass someClass2 = new SomeClass(); someClass2.field++;
Переменнаяfield
увеличивается на 1:field = 2
- Создание
someClass3
и инкремент:SomeClass someClass3 = new SomeClass(); someClass3.field++;
Переменнаяfield
увеличивается на 1:field = 3
- Строка:
SomeClass.field++;
Переменнаяfield
увеличивается на 1:field = 4
- Строка:javaКопировать код
++SomeClass.field;
Переменнаяfield
увеличивается на 1 (префиксный инкремент):field = 5
- Вывод результата:
System.out.println(++SomeClass.field);
Префиксный инкремент снова увеличивает значениеfield
на 1 перед выводом:field = 6
📌Правильный ответ:
Вопрос 3
Что будет выведено при исполнении данного кода?
Обоснование:
1. Функция foo
Метод foo(char a)
принимает символ, выводит его с помощью System.out.print(a)
, а затем возвращает true
.
2. Цикл for
for(foo('A'); foo('B') && (i<2); foo('C')) { i++; foo('D'); }
3. Пошаговый вывод
foo('A')
→ A- Условие:
foo('B')
→ B и проверкаi < 2
(i = 0, условиеtrue
). - Тело цикла:
i++
(i = 1),foo('D')
→ D - Шаг:
foo('C')
→ C
- Условие:
foo('B')
→ B и проверкаi < 2
(i = 1, условиеtrue
). - Тело цикла:
i++
(i = 2),foo('D')
→ D - Шаг:
foo('C')
→ C
📌Правильный ответ:
Вопрос 4
Какой будет результат выполнения данного кода?
- false false true true
- true false true true
- true false true false
- false true false true
- true true true false
Обоснование:
Важное о Integer в Java:
- Класс
Integer
кеширует значения в диапазоне от -128 до 127 (это называется Integer Cache). - Если два объекта типа
Integer
содержат значения в этом диапазоне и созданы с помощью автоупаковки (autoboxing) илиInteger.valueOf()
, они будут ссылаться на один и тот же объект. - Для значений вне диапазона кеширования,
==
сравнивает ссылки на разные объекты.
Пошаговый анализ кода:
- Переменные
a
иb
:Integer a = 100; Integer b = 100;
- Значение
100
попадает в диапазон кеширования (-128
до127
). a
иb
будут ссылаться на один и тот же объект.a == b
→ true.- Переменные
c
иd
:Integer c = 200; Integer d = 200;
- Значение
200
не попадает в диапазон кеширования. c
иd
будут ссылаться на разные объекты.c == d
→ false.- Переменные
e
иa
:Integer e = Integer.valueOf(100);
Integer.valueOf(100)
использует кеширование.e
будет ссылаться на тот же объект, что иa
(значение100
в кеше).e == a
→ true.- Переменные
f
иc
:Integer f = Integer.valueOf(200);
Результаты сравнений:
📌Правильный ответ:
Вопрос 5
Какой из перечисленных типов НЕ является reifiable?
В Java reifiable типы – это типы, которые сохраняют всю информацию о своем типе во время выполнения. В отличие от них, non-reifiable типы теряют часть информации из-за type erasure (стирания типов).
- Примитивные типы:
int
,double
, и т.д. - Неформализованные типы:
String
,Integer
,ArrayList
- Массивы с известным типом элементов:
String[]
,Integer[]
- Wildcard типы с
?
(например,List<?>
) - Generic типы с ограничениями (
Comparable<? super String>
) - Массивы с wildcard (
List<?>[]
)
Анализ вариантов:
- String – это reifiable тип (не generic, не теряет информацию).
- List<?>[] – это non-reifiable тип, так как содержит wildcard
?
и массив с wildcard-типом. - ArrayList – это reifiable тип, т.к. это конкретный класс.
- Comparable<? super String> – это non-reifiable, так как содержит wildcard с ограничением.
- Integer – это reifiable тип.
📌Правильный ответ:
Вопрос 6
Каким способом в Java достигается полиморфизм?
Обоснование:
Полиморфизм в Java достигается в первую очередь через переопределение методов (method overriding). Полиморфизм позволяет объекту принимать множество форм и предоставляет возможность использовать единый интерфейс для взаимодействия с различными типами объектов.
- Переопределение методов – ключевой механизм полиморфизма в Java. Метод в подклассе переопределяет метод родительского класса, позволяя вызвать метод в зависимости от типа объекта во время выполнения (runtime).
- Композиция, Generics и инкапсуляция не относятся напрямую к реализации полиморфизма.
- "Слабые" ссылки (Weak References) также не имеют отношения к полиморфизму.
📌Правильный ответ:
Переопределение методов
Вопрос 7
Каков результат работы этого кода?
- Ключевой момент:
- В блоке
try
создается переменнаяsomeClass
, которая равна null:SomeClass someClass = null; return someClass.i;
- Поле
i
классаSomeClass
является static. - Доступ к статическим полям через null-ссылку допустим и не вызывает исключение
NullPointerException
. Поэтому код скомпилируется и выполнится корректно. - Значение возвращаемое из блока
try
: - Блок
finally
:
Вопрос 8
У вас есть 10 потоков, которые одновременно обращаются к одному и тому же ресурсу — очереди задач, реализованной с помощью BlockingQueue
. Вам необходимо реализовать механизм, который гарантирует, что не более 5 потоков одновременно могут обрабатывать задачи из очереди.
Какой из следующих способов НЕ подходит для решения задачи?
- Использовать
CountDownLatch
с начальным значением 5 и каждый раз перед получением задачи из очереди вызывать методawait()
уCountDownLatch
. Когда поток заканчивает обработку задачи, он вызываетcountDown()
. - Использовать
ReentrantLock
для блокировки доступа к очереди задач, а внутри блокировки проверять, не превышает ли количество обрабатывающих задач 5, и, если да, ждать освобождения блокировки. - Использовать
Semaphore
с начальным значением 5 и каждый раз перед получением задачи из очереди вызывать методacquire()
у семафора, а после обработки задачи —release()
. - Использовать
ExecutorService
с фиксированным размером пула потоков, равным 5, и запускать все потоки через этот пул. - Создать отдельный класс-посредник, который будет обрабатывать запросы на получение задач из очереди и запускать обработку задач только для 5 потоков одновременно.
CountDownLatch
:CountDownLatch
используется для синхронизации потоков, но он не предназначен для ограничения количества потоков, одновременно обрабатывающих задачи.CountDownLatch
нельзя "сбросить" или повторно использовать – его счетчик только уменьшается. Это делает его неподходящим для ситуации с циклическим ограничением 5 потоков.ReentrantLock
:- С
ReentrantLock
можно реализовать блокировку доступа и проверку количества обрабатывающих потоков. - Однако реализация потребует дополнительной логики, но технически это возможно.
Semaphore
:Semaphore
идеально подходит для ограничения количества потоков с помощью счетчика ресурсов.- Метод
acquire()
уменьшает счетчик, аrelease()
увеличивает его, что позволяет легко ограничить до 5 потоков. ExecutorService
:ExecutorService
с фиксированным размером пула потоков (newFixedThreadPool(5)
) гарантирует, что не более 5 потоков будут одновременно работать.- Это наиболее элегантное и простое решение.
- Создание класса-посредника:
📌Правильный ответ:
Использовать CountDownLatch
с начальным значением 5...
Этот способ НЕ подходит, так как CountDownLatch
не предназначен для повторного использования и ограничения количества одновременно активных потоков.
Вопрос 9
Какая структура данных из перечисленных НЕ является lock-free?
- ConcurrentLinkedQueue
- Реализует lock-free алгоритм на основе CAS (Compare-And-Swap) для многопоточной обработки.
- Это lock-free структура данных.
- ConcurrentSkipListMap
- НЕ является lock-free, так как использует блокировки для обеспечения безопасности доступа при обновлении данных.
- Она поддерживает более сложную структуру (skip list) и требует блокировок на уровнях узлов.
- AtomicInteger
- ConcurrentLinkedDeque
- ConcurrentHashMap
📌Правильный ответ:
ConcurrentSkipListMap
Вопрос 10
Что из перечисленных функциональных ограничений синхронизации в Java является ЛОЖЬЮ?
- Если поток, контролирующий блокирование, держит блокировку, то ни один поток, нуждающийся в этой блокировке, не продвинется в обработке.
- Volatile переменная гарантирует атомарность последовательности читать-модифицировать-читать.
- Можно прервать поток, который ожидает блокировки.
- Можно опрашивать или пытаться получить блокировку, не будучи готовым к долгому ожиданию.
- Блокировка не должна быть снята в том же стековом фрейме, в котором была начата.
- "Если поток, контролирующий блокирование, держит блокировку, то ни один поток, нуждающийся в этой блокировке, не продвинется в обработке"
- Это правда, так как поток, владеющий блокировкой, должен освободить её, прежде чем другие потоки смогут получить доступ.
- "Volatile переменная гарантирует атомарность последовательности читать-модифицировать-читать"
- Это ЛОЖЬ.
- Ключевое слово
volatile
гарантирует видимость изменений переменной между потоками, но не обеспечивает атомарности операций. Операция читать-модифицировать-читать (например,i++
) не атомарна и требует использования синхронизации или атомарных классов (например,AtomicInteger
). - "Можно прервать поток, который ожидает блокировки"
- Это правда, поток может быть прерван во время ожидания блокировки с использованием метода
interrupt()
. - "Можно опрашивать или пытаться получить блокировку, не будучи готовым к долгому ожиданию"
- Это правда. Например,
tryLock()
вReentrantLock
позволяет попытаться получить блокировку без ожидания. - "Блокировка не должна быть снята в том же стековом фрейме, в котором была начата"
📌Правильный ответ:
"Volatile переменная гарантирует атомарность последовательности читать-модифицировать-читать"
Вопрос 11
В компании N наняли junior разработчика. Первой его задачей было написать несложный асинхронный код, который последовательно вызывает сервисы с некоторыми условиями. После вызова сервиса 1 должен одновременно вызываться сервис 2 и сервис 3. Если на момент вызова сервиса 3 вызов сервиса 2 еще не завершен - задача на вызов сервиса 3 ожидает ответа от сервиса 2 и печатает лог об ожидании.
Junior разработчик написал следующий код и попросил сделать Вас ревью. Допустили бы Вы данный код в продакшн? (Обработкой ошибок можно пренебречь).
- Да. С кодом все хорошо.
- Данный код валиден, но для третьего и второго сервиса необязательно передавать результат вызова первого сервиса.
thenAccept
можно заменить наthenRun
. - Код невалиден, вместо использования
CompletableFuture
chain Паша должен просто сделатьFuture
для вызова первого сервиса и сделать на немjoin
. Затем линейно вызвать сначала сервис 2, а затем сервис 3. - Нет. Лучше использовать метод
thenCombine
на вызове третьего сервиса и передать в него вызов второго сервиса первым параметром. - Нет. Вместо
thenAccept
иthenRun
(в случае сервиса 3) надо использоватьthenAcceptAsync
иthenRunAsync
.
Анализ кода:
В коде используется CompletableFuture
для асинхронного вызова сервисов с условиями. Однако имеются несколько недочетов, которые могут повлиять на производительность и правильность работы.
thenAccept
и блокирующийjoin()
:- Использование
thenAccept
и вызоваjoin()
для ожидания завершения второго сервиса не является идеальным решением.join()
блокирует текущий поток, что противоречит принципу асинхронности. - Это приводит к блокировке и потенциальному снижению производительности.
- Проблема многопоточной безопасности с
AtomicBoolean
: - Использование
AtomicBoolean
для проверки и изменения флага в разных асинхронных ветках может привести к некорректному результату при высоких нагрузках или конкурентном доступе. - Отсутствие thenCombine:
- Метод
thenCombine
идеально подходит для объединения результатов двух асинхронных задач. В текущем коде этого подхода не используется. - Использование thenAccept вместо thenAcceptAsync:
Наиболее корректный вариант:
"Нет. Лучше использовать метод thenCombine на вызове третьего сервиса и передать в него вызов второго сервиса первым параметром."
thenCombine
позволяет запустить два асинхронных вызова параллельно и комбинировать их результаты, что избавляет от необходимости вручную блокировать поток.- Код станет более чистым, асинхронным и производительным.
📌Правильный ответ:
"Нет. Лучше использовать метод thenCombine на вызове третьего сервиса и передать в него вызов второго сервиса первым параметром."
Вопрос 12
Какие структуры данных требуют, чтобы данные реализовывали интерфейс Comparable?
- TreeMap
- TreeMap требует, чтобы ключи реализовывали интерфейс Comparable или был передан компаратор (Comparator) для определения порядка элементов.
- Без реализации
Comparable
или компаратора добавление элементов приведет к исключениюClassCastException
. - ConcurrentHashSet
- ConcurrentHashSet (не входит в стандартный JDK, но его часто используют как потокобезопасный Set на основе
ConcurrentHashMap
) не требует, чтобы элементы реализовывали Comparable. Порядок элементов не важен. - HashMap
- HashMap не требует реализации Comparable, так как элементы хранятся в неупорядоченном виде и сравнение осуществляется через
equals()
иhashCode()
. - ArrayList
- ArrayList не требует реализации Comparable. Элементы добавляются в порядке вставки, а сортировка выполняется вручную (например, через
Collections.sort
), гдеComparable
илиComparator
может быть использован. - LinkedDeque
Вопрос 13
После продолжительной работы сервиса графики показали, что память постепенно растет. Вы попросили вашего напарника проинспектировать код на наличие утечки памяти. Через пару дней он пришел с результатом, что утечкой памяти является циклическая ссылка в коде класса реализации графа.
Выглядит это следующим образом. Может ли это быть причиной утечки памяти? (в качестве GC используется ParallelGC).
- Да. Из-за цикличности ссылок объекты не могут быть перемещены из молодого поколения в старое.
- Нет. Благодаря trace-логике GC циклические объекты легко очищаются, если до них невозможно дойти по ссылкам от root объектов.
- Нет. Данные объекты не аллоцируются на куче при такой логике и тем самым не вызывают утечку.
- Да. Так как у объектов с циклическими ссылками надо вызывать метод
.finalize()
чтобы он очистился. Иначе GC не может их удалить. - Да. ParallelGC не умеет работать с циклическими ссылками, поэтому каждый вызов метода
createAndDelete()
увеличивает количество неуничтожаемых объектов.
- Работа GC в Java:
- Сборщик мусора в Java основан на trace-логике, которая определяет достижимость объектов от корневых объектов (GC Roots).
- Если объект (или группа объектов с циклическими ссылками) не достижим из GC Roots, то GC пометит их как мусор и удалит.
- Циклические ссылки:
- Циклические ссылки не мешают сборке мусора, если на объекты нет внешних активных ссылок.
- В Java GC умеет определять, что группа объектов недостижима, даже если они ссылаются друг на друга.
- ParallelGC:
- ParallelGC, используемый по умолчанию в Java 8, способен корректно работать с циклическими ссылками и очищать такие объекты.
- Ошибочные утверждения:
Современные сборщики мусора в JVM, включая ParallelGC
, используют алгоритмы на основе графа объектов (например, Mark-and-Sweep). Они определяют, какие объекты достижимы от корневых объектов (root objects). Циклические ссылки не являются проблемой, если на эти объекты нет внешних ссылок, делающих их достижимыми. Таким образом, такие объекты автоматически удаляются, и это не приводит к утечкам памяти.
2. Нет. Благодаря trace-логике GC циклические объекты легко очищаются, если до них невозможно дойти по ссылкам от root объектов.
Разбор практического задания продвинутого уровня в следующей статье