Перейти к основному содержимому

Вопросы на собеседовании по Java Exceptions (+ ответы)

· 9 мин. чтения

Задача: Наибольшая подстрока без повторений

Для заданной строки s, найдите длину наибольшей подстроки без повторяющихся символов. Подстрока — это непрерывная непустая последовательность символов внутри строки...

ANDROMEDA 42

1. Обзор

Исключения — важная тема, с которой должен быть знаком каждый Java-разработчик. В этой статье даны ответы на некоторые вопросы, которые могут возникнуть во время собеседования.

2. Вопросы

Q1. Что такое исключение?

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

Q2. Какова цель ключевых слов Throw и Throws?

Ключевое слово throws используется для указания того, что метод может вызвать исключение во время его выполнения. Он обеспечивает явную обработку исключений при вызове метода:

public void simpleMethod() throws Exception {
// ...
}

Ключевое слово throw позволяет нам генерировать объект исключения, чтобы прервать нормальный ход программы. Это чаще всего используется, когда программа не удовлетворяет заданному условию:

if (task.isTooComplicated()) {
throw new TooComplicatedException("The task is too complicated");
}

Q3. Как вы можете обработать исключение?

Используя оператор try-catch-finally :

try {
// ...
} catch (ExceptionType1 ex) {
// ...
} catch (ExceptionType2 ex) {
// ...
} finally {
// ...
}

Блок кода, в котором может возникнуть исключение, заключен в блок try . Этот блок также называют «защищенным» или «защищенным» кодом.

Если возникает исключение, выполняется блок catch , соответствующий выбрасываемому исключению, в противном случае все блоки catch игнорируются.

Блок finally всегда выполняется после выхода из блока try , вне зависимости от того, было ли внутри него сгенерировано исключение.

Q4. Как вы можете поймать несколько исключений?

Существует три способа обработки нескольких исключений в блоке кода.

Во-первых, использовать блок catch , который может обрабатывать все типы выбрасываемых исключений:

try {
// ...
} catch (Exception ex) {
// ...
}

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

Слишком широкие обработчики исключений могут сделать ваш код более подверженным ошибкам, перехватывать непредвиденные исключения и вызывать непредвиденное поведение вашей программы.

Второй способ — реализовать несколько блоков catch:

try {
// ...
} catch (FileNotFoundException ex) {
// ...
} catch (EOFException ex) {
// ...
}

Обратите внимание, что если исключения имеют отношения наследования; дочерний тип должен стоять первым, а родительский тип — позже. Если мы этого не сделаем, это приведет к ошибке компиляции.

Третий — использовать блок multi-catch:

try {
// ...
} catch (FileNotFoundException | EOFException ex) {
// ...
}

Эта функция, впервые представленная в Java 7; уменьшает дублирование кода и упрощает его обслуживание.

Q5. В чем разница между проверенным и непроверенным исключением?

Проверяемое исключение должно быть обработано в блоке try-catch или объявлено в предложении throws ; тогда как непроверенное исключение не требуется ни обрабатывать, ни объявлять.

Проверенные и непроверенные исключения также известны как исключения времени компиляции и времени выполнения соответственно.

Все исключения являются проверенными исключениями, за исключением тех, на которые указывают Error , RuntimeException и их подклассы.

Q6. В чем разница между исключением и ошибкой?

Исключение — это событие, представляющее собой состояние, из которого можно выйти, тогда как ошибка представляет собой внешнюю ситуацию, из которой обычно невозможно выйти.

Все ошибки, выдаваемые JVM, являются экземплярами Error или одного из его подклассов, наиболее распространенные из которых включают, но не ограничиваются:

  • OutOfMemoryError — выдается, когда JVM не может выделить больше объектов из-за нехватки памяти, а сборщик мусора не смог сделать больше доступным.
  • StackOverflowError — возникает, когда пространство стека для потока исчерпано, как правило, из-за слишком глубокой рекурсии приложения.
  • ExceptionInInitializerError — сигнализирует о том, что во время оценки статического инициализатора произошло непредвиденное исключение.
  • NoClassDefFoundError — выдается, когда загрузчик классов пытается загрузить определение класса и не может его найти, обычно потому, что необходимые файлы классов не были найдены в пути к классам.
  • UnsupportedClassVersionError — возникает, когда JVM пытается прочитать файл класса и определяет, что версия в файле не поддерживается, обычно потому, что файл был создан с более новой версией Java.

Хотя ошибку можно обработать с помощью оператора try , это не рекомендуется, поскольку нет гарантии, что программа сможет что-либо надежно сделать после возникновения ошибки.

Q7. Какое исключение будет выдано при выполнении следующего блока кода?

Integer[][] ints = { { 1, 2, 3 }, { null }, { 7, 8, 9 } };
System.out.println("value = " + ints[1][1].intValue());

Он генерирует исключение ArrayIndexOutOfBoundsException , поскольку мы пытаемся получить доступ к позиции, превышающей длину массива.

Q8. Что такое цепочка исключений?

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

try {
task.readConfigFile();
} catch (FileNotFoundException ex) {
throw new TaskException("Could not perform task", ex);
}

Q9. Что такое трассировка стека и как она связана с исключением?

Трассировка стека предоставляет имена классов и методов, которые были вызваны, от запуска приложения до момента возникновения исключения.

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

Q10. Зачем вам подкласс исключения?

Если тип исключения не представлен теми, которые уже существуют на платформе Java, или если вам нужно предоставить дополнительную информацию клиентскому коду для более точной обработки, вам следует создать пользовательское исключение.

Решение о том, следует ли проверять или не проверять пользовательское исключение, полностью зависит от бизнес-кейса. Однако, как правило; если можно ожидать, что код, использующий ваше исключение, восстановится после него, создайте проверенное исключение, в противном случае сделайте его непроверенным.

Кроме того, вы должны наследовать от наиболее конкретного подкласса Exception , который тесно связан с тем, который вы хотите сгенерировать. Если такого класса нет, то выберите Exception в качестве родителя.

Q11. Каковы некоторые преимущества исключений?

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

Кроме того, поскольку JVM просматривает стек вызовов в обратном направлении, чтобы найти любые методы, заинтересованные в обработке конкретного исключения; мы получаем возможность распространять ошибку вверх по стеку вызовов без написания дополнительного кода.

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

Q12. Можете ли вы создать какое-либо исключение внутри тела лямбда-выражения?

При использовании стандартного функционального интерфейса, уже предоставленного Java, вы можете генерировать только непроверенные исключения, потому что стандартные функциональные интерфейсы не имеют предложения «throws» в сигнатурах методов:

List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> {
if (i == 0) {
throw new IllegalArgumentException("Zero not allowed");
}
System.out.println(Math.PI / i);
});

Однако, если вы используете пользовательский функциональный интерфейс, возможно создание проверенных исключений:

@FunctionalInterface
public static interface CheckedFunction<T> {
void apply(T t) throws Exception;
}
public void processTasks(
List<Task> taks, CheckedFunction<Task> checkedFunction) {
for (Task task : taks) {
try {
checkedFunction.apply(task);
} catch (Exception e) {
// ...
}
}
}

processTasks(taskList, t -> {
// ...
throw new Exception("Something happened");
});

Q13. Каким правилам нужно следовать при переопределении метода, выдающего исключение?

Несколько правил определяют, как должны объявляться исключения в контексте наследования.

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

Вот пример кода, чтобы продемонстрировать это:

class Parent {
void doSomething() {
// ...
}
}

class Child extends Parent {
void doSomething() throws IllegalArgumentException {
// ...
}
}

Следующий пример не скомпилируется, так как метод переопределения выдает проверенное исключение, не объявленное в переопределенном методе:

class Parent {
void doSomething() {
// ...
}
}

class Child extends Parent {
void doSomething() throws IOException {
// Compilation error
}
}

Когда метод родительского класса выдает одно или несколько проверенных исключений, метод дочернего класса может выдать любое непроверенное исключение; все, ни одно или подмножество объявленных проверенных исключений и даже большее их количество, если они имеют ту же область действия или более узкую.

Вот пример кода, который успешно следует предыдущему правилу:

class Parent {
void doSomething() throws IOException, ParseException {
// ...
}

void doSomethingElse() throws IOException {
// ...
}
}

class Child extends Parent {
void doSomething() throws IOException {
// ...
}

void doSomethingElse() throws FileNotFoundException, EOFException {
// ...
}
}

Обратите внимание, что оба метода соблюдают правило. Первый генерирует меньше исключений, чем переопределенный метод, а второй, хотя и генерирует больше; они уже по объему.

Однако, если мы попытаемся сгенерировать проверенное исключение, которое метод родительского класса не объявляет, или мы сгенерируем исключение с более широкой областью действия; мы получим ошибку компиляции:

class Parent {
void doSomething() throws FileNotFoundException {
// ...
}
}

class Child extends Parent {
void doSomething() throws IOException {
// Compilation error
}
}

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

Вот пример, который соблюдает правило:

class Parent {
void doSomething() throws IllegalArgumentException {
// ...
}
}

class Child extends Parent {
void doSomething()
throws ArithmeticException, BufferOverflowException {
// ...
}
}

Q14. Будет ли компилироваться следующий код?

void doSomething() {
// ...
throw new RuntimeException(new Exception("Chained Exception"));
}

Да. При цепочке исключений компилятор заботится только о первом в цепочке, и, поскольку он обнаруживает непроверенное исключение, нам не нужно добавлять предложение throws.

Q15. Есть ли способ генерировать проверенное исключение из метода, который не имеет пункта Throws?

Да. Мы можем воспользоваться стиранием типов, выполняемым компилятором, и заставить его думать, что мы выбрасываем непроверенное исключение, когда на самом деле; мы выбрасываем проверенное исключение:

public <T extends Throwable> T sneakyThrow(Throwable ex) throws T {
throw (T) ex;
}

public void methodWithoutThrows() {
this.<RuntimeException>sneakyThrow(new Exception("Checked Exception"));
}

3. Заключение

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

Мы в ForEach желаем вам успехов в любых предстоящих интервью.