Особенности обработки исключений

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


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

    public class Test01 {
       static int doTest() throws Exception {
        for (int i = 0; i < 10; i++) {
          System.out.println("i = " + i);
          if (i == 3) {
            throw new Exception();
          }
        }
        return -1;
      }
      public static void main(String[] args) throws Exception {
        System.out.println("doTest() = " + doTest());
      }
    }

    Здесь всё просто и интуитивно понятно.

    Исключения случаются, но мы их ловим:

    public class Test02 {
       static int doTest() {
        for (int i = 0; i < 10; i++) {
          System.out.println("i = " + i);
          try {
            if (i == 3) {
              throw new Exception();
            }
          } catch (Exception e) {
            System.out.println("Exception!");
            return i;
          } finally {
            System.out.println("Finally block");
          }
        }
        return -1;
      }
      public static void main(String[] args) {
        System.out.println("doTest() = " + doTest());
      }
    }

    Здесь тоже всё элементарно. Блок finally отрабатывает на каждой итерации, и после отлова исключения, то есть в принципе всегда, было исключение или его не было, вышли мы из метода, или нет.

    Исключения случаются, мы их ловим и… отменяем:

    public class Test03 {
      static int doTest(int n) {
        for (int i = 0; i < n; i++) {
          System.out.println("i = " + i);
          try {
            if (i % 3 == 0) {
              throw new Exception();
            }
          } catch (Exception e) {
            System.out.println("Exception!");
            return i;
          } finally {
            System.out.println("Finally block");
            if (i % 3 == 0) {
              if (i < 5) {
                System.out.println("Cancel exception, please");
                continue;
              } else {
                System.out.println("OK, now everything is done");
                return 42;
              }
            }
          }
        }
        return -1;
      }
      public static void main(String[] args) {
        System.out.println("doTest(2) = " + doTest(2));
        System.out.println();
        System.out.println("doTest(10) = " + doTest(10));
      }
    }


    А вот этот вариант показывает тот момент, на котором я засыпался месяца три тому на собеседовании:
    i = 0
    Exception!
    Finally block
    Cancel exception, please
    i = 1
    Finally block
    doTest(2) = -1

    i = 0
    Exception!
    Finally block
    Cancel exception, please
    i = 1
    Finally block
    i = 2
    Finally block
    i = 3
    Exception!
    Finally block
    Cancel exception, please
    i = 4
    Finally block
    i = 5
    Finally block
    i = 6
    Exception!
    Finally block
    OK, now everything is done
    doTest(10) = 42

    Блок finally исполняется всегда, кроме того, он может переопределить результат возврата из метода, либо отменить возврат.

    И даже с многопоточностью?

    import java.util.concurrent.*;
    public class Test04 implements Runnable {
      public void run() {
        try {
          for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread() + ": " + i);
            TimeUnit.SECONDS.sleep(1);
          }
        } catch (InterruptedException e) {
          System.out.println("Interrupted!");
        } finally {
          System.out.println("I'm in the finally block!");
        }
      }
      public static void main(String[] args) throws Exception {
        Thread t = new Thread(new Test04());
        t.start();
        TimeUnit.SECONDS.sleep(5);
        System.out.println("main() finished");
      }
    }

    Кажется, да:
    Thread[Thread-0,5,main]: 0
    Thread[Thread-0,5,main]: 1
    Thread[Thread-0,5,main]: 2
    Thread[Thread-0,5,main]: 3
    Thread[Thread-0,5,main]: 4
    main() finished
    Thread[Thread-0,5,main]: 5
    Thread[Thread-0,5,main]: 6
    Thread[Thread-0,5,main]: 7
    Thread[Thread-0,5,main]: 8
    Thread[Thread-0,5,main]: 9
    I'm in the finally block!


    А теперь — печеньки!

    import java.util.concurrent.*;
    public class Test05 implements Runnable {
      public void run() {
        try {
          for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread() + ": " + i);
            TimeUnit.SECONDS.sleep(1);
          }
        } catch (InterruptedException e) {
          System.out.println("Interrupted!");
        } finally {
          System.out.println("I'm in the finally block!");
        }
      }
      public static void main(String[] args) throws Exception {
        Thread t = new Thread(new Test05());
        // обратите внимание на следующую строчку
        t.setDaemon(true);
        t.start();
        TimeUnit.SECONDS.sleep(5);
        System.out.println("main() finished");
      }
    }

    Не работает!
    Thread[Thread-0,5,main]: 0
    Thread[Thread-0,5,main]: 1
    Thread[Thread-0,5,main]: 2
    Thread[Thread-0,5,main]: 3
    Thread[Thread-0,5,main]: 4
    main() finished
    Thread[Thread-0,5,main]: 5


    Даже нормальном завершении программы, в потоках типа «daemon» блок finally не отрабатывает.

    И да, как было замечено, при выходе из блока try-catch через System.exit(0) блок finally также не отрабатывает.
    up: вместо System.exit(int) можно использовать Runtime.getRuntime().halt(int)

    Мой оригинал
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 29

      +1
      А где же знаминитая потеря первопричины… если уж топик обзорный?
      } catch (Exception e) {
            throw new Exception();
      } 
      

      или
      } finally {
            throw new Exception();
      } 
      

        0
        На самом деле просто начал писать про заковырки с «finally», как-то не подумал :)
          +3
          во-первых, да, про finally { throw new Exception(); } это тоже хороший пример.
          а во-вторых, самого главного не написали, а именно — в какую степь должен послать интервьюируемый своего интервьюера, если тот пристанет к нему с таким примером.
            0
            Я тогда нубом был (хотя, сейчас я узнал ещё больше направлений, которые было бы интересно изучить) и вакансия была очень интересная
              +2
              Не, я не имею в виду вставать и уходить; но знать подобные моменты — чести не больше, чем писать нерасшифровываемые однострочники на перле. Использовать же это в коммерческом коде смерти подобно. Сообщить об этом интервьюеру, если он человек вменяемый и дело свое знает, значит заработать больше очков, чем просто четко ответив на поставленный вопрос «так какой эксепшен все-таки вылетит».
                +2
                Я иногда даю на собеседовании подобные (но уровнем ниже) задачки, чтобы выявлять фриков от программирования. Если человек с радостью отвечает и начинает рассуждать о хитрых тонких механизмах — его ждет полный цикл тестов на программистскую вменяемость. Если в ответ смотрит на меня как на не совсем адекватного человека и демонстрирует недовольство неуместным вопросом — тест на вменяемость отменяется, человек явно здоров.

                По крайней мере дважды это спасло мой проект — фриков радостно взяли соседи, потрясенные удивительной эрудицией. С соответствующимии последствиями.
              0
              Посылать не нужно. Нужно сказать, что «данный код требует тщательного анализа и встретив его в реальном проекте я бы вначале выяснил, что он должен делать согласно требованиям, а затем переписал в форме, не вызывающей вопросов. Если Вы желаете, я могу проделать это сейчас. Что должен был делать этот код?»

              Примерно так. :)
                +1
                Это и называется «посылать», если собеседуешься в занудную контору. :)
                  0
                  Посылать — это когда что-то неконструктивное в стиле «что за идиот у вас придумывает такие вопросы?»

                  А здесь — вполне конструктивный ответ. И по реакции на него будет видно, кого же на самом деле ищут на данную вакансию и есть ли далее смысл тратить время на интервью. Лично мне не случалось встать и уйти, но на моих собеседованиях это пару раз было.
          0
          Буквально вчера или позавчера об этом писали.
            +1
            Немножко не о том. Если не ошибаюсь, была статья о нетривиальных возможностях, где был упомянут один момент с finally (фокус с System.exit).
            Свою я написал независимо (несколько дней не заходя на хабр — проявлял силу воли :) ) и перед постингом, естественно читал хабр, чтобы не было дубля
          • UFO just landed and posted this here
              +1
              А еще Error от Exception не наследуется (о ужас?!). Будьте внимательны и осторожны.
              • UFO just landed and posted this here
              0
              По следам последнего примера.
              Ты уверен, что у тебя происходит «нормальный» выход из программы? Ведь не зря ты поток демоном назвал.
                0
                Вполне уверен. Все демон-потоки при завершении мгновенно и молча убиваются. Этот случай описан в «Thinking in java» в разделе Concurrency.
                  0
                  "… при завершении всех не-демон-потоков"
                    0
                    Я правильно понимаю, что поток t завершится/умрет как только завершится выполнение метода main(...)?
                      0
                      В приведённом примере — да.
                      Если же в приведённый код добавить запуск ещё одного потока, например t2, но для него не устанавливать флаг daemon, то оба потока продолжат работать после завершения main и до завершения t2.
                        0
                        Да, все вкурил javadoc.
                  0
                  Я не понял, в JAVA return не является точкой выхода?
                    0
                    Является. Но не всегда мгновенной и окончательной :)
                    Хотя, код с несколькими вложенными try-catch-finally с отменами и переопределениями return-ов у меня до сих пор с магией ассоциируется
                      0
                      То есть не является, а является чем-то вроде паскалевского := Точка выхода — это значит, что она мгновенная и окончательная.
                    0
                    Даже нормальном завершении программы, в потоках типа «daemon» блок finally не отрабатывает.

                    Ответ неверный. Все хуже: при завершении возможно частичное выполнение блоков finally в daemon-потоках. Если учесть, что оптимизатор имеет право переставлять инструкции при условии соблюдения семантики, то даже не факт, что будут исполнены именно первые операторы. :)
                      0
                      Мудрые книги рекомендуют использовать реализации ExecutorService и при завершении программы делать shutdownNow(). В таком случае можно поймать InterruptedException и корректно завершиться.
                      Собственно, пока я не видел тех случаев, где действительно требуется применение daemon-потоков, но, возможно всё ещё впереди.
                        0
                        Daemon-потоки предпочтительны именно в тех случаях, когда известно, что процесс допустимо прервать в любой момент и не требуется никакая очистка. Вариант с ExecutionService решает задачу в более общем виде, но требует, чтобы в какой-то точке мы приняли решение, что пора делать shutdown. Вариант с daemon-потоком позволяет получить это автоматически.

                        P.S. Я за последние 10 лет использовал daemon-потоки в реальном коде как максимум 2 или 3 раза. :)
                      0
                      return в finally это дурной тон
                        0
                        Но тем не менее компилятор его пропускает. Поэтому всегда найдётся тот, кто напишет такой код. И кто-то другой, кому этот код придётся понять и модифицировать.
                          0
                          IntelliJ IDEA отлавливает return в finally и предупреждает — так что пишущий такое должен заметить.

                      Only users with full accounts can post comments. Log in, please.