Привет! Не хочется тебе об этом говорить, но огромная часть работы программиста — это работа с ошибками. Причем чаще всего — со своими собственными.
Так уж сложилось, что не бывает людей, которые не допускают ошибок. И программ таких тоже не бывает. Конечно, главное при работе над ошибкой — понять ее причину. А причин таких в программе может быть целая куча.
В один прекрасный момент перед создателями Java встал вопрос: что делать с этими самыми потенциальными ошибками в программах? Избежать их полностью — нереально. Программисты могут понаписать такого, что невозможно даже представить :)
Значит, надо заложить в язык механизм работы с ошибками. Иными словами, если уж в программе произошла какая-то ошибка, нужен сценарий для дальнейшей работы. Что именно программа должна делать при возникновении ошибки?
Сегодня мы познакомимся с этим механизмом. И называется он “Исключения” (Exceptions).
Что такое исключение в Java
Исключение — некая исключительная, незапланированная ситуация, которая произошла при работе программы. Примеров исключений в Java может быть много. Например, ты написал код, который считывает текст из файла и выводит в консоль первую строку.public class Main {
public static void main(String[] args) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));
String firstString = reader.readLine();
System.out.println(firstString);
}
}
Но такого файла не существует!
Результатом работы программы будет исключение — FileNotFoundException
.
Вывод:
Exception in thread "main" java.io.FileNotFoundException: C:\Users\Username\Desktop\test.txt (Системе не удается найти указанный путь)
Каждое исключение представлено в Java отдельным классом.
Все классы исключений происходят от общего “предка” — родительского класса Throwable
.
Название класса-исключения обычно коротко отображает причину его возникновения:
FileNotFoundException
(файл не найден)ArithmeticException
(исключение при выполнении математической операции)ArrayIndexOutOfBoundsException
(указан номер ячейки массива за пределами его длины). Например, если попытаться вывести в консоль ячейку array[23] для массива array длиной 10.
Exception in thread "main"
Э-э-э-м :/ Ничего не понятно. Что за ошибка, откуда взялась — неясно. Никакой полезной информации нет.
А вот благодаря такому разнообразию классов программист получает для себя главное — тип ошибки и ее вероятную причину, которая заложена в названии класса.
Ведь совсем другое дело увидеть в консоли:
Exception in thread "main" java.io.FileNotFoundException: C:\Users\Username\Desktop\test.txt (Системе не удается найти указанный путь)
Сразу становится понятно, в чем может быть дело и “в какую сторону копать” для решения проблемы!
Исключения, как и любые экземпляры классов, являются объектами.
Перехват и обработка исключений
Для работы с исключениями в Java существуют специальные блоки кода:try
, catch
и finally
.
Код, в котором программист ожидает возникновения исключений, помещается в блок try
. Это не значит, что исключение в этом месте обязательно произойдет. Это значит, что оно может там произойти, и программист в курсе этого.
Тип ошибки, который ты ожидаешь получить, помещается в блок catch
(“перехват”). Сюда же помещается весь код, который нужно выполнить, если исключение произойдет.
Вот пример:
public static void main(String[] args) throws IOException {
try {
BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));
String firstString = reader.readLine();
System.out.println(firstString);
} catch (FileNotFoundException e) {
System.out.println("Ошибка! Файл не найден!");
}
}
Вывод:
Ошибка! Файл не найден!
Мы поместили наш код в два блока. В первом блоке мы ожидаем, что может произойти ошибка “Файл не найден”. Это блок try
.
Во втором — указываем программе что делать, если произошла ошибка. Причем ошибка конкретного вида — FileNotFoundException
. Если мы передадим в скобки блока catch
другой класс исключения, оно не будет перехвачено.
public static void main(String[] args) throws IOException {
try {
BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));
String firstString = reader.readLine();
System.out.println(firstString);
} catch (ArithmeticException e) {
System.out.println("Ошибка! Файл не найден!");
}
}
Вывод:
Exception in thread "main" java.io.FileNotFoundException: C:\Users\Username\Desktop\test.txt (Системе не удается найти указанный путь)
Код в блоке catch
не отработал, потому что мы “настроили” этот блок на перехват ArithmeticException
, а код в блоке try
выбросил другой тип — FileNotFoundException
. Для FileNotFoundException
мы не написали сценарий, поэтому программа вывела в консоль ту информацию, которая выводится по умолчанию для FileNotFoundException
.
Здесь тебе нужно обратить внимание на 3 вещи.
Первое.
Как только в какой-то строчке кода в блоке try возникнет исключение, код после нее уже не будет выполнен. Выполнение программы сразу “перепрыгнет” в блок catch
.
Например:
public static void main(String[] args) {
try {
System.out.println("Делим число на ноль");
System.out.println(366/0);//в этой строчке кода будет выброшено исключение
System.out.println("Этот");
System.out.println("код");
System.out.println("не");
System.out.println("будет");
System.out.println("выполнен!");
} catch (ArithmeticException e) {
System.out.println("Программа перепрыгнула в блок catch!");
System.out.println("Ошибка! Нельзя делить на ноль!");
}
}
Вывод:
Делим число на ноль
Программа перепрыгнула в блок catch!
Ошибка! Нельзя делить на ноль!
В блоке try
во второй строчке мы попытались разделить число на 0, в результате чего возникло исключение ArithmeticException
.
После этого строки 6-10 блока try
выполнены уже не будут. Как мы и говорили, программа сразу начала выполнять блок catch
.
Второе.
Блоков catch
может быть несколько.
Если код в блоке try
может выбросить не один, а несколько видов исключений, для каждого из них можно написать свой блок catch
.
public static void main(String[] args) throws IOException {
try {
BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));
System.out.println(366/0);
String firstString = reader.readLine();
System.out.println(firstString);
} catch (FileNotFoundException e) {
System.out.println("Ошибка! Файл не найден!");
} catch (ArithmeticException e) {
System.out.println("Ошибка! Деление на 0!");
}
}
В этом примере мы написали два блока catch
. Если в блоке try
произойдет FileNotFoundException
, будет выполнен первый блок catch
.
Если произойдет ArithmeticException
, выполнится второй.
Блоков catch
ты можешь написать хоть 50. Но, конечно, лучше не писать код, который может выбросить 50 разных видов ошибок :)
Третье.
Откуда тебе знать, какие исключения может выбросить твой код?
Ну, про некоторые ты, конечно, можешь догадываться, но держать все в голове невозможно. Поэтому компилятор Java знает о самых распространенных исключениях и знает, в каких ситуациях они могут возникнуть.
Например, если ты написал код и компилятор знает, что при его работе могут возникнуть 2 вида исключений, твой код не скомпилируется, пока ты их не обработаешь. Примеры этого мы увидим ниже.
Теперь что касается обработки исключений. Существует 2 способа их обработки.
С первым мы уже познакомились — метод может обработать исключение самостоятельно в блоке catch()
.
Есть и второй вариант — метод может выбросить исключение вверх по стеку вызовов.
Что это значит?
Например, у нас в классе есть метод — все тот же printFirstString()
, который считывает файл и выводит в консоль его первую строку:
public static void printFirstString(String filePath) {
BufferedReader reader = new BufferedReader(new FileReader(filePath));
String firstString = reader.readLine();
System.out.println(firstString);
}
На текущий момент наш код не компилируется, потому что в нем есть необработанные исключения.
В строке 1 ты указываешь путь к файлу. Компилятор знает, что такой код легко может привести к FileNotFoundException
.
В строке 3 ты считываешь текст из файла. В этом процессе легко может возникнуть IOException
— ошибка при вводе-выводе данных (Input-Output).
Сейчас компилятор говорит тебе: “Чувак, я не одобрю этот код и не скомпилирую его, пока ты не скажешь мне, что я должен делать в случае, если произойдет одно из этих исключений. А они точно могут произойти, исходя из того кода, который ты написал!”.
Деваться некуда, нужно обрабатывать оба!
Первый вариант обработки нам уже знаком: надо поместить наш код в блок try
, и добавить два блока catch
:
public static void printFirstString(String filePath) {
try {
BufferedReader reader = new BufferedReader(new FileReader(filePath));
String firstString = reader.readLine();
System.out.println(firstString);
} catch (FileNotFoundException e) {
System.out.println("Ошибка, файл не найден!");
e.printStackTrace();
} catch (IOException e) {
System.out.println("Ошибка при вводе/выводе данных из файла!");
e.printStackTrace();
}
}
Но это не единственный вариант.
Мы можем не писать сценарий для ошибки внутри метода, и просто пробросить исключение наверх.
Это делается с помощью ключевого слова throws
, которое пишется в объявлении метода:
public static void printFirstString(String filePath) throws FileNotFoundException, IOException {
BufferedReader reader = new BufferedReader(new FileReader(filePath));
String firstString = reader.readLine();
System.out.println(firstString);
}
После слова throws
мы через запятую перечисляем все виды исключений, которые этот метод может выбросить при работе.
Зачем это делается?
Теперь, если кто-то в программе захочет вызвать метод printFirstString()
, он должен будет сам реализовать обработку исключений. К примеру, в другой части программы кто-то из твоих коллег написал метод, внутри которого вызывает твой метод printFirstString()
:
public static void yourColleagueMethod() {
//...метод твоего коллеги что-то делает
//...и в один момент вызывает твой метод printFirstString() c нужным ему файлом
printFirstString("C:\\Users\\Евгений\\Desktop\\testFile.txt");
}
Ошибка, код не компилируется!
В методе printFirstString()
мы не написали сценарий обработки ошибок. Поэтому задача ложится на плечи тех, кто будет этот метод использовать.
То есть перед методом yourColleagueMethod()
теперь стоят те же 2 варианта: он должен или обработать оба исключения, которые ему “прилетели”, с помощью try-catch
, или пробросить их дальше.
public static void yourColleagueMethod() throws FileNotFoundException, IOException {
//...метод что-то делает
//...и в один момент вызывает твой метод printFirstString() c нужным ему файлом
printFirstString("C:\\Users\\Евгений\\Desktop\\testFile.txt");
}
Во втором случае обработка ляжет на плечи следующего по стэку метода — того, который будет вызывать yourColleagueMethod()
.
Вот поэтому такой механизм называется “пробрасыванием исключения наверх”, или “передачей наверх”.
Когда ты пробрасываешь исключения наверх с помощью throws
, код компилируется. Компилятор в этот момент как бы говорит: “Окей, ладно. Твой код содержит кучу потенциальных исключений, но я, так и быть, его скомпилирую. Мы еще вернемся к этому разговору!”
И когда ты где-то в программе вызываешь метод, который не обработал свои исключения, компилятор выполняет свое обещание и снова напоминает о них.
В завершении мы поговорим о блоке finally
(простите за каламбур). Это последняя часть триумвирата обработки исключений try-catch-finally
.
Его особенность в том, что он выполняется при любом сценарии работы программы.
public static void main(String[] args) throws IOException {
try {
BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));
String firstString = reader.readLine();
System.out.println(firstString);
} catch (FileNotFoundException e) {
System.out.println("Ошибка! Файл не найден!");
e.printStackTrace();
} finally {
System.out.println("А вот и блок finally!");
}
}
В этом примере код внутри блока finally
выполняется в обоих случаях.
Если код в блоке try
выполнится целиком и не выбросит исключения, в конце сработает блок finally
.
Если код внутри try
прервется, и программа перепрыгнет в блок catch
, после того, как отработает код внутри catch
, все равно будет выбран блок finally
.
Зачем он нужен?
Его главное назначение — выполнить обязательную часть кода; ту часть, которая должна быть выполнена независимо от обстоятельств.
Например, в нем часто освобождают какие-то используемые программой ресурсы.
В нашем коде мы открываем поток для чтения информации из файла и передаем его в объект BufferedReader
.
Наш reader
нужно закрыть и освободить ресурсы. Это нужно сделать в любом случае: неважно, отработает программа как надо или вызовет исключение.
Это удобно делать в блоке finally
:
public static void main(String[] args) throws IOException {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));
String firstString = reader.readLine();
System.out.println(firstString);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
System.out.println("А вот и блок finally!");
if (reader != null) {
reader.close();
}
}
}
Теперь мы точно уверены, что позаботились о занятых ресурсах независимо от того, что произойдет при работе программы :)
Это еще не все, что тебе нужно знать об исключениях.
Обработка ошибок — очень важная тема в программировании: ей посвящена не одна статья.
На следующем занятии мы узнаем, какие бывают виды исключений и как создать свое собственное исключение:) До встречи!