JavaRush /Java блог /Random /Разбор вопросов и ответов с собеседований на Java-разрабо...
Константин
36 уровень

Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 12

Статья из группы Random
Привет! Знания — сила. Чем больше знаний будет у вас к первому вашему собеседованию, тем увереннее на нем будете себя чувствовать. Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 12 - 1С хорошим багажом знаний вас будет сложно сбить с толку, и в то же время вы сможете приятно удивить вашего интервьюера. Поэтому сегодня мы без лишних предисловий продолжим усиливать вашу теоретическую базу, разбирая 250+ вопросов для Java-разработчика. Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 12 - 2

103. Какие есть правила для проверки исключений при наследовании?

Если я правильно понимаю вопрос, тут спрашивают про правила работы с исключениями при наследовании, и они таковы:
  • Переопределяемый или реализуемый метод в наследнике / реализации не может выбрасывать контролируемые исключения, которые выше по иерархии чем исключения в методе суперкласса / интерфейса.
То есть если у нас есть некий интерфейс Animal с методом, бросающим IOException:

public  interface Animal {
   void voice() throws IOException;
}
В реализации данного интерфейса мы не можем выставить более общее бросаемое исключение (например, Exception, Throwable), но можем заменить его на исключение наследника, как, например, на FileNotFoundException:

public class Cat implements Animal {
   @Override
   public void voice() throws FileNotFoundException {
// некоторая реализация
   }
}
  • Конструктор подкласса должен включить в свой блок throws все классы исключений пробрасываемым конструктором суперкласса, к которому идет обращение при создании объекта.
Предположим, что конструктор класса Animal бросает множество исключений:

 public class Animal {
   public Animal() throws ArithmeticException, NullPointerException, IOException {
   }
Тогда наследник класса в конструкторе также обязан их указать:

public class Cat extends Animal {
   public Cat() throws ArithmeticException, NullPointerException, IOException {
       super();
   }
Либо, как и в случае с методами — можно указать не те же исключения, а более общие. В нашем случае достаточно будет указать более общее исключение — Exception, так как это общий предок всех рассмотренных трех исключений:

public class Cat extends Animal {
   public Cat() throws Exception {
       super();
   }

104. Могли бы вы написать код, когда блок finally не будет выполнен?

Для начала вспомним, что же такое finally. Ранее мы рассматривали механизм перехвата исключений: блок try очерчивает зону перехвата, в то же время блок (блоки) catch — это код, который сработает при падении определенного исключения. Finally — третий блок кода после слова finally, который является взаимозаменяемым с catch, но не взаимоисключающим. Суть данного блока в том, что код в нем срабатывает всегда, независимо от результата срабатывания try или catch (независимо от того упало ли исключение или нет). Случаи его несрабатывания весьма редки и они являются нештатными. Самый простой случай несрабатывания — это когда в коде выше будет вызван метод System.exit(0), который завершает программу (тушит ее):

try {
   throw new IOException();
} catch (IOException e) {
   System.exit(0);
} finally {
   System.out.println("Данное сообщение не будет выведенно в консоль");
}
Также есть некоторые другие ситуации, в которых finally не сработает:
  • Аварийное завершение программы, вызванное критическими неполадками системы, либо падение некоторой Error, которая “уронит” приложение (примером error может стать та же StackOwerflowError возникающая при переполнении памяти стека).
  • Когда через блок ry…finally проходит deamon-поток и параллельно с этим программа завершается. Ведь deamon-поток — это поток для фоновых действий, то есть он не является приоритетным и обязательным и приложение не будет ждать окончания его работы.
  • Самый банальный бесконечный цикл, в try или catch, попав в который поток там останется навсегда:

    
    try {
       while (true) {
       }
    } finally {
       System.out.println("Данное сообщение не будет выведенно в консоль");
    }
    

Данный вопрос весьма популярен на собеседованиях новичков, поэтому парочку этих исключительных ситуаций стоит помнить. Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 12 - 3

105. Напишите пример обработки нескольких исключений в одном блоке catch

1) Возможно вопрос был задан некорректно. Насколько я понимаю в данном вопросе имеется в виду множество catch для одного блока try:

try {
  throw new FileNotFoundException();
} catch (FileNotFoundException e) {
   System.out.print("Упс, у вас упало исключение - " + e);
} catch (IOException e) {
   System.out.print("Упс, у вас упало исключение - " + e);
} catch (Exception e) {
   System.out.print("Упс, у вас упало исключение - " + e);
}
Если в блоке try падает исключение, то блоки catch поочередно сверху вниз пытаются его перехватить, если у некоторого блока catch это получается, он получает право на обработку исключения, в то время когда остальные блоки ниже уже не смогут попытаться перехватить и по своему обработать его. Поэтому, более узкие исключения ставятся в цепочке catch блоков выше, а более широкие — пониже. К примеру, если у нас в первом catch блоке отлавливается исключение класса Exception, то в остальные блоки проверяемые исключения попасть никак не смогут (остальные блоки с наследниками Exception будут абсолютно бесполезны). 2) Вопрос был задан корректно В таком случае наша обработка будет иметь вид вроде следующего:

try {
  throw new NullPointerException();
} catch (Exception e) {
   if (e instanceof FileNotFoundException) {
       // некоторая обработка с сужением типа (FileNotFoundException)e
   } else if (e instanceof ArithmeticException) {
       // некоторая обработка с сужением типа (ArithmeticException)e
   } else if(e instanceof NullPointerException) {
       // некоторая обработка с сужением типа (NullPointerException)e
   }
Словив исключение через catch, мы пытаемся выяснить его конкретный тип через метод instanceof, который используется для проверки — принадлежит ли объект определенному типу, чтобы потом без негативных последствий можно было сузить его до этого типа. Оба рассмотренных подхода могут применяться в одной и той же ситуации, но про некорректность вопроса я сказал из-за того, что второй вариант я бы не назвал хорошим и в своей практике ни разу не встречал, в то же время первый способ с мультикетчами получил широкое распространение. Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 12 - 4

106. Какой оператор позволяет принудительно выбросить исключение? Напишите пример

Выше я уже неоднократно использовал, тем не менее повторю это ключевое слово — throw. Пример использования (принудительная выброска исключения):

throw new NullPointerException();

107. Может метод main выбросить throws-исключение? Если да, то куда передаст?

В первую очередь хочу отметить, то что main это не более чем обычный метод, и да, он вызывается виртуальной машиной для начала выполнения программы, но кроме этого он может быть вызван и из любого другого кода. То есть на него также распространяются обычные правила указания выбрасываемых проверяемых исключений (checked exceptions) после throws:

public static void main(String[] args) throws IOException {
Соответственно в нем тоже могут падать исключения. Если main не был вызван в каком-то методе, а был запущен как точка запуска программы, то выброшенное им исключение будет обрабатываться перехватчиком .UncaughtExceptionHandler. Данный обработчик один на свой поток (то есть по одному обработчику в каждом потоке). При надобности можно создать свой обработчик и задать его с помощью метода setDefaultUncaughtExceptionHandler, вызванного у объекта Thread.

Многопоточность

Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 12 - 5

108. Какие средства для работы с многопоточностью знаете?

Основные/базовые средства для использования многопоточности в Java:
  • Synchronized — механизм закрытия (блокирования) метода / блока при вхождении в него потока, от других потоков.
  • Volatile — механизм обеспечения согласованного доступа к переменной разными потокам, то есть с наличием данного модификатора у переменной все операции присвоения и чтения ее должны быть атомарными. Другими словами потоки не будут копировать эту переменную в свою локальную память и изменять ее, а будут изменять ее изначальное значение.
Подробнее про volatile читайте вот тут.
  • Runnable — интерфейс, реализовав который (в частности его метод run) в некотором классе:

public class CustomRunnable implements Runnable {
   @Override
   public void run() {
       // некоторая логика
   }
}
И создав объект этого класса, можно запустить новый поток, задав этот объект в конструктор нового объекта Thread, и вызвав у него метод start():

Runnable runnable = new CustomRunnable();
new Thread(runnable).start();
Метод start запускает реализованный метод run() в отдельном потоке.
  • Thread — класс, наследовав от которого (при этом переопределив метод run):

public class CustomThread extends Thread {
   @Override
   public void run() {
       // некоторая логика
   }
}
И создав объект данного класса и запустив его с помощью метода start(), запустим тем самым новый поток:

new CustomThread().start();
  • Concurrency — пакет с инструментами для работы в многопоточной среде.
Он состоит из:
  • Concurrent Collections — набор коллекций, специализированных под работу в многопоточной среде.
  • Queues — специализированные очереди для многопоточной среды (блокирующие и неблокирующие).
  • Synchronisers — специализированные утилиты для работы в многопоточной среде.
  • Executors — механизмы для создания пулов потоков.
  • Locks — механизмы синхронизации потоков (более гибкие нежели стандартные — synchronized, wait, notify, notifyAll).
  • Atomics — классы оптимизированные под многопоточное выполнение, именно у них каждая операция — атомарная.
Подробнее о concurrent пакете читайте вот тут.

109. Расскажите о синхронизации между потоками. Для чего используют методы wait (), notify () — notifyAll () join ()?

Насколько я понял вопрос, синхронизация между потоками — это о ключевом модификаторе — synhronized. Данный модификатор можно поставить либо непосредственно у блока:

synchronized (Main.class) {
   // некоторая логика
}
Либо непосредственно в сигнатуре метода:

public synchronized void move() {
   // некоторая логика}
Как я и сказал ранее, synhronized это механизм, позволяющий закрывать блок / метод от других потоков, когда в него уже зашел один поток. Представим блок / метод как комнату. Некоторый поток, придя к ней, зайдет в нее и закроет ее на ключ, другие потоки, придя к комнате и увидев, что она закрыта, будут ждать возле нее пока она не освободится. Сделав свои дела, первый поток выходит из комнаты и освобождает ключ. И я не зря постоянно говорил о ключе, ведь он действительно существует. Это специальный объект, который имеет состояние занято / свободно. Этот объект прикреплен к каждому объекту Java, поэтому при использовании synhronized блока нам нужно в скобках указывать объект, на чей мьютекс мы хотим закрывать дверь:

Cat cat = new Cat();
synchronized (cat) {
   // некоторая логика
}
Также можно использовать мьютекс класса, как я это сделал в первом примере (Main.class). Когда мы используем synhronized на методе, мы ведь не указываем объект на которым хотим закрыть верно? В таком случае для нестатического метода будет закрываться на мьютекс объекта this, то есть текущий объект данного класса. Статический будет закрываться на мьютекс текущего класса (this.getClass();). Подробнее про мьютекс можно почитать вот тут. Ну а про synhronized читать вот тут. Wait() — метод, который освобождает мьютекс и переводит текущий поток в режим ожидания как бы прикрепляясь к текущему монитору (что-то вроде якоря). Из-за этого данный метод может быть вызван только из synhronized блока или метода (иначе что ему освобождать и чего ему ожидать). Также отмечу, что это метод класса Object. Точнее не один, а даже три:
  • Wait() — переводит текущий поток в режим ожидания пока другой поток не вызовет метод notify() или notifyAll() для этого объекта (об этих методах поговорим дальше).

  • Wait (long timeout) — переводит текущий поток в режим ожидания пока другой поток не вызовет метод notify() или notifyAll() для этого объекта или не истечет указанное время — timeout.

  • Wait (long timeout, int nanos) — аналогичен предыдущему, только nanos позволяет указать наносекунды (более точная настройка времени).

  • Notify() — метод позволяющий пробудить один рандомный поток текущего блока синхронизации. Опять же, может быть вызван только в synhronized блоке или методе (ведь в других местах ему не будет кого размораживать).

  • NotifyAll() — метод, пробуждающий все ожидающие потоки на текущем мониторе (тоже используется только в synhronized блоке или методе).

110. Как остановить поток?

Первое, что нужно сказать — это то, что при полном выполнении метода run() поток автоматически уничтожается. Но иногда нужно убить его досрочно, до завершения данного метода. И что тогда делать? Возможно, у объекта Thread использовать метод stop()? Как бы не так! Данный метод считается устаревшим и может привести к сбоям системы. Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 12 - 6Ну а что тогда? Есть два способа сделать это: Первый — это использование своего внутреннего логического флага. Давайте рассмотрим на примере. У нас есть своя реализация потока, который должен выводить на экран некую фразу до его полной остановки:

public class CustomThread extends Thread {
private boolean isActive;
 
   public CustomThread() {
       this.isActive = true;
   }
 
   @Override
   public void run() {
       {
           while (isActive) {
               System.out.println("Поток выполняет некую логику...");
           }
           System.out.println("Поток остановлен!");
       }
   }
 
   public void stopRunningThread() {
       isActive = false;
   }
}
При использовании метода stopRunning() внутренний флаг становится false и метод run прекращает свою работу. Давайте запустим его в main:

System.out.println("Начало выполнения программы");
CustomThread thread = new CustomThread();
thread.start();
Thread.sleep(3);
// пока наш основной поток спит, вспомогательный  CustomThread работает и выводит в коноль своё сообщение
thread.stopRunningThread();
System.out.println("Конец выполнения программы");
В итоге в консоли мы увидим что-то вроде этого:
Начало выполнения программы Поток выполняет некую логику... Поток выполняет некую логику... Поток выполняет некую логику... Поток выполняет некую логику... Поток выполняет некую логику... Поток выполняет некую логику... Конец выполнения программы Поток остановлен!
А это значит, что наш поток отработал, вывел некоторое количество сообщений в консоль и был успешно остановлен. Отмечу, что количество выведенных сообщений от запуска к запуску будет разным, иногда дополнительный поток даже и не выводил ничего. Как я заметил, это зависит от времени сна основного потока, чем оно больше, тем меньше шанс что дополнительный поток ничего не выведет. При времени сна 1мс вывода сообщений почти никогда не происходит, но если выставить 20мс, почти всегда срабатывает. Возможно, при малом времени поток просто не успевает запуститься и начать свою работу, а его сразу же останавливают. Второй способ заключается в использовании на объекте Thread метода interrupted(), который возвращает значение внутреннего флага прерывания (по умолчанию этот флаг false) и другого его метода interrupt() — ставящего этот флаг в true (когда этот флаг true поток должен остановить свою работу). Смотрим пример:

public class CustomThread extends Thread {
 
   @Override
   public void run() {
       {
           while (!Thread.interrupted()) {
               System.out.println("Поток выполняет некую логику...");
           }
           System.out.println("Поток остановлен!");
       }
   }
}
Запуск в main:

System.out.println("Начало выполнения программы");
Thread thread = new CustomThread();
thread.start();
Thread.sleep(3);
thread.interrupt();
System.out.println("Конец выполнения программы");
Итог выполнения будет таким же, как и в первом случае, но данный подход мне нравится больше: мы пишем меньше кода и больше используем уже готовый, стандатный функционал. Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 12 - 7На этом сегодня и остановимся.Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 12 - 8
Другие материалы серии:
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Mikhail Уровень 22
15 августа 2022
synchronized 8 раз написано как synhronized
Justinian Уровень 41 Master
19 июля 2021

105. Напишите пример обработки нескольких исключений в одном блоке catch
этот оператор появился в Java SE 7, очень удобный, на практике часто встречаю:

try {
   //some logic
  cargoService.execute(providers);
} catch (IllegalArgumentExeption | CargoServiceException e) {
    // log or rethrow
} catch (DaoException e) {
   //log or rethrow
}
удобный тем, что не нужно дублировать логику обработки эксепшенов, можно группировать операции, + на уровне языка этот оператор оптимизирован и генерирует меньше байткода и не дублирует код.