Одним из основных нововведений, которые были включены в Java 7, стало введение нового оператора «try c ресурсами». «try c ресурсами» это оператор try, в котором объявляются один или несколько ресурсов. Ресурс — это объект, который должен быть закрыт после того, как программа закончит с ним работу. «try c ресурсами» берет всю работу по закрытию ресурсов на себя. Прежде, чем подробно его рассмотреть давайте разберемся в причинах, которые вызвали его появление.
Закрытие ресурсов до Java 7.
Предположим нам нужно написать метод, который считывает первую строчку из одного файла и записывает ее в другой файл. Задача очень простая и ее реализация не должна вызвать каких-либо затруднений:
1 2 3 4 5 6 7 8 9 10 11 12 |
public void rwLine(Path pathRead, Path pathWrite) throws IOException{ BufferedReader in = null; BufferedWriter out = null; try{ in = Files.newBufferedReader(pathRead); out = Files.newBufferedWriter(pathWrite); out.write(in.readLine()); } finally { if (in != null) in.close(); if (out != null) out.close(); } } |
Слишком много кода для столь простой операции, не находите? Но и это еще не все, в коде выше есть существенный изъян, если при закрытии первого ресурса:
1 |
if (in != null) in.close(); |
будет сгенерировано исключение, то второй ресурс закрыт не будет:
1 |
if (out != null) out.close(); |
Самым очевидным выходом из данной ситуации – будет окружить закрытие ресурсов дополнительными блоками try:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
finally { try{ if (in != null) in.close(); } catch(IOException e){ } try{ if (out != null) out.close(); } catch(IOException e){ } } |
Согласитесь, что выглядит ужасно, но до выхода Java 7 с этим приходилось мириться.
Использование try с ресурсами.
Появление оператора try с ресурсами значительно упростило жизнь программистам, больше не было необходимости громоздить конструкции try/catch в надежде, что ты закрыл все ресурсы и предусмотрел все возможные исключительные ситуации, большую часть этой работы try с ресурсами взял на себя. Рассмотрим его общий вид:
1 2 3 4 5 6 7 8 |
try(BufferedReader reader = Files.newBufferedReader(path1); BufferedWriter writer = Files.newBufferedWriter(path2)){ } catch(IOException){ } finally{ } |
Главное отличие от привычного блока try в круглых скобках, в которых создаются ресурсы, которые впоследствии нужно закрыть, ресурсы будут закрываться снизу-вверх автоматически после завершения работы блока try, т.е. в примере, сначала закроется writer, а потом reader. Ресурсы созданные в блоке try(), в нашем случае reader и writer будут видны только в блоке try, в блоках catch и finally попытка их использования вызовет ошибку компиляции.
При использовании оператора try с ресурсами, совершенно не обязательно использовать блоки catch и finally, они являются опциональными, исходя из этого предыдущий пример можно переписать следующим образом:
1 2 3 4 |
try(BufferedReader reader = Files.newBufferedReader(path1); BufferedWriter writer = Files.newBufferedWriter(path2)){ } |
Подобный код абсолютно легален и никаких ошибок компиляции не вызовет.
Настало время улучшить наш пример из начала статьи, который копирует первую строчку из одного файла в другой:
1 2 3 4 5 6 |
public void rwLine(Path pathRead, Path pathWrite) throws IOException{ try(BufferedReader in = Files.newBufferedReader(pathRead); BufferedWriter out = Files.newBufferedWriter(pathWrite);){ out.write(in.readLine()); } } |
Получилось в два раза меньше кода, чем в первоначальном примере.
Интерфейс AutoCloseable.
Для того, чтобы класс можно было использовать в операторе try с ресурсами, он должен реализовывать интерфейс AutoCloseable. Хорошая новость, в том, что сделать это не так уж и сложно – необходимо реализовать всего лишь один метод – public void close() throws Exception.
1 2 3 4 5 6 7 8 9 10 11 12 |
public class Example implements AutoCloseable{ public static void main(String[] args){ try(Example ex = new Example()){ System.out.println("Try"); } } @Override public void close(){ System.out.println("Close"); } } |
Вывод:
Try
Close
Как и следовало ожидать сначала выполнился код из блока try, а потом произошло закрытие ресурса, с помощью переопределенного метода close(). Вы наверное заметили, что в примере, метод close() не генерирует и не объявляет никаких исключений, в отличии от метода объявленного в интерфейсе AutoCloseable – это вполне легально, переопределяемый метод может генерировать более конкретные исключения или не генерировать их вовсе. Еще на что хотелось бы обратить пристальное внимание, так это на саму реализацию метода close(). Метод close() не должен содержать никакой бизнес логики, только закрытие ресурсов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class Example implements AutoCloseable{ static int number = 0; public static void main(String[] args){ try(Example ex = new Example()){ System.out.println("Try"); } } @Override public void close(){ System.out.println("Close"); Example.number++; } } |
Данный код конечно же скомпилируется, но закрытие ресурсов будет вызывать побочный эффект и изменять значение переменной, чего конечно же нужно избегать.
Подавленные исключения.
Подавленные исключения (suppressed exception) образуются, когда в блоке try генерируется исключение и в методе close() при закрытии ресурса генерируется исключение, в этом случае первое исключение считается главным остальные подавленные, чтобы было понятнее рассмотрим пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class Example implements AutoCloseable{ static int number = 0; public static void main(String[] args){ try(Example ex1 = new Example(); Example ex2 = new Example()){ throw new RuntimeException("Main exception"); } catch (Exception e){ System.out.println(e.getMessage()); for(Throwable t : e.getSuppressed()){ System.out.println(t.getMessage()); } } } @Override public void close() throws IllegalStateException{ throw new IllegalStateException("Suppressed Exception"); } } |
Вывод:
Main exception
Suppressed Exception
Suppressed Exception
В нашем случае получился следующий ход выполнения программы: сначала создаются два объекта Example ex1 и ex2, после чего происходит вход в блок try, где генерируется исключение, программа прекращает выполнения кода в блоке и приступает к закрытию ресурсов снизу-вверх. При закрытии каждого из ресурсов в методе close() генерируется еще по одному исключению, которые последовательно добавляются к главному исключению, сгенерированному в блоке try. После того, как ресурсы были закрыты, программа приступает к обработке исключения в блоке catch, где первой строчкой мы выводим информацию о главном исключении, а последующим циклом получаем подавленные исключения и выводим информацию о них.
Последнее на что стоит обратить внимание, так это, что подавленные исключения работают только в блоке try, в следующем примере будет сгенерировано два самостоятельных и независимых друг от друга исключения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Example implements AutoCloseable{ public static void main(String[] args){ try(Example ex1 = new Example()){ throw new RuntimeException(); } finally{ throw new IllegalStateException(); } } @Override public void close(){ System.out.println("close"); } } |
Даже, если в методе close() будет сгенерировано исключение и оно добавится к подавленным исключения для блока try, все они заменятся исключением, которое будет сгенерировано блоком finally.
Основная статья по работе с исключениями в Java: Обработка исключений в Java