Руководство по Java Core. Исключения.

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

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

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

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

Вот иерархия исключений в Java:

Checked-and-Unchecked-Exceptions-in-Java


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

  • public String getMessage()
    Возвращает детальное сообщение о вызванном исключении.
  • public Throwable getCause()
    Возвращает причину исключения.
  • public String toString()
    Возвращает имя класса и является частью результат метода getMessage().
  • public void printStackTrace()
    Выводит результат метода toString() вместе с цепочкой исключений в выходной поток System.err
  • public StackTraceElement[] getStackTrace()
    Возвращает массив, который содержит  каждый элемент цепочки исключения. 0 элемент массива является верхним элементом стека, а крайний – нижним.
  • public Throwable fillinStackTrace()
    Заполняет стэк объекта Throwable текущим стеком исключения, добавляясь к предыдущей информации в стеке.

Все исключения мы можем поделить на 2 большие группы:

  • Проверяемые исключения
  • Непроверяемые исключения
  • Ошибки

Проверяемые исключения

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

Рассмотрим пример простого приложения.

Пример:


import java.io.File;
import java.io.FileReader;
import java.io.IOException;

public class FileNotFoundEXceptionDemo {
    public static void main(String[] args) throws IOException {
        File file = new File("/home/proselyte/Programming/Projects/Proselyte/JavaCore/resources/nullFile.txt");
        FileReader fileReader = new FileReader(file);

        char[] charArray = new char[100];
        fileReader.read(charArray);

        for (char character : charArray) {
            System.out.print(character);
        }
        fileReader.close();
    }
}

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


/*Some system messages*/
Exception in thread "main" java.io.FileNotFoundException: /home/proselyte/Programming/Projects/Proselyte/JavaCore/resources/nullFile.txt (No such file or directory)
	at java.io.FileInputStream.open(Native Method)
	at java.io.FileInputStream.(FileInputStream.java:146)
	at java.io.FileReader.(FileReader.java:72)
	at net.proselyte.javacore.exceptions.FileNotFoundEXceptionDemo.main(FileNotFoundEXceptionDemo.java:10)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:606)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)


Непроверяемые исключения

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

Например, у нас есть массив целых чисел на 10 элементов и мы пытаемся получить значение 11 элемента. Т.е. мы пытаемся залезть в участок физической памяти, содержимое которого нам неизвестно. JVM не даст нам этого сделать и выдаст исключение ArrayIndexOutOfBoundsException,

Рассмотрим, как это выглядит в приложении.

Пример:


package net.proselyte.javacore.exceptions;

public class IndexOutOfBoundsExceptionDemo {
    public static void main(String[] args) {
        int[] integers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

        System.out.println("Trying to call 10th element of array: ");
        System.out.println(integers[10]);
    }
}

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


Trying to call 10th element of array: 
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 10
	at net.proselyte.javacore.exceptions.IndexOutOfBoundsExceptionDemo.main(IndexOutOfBoundsExceptionDemo.java:8)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:606)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)

Ошибки

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

Например, в нашей программе случилась такая ситуации, при которой 2 метода рекурсивно вызывают друг друга. В результате мы столкнёмся с тем, что у нас “закончится” выделенная нам память.

Для понимания, того, как это работает на практике, рассмотрим пример простого приложения.

Пример:


public class StackOverFlowErrorDemo {
    public static void main(String[] args) {
        methodA();
    }

    static public void methodA(){
        methodB();
    }

    static public void methodB(){
        methodA();
    }
}

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


Exception in thread "main" java.lang.StackOverflowError
	at net.proselyte.javacore.exceptions.StackOverFlowErrorDemo.methodA(StackOverFlowErrorDemo.java:9)
	at net.proselyte.javacore.exceptions.StackOverFlowErrorDemo.methodB(StackOverFlowErrorDemo.java:13)
	at net.proselyte.javacore.exceptions.StackOverFlowErrorDemo.methodA(StackOverFlowErrorDemo.java:9)
	at net.proselyte.javacore.exceptions.StackOverFlowErrorDemo.methodB(StackOverFlowErrorDemo.java:13)

Как мы видим, оба метода используются в программе и не могут быть удалены сборщиком мусора. Поэтому, когда место в стеке JVM закичилось – мы получили исключение StackOverflowError


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

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

Вот, как это, в общем виде, выглядит в программе:


try{
   //Code that can call an exception
} catch (SomeException ex){
   //Sequence of actions in case of exception
}

Другими словами, мы “пытаемся” (try) выполнить кусок кода и “ловим” (catch) исключения, которые могут возникнуть.

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

Пример:


import java.io.File;
import java.io.FileReader;
import java.io.IOException;

public class TryCatchDemo {
    public static void main(String[] args) {
        File file = new File("/home/proselyte/Programming/Projects/Proselyte/JavaCore/resources/nullFile.txt");

        try {


            FileReader fileReader = new FileReader(file);
            char[] charArray = new char[100];
            fileReader.read(charArray);

            for (char character : charArray) {
                System.out.print(character);
            }
            fileReader.close();
        }catch (IOException e){
            System.out.println("We have IOException here.");
         }
    }
}

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


/*Some system messages*/
We have IOException here.

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

Вот, как это выглядит в коде.

Пример:


catch (IOException|FileNotFoundException ex) {
   System.out.println("We have an exception here...");
   throw ex;

Ключевые слова throw/throws

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

Для того чтобы “бросить” исключение, либо полученное, либо созданное, мы используем ключевое слово throw.

Вот что имеется в виду:


import java.io.*;

public class ThrowThrowsDemo{
   public void calculateSum(int a, int b) throws SomeException {
      //Some code
      throw new SomeException(); 
   }
}

Блок finally

Блок finally может быть частью блока try/catch и код, который находится в нём, будет выполнен в любом случае (только если мы не “положим” JVM).

Давайте посмотрим, как это работает на примере простого приложения.

Пример:


import java.util.Arrays;

public class FinallyDemo {
    public static void main(String[] args) {
        int[] intArray = new int[10];

        System.out.println("Filling our array");
        for (int i = 0; i < intArray.length; i++) {
            intArray[i] = i;
        }

        try {
            System.out.println("Element number 11: " + intArray[10]);
        } catch (ArrayIndexOutOfBoundsException ex) {
            System.out.println("We have an exception here: " + ex);
        } finally {
            System.out.println("\n=================================================");
            System.out.println("intArray: " + Arrays.toString(intArray));
            System.out.println("=================================================\n");
        }
    }
}

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


/*Some system messages*/
Filling our array
We have an exception here: java.lang.ArrayIndexOutOfBoundsException: 10

=================================================
intArray: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
=================================================

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


Блок try/catch с ресурсами

Мы имеем возможность использовать соединения, потоки и т.д. вместе с блоком try/catch.

Эта конструкция называется try-with-resources.

Рассмотрим пример простого приложения.

Пример:


import java.io.FileReader;
import java.io.IOException;

public class TryWithResourcesDemo {
    public static void main(String[] args) {
        try (FileReader fileReader = new FileReader("resources/message.txt")) {
            char[] charArray = new char[100];
            fileReader.read(charArray);
            for (char character : charArray) {
                System.out.print(character);
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

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


/*Some system messages*/
This is test message for TryWithResources class.

Создание собственных исключений

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

Вот условия их создания:

  • Исключение должно быть наследником класса Throwable
  • Для создания проверяемого исключения, мы должны наследоваться от класса Exception.
  • Для создания непроверяемого исключения, мы должны наследоваться от класса RuntimeException

Рассмотрим пример, приведённый ниже:

Пример:
Класс NegativeWidthException


public class NegativeWidthException extends Exception{
    private int width;

    public NegativeWidthException(int width){
        this.width = width;
    }

    public int getWidth() {
        return width;
    }
}

Класс Square


public class Square {
    private int width;

    public Square(int width) {
        this.width = width;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int calculateArea(int width) throws NegativeWidthException {
        if (width >= 0) {
            return width * width;
        }else {
            throw new NegativeWidthException(width);
        }
    }
}

Класс NegativeWidthExceptionDemo


public class NegativeWidthExceptionDemo {
    public static void main(String[] args)throws NegativeWidthException{
        Square square = new Square(-1);

        System.out.println("Square width: " + square.getWidth());

        System.out.printf("Calculating area...\n");
        System.out.println("Area: " + square.calculateArea(square.getWidth()));
    }
}

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


/*Some system messages*/
Exception in thread "main" Square width: -1
Calculating area...
net.proselyte.javacore.exceptions.NegativeWidthException
	at net.proselyte.javacore.exceptions.Square.calculateArea(Square.java:22)
	at net.proselyte.javacore.exceptions.NegativeWidthExceptionDemo.main(NegativeWidthExceptionDemo.java:10)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)

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


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

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