December 25, 2024

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

В данном разборе мы подробно рассмотрим пример тестового задания среднего уровня, обратив внимание на:

  • Типичные ошибки, возникающие при работе с многопоточностью (synchronized, ExecutorService).
  • Правила работы с исключениями (проверяемыми и непроверяемыми).
  • Подводные камни при использовании Generics, Type Erasure и управления строками в Java.

Нашей целью будет показать, как избегать этих ошибок и как аргументированно выбирать оптимальные решения. Если вас заинтересовали детали разборов подобных задач, приглашаем ознакомиться с более глубоким анализом практического задания среднего уровня, который вы найдете в следующей статье:

Продолжим с разбором тестовых заданий!

Вопрос 1

Приведённая ниже программа не компилируется, какая ошибка допущена?

Код программы:

class Printer { public static void print(String message) { System.out.println(message); } }

public class Example { private static void main(String name, int age) { // метод 1 if (age > 18) { String message = name + " is adult"; } else { String message = name + " is not adult"; } Printer printer = null; printer.print(message); }

public static void main(String[] args) { // метод 2 main("Alex", 18); } }

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

  1. Метод 1 недоступен в методе 2, так как имеет модификатор доступа private
  2. Неверно назван класс, в котором находится точка входа в программу
  3. Попытка вызова метода на null переменной
  4. Использование переменной вне её области видимости
  5. Перегружен метод main

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

  1. Метод 1 недоступен в методе 2. Это неверно, так как модификатор private допускает доступ к методу внутри одного и того же класса. В данном случае метод main(String name, int age) вызывается из того же класса Example, что корректно.
  2. Неверно назван класс, в котором находится точка входа в программу. Точка входа в программу (public static void main(String[] args)) существует, и её расположение в классе Example соответствует требованиям Java. Название класса никак не нарушает компиляцию.
  3. Попытка вызова метода на null переменной. В коде переменная printer инициализируется как null, а затем вызывается метод print. Так как метод print является статическим, вызов происходит через экземпляр (printer.print(message)), что приводит к NullPointerException.
  4. Использование переменной вне её области видимости. Переменная message создаётся внутри блока if или else, но попытка её использования вне этих блоков вызывает ошибку компиляции. Это связано с тем, что область видимости переменной message ограничена блоком, в котором она объявлена.
  5. Перегружен метод main. Это допустимо в Java. Перегрузка метода main допустима, и это не вызывает ошибку компиляции.

📌Правильный ответ:
4. Использование переменной вне её области видимости.


Вопрос 2

Что выведет фрагмент кода?

Код программы:

int day = 4; switch (day) { case 1: System.out.print("One"); break; case 2: case 4: System.out.print("Four"); day = 1; case 5: System.out.print("Five"); }

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

  1. FourOne
  2. FourFive
  3. FourFiveOne
  4. One
  5. Код не компилируется

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

  1. Переменная day и её значение. Изначально переменной day присваивается значение 4, а далее проверка в switch начинается с этого значения.
  2. Переход к case 4. Так как значение day = 4, выполнение начнётся с case 4. Код внутри case 4 выполнит System.out.print("Four");, затем изменит значение day на 1. Однако, несмотря на изменение значения day, это не влияет на выполнение текущего switch (значение day не проверяется повторно). После выполнения этого блока выполнение продолжается.
  3. Переход к case 5. Следующим блоком является case 5, так как в case 4 отсутствует break. Код в case 5 выполнит System.out.print("Five");.
  4. Пропуск других case. Ни один из других case не будет выполнен, так как выполнение началось с case 4 и продолжилось до конца switch без break.
  5. Вывод программы. Итоговый вывод программы: FourFive.

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


Вопрос 3

Что произойдет в данной программе?

Код программы:

System.out.print("Hello"); while (1) { System.out.println(" World"); break; }

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

  1. Программа не скомпилируется
  2. В консоли будет напечатано “Hello”
  3. Выбросится исключение
  4. В консоли будет напечатано “Hello World”
  5. Программа уйдёт в бесконечный цикл

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

  1. Компиляция программы. Код while (1) в Java не компилируется, так как условие цикла должно быть булевым выражением (true или false). Число 1 в данном случае не является корректным логическим выражением в Java, в отличие от языков, таких как C или C++.
  2. Вывод до ошибки. Команда System.out.print("Hello"); выполнится до того, как произойдет ошибка компиляции, но программа не запустится из-за указанного выше синтаксического несоответствия.
  3. Исключение или ошибка. Исключение не выбрасывается, так как код не проходит стадию компиляции.

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

  1. Программа не скомпилируется.


Вопрос 4

В программе необходимо хранить температуру воды в градусах Цельсия в рамках теста, где она может колебаться от 0 до 100 градусов (только целые значения).
Какой из перечисленных ниже типов данных вы бы выбрали для хранения температуры?

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

  1. long
  2. short
  3. int
  4. byte
  5. char

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

  1. Диапазон значений:
    • Диапазон температур, который нужно хранить, составляет от 0 до 100.
    • Тип byte (8 бит) в Java имеет диапазон значений от -128 до 127. Это означает, что данный тип подходит для хранения значений в указанном диапазоне.
    • Тип short (16 бит) имеет больший диапазон (-32,768 до 32,767), но он избыточен для данной задачи.
    • Тип int (32 бита) также избыточен, так как занимает больше памяти и подходит для значительно больших диапазонов значений.
    • Тип long (64 бита) предназначен для очень больших чисел и здесь неуместен.
    • Тип char используется для хранения символов, а не чисел, поэтому он сразу исключается.
  2. Эффективность: Использование типа byte минимизирует использование памяти и соответствует требуемому диапазону. Выбор short или int возможен, но не оптимален.

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


Вопрос 5

Что такое стирание типов (type erasure) в контексте Generics?

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

  1. Процесс, при котором компилятор заменяет все generic-типы на Object и добавляет автоматические приведения типов во время выполнения
  2. Процесс, при котором компилятор удаляет информацию о параметрах generic-типа во время компиляции, заменяя их на ограничивающие типы или Object
  3. Процесс, при котором компилятор добавляет дополнительные проверки типов во время выполнения программы для обеспечения безопасности типов
  4. Процесс, при котором параметры обобщенных типов преобразуются в объекты примитивных типов во время выполнения программы
  5. Процесс, при котором компилятор заменяет все использования generics конкретными типами данных, указанными при инициализации, чтобы улучшить производительность программы

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

  1. Type Erasure (Стирание типов):
    Type Erasure — это механизм в Java, при котором информация о параметризованных типах (generics) удаляется во время компиляции. Вместо конкретных типов используется либо ограничивающий тип (если указан, например <T extends Number>), либо Object (если ограничение отсутствует). Это позволяет сохранить совместимость с кодом, написанным до появления Generics.
  2. Как это работает:
    • Компилятор заменяет параметры типов их ограничивающими типами или Object.
    • Приведения типов (type casting) добавляются автоматически, чтобы обеспечить корректное выполнение программы.
  3. Ошибочные утверждения:
    • Вариант 1: Приведения типов выполняются, но не "во время выполнения", а добавляются на этапе компиляции.
    • Вариант 3: Проверки типов выполняются на этапе компиляции, но не во время выполнения.
    • Вариант 4: Generics не работают с примитивными типами (такими как int), так что этот процесс не имеет отношения к стиранию типов.
    • Вариант 5: Generics не заменяются конкретными типами данных на этапе компиляции, это концепция не относится к Java Generics.

📌Правильный ответ:
2. Процесс, при котором компилятор удаляет информацию о параметрах generic-типа во время компиляции, заменяя их на ограничивающие типы или Object.


Вопрос 6

Имеются два параметризованных списка. Какой тип будет у объектов этих списков после стирания типов?

List<? extends Number> list1; List<? super Integer> list2;

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

  1. Для list1 типом будет Number, для list2Object
  2. Для list1 и list2 будет использован Serializable
  3. Для list1 типом будет Number, для list2Integer
  4. Для list1 и list2 будет использован Object
  5. Для list1 типом будет Object, для list2Integer

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

  1. Стирание типов для List<? extends Number>:
    • Параметр типа заменяется на его ограничивающий тип. В данном случае ограничением является Number, поэтому во время компиляции параметр ? extends Number будет заменён на Number.
  2. Стирание типов для List<? super Integer>:
    • Параметр типа заменяется на самый общий тип, который может быть использован. Для ? super Integer самым общим типом является Object, так как любой супертип Integer (Number, Object) может быть использован.
  3. Ошибочные утверждения:
    • Вариант 2: Serializable не имеет отношения к указанным параметрам типа.
    • Вариант 3: Для list2 типом будет Object, а не Integer.
    • Вариант 4: Для list1 используется ограничивающий тип Number, а не Object.

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

  1. Для list1 типом будет Number, для list2Object.

Вопрос 7

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

Код программы:

String str1 = "HelloWorld"; String temp = "World";

String str2 = "Hello" + "World"; String str3 = "Hello" + temp; String str4 = new String("HelloWorld");

System.out.print("str1 == str2: " + (str1 == str2) + "; "); System.out.print("str1 == str3: " + (str1 == str3) + "; "); System.out.print("str1 == str4: " + (str1 == str4));

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

  1. str1 == str2: true; str1 == str3: false; str1 == str4: false
  2. str1 == str2: true; str1 == str3: true; str1 == str4: true
  3. str1 == str2: false; str1 == str3: false; str1 == str4: true
  4. str1 == str2: false; str1 == false; str1 == str4: false
  5. str1 == str2: true; str1 == str3: true; str1 == str4: false

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

  1. Переменная str1:
    • Переменная str1 указывает на строковый литерал "HelloWorld", который находится в строковом пуле Java.
  2. Переменная str2:
    • str2 — это результат конкатенации двух строковых литералов ("Hello" + "World").
    • Компилятор выполняет оптимизацию на этапе компиляции и заменяет это выражение на строковый литерал "HelloWorld".
    • Поэтому str2 указывает на тот же объект в строковом пуле, что и str1.
    • Результат: str1 == str2true.
  3. Переменная str3:
    • str3 — это результат конкатенации строки "Hello" и переменной temp, содержащей "World".
    • Поскольку переменная temp вычисляется во время выполнения, оптимизация строкового пула не применяется.
    • В результате создаётся новый объект в куче.
    • Результат: str1 == str3false.
  4. Переменная str4:
    • str4 создаётся с использованием конструктора new String("HelloWorld").
    • Это явно создаёт новый объект в куче, даже если строка "HelloWorld" уже существует в строковом пуле.
    • Результат: str1 == str4false.

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

  1. str1 == str2: true; str1 == str3: false; str1 == str4: false


Вопрос 8

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

Код программы:

public static void main(String[] args) { interface A { default void foo() { System.out.println("A"); } }

interface B { default void foo() { System.out.println("B"); } }

class C implements A, B { @Override public void foo() { System.out.println("Foo"); } }

A c = new C(); c.foo(); }

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

  1. A
  2. B
  3. Foo
  4. Ошибка времени выполнения
  5. Ошибка компиляции

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

  1. Наследование default-методов в интерфейсах:
    • В Java, если класс реализует два интерфейса, которые содержат default-методы с одинаковыми именами, то возникает конфликт (ambiguity conflict). Для решения этого конфликта класс обязан переопределить метод.
  2. Класс C:
    • Класс C реализует оба интерфейса (A и B) и содержит собственную реализацию метода foo(). Эта реализация переопределяет default-методы интерфейсов A и B.
    • Таким образом, при вызове метода foo() будет использована реализация из класса C.
  3. Создание объекта:
    • Объект создаётся как A c = new C();.
    • При вызове c.foo() будет использована реализация метода foo() из класса C, так как она переопределяет метод.
  4. Вывод программы:
    • Метод foo() из класса C выводит строку "Foo".

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


Вопрос 9

Какое из следующих утверждений об интерфейсах в Java верно? (Java 8+)

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

  1. Интерфейс не может содержать переменные, только методы
  2. Интерфейс может быть инстанцирован с использованием ключевого слова new
  3. Интерфейс не может расширять (наследовать) другие интерфейсы
  4. Интерфейс может содержать статические методы с реализацией
  5. Интерфейс может содержать только абстрактные методы

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

  1. Интерфейс не может содержать переменные, только методы:
    • Неверно. Интерфейсы могут содержать переменные, но они всегда являются public static final (константами).
  2. Интерфейс может быть инстанцирован с использованием ключевого слова new:
    • Неверно. Интерфейсы не могут быть инстанцированы напрямую. Однако можно использовать анонимные классы для создания экземпляров интерфейса.
  3. Интерфейс не может расширять (наследовать) другие интерфейсы:
    • Неверно. Интерфейсы могут наследовать (расширять) другие интерфейсы с помощью ключевого слова extends.
  4. Интерфейс может содержать статические методы с реализацией:
    • Верно. Начиная с Java 8, интерфейсы могут содержать статические методы с реализацией. Эти методы принадлежат интерфейсу и вызываются через его имя.
  5. Интерфейс может содержать только абстрактные методы:
    • Неверно. Начиная с Java 8, интерфейсы могут содержать default методы с реализацией и статические методы.

📌Правильный ответ:
4. Интерфейс может содержать статические методы с реализацией.


Вопрос 10

В чем основное отличие между проверяемыми (checked) и непроверяемыми (unchecked) исключениями в Java?

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

  1. Проверяемые исключения обрабатываются JVM автоматически, а непроверяемые требуют явного кода для обработки
  2. Проверяемые исключения возникают при ошибках на уровне операционной системы, а непроверяемые — в коде приложения
  3. Проверяемые исключения обязательно должны быть объявлены в сигнатуре метода или обработаны, а непроверяемые — нет
  4. Непроверяемые исключения нельзя поймать с помощью блока try-catch, а проверяемые можно
  5. Проверяемые исключения требуют явного указания типа в обработчике catch, а непроверяемые исключения могут быть обработаны только через базовый класс Exception

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

  1. Проверяемые исключения (checked exceptions):
    • Проверяемые исключения представляют собой подтипы класса Exception, но не включают RuntimeException и его подклассы.
    • Компилятор требует, чтобы такие исключения либо обрабатывались с помощью блока try-catch, либо указывались в сигнатуре метода через throws.
  2. Непроверяемые исключения (unchecked exceptions):
    • Это исключения, являющиеся подтипами класса RuntimeException.
    • Компилятор не требует их явного объявления в сигнатуре метода или обработки.
  3. Ошибочные утверждения:
    • Вариант 1: Ни проверяемые, ни непроверяемые исключения не обрабатываются JVM автоматически. Обработка исключений требует явного кода.
    • Вариант 2: Это неверно, так как оба типа исключений могут возникать как на уровне операционной системы, так и в коде приложения.
    • Вариант 4: Непроверяемые исключения также можно поймать с помощью блока try-catch.
    • Вариант 5: Это утверждение некорректно. Непроверяемые исключения можно обработать, как и проверяемые, независимо от базового класса.
  4. Корректное утверждение:
    • Вариант 3: Проверяемые исключения требуют обработки или явного указания в сигнатуре метода, а непроверяемые исключения этого не требуют.

📌Правильный ответ:
3. Проверяемые исключения обязательно должны быть объявлены в сигнатуре метода или обработаны, а непроверяемые — нет.


Вопрос 11

В чем преимущество использования ExecutorService для управления потоками по сравнению с ручным созданием и управлением фиксированным количеством потоков с помощью класса Thread?

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

  1. Упрощает управление потоками за счет их повторного использования и предоставляет встроенные средства для планирования
  2. Создает поток для каждой новой задачи, что обеспечивает большую гибкость, чем фиксированное количество потоков
  3. Позволяет создавать потоки с более высоким приоритетом, чем при использовании Thread
  4. Гарантирует, что все задачи будут выполнены в порядке их добавления, чего нельзя достичь при ручном управлении потоками
  5. Всегда обеспечивает более высокую производительность, поскольку потоки создаются и управляются на уровне аппаратного обеспечения

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

  1. ExecutorService и его особенности:
    • Основное преимущество использования ExecutorService заключается в том, что он позволяет повторно использовать потоки из пула. Это сокращает накладные расходы на создание новых потоков и завершение их работы.
    • Кроме того, он предоставляет встроенные средства для управления задачами, такими как планирование, тайм-ауты и контроль за завершением выполнения.
  2. Ошибка в утверждениях:
    • Вариант 2: ExecutorService не создает новый поток для каждой задачи, он использует потоки из пула.
    • Вариант 3: ExecutorService не влияет на приоритет потоков, это можно настроить отдельно.
    • Вариант 4: Порядок выполнения задач в ExecutorService зависит от реализации, например, фиксированный пул потоков (FixedThreadPool) выполняет задачи в порядке их добавления, но другие реализации могут работать иначе.
    • Вариант 5: Использование ExecutorService не гарантирует улучшение производительности на уровне аппаратного обеспечения.
  3. Преимущество:
    • Вариант 1 верно отражает основное преимущество ExecutorService: управление потоками становится проще благодаря их повторному использованию, а также наличию встроенных инструментов для планирования и контроля задач.

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

  1. Упрощает управление потоками за счет их повторного использования и предоставляет встроенные средства для планирования.


Вопрос 12

Что произойдет с потоком, если он попытается войти в synchronized блок, который уже занят другим потоком?

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

  1. Немедленно завершит свое выполнение
  2. Продолжит выполнение, но с меньшим приоритетом
  3. Продолжит выполнение без каких-либо ожиданий, так как synchronized блоки не влияют на его выполнение
  4. Заблокируется до тех пор, пока другой поток не освободит монитор, после чего продолжит выполнение
  5. Приостановит свое выполнение до момента, когда из другого потока будет вызван метод notify

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

  1. Работа с synchronized блоками:
    • В Java synchronized используется для обеспечения взаимного исключения, чтобы только один поток мог выполнить код внутри блока или метода, связанного с одним и тем же монитором (объектом, используемым для синхронизации).
    • Если поток пытается войти в synchronized блок, а монитор уже занят другим потоком, поток переходит в состояние ожидания и блокируется, пока монитор не будет освобожден.
  2. Ошибочные утверждения:
    • Вариант 1: Поток не завершает свое выполнение, он ждет освобождения монитора.
    • Вариант 2: Приоритет потока не изменяется, он просто ожидает своей очереди на вход в synchronized блок.
    • Вариант 3: Это неверно, так как выполнение потока останавливается, если монитор уже занят.
    • Вариант 5: Метод notify используется для управления потоками в механизме ожидания и уведомления (wait/notify), но он не связан с доступом к synchronized блокам.
  3. Корректное поведение:
    • Поток будет заблокирован до тех пор, пока монитор, связанный с synchronized блоком, не будет освобожден другим потоком. После освобождения монитор переходит к ожидающему потоку, который продолжает выполнение.

📌Правильный ответ:
4. Заблокируется до тех пор, пока другой поток не освободит монитор, после чего продолжит выполнение.


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