Что такое ошибка а что такое исключительная ситуация

Java_Deep_7.4-5020-83cb21.png

JavaSpec_Welcome_970x90-1801-439a19.png

В нашей жизни нередко возникают ситуации, которые мы не планировали. К примеру, пошли вы утром умываться и с досадой обнаружили, что отключили воду. Вышли на улицу, сели в машину, а она не заводится. Позвонили другу, а он недоступен. И так далее и тому подобное… В большинстве случаев человек без труда справится с подобными проблемами. А вот как с непредвиденными ситуациями справляется Java, мы сейчас и поговорим.

Что называют исключением. Исключения в мире программирования

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

При этом в отличие от «человеческого мира», программное приложение должно чётко понимать, как поступать в подобной ситуации. И вот как раз для этого в Java и существует механизм исключений (exception).

Используемые ключевые слова

При обработке исключений в Java применяются следующие ключевые слова:
try – служит для определения блока кода, в котором может произойти исключение;
catch – необходим для определения блока кода, где происходит обработка исключения;
finally – применяется для определения блока кода, являющегося необязательным, однако при его наличии он выполняется в любом случае вне зависимости от результата выполнения блока try.

Вышеперечисленные ключевые слова необходимы для создания в коде ряда специальных обрабатывающих конструкций: try{}finally{}, try{}catch, try{}catch{}finally.

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

Давайте на примере посмотрим, как используются ключевые слова в Java-программе:

//метод считывает строку с клавиатуры

public String input() throws MyException {//предупреждаем с помощью throws,
// что метод может выбросить исключение MyException
      BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    String s = null;
//в блок try заключаем код, в котором может произойти исключение, в данном
// случае компилятор нам подсказывает, что метод readLine() класса
// BufferedReader может выбросить исключение ввода/вывода
    try {
        s = reader.readLine();
// в блок  catch заключаем код по обработке исключения IOException
    } catch (IOException e) {
        System.out.println(e.getMessage());
// в блоке finally закрываем поток чтения
    } finally {
// при закрытии потока тоже возможно исключение, например, если он не был открыт, поэтому “оборачиваем” код в блок try
        try {
            reader.close();
// пишем обработку исключения при закрытии потока чтения
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }

    if (s.equals("")) {
// мы решили, что пустая строка может нарушить в дальнейшем работу нашей программы, например, на результате этого метода нам надо вызывать метод substring(1,2), поэтому мы вынуждены прервать выполнение программы с генерацией своего типа исключения MyException с помощью throw
        throw new MyException("String can not be empty!");
    }
    return s;
}

Зачем нам механизм исключений?

Для понимания опять приведём пример из обычного мира. Представьте, что на какой-нибудь автодороге имеется участок с аварийным мостом, на котором ограничена грузоподъёмность. И если по такому мосту проедет грузовик со слишком большой массой, мост разрушится, а в момент этого ЧП ситуация для шофёра станет, мягко говоря, исключительной. И вот, дабы такого не произошло, дорожные службы заранее устанавливают на дороге соответствующие предупреждающие знаки. И тогда водитель, посмотрев на знак, сравнит массу своего авто со значением разрешённой грузоподъёмности и примет соответствующее решение, например, поедет по другой дороге.

То есть мы видим, что из-за правильных действий дорожной службы шоферы крупногабаритных транспортных средств:
1) получили возможность заранее изменить свой путь;
2) были предупреждены об опасности;
3) были предупреждены о невозможности проезжать по мосту при определённых условиях.

Вот как наш жизненный пример соотносится с применением исключения на Java:

1-20219-9bd18c.jpg

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

Что же, давайте ещё раз побудем дорожной службой. Чтобы установить знак, мы ведь должны знать места, где водителей ТС могут ждать различные неприятности. Это первое. Далее, нам ведь надо заготовить и установить знаки. Это второе. И, наконец, надо предусмотреть маршруты объезда, позволяющие избежать опасности.

В общем, механизм исключений в Java работает схожим образом. На стадии разработки программы мы выполняем «ограждение» опасных участков кода в отношении наших исключений, используя блок try{}. Чтобы предусмотреть запасные пути, применяем блок catch{}. Код, выполняемый в программе при любом исходе, пишем в блоке finally{}.

Иногда бывает, что мы не можем предусмотреть «запасной аэродром» либо специально желаем предоставить право его выбора юзеру. Но всё равно мы должны как минимум предупредить пользователя об опасности. Иначе он превратится в разъярённого шофёра, который ехал долго, не встретил ни одного предупреждающего знака и в итоге добрался до аварийного моста, проехать по которому не представляется возможным.

Что касается программирования на Java, то мы, когда пишем свои классы и методы, далеко не всегда можем предвидеть контекст их применения другими программистами в своих программах, а значит, не можем со стопроцентной вероятностью предвидеть правильный путь для разрешения исключительных ситуаций. Но предупредить коллег о возможной исключительной ситуации мы всё-таки должны, и это не что иное, как правило хорошего тона.

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

Предупреждаем о неприятностях

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

После упоминания ключевого слова throws мы указываем тип исключения. Как правило, речь идёт о наследниках класса Exception Java. Но так как Java — это объектно-ориентированный язык программирования, все исключения представляют собой объекты.

2-20219-ee1e82.jpg

Иерархия исключений в Java

Когда возникают ошибки при выполнении программы, исполняющая среда Java Virtual Machine обеспечивает создание объекта нужного типа, используя иерархию исключений Java — речь идёт о множестве возможных исключительных ситуаций, которые унаследованы от класса Throwable — общего предка. При этом исключительные ситуации, которые возникают в программе, делят на 2 группы:
1. Ситуации, при которых восстановление нормальной дальнейшей работы невозможно.
2. Ситуации с возможностью восстановления.

К первой группе можно отнести случаи, при которых возникают исключения, которые унаследованы из класса Error. Это ошибки, возникающие во время выполнения программы при сбое работы Java Virtual Machine, переполнении памяти либо сбое системы. Как правило, такие ошибки говорят о серьёзных проблемах, устранение которых программными средствами невозможно. Данный вид исключений в Java относят к неконтролируемым исключениям на стадии компиляции (unchecked). К этой же группе относятся и исключения-наследники класса Exception, генерируемые Java Virtual Machine в процессе выполнения программы — RuntimeException. Данные исключения тоже считаются unchecked на стадии компиляции, а значит, написание кода по их обработке необязательно.

Что касается второй группы, то к ней относят ситуации, которые можно предвидеть ещё на стадии написания приложения, поэтому для них код обработки должен быть написан. Это контролируемые исключения (checked). И в большинстве случаев Java-разработчики работают именно с этими исключениями, выполняя их обработку.

Создание исключения

В процессе исполнения программы исключение генерируется Java Virtual Machine либо вручную посредством оператора throw. В таком случае в памяти происходит создание объекта исключения, выполнение основного кода прерывается, а встроенный в JVM обработчик исключений пробует найти способ обработать это самое исключение.

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

Обработка исключений в Java подразумевает создание блоков кода и производится в программе посредством конструкций try{}finally{}, try{}catch, try{}catch{}finally.

3-20219-4ec690.jpg

В процессе возбуждения исключения в try обработчик исключения ищется в блоке catch, который следует за try. При этом если в catch присутствует обработчик данного вида исключения, происходит передача управления ему. Если же нет, JVM осуществляет поиск обработчика данного типа исключения, используя для этого цепочку вызова методов. И так происходит до тех пор, пока не находится подходящий catch. После того, как блок catch выполнится, управление переходит в необязательный блок finally. Если подходящий блок catch найден не будет, Java Virtual Machine остановит выполнение программы, выведя стек вызовов методов под названием stack trace. Причём перед этим выполнится код блока finally при наличии такового.

Рассмотрим практический пример обработки исключений:

public class Print {

     void print(String s) {
        if (s == null) {
            throw new NullPointerException("Exception: s is null!");
        }
        System.out.println("Inside method print: " + s);
    }

    public static void main(String[] args) {
        Print print = new Print();
        List list= Arrays.asList("first step", null, "second step");

        for (String s:list) {
            try {
                print.print(s);
            }
            catch (NullPointerException e) {
                System.out.println(e.getMessage());
                System.out.println("Exception was processed. Program continues");
            }
            finally {
                System.out.println("Inside bloсk finally");
            }
            System.out.println("Go program....");
            System.out.println("-----------------");
        }

    }
    }

А теперь глянем на результаты работы метода main:

Inside method print: first step
Inside bloсk finally
Go program....
-----------------
Exception: s is null!
Exception was processed. Program continues
Inside bloсk finally
Go program....
-----------------
Inside method print: second step
Inside bloсk finally
Go program....
-----------------

Блок finally чаще всего используют, чтобы закрыть открытые в try потоки либо освободить ресурсы. Но при написании программы уследить за закрытием всех ресурсов возможно не всегда. Чтобы облегчить жизнь разработчикам Java, была предложена конструкция try-with-resources, автоматически закрывающая ресурсы, открытые в try. Используя try-with-resources, мы можем переписать наш первый пример следующим образом:

public String input() throws MyException {
    String s = null;
    try(BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))){
        s = reader.readLine();
   } catch (IOException e) {
       System.out.println(e.getMessage());
   }
    if (s.equals("")){
        throw new MyException ("String can not be empty!");
    }
    return s;
}

А благодаря появившимся возможностям Java начиная с седьмой версии, мы можем ещё и объединять в одном блоке перехват разнотипных исключений, делая код компактнее и читабельнее:

public String input() {
    String s = null;
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
        s = reader.readLine();
        if (s.equals("")) {
            throw new MyException("String can not be empty!");
        }
    } catch (IOException | MyException e) {
        System.out.println(e.getMessage());
    }
    return s;
}

Итоги

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

Основные вопросы об исключениях в Java

1.Что такое проверяемые и непроверяемые исключения?
Если говорить коротко, то первые должны быть явно пойманы в теле метода либо объявлены в секции throws метода. Вторые вызываются проблемами, которые не могут быть решены. Например, это нулевой указатель или деление на ноль. Проверяемые исключения очень важны, ведь от других программистов, использующих ваш API, вы ожидаете, что они знают, как обращаться с исключениями. К примеру, наиболее часто встречаемое проверяемое исключение — IOException, непроверяемое — RuntimeException.
2.Почему переменные, определённые в try, нельзя использовать в catch либо finally?
Давайте посмотрим на нижеследующий код. Обратите внимание, что строку s, которая объявлена в блоке try, нельзя применять в блоке catch. То есть данный код не скомпилируется.

try {
    File file = new File("path");
    FileInputStream fis = new FileInputStream(file);
    String s = "inside";
} catch (FileNotFoundException e) {
    e.printStackTrace();
    System.out.println(s);
}

А всё потому, что неизвестно, где конкретно в try могло быть вызвано исключение. Вполне вероятно, что оно было вызвано до объявления объекта.
3.Почему Integer.parseInt(null) и Double.parseDouble(null) вызывают разные исключения?
Это проблема JDK. Так как они были разработаны разными людьми, то заморачиваться вам над этим не стоит:

Integer.parseInt(null);
// вызывает java.lang.NumberFormatException: null

Double.parseDouble(null);
// вызывает java.lang.NullPointerException

4.Каковы основные runtime exceptions в Java?
Вот лишь некоторые из них:

IllegalArgumentException
ArrayIndexOutOfBoundsException

Их можно задействовать в операторе if, если условие не выполняется:

if (obj == null) {
   throw new IllegalArgumentException("obj не может быть равно null");

5.Возможно ли поймать в одном блоке catch несколько исключений?
Вполне. Пока классы данных исключений можно отследить вверх по иерархии наследования классов до одного и того же суперкласса, возможно применение только этого суперкласса.
6.Способен ли конструктор вызывать исключения?
Способен, ведь конструктор — это лишь особый вид метода.

class FileReader{
    public FileInputStream fis = null;

    public FileReader() throws IOException{
        File dir = new File(".");//get current directory
        File fin = new File(dir.getCanonicalPath() + File.separator + "not-existing-file.txt");
        fis = new FileInputStream(fin);
    }
}

7.Возможен ли вызов исключений в final?
В принципе, можете сделать таким образом:

public static void main(String[] args) {
    File file1 = new File("path1");
    File file2 = new File("path2");
    try {

        FileInputStream fis = new FileInputStream(file1);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally {
        try {
            FileInputStream fis = new FileInputStream(file2);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Но если желаете сохранить читабельность, объявите вложенный блок try-catch в качестве нового метода и вставьте вызов данного метода в блок finally.

finally. 
public static void main(String[] args) {
    File file1 = new File("path1");
    File file2 = new File("path2");
    try {

        FileInputStream fis = new FileInputStream(file1);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally {
        methodThrowException();
    }
}

JavaSpec_Welcome_970x90-1801-439a19.png


Содержание

  • 1. Что такое исключительная ситуация?
  • 2. Понятие исключения в языке Java
  • 3. Какими способами могут генерироваться исключения?
  • 4. В каких случаях вызывается стандартный обработчик исключений Java?
  • 5. Какие ключевые слова используются для обработки исключений в Java?
  • 6. Какое назначение конструкции try…catch…finally? Общая форма
  • 7. Пример генерирования исключения и его перехват стандартным обработчиком исключений Java
  • 8. Пример перехвата и обработки исключения с помощью оператора try…catch
  • 9. Пример перехвата и обработки исключения блоком try…catch…finally. Демонстрация работы блока операторов finally
  • 10. Как реализовать вывод описания исключения? Пример
  • 11. Пример перехвата нескольких исключительных ситуаций (несколько операторов catch)
  • Связанные темы

Поиск на других ресурсах:

1. Что такое исключительная ситуация?

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

  • попытки деления на нуль
  • попытки обращения к элементу массива, номер индекса которого выходит за пределы объявленного;
  • попытки взять корень из отрицательного числа;
  • попытки открыть файл по имени, которого нет на диске;
  • другие случаи.

Правильная обработка исключений есть важным элементом написания профессиональных программ на Java.

 

2. Понятие исключения в языке Java

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

В языке программирования Java, исключение – это специальный объект, описывающий исключительную ситуацию, которая возникла в некоторой части программного кода. Объект представляющий исключение, генерируется в момент возникновения исключительной ситуации. После возникновения критической (исключительной) ситуации исключение перехватывается и обрабатывается. Таким образом, возникает понятие генерирования исключения. Генерирование исключения – процесс создания объекта, который описывает данное исключение.

 

3. Какими способами могут генерироваться исключения?

В языке Java исключения могут генерироваться одним из двух способов:

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

 

4. В каких случаях вызывается стандартный обработчик исключений Java?

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

  • не использует блок try…catch для обработки и перехвата исключительной ситуации вообще;
  • содержит блок try…catch, однако в этом блоке данный тип исключения не перехватывается.

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

 



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

Для обработки исключений в Java используются следующие ключевые слова:

  • try;
  • catch;
  • throw;
  • throws;
  • finally.

 

6. Какое назначение конструкции try… catch…finally? Общая форма

В языке Java можно вручную перехватывать исключения (исключительные ситуации) и соответствующим образом обрабатывать их. Важно, чтобы при возникновении исключительной ситуации программа не прекратила свое выполнение.

Для отслеживания, генерирования и обработки исключений в языке программирования Java используется конструкция try…catch…finally, которая имеет следующую общую форму

try {
    // блок кода, в котором отслеживаются и генерируются исключения
    // ...
}
catch (type_exception_1 objEx1) {
    // обработчик исключения типа type_exception_1
    // ...
}
catch (type_exception_2 objEx2) {
    // обработчик исключения типа type_exception_2
    // ...
}

...

catch (type_exception_N objEx) {
    // обработчик исключения типа type_exception_N
    // ...
}
finally {
    // блок кода, который должен быть обязательно выполнен
    // после завершения блока try
    // ...
}

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

В блоке catch размещается программный код, который обрабатывает перехваченное исключение (обработчик исключения). Код в блоке catch реализует выполнение соответствующих действий, когда произошла исключительная ситуация в блоке try.   Блоков catch может быть несколько. Если генерируется исключение, механизм обработки исключений ищет первый из обработчиков исключений, аргумент которого соответствует текущему типу исключения. После этого он входит в блок catch, и, в результате исключение считается обработанным. Только один соответствующий блок catch обрабатывается.

В блоке finally указывается код, который должен быть обязательно выполнен после завершения блока try. Блок операторов finally выполняется независимо от того, будет ли сгенерировано исключение или нет. Если исключение сгенерировано, блок операторов finally выполняется даже при условии, что ни один из операторов catch не совпадает с этим исключением.

Операторы try и catch составляют единое целое. Оператор finally может отсутствовать.

 

7. Пример генерирования исключения и его перехват стандартным обработчиком исключений Java

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

Объявляется класс DemoExceptions. С целью демонстрации в классе объявляется метод DivNumbers(). Метод возвращает число типа double, которое есть результатом деления двух целых значений переменных a и b. Эти переменные есть входными параметрами метода.

Если b = 0 то при делении a на b может возникнуть исключительная ситуация «деление на нуль» типа ArithmeticException. В программе нет кода, который явным образом перехватывает и обрабатывает эту ситуацию. Поэтому, вызывается стандартный обработчик Java.

Текст класса DemoExceptions следующий:

// класс реализующий метод,
// в котором может возникнуть исключительная ситуация
class DemoExceptions {

    // метод, в котором может возникнуть деление на 0
    double DivNumbers(int a, int b) {

        // при делении на 0 будет вызван стандартный обработчик Java
        double res;
        res = a/b;
        return res;
    }
}

Если в другом классе использовать метод DivNumbers() следующим образом

public class Train01 {

    public static void main(String[] args) {
        DemoExceptions d = new DemoExceptions(); // создать экземпляр класса
        double res;

        // вызвать метод DivNumbers()
        res = d.DivNumbers(2, 0); // 2/0 => деление на ноль
        System.out.println("res = " + res);
    }
}

то будет вызванный стандартный обработчик Java, который выведет следующее сообщение

Exception in thread "main" java.lang.ArithmeticException: / by zero
at DemoExceptions.DivNumbers(Train01.java:10)
at Train01.main(Train01.java:82)

В сообщении указывается тип класса ArithmeticException. Это подкласс производный от класса RunTimeException. Этот подкласс описывает арифметические ошибки «деление на 0», «взятие корня из отрицательного числа» и т.п..

 

8. Пример перехвата и обработки исключения с помощью оператора try…catch

В примере реализован класс DemoExceptions, в котором объявляется метод с именем DivNumbers2(). Метод DivNumbers2() делит входящий параметр a на входящий параметр b. В методе DivNumbers2() продемонстрирован перехват исключения деления на ноль с помощью блока try…catch.

Текст класса DemoExceptions следующий

// класс реализующий метод,
// в котором может возникнуть исключительная ситуация
class DemoExceptions {

    // метод, в котором обработано деление на 0,
    // блок try..catch
    double DivNumbers2(int a, int b) {
        double res=0; // переменная res обязательно должна быть инициализирована

        try {
            res = a/b; // если b=0, то генерируется исключение
        }
        catch (ArithmeticException e) {
            System.out.println("Деление на 0.");
        }

        return res;
    }
}

Теперь, при использовании метода DivNumbers2() в другом классе

public class Train01 {

    public static void main(String[] args) {
        DemoExceptions d = new DemoExceptions(); // создать экземпляр класса
        double res;

        // вызвать метод DivNumbers2()
        res = d.DivNumbers2(2, 0); // 2/0 => деление на ноль

        System.out.println("res = " + res);
    }
}

система выдаст следующий результат после запуска программы на выполнение

Деление на 0.
res = 0.0

Как видно из вышеприведенного кода, в методе DivNumbers2() будет вызван собственный обработчик, который реализован в операторе try…catch. В блок try помещается программный код, который нужно проверить

...
res = a/b;
...

Если возникает исключение (b=0), то управление сразу передается из блока try в блок catch. В блоке catch происходит обработка исключения с выводом соответствующего сообщения

...
catch (ArithmeticException e) {
    System.out.println("Деление на 0.");
}
...

Итак, если b=0:

  • генерируется исключение типа ArithmeticException;
  • происходит переход к обработке исключения (блок catch);
  • в блоке catch выводится соответствующее сообщение;
  • после блока catch выполнение программы не останавливается. В результате переменная res получает значение 0.

Можно изменить вывод собственного сообщения об исключении на стандартное сообщение. В этом случае нужно изменить текст в блоке catch на следующий

...
catch (ArithmeticException e) {
    System.out.println(e);
    //System.out.println("Деление на 0.");
}
...

После изменений, результат выполнения программы будет такой

java.lang.ArithmeticException: / by zero
res = 0.0

 

9. Пример перехвата и обработки исключения блоком try…catch…finally. Демонстрация работы блока операторов finally

В примере продолжено развитие двух предшествующих пунктов 7, 8. Пример демонстрирует назначение блока finally в операторе try…catch…finally.

Объявляется класс DemoExceptions. В классе продемонстрирован перехват исключительной ситуации деления на 0. Этот перехват реализован в методе DivNumbers3() класса. Для перехвата и обработки исключительной ситуации, метод DivNumbers3() использует оператор try…catch…finally.

Текст класса DemoExceptions следующий

// класс, который реализует метод,
// в котором может возникнуть исключительная ситуация
class DemoExceptions {

    // метод, в котором обработано деление на 0,
    // блок try..catch..finally
    double DivNumbers3(int a, int b) {
        double res; // не обязательно инициализировать, поскольку есть блок finally
        try {
            res = a/b;
        }
        catch (ArithmeticException e) {
            System.out.println("Деление на 0.");
        }
        finally {
            res = 0; // вызовется обязательно
        }
        return res;
    }
}

В методе DivNumbers3() объявляется переменная res, что есть результатом деления a на b. Поскольку оператор try…catch содержит блок finally, то строка

...
finally {
    res = 0; // вызовется обязательно
}
...

будет вызываться всегда. И перед оператором

...
return res;
...

переменная res будет всегда инициализирована. Поэтому компилятор Java не требует инициализации переменной в начале кода. Как известно, чтобы возвратить значение переменной оператором return, эта переменная должна быть обязательно инициализирована. Итак, блок finally вызывается всегда.

Использование метода DivNumbers3() в другом классе может быть следующим

public class Train01 {

    public static void main(String[] args) {
        DemoExceptions d = new DemoExceptions();
        double res;
        res = d.DivNumbers3(2, 0); // 2/0 => деление на ноль
        System.out.println("res = " + res);
    }
}

Результат выполнения программы

Деление на 0.
res = 0.0

 

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

Если нужно вывести описание исключения e, то можно изменить вывод в обработчике catch следующим образом (см. пункт 8)

...
catch (ArithmeticException e) {
    // перехватить ошибку деления на 0
    System.out.println("Исключение: " + e);
}
...

В результате программа будет выводить следующий результат

Исключение: java.lang.ArithmeticException: / by zero

 



11. Пример перехвата нескольких исключительных ситуаций (несколько операторов catch)

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

Реализация класса DivArrays следующая

// несколько операторов catch
// класс, который делит поэлементно массивы
class DivArrays {
    double[] A1;
    double[] A2;

    // конструктор класса
    DivArrays(int n1, double[] _A1, int n2, double[] _A2) {
        // создать массивы
        A1 = new double[n1];
        for (int i=0; i<n1; i++)
            A1[i] = _A1[i];

        A2 = new double[n2];
        for (int i=0; i<n2; i++)
            A2[i] = _A2[i];
    }

    // метод, который делит поэлементно массив A1 на массив A2
    // метод возвращает массив типа double
    double[] Division() {
        double[] A3;
        int n3;

        // установить наибольший из размеров
        if (A1.length > A2.length)
            n3 = A1.length;
        else
            n3 = A2.length;

        A3 = new double[n3];

        // цикл по i, в цикле обрабатывается исключительная ситуация
        for (int i=0; i<n3; i++) {
            try {
                // сгенерировать исключение, если деление на 0
                if (A2[i]==0.0)
                    throw new ArithmeticException();
                A3[i] = A1[i]/A2[i];
            }
            catch (ArithmeticException e) {
                // перехватить деление на 0
                A3[i] = 0; // просто установить в A3 значение 0
            }
            catch (ArrayIndexOutOfBoundsException e) {
                // перехватить выход индекса за границы массива
                // это случай, когда длины массивов отличаются
                A3[i] = -1;
            }
        }
        return A3;
    }
}

Использование класса в другом программном коде

public class Train03 {

    public static void main(String[] args) {
        double[] A1 = { 2.5, 2.0, 1.5, 0.5 };
        double[] A2 = { 1.0, 0.0, 2.0 };
        double[] A3;

        DivArrays dA = new DivArrays(4,A1,3,A2);

        A3 = dA.Division();

        // вывести массив d
        for (int i=0; i<A3.length; i++)
            System.out.println("A[" + i + "] = " + A3[i]);
    }
}

Результат выполнения программы

A[0] = 2.5
A[1] = 0.0
A[2] = 0.75
A[3] = -1.0

 


Связанные темы

  • Операторы throw, throws. Примеры
  • Классы Java для обработки исключительных ситуаций из пакета java.lang. Методы класса ThrowableПримеры
  • Класс Exception. Создание собственных классов исключений. Примеры

 


Эта статья посвящается очень важному вопросу программирования — исключительным ситуациям и ошибкам (exceptions and errors).

В языке Java исключения (Exceptions) и ошибки (Errors) являются объектами. Когда метод вызывает, еще говорят «бросает» от слова «throws», исключительную ситуацию, он на самом деле работает с объектом. Но такое происходит не с любыми объектами, а только с теми, которые наследуются от Throwable.

Упрощенную диаграмму классов ошибок и исключительний вы можете увидеть на следующем рисунке:

Иерархия Exceptions и Errors

RuntimeException, Error и их наследников еще называют unchecked exception, а всех остальных наследников класса Exception — checked exception.

Checked Exception обязывает пользователя обработать ее (использую конструкцию try-catch) или же отдать на откуп обрамляющим методам, в таком случае к декларации метода, который бросает проверяемое (checked) исключение, дописывают конструкцию throws, например:

    public Date parse(String source) throws ParseException { ... }

К unchecked исключениям относятся, например, NullPointerException, ArrayIndexOutOfBoundsException, ClassCastExcpetion и так далее. Это те ошибки, которые могут возникнут практически в любом методе. Несомненно, описывать каждый метод как тот, который бросает все эти исключения, было бы глупо.

1. Так когда же нужно бросать ошибки?. На этот вопрос можно ответить просто: если в методе возможна ситуация, которую метод не в состоянии обработать самостоятельно, он должен «бросать» ошибку. Но ни в коем случае нельзя использовать исключительные ситуации для управления ходом выполнения программы.

Чаще всего Exceptions бросаются при нарушении контракта метода. Контракт (contract) — это негласное соглашение между создателем метода (метод сделает и/или вернет именно то, что надо) и пользователем метода (на вход метода будут передаваться значения из множества допустимых).

Нарушение контракта со стороны создателя метода — это, например, что-нибудь на подобии MethodNotImplementedYetException :).

Пользователь метода может нарушить контракт, например, таким способом: на вход Integer.parseInt(String) подать строку с буквами и по заслугам получить NumberFormatException.

Часто при реализации веб-сервисов первыми строками методов я пишу конструкции вида:

    public Contract getContractById(String id) {
        if (id == null) throw new NullPointerException("id is null");
        ...
    }

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

2. А что собственно бросать?. Выбор не то чтобы сильно велик, но и не однозначен: checked, unchecked (runtime), unchecked (error).

Сразу скажу, в подавляющем большинстве случаев Error вам не понадобится. Это в основном критические ошибки (например, StackOverflowError), с которыми пусть работает JVM.

Checked Exceptions, как было написано выше, заставляет программиста-пользователя написать код для ее обработки или же описать метод как «вызывающий исключительную ситуацию».

С unchecked exception можно поступить по-разному. В случае с такими ошибками, пользователь сам решает, будет он обрабатывать эту ошибку, или же нет (компилятор не заставляет это делать).

Можно написать следующее простое правило: если некоторый набор входящих в метод данных может привести к нарушению контракта, и вы считаете, что программисту-пользователю важно разобраться с этим (и что он сможет это сделать), описывайте метод с конструкцией throws, иначе бросайте unchecked exception.

3. Ну и как это обрабатывать? Обрабатывать ошибку лучше там, где она возникла. Если в данном фрагменте кода нет возможности принять решение, что делать с исключением, его нужно бросать дальше, пока не найдется нужный обработчик, либо поток выполнения программы не вылетит совсем.

Вы наверняка знаете, что обработка исключений происходит с помощью блока try-catch-finally. Сразу скажу вам такую вещь: никогда не используйте пустой catch блок!. Выглядит этот ужас так:

  try { ... } 
  catch(Exception e) { }

Если Вы уверены, что исключения в блоке try не возникнет никогда, напишите комментарий, как например в этом фрагменте кода:

        StringReader reader = new StringReader("qwerty");
        try { reader.read(); } 
        catch (IOException e) { /* cannot happen */ }

Если же исключение в принципе может возникнуть, но только действительно в «исключительной ситуации» когда с ним ничего уже сделать будет нельзя, лучше оберните его в RuntimeException и пробросьте наверх, например:

        String siteUrl = ...;
        ...
        URL url;
        try { url = new URL(siteUrl); }
        catch (MalformedURLException e) { 
            throw new RuntimeException(e); 
        }

Скорее всего ошибка здесь может возникнуть только при неправильной конфигурации приложения, например, siteUrl читается из конфигурационного файла и кто-то допустил опечатку при указании адреса сайта. Без исправления конфига и перезапуска приложения тут ничего поделать нельзя, так что RuntimeException вполне оправдан.

4. Зачем нужно все это делать? А почему бы и нет :). Если серьезно — правильное использование Exceptions и корректная их обработка сделают код более понятным, гибким, структурированным и возможным для повторного использования.

Updated 28.08.2009: Хочу показать вам несколько интересных моментов, которые касаются исключений и блоков try-catch-finally.

Можно ли сделать так, чтобы блок finally не выполнился? Можно:

public class Test1 {
    public static void main(String[] args) {
        try {
            throw new RuntimeException();
        } catch (Exception e) {
            System.exit(0);
        } finally {
            System.out.println("Please, let me print this.");
        }
    }
}

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

public class Test {
    public static void main(String[] args) {
        try {
            throw new NullPointerException();
        } catch (NullPointerException e) {
            throw e;
        } finally {
            throw new IllegalStateException();
        }
    }
}

Правильный ответ — IllegalStateException. Не смотря на то, что в блоке catch происходит повторный «выброс» NPE, после него выполняется блок finally, который перехватывает ход выполнения программы и бросает исключение IllegalStateException.

Жду ваших вопросов и комментариев.


Подборка по базе: Что такое философия.docx, Бывает и такое.docx, Что такое волатильность опционов.docx, Что такое сердечная недостаточность.docx, Что такое Родина.docx, Что такое счастье(по рассказу А. В. Вампилова Последняя просьба., Что такое радиация.docx, Что такое экосистема Сбера.docx, что такое эргономика.docx, что такое родина.doc


1) Что такое исключение.

2) Иерархия исключений.

3) Как создать/бросить/поймать исключение.

4) В чем разница между checked и unchecked исключениями

5) Что такое стектрейс. Какую информацию из него можно получить?

6) блок finaly

7) Конструкция try-catch-with-resource.

8) Что такое логгирование и для чего используется.

1) Что такое исключение

Событие, которое возникает во время выполнения программы и прерывает нормальный поток выполнения инструкций. Причины возникновения — Пользователь ввел недопустимые данные / Файл не найден / Потеряно соединение.
Зачем нужны исключения?

Исключения используются для обработки ошибок и других особых ситуаций.

Нужны для продолжения выполнения программы, при возникновении ошибок программирования, неправильного использования API или непредусмотренного поведения пользователя.

Можно ли указывать в сигнатуре метода throws unchecked исключения?

Можно (ArrayIndexOutOfBoundsException / ClassCastException / IllegalArgumentException и т.д.)

Чем отличаются исключения от обычных классов?

Исключения должны быть унаследованы от класса Exception или его подкласса RuntimeException

Класс исключений Java — это своего рода класс со специальной функцией, но синтаксис не отличается от обычных классов

Классы-ошибки, расширяющие класс Error, свидетельствуют о возникновении сложных ситуаций в виртуальной машине Java. … Имена классов-ошибок, по соглашению, заканчиваются словом Error. Классы-исключения, расширяющие класс Exception, отмечают возникновение обычной нештатной ситуации, которую можно и даже нужно обработать.

Все ключевые слова и всё что с ними связано

try — используется для отметки начала блока кода, который потенциально может привести к ошибке

catch — отметка начала блока кода, предназначенного для перехвата и обработки исключений

finally — отметка начала блока кода, которой является дополнительным. Этот блок помещается после последнего блока catch. Управление обычно передаётся в блок finally в любом случае.

throw — служит для генерации исключений (throw new)

throws — прописывается в сигнатуре метода, и обозначающее что метод потенциально может выбросить исключение с указанным типом

Все исключения и как генерировать различные виды исключений

Throwable -> Exception ->RuntimeException (unchecked), IOException (checked), ClassNonFoundException (checked) и др, кастомные исключения

Генерировать через try catch и ключевое слово new.

Проверяемое исключение – это значит что оно контролируется компилятором

Где возможно вызывать исключения?

В методе / конструкторе / интерфейсе

Разница try catch и if

Код более читабельный, а обработка ошибок выведена в отдельный блок

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

Исключения можно передавать цепочкой наверх

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

If более производительный, но менее удобный

Try/catch рекомендуется использовать в специфических ситуациях, например при проверке существования файла перед обращением к этому файлу. (if else может проверить его существование, но состояние файла может быть изменено уже после проверки и тогда вы получим исключение и программа упадёт)

Расскажи про информацию, которая находится внутри исключения? Как с ней работать?

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

Исключение содержит в себе stacktrace(стектрейс вызовов), т.е. последовательность вызванных методов, а также номер строки, на которой возникла ошибка.

Мы можем вызвать массив элементов СтэкТрэйсЭлемент через метод getStackTrace у объекта исключения и получить информацию о классе, методе где было выброшено исключение.

Механизм обработки исключений:

1. Создание объекта-исключения

2. Заполнение stack trace’а этого исключения

3. Раскрутка стека в поисках нужного обработчика

К механизму обработки исключений в Java имеют отношение 5 ключевых слов: — try, catch, throw, throws и finally. Схема работы этого механизма следующая. Вы пытаетесь (try) выполнить блок кода, и если при этом возникает ошибка, система возбуждает (throw) исключение, которое в зависимости от его типа вы можете перехватить (catch) или передать умалчиваемому (finally) обработчику.

Что такое ошибка, а что такое исключительная ситуация?

Исключительная ситуация (Exceptions) – ситуация которую мы можем предсказать и обработать.

Ошибка (Error) — возникает на уровне JVM и не может быть обработана программистом (закончилась память в компьютере или переполнен стек – и взять её неоткуда, поэтому и обрабатывать смысла нет)

Методы исключений

getMessage(),getCause(),printStackTrace(), getStackTrace()

Что нужно делать программисту, если в коде происходит деление на ноль?

Устранить ошибку

Можно ли так написать try { throw new Object(); }?

Нет, так как Object не наследуется от Throwable

Что происходит если не обработать исключение?

Поток, в котором было выброшено и не обработано исключение, остановится, и распечатает стектрейс в вывод System
2) Иерархия исключений

Класс исключений является подклассом класса Throwable

Помимо класса исключений существует подкласс ошибок (Error), В большинстве случаев Error не нужно обрабатывать (StackOverflowError / OutOfMemoryError)

Все классы исключений представляют подтипы класса java.lang.Exception

Класс исключений делится на два подкласса unchecked и checked:

RuntimeException (unchecked) – исключения на этапе выполнения

IOException (checked) – другие исключения

Какое назначение класса Throwable? Методы класса Throwable

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

Назначение – быть выброшенным

Методы класса Throwable:

getMessage — возвращает описание исключения

getStackTrace — возвращает массив, содержащий поэлементную трассировку стека

printStackTrace — выводит трассировку стека

addSuppressed — добавляет заданное исключение в список подавляемых исключений

fillInStackTrace — возвращает объект класса Throwable, содержащий полную трассировку стека

getCause — возвращает исключение, лежащее в основе текущего исключения. null если такое исключение отсутствует

getLocalizedMessage — возвращает локализованное описание исключения

Зачем создавать свой класс и наследовать его от Exception?

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

Чтобы создать свой класс исключений, надо унаследовать его от класса Exception.

От каких классов Throwable и его подклассов нельзя наследоваться?

Прямого запрета нет, НО Классы-ошибки, расширяющие класс Error, свидетельствуют о возникновении сложных ситуаций в виртуальной машине Java. Их обработка требует глубокого понимания всех тонкостей работы JVM. Не советуют даже выбрасывать ошибки оператором throw. Не следует делать свои классы-исключения расширениями класса Error или какого-то его подкласса

Что такое подавленные исключения?

Подавленное исключение — это исключение, которое выбрасывается, но каким-то образом игнорируется.

Например, мы можем не получить информацию об исключении, вызванным в блоке try в случае если блок finally вызвал своё исключение. В таком случае нам будет необходимо вызвать на объекте второго исключения метод Throwable.addSuppressed чтобы добавить информацию о первом исключении.

Однако, если речь идёт о try with resource то Подавленные исключения (suppressed exception) образуются, когда в блоке try генерируется исключение и в методе close() при закрытии ресурса генерируется исключение, в этом случае первое исключение считается главным остальные подавленные.

Так же следует учитывать, что даже если в методе close() будет сгенерировано исключение и оно добавится к подавленным исключениям для блока try, все они заменятся исключением, которое будет сгенерировано блоком finally.

Как достать подавленное исключение?

С помощью метода Throwable.addSuppressed
3) Как создать/бросить/поймать исключение.

Как создать, выбросить, поймать свое исключение?

Создать – создать класс и унаследоваться от Exception или его наследника.

Бросить – методом throw new()

Поймать – конструкцией try catch
4) В чем разница между checked и unchecked исключениями? В чём разница с точки зрения синтаксиса и идеологическая при использовании?

Checked (проверяемые исключение) мы должны или обработать с помощью try/catch или пробросить дальше добавлением в сигнатуру throws. Непроверямые ни обрабатывать ни делать что-либо ещё не нужно.

Unchecked исключения это не ошибки JVM, но RunTimeException это больше ошибка кода (программиста), которую нужно исправить.

Встроенные исключения:

Unchecked RuntimeException

ArrayIndexOutOfBoundsException — Индекс массива выходит за пределы

ClassCastException — Недопустимое приведение типов

IllegalArgumentException — Недопустимый аргумент, используемый для вызова метода

NullPointerException — Недопустимое использование нулевой ссылки

NumberFormatException — Неверное преобразование строки в числовой формат

Checked Exceptions

ClassNotFoundException — Класс не найден

NoSuchMethodException — Запрошенный метод не существует

IllegalAccessException — Запрещен доступ к классу.

и другие
5) Что такое StackTrace и какую информацию можно получить из StackTraceElement?

StackTrace – это стэк (работает по принципу Last In First Out) с вызванными методами во время работы программы. (трассировка стека – это список методов, которые были вызваны до момента, когда в приложении произошло исключение.)

До возникновения ошибки программа идёт вглубь методов, но как только возникает исключительная ситуация программа начинает идти в обратном порядке и заполняет StackTraceElement. Первым идёт строка с описанием проблемы (название потока, класс ошибки, причина), далее названия методов в порядке вложенности.

Получить информацию можно по индексу элемента массива StackTraceElement.и получить информацию об имени файла, имени модуля, имени класса, имени метода, номер строки. (методы getClassName, getMethodName, getLineNumber и тд)

Что возвращает getStackTrace()?

Возвращает массив элементов StackTrace. (см выше)

6) Блок finaly

Оператор finallу образует блок кода, который будет выполнен по завершении блока операторов try-catch.

Блок оператора finallу выполняется независимо от того, выброшено ли исключение или нет. Если исключение выброшено, блок оператора finallу выполняется, даже при условии, что ни один из операторов catch не совпадает с этим исключением. В любой момент, когда метод собирается возвратить управление вызывающему коду из блока оператора try-саtch (через необработанное исключение или явным образом через оператор return), блок оператора finallу выполняется перед возвратом из метода.

Конструкция tryfinally

Допускается конструкция tryfinally без блока catch. Блок finally выполнится в любом случае.

В случае если в блоке try предусмотрен return, блок finally выполнится до return.

Обязателен ли блок finally?

нет

Будет ли выполнен finally при Error?

да

Какой return вернется? Из try или finally?

Выполнится return из блока fianlly

Что если в конструкции try finally вылетело исключение сначала в try а потом в finally? Какое исключение вылетит? Что будет с другим?

Вылетит исключение которые было выброшено в finally, исключение из catch будет подавлено

Когда будет выполнен finally? Когда не будет выполнен(4 случая)?

Не будет выполнен в следующих случаях :,

1. Если упала JVM (например случилась проблема с hardware)

2. Бесконечный цикл в JVM (в блоке try/catch)

3. Была вызвана команда System.exit

4. Если это поток демон, все не-демон потоки завершились, то он завершится до finally
7) Конструкция try-catch-with-resource.

Первоначально он был представлен в Java 7, и вся идея заключалась в том, что разработчику не нужно беспокоиться об управлении ресурсами для ресурсов, которые они используют только в одном блоке try-catch-finally. Это достигается за счет устранения необходимости в finally , которые на практике разработчики использовали только для закрытия ресурсов.

Java понимает это так — Ресурсы, открытые в круглых скобках после оператора try, понадобятся только здесь и сейчас. Я вызову их .close() как только закончу работу с блоком try. Если в блоке try возникает исключение, я все равно закрою эти ресурсы.

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

Ресурс – это объект класса, реализующего интерфейсы AutoCloseable или Closeable.

Что такое ресурс в конструкции try-with-resources?

Ресурс – это объект класса, реализующего интерфейсы AutoCloseable или Closeable.

Когда происходит закрытие ресурса в конструкции try-with-resources если в try возникло исключение: до перехода в catch или после того как catch отработает?

In a try-with-resources statement, any catch or finally block is run after the resources declared have been closed

Блок catch будет отработан после того как ресурс будет закрыт

In case a resource throws an exception when you try to close it, any other resources opened within the same try-with-resources block will still get closed. After closing all resources, the exception from the failed close-attempt will get propagated up the call stack.

Если в try с ресурсами, — в try не вылетало исключений, а при закрытии ресурсов возникло исключение — ресурс не закроется (но закроются другие ресурсы объявленные в этом же блоке), а программа аварийно завершится с выводом StackTrace

Порядок создания (инициализации) ресурсов в try-resource? –

В обратном порядке. От последнего к первому.

Что если исключение вылетело сначала в try, а потом в close в конструкции try-with-recources? Какое исключение вылетит? Что будет с другим?

Если речь идёт о try with resource то Подавленные исключения (suppressed exception) образуются, когда в блоке try генерируется исключение и в методе close() при закрытии ресурса генерируется исключение, в этом случае первое исключение считается главным остальные подавленные.

Так же следует учитывать, что даже если в методе close() будет сгенерировано исключение и оно добавится к подавленным исключениям для блока try, все они заменятся исключением, которое будет сгенерировано блоком finally.
Catch

Блок кода с описанием логики и действий при возникновении указанной исключительной ситуации

Обязателен ли блок catch?

Блок catch не обязателен

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

ДА. Пока классы этих исключений можно отследить вверх по иерархии наследования классов до одного и того же суперкласса, можно использовать только этот суперкласс.

Какое правило должно соблюдаться при использовании нескольких блоков catch (с одним try)?

По иерархии от младшего к старшему

Какое правило должно соблюдаться при попытке поймать несколько исключений в одном catch?

Это называется multi-catch — многократный перехват. В нём можно перечислить несколько исключений, разделив их знаком | по иерархии (слева младше, справа старше)

8) Что такое логгирование и для чего используется.

Логирование — это процесс, при котором программа записывает сведения о своем исполнении в файл или базу данных. Логирование дает возможность отслеживать ход исполнения программы.

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

Лог-файлы помогают «следить» за действиями программы, например, что она функционирует в конкретный момент времени или как она реагирует на действия пользователя.

У одного программного продукта лог-файлы могут быть разные. Например, может быть лог-файл типа:«access_log», в котором фиксируется взаимодействие с пользователями;

«error_log», в котором фиксируются все ошибки, произошедшие в результате работы программы;

Logger —объект, который отвечает за запись информации в лог-файлы, опираясь на заданные уровни логирования. Logger это статическое поле класса инициализируемое при загрузке класса, имеет простое, короткое имя, важно чтобы во всех Ваших классах переменная логера называлась одинаково.

Имя логгера не обязательно должно совпадать с именем класса, но как показывает практика выдумывать и главное потом неукоснительно следовать такой иерархии крайне сложно.
Имя логера берется как Class.class.getName() а не как «com.dataart.demo.java.logging.SomeClass»

Appender или Handlers — конечная точка (файл/консоль/БД), куда записывается информация.

Layout — формат, в котором выводятся сообщения

Логгеры — JUL(Java.util.logging) / Logback / SLF4J
Какие есть уровни логирования и для чего они нужны?

OFF — сообщения не выводятся;

SEVERE — ошибка, после которой приложение будет остановлено, например, JVM out of memory error

WARNING — логи, которые содержат предостережение. Произошло неожиданное действие, но система устояла и выполнила запрос;

INFO — важные действия в приложении. Это не предостережение, это ожидаемые действия системы

CONFIG

FINE

FINER — сообщения об успешной операции

FINEST

ALL— выводится вся информация.
«Поддержать» уровни логирования в Java можно двумя способами:

  • Внутри программы можно расставить вызов нужной библиотеки в соответствии с заданным уровнем.
  • В момент запуска программы нужно указать уровень логирования для конкретной ситуации. Если ничего не указывать, то для программы будет применяться уровень «info»

Обязательно ли передавать в метод getLogger() имя класса? Почему так принято?

Logger.getLogger создает один регистратор на класс, а не один регистратор на экземпляр класса. Logger.getLogger(Class) является сокращением для getLogger(clazz.getName()). Соглашение, используемое с log4j и другими фреймворками регистрации, — это определение статического регистратора для каждого класса.

  • Простота использования. Не нужно беспокоиться о дублировании имени регистратора в приложении
  • Легко проверить класс ведения журнала, так как имя регистратора будет отображаться в файле журнала
  • Защита от перенаправления ведение журнала из вашего класса в другой файл или в другое место

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

Пример: имя класса com.github.artemiy.loggingexamp.MainDemo

↓RootLogger — главный узел. Это узел, который принимает все логи всего приложения.

com –следующий узел.

↓com.github

com.github.artemiy

com.github.artemiy.loggingexamp

Сообщения каких уровней мы увидим, задав уровень INFO?

все, что выше INFO

INFO, WARNING, SEVERE

Как Java понимает какой уровень главнее при установке .setLevel().

Метод setLevel() определяет наименьшую степень серьезности, которая будет отправлена в соответствующее место назначения.

Понимает из файла настройки logging.properties по умолчанию в JAVA_HOME/jre/lib

Исключения

В нашем разговоре о потоке исполнения команд различными подсистемами пришло время поговорить про исключения или, скорее, исключительные ситуации. И прежде чем продолжить стоит совсем немного остановиться именно на самом определении. Что такое исключительная ситуация? Это такая ситуация, которая делает исполнение дальнейшего или текущего кода абсолютно не корректным. Не таким как задумывалось, проектировалось. Переводит состояние приложения в целом или же его отделой части (например, объекта) в состояние нарушенной целостности. Т.е. что-то экстраординарное, исключительное.

Почему это так важно — определить терминологию? Работа с терминологией очень важна, т.к. она держит нас в рамках. Вот, например: будет ли являться исключительной ситуация когда пользователь ввел в поле ввода чисел букву ‘a’? Наверное, нет: мы можем легко проигнорировать ввод. Но если мы поделим любое целое число на ноль это будет исключительной ситуацией: на ноль делить нельзя. Дальнейшее выполнение программы бессмысленно, т.к. расчеты гарантированно не корректны. Исключительные ситуации, возникающие в приложении должны прерывать исполнение текущего, уже более не корректного, кода и искать способы исправить ситуацию. Здесь я попрошу вас обратить внимание на слово «прерывать». Оно очень интересно в первую очередь тем что помимо механизма исключений существует еще один механизм: механизм прерываний. И разница между этими двумя механизмами состоит в том что прерывания останавливают приложение на время, выполняют некоторый код и продолжают выполнение кода программы тогда как исключения работают известным всеми способом: полностью обрубают выполнение кода текущего метода, уводя поток исполнения инструкций процессором в выше-стоящие методы, способные возникшую ошибку обработать.

О чем пойдет речь в этом разделе:

  • Состав и развертка блока обработки исключительных ситуаций
  • События о исключительных ситуациях: AppDomain.FirstChanceException и AppDomain.UnhandledException
  • Виды исключений: что тянется из CLR, а что — из более низкого слоя (Windows SEH)
  • Исключения с особым поведением: ThreadAbortException, OutOfMemoryException и прочие
  • Каким образом идет сборка стека вызовов и производительность выброса исключений
  • Асинхронные исключения
  • Structured Exception Handling
  • Vectored Exception Handling
  • Прерывания

Состав и развертка блока обработки исключительных ситуаций

Общая картина

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

try {
    // 1
} catch (ArgumentsOutOfRangeException exception)
{
    // 2
} catch (IOException exception)
{
    // 3
} catch
{
    // 4
} finally {
    // 5
}

Т.е. существует некий участок кода от которого ожидается некоторое нарушение поведения. Причем не просто некоторое, а вполне конкретные ситуации. Однако, если заглянуть в результирующий код, то мы увидим что по факту эта самая конструкция, которая в C# выглядит как единое целое, в CLI на самом деле разделена на отдельные блоки. Т.е. не существует возможности построить вот такую единую цепочку обработки ошибок, однако есть возможность построить для одного и того же участка отдельные блоки try-catch и try-finally. И если переводить MSIL обратно в C#, то получим мы следующий код:

try {
    try {
        try {
            try {
                // 1
            } catch (ArgumentsOutOfRangeException exception)
            {
                // 2
            }
        } catch (IOException exception)
        {
            // 3
        }
    } catch
    {
        // 4
    }
} finally {
    // 5
}

// 6

Отлично. Однако если мы хотим увидеть картину с точки зрения «как оно все устроено», то полученный код выглядит все же несколько искусственно. Ведь эти блоки — конструкции языка и не более того. Как они разворачиваются в конечном коде? На данном этапе я ограничусь псевдокодом, однако без лишних подробностей он прекрасно покажет во что примерно разворачивается конструкция:

GlobalHandlers.Push(BlockType.Finally, FinallyLabel);
GlobalHandlers.Push(BlockType.Catch, typeof(Exception), ExceptionCatchLabel);
GlobalHandlers.Push(BlockType.Catch, typeof(IOException), IOExceptionCatchLabel);
GlobalHandlers.Push(BlockType.Catch, typeof(ArgumentsOutOfRangeException), ArgumentsOutOfRangeExceptionCatchLabel);

// 1

GlobalHandlers.Pop(4);
FinallyLabel:

// 5

goto AfterTryBlockLabel;
ExceptionCatchLabel:
GlobalHandlers.Pop(4);

// 4

goto FinallyLabel;
IOExceptionCatchLabel:
GlobalHandlers.Pop(4);

// 3

goto FinallyLabel;
ArgumentsOutOfRangeExceptionCatchLabel:
GlobalHandlers.Pop(4);

// 2

goto FinallyLabel;
AfterTryBlockLabel:

// 6

return;

Также о чем хотелось бы упомянуть во вводной части — это фильтры исключительных ситуаций. Для платформы .NET это новшеством не является, однако является таковым для разработчиков на языке программирования C#: фильтрация исключительных ситуаций появилась у нас только в шестой версии языка. Особенностью исполнения кода по уверениям многих источников является то, что код фильтрации происходит до того как произойдет развертка стека. Это можно наблюдать в ситуациях, когда между местом выброса исключения и местом проверки на фильтрацию нет никаких других вызовов кроме обычных:

static void Main()
{
    try
    {
        Foo();
    }
    catch (Exception ex) when (Check(ex))
    {
        ;
    }
}

static void Foo()
{
    Boo();
}

static void Boo()
{
    throw new Exception("1");
}

static bool Check(Exception ex)
{
    return ex.Message == "1";
}

Как видно на изображении трассировка стека содержит не только первый вызов Main как место отлова исключительной ситуации, но и весь стек до точки выброса исключения плюс повторный вход в Main через некоторый неуправляемый код. Можно предположить что этот код и есть код выброса исключений, который просто находится в стадии фильтрации и выбора конечного обработчика. Однако стоит отметить что не все вызовы позволяют работать без раскрутки стека. Если посмотреть на результаты работы следующего кода (я добавил проброс вызова через границу между доменами приложения):

    class Program
    {
        static void Main()
        {
            try
            {
                ProxyRunner.Go();
            }
            catch (Exception ex) when (Check(ex))
            {
                ;
            }
        }

        static bool Check(Exception ex)
        {
            var domain = AppDomain.CurrentDomain.FriendlyName; // -> TestApp.exe
            return ex.Message == "1";
        }

        public class ProxyRunner : MarshalByRefObject
        {
            private void MethodInsideAppDomain()
            {
                throw new Exception("1");
            }

            public static void Go()
            {
                var dom = AppDomain.CreateDomain("PseudoIsolated", null, new AppDomainSetup
                {
                    ApplicationBase = AppDomain.CurrentDomain.BaseDirectory
                });
                var proxy = (ProxyRunner) dom.CreateInstanceAndUnwrap(typeof(ProxyRunner).Assembly.FullName, typeof(ProxyRunner).FullName);
                proxy.MethodInsideAppDomain();
            }
        }
    }

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

StackUnroll

А второй — после:

StackUnroll2

Изучим трассировку вызовов до и после попадания в фильтр исключений. Что же здесь происходит? Здесь мы видим что разработчики платформы сделали некоторую с первого взгляда защиту дочернего домена. Трассировка обрезана по крайний метод в цепочке вызовов, после которого идет переход в другой домен. На самом деле как по мне так это выглядит несколько странно. Чтобы понять, почему так происходит, вспомним основное правило для типов, организующих взаимодействие между доменами. Типы должны наследовать MarshalByRefObject плюс — быть сериализуемыми. Однако как бы ни был строг C#, типы исключений могут быть какими угодно. Это могут быть вовсе не Exception-based типы. А что это значит? Это значит что могут быть ситуации, когда исключительная ситуация внутри дочернего домена может привести у уводу в родительский домен объекта, который передан по ссылке, и у которого есть какие-либо опасные методы с точки зрения безопасности. Чтобы такого избежать, исключение сериализуется, проходит через границу доменов приложений и возникает вновь — с новым стеком. Давайте проверим эту стройную теорию:

[StructLayout(LayoutKind.Explicit)]
class Cast
{
    [FieldOffset(0)]
    public Exception Exception;

    [FieldOffset(0)]
    public object obj;
}

static void Main()
{
    try
    {
        ProxyRunner.Go();
        Console.ReadKey();
    }
    catch (RuntimeWrappedException ex) when (ex.WrappedException is Program)
    {
        ;
    }
}

static bool Check(Exception ex)
{
    var domain = AppDomain.CurrentDomain.FriendlyName; // -> TestApp.exe
    return ex.Message == "1";
}

public class ProxyRunner : MarshalByRefObject
{
    private void MethodInsideAppDomain()
    {
        var x = new Cast {obj = new Program()};
        throw x.Exception;
    }

    public static void Go()
    {
        var dom = AppDomain.CreateDomain("PseudoIsolated", null, new AppDomainSetup
        {
            ApplicationBase = AppDomain.CurrentDomain.BaseDirectory
        });
        var proxy = (ProxyRunner)dom.CreateInstanceAndUnwrap(typeof(ProxyRunner).Assembly.FullName, typeof(ProxyRunner).FullName);
        proxy.MethodInsideAppDomain();
    }
}

В данном примере для того чтобы выбросить исключение любого типа из C# кода (я не хочу никого мучать вставками на MSIL) был проделан трюк с приведением типа к не сопоставимому: чтобы мы бросили исключение любого типа, а транслятор C# думал бы что мы используем тип Exception. Мы создаем экземпляр типа Program — гарантированно не сериализуемого и бросаем исключение с ним в виде полезной нагрузки. Хорошие новости заключаются в том что вы получите обертку над не-Exception исключениями RuntimeWrappedException, который внутри себя сохранит экземпляр нашего объекта типа Program и в C# перехватить такое исключение мы сможем. Однако есть и плохая новость, которая подтверждает наше предположение: вызов proxy.MethodInsideAppDomain(); приведет к исключению SerializationException:

Т.е. проброс между доменами такого исключения не возможен, т.к. его нет возможности сериализовать. А это в свою очередь значит что оборачивание вызовов методов, находящихся в других доменах фильтрами исключений все равно приведет к развертке стека несмотря на то что при FullTrust настройках дочернего домена сериализация казалось бы не нужна.

Стоит дополнительно обратить внимание на причину, по которой сериализация между доменами так необходима. В нашем синтетическом примере мы создаем дочерний домен, который не имеет никаких настроек. А это значит что он работает в FullTrust. Т.е. CLR полностью доверяет его содержимому как себе и никаких дополнительных проверок делать не будет. Но как только вы выставите хоть одну настройку безопасности, полная доверенность пропадет и CLR начнет контролировать все что происходит внутри этого дочернего домена. Так вот когда домен полностью доверенный, сериализация по-идее не нужна. Нам нет необходимости как-то защищаться, согласитесь. Но сериализация создана не только для защиты. Каждый домен грузит библиотеки в себя по второму разу, создавая их копии. Тем самым создавая копии всех типов и всех таблиц виртуальных методов. Передавая объект по ссылке из домена в домен вы получите, конечно, тот же объект. Но у него будут чужие таблицы виртуальных методов и как результат — этот объект не сможет быть приведен к другому типу. Другими словами, если вы создали экземпляр типа Boo, то получив его в другом домене приведение типа (Boo)boo не сработает. А сериализация и десериализация решает проблему

Передавая серализуемый объект между доменами вы получите в другом домене полную копию объекта из первого сохранив некоторую разграниченность по памяти. Разграниченность тоже мнимая. Она — только для тех типов, которые не находятся в Shared AppDomain. Т.е., например, если в качестве исключения бросить что-нибудь несериализуемое, но из Shared AppDomain, то ошибки сериализации не будет (можно попробовать вместо Program бросить Action). Однако раскрутка стека при этом все равно произойдет: оба случая должны работать стандартно. Чтобы никого не путать.

События об исключительных ситуациях

В общем случае мы не всегда знаем о тех исключениях, которые произойдут в наших программах потому что практчески всегда мы используем что-то, что написано другими людьми и что находится в других подсистемах и библиотеках. Мало того что возможны самые разные ситуации в вашем собственном коде, так еще и существует множество проблем, связанных с исполнением кода в изоблированных доменах. И как раз в этом случае было бы крайне полезно уметь получать данные о работе изолированного кода. Ведь вполне реальной может быть ситуация, когда сторонний код перехватывает все без исключения ошибки, заглушив их fault блоком:

    try {
        // ...
    } catch {
        // do nothing, just to make code call more safe
    }

В такой ситуации может оказаться что выполнение кода уже не так безопасно как выглядит, но сообщений о том что произошли какие-то проблемы мы не имеем. Второй вариант — когда приложение глушит некоторое, пусть даже легальное, исключение. А результат — следующее исключение в случайном месте — падение приложения в некотором будущем от случайной казалось бы ошибки. Тут хотелось бы иметь представление, какая была предыстория этой ошибки. Каков ход событий привел к такой ситуации. Один из способов сделать это возможным — использовать дополнительные события, которые относятся к исключительным ситуациям: AppDomain.FirstChanceException и AppDomain.UnhandledException.

Фактически, когда вы «бросаете исключение», то вызывается обычный метод некоторой внутренней подсистемы Throw, который внутри себя проделывает следующие операции:

  • Вызывает AppDomain.FirstChanceException
  • Ищет в цепочке обработчиков подходящий по фильтрам
  • Вызывает обработчик предварительно откатив стек на нужный кадр
  • Если обработчик найден не был, вызывается AppDomain.UnhandledException, обрушивая поток, в котором произошло исключение.

Сразу следует оговориться, ответив на мучающий многие умы вопрос: есть ли возможность как-то обработать исключение, не обрушивая тем самым поток, в котором это исключение было выброшено? Ответ лаконичен и прост: нет. Если исключение не перехватывается на всем диапазоне вызванных методов, оно не может быть обработано в принципе. Иначе возникает странная ситуация: если мы при помощи AppDomain.FirstChanceException обрабатываем (некий синтетический catch) исключение, то на какой кадр должен откатитья стек потока? Как это задать в рамках правил .NET CLR? Никак. Это просто не возможно. Единственное что мы можем сделать — запротоколировать полученную информацию для будущих исследований.

Второе, о чем следует рассказать «на берегу» — это почему эти события введены не у Thread, а у AppDomain. Ведь если следовать логике, исключения возникают где? В потоке исполнения команд. Т.е. фактически у Thread. Так почему же проблемы возникают у домена? Ответ очень прост: для каких ситуаций создавались AppDomain.FirstChanceException и AppDomain.UnhandledException? Помимо всего прочего — для создания песочниц для плагинов. Т.е. для ситуаций, когда есть некий AppDomain, который настроен на PartialTrust. Внутри этого AppDomain может происходить что угодно, создаются потоки, используются уже существующие из ThreadPool. Тогда получается что мы, будучи находясь снаружи от этого процесса (не мы писали тот код) не можем никак подписаться на события внутренних потоков. Просто потому что мы понятия не имеем что там за потоки были созданы. Зато мы гарантированно имеем AppDomain, который организует песочницу и ссылка на который у нас есть.

Итак, по факту нам предоставляется два краевых события: что-то произошло, чего не предполагалось (FirstChanceExecption) и «все плохо», никто не обработал исключительную ситуацию: она оказалась не предусмотренной. А потому поток исполнения команд не имеет смысла и он (Thread) будет отгружен.

Что можно получить, имея данные события и почему плохо что разработчики обходят эти события стороной?

AppDomain.FirstChanceException

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

Но давайте для начала посмотрим на простой синтетический пример его обработки:

void Main()
{
    var counter = 0;

    AppDomain.CurrentDomain.FirstChanceException += (_, args) => {
        Console.WriteLine(args.Exception.Message);
        if(++counter == 1) {
            throw new ArgumentOutOfRangeException();
        }
    };

    throw new Exception("Hello!");
}

Чем примечателен данный код? Где бы некий код ни сгенерировал бы исключение первое что произойдет — это его логгирование в консоль. Т.е. даже если вы забудете или не сможете предусмотреть обработку некоторого типа исключения оно все равно появится в журнале событий, которое вы организуете. Второе — несколько странное условие выброса внутреннего исключения. Все дело в том что внутри обработчика FirstChanceException вы не можете просто взять и бросить еще одно исключение. Скорее даже так: внутри обработчика FirstChanceException вы не имеете возможности бросить хоть какое-либо исключение. Если вы так сделаете, возможны два варианта событий. При первом, если бы не было условия if(++counter == 1), мы бы получили бесконечный выброс FirstChanceException для все новых и новых ArgumentOutOfRangeException. А что это значит? Это значит что на определенном этапе мы бы получили StackOverflowException: throw new Exception("Hello!") вызывает CLR метод Throw, который вызывает FirstChanceException, который вызывает Throw уже для ArgumentOutOfRangeException и далее — по рекурсии. Второй вариант — мы защитились по глубине рекурсии при помощи условия по counter. Т.е. в данном случае мы бросаем исключение только один раз. Результат более чем неожиданный: мы получим исключительную ситуацию, которая фактически отрабатывает внутри инструкции Throw. А что подходит более всего для данного типа ошибки? Согласно ECMA-335 если инструкция была введена в исключительное положение, должно быть выброшено ExecutionEngineException! А эту исключительную ситуацию мы обработать никак не в состоянии. Она приводит к полному вылету из приложения. Какие же варианты безопасной обработки у нас есть?

Первое, что приходит в голову — это выставить try-catch блок на весь код обработчика FirstChanceException:

void Main()
{
    var fceStarted = false;
    var sync = new object();
    EventHandler<FirstChanceExceptionEventArgs> handler;
    handler = new EventHandler<FirstChanceExceptionEventArgs>((_, args) =>
    {
        lock (sync)
        {
            if (fceStarted)
            {
                // Этот код по сути - заглушка, призванная уведомить что исключение по своей сути - родилось не в основном коде приложения, 
                // а в try блоке ниже.
                Console.WriteLine($"FirstChanceException inside FirstChanceException ({args.Exception.GetType().FullName})");
                return;
            }
            fceStarted = true;

            try
            {
                // не безопасное логгирование куда угодно. Например, в БД
                Console.WriteLine(args.Exception.Message);
                throw new ArgumentOutOfRangeException();
            }
            catch (Exception exception)
            {
                // это логгирование должно быть максимально безопасным
                Console.WriteLine("Success");
            }
            finally
            {
                fceStarted = false;
            }
        }
    });
    AppDomain.CurrentDomain.FirstChanceException += handler;

    try
    {
        throw new Exception("Hello!");
    } finally {
        AppDomain.CurrentDomain.FirstChanceException -= handler;
    }
}

OUTPUT:

Hello!
Specified argument was out of the range of valid values.
FirstChanceException inside FirstChanceException (System.ArgumentOutOfRangeException)
Success

!Exception: Hello!

Т.е. с одной стороны у нас есть код обработки события FirstChanceException, а с другой — дополнительный код обработки исключений в самом FirstChanceException. Однако методики логгирования обоих ситуаций должны отличаться. Если логгирование обработки события может идти как угодно, то обработка ошибки логики обработки FirstChanceException должно идти без исключительных ситуаций в принципе. Второе, что вы наверняка заметили — это синхронизация между потоками. Тут может возникнуть вопрос: зачем она тут если любое исключение рождено в каком-либо потоке а значит FirstChanceException по-идее должен быть потокобезопасным. Однако, все не так жизнерадостно. FirstChanceException у нас возникает у AppDomain. А это значит, что он возникает для любого потока, стартованного в определенном домене. Т.е. если у нас есть домен, внутри которого стартовано несколько потоков, то FirstChanceException могут идти в параллель. А это значит что нам необходимо как-то защитить себя синхронизацией: например при помощи lock.

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

static void  Main()
{
    using (ApplicationLogger.Go(AppDomain.CurrentDomain))
    {
        throw new Exception("Hello!");
    }
}

public class ApplicationLogger : MarshalByRefObject
{
    ConcurrentQueue<Exception> queue = new ConcurrentQueue<Exception>();
    CancellationTokenSource cancellation;
    ManualResetEvent @event;

    public void LogFCE(Exception message)
    {
        queue.Enqueue(message);
    }

    private void StartThread()
    {
        cancellation = new CancellationTokenSource();
        @event = new ManualResetEvent(false);
        var thread = new Thread(() =>
        {
            while (!cancellation.IsCancellationRequested)
            {
                if (queue.TryDequeue(out var exception))
                {
                    Console.WriteLine(exception.Message);
                }
                Thread.Yield();
            }
            @event.Set();
        });
        thread.Start();
    }

    private void StopAndWait()
    {
        cancellation.Cancel();
        @event.WaitOne();
    }

    public static IDisposable Go(AppDomain observable)
    {
        var dom = AppDomain.CreateDomain("ApplicationLogger", null, new AppDomainSetup
        {
            ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
        });

        var proxy = (ApplicationLogger)dom.CreateInstanceAndUnwrap(typeof(ApplicationLogger).Assembly.FullName, typeof(ApplicationLogger).FullName);
        proxy.StartThread();

        var subscription = new EventHandler<FirstChanceExceptionEventArgs>((_, args) =>
        {
            proxy.LogFCE(args.Exception);
        });
        observable.FirstChanceException += subscription;

        return new Subscription(() => {
            observable.FirstChanceException -= subscription;
            proxy.StopAndWait();
        });
    }

    private class Subscription : IDisposable
    {
        Action act;
        public Subscription (Action act) {
            this.act = act;
        }
        public void Dispose()
        {
            act();
        }
    }
}

В данном случае обработка FirstChanceException происходит максимально безопасно: в соседнем потоке, принадлежащим соседнему домену. Ошибки обработки сообщения при этом не могут обрушить рабочие потоки приложения. Плюс отдельно можно послушать UnhandledException домена логгирования сообщений: фатальные ошибки при логгировании не обрушат все приложение.

AppDomain.UnhandledException

Второе сообщение, которое мы можем перехватить и которое касается обработки исключительных ситуаций — это AppDomain.UnhandledException. Это сообщение — очень плохая новость для нас поскольку обозначает что не нашлось никого кто смог бы найти способ обработки возникшей ошибки в некотором потоке. Также, если произошла такая ситуация, все что мы можем сделать — это «разгрести» последствия такой ошибки. Т.е. каким-либо образом зачистить ресурсы, принадлежащие только этому потоку если таковые создавались. Однако, еще более лучшая ситуация — обрабатывать исключения, находясь в «корне» потоков не заваливая поток. Т.е. по сути ставить try-catch. Давайте попробуем рассмотреть целесообразность такого поведения.

Пусть мы имеем библиотеку, которой необходимо создавать потоки и осуществлять какую-то логику в этих потоках. Мы, как пользователи этой библиотеки интересуемся только гарантией вызовов API а также получением сообщений об ошибках. Если библиотека будет рушить потоки не нотифицируя об этом, нам это мало чем может помочь. Мало того обрушение потока приведет к сообщению AppDomain.UnhandledException, в котором нет информации о том, какой конкретно поток лег на бок. Если же речь идет о нашем коде, обрушивающийся поток нам тоже вряд-ли будет полезным. Во всяком случае необходимости в этом я не встречал. Наша задача — обработать ошибки правильно, отдать информацию об их возникновении в журнал ошибок и корректно завершить работу потока. Т.е. по сути обернуть метод, с которого стартует поток в try-catch:

    ThreadPool.QueueUserWorkitem(_ => {
        using(Disposables aggregator = ...){
            try {
                // do work here, plus:
                aggregator.Add(subscriptions);
                aggregator.Add(dependantResources);
            } catch (Exception ex)
            {
                logger.Error(ex, "Unhandled exception");
            }
        }
    });

В такой схеме мы получим то что надо: с одной стороны мы не обрушим поток. С другой — корректно очистим локальные ресурсы если они были созданы. Ну и в довесок — организуем журналирование полученной ошибки. Но постойте, скажете вы. Как-то вы лихо соскочили с вопроса события AppDomain.UnhandledException. Неужели оно совсем не нужно? Нужно. Но только для того чтобы сообщить что мы забыли обернуть какие-то потоки в try-catch со всей необходимой логикой. Именно со всей: с логгированием и очисткой ресурсов. Иначе это будет совершенно не правильно: брать и гасить все исключения, как будто их и не было вовсе.

Виды исключений

Практически все исключения на платформе .NET выглядят абсолютно одинаково. И это свойство в первую очередь — заслуга разработчиков ядра платформы. Ведь то что мы получаем по своей сути является унификацией разнородных источников ошибок. Особенно это чувствуется на Core CLR, где наши приложения исполняются как на Windows, так и на Linux или OS X. Как же можно поделить исключительные ситуации? Я бы предложил поделить их как-то так:

  • Пользовательские исключения. Это те исключения, которые выбрасываются при помощи инструкции CLR throw. Все что выброшено данным способом является пользовательским. А это значит что никаких особенностей их перехвата не существует;
  • Исключения платформы CLR. Это — специальный тип исключений, которые генерируются из внутренностей платформы. Эти исключения могут обладать особыми свойствами поведения, о которых стоит помнить;
  • Исключения unsafe кода пользовательского уровня. Как правило перехватываются без особых проблем;
  • Исключения unsafe кода уровня операционной системы. Чаще всего имеют очень специализированные свойства при перехвате.

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

Исключения платформы CLR

ThreadAbortException

Вообще, это может показаться не очевидным, но существует четыре типа Thread Abort.

  • Грубый вариант ThreadAbort, который, отрабатывая не может быть никак остановлен и который не запускает обработчиков исключительных ситуаций вообще включая секции finally
  • Вызов метода Thread.Abort() на текущем потоке
  • Асинхронное исключение ThreadAbortException, вызванное из другого потока
  • Если во время выгрузки AppDomain существуют потоки, в рамках которых запущены методы, скомпилированные для этого домена, будет произведен ThreadAbort тех потоков, в которых эти методы запущены

Стоит заметить что ThreadAbortException довольно часто используется в большом .NET Framework, однако его не существует на CoreCLR, .NET Core или же под Windows 8 «Modern app profile». Попробуем узнать, почему.

Итак, если мы имеем дело с не принципиальным типом обрыва потока, когда мы еще можем с ним что-то сделать (т.е. второй, третий и четвертый вариант), виртуальная машина при возникновении такого исключения начинает идти по всем обработчикам исключительных ситуаций и искать как обычно те, тип исключения которых является тем, что было выброшено либо более базовым. В нашем случае это три типа: ThreadAbortException, Exception и object (помним что Exception — это по своей сути — хранилище данных и тип исключения может быть любым. Даже int). Отрабатывая все подходящие catch блоки виртуальная машина пробрасыват ThreadAbortException дальше по цепочке обработки исключений попутно входя во все finally блоки. В целом, ситуации в двух примерах, описанных ниже абсолютно одинаковые:

var thread = new Thread(() =>
{
    try {
        // ...
    } catch (Exception ex)
    {
        // ...
    }
});
thread.Start();
//...
thread.Abort();

var thread = new Thread(() =>
{
    try {
        // ...
    } catch (Exception ex)
    {
        // ...
        if(ex is ThreadAbortException)
        {
            throw;
        }
    }
});
thread.Start();
//...
thread.Abort();

Конечно же, всегда возникнет ситуация, когда возникающий ThreadAbort может быть нами вполне ожидаем. Тогда может возникнуть понятное желание его все-таки обработать. Как раз для таких случаев был разработан и открыт метод Thread.ResetAbort(), который делает именно то, что нам нужно: останавливает сквозной проброс исключения через всю цепочку обработчиков, делая его обработанным:

void Main()
{
    var barrier = new Barrier(2);

    var thread = new Thread(() =>
    {
        try {
            barrier.SignalAndWait();  // Breakpoint #1
            Thread.Sleep(TimeSpan.FromSeconds(30));
        }
        catch (ThreadAbortException exception)
        {
            "Resetting abort".Dump();
            Thread.ResetAbort();
        }

        "Catched successfully".Dump();
        barrier.SignalAndWait();     // Breakpoint #2
    });

    thread.Start();
    barrier.SignalAndWait();         // Breakpoint #1

    thread.Abort();
    barrier.SignalAndWait();         // Breakpoint #2
}

Output:
Resetting abort
Catched successfully

Однако реально ли стоит этим пользоваться? И стоит ли обижаться на разработчиков CoreCLR что там этот код попросту выпилен? Представьте что вы — пользователь кода, который по вашему мнению «повис» и у вас возникло непреодолимое желание вызвать для него ThreadAbortException. Когда вы хотите оборвать жизнь потока все чего вы хотите — чтобы он действительно завершил свою работу. Мало того, редкий алгоритм просто обрывает поток и бросает его, уходя к своим делам. Обычно внешний алгоритм решает дождаться корректного завершения операций. Или же наоборот: может решить что поток более уже ничего делать не будет, декрементирует некие внутренние счетчики и более не будет завязываться на то что есть какая-то многопоточная обработка какого-либо кода. Тут в общем не скажешь, что хуже. Я даже так вам скажу: отработав много лет программистом я до сих пор не могу вам дать прекрасный способ его вызова и обработки. Сами посудите: вы бросаете ThreadAbort не прямо сейчас а в любом случае спустя некоторое время после осознания безвыходности ситуации. Т.е. вы можете как попасть по обработчику ThreadAbortException так и промахнуться мимо него: «зависший код» мог оказаться вовсе не зависшим, а попросту долго работающим. И как раз в тот момент, когда вы хотели оборвать его жизнь, он мог вырваться из ожидания и корректно продолжить работу. Т.е. без лишней лирики выйти из блока try-catch(ThreadAbortException) { Thread.ResetAbort(); }. Что мы получим? Оборванный поток, который ни в чем не виноват. Шла уборщица, выдернула провод, сеть пропала. Метод ожидал таймаута, уборщица вернула провод, все заработало, но ваш контролирующий код не дождался и убил поток. Хорошо? Нет. Как-то можно защититься? Нет. Но вернемся к навящивой идее легализации Thread.Abort(): мы кинули кувалдой в поток и ожидаем что он с вероятностью 100% оборвется, но этого может не произойти. Во-первых становится не понятно как его оборвать в таком случае. Ведь тут все может быть намного сложнее: в подвисшем потоке может быть такая логика, которая перехватывает ThreadAbortException, останавливает его при помощи ResetAbort, однако продолжает висеть из-за сломанной логики. Что тогда? Делать безусловный thread.Interrupt()? Попахивает попыткой обойти ошибку в логике программы грубыми методами. Плюс, я вам гарантирую что у вас поплывут утечки: thread.Interrupt() не будет заниматься вызовом catch и finally, а это значит что при всем опыте и сноровке очистить ресурсы вы не сможете: ваш поток просто исчезнет, а находясь в соседнем потоке вы можете не знать ссылок на все ресурсы, которые были заняты умирающим потоком. Также прошу заметить что в случае промаха ThreadAbortException мимо catch(ThreadAbortException) { Thread.ResetAbort(); } у вас точно также потекут ресурсы.

После того что вы прочитали чуть выше я надеюсь, вы остались в некотором состоянии запутанности и желания перечитать абзац. И это будет совершенно правильная мысль: это будет доказательством того что пользоваться Thread.Abort() попросту нельзя. Как и нельзя пользоваться thread.Interrupt();. Оба метода приводят к неконтролируемому поведению вашего приложения. По своей сути они нарушают принцип целостности: основной принцип .NET Framework.

Однако, чтобы понять для каких целей этот метод введен в эксплуатацию достаточно посмотреть исходные коды .NET Framework и найти места использования Thread.ResetAbort(). Ведь именно его наличие по сути легализует thread.Abort().

Класс ISAPIRuntime ISAPIRuntime.cs

try {

    // ...

}
catch(Exception e) {
    try {
        WebBaseEvent.RaiseRuntimeError(e, this);
    } catch {}

    // Have we called HSE_REQ_DONE_WITH_SESSION?  If so, don't re-throw.
    if (wr != null && wr.Ecb == IntPtr.Zero) {
        if (pHttpCompletion != IntPtr.Zero) {
            UnsafeNativeMethods.SetDoneWithSessionCalled(pHttpCompletion);
        }
        // if this is a thread abort exception, cancel the abort
        if (e is ThreadAbortException) {
            Thread.ResetAbort();
        }                    
        // IMPORTANT: if this thread is being aborted because of an AppDomain.Unload,
        // the CLR will still throw an AppDomainUnloadedException. The native caller
        // must special case COR_E_APPDOMAINUNLOADED(0x80131014) and not
        // call HSE_REQ_DONE_WITH_SESSION more than once.
        return 0;
    }

    // re-throw if we have not called HSE_REQ_DONE_WITH_SESSION
    throw;
}

В данном примере происходит вызов некоторого внешнего кода и если тот был завершен не корректно: с ThreadAbortException, то при определенных условиях помечаем поток как более не прерываемый. Т.е. по сути обрабатываем ThreadAbort. Почему в данном конкретно случае мы обрываем Thread.Abort? Потому что в данном случае мы имеем дело с серверным кодом, а он в свою очередь вне зависимости от наших ошибок вернуть корректные коды ошибок вызывающей стороне. Обрыв потока привел бы к тому что сервер не смог бы вернуть нужный код ошибки пользователю, а это совершенно не правильно. Также оставлен комментарий о Thread.Abort() во время AppDomin.Unload(), что является экстримальной ситуацией для ThreadAbort поскольку такой процесс не остановить и даже если вы сделаете Thread.ResetAbort. Это хоть и остановит сам Abortion, но не остановит выгрузку потока с доменом, в котором он находится: поток же не может исполнять инструкции кода, загруженного в домен, который отгружен.

Класс HttpContext HttpContext.cs

internal void InvokeCancellableCallback(WaitCallback callback, Object state) {
    // ...

    try {
        BeginCancellablePeriod();  // request can be cancelled from this point
        try {
            callback(state);
        }
        finally {
            EndCancellablePeriod();  // request can be cancelled until this point
        }
        WaitForExceptionIfCancelled();  // wait outside of finally
    }
    catch (ThreadAbortException e) {
        if (e.ExceptionState != null &&
            e.ExceptionState is HttpApplication.CancelModuleException &&
            ((HttpApplication.CancelModuleException)e.ExceptionState).Timeout) {

            Thread.ResetAbort();
            PerfCounters.IncrementCounter(AppPerfCounter.REQUESTS_TIMED_OUT);

            throw new HttpException(SR.GetString(SR.Request_timed_out),
                                null, WebEventCodes.RuntimeErrorRequestAbort);
        }
    }
}

Здесь приведен прекрасный пример перехода от неуправляемого асинхронного исключения ThreadAbortException к управляемому HttpException с логгированием ситуации в журнал счетчиков производительности.

Класс HttpApplication HttpApplication.cs

 internal Exception ExecuteStep(IExecutionStep step, ref bool completedSynchronously) 
 {
    Exception error = null;

    try {
        try {

        // ...

        }
        catch (Exception e) {
            error = e;

            // ...

            // This might force ThreadAbortException to be thrown
            // automatically, because we consumed an exception that was
            // hiding ThreadAbortException behind it

            if (e is ThreadAbortException &&
                ((Thread.CurrentThread.ThreadState & ThreadState.AbortRequested) == 0))  {
                // Response.End from a COM+ component that re-throws ThreadAbortException
                // It is not a real ThreadAbort
                // VSWhidbey 178556
                error = null;
                _stepManager.CompleteRequest();
            }
        }
        catch {
            // ignore non-Exception objects that could be thrown
        }
    }
    catch (ThreadAbortException e) {
        // ThreadAbortException could be masked as another one
        // the try-catch above consumes all exceptions, only
        // ThreadAbortException can filter up here because it gets
        // auto rethrown if no other exception is thrown on catch
        if (e.ExceptionState != null && e.ExceptionState is CancelModuleException) {
            // one of ours (Response.End or timeout) -- cancel abort

            // ...

            Thread.ResetAbort();
        }
    }
}

Здесь описывается очень интересный случай: когда мы ждем не настоящий ThreadAbort (мне вот в некотором смысле жалко команду CLR и .NET Framework. Сколько не стандартных ситуаций им приходится обрабатывать, подумать страшно). Обработка ситуации идет в два этапа: внутренним обработчиком мы ловим ThreadAbortException но при этом проверяем наш поток на флаг реальной прерываемости. Если поток не помечен как прерывающийся, то на самом деле это не настоящий ThreadAbortException. Такие ситуации мы должны обработать соответствующим образом: спокойно поймать исключение и обработать его. Если же мы получаем настоящий ThreadAbort, то он уйдет во внешний catch поскольку ThreadAbortException должен войти во все подходящие обработчики. Если он удовлетворяет необходимым условиям, он также будет обработан путем очистки флага ThreadState.AbortRequested методом Thread.ResetAbort().

Если говорить про примеры самого вызова Thread.Abort(), то все примеры кода в .NET Framework написаны так что могут быть переписаны без его использования. Для наглядности приведу только один:

Класс QueuePathDialog QueuePathDialog.cs

protected override void OnHandleCreated(EventArgs e)
{
    if (!populateThreadRan)
    {
        populateThreadRan = true;
        populateThread = new Thread(new ThreadStart(this.PopulateThread));
        populateThread.Start();
    }

    base.OnHandleCreated(e);
}

protected override void OnFormClosing(FormClosingEventArgs e)
{
    this.closed = true;

    if (populateThread != null)
    {
        populateThread.Abort();
    }

    base.OnFormClosing(e);
}

private void PopulateThread()
{
    try
    {
        IEnumerator messageQueues = MessageQueue.GetMessageQueueEnumerator();
        bool locate = true;
        while (locate)
        {
            // ...
            this.BeginInvoke(new FinishPopulateDelegate(this.OnPopulateTreeview), new object[] { queues });
        }
    }
    catch
    {
        if (!this.closed)
            this.BeginInvoke(new ShowErrorDelegate(this.OnShowError), null);
    }

    if (!this.closed)
        this.BeginInvoke(new SelectQueueDelegate(this.OnSelectQueue), new object[] { this.selectedQueue, 0 });
}
TheradAbortException во время AppDomain.Unload

Попробуем отгрузить AppDomain во время исполнения кода, который в него загружен. Для этого искусственно создадим не вполне нормальную ситуацию, но достаточно интересную с точки зрения исполнения кода. В данном примере у нас два потока: один создан для того чтобы получить в нем ThreadAbortException, а другой — основной. В основном мы создаем новый домен, в котором запускаем новый поток. Задача этого потока — уйти в основной домен. Чтобы методы дочернего домена осталиь бы только в Stack Trace. После этого основной домен отгружает дочерний:

class Program : MarshalByRefObject
{
    static void Main()
    {
        try
        {
            var domain = ApplicationLogger.Go(new Program());
            Thread.Sleep(300);
            AppDomain.Unload(domain);

        } catch (ThreadAbortException exception)
        {
            Console.WriteLine("Main AppDomain aborted too, {0}", exception.Message);
        }
    }

    public void InsideMainAppDomain()
    {
        try
        {
            Console.WriteLine($"InsideMainAppDomain() called inside {AppDomain.CurrentDomain.FriendlyName} domain");

            // AppDomain.Unload will be called while this Sleep
            Thread.Sleep(-1);
        }
        catch (ThreadAbortException exception)
        {
            Console.WriteLine("Subdomain aborted, {0}", exception.Message);

            // This sleep to allow user to see console contents
            Thread.Sleep(-1);
        }
    }

    public class ApplicationLogger : MarshalByRefObject
    {
        private void StartThread(Program pro)
        {
            var thread = new Thread(() =>
            {
                pro.InsideMainAppDomain();
            });
            thread.Start();
        }

        public static AppDomain Go(Program pro)
        {
            var dom = AppDomain.CreateDomain("ApplicationLogger", null, new AppDomainSetup
            {
                ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
            });

            var proxy = (ApplicationLogger)dom.CreateInstanceAndUnwrap(typeof(ApplicationLogger).Assembly.FullName, typeof(ApplicationLogger).FullName);
            proxy.StartThread(pro);

            return dom;
        }
    }

}

Происходит крайне интересная вещь. Код выгрузки домена помимо самой выгрузки ищет вызванные в этом домене методы, которые еще не завершили работу в том числе в глубине стека вызова методов и вызывает ThreadAbortException в этих потоках. Это важно, хоть и не очевидно: если домен отгружен, нельзя осуществить возврат в метод, из которого был вызван метод основного домена, но который находится в отгружаемом. Т.е. другими словами AppDomain.Unload может выбрасывать потоки, исполняющие в данный момент код из других доменов. Прервать Thread.Abort в таком случае не получится: исполнять код выгруженного домена вы не сможете, а значит Thread.Abort завершит свое дело, даже если вы вызовите Thread.ResetAbort.

Выводы по ThreadAbortException
  • Это — асинхронное исключение, а значит оно может возникнуть в любой точке вашего кода (но, стоит отметить, что для этого надо постараться);
  • Обычно код обрабатывает только те ошибки, которые ждет: нет доступа к файлу, ошибка парсинга строки и прочие подобные. Наличие асинхронного (в плане возникновения в любом месте кода) исключения создает ситуацию, когда try-catch могут быть не обработаны: вы же не можете быть готовым к ThreadAbort в любом месте приложения. И получается, что это исключение в любом случае породит утечки;
  • Обрыв потока может также происходить из-за выгрузки какого-либо домена. Если в Stack Trace потока существуют вызовы методов отгружаемого домена, поток получит ThreadAbortException без возможности ResetAbort;
  • В общем случае не должно возникать ситуаций, когда вам нужно вызвать Thread.Abort(), поскольку результат практически всегда — не предсказуем.
  • CoreCLR более не содержит ручной вызов Thread.Abort(): он просто удален из класса. Но это не означает что его нет возможности получить.

ExecutionEngineException

В комментарии к этому исключению стоит атрибут Obsolete с комментарием:

This type previously indicated an unspecified fatal error in the runtime. The runtime no longer raises this exception so this type is obsolete

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

void Main()
{
    var counter = 0;

    AppDomain.CurrentDomain.FirstChanceException += (_, args) => {
        Console.WriteLine(args.Exception.Message);
        if(++counter == 1) {
            throw new ArgumentOutOfRangeException();
        }
    };

    throw new Exception("Hello!");
}

Результатом данного кода будет ExecutionEngineException, хотя ожидаемое мной поведение Unhandled Exception ArgumentOutOfRangeException из инструкции throw new Exception("Hello!"). Возможно это показалось страным разработчикам ядра и они посчитали что корректнее выбросить ExecutionEngineException.

Второй вполне простой путь получить ExecutionEngineException — это не корректно настроить маршаллинг в мир unsafe. Если вы напутаете с размерами типов, передадите больше чем надо, чем испортите, например, стек потока, ждите ExecutionEngineException. И это будет логичный, правильный результат: ведь в данной ситуации CLR вошла в состояние, которое она нашла не консистентным. Не понятным, как его восстанавливать. И как результат, ExecutionEngineException.

Отдельно стоит поговорить про диагностику ExecutionEngineException:

  • Используются ли в вашем приложении unsafe библиотеки? Вами или же может сторонними библиотеками. Попробуйте для начала выяснить, где конкретно приложение получает данную ошибку. Если код уходит в unsafe мир и получает ExecutionEngineException там, тогда необходимо тщательно проверить сходимость сигнатур методов: в нашем коде и в импортируемом. Помните что если импортируются модули написанные на Delphi и прочих вариациях языка Pascal, то аргументы должны идти в обратном порядке (настройка производится в DllImport: CallingConvention.StdCall;
  • Подписаны ли вы на FirstChanceException? Возможно его код вызвал исключение. В таком случае просто оберните обработчик в try-catch(Exception) и обязательно сохраните в журнал ошибок происходящее;
  • Может быть ваше приложение частично собрано под одну платформу, а частично — под другую. Попробуйте очистить кэш nuget пакетов, полностью пересобрать приложение с нуля: с очищенными вручную папками obj/bin
  • Проблема иногда бывает в самом фреймворке. Например, в ранних версиях .NET Framework 4.0. В этом случае стоит протестировать отдельный участок кода, который вызывает ошибку — на более новой версии фреймворка;

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

AccessViolationException

Выброс этого исключения — одна из тех новостей, которые не хотелось бы никому получить. А когда получаешь, становится совсем не ясно что с этим делать. AccessViolationException — это исключение «промаха» мимо выделенного для приложения участка памяти и по своей сути выбрасывается при попытке чтения или записи в защищенную область памяти. Здесь под словом «защита» лучше понимать именно попытку работы с еще не выделенным участком памяти или же уже освобожденным. Тут, заметьте не имеется ввиду процесс выделения и освобождения памяти сборщиком мусора. Тот просто размечает уже выделенную память под свои и ваши нужды. Память — она имеет в некоторой степени слоистую структуру. Когда после слоя управления памятью сборщиком мусора идет слой управления выделением памяти операционной системой — из пула доступных фрагментов линейного адресного пространства. Так вот когда приложение промахивается мимо своей памяти и пытается работать с невыделенным участком, тогда и возникает это исключение. Когда оно возникает, вам доступно не так много вариантов для анализа:

  • Если StackTrace уходит в недра CLR, вам сильно не повезло: это скорее всего ошибка ядра. Однако этот случай почти никогда не срабатывает. Из вариантов обхода — либо действовать как-то иначе либо обновить версию ядра если возможно;
  • Если же StackTrace уходит в unsafe код некоторой библиотеки, тут доступны такие варианты: либо вы напутали с настройкой маршаллинга либо же в unsafe библиотеке закралась серьезная ошибка. Тщательно проверьте аргументы метода: возможно аргументы нативного метода имеют другую разрядность или же другой порядок или попросту размер. Проверьте что структуры передаются там где надо — по ссылке, а там, где надо — по значению

Чтобы перехватить такое исключение на данный момент необходимо показать JIT компилятору что это реально необходимо. В противном случае оно перехвачено никак не будет и вы получите упавшее приложение. Однако, конечно же, его стоит перехватывать только тогда, когда вы понимаете что вы сможете его правильно обработать: его наличие может свидетельствовать о произошедшей утечке памяти если она была выделена unsafe методом между точкой его вызова и точкой выброса AccessViolationException и тогда хоть приложение и не будет «завалено», но его работа возможно станет не корректной: ведь перехватив поломку вызова метода вы наверняка попробуете вызвать этот метод еще раз, в будущем. А в этом случае что может пойти не так не известно никому: вы не можете знать каким образом было нарушено состояние приложения в прошлый раз. Однако, если желание перехватить такое исключение у вас сохранилось, прошу посмотреть на таблицу возможности перехвата этого исключения в различных версиях .NET Framework:

Версия .NET Framework AccessViolationExeception
1.0 NullReferenceException
2.0, 3.5 AccessViolation перехватить можно
4.0+ AccessViolation перехватить можно, но необходима настройка
.NET Core AccessViolation перехватить нельзя

Т.е. другими словами, если вам попалоось очень старое приложение на .NET Framework 1.0, ~~покажите его мне~~ вы получите NRE, что будет в некоторой степени обманом: вы отдали указатель со значением больше нуля, а получили NullReferenceException. Однако, на мой взгляд, такое поведение обосновано: находясь в мире управляемого кода вам меньше всего должно хотеться изучать типы ошибок неуправляемого кода и NRE — что по сути и есть «плохой указатель на объект» в мире .NET — тут вполне подходит. Однако, мир был бы прекрасен если бы все так было просто. В реальных ситуациях пользователям решительно не хватало этого типа исключений и потому — достаточно скоро — его ввели в версии 2.0. Просуществовав несколько лет в перехватываемом варианте, исключение перестало быть перехватываемым, однако появилась специальная настройка, которая позволяет включить перехват. Такой порядок выбора в команде CLR в целом на каждом этапе выглядит достаточно обоснованным. Посудите сами:

  • 1.0 Ошибка промаха мимо выделенных участков памяти должна быть именно исключительной ситуацией потому как если приложение работает с каким-либо адресом, оно его откуда-то получило. В managed мире этим местом является оператор new. В unmanaged мире — в целом любой участок кода может выступать точкой для возникновения такой ошибки. И хотя с точки зрения философии смысл обоих исключений диаметрально противоположен (NRE — работа с не проинициализированным указателем, AVE — работа с некорректно проинициализированным указателем), с точки зрения идеологии .NET некорректно проинициализированных указателей быть не может. Оба случая можно свести к одному и придать философский смысл: не корректно заданный указатель. А потому давайте так и сделаем: в обоих случаях будем выбрасывать NullReferenceException.
  • 2.0 На ранних этапах существования .NET Framework оказалось что кода, который наследуется через COM библиотеки больше собственного: существует огромная кодовая база коммерческих компонент для взаимодействия с сетью, UI, БД и прочими подсистемами. А значит, вопрос получения именно AccessViolationException все-таки стоит: неверная диагностика проблем может сделать процесс поимки проблемы более дорогим. В .NET Framework введено исключение AccessViolationException.
  • 4.0 .NET Framework укоренился, потеснив традиционную разработку на низкоуровневых узыках программирования. Резко сокращено количество COM компонент: практически все основные задачи уже решаются в рамках самого фреймворка, а работа в unsafe кодом начинает считаться чем-то странным, неправильным. В этих условиях можно вернуться к идеологии, введенной в фреймворк с самого начала: .NET — он только для .NET. Unsafe код — это не норма, а вынужденное состояние, а потому идеологичность наличия перехвата AccessViolationException идет вразрез с идеологией понятия фреймворк — как платформа (т.е. имитация полной песочницы со своими законами). Однако мы все еще находимся в реалиях платформы, на которой работаем и во многих ситуациях перехватывать это исключение все еще необходимо: вводим специальный режим перехвата: только если введена соответствующая конфигурация;
  • .NET Core Наконец, сбылась мечта команды CLR: .NET более не предполагает законности работы с unsafe кодом, а потому существование AccessViolationException теперь вне закона даже на уровне конфигурации. .NET вырос настолько чтобы самостоятельно устанавливать правила. Теперь существование этого исключения в приложении приведет его к гибели, а потому любой unsafe код (т.е. сам CLR) обязан быть безопасным с точки зрения этого исключения. Если оно появляется в unsafe библиотеке, с ней просто не будут работать, а значит разработчики сторонних компонент на unsafe языках будут более аккуратными и обрабатывать его — у себя.

Вот так, на примере одного исключения можно проследить историю становления .NET Framework как платформы: от неуверенного подчинения внешним правилам до самостоятельного установления правил самой платформой.

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

  • Добавить в секцию configuration/runtime следующий код: <legacyCorruptedStateExceptionsPolicy enabled="true|false"/>
  • Для каждого метода, где необходимо обработать AccessViolationException, надо добавить два атрибута: HandleProcessCorruptedStateExceptions и SecurityCritical. Эти атрибуты позволяют включить обработку Corrupted State Exceptions, для конкретных методов, а не для всех вообще. Эта схема очень правильная, поскольку вы должны быть точно уверены что хотите их обрабатывать и знать, где: иногда более правильный вариант — просто завалить приложение на бок.

Для примера включения обработчика CSE и их примитивной обработки рассмотрим следующий код:

[HandleProcessCorruptedStateExceptions]
[SecurityCritical]
public bool TryCallNativeApi()
{
    try
    {
        // Запуск метода, который может выбросить AccessViolationException
    }
    catch (Exception e)
    {
        // Журналирование, выход
        System.Console.WriteLine(e.Message);
        return false;
    }

  return true;
}

NullReferenceException

SecurityException

OutOfMemoryException

StackOverflowException

Выводы

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

try {
    UnmanagedApiWrapper.SomeMethod();
} catch (WrappedException ex) when (ex.ErrorCode == ErrorCodes.DeviceNotFound)
{
    // ...
} catch (WrappedException ex) when (ex.ErrorCode == ErrorCodes.ConnectionLost)
{
    // ...
} catch (WrappedException ex) when (ex.ErrorCode == ErrorCodes.TimeOut)
{
    // ...
} catch (WrappedException ex) when (ex.ErrorCode == ErrorCodes.Disconnected)
{
    // ...
}

Согласитесь, это выглядит интереснее чем один блок catch и switch внутри с throw; в default блоке. Это выглядит более разграниченным, более правильным с точки зрения разделения ответственности. Ведь исключение с кодом ошибки по своей сути — ошибка дизайна, а фильтрация — это выправка нарушения архитектуры, переводя в концепцию раздельных типов исключений.

Виды исключительных ситуаций

TODO

  • FailFast

Обработка ошибок

  1. Возврат кода ошибки
  2. Исключения

Возврат кода ошибки

enum class Error
{
    Success,
    Failure
};

Error doSomething()
{
    return Error::Success;
}

if (doSomething() != Error::Success)
{
    showError();
}

+ Простота

— Ошибку можно проигнорировать

— Делает код громозким

auto data = readData("data.json");

Json data;
auto error = readData(data, "data.json");
if (error != Success)
{
    ...
}

Поддержка со стороны C++

enum class HttpError
{
    NoError = 200,
    NotFound = 404
};

class HttpCategory:
    public std::error_category
{
public:
    const char* name() const noexcept override
    {
        return "http";
    }

    std::string message(int code) const override
    {
        switch (code)
        {
        case 200: return "ok";
        case 404: return "not found";
        }
        assert(!"invalid error code");
    }
};

std::error_code make_error_code(HttpError error)
{
    static const HttpCategory instance;
    return std::error_code(
        static_cast<int>(error),
        instance);
}
std::error_code download(const std::string& url)
{
    return make_error_code(HttpError::NotFound);
}

const auto error = download("http://1.1.1.1");
if (error)
{
    std::cerr << error << 'n';
    std::cerr << error.message() << 'n';
}

Исключения

— Вопросы производительности

— При неправильном использовании могут усложнить программу

+ Нельзя проигнорировать

struct Error
{
    std::string message_;
    const char* fileName_;
    int line_;
    Error(const std::string& message,
        const char* fileName, int line)
        : message_(message)
        , fileName_(fileName)
        , line_(line)
    {
    }
};

void doSomething()
{
    throw Error(
        "doSomething error", __FILE__, __LINE__);
}

try
{
    doSomething();
}
catch (const Error& error)
{
    showError();
}

Что такое исключительная ситуация?

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

Гарантии безопасности исключений (exception safety)

  1. Гарантировано искючений нет (No-throw guarantee)

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

  1. Строгая гарантия (Strong exception safety)

Также известна как коммит ролбек семантика (commit/rollback semantics). Операции могут завершиться неудачей, но неудачные операции гарантированно не имеют побочных эффектов, поэтому все данные сохраняют свои исходные значения.

std::vector<int> source = ...;
try
{
    std::vector<int> tmp = source;
    tmp.push_back(getNumber());
    tmp.push_back(getNumber()); <-- Исключение
    tmp.push_back(getNumber());
    source.swap(tmp);
}
catch (...)
{
    return;
}
  1. Базовая гарантия (Basic exception safety)

Выполнение неудачных операций может вызвать побочные эффекты, но все инварианты сохраняются и нет утечек ресурсов (включая утечку памяти). Любые сохраненные данные будут содержать допустимые значения, даже если они отличаются от того, что они были до исключения.

source.push_back(getNumber());
source.push_back(getNumber()); <-- Исключение
source.push_back(getNumber());
  1. Никаких гарантий (No exception safety)

Поиск подходящего обработчика

class Error {};

class ArgumentError : public Error
{
    std::string message_;
public:
    ArgumentError(std::string&& message);
    const std::string& getMessage() const;
};

File openFile(const std::string& name)
{
    if (name.empty())
        throw ArgumentError("empty file name");
}

try
{
    auto file = openFile("data.json");
    auto json = file.readAll();
}
catch (const ArgumentError& error)
{
    std::cerr << error.getMessage();
}
catch (const Error& error)
{
}
catch (...)
{
}
  1. Поиск подходящего обработчика идет в порядке следования обработчиков в коде
  2. Полного соответствия типа не требуется, будет выбран первый подходящий обработчик
  3. Если перехватывать исключение по значению, то возможна срезка до базового класса
  4. Если наиболее общий обработчик идет раньше, то более специализированный обработчик никогда не будет вызван
  5. Три точки — перехват любого исключения

Исключения ОС — не исключения С++, например, деление на ноль. Для их обработки нужно использовать средства предоставляемые конкретной платформой

Раскрутка стека

struct A {};
struct Error {};
struct FileError : public Error {};

void foo()
{
    A a1;
    throw Error();
}

void bar()
{
    A a2;
    try
    {
        A a3;
        foo();
    }
    catch (const FileError&)
    {
    }
}

bar();

Поиск подходящего обработчика вниз по стеку вызовов с вызовом деструкторов локальных объектов — раскрутка стека.

Если подходящий обработчик не был найден вызывается стандартная функция terminate.

terminate

Вызывает стандартную функцию С — abort.

abort — аварийное завершение программы, деструкторы объектов вызваны не будут.

Поведение terimnate можно изменить установив свой обработчик функцией set_terminate.

Где уместен catch (…)?

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

int main()
{
    try
    {
        ...
    }
    catch (...)
    {
        std::cerr << "unknown error";
    }
}

Перезапуск исключения

try
{
    foo();
}
catch (...)
{
    std::cerr << "something wrong";
    throw;
}

noexcept

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

Исключения в деструкторе

Исключение покинувшее деструктор во время раскрутки стека или у глобального/статического объекта приведет к вызову terminate.

Начиная с С++11 все деструкторы компилятором воспринимаются как помеченные noexcept — теперь исключения не должны покидать деструктора никогда.

Исключения в конструкторе

Клиент либо получает объект в консистентном состоянии, либо не получает ничего.

class Socket
{
    static constexpr size_t BufferSize = 2048;
    char* buffer_;
public:
    explicit Socket(const std::string& address)
        : buffer_(new char[BufferSize]) // <- утечка
    {
        if (address.empty())
            throw ArgumentError();
    }
    
    ~Socket()
    {
        delete[] buffer_; // Не будет вызван
    }
};

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

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

class exception
{
public:
    explicit exception(char const* const message);

    virtual char const* what() const;

Управление ресурсами

Используем идеому RAII (Resource Acquire Is Initialization):

struct Buffer
{
    explicit Buffer(size_t size)
        : data_(new char[size])
    {
    }
    
    ~Buffer()
    {
        delete[] data_;
    }
    
    char* data_;
};
class Socket
{
    static constexpr size_t BufferSize = 2048;
    Buffer buffer_;
public:
    explicit Socket(const std::string& address)
        : buffer_(BufferSize)
    {
        if (address.empty())
            throw ArgumentError();
    }
};

Исключения под капотом

struct A
{
    A() {}
    ~A() {}	
};

void bar() noexcept
{
}

void foo()
{
    A a;
    bar();
}
A::A() [base object constructor]:
        ret
A::~A() [base object destructor]:
        ret
bar(): 
        ret
foo():
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        lea     rdi, [rbp - 8]
        call    A::A() [base object constructor]
        call    bar()
        lea     rdi, [rbp - 8]
        call    A::~A() [base object destructor]
        add     rsp, 16
        pop     rbp
        ret
Убираем noexcept
struct A
{
    A() {}
    ~A() {}	
};

void bar() {}

void foo()
{
    A a;
    bar();
}
A::A() [base object constructor]:
        ret
A::~A() [base object destructor]:
        ret
bar(): 
        ret
foo():
        call    A::A() [base object constructor]
        call    bar()
        jmp     .LBB1_1
.LBB1_1:
        call    A::~A() [base object destructor]
        ret
.LBB1_2: # landing pad
        call    A::~A() [base object destructor]
        call    _Unwind_Resume
void _Unwind_Resume(struct _Unwind_Exception * object);

Появился специальный блок (landing pad) используемый при раскрутке стека.

Добавляем блок catch
struct A
{
    A() {}
    ~A() {}	
};

void bar() {}

void baz() noexcept {}

void foo()
{
    A a;
    try
    {
        bar();
    }
    catch (...)
    {
        baz();
    }
}
foo():
        call    A::A() [base object constructor]
        call    bar()
        jmp     .LBB2_1
.LBB2_1:
        jmp     .LBB2_5
.LBB2_2:
        call    __cxa_begin_catch
        call    baz()
        call    __cxa_end_catch
        jmp     .LBB2_4
.LBB2_4:
        jmp     .LBB2_5
.LBB2_5:
        call    A::~A() [base object destructor]
        ret
.LBB2_6:
        call    A::~A() [base object destructor]
        call    _Unwind_Resume
Выбрасываем исключение
struct A
{
    A() {}
    ~A() {}	
};

void bar()
{
    throw A();
}

void baz() noexcept
{
}

void foo()
{
    A a;
    try
    {
        bar();
    }
    catch (...)
    {
        baz();
    }
}
bar():
        call    __cxa_allocate_exception
        call    A::A() [base object constructor]
        jmp     .LBB0_1
.LBB0_1:
        call    __cxa_throw
.LBB0_2: # landing pad
        call    __cxa_free_exception
        call    _Unwind_Resume
foo():
        call    A::A() [base object constructor]
        call    bar()
        jmp     .LBB4_1
.LBB4_1:
        jmp     .LBB4_5
.LBB4_2:
        call    __cxa_begin_catch
        call    baz()
        call    __cxa_end_catch
        jmp     .LBB4_4
.LBB4_4:
        jmp     .LBB4_5
.LBB4_5:
        call    A::~A() [base object destructor]
        ret
.LBB4_6:
        call    A::~A() [base object destructor]
        call    _Unwind_Resume
typeinfo name for A:
        .asciz  "1A"
typeinfo for A:
        .quad   vtable for __cxxabiv1::__class_type_info+16
        .quad   typeinfo name for A
void __cxa_throw(
    void* thrown_exception,
    struct std::type_info * tinfo,
    void (*dest)(void*));
Компиляция с включенной оптимизацией
struct A
{
    A() {}
    ~A() {}	
};

void bar(int x)
{
    if (x == 1)
        throw A();
}

void baz() noexcept
{
}

void foo(int x)
{
    A a;
    try
    {
        bar(x);
    }
    catch (...)
    {
        baz();
    }
}
bar(int):
        cmp     edi, 1
        je      .LBB0_2
        ret
.LBB0_2:
        call    __cxa_allocate_exception
        call    __cxa_throw
foo(int):
        cmp     edi, 1
        je      .LBB3_1
        ret
.LBB3_1:
        call    __cxa_allocate_exception
        call    __cxa_throw
.LBB3_3:
        call    __cxa_begin_catch
        jmp     __cxa_end_catch         # TAILCALL
typeinfo name for A:
        .asciz  "1A"
typeinfo for A:
        .quad   vtable for __cxxabiv1::__class_type_info+16
        .quad   typeinfo name for A

Управление памятью

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

  1. unique_ptr
  2. shared_ptr / weak_ptr

unique_ptr

  • Монопольное владение памятью, в конструкторе захват, в деструкторе освобождение
  • Копирование запрещено, перемещение разрешено
std::unique_ptr<MyClass> x(new MyClass());
auto y = std::make_unique<MyClass>(); // C++14

std::unique_ptr<char[]> z(new char[1024]);

shared_ptr

  • Совместное владение памятью
  • Копирование увеличивает счетчик ссылок
  • В деструкторе счетчик уменьшается и если становится равным 0, то объект уничтожается
std::shared_ptr<MyClass> x(new MyClass());
auto y = std::make_shared<MyClass>();

Точки следования (sequence points)

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

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

// Может быть утечка
foo(
    std::shared_ptr<MyClass>(new MyClass()), 
    bar());

Компилятор может заменить это выражение на следующее:

auto tmp1 = new MyClass();
auto tmp2 = bar();
auto tmp3 = std::shared_ptr<MyClass>(tmp1);
foo(tmp1, tmp3);

Если из bar вылетит исключение, то объект на который указывает tmp1 будет некому удалить.

Решение 1:

std::shared_ptr<MyClass> x(new MyClass());
foo(x, bar()); // ok

Решение 2:

foo(std::make_shared<MyClass>(), bar()); // ok

Местонахождение точек:

  1. В конце каждого полного выражения — ;
  2. В точке вызова функции после вычисления всех аргументов
  3. Сразу после возврата функции, перед тем как любой другой код из вызвавшей функции начал выполняться
  4. После первого выражения (а) в следующих конструкциях:
    a || b 
    a && b
    a, b
    a ? b : c

Если программа пытается модифицировать одну переменную дважды не пересекая точку следования, то это ведет к неопределенному поведению (undefined behavior):

int x = 0;
x = x++; // <-- UB

int i = 0;
i = i++ + ++i; // <-- UB

Схематичное устройство shared_ptr

#include <cassert>
#include <iostream>

template <class T>
class SharedPtr
{
    struct Data
    {
        T* object_;
        int counter_;
    };

    Data* data_;

    void release()
    {
        --data_->counter_;
        if (data_->counter_ == 0)
        {
            delete data_->object_;
            delete data_;
        }
    }

public:
    SharedPtr(T* object = nullptr)
        : data_(new Data { object, 1 })
    {
    }

    ~SharedPtr()
    {
        release();
    }

    SharedPtr(const SharedPtr<T>& copied)
        : data_(copied.data_)
    {
        ++data_->counter_;
    }

    SharedPtr& operator=(const SharedPtr<T>& copied)
    {
        if (data_ == copied.data_)
            return *this;

        release();

        data_ = copied.data_;
        ++data_->counter_;
        return *this;
    }

    T& operator*()
    {
        return *data_->object_;
    }

    const T& operator*() const
    {
        return *data_->object_;
    }

    T* operator->()
    {
        return data_->object_;
    }

    const T* operator->() const
    {
        return data_->object_;
    }
};

struct A
{
    A() { std::cout << "A" << std::endl; }
    ~A() { std::cout << "~A" << std::endl; }
    void foo() { std::cout << this << std::endl; }
};

SharedPtr<A> foo(SharedPtr<A> x)
{
    return x;
}

int main()
{
    auto x = foo(new A());
    auto y = x;
    y->foo();
    (*x).foo();
    y = nullptr;
    return 0;
}
Предпочитайте make_shared
auto x = std::shared_ptr<MyClass>(new MyClass());
auto x = std::make_shared<MyClass>();
  1. Нет дублирования (MyClass два раза)
  2. Безопасно в вызове функций
  3. Оптимально — 1 вызов new вместо 2

Проблема циклических ссылок

class Widget;

class Window
{
    std::vector<std::shared_ptr<Widget>> children_;
};

class Widget
{
    std::shared_ptr<Window> parent_;
};

Winwow не может быть удален, так как в Widget жив shared_ptr на него, а Widget в свою очередь не может быть удален, так как жив Window.

Ключевой вопрос С++ — кто кем владеет

weak_ptr

class Widget;

class Window
{
    std::vector<std::shared_ptr<Widget>> children_;
};

class Widget
{
    std::weak_ptr<Window> parent_;
};

weak_ptr не принимает владение объектом, но располагая weak_ptr всегда можно узнать жив ли объект и если жив, то получить на него shared_ptr.

std::shared_ptr<A> x;
std::weak_ptr<A> weak = x;
std::shared_ptr<A> y = weak.lock();
if (y)
{
    ...
}

enable_shared_from_this

Иногда нужно получить shared_ptr от самого себя, например, очень актуально при асинхронном взаимодействии, когда время жизни объекта не определено.

class A
{
    std::shared_ptr<A> getSharedPtr()
    {
        // Приведет к многократному удалению
        return std::shared_ptr<A>(this);
    }
};

Решение:

class A
    : public std::enable_shared_from_this<A>
{
    std::shared_ptr<A> getSharedPtr()
    {
        return shared_from_this(); // Ok
    }
};
Ограничения enable_shared_from_this
class A
    : public std::enable_shared_from_this<A>
{
    A()
    {
        shared_from_this(); // throw std::bad_weak_ptr
    }

    ~A()
    {
        shared_from_this(); // throw std::bad_weak_ptr
    }
};

Также перед использованием shared_from_this на объект уже должен ссылаться shared_ptr:

auto a = std::make_shared<A>();
auto b = a->getSharedPtr();

Практическая часть

Написать функцию для форматирования строки, поддерживаться должен любой тип, который может быть выведен в поток вывода. Формат строки форматирования:

Номер в фигурных скобках — номер аргумента. Если аргументов меньше, чем число в скобках, и в случае прочих ошибок выбрасывать исключение std::runtime_error

Пример:

auto text = format("{1}+{1} = {0}", 2, "one");
assert(text == "one+one = 2");

Фигурные скобки — зарезервированный символ, если встречаются вне контекста {n} выбрасывать исключение std::runtime_error

EOF

Определение
понятия ошибки и исключения.
Классификация
ошибок.
Общий способ обработки
ошибок.
Обработка ошибок в языке Java.

— исключение в Java.

— причины возникновения исключений
в Java

— конструкция обработки исключения

— асинхронные исключения

иерархия исключений

классы Exception и RuntimeException

— класс Error
Приложения.

Определение понятия ошибки и исключения.

В
качестве введения рассмотрим определения
понятия «ошибка». Начнем с наиболее
общего трактования этого:
В самом
общем случае под ошибкой понимается
какой-то сбой в программе на этапе ее
выполнения.
По определению стандарта
ISO
9241-13 ошибка это – несоответствие между
целями пользователя и ответом системы.

Ошибкой также можно назвать
недокументированные или нежелательные,
«побочные» реакции программы на
те или иные действия пользователя равно
как и при использовании ее одновременно
с другим программами или на другой
аппаратной платформе.
В книге «К.
Браун, Р. Калбертсон, Г. Кобб. Быстрое
тестирование» приводится такое
определение программных ошибок: «Говоря
простыми словами, программная ошибка
— не что иное, как изъян в разработке
программного продукта, который вызывает
несоответствие ожидаемых результатов
выполнения программного продукта и
фактически полученных результатов.
Дефект может возникнуть на стадии
кодирования, на стадии формулирования
требований или на стадии проектирования,
либо же его причина может крыться в
некорректной конфигурации или данных.
Дефектом может быть также что-то другое,
что не соответствует ожиданиям заказчика
и что может быть, а может и не быть
определено в спецификации программного
продукта».
Для исключения Г.
Шилдт дает такое определение: исключение
– это нештатная ситуация , возникающая
во время выполнения последовательности
кода.
Как можно заметить, под понятием
ошибки и исключительной ситуацией можно
подразумевать одно и то же.
Исключение
— это некое специальное событие, которое
сигнализирует об ошибке в программе.
Другими
словами, исключение – это ошибка времени
выполнения.

Классификация
ошибок.

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

Ошибки компоновки, как следует из
названия, связаны с проблемами,
обнаруженными при разрешении внешних
ссылок. В разных языках может осуществляться
на различных этапах.
Логические
ошибки имеют разную природу. Так они
могут следовать из ошибок, допущенных
при проектировании, например, при выборе
методов, разработке алгоритмов или
определении структуры классов, а могут
быть непосредственно внесены при
кодировании модуля.
Ошибки
компиляции:
1. Синтаксические
ошибки.
2. Семантические ошибки.

1. Описания  идентификаторов.

1. Неинициализированный
указатель.
2.
Неинициализированная переменная.

3. Ошибочная инициализация.

2. Статический контроль
типов.
1. Не корректное
присваивание.
2. Не
корректная операция.

3. Не корректная передача параметров.

Ошибки
выполнения:
1. Синхронные ошибки.

1. Ошибки определения данных.

1. Ошибки передачи.
2.
Ошибки преобразования.
3.
Ошибки перезаписи.
4. Ошибочные
данные.
5. Динамический
контроль типов.
6. Ошибки
индексации.
2. Ошибки накопления
погрешностей.
1. Игнорирование
способов уменьшения погрешностей.

2. Переполнение разрядной
сетки.
3. Арифметические ошибки.

4. Ссылочные ошибки.
5. Ошибки
сети.
6. Ошибки ввода/вывода.
2.
Асинхронные ошибки.
1. Ошибки
виртуальной машины.
2. Ошибки системы.

Ошибки
компоновки
1. Ошибки получения
данных по внешним ссылкам.
2.
Объединение модулей
1. Ошибки
обнаружения модулей.
2.
Ошибка состыковки списков параметров
модулей.

Логические
ошибки
1. Ошибки проектирования

1. Неприменимый метод.

2. Неверный алгоритм.

3. Неверная структура данных.
2.
Ошибки кодирования
1.
Некорректное вычисление.

2. Ошибки реализации алгоритмов.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]

  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #

Ошибки и исключительные ситуации

Мы завершаем путь. Всё это время мы стремились не допускать ошибок в выражениях, операторах, объявлениях, определениях, макроопределениях, программах. Но до сих пор у нас нет чёткого представления о том, что такое ошибка.

В общем случае под ошибкой мы будем понимать несоответствие правилу, алгоритму. Это рабочее определение. Конечно, правила бывают нечёткими, алгоритмы — некорректными. Это неважно. В любом случае можно сказать, что «всё не так, как должно быть». И этого достаточно.

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

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

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

Транслятор распознаёт константные выражения различной сложности. Он способен самостоятельно производить арифметические вычисления. Так что с вопросами определения статических массивов также не возникает никаких проблем.

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

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

На этапе создания исполнительного модуля программа (или система) компоновки способна распознать объявленные и неопределённые переменные и функции, а также незавершённые объявления классов.

В ряде случаев на этапе создания объектного кода могут выявляться неопределённые и неиспользуемые переменные, и даже потенциально опасные фрагменты кода. Транслятор может предупредить об использовании в выражениях неинициализированных переменных.

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

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

Мы не раз подчёркивали, что в C++ часто возникают ситуации, при которых ответственность за правильность выполнения операций, операторов и даже отдельных функций целиком возлагается на программиста. Арифметические вычисления (деление на нуль), преобразования типа, работа с индексами и адресами, корректная формулировка условий в операторах управления, работа с потоками ввода-вывода — это далеко не полный перечень неконтролируемых в C++ ситуаций.

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

Различают синхронные и асинхронные исключительные ситуации.

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

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

Реакция на исключительную ситуацию называется исключением.

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

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

#define MAX 10
int PushIntArray(int* const, int, int);
void main()
{
int intArray[MAX];
int IndexForArray, ValueForArray;
:::::
for (;;)
{
:::::
// Значения IndexForArray и ValueForArray меняются в цикле.
if (!PushIntArray(intArray, IndexForArray, ValueForArray))
 {
 cout << "Некорректное значение индекса" << endl;
 IndexForArray = 0;
 }
:::::
}
:::::
}
int PushIntArray(int* const keyArray, int index, int keyVal)
{
 if (index >= 0 && index < MAX)
 {
  keyArray[index] = keyVal;// Спрятали значение и сообщили об успехе.
  return 1;
 }
 else
 return 0; // Сообщили о неудаче.
}

Перед нами самый простой вариант исключения как формы противодействия синхронной исключительной ситуации. Из функции main вызывается функция, PushIntArray, которой в качестве параметров передаются адрес массива, значение индекса и значение, предназначенное для сохранения в массиве.

Функция PushIntArray проверяет значение индекса и возвращает соответствующее сообщение. Эта функция выявляет возможные ошибки и уведомляет о них вызывающую функцию. Подобное сообщение о неудаче можно рассматривать как прообраз генерации (или возбуждения) исключения.

Вызывающая функция может корректировать значение индекса: исправление выявленных ошибок (то есть реакция на исключение) — компетенция вызывающей функции.

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

#include <iostream.h>
#define EXDIVERROR 0.0
/*
Здесь может быть определено любое значение. Это не меняет сути дела.
Так кодируется значение, предупреждающее об ошибке. Не самая хорошая
идея: некоторые корректные значения всегда будут восприниматься как
уведомления об ошибке.
*/
float exDiv (float, float);
void main()
{
float val1, val2;
:::::
if (exDiv(val1, val2) == EXDIVERROR)
{
:::::
   cout << "exDiv error…";
// Здесь можно попытаться исправить ситуацию.
:::::
}
}
float exDiv (float keyVal1, float keyVal2)
{
 if (val2) return keyVal1/keyVal2;
 return    EXDIDERROR;
}

Функция exDiv может быть модифицирована следующим образом: возвращаемое целочисленное значение сообщает о ходе вычисления, а непосредственно сам результат вычисления передаётся по ссылке.

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

#include <iostream.h>
int exDiv (float, float, float&);
void main()
{
float val1, val2, resDiv;
:::::
if (!exDiv(val1, val2, resDiv))
{
:::::
   cout << "exDiv error…";
:::::
}
}
int exDiv (float keyVal1, float keyVal2, float& keyRes)
{
 if (val2) {keyRes = keyVal1/keyVal2; return 1;}
 return 0;
}

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

#include <iostream.h>
class DivAnsver
{
 public:
 int   res;
 float fValue;
// Конструктор.
 DivAnsver(): res(1), fValue(0.0) {};
// ctorИнициализаторы в действии!
};
DivAnsver exDiv (float, float);
void main()
{
DivAnsver Ansver;
Ansver = exDiv(0.025, 0.10);
cout << Ansver.fValue << "..." << Ansver.res << endl;
Ansver = exDiv(0.025, 0.0);
cout << Ansver.fValue << "..." << Ansver.res << endl;
}
DivAnsver exDiv (float val1, float val2)
{
 DivAnsver Ans;
 if (val2) Ans.fValue = val1/val2;
 else      Ans.res = 0;
 return    Ans;
}

Функция exDiv возвращает значение объекта Ans (предопределённый конструктор копирования об этом позаботится). При этом, если деление возможно, значение данного-члена res оказывается равным единице, а fValue принимает значение частного от деления. В противном случае res устанавливается в нуль и объект Ans становится исключением.

Подобным изменениям можно подвергнуть объявление класса, реализующего стек: возвращаемое функцией pop() значение объекта-представителя шаблонного класса мог бы содержать результат выполнения функции и значение содержимого стека.

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

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

Казалось бы, всё хорошо и на этом можно было бы остановиться. Однако, нет пределов совершенству!

Существует целый ряд проблем, связанных с подобным способом организации программного кода. Рассмотрим некоторые из них.

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

С ростом числа вариантов возвращаемых значений становится всё более актуальной проблема разделения «положительных» и «отрицательных» ответов.

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

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

Назад |
Содержание |
Вперед

Новости мира IT:

  • 30.01 — Роскомнадзор получил более 44 тыс. жалоб о неправомерной обработке персональных данных в 2022 году
  • 30.01 — На Apple подали в суд из-за сбора данных пользователей
  • 30.01 — «Ростелеком», возможно, интересуется покупкой «Мегафона»
  • 30.01 — В идеале Apple стремится создать очки дополненной реальности, которые можно носить весь день
  • 30.01 — Российские мобильные операторы перешли на отечественную техподдержку
  • 30.01 — Продажа «Билайна» российскому топ-менеджменту затягивается
  • 27.01 — «Яндекс» попал в десятку самых посещаемых сайтов в мире
  • 27.01 — В списке кредиторов криптобиржи FTX оказались Apple, Google, Amazon, Microsoft, а также авиакомпании, СМИ и университеты
  • 27.01 — IBM и SAP показали хорошие квартальные результаты, но всё равно сокращают рабочие места
  • 27.01 — От антимонопольного иска против Google выиграет Apple и другие компании
  • 26.01 — Власти США подали иск, чтобы заблокировать сделку между Microsoft и Activision Blizzard
  • 26.01 — Apple намерена вытеснить сервисы Google из iPhone — грядут «яблочные» заменители для рекламы и даже поиска
  • 26.01 — Минюст США потребовал от Google раздробить рекламное подразделение и выплатить компенсации за завышение цен
  • 26.01 — ЕС готовит антимонопольное расследование против Microsoft из-за сервиса Teams
  • 26.01 — Российские компании активизировали наем IT-специалистов — число вакансий растёт
  • 26.01 — Создан простой для вживления мозговой имплантат, который позволит набирать текст силой мысли
  • 25.01 — Представлен стабильный релиз Wine 8.0
  • 25.01 — «Яндекс» научил нейросеть расшифровывать архивные документы даже с дореволюционной орфографией
  • 25.01 — Объём атак на российских операторов связи снизился в 2022 году
  • 25.01 — В России возникли проблемы с доступом к Apple App Store и некоторым другим сервисам

Архив новостей

  • Perfect Quality Hosting – компания со своим подходом к услугам аренды серверов
  • Создали сайт – выберите для него достойный хостинг
  • Гиперконвергентные решения: что нужно знать при переходе на них

Исключение != ошибка

Время на прочтение
4 мин

Количество просмотров 27K

Многие программисты почему-то считают, что исключения и ошибки — это одно и то же. Кто-то постоянно кидает exception, кто-то через errorHandler превращает ошибки в исключения. Некоторые пытаются увеличить производительность, используя исключения. Но, на самом деле, exception и ошибки — это совершенно разные механизмы. Не надо одним механизмом заменять другой. Они созданы для разных целей.

Когда появился php5 с исключениями, а затем ZendFramework, который всегда кидает исключения — я не мог понять: чем же exception лучше моего любимого trigger_error()? Долго думал, обсуждал с коллегами и разобрался в этом вопросе. Теперь я чётко знаю, где использовать trigger_error(), а где throw new Exception().

В чём же принципиальная разница между ними?

Ошибки

Ошибки — это то, что нельзя исправить, об этом можно только сообщить: записать в лог, отправить email разработчику и извинится перед пользователем. Например, если мой движок не может подключиться к БД, то это ошибка. Всё. Точка. Без БД сайт не работает, и я не могу с этим ничего сделать. Поэтому я вызываю ales_kaput() и trigger_error(), а мой errorHandler отправит мне email и покажет посетителю сообщение «Извините, сайт не работает».

Exception

Исключения — это не ошибки, это всего лишь особые ситуации, которые нужно как-то обработать. Например, если в калькуляторе вы попробуете разделить на ноль, то калькулятор не зависнет, не будет отсылать сообщения разработчику и извинятся перед вами. Такие ситуации можно обрабатывать обычным if-ом. Строго говоря, исключения — это конструкция языка позволяющая управлять потоком выполнения. Это конструкция, стоящая в одном ряду с if, for и return. И всё. Этот механизм ничем более не является. Только управление потоком.

Их основное предназначение: пробрасывать по каскаду. Покажу это на примере: есть три функции, которые вызывают друг друга каскадом:

<?php
a();
function a()
{
	b();
}


function b()
{
	c(99);
}

function c($x)
{
	if ($x === 0) {
		// Некоторая особенная ситуация,
		// которая должна остановить выполнение функций c() и b(),
		// а функция a() должна узнать об этом
	}
	return 100 / $x;
}

Эту задачу можно было бы решить без механизма exception. Например, можно заставить все функции возвращать специальный тип (если ты матёрый пэхапэшник, то должен вспомнить PEAR_Error). Для простоты я обойдусь null-ом:

<?php
a();
function a()
{
	echo 'a-begin';

	$result = b();
	if ($result === null) {
		echo 'Делить на ноль нехорошо';
		return;
	}

	echo 'a-stop';
}

function b()
{
	echo 'b-begin';

	$result = c(0);
	if ($result === null) {
		return null;
	}

	echo 'b-stop';
	return true;
}

function c($x)
{
	echo 'c-begin';

	if ($x === 0) {
		return null;
	}

	echo 'c-stop';
	return 100 / $x;
}

Результат работы:

a-begin
b-begin
c-begin
Делить на ноль нехорошо

Задача выполнена, но, обратите внимание, мне пришлось модифицировать промежуточную функцию b(), чтобы она пробрасывала результат работы нижестоящей функции выше по каскаду. А если у меня каскад из 5 или 10 функций? То мне пришлось бы модифицировать ВСЕ промежуточные функции. А если исключительная ситуация в конструкторе? То мне пришлось бы подставлять костыли.

А теперь решение с использованием Exception:

a();
function a()
{
	echo 'a-begin';

	try {
		b();
		echo 'a-stop';
	} catch (Exception $e) {
		echo $e->getMessage();
	}
}


function b()
{
	echo 'b-begin';
	c(0);
	echo 'b-stop';
}

function c($x)
{
	echo 'c-begin';

	if ($x === 0) {
		throw new Exception('Делить на ноль нехорошо');
	}

	echo 'c-stop';
	return 100 / $x;

}

Результат выполнения будет идентичен. Функция b() осталась в первоначальном виде, не тронутая. Это особенно актуально, если у вас длинные каскады. И ещё объект $e может содержать дополнительную информацию о произошедшей ситуации.

Таким образом, получается, что ошибки и исключения — это совершенно разные инструменты для решения совершенно разных задач:
ошибка — не поправимая ситуация;
исключение – позволяет прервать выполнение каскада функций и пробросить некоторую информацию. Что-то вроде глобального оператора return. Если у Вас нет каскада, то вам достаточно использовать if или return.

Ошибки не всегда являются ошибками

Некоторые могут мне возразить: «Посмотри в Zend Framework — там всегда кидают исключения. Это best practics, и надо делать также. Даже если не удалось подключиться к БД, надо кидать исключение».

В этой статье я как раз хочу развеять это заблуждение. Zend действительно является best practics, но программисты Зенда находятся на другой лодке и делают другие вещи. Принципиальная разница между ними и мной в том, что они пишут универсальную библиотеку, которая будет использоваться во многих проектах. И они со своей колокольни не могут сказать, что является критической ошибкой, а что является поправимой.

Например, в вашем проекте может быть несколько MySQL серверов и вы можете переключаться между ними при падении одного из них. По этому, Zend_Db, как универсальная библиотека, кидает исключение, а что с ним делать — решайте сами. Exception это гибко — вы сами решаете на каком уровне и какой тип ситуаций ловить. Вы можете вывести сообщение об ошибке или попытаться исправить возникшую ситуацию, если знаете как. При написании универсальных библиотек необходимо всегда кидать исключения. Это делает библиотеку более гибкой.

В итоге, могу сказать, что у обоих механизмов есть свои особенности и, самое главное, что у них есть своё предназначение и эти предназначения нисколько не пересекаются.

Ошибки != исключения

. Не надо использовать исключения для улучшения быстродействия или сообщения об ошибках. Не надо в классе My_Custom_Exception реализовывать какую-либо логику исправления ситуации. Этот класс должен быть пустым, он создаётся только что бы определить тип ситуации и поймать только то, что надо. Название класса ‘My_Custom_Exception’ это такой древовидный аналог линейному списку констант E_*** (E_NOTICE, E_WARNING, …).

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


Подборка по базе: Кто такие рейдеры что такое рейдерский захват в бизнесе.docx, Конспект _Что такое алгоритм_ (1).docx, Что такое стежок и как он образуется.docx, Что такое физика унич.docx, Итак давайте начнем сегодня с лекции по заболеваний пищевода это, Что такое присасывающий протез.docx, Презентация исследовательской работы _Что такое финансовая пирам, что такое идеальная информационная система курсовая работа — Янд, Тема 69 Что такое параллелизм.docx, Что такое философия.docx


Что такое исключения

  • Исключение — это проблема (ошибка), возникающая во время выполнения программы. Исключения могут возникать во многих случаях, например:
    • Пользователь ввел некорректные данные.
    • Файл, к которому обращается программа, не найден.
    • Сетевое соединение с сервером было утеряно во время передачи данных. И т.д.
  • Все исключения в Java являются объектами. Поэтому они могут порождаться не только автоматически при возникновении исключительной ситуации, но и создаваться самим разработчиком.

Что такое Error? В каком случае используется Error. Приведите пример Error’а.

Ошибки (Errors) представляют собой более серьёзные проблемы, которые, согласно спецификации Java, не следует пытаться обрабатывать в собственной программе, поскольку они связаны с проблемами уровня JVM. Например, исключения такого рода возникают, если закончилась память, доступная виртуальной машине.

Как пример — OutOfMemoryError.
Можно/нужно ли обрабатывать ошибки jvm?

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

О чем говорит ключевое слово throws?

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

 В чем особенность RuntimeException?

  • public class RuntimeException extends Exception — базовый класс для ошибок во время выполнения. Относится к необрабатываемым исключениям (uncatchedunchecked). Как сказано в описании класса — это суперкласс, исключения которого могут быть выброшены во время нормальной работы JVM.

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

  • Может и оно будет передано в виртуальную машину Java (JVM).

Чем отличаются исключения от обычных классов?

Не совсем понятен вопрос.

  • С точки зрения синтаксиса только то, что мы должны указать в сигнатуре метода проверяемые исключения.
  • А с точки зрения идеологии в том, что проверяемые исключения это дополнительная проверка программиста компилятором
  • Все исключения наследуются от класса Throwable.

Все ключевые слова и всё что с ними связано.

  • try — данное ключевое слово используется для отметки начала блока кода, который потенциально может привести к ошибке.
  • catch — ключевое слово для отметки начала блока кода, предназначенного для перехвата и обработки исключений.
  • finally — ключевое слово для отметки начала блока кода, которой является дополнительным. Этот блок помещается после последнего блока ‘catch’. Управление обычно передаётся в блок ‘finally’ в любом случае.
  • throw — служит для генерации исключений.
  • throws — ключевое слово, которое прописывается в сигнатуре метода, и обозначающее что метод потенциально может выбросить исключение с указанным типом.

Всё исключения и как генерировать различные виды исключений.

  • Чтобы сгенерировать исключение используется ключевое слово throw. Как и любой объект в Java, исключения создаются с помощью new.
  • Есть два стандартных конструктора для всех исключений: первый — конструктор по умолчанию. Второй принимает строковый аргумент, в него можно поместить подходящую информацию в исключение.
  • Так же практически у каждого класса исключения есть конструктор, принимающий в качестве параметра Throwable – причину исключительной ситуации. Когда одно исключение становится причиной другого, этот конструктор можно использовать для передачи цепочки исключений (exception chaining). Если же такого конструктора нет, то у Throwable есть метод initCause(Throwable), который можно вызвать один раз, и передать ему исключение-причину.

Как создать, выбросить, поймать свое исключение?

  • Необходимо создать класс и унаследоваться от базового класса требуемого типа исключений (например, от Exception или RuntimeException). Руководствоваться нужно определением типа исключения. В зависимости от того, что необходимо обрабатывать.
  • В конструкторе класса своего исключения можно передать в конструктор базового класса исключения (например Exception) сообщение об ошибке используя ключевое слово super(message).
  • Что бы в дальнейшем использовать свое исключение, нужно воспользоваться ключевым словом throws с названием исключения в строке объявления метода после скобок с параметрами. И сгенерировать исключение с помощью оператора throw: throw new имя исключения, внутри метода.
  • Поймать исключение можно через try catch, указывав в блоке catch имя исключения.

Можно ли бросить НЕ новое исключение?

В чистом виде нет. Можно передать то же исключение в качестве ссылки в конструкторе другого исключения. Так сказать обернуть его.
Где возможно вызывать исключения?

  • В любом месте кода с помощью throw new Exception();.

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

  • В Java 7 стала доступна новая конструкция, с помощью которой можно перехватывать несколько исключений одним блоком catch указав через знак « необходимые исключения: catch( IOException | SQLException ex )

Обязателен ли блок finally? Конструкция try-finally

  • Оператор finally не обязателен, однако каждый оператор try требует наличия либо catch, либо finally.

А catch?

  • Catch обязателен ели нет finally. Такая запись допустима, если имеется связка try finally. Но же лучше иметь блок catch в котором будет обрабатываться необходимое исключение.

Когда будет выполнен finally?

  • Код в блоке finally будет выполнен всегда. За исключение нескольких случаев.

Когда не будет выполнен (4 случая)?

  • Если вы вызываете System.exit();
  • Если сначала произойдет сбой JVM;
  • Если JVM достигает бесконечного цикла (или другого не прерывающегося, не заканчивающегося оператора) в блоке try или catch;
  • Если ОС принудительно завершает процесс JVM специально или из-за ошибки, например: сбой питания, аппаратная ошибка итп.

Разница try catch и if

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

Какой return вернется из try или finaly?

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

Что если в конструкции try finally вылетело исключение сначала в try а потом в finally?

  • Ответ аналогичный случаю с двумя return — будет обработано в finally блоке.
  • Исключение из finally заменит собой исключение из try и пойдет вверх по стеку вместо него. Оригинальная проблема будет попросту забыта. Такая маскировка исключения усложняет отладку.

Будет ли выполнен finally при OutOfMemoryError?

  • для блока finally выделяется память. Так что при OutOfMemoryError блок finally будет выполнен!

Какое исключение вылетит?

  • Если было выброшено два исключения — одно в try, второе в finally, то исключение в finally «проглотит» исключение выше.

Что будет с другим?

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

Расскажи про информацию, которая находится внутри исключения?

  • Причина ошибки(cause)
  • Возможно, присутствует сообщение об ошибке.
  • StackTrace — список методов, которые были вызваны до момента, когда в приложении произошло исключение.
  • Список подавленных исключений

Как с ней работать? Что возвращает getStackTrace()?

  • Используя метод getStackTrace() можно получить массив элементов StackTraceElement, каждый из которых содержит информацию об одном методе. Все элементы вместе и образуют stack trace.

Какую информацию можно получить из StackTraceElement?

  • StackTraceElement хранит информацию по одному элементу stack trace — т.е. по одному методу из StackTrace. Используется как массив элементов StackTrace.

Что такое Стек?

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

Какое правило должно соблюдаться при использовании нескольких блоков catch (с одним try)?

  • Исполняется только тот, который первым соответствует типу исключения.
  • Используется правило перехвата: сначала более конкретные исключения за ними следуют менее конкретные (более общие).

Какое правило должно соблюдаться при попытке поймать несколько исключений в одном catch?

  • В одном блоке нельзя ловить исключения предка и потомка(наследников класса). В этом случае, в список исключений нужно добавить исключения предка, так как он также будет ловить исключения типа потомка.

Зачем создавать свой класс и наследовать его от Exception?

  • Можно создать свой класс исключений если вам нужно более точно обрабатывать исключения.
  • Свой класс исключений нужно создавать только тогда, когда уже имеющиеся классы исключений не подходят по вашу ситуацию. Иными словами: если существующие имена исключений не покрывают ваши потребности.
  • Наследуясь от Exception, можно обрабатывать исключения и переопределить методы, которые использует этот класс. (Класс Exception пустой, он использует методы Throwable).

Что такое ошибка, а что такое исключительная ситуация?

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

Что нужно делать программисту, если в коде происходит деление на ноль?

  • При делении на ноль целочисленное деление всегда вызывает исключение, но это не относится к числам с плавающей запятой. Для них результатом будет являться бесконечность или не число.
  • Сделать проверку через условный оператор на то, что делитель является нулем и выводить пользователю сообщения об этом.
  • Отлавливать исключение ArithmeticException через try catch.
  • Создать собственный класс исключений.

От каких классов Throwable и его подклассов нельзя наследоваться?

  • Не следует делать свои классы-исключения наследниками класса Error или какого-то его подкласса. Из-за больших сложностей обработки этих исключений.

Можно ли так написать try { throw new Object(); }?

  • Можно, но только если явно привести Object к Throwable и добавить блок catch (Throwable e). И это не имеет смысла.

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

  • Все исключительные ситуации делятся на «проверяемые» (checked) и «непроверяемые» (unchecked) компилятором. Это свойство присуще родительскому классу Throwable и всем его наследникам, Error, Exception, RuntimeException и далее передается по наследству. Никак не видимо в исходном коде класса исключения.
    • Throwable и Exception и все их наследники (за исключением наследников Error-а и RuntimeException-а) — checked
    • Error и RuntimeException и все их наследники — unchecked
  • Checked исключения отличаются от Unchecked исключения в Java, тем что:
    • Наличиеобработка Checked исключения проверяются на этапе компиляции. Наличиеобработка Unchecked исключения происходит на этапе выполнения.

В чём разница с точки зрения синтаксиса и идеологическая при использовании?

  • Checked исключения, это те, которые должны обрабатываться блоком catch или описываться в сигнатуре метода. Unchecked могут не обрабатываться и не быть описанными.

Какое назначение класса Throwable?

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

Методы класса Throwable

  1. Метод final void addSuppressed(Throwable исключение) — добавляет заданное исключение в список подавляемых исключений. Этот список связывается с вызывающим (данным) исключением. Метод используется для применения в операторе try с ресурсами.
  2. Метод Throwable fillInStackTrace() — возвращает объект класса Throwable, содержащий полную трассировку стека. Этот объект может быть сгенерирован повторно.
  3. Метод Throwable getCause() — возвращает исключение, лежащее в основе текущего исключения. Метод возвращает null в случае, если такое исключение отсутствует. Этот метод используется при создании цепочек исключений – он вызывает исключение, вызывающее текущее исключение.
  4. Метод String getLocalizedMessage() — возвращает локализованное описание исключения.
  5. Метод String getMessage() — возвращает описание исключения.
  6. Метод StackTraceElement[] getStackTrace() — возвращает массив, содержащий поэлементную трассировку стека в виде объектов класса StackTraceElement.
  7. Метод final Throwable[] getSuppressed() — получает подавленные исключения, связанные с вызывающим исключением, и возвращает массив, который содержит результат. Подавленные исключения генерируются в операторе try с ресурсами.
  8. Метод Throwable initCause(Throwable причина_исключения) — связывает входной параметр причина_исключения с вызывающим исключением, указывая его как причину этого вызывающего исключения. Возвращает ссылку на исключение. Метод используется при создании цепочек исключений.
  9. Метод printStackTrace() — выводит трассировку стека.
  10. Метод printStackTrace() — Метод направляет трассировку стека в заданный поток_вывода. имеет еще две перегруженных реализации:
  11. void printStackTrace(PrintStream поток_вывода)
  12. void printStackTrace(PrintWriter поток_вывода)
  13. Метод void setStackTrace(StackTraceElement элементы[]) — устанавливает трассировку стека для заданных элементов.
  14. Метод String toString() — возвращает объект типа String содержащий описание исключения. Этот метод можно вызвать из метода println() при выводе объекта типа Throwable.

Что происходит если не обработать исключение?

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

Что такое подавленные исключения?

  • Подавленное исключение — это исключение, которое выбрасывается, но каким-то образом игнорируется. Распространенный сценарий для этого в Java — это когда блок finally создает исключение, а любое исключение, первоначально возникшее в блоке try , подавляется.

Как достать подавленное исключение?

  • Получить доступ к подавленному исключению возможно, например, если использовать метод addSuppressed() в блоке finally и передать в качестве параметра исходное исключение из блока catch через промежуточную переменную, что бы исключение не потерялось.

Что такое ресурс в конструкции try-with-resources?

  • Ресурсы объявляются в скобках сразу после try, а компилятор уже сам неявно создаёт секцию finally.
  • Ресурсом называется объект, являющийся экземпляром класса, который реализует интерфейс java.lang.AutoCloseable или java.io.Closable
  • Можно указывать несколько ресурсов разделяя их точкой с запятой.
  • Закрываться эти ресурсы будут в порядке, обратном их объявлению.
  • В Java 9 стало возможным использовать в try-блоке ресурсы, объявленные вне try-блока.
  • Ресурс становится final и его нельзя изменить.
  • При выходе из блока try-with-resources будет вызван метод close() и освобождены все ресурсы.

Что если исключение вылетело сначала в try, а потом в close в конструкции try-with-recources?

  • Брошенное try-блоком исключение имеет больший приоритет, чем исключения, получившиеся во время закрытия. В конструкции try-finally наоборот, исключение из finally перекроет исключение в try.

Какое исключение вылетит?

  • Исключение в try перекроет исключение в close

Что будет с другим?

  • Исключение будет подавленно, и его можно получить через метод getSuppress.

Когда происходит закрытие ресурса в конструкции try-with-resources если в try возникло исключение: до перехода в catch или после того, как catch отработает?

  • catch и явный finally выполняются уже после того, как закрываются ресурсы в неявном finally(ментор сказал в неявном try catch), который компилятор сам неявно создаёт.

Какие есть уровни логирования и для чего они нужны?

Для контроля объема информации и уменьшения нагрузки из-за процесса логирования создали уровни логирования. Логгер можно конфигурировать для того, чтобы игнорировать логи с уровнем ниже заданного, например логгер WARNING будет записывать SEVERE и WARNING, но будет пропускать все что ниже него INFO и ниже.

    • SEVERE (серьезные ошибки программы)
    • WARNING (предупреждение)
    • INFO (информационное сообщение)
    • CONFIG (конфигурационные параметры)

(детальное логирование)

    • FINE (сообщение об успешной операции)
    • FINER
    • FINEST
    • OFF(отключить)
    • ALL(записывать все)

Обязательно ли передавать в метод getLogger() имя класса?

  • Нет не обязательно, можно передать любой строковый параметр. Но желательно.

Почему так принято?

  • Легко проверить класс ведения журнала, так как имя журнала будет отображаться в файле журнала. Можно быстро перейти к определенному классу.
  • Не нужно беспокоиться о дублировании имен журналов в сложном приложении. Имя всегда будет уникальным.

Сообщения каких уровней мы увидим, задав уровень INFO?

  • SEVERE (серьезные ошибки программы)
  • WARNING (предупреждение)
  • INFO (информационное сообщение)

Как Java понимает какой уровень главнее при установке .setLevel().

  • В классе Level первым параметром идет строковое название уровня логирования, а вторым параметром int значение приоритета. У уровня info приоритет 800 у warning 900, чем больше значение, тем выше приоритет.

Обработка ошибок

  1. Возврат кода ошибки
  2. Исключения

Возврат кода ошибки

enum class Error
{
    Success,
    Failure
};

Error doSomething()
{
    return Error::Success;
}

if (doSomething() != Error::Success)
{
    showError();
}

+ Простота

— Ошибку можно проигнорировать

— Сложности с инициализацией

auto data = readData("data.json");

Json data;
auto error = readData(data, "data.json");

Исключения

— Сложно

+ Нельзя проигнорировать

class Error
{
};

void doSomething()
{
    throw Error();
}

try
{
    doSomething();
}
catch (const Error& error)
{
    showError();
}

Что такое исключительная ситуация?

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

Гарантии безопасности исключений (exception safety)

  1. Гарантировано искючений нет (No-throw guarantee)

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

  1. Строгая гарантия (Strong exception safety)

Также известна как коммит ролбек семантика (commit or rollback semantics). Операции могут завершиться неудачей, но неудачные операции гарантированно не имеют побочных эффектов, поэтому все данные сохраняют свои исходные значения.

std::vector<int> source = ...;
try
{
    std::vector<int> tmp = source;
    tmp.push_back(getNumber());
    tmp.push_back(getNumber()); <-- Исключение
    tmp.push_back(getNumber());
    source.swap(tmp);
}
catch (...)
{
    return;
}
  1. Базовая гарантия (Basic exception safety)

Выполнение неудачных операций может вызвать побочные эффекты, но все инварианты сохраняются и нет утечек ресурсов (включая утечку памяти). Любые сохраненные данные будут содержать допустимые значения, даже если они отличаются от того, что они были до исключения.

source.push_back(getNumber());
source.push_back(getNumber()); <-- Исключение
source.push_back(getNumber());
  1. Никаких гарантий (No exception safety)

Поиск подходящего обработчика

class Error {};

class ArgumentError : public Error
{
    std::string message_;
public:
    ArgumentError(std::string&& message);
    const std::string& getMessage() const;
};

File openFile(const std::string& name)
{
    if (name.empty())
        throw ArgumentError("empty file name");
}

try
{
    auto file = openFile("data.json");
    auto json = file.readAll();
}
catch (const ArgumentError& error)
{
    std::cerr << error.getMessage();
}
catch (const Error& error)
{
}
catch (...)
{
}
  1. Поиск подходящего обработчика идет в порядке следования обработчиков в коде
  2. Полного соответствия типа не требуется, будет выбран первый подходящий обработчик
  3. Если перехватывать исключение по значению, то возможна срезка до базового класса
  4. Если наиболее общий обработчик идет раньше, то более специализированный обработчик никогда не будет вызван
  5. Три точки — перехват любого исключения

Исключения ОС — не исключения С++, например, деление на ноль. Для их обработки нужно использовать средства предоставляемые конкретной платформой

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

Перезапуск исключения

try
{
    foo();
}
catch (...)
{
    std::cerr << "something wrong";
    throw;
}

noexcept

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

terminate

Вызывает стандартную функцию С — abort.

abort — аварийное завершение программы, деструкторы объектов вызваны не будут.

Поведение terimnate можно изменить установив свой обработчик функцией set_terminate.

Раскрутка стека

void foo()
{
    throw Error();
}

void bar()
{
    try
    {
        foo();
    }
    catch (const FileError&)
    {
    }
}

int main()
{
    try
    {
        bar();
    }
    catch (...)
    {
        std::cerr << "unknown error";
    }
    return 0;
}

Поиск подходящего обработчика вниз по стеку вызовов с вызовом деструкторов локальных объектов — раскрутка стека.

Если подходящий обработчик не был найден вызывается стандартная функция terminate

Исключения в деструкторе

Исключение покинувшее деструктор во время раскрутки стека или у глобального/статического объекта приведет к вызову terminate.

Начиная с С++11 все деструкторы компилятором воспринимаются как помеченные noexcept — теперь исключения не должны покидать деструктора никогда.

Исключения в конструкторе

struct Socket
{
    static constexpr size_t BufferSize = 2048;
    
    Socket(const std::string& address)
    	: data_(new char[BufferSize])
    {
        if (address.empty())
            throw ArgumentError();
    }
    
    ~Socket()
    {
        delete[] buffer_;
    }
    
    char* buffer_;
};

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

Управление ресурсами

Используем идеому RAII (Resource Acquire Is Initialization):

struct Buffer
{
    Buffer(size_t size)
    	: data_(new char[size])
    {
    }
    
    ~Buffer()
    {
        delete[] data_;
    }
    
    char* data_;
};
struct Socket
{
    static constexpr size_t BufferSize = 2048;
    
    Socket(const std::string& address)
    	: buffer_(BufferSize)
    {
        if (address.empty())
            throw ArgumentError();
    }
    
    Buffer buffer_;
};

Управление памятью

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

  1. unique_ptr
  2. shared_ptr / weak_ptr

unique_ptr

  • Монопольное владение памятью, в конструкторе захват, в деструкторе освобождение
  • Копирование запрещено, перемещение разрешено
std::unique_ptr<MyClass> x(new MyClass());
std::unique_ptr<char[]> x(new char[1024]);
auto y = std::make_unique<MyClass>(); // C++14

shared_ptr

  • Совместное владение памятью
  • Копирование увеличивает счетчик ссылок
  • В деструкторе счетчик уменьшается и если становиться равным 0, то объект уничтожается
std::shared_ptr<MyClass> x(new MyClass());
auto y = std::make_shared<MyClass>();

Точки следования (sequence points)

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

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

// Может быть утечка
foo(std::shared_ptr<MyClass>(new MyClass()), bar());
std::shared_ptr<MyClass> x(new MyClass());
foo(x, bar()); // ok
foo(std::make_shared<MyClass>(), bar()); // ok

Местонахождение точек:

  1. В конце каждого полного выражения — ;
  2. В точке вызова функции после вычисления всех аргументов
  3. Сразу после возврата функции, перед тем как любой другой код из вызвавшей функции начал выполняться
  4. После первого выражения (а) в следующих конструкциях:
	a || b 
	a && b
	a, b
	a ? b : c

Если программа пытается модифицировать одну переменную дважды не пересекая точку следования, то это ведет к неопределенному поведению (undefined behavior):

int x = 0;
x = x++; // <-- UB

int i = 0;
i = i++ + ++i; // <-- UB

Схематичное устройство shared_ptr

#include <cassert>
#include <iostream>

template <class T>
class SharedPtr
{
    struct Data
    {
        T* object_;
        int counter_;
    };

    Data* data_;

    void release()
    {
        --data_->counter_;
        if (data_->counter_ == 0)
        {
            delete data_->object_;
            delete data_;
        }
    }

public:
    SharedPtr(T* object = nullptr)
        : data_(new Data{ object, 1 })
    {
    }

    ~SharedPtr()
    {
        release();
    }

    SharedPtr(const SharedPtr<T>& copied)
        : data_(copied.data_)
    {
        ++data_->counter_;
    }

    SharedPtr& operator=(const SharedPtr<T>& copied)
    {
        if (data_ == copied.data_)
            return *this;

        release();

        data_ = copied.data_;
        ++data_->counter_;
        return *this;
    }

    T& operator*()
    {
        return *data_->object_;
    }

    const T& operator*() const
    {
        return *data_->object_;
    }

    T* operator->()
    {
        return data_->object_;
    }

    const T* operator->() const
    {
        return data_->object_;
    }
};

struct A
{
    A() { std::cout << "A" << std::endl; }
    ~A() { std::cout << "~A" << std::endl; }
    void foo() { std::cout << this << std::endl; }
};

SharedPtr<A> foo(SharedPtr<A> x)
{
    return x;
}

int main()
{
    auto x = foo(new A());
    auto y = x;
    y->foo();
    (*x).foo();
    y = nullptr;
    return 0;
}
Предпочитайте make_shared
auto x = std::shared_ptr<MyClass>(new MyClass());
auto x = std::make_shared<MyClass>();
  1. Нет дублирования (MyClass два раза)
  2. Безопасно в вызове функций
  3. Оптимально — 1 вызов new вместо 2

Проблема циклических ссылок

class Widget;

class Window
{
    std::vector<std::shared_ptr<Widget>> children_;
};

class Widget
{
    std::shared_ptr<Window> parent_;
};

Winwow не может быть удален, так как в Widget жив shared_ptr на него, а Widget в свою очередь не может быть удален, так как жив Window.

Ключевой вопрос С++ — кто кем владеет

weak_ptr

class Widget;

class Window
{
    std::vector<std::shared_ptr<Widget>> children_;
};

class Widget
{
    std::weak_ptr<Window> parent_;
};

weak_ptr не принимает владение объектом, но располагая weak_ptr всегда можно узнать жив ли объект и если жив, то получить на него shared_ptr.

std::shared_ptr<A> x;
std::weak_ptr<A> weak = x;
std::shared_ptr<A> y = weak.lock();
if (y)
{
    ...
}

enable_shared_from_this

Иногда нужно получить shared_ptr от самого себя, например, очень актуально при асинхронном взаимодействии, когда время жизни объекта не определено.

class A
{
    std::shared_ptr<A> getSharedPtr()
    {
        // Приведет к многократному удалению
        return std::shared_ptr<A>(this);
    }
};
class A
    : public std::enable_shared_from_this<A>
{
    std::shared_ptr<A> getSharedPtr()
    {
        return shared_from_this(); // Ok
    }
};
Ограничения enable_shared_from_this
class A
    : public std::enable_shared_from_this<A>
{
    A()
    {
        shared_from_this(); // throw std::bad_weak_ptr
    }

    ~A()
    {
        shared_from_this(); // throw std::bad_weak_ptr
    }
};

Практическая часть

Используя метод рекурсивного спуска написать калькулятор. Поддерживаемые операции:

  • умножение
  • деление
  • сложение
  • вычитание
  • унарный минус

Калькулятор — шаблонный класс, но в тестах шаблон буден инстанцирован int, приоритет операций стандартный. Передача выражения осуществляется через аргумент командной строки, результат выводится в cout. Пример:

Вывод:

Должна быть обработка ошибок, в случае некорректного выражения выводить в консоль error

Подсказки
int main(int argc, char* argv[])
{
    // argc - количество аргументов
    // argv - сами аргументы, первый всегда - имя программы
}

Отныне если в коде будет замечен delete/free, то будет снят 1 бал.

EOF

Java_Deep_7.4-5020-83cb21.png

JavaSpec_Welcome_970x90-1801-439a19.png

В нашей жизни нередко возникают ситуации, которые мы не планировали. К примеру, пошли вы утром умываться и с досадой обнаружили, что отключили воду. Вышли на улицу, сели в машину, а она не заводится. Позвонили другу, а он недоступен. И так далее и тому подобное… В большинстве случаев человек без труда справится с подобными проблемами. А вот как с непредвиденными ситуациями справляется Java, мы сейчас и поговорим.

Что называют исключением. Исключения в мире программирования

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

При этом в отличие от «человеческого мира», программное приложение должно чётко понимать, как поступать в подобной ситуации. И вот как раз для этого в Java и существует механизм исключений (exception).

Используемые ключевые слова

При обработке исключений в Java применяются следующие ключевые слова:
try – служит для определения блока кода, в котором может произойти исключение;
catch – необходим для определения блока кода, где происходит обработка исключения;
finally – применяется для определения блока кода, являющегося необязательным, однако при его наличии он выполняется в любом случае вне зависимости от результата выполнения блока try.

Вышеперечисленные ключевые слова необходимы для создания в коде ряда специальных обрабатывающих конструкций: try{}finally{}, try{}catch, try{}catch{}finally.

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

Давайте на примере посмотрим, как используются ключевые слова в Java-программе:

//метод считывает строку с клавиатуры

public String input() throws MyException {//предупреждаем с помощью throws,
// что метод может выбросить исключение MyException
      BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    String s = null;
//в блок try заключаем код, в котором может произойти исключение, в данном
// случае компилятор нам подсказывает, что метод readLine() класса
// BufferedReader может выбросить исключение ввода/вывода
    try {
        s = reader.readLine();
// в блок  catch заключаем код по обработке исключения IOException
    } catch (IOException e) {
        System.out.println(e.getMessage());
// в блоке finally закрываем поток чтения
    } finally {
// при закрытии потока тоже возможно исключение, например, если он не был открыт, поэтому “оборачиваем” код в блок try
        try {
            reader.close();
// пишем обработку исключения при закрытии потока чтения
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }

    if (s.equals("")) {
// мы решили, что пустая строка может нарушить в дальнейшем работу нашей программы, например, на результате этого метода нам надо вызывать метод substring(1,2), поэтому мы вынуждены прервать выполнение программы с генерацией своего типа исключения MyException с помощью throw
        throw new MyException("String can not be empty!");
    }
    return s;
}

Зачем нам механизм исключений?

Для понимания опять приведём пример из обычного мира. Представьте, что на какой-нибудь автодороге имеется участок с аварийным мостом, на котором ограничена грузоподъёмность. И если по такому мосту проедет грузовик со слишком большой массой, мост разрушится, а в момент этого ЧП ситуация для шофёра станет, мягко говоря, исключительной. И вот, дабы такого не произошло, дорожные службы заранее устанавливают на дороге соответствующие предупреждающие знаки. И тогда водитель, посмотрев на знак, сравнит массу своего авто со значением разрешённой грузоподъёмности и примет соответствующее решение, например, поедет по другой дороге.

То есть мы видим, что из-за правильных действий дорожной службы шоферы крупногабаритных транспортных средств:
1) получили возможность заранее изменить свой путь;
2) были предупреждены об опасности;
3) были предупреждены о невозможности проезжать по мосту при определённых условиях.

Вот как наш жизненный пример соотносится с применением исключения на Java:

1-20219-9bd18c.jpg

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

Что же, давайте ещё раз побудем дорожной службой. Чтобы установить знак, мы ведь должны знать места, где водителей ТС могут ждать различные неприятности. Это первое. Далее, нам ведь надо заготовить и установить знаки. Это второе. И, наконец, надо предусмотреть маршруты объезда, позволяющие избежать опасности.

В общем, механизм исключений в Java работает схожим образом. На стадии разработки программы мы выполняем «ограждение» опасных участков кода в отношении наших исключений, используя блок try{}. Чтобы предусмотреть запасные пути, применяем блок catch{}. Код, выполняемый в программе при любом исходе, пишем в блоке finally{}.

Иногда бывает, что мы не можем предусмотреть «запасной аэродром» либо специально желаем предоставить право его выбора юзеру. Но всё равно мы должны как минимум предупредить пользователя об опасности. Иначе он превратится в разъярённого шофёра, который ехал долго, не встретил ни одного предупреждающего знака и в итоге добрался до аварийного моста, проехать по которому не представляется возможным.

Что касается программирования на Java, то мы, когда пишем свои классы и методы, далеко не всегда можем предвидеть контекст их применения другими программистами в своих программах, а значит, не можем со стопроцентной вероятностью предвидеть правильный путь для разрешения исключительных ситуаций. Но предупредить коллег о возможной исключительной ситуации мы всё-таки должны, и это не что иное, как правило хорошего тона.

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

Предупреждаем о неприятностях

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

После упоминания ключевого слова throws мы указываем тип исключения. Как правило, речь идёт о наследниках класса Exception Java. Но так как Java — это объектно-ориентированный язык программирования, все исключения представляют собой объекты.

2-20219-ee1e82.jpg

Иерархия исключений в Java

Когда возникают ошибки при выполнении программы, исполняющая среда Java Virtual Machine обеспечивает создание объекта нужного типа, используя иерархию исключений Java — речь идёт о множестве возможных исключительных ситуаций, которые унаследованы от класса Throwable — общего предка. При этом исключительные ситуации, которые возникают в программе, делят на 2 группы:
1. Ситуации, при которых восстановление нормальной дальнейшей работы невозможно.
2. Ситуации с возможностью восстановления.

К первой группе можно отнести случаи, при которых возникают исключения, которые унаследованы из класса Error. Это ошибки, возникающие во время выполнения программы при сбое работы Java Virtual Machine, переполнении памяти либо сбое системы. Как правило, такие ошибки говорят о серьёзных проблемах, устранение которых программными средствами невозможно. Данный вид исключений в Java относят к неконтролируемым исключениям на стадии компиляции (unchecked). К этой же группе относятся и исключения-наследники класса Exception, генерируемые Java Virtual Machine в процессе выполнения программы — RuntimeException. Данные исключения тоже считаются unchecked на стадии компиляции, а значит, написание кода по их обработке необязательно.

Что касается второй группы, то к ней относят ситуации, которые можно предвидеть ещё на стадии написания приложения, поэтому для них код обработки должен быть написан. Это контролируемые исключения (checked). И в большинстве случаев Java-разработчики работают именно с этими исключениями, выполняя их обработку.

Создание исключения

В процессе исполнения программы исключение генерируется Java Virtual Machine либо вручную посредством оператора throw. В таком случае в памяти происходит создание объекта исключения, выполнение основного кода прерывается, а встроенный в JVM обработчик исключений пробует найти способ обработать это самое исключение.

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

Обработка исключений в Java подразумевает создание блоков кода и производится в программе посредством конструкций try{}finally{}, try{}catch, try{}catch{}finally.

3-20219-4ec690.jpg

В процессе возбуждения исключения в try обработчик исключения ищется в блоке catch, который следует за try. При этом если в catch присутствует обработчик данного вида исключения, происходит передача управления ему. Если же нет, JVM осуществляет поиск обработчика данного типа исключения, используя для этого цепочку вызова методов. И так происходит до тех пор, пока не находится подходящий catch. После того, как блок catch выполнится, управление переходит в необязательный блок finally. Если подходящий блок catch найден не будет, Java Virtual Machine остановит выполнение программы, выведя стек вызовов методов под названием stack trace. Причём перед этим выполнится код блока finally при наличии такового.

Рассмотрим практический пример обработки исключений:

public class Print {

     void print(String s) {
        if (s == null) {
            throw new NullPointerException("Exception: s is null!");
        }
        System.out.println("Inside method print: " + s);
    }

    public static void main(String[] args) {
        Print print = new Print();
        List list= Arrays.asList("first step", null, "second step");

        for (String s:list) {
            try {
                print.print(s);
            }
            catch (NullPointerException e) {
                System.out.println(e.getMessage());
                System.out.println("Exception was processed. Program continues");
            }
            finally {
                System.out.println("Inside bloсk finally");
            }
            System.out.println("Go program....");
            System.out.println("-----------------");
        }

    }
    }

А теперь глянем на результаты работы метода main:

Inside method print: first step
Inside bloсk finally
Go program....
-----------------
Exception: s is null!
Exception was processed. Program continues
Inside bloсk finally
Go program....
-----------------
Inside method print: second step
Inside bloсk finally
Go program....
-----------------

Блок finally чаще всего используют, чтобы закрыть открытые в try потоки либо освободить ресурсы. Но при написании программы уследить за закрытием всех ресурсов возможно не всегда. Чтобы облегчить жизнь разработчикам Java, была предложена конструкция try-with-resources, автоматически закрывающая ресурсы, открытые в try. Используя try-with-resources, мы можем переписать наш первый пример следующим образом:

public String input() throws MyException {
    String s = null;
    try(BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))){
        s = reader.readLine();
   } catch (IOException e) {
       System.out.println(e.getMessage());
   }
    if (s.equals("")){
        throw new MyException ("String can not be empty!");
    }
    return s;
}

А благодаря появившимся возможностям Java начиная с седьмой версии, мы можем ещё и объединять в одном блоке перехват разнотипных исключений, делая код компактнее и читабельнее:

public String input() {
    String s = null;
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
        s = reader.readLine();
        if (s.equals("")) {
            throw new MyException("String can not be empty!");
        }
    } catch (IOException | MyException e) {
        System.out.println(e.getMessage());
    }
    return s;
}

Итоги

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

Основные вопросы об исключениях в Java

1.Что такое проверяемые и непроверяемые исключения?
Если говорить коротко, то первые должны быть явно пойманы в теле метода либо объявлены в секции throws метода. Вторые вызываются проблемами, которые не могут быть решены. Например, это нулевой указатель или деление на ноль. Проверяемые исключения очень важны, ведь от других программистов, использующих ваш API, вы ожидаете, что они знают, как обращаться с исключениями. К примеру, наиболее часто встречаемое проверяемое исключение — IOException, непроверяемое — RuntimeException.
2.Почему переменные, определённые в try, нельзя использовать в catch либо finally?
Давайте посмотрим на нижеследующий код. Обратите внимание, что строку s, которая объявлена в блоке try, нельзя применять в блоке catch. То есть данный код не скомпилируется.

try {
    File file = new File("path");
    FileInputStream fis = new FileInputStream(file);
    String s = "inside";
} catch (FileNotFoundException e) {
    e.printStackTrace();
    System.out.println(s);
}

А всё потому, что неизвестно, где конкретно в try могло быть вызвано исключение. Вполне вероятно, что оно было вызвано до объявления объекта.
3.Почему Integer.parseInt(null) и Double.parseDouble(null) вызывают разные исключения?
Это проблема JDK. Так как они были разработаны разными людьми, то заморачиваться вам над этим не стоит:

Integer.parseInt(null);
// вызывает java.lang.NumberFormatException: null

Double.parseDouble(null);
// вызывает java.lang.NullPointerException

4.Каковы основные runtime exceptions в Java?
Вот лишь некоторые из них:

IllegalArgumentException
ArrayIndexOutOfBoundsException

Их можно задействовать в операторе if, если условие не выполняется:

if (obj == null) {
   throw new IllegalArgumentException("obj не может быть равно null");

5.Возможно ли поймать в одном блоке catch несколько исключений?
Вполне. Пока классы данных исключений можно отследить вверх по иерархии наследования классов до одного и того же суперкласса, возможно применение только этого суперкласса.
6.Способен ли конструктор вызывать исключения?
Способен, ведь конструктор — это лишь особый вид метода.

class FileReader{
    public FileInputStream fis = null;

    public FileReader() throws IOException{
        File dir = new File(".");//get current directory
        File fin = new File(dir.getCanonicalPath() + File.separator + "not-existing-file.txt");
        fis = new FileInputStream(fin);
    }
}

7.Возможен ли вызов исключений в final?
В принципе, можете сделать таким образом:

public static void main(String[] args) {
    File file1 = new File("path1");
    File file2 = new File("path2");
    try {

        FileInputStream fis = new FileInputStream(file1);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally {
        try {
            FileInputStream fis = new FileInputStream(file2);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Но если желаете сохранить читабельность, объявите вложенный блок try-catch в качестве нового метода и вставьте вызов данного метода в блок finally.

finally. 
public static void main(String[] args) {
    File file1 = new File("path1");
    File file2 = new File("path2");
    try {

        FileInputStream fis = new FileInputStream(file1);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally {
        methodThrowException();
    }
}

JavaSpec_Welcome_970x90-1801-439a19.png

Понравилась статья? Поделить с друзьями:
  • Что такое ошибка дефект сбой и отказ
  • Что такое ошибка xlive dll
  • Что такое ошибка дефект баг
  • Что такое ошибка декодирования
  • Что такое ошибка датчика фаз