December 25, 2024

Java: Senior. Тестирование hh.ru

Тестирование Java-приложений — неотъемлемая часть процесса разработки, особенно если речь идет о сложных, многопоточных или масштабируемых системах. Эта статья раскрывает ключевые аспекты продвинутого тестирования, которые помогут вам вывести ваш процесс проверки на новый уровень.

Эта статья станет вашим путеводителем в мире продвинутого тестирования Java, объединяя теорию, практические примеры и лучшие инструменты.

Это наш финальный разбор тестовых материалов за 2024 год. Мы тщательно проработали все темы и уровни — от английского языка до Java, разобрав в общей сложности 59 тестов.

👉🏻Навигацию по материалам вы найдете в Telegram-канале.

Вопрос 1

Что выведет программа?

Варианты ответа

  1. foo(int...)
  2. foo(Integer)
  3. foo(object)
  4. foo(long)
  5. foo(short)

Обоснование
При вызове метода foo(10), аргумент 10 имеет тип int (примитивный тип данных). В Java при перегрузке методов применяется следующее правило: компилятор выбирает наиболее подходящий метод из доступных на основе типа аргументов.

Анализ методов:

  1. foo(Integer i) — принимает тип Integer. Не будет вызван, так как нет автоматического преобразования из int в Integer в первую очередь. Автобоксинг происходит только если нет подходящего метода для int.
  2. foo(short i) — принимает тип short. Не подходит напрямую, так как int нельзя сузить (widening conversions не сужают тип).
  3. foo(long i) — принимает тип long. Это расширение типа (widening), и метод подходит для int.
  4. foo(Object i) — подходит только в случае автобоксинга до Integer, но это менее приоритетно, чем расширение типа.
  5. foo(int... i) — метод с varargs. Он рассматривается последним, если другие перегрузки не подходят.

Выбор метода
Поскольку foo(long i) является наиболее подходящей перегрузкой для типа int (расширение), именно этот метод будет вызван.

📌Правильный ответ
foo(long)


Вопрос 2

Что будет выведено при исполнении данного участка кода?

Варианты ответа:

  1. 2
  2. 6
  3. 0
  4. 3
  5. 5

Обоснование:

Переменная field в классе SomeClass является static, что означает, что она общая для всех экземпляров класса.

Начальное значение переменной field = 0.

Выполнение шагов кода:

  1. Создание someClass1 и инкремент: SomeClass someClass1 = new SomeClass(); someClass1.field++; Переменная field увеличивается на 1:
    field = 1
  2. Создание someClass2 и инкремент: SomeClass someClass2 = new SomeClass(); someClass2.field++; Переменная field увеличивается на 1:
    field = 2
  3. Создание someClass3 и инкремент: SomeClass someClass3 = new SomeClass(); someClass3.field++; Переменная field увеличивается на 1:
    field = 3
  4. Строка: SomeClass.field++; Переменная field увеличивается на 1:
    field = 4
  5. Строка:javaКопировать код++SomeClass.field; Переменная field увеличивается на 1 (префиксный инкремент):
    field = 5
  6. Вывод результата: System.out.println(++SomeClass.field); Префиксный инкремент снова увеличивает значение field на 1 перед выводом:
    field = 6

📌Правильный ответ:

6


Вопрос 3

Что будет выведено при исполнении данного кода?

Варианты ответа:

  1. ABCDBABC
  2. ABDCBDCB
  3. BBCDDDBA
  4. ADBCDC
  5. ABCD

Обоснование:

1. Функция foo

Метод foo(char a) принимает символ, выводит его с помощью System.out.print(a), а затем возвращает true.

2. Цикл for

Структура цикла:

for(foo('A'); foo('B') && (i<2); foo('C')) {
    i++;
    foo('D');
}

Разберем по шагам:

  1. Инициализация:
    • foo('A') выполняется один раз перед началом цикла.
    • Печатается: A.
  2. Условие:
    • foo('B') выполняется каждый раз перед проверкой условия.
    • Затем проверяется i < 2.
  3. Тело цикла:
    • i++ увеличивает значение i на 1.
    • foo('D') выполняется в теле цикла.
  4. Шаг цикла:
    • foo('C') выполняется после каждой итерации тела цикла, перед повторной проверкой условия.

3. Пошаговый вывод

Итерация 1:

  1. foo('A')A
  2. Условие: foo('B')B и проверка i < 2 (i = 0, условие true).
  3. Тело цикла: i++ (i = 1), foo('D')D
  4. Шаг: foo('C')C

Итерация 2:

  1. Условие: foo('B')B и проверка i < 2 (i = 1, условие true).
  2. Тело цикла: i++ (i = 2), foo('D')D
  3. Шаг: foo('C')C

Итерация 3:

  1. Условие: foo('B')B и проверка i < 2 (i = 2, условие false). Цикл завершается.

4. Итоговый вывод:

"ABCDBABC"

📌Правильный ответ:

ABCDBABC


Вопрос 4

Какой будет результат выполнения данного кода?

Варианты ответа:

  1. false false true true
  2. true false true true
  3. true false true false
  4. false true false true
  5. true true true false

Обоснование:

Важное о Integer в Java:

  • Класс Integer кеширует значения в диапазоне от -128 до 127 (это называется Integer Cache).
  • Если два объекта типа Integer содержат значения в этом диапазоне и созданы с помощью автоупаковки (autoboxing) или Integer.valueOf(), они будут ссылаться на один и тот же объект.
  • Для значений вне диапазона кеширования, == сравнивает ссылки на разные объекты.

Пошаговый анализ кода:

  1. Переменные a и b: Integer a = 100; Integer b = 100;
    • Значение 100 попадает в диапазон кеширования (-128 до 127).
    • a и b будут ссылаться на один и тот же объект.
    • a == btrue.
  2. Переменные c и d: Integer c = 200; Integer d = 200;
    • Значение 200 не попадает в диапазон кеширования.
    • c и d будут ссылаться на разные объекты.
    • c == dfalse.
  3. Переменные e и a: Integer e = Integer.valueOf(100);
    • Integer.valueOf(100) использует кеширование.
    • e будет ссылаться на тот же объект, что и a (значение 100 в кеше).
    • e == atrue.
  4. Переменные f и c: Integer f = Integer.valueOf(200);
    • Integer.valueOf(200) не кеширует значение 200, так как оно вне диапазона.
    • f и c будут ссылаться на разные объекты.
    • f == cfalse.

Результаты сравнений:

  1. a == btrue
  2. c == dfalse
  3. e == atrue
  4. f == cfalse

📌Правильный ответ:

true false true false


Вопрос 5

Какой из перечисленных типов НЕ является reifiable?

Варианты ответа:

  1. String
  2. List<?>[]
  3. ArrayList
  4. Comparable<? super String>
  5. Integer

Обоснование:

В Java reifiable типы – это типы, которые сохраняют всю информацию о своем типе во время выполнения. В отличие от них, non-reifiable типы теряют часть информации из-за type erasure (стирания типов).

Примеры reifiable типов:

  • Примитивные типы: int, double, и т.д.
  • Неформализованные типы: String, Integer, ArrayList
  • Массивы с известным типом элементов: String[], Integer[]

Примеры non-reifiable типов:

  • Wildcard типы с ? (например, List<?>)
  • Generic типы с ограничениями (Comparable<? super String>)
  • Массивы с wildcard (List<?>[])

Анализ вариантов:

  1. String – это reifiable тип (не generic, не теряет информацию).
  2. List<?>[] – это non-reifiable тип, так как содержит wildcard ? и массив с wildcard-типом.
  3. ArrayList – это reifiable тип, т.к. это конкретный класс.
  4. Comparable<? super String> – это non-reifiable, так как содержит wildcard с ограничением.
  5. Integer – это reifiable тип.

📌Правильный ответ:

List<?>[]


Вопрос 6

Каким способом в Java достигается полиморфизм?

Варианты ответа:

  1. «Слабые» ссылки
  2. Переопределение методов
  3. Композиция
  4. Generics
  5. Инкапсуляция

Обоснование:
Полиморфизм в Java достигается в первую очередь через переопределение методов (method overriding). Полиморфизм позволяет объекту принимать множество форм и предоставляет возможность использовать единый интерфейс для взаимодействия с различными типами объектов.

  • Переопределение методов – ключевой механизм полиморфизма в Java. Метод в подклассе переопределяет метод родительского класса, позволяя вызвать метод в зависимости от типа объекта во время выполнения (runtime).
  • Композиция, Generics и инкапсуляция не относятся напрямую к реализации полиморфизма.
  • "Слабые" ссылки (Weak References) также не имеют отношения к полиморфизму.

📌Правильный ответ:
Переопределение методов


Вопрос 7

Каков результат работы этого кода?

Варианты ответа:

  1. Не компилируется
  2. 2
  3. 4
  4. 1
  5. 3

Обоснование:

  1. Ключевой момент:
    • В блоке try создается переменная someClass, которая равна null:
      SomeClass someClass = null; return someClass.i;
    • Поле i класса SomeClass является static.
    • Доступ к статическим полям через null-ссылку допустим и не вызывает исключение NullPointerException. Поэтому код скомпилируется и выполнится корректно.
  2. Значение возвращаемое из блока try:
    • Статическое поле i имеет значение 1.
    • Таким образом, инструкция return someClass.i возвращает 1.
  3. Блок finally:
    • Независимо от того, что возвращается в блоке try, блок finally всегда выполняется.
    • Если в блоке finally есть инструкция return, она переопределяет значение, возвращаемое из try или catch.
    • В данном случае:
      finally { return 4; } Таким образом, 4 будет возвращено вместо 1.

📌Правильный ответ: 4.


Вопрос 8

У вас есть 10 потоков, которые одновременно обращаются к одному и тому же ресурсу — очереди задач, реализованной с помощью BlockingQueue. Вам необходимо реализовать механизм, который гарантирует, что не более 5 потоков одновременно могут обрабатывать задачи из очереди.

Какой из следующих способов НЕ подходит для решения задачи?

Варианты ответа:

  1. Использовать CountDownLatch с начальным значением 5 и каждый раз перед получением задачи из очереди вызывать метод await() у CountDownLatch. Когда поток заканчивает обработку задачи, он вызывает countDown().
  2. Использовать ReentrantLock для блокировки доступа к очереди задач, а внутри блокировки проверять, не превышает ли количество обрабатывающих задач 5, и, если да, ждать освобождения блокировки.
  3. Использовать Semaphore с начальным значением 5 и каждый раз перед получением задачи из очереди вызывать метод acquire() у семафора, а после обработки задачи — release().
  4. Использовать ExecutorService с фиксированным размером пула потоков, равным 5, и запускать все потоки через этот пул.
  5. Создать отдельный класс-посредник, который будет обрабатывать запросы на получение задач из очереди и запускать обработку задач только для 5 потоков одновременно.

Обоснование:

  1. CountDownLatch:
    • CountDownLatch используется для синхронизации потоков, но он не предназначен для ограничения количества потоков, одновременно обрабатывающих задачи.
    • CountDownLatch нельзя "сбросить" или повторно использовать – его счетчик только уменьшается. Это делает его неподходящим для ситуации с циклическим ограничением 5 потоков.
  2. ReentrantLock:
    • С ReentrantLock можно реализовать блокировку доступа и проверку количества обрабатывающих потоков.
    • Однако реализация потребует дополнительной логики, но технически это возможно.
  3. Semaphore:
    • Semaphore идеально подходит для ограничения количества потоков с помощью счетчика ресурсов.
    • Метод acquire() уменьшает счетчик, а release() увеличивает его, что позволяет легко ограничить до 5 потоков.
  4. ExecutorService:
    • ExecutorService с фиксированным размером пула потоков (newFixedThreadPool(5)) гарантирует, что не более 5 потоков будут одновременно работать.
    • Это наиболее элегантное и простое решение.
  5. Создание класса-посредника:
    • Хотя это решение требует дополнительных разработок, оно технически возможно и решает задачу.

📌Правильный ответ:
Использовать CountDownLatch с начальным значением 5...
Этот способ НЕ подходит, так как CountDownLatch не предназначен для повторного использования и ограничения количества одновременно активных потоков.


Вопрос 9

Какая структура данных из перечисленных НЕ является lock-free?

Варианты ответа:

  1. ConcurrentLinkedQueue
  2. ConcurrentSkipListMap
  3. AtomicInteger
  4. ConcurrentLinkedDeque
  5. ConcurrentHashMap

Обоснование:

  1. ConcurrentLinkedQueue
    • Реализует lock-free алгоритм на основе CAS (Compare-And-Swap) для многопоточной обработки.
    • Это lock-free структура данных.
  2. ConcurrentSkipListMap
    • НЕ является lock-free, так как использует блокировки для обеспечения безопасности доступа при обновлении данных.
    • Она поддерживает более сложную структуру (skip list) и требует блокировок на уровнях узлов.
  3. AtomicInteger
    • Это lock-free структура, использующая атомарные операции (например, CAS) для обновления значений.
  4. ConcurrentLinkedDeque
    • Как и ConcurrentLinkedQueue, это lock-free структура данных для двусвязной очереди.
  5. ConcurrentHashMap
    • Использует сегментированную блокировку для достижения конкурентного доступа, но это частично блокирующая структура данных, а не полностью lock-free.

📌Правильный ответ:
ConcurrentSkipListMap


Вопрос 10

Что из перечисленных функциональных ограничений синхронизации в Java является ЛОЖЬЮ?

Варианты ответа:

  1. Если поток, контролирующий блокирование, держит блокировку, то ни один поток, нуждающийся в этой блокировке, не продвинется в обработке.
  2. Volatile переменная гарантирует атомарность последовательности читать-модифицировать-читать.
  3. Можно прервать поток, который ожидает блокировки.
  4. Можно опрашивать или пытаться получить блокировку, не будучи готовым к долгому ожиданию.
  5. Блокировка не должна быть снята в том же стековом фрейме, в котором была начата.

Анализ вариантов:

  1. "Если поток, контролирующий блокирование, держит блокировку, то ни один поток, нуждающийся в этой блокировке, не продвинется в обработке"
    • Это правда, так как поток, владеющий блокировкой, должен освободить её, прежде чем другие потоки смогут получить доступ.
  2. "Volatile переменная гарантирует атомарность последовательности читать-модифицировать-читать"
    • Это ЛОЖЬ.
    • Ключевое слово volatile гарантирует видимость изменений переменной между потоками, но не обеспечивает атомарности операций. Операция читать-модифицировать-читать (например, i++) не атомарна и требует использования синхронизации или атомарных классов (например, AtomicInteger).
  3. "Можно прервать поток, который ожидает блокировки"
    • Это правда, поток может быть прерван во время ожидания блокировки с использованием метода interrupt().
  4. "Можно опрашивать или пытаться получить блокировку, не будучи готовым к долгому ожиданию"
    • Это правда. Например, tryLock() в ReentrantLock позволяет попытаться получить блокировку без ожидания.
  5. "Блокировка не должна быть снята в том же стековом фрейме, в котором была начата"
    • Это правда для некоторых механизмов синхронизации в Java. Например, с использованием ReentrantLock, блокировку можно освободить в другом методе.

📌Правильный ответ:
"Volatile переменная гарантирует атомарность последовательности читать-модифицировать-читать"


Вопрос 11

В компании N наняли junior разработчика. Первой его задачей было написать несложный асинхронный код, который последовательно вызывает сервисы с некоторыми условиями. После вызова сервиса 1 должен одновременно вызываться сервис 2 и сервис 3. Если на момент вызова сервиса 3 вызов сервиса 2 еще не завершен - задача на вызов сервиса 3 ожидает ответа от сервиса 2 и печатает лог об ожидании.

Junior разработчик написал следующий код и попросил сделать Вас ревью. Допустили бы Вы данный код в продакшн? (Обработкой ошибок можно пренебречь).

Варианты ответа:

  1. Да. С кодом все хорошо.
  2. Данный код валиден, но для третьего и второго сервиса необязательно передавать результат вызова первого сервиса. thenAccept можно заменить на thenRun.
  3. Код невалиден, вместо использования CompletableFuture chain Паша должен просто сделать Future для вызова первого сервиса и сделать на нем join. Затем линейно вызвать сначала сервис 2, а затем сервис 3.
  4. Нет. Лучше использовать метод thenCombine на вызове третьего сервиса и передать в него вызов второго сервиса первым параметром.
  5. Нет. Вместо thenAccept и thenRun (в случае сервиса 3) надо использовать thenAcceptAsync и thenRunAsync.

Анализ кода:
В коде используется CompletableFuture для асинхронного вызова сервисов с условиями. Однако имеются несколько недочетов, которые могут повлиять на производительность и правильность работы.

Недочеты:

  1. thenAccept и блокирующий join():
    • Использование thenAccept и вызова join() для ожидания завершения второго сервиса не является идеальным решением. join() блокирует текущий поток, что противоречит принципу асинхронности.
    • Это приводит к блокировке и потенциальному снижению производительности.
  2. Проблема многопоточной безопасности с AtomicBoolean:
    • Использование AtomicBoolean для проверки и изменения флага в разных асинхронных ветках может привести к некорректному результату при высоких нагрузках или конкурентном доступе.
  3. Отсутствие thenCombine:
    • Метод thenCombine идеально подходит для объединения результатов двух асинхронных задач. В текущем коде этого подхода не используется.
  4. Использование thenAccept вместо thenAcceptAsync:
    • thenAccept выполняется в том же потоке, который завершил предыдущее действие. Для улучшения асинхронности стоит использовать thenAcceptAsync.

Наиболее корректный вариант:
"Нет. Лучше использовать метод thenCombine на вызове третьего сервиса и передать в него вызов второго сервиса первым параметром."

Обоснование:

  • thenCombine позволяет запустить два асинхронных вызова параллельно и комбинировать их результаты, что избавляет от необходимости вручную блокировать поток.
  • Код станет более чистым, асинхронным и производительным.

📌Правильный ответ:
"Нет. Лучше использовать метод thenCombine на вызове третьего сервиса и передать в него вызов второго сервиса первым параметром."


Вопрос 12

Какие структуры данных требуют, чтобы данные реализовывали интерфейс Comparable?

Варианты ответа:

  1. TreeMap
  2. ConcurrentHashSet
  3. HashMap
  4. ArrayList
  5. LinkedDeque

Обоснование:

  1. TreeMap
    • TreeMap требует, чтобы ключи реализовывали интерфейс Comparable или был передан компаратор (Comparator) для определения порядка элементов.
    • Без реализации Comparable или компаратора добавление элементов приведет к исключению ClassCastException.
  2. ConcurrentHashSet
    • ConcurrentHashSet (не входит в стандартный JDK, но его часто используют как потокобезопасный Set на основе ConcurrentHashMap) не требует, чтобы элементы реализовывали Comparable. Порядок элементов не важен.
  3. HashMap
    • HashMap не требует реализации Comparable, так как элементы хранятся в неупорядоченном виде и сравнение осуществляется через equals() и hashCode().
  4. ArrayList
    • ArrayList не требует реализации Comparable. Элементы добавляются в порядке вставки, а сортировка выполняется вручную (например, через Collections.sort), где Comparable или Comparator может быть использован.
  5. LinkedDeque
    • LinkedDeque (двусвязная очередь) также не требует Comparable, так как порядок элементов определяется порядком добавления.

📌Правильный ответ:
TreeMap


Вопрос 13

После продолжительной работы сервиса графики показали, что память постепенно растет. Вы попросили вашего напарника проинспектировать код на наличие утечки памяти. Через пару дней он пришел с результатом, что утечкой памяти является циклическая ссылка в коде класса реализации графа.

Выглядит это следующим образом. Может ли это быть причиной утечки памяти? (в качестве GC используется ParallelGC).

Варианты ответа:

  1. Да. Из-за цикличности ссылок объекты не могут быть перемещены из молодого поколения в старое.
  2. Нет. Благодаря trace-логике GC циклические объекты легко очищаются, если до них невозможно дойти по ссылкам от root объектов.
  3. Нет. Данные объекты не аллоцируются на куче при такой логике и тем самым не вызывают утечку.
  4. Да. Так как у объектов с циклическими ссылками надо вызывать метод .finalize() чтобы он очистился. Иначе GC не может их удалить.
  5. Да. ParallelGC не умеет работать с циклическими ссылками, поэтому каждый вызов метода createAndDelete() увеличивает количество неуничтожаемых объектов.

Обоснование:

  1. Работа GC в Java:
    • Сборщик мусора в Java основан на trace-логике, которая определяет достижимость объектов от корневых объектов (GC Roots).
    • Если объект (или группа объектов с циклическими ссылками) не достижим из GC Roots, то GC пометит их как мусор и удалит.
  2. Циклические ссылки:
    • Циклические ссылки не мешают сборке мусора, если на объекты нет внешних активных ссылок.
    • В Java GC умеет определять, что группа объектов недостижима, даже если они ссылаются друг на друга.
  3. ParallelGC:
    • ParallelGC, используемый по умолчанию в Java 8, способен корректно работать с циклическими ссылками и очищать такие объекты.
  4. Ошибочные утверждения:
    • "Надо вызывать метод finalize()" – устаревший и ненужный подход, GC очищает объекты автоматически.
    • "ParallelGC не умеет работать с циклическими ссылками" – неверно, все стандартные GC в Java справляются с этой задачей.


Современные сборщики мусора в JVM, включая ParallelGC, используют алгоритмы на основе графа объектов (например, Mark-and-Sweep). Они определяют, какие объекты достижимы от корневых объектов (root objects). Циклические ссылки не являются проблемой, если на эти объекты нет внешних ссылок, делающих их достижимыми. Таким образом, такие объекты автоматически удаляются, и это не приводит к утечкам памяти.

📌Правильный ответ:

2. Нет. Благодаря trace-логике GC циклические объекты легко очищаются, если до них невозможно дойти по ссылкам от root объектов.

Разбор практического задания продвинутого уровня в следующей статье