JavaRush /Java блог /Random /Кофе-брейк #203. Как обрабатывать исключения с помощью оп...

Кофе-брейк #203. Как обрабатывать исключения с помощью оператора try-with-resource

Статья из группы Random
Источник: Medium В этом руководстве описаны преимущества оператора try-with-resource по сравнению с try-catch-finally. Также вы узнаете, при каких условиях появляются подавленные исключения и как использовать try-with-resources с несколькими ресурсами. Кофе-брейк #203. Как обрабатывать исключения с помощью оператора try-with-resource - 1Конструкция try with resources, также известная как try-with-resources, представляет собой механизм обработки исключений в Java, который может автоматически закрывать такие ресурсы, как Java InputStream или JDBC Connection, после завершения работы с ними. В 2011 году компания Oracle добавила try with resources в синтаксис языка Java, чтобы гарантировать, что такие объекты, как сетевые сокеты, подключения к базе данных и ссылки на файлы и папки, будут корректно закрываться после их использования. Невозможность закрыть эти ресурсы после того, как разработчик открывает для них дескриптор, может привести к утечке памяти, срабатыванию предотвратимых процедур сборки мусора и перегрузке процессора на сервере.

До Java 7

В Java, если вы используете такие ресурсы, как потоки ввода-вывода (input/output streams), вам всегда нужно закрывать их после использования. Поскольку они также могут генерировать исключения, они должны находиться в блоке try-catch. Закрытие должно происходить в блоке finally. По крайней мере, так было до Java 7. Но у этого есть несколько недостатков:
  • Вам нужно проверить, не является ли ваш ресурс null, прежде чем закрывать его.
  • Само закрытие может генерировать исключения, поэтому в вашем блоке finally приходится иметь еще один try-catch.
  • Программисты склонны забывать закрывать свои ресурсы.

Как использовать оператор try-with-resource?

Первоначально этот оператор представили в Java 7 и его идея заключалась в том, что разработчикам теперь не нужно беспокоиться об управлении ресурсами, которые они используют в блоке try-catch-finally. Это достигается за счет устранения необходимости в блоках finally, которые на практике использовались разработчиками лишь для закрытия ресурсов. В Java оператор try-with-resources — это оператор try, который объявляет один или несколько ресурсов. Ресурс представляет собой объект, который необходимо закрыть после завершения программы. Когда выполнение кода покидает блок try-with-resources, то любой ресурс, открытый в блоке try-with-resources, автоматически закрывается, независимо от того, появляются ли какие-либо исключения либо внутри блока try-with-resources, либо при попытке закрыть ресурсы. Чтобы использовать языковую функцию Java try-with-resources, применяются следующие правила:
  • Все объекты, управляемые оператором try-with-resources, должны реализовывать интерфейс AutoCloseable.
  • В блоке try-with-resources могут быть созданы несколько объектов AutoCloseable.
  • Объекты, объявленные в операторе try-with-resources, выполняются в блоке try, но не в блоках catch и finally.
  • Метод close() объектов, объявленных в блоке try-with-resources, вызывается независимо от того, появляется ли исключение во время выполнения.
  • Если в методе close() генерируется исключение, оно может быть классифицировано как подавленное исключение (suppressed exception).
Блоки catch и finally по-прежнему можно использовать в блоке try-with-resource, и они будут работать так же, как и в обычном блоке try. Ресурс, на который ссылается объект AutoCloseable, всегда будет закрыт, если используется try-with-resource. Так устраняются потенциальные утечки памяти, обычно вызываемые неправильным распределением ресурсов.

Синтаксис


try(declare resources here) {
    // использовать ресурсы
}
catch(FileNotFoundException e) {
    // обработка исключений
}

Практическое использование try-with-resource

Для автоматического закрытия ресурс должен быть объявлен и инициализирован внутри try:

try (PrintWriter writer = new PrintWriter(new File("test.txt"))) {
    writer.println("Hello World");
}

Замена try-catch-finally на try-with-resources

Простой и очевидный способ использования функциональности try-with-resources — это замена традиционного и многословного блока try-catch-finally. Давайте сравним следующие примеры кода. Первый пример — это типичный блок try-catch-finally:

Scanner scanner = null;
try {
    scanner = new Scanner(new File("test.txt"));
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} finally {
    if (scanner != null) {
        scanner.close();
    }
}
А вот новое супер лаконичное решение с использованием try-with-resources:

try (Scanner scanner = new Scanner(new File("test.txt"))) {
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException fnfe) {
    fnfe.printStackTrace();
}

Разница между try и try-with-resource

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

Почему я должен использовать оператор try-with-resource?

Оператор try-with-resources гарантирует, что каждый ресурс будет закрыт в конце выполнения оператора. Если мы не закроем ресурсы, это может привести к утечке ресурсов и программа может исчерпать доступные ей ресурсы. Так происходит, когда вы используете блок try-catch-finally. До Java SE 7 вы могли использовать блок finally, чтобы гарантировать, что ресурс будет закрыт независимо от того, как завершает работу оператор try: нормально или внезапно. В следующем примере вместо оператора try-with-resources используется блок finally:

static String readFirstLineFromFileWithFinallyBlock(String path) throws IOException {
   
    FileReader fr = new FileReader(path);
    BufferedReader br = new BufferedReader(fr);
    try {
        return br.readLine();
    } finally {
        br.close();
        fr.close();
    }
}
В этом примере может быть утечка ресурсов. Программа должна делать больше, чем полагаться на сборщик мусора для освобождения памяти ресурса после завершения работы с ним. Программа также должна вернуть ресурс обратно в операционную систему, обычно с помощью вызова метода ресурса close. Однако если программа не сделает этого до того, как сборщик мусора вернет ресурс, то информация, необходимая для освобождения ресурса, будет утеряна. Происходит утечка ресурса, который операционная система до сих пор считает используемым. В показанном выше примере, если метод readLine выдает исключение, и оператор br.close() в блоке finally выдает исключение, то произошла утечка FileReader. Именно поэтому вам лучше использовать оператор try-with-resources вместо блока finally, чтобы закрыть ресурсы вашей программы.

Еще один пример

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

import java.io.*;
class Main {
  public static void main(String[] args) {
    String line;
    try(BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
      while ((line = br.readLine()) != null) {
        System.out.println("Line =>"+line);
      }
    } catch (IOException e) {
      System.out.println("IOException in try block =>" + e.getMessage());
    }
  }
}
Как видите, ресурсом, объявленном в операторе try-with-resources, является BufferedReader. Операторы объявления этого ресурса появляются в круглых скобках сразу после ключевого слова try. Классы BufferedReader в Java SE 7 и более поздних версиях реализуют интерфейс java.lang.AutoCloseable. Поскольку экземпляры BufferedReader объявлены в операторе try-with-resource, они будут закрыты независимо от того, завершит ли работу оператор try нормально или внезапно (если метод BufferedReader.readLine() сгенерировал исключение IOException).

Подавленные исключения

Если блок try выдает исключение, и одно или несколько исключений появляется в блоке try-with-resources, то исключения, создаваемые блоком try-with-resources, подавляются. Иными словами, мы можем сказать, что исключения, генерируемые try-with-resources, являются подавленными исключениями. Вы можете получить эти исключения, используя метод getSuppress() класса Throwable. В показанном раньше примере исключения были вызваны оператором try-with-resources, при следующих условиях:
  • Файл test.txt не найден.
  • Закрытие объекта BufferedReader.
Исключение также может быть выброшено из блока try, поскольку чтение файла может завершиться ошибкой по многим причинам в любое время. Если исключения выбрасываются как из блока try, так и из оператора try-with-resources, то в первом случае возникает исключение, а во втором случае исключение подавляется.

Получение подавленных исключений

В Java 7 и более поздних версиях подавленные исключения можно получить, вызвав метод Throwable.getSuppressed() из исключения, созданного блоком try. getSuppress() возвращает массив, содержащий все исключения, которые были подавлены оператором try-with-resources. Если никакие исключения не были подавлены или подавление отключено, возвращается пустой массив. Перед вами пример получения подавленных исключений в блоке catch:

catch(IOException e) {
  System.out.println("Thrown exception=>" + e.getMessage());
  Throwable[] suppressedExceptions = e.getSuppressed();
  for (int i=0; i" + suppressedExceptions[i]);
  }
}

Преимущества использования try-with-resources

  • Удобочитаемый и простой в написании код.
  • Автоматическое управление ресурсами.
  • Количество строк кода уменьшено.
  • Когда несколько ресурсов открываются в try-with-resources, они закрываются в обратном порядке, чтобы избежать проблем с зависимостями.
И конечно же, теперь для закрытия ресурса не требуется блок finally. Ранее, до Java 7, нам приходилось использовать блок finally, чтобы убедиться, что ресурс закрыт, чтобы избежать утечек ресурсов. Вот программа, похожая на самый первый пример. В этой программе мы использовали блок finally для закрытия ресурсов.

import java.io.*;
class Main {
  public static void main(String[] args) {
    BufferedReader br = null;
    String line;
    try {
      System.out.println("Entering try block");
      br = new BufferedReader(new FileReader("test.txt"));
      while ((line = br.readLine()) != null) {
        System.out.println("Line =>"+line);
      }
    } catch (IOException e) {
      System.out.println("IOException in try block =>" + e.getMessage());
    } finally {
      System.out.println("Entering finally block");
      try {
        if (br != null) {
          br.close();
        }
      } catch (IOException e) {
        System.out.println("IOException in finally block =>"+e.getMessage());
      }
    }
  }
}
Вывод:
Entering try block Line =>line from test.txt file Entering finally block
Как видно из приведенного выше примера, использование блока finally для очистки ресурсов усложняет код. Обратили внимание на блок try...catch в блоке finally? Это связано с тем, что исключение IOException также может возникнуть при закрытии экземпляра BufferedReader внутри блока finally, поэтому оно также перехватывается и обрабатывается. Оператор try-with-resources выполняет автоматическое управление ресурсами. Нам не нужно явно закрывать ресурсы, поскольку JVM автоматически их закрывает. Это делает код более читабельным и легким для написания.

Try-with-resources с несколькими ресурсами

Мы можем объявить несколько ресурсов в блоке try-with-resources, просто разделив их точкой с запятой:

try (Scanner scanner = new Scanner(new File("testRead.txt"));
    PrintWriter writer = new PrintWriter(new File("testWrite.txt"))) {
    while (scanner.hasNext()) {
	writer.print(scanner.nextLine());
    }
}

Java 9 — Effectively Final Variables

До Java 9 мы могли использовать свежие переменные только внутри блока try-with-resources:

try (Scanner scanner = new Scanner(new File("testRead.txt")); 
    PrintWriter writer = new PrintWriter(new File("testWrite.txt"))) { 
    // omitted
}
Заметьте, это довольно многословно при объявлении нескольких ресурсов. Начиная с Java 9 (обновление JEP 213), мы можем использовать переменные final или даже effectively final внутри блока try-with-resources:

final Scanner scanner = new Scanner(new File("testRead.txt"));
PrintWriter writer = new PrintWriter(new File("testWrite.txt"))
try (scanner;writer) { 
    // omitted
}
Проще говоря, переменная фактически является final, если она не изменяется после первого присваивания, даже если она явно не помечена как final. Как показано выше, переменная scanner явно объявлена ​​как final, поэтому мы можем использовать ее с блоком try-with-resources. Хотя переменная writer не является явно final, она не меняется после первого присваивания. Таким образом, мы также можем использовать переменную writer. Надеюсь, сегодня вы получили лучшее представление о том, как обрабатывать исключения с помощью оператора try-with-resource. Приятного обучения!
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Ivan Уровень 30
13 июня 2023
Классная статья, спасибо большое за труд!