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: falsestr1 == str2: true; str1 == str3: true; str1 == str4: truestr1 == str2: false; str1 == str3: false; str1 == str4: truestr1 == str2: false; str1 == false; str1 == str4: falsestr1 == 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. Заблокируется до тех пор, пока другой поток не освободит монитор, после чего продолжит выполнение.
Разбор практического задания среднего уровня в следующей статье