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 недоступен в методе 2, так как имеет модификатор доступа private
- Неверно назван класс, в котором находится точка входа в программу
- Попытка вызова метода на null переменной
- Использование переменной вне её области видимости
- Перегружен метод main
- Метод 1 недоступен в методе 2. Это неверно, так как модификатор
private
допускает доступ к методу внутри одного и того же класса. В данном случае методmain(String name, int age)
вызывается из того же классаExample
, что корректно. - Неверно назван класс, в котором находится точка входа в программу. Точка входа в программу (
public static void main(String[] args)
) существует, и её расположение в классеExample
соответствует требованиям Java. Название класса никак не нарушает компиляцию. - Попытка вызова метода на null переменной. В коде переменная
printer
инициализируется какnull
, а затем вызывается методprint
. Так как методprint
является статическим, вызов происходит через экземпляр (printer.print(message)
), что приводит кNullPointerException
. - Использование переменной вне её области видимости. Переменная
message
создаётся внутри блокаif
илиelse
, но попытка её использования вне этих блоков вызывает ошибку компиляции. Это связано с тем, что область видимости переменнойmessage
ограничена блоком, в котором она объявлена. - Перегружен метод 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");
}
- Переменная
day
и её значение. Изначально переменнойday
присваивается значение4
, а далее проверка вswitch
начинается с этого значения. - Переход к
case 4
. Так как значениеday = 4
, выполнение начнётся сcase 4
. Код внутриcase 4
выполнитSystem.out.print("Four");
, затем изменит значениеday
на1
. Однако, несмотря на изменение значенияday
, это не влияет на выполнение текущегоswitch
(значениеday
не проверяется повторно). После выполнения этого блока выполнение продолжается. - Переход к
case 5
. Следующим блоком являетсяcase 5
, так как вcase 4
отсутствуетbreak
. Код вcase 5
выполнитSystem.out.print("Five");
. - Пропуск других
case
. Ни один из другихcase
не будет выполнен, так как выполнение началось сcase 4
и продолжилось до концаswitch
безbreak
. - Вывод программы. Итоговый вывод программы:
FourFive
.
Вопрос 3
Что произойдет в данной программе?
System.out.print("Hello");
while (1) {
System.out.println(" World");
break;
}
- Программа не скомпилируется
- В консоли будет напечатано “Hello”
- Выбросится исключение
- В консоли будет напечатано “Hello World”
- Программа уйдёт в бесконечный цикл
- Компиляция программы. Код
while (1)
в Java не компилируется, так как условие цикла должно быть булевым выражением (true
илиfalse
). Число1
в данном случае не является корректным логическим выражением в Java, в отличие от языков, таких как C или C++. - Вывод до ошибки. Команда
System.out.print("Hello");
выполнится до того, как произойдет ошибка компиляции, но программа не запустится из-за указанного выше синтаксического несоответствия. - Исключение или ошибка. Исключение не выбрасывается, так как код не проходит стадию компиляции.
Вопрос 4
В программе необходимо хранить температуру воды в градусах Цельсия в рамках теста, где она может колебаться от 0 до 100 градусов (только целые значения).
Какой из перечисленных ниже типов данных вы бы выбрали для хранения температуры?
- Диапазон значений:
- Диапазон температур, который нужно хранить, составляет от 0 до 100.
- Тип
byte
(8 бит) в Java имеет диапазон значений от -128 до 127. Это означает, что данный тип подходит для хранения значений в указанном диапазоне. - Тип
short
(16 бит) имеет больший диапазон (-32,768 до 32,767), но он избыточен для данной задачи. - Тип
int
(32 бита) также избыточен, так как занимает больше памяти и подходит для значительно больших диапазонов значений. - Тип
long
(64 бита) предназначен для очень больших чисел и здесь неуместен. - Тип
char
используется для хранения символов, а не чисел, поэтому он сразу исключается. - Эффективность: Использование типа
byte
минимизирует использование памяти и соответствует требуемому диапазону. Выборshort
илиint
возможен, но не оптимален.
Вопрос 5
Что такое стирание типов (type erasure) в контексте Generics?
- Процесс, при котором компилятор заменяет все generic-типы на
Object
и добавляет автоматические приведения типов во время выполнения - Процесс, при котором компилятор удаляет информацию о параметрах generic-типа во время компиляции, заменяя их на ограничивающие типы или
Object
- Процесс, при котором компилятор добавляет дополнительные проверки типов во время выполнения программы для обеспечения безопасности типов
- Процесс, при котором параметры обобщенных типов преобразуются в объекты примитивных типов во время выполнения программы
- Процесс, при котором компилятор заменяет все использования generics конкретными типами данных, указанными при инициализации, чтобы улучшить производительность программы
- Type Erasure (Стирание типов):
Type Erasure — это механизм в Java, при котором информация о параметризованных типах (generics) удаляется во время компиляции. Вместо конкретных типов используется либо ограничивающий тип (если указан, например<T extends Number>
), либоObject
(если ограничение отсутствует). Это позволяет сохранить совместимость с кодом, написанным до появления Generics. - Как это работает:
- Компилятор заменяет параметры типов их ограничивающими типами или
Object
. - Приведения типов (type casting) добавляются автоматически, чтобы обеспечить корректное выполнение программы.
- Ошибочные утверждения:
- Вариант 1: Приведения типов выполняются, но не "во время выполнения", а добавляются на этапе компиляции.
- Вариант 3: Проверки типов выполняются на этапе компиляции, но не во время выполнения.
- Вариант 4: Generics не работают с примитивными типами (такими как
int
), так что этот процесс не имеет отношения к стиранию типов. - Вариант 5: Generics не заменяются конкретными типами данных на этапе компиляции, это концепция не относится к Java Generics.
📌Правильный ответ:
2. Процесс, при котором компилятор удаляет информацию о параметрах generic-типа во время компиляции, заменяя их на ограничивающие типы или Object
.
Вопрос 6
Имеются два параметризованных списка. Какой тип будет у объектов этих списков после стирания типов?
List<? extends Number> list1;
List<? super Integer> list2;
- Для
list1
типом будетNumber
, дляlist2
—Object
- Для
list1
иlist2
будет использованSerializable
- Для
list1
типом будетNumber
, дляlist2
—Integer
- Для
list1
иlist2
будет использованObject
- Для
list1
типом будетObject
, дляlist2
—Integer
- Стирание типов для
List<? extends Number>
: - Параметр типа заменяется на его ограничивающий тип. В данном случае ограничением является
Number
, поэтому во время компиляции параметр? extends Number
будет заменён наNumber
. - Стирание типов для
List<? super Integer>
: - Параметр типа заменяется на самый общий тип, который может быть использован. Для
? super Integer
самым общим типом являетсяObject
, так как любой супертипInteger
(Number
,Object
) может быть использован. - Ошибочные утверждения:
Вопрос 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));
str1 == str2: true; str1 == str3: false; str1 == str4: false
str1 == str2: true; str1 == str3: true; str1 == str4: true
str1 == str2: false; str1 == str3: false; str1 == str4: true
str1 == str2: false; str1 == false; str1 == str4: false
str1 == str2: true; str1 == str3: true; str1 == str4: false
- Переменная
str1
: - Переменная
str1
указывает на строковый литерал"HelloWorld"
, который находится в строковом пуле Java. - Переменная
str2
: str2
— это результат конкатенации двух строковых литералов ("Hello" + "World"
).- Компилятор выполняет оптимизацию на этапе компиляции и заменяет это выражение на строковый литерал
"HelloWorld"
. - Поэтому
str2
указывает на тот же объект в строковом пуле, что иstr1
. - Результат:
str1 == str2
→true
. - Переменная
str3
: str3
— это результат конкатенации строки"Hello"
и переменнойtemp
, содержащей"World"
.- Поскольку переменная
temp
вычисляется во время выполнения, оптимизация строкового пула не применяется. - В результате создаётся новый объект в куче.
- Результат:
str1 == str3
→false
. - Переменная
str4
:
Вопрос 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");
}
}
- Наследование default-методов в интерфейсах:
- В Java, если класс реализует два интерфейса, которые содержат
default
-методы с одинаковыми именами, то возникает конфликт (ambiguity conflict). Для решения этого конфликта класс обязан переопределить метод. - Класс
C
: - Класс
C
реализует оба интерфейса (A
иB
) и содержит собственную реализацию методаfoo()
. Эта реализация переопределяетdefault
-методы интерфейсовA
иB
. - Таким образом, при вызове метода
foo()
будет использована реализация из классаC
. - Создание объекта:
- Объект создаётся как
A c = new C();
. - При вызове
c.foo()
будет использована реализация методаfoo()
из классаC
, так как она переопределяет метод. - Вывод программы:
Вопрос 9
Какое из следующих утверждений об интерфейсах в Java верно? (Java 8+)
- Интерфейс не может содержать переменные, только методы
- Интерфейс может быть инстанцирован с использованием ключевого слова
new
- Интерфейс не может расширять (наследовать) другие интерфейсы
- Интерфейс может содержать статические методы с реализацией
- Интерфейс может содержать только абстрактные методы
- Интерфейс не может содержать переменные, только методы:
- Неверно. Интерфейсы могут содержать переменные, но они всегда являются
public static final
(константами). - Интерфейс может быть инстанцирован с использованием ключевого слова
new
: - Неверно. Интерфейсы не могут быть инстанцированы напрямую. Однако можно использовать анонимные классы для создания экземпляров интерфейса.
- Интерфейс не может расширять (наследовать) другие интерфейсы:
- Неверно. Интерфейсы могут наследовать (расширять) другие интерфейсы с помощью ключевого слова
extends
. - Интерфейс может содержать статические методы с реализацией:
- Верно. Начиная с Java 8, интерфейсы могут содержать статические методы с реализацией. Эти методы принадлежат интерфейсу и вызываются через его имя.
- Интерфейс может содержать только абстрактные методы:
📌Правильный ответ:
4. Интерфейс может содержать статические методы с реализацией.
Вопрос 10
В чем основное отличие между проверяемыми (checked) и непроверяемыми (unchecked) исключениями в Java?
- Проверяемые исключения обрабатываются JVM автоматически, а непроверяемые требуют явного кода для обработки
- Проверяемые исключения возникают при ошибках на уровне операционной системы, а непроверяемые — в коде приложения
- Проверяемые исключения обязательно должны быть объявлены в сигнатуре метода или обработаны, а непроверяемые — нет
- Непроверяемые исключения нельзя поймать с помощью блока
try-catch
, а проверяемые можно - Проверяемые исключения требуют явного указания типа в обработчике
catch
, а непроверяемые исключения могут быть обработаны только через базовый классException
- Проверяемые исключения (checked exceptions):
- Проверяемые исключения представляют собой подтипы класса
Exception
, но не включаютRuntimeException
и его подклассы. - Компилятор требует, чтобы такие исключения либо обрабатывались с помощью блока
try-catch
, либо указывались в сигнатуре метода черезthrows
. - Непроверяемые исключения (unchecked exceptions):
- Это исключения, являющиеся подтипами класса
RuntimeException
. - Компилятор не требует их явного объявления в сигнатуре метода или обработки.
- Ошибочные утверждения:
- Вариант 1: Ни проверяемые, ни непроверяемые исключения не обрабатываются JVM автоматически. Обработка исключений требует явного кода.
- Вариант 2: Это неверно, так как оба типа исключений могут возникать как на уровне операционной системы, так и в коде приложения.
- Вариант 4: Непроверяемые исключения также можно поймать с помощью блока
try-catch
. - Вариант 5: Это утверждение некорректно. Непроверяемые исключения можно обработать, как и проверяемые, независимо от базового класса.
- Корректное утверждение:
📌Правильный ответ:
3. Проверяемые исключения обязательно должны быть объявлены в сигнатуре метода или обработаны, а непроверяемые — нет.
Вопрос 11
В чем преимущество использования ExecutorService
для управления потоками по сравнению с ручным созданием и управлением фиксированным количеством потоков с помощью класса Thread
?
- Упрощает управление потоками за счет их повторного использования и предоставляет встроенные средства для планирования
- Создает поток для каждой новой задачи, что обеспечивает большую гибкость, чем фиксированное количество потоков
- Позволяет создавать потоки с более высоким приоритетом, чем при использовании
Thread
- Гарантирует, что все задачи будут выполнены в порядке их добавления, чего нельзя достичь при ручном управлении потоками
- Всегда обеспечивает более высокую производительность, поскольку потоки создаются и управляются на уровне аппаратного обеспечения
ExecutorService
и его особенности:- Основное преимущество использования
ExecutorService
заключается в том, что он позволяет повторно использовать потоки из пула. Это сокращает накладные расходы на создание новых потоков и завершение их работы. - Кроме того, он предоставляет встроенные средства для управления задачами, такими как планирование, тайм-ауты и контроль за завершением выполнения.
- Ошибка в утверждениях:
- Вариант 2:
ExecutorService
не создает новый поток для каждой задачи, он использует потоки из пула. - Вариант 3:
ExecutorService
не влияет на приоритет потоков, это можно настроить отдельно. - Вариант 4: Порядок выполнения задач в
ExecutorService
зависит от реализации, например, фиксированный пул потоков (FixedThreadPool
) выполняет задачи в порядке их добавления, но другие реализации могут работать иначе. - Вариант 5: Использование
ExecutorService
не гарантирует улучшение производительности на уровне аппаратного обеспечения. - Преимущество:
- Упрощает управление потоками за счет их повторного использования и предоставляет встроенные средства для планирования.
Вопрос 12
Что произойдет с потоком, если он попытается войти в synchronized
блок, который уже занят другим потоком?
- Немедленно завершит свое выполнение
- Продолжит выполнение, но с меньшим приоритетом
- Продолжит выполнение без каких-либо ожиданий, так как
synchronized
блоки не влияют на его выполнение - Заблокируется до тех пор, пока другой поток не освободит монитор, после чего продолжит выполнение
- Приостановит свое выполнение до момента, когда из другого потока будет вызван метод
notify
- Работа с
synchronized
блоками: - В Java
synchronized
используется для обеспечения взаимного исключения, чтобы только один поток мог выполнить код внутри блока или метода, связанного с одним и тем же монитором (объектом, используемым для синхронизации). - Если поток пытается войти в
synchronized
блок, а монитор уже занят другим потоком, поток переходит в состояние ожидания и блокируется, пока монитор не будет освобожден. - Ошибочные утверждения:
- Вариант 1: Поток не завершает свое выполнение, он ждет освобождения монитора.
- Вариант 2: Приоритет потока не изменяется, он просто ожидает своей очереди на вход в
synchronized
блок. - Вариант 3: Это неверно, так как выполнение потока останавливается, если монитор уже занят.
- Вариант 5: Метод
notify
используется для управления потоками в механизме ожидания и уведомления (wait/notify), но он не связан с доступом кsynchronized
блокам. - Корректное поведение:
📌Правильный ответ:
4. Заблокируется до тех пор, пока другой поток не освободит монитор, после чего продолжит выполнение.
Разбор практического задания среднего уровня в следующей статье