Есть ли предпочтение для вложенных блоков try/catch?
Одна из вещей, которая всегда мешает мне использовать Readers and Streams в Java, это то, что close()
Метод может выдать исключение. Поскольку хорошая идея поместить метод close в блок finally, для этого требуется немного неловкая ситуация. Я обычно использую эту конструкцию:
FileReader fr = new FileReader("SomeFile.txt");
try {
try {
fr.read();
} finally {
fr.close();
}
} catch(Exception e) {
// Do exception handling
}
Но я также видел эту конструкцию:
FileReader fr = new FileReader("SomeFile.txt");
try {
fr.read()
} catch (Exception e) {
// Do exception handling
} finally {
try {
fr.close();
} catch (Exception e) {
// Do exception handling
}
}
Я предпочитаю первую конструкцию, потому что есть только один блок захвата, и он просто кажется более элегантным. Есть ли причина на самом деле предпочесть вторую или альтернативную конструкцию?
ОБНОВЛЕНИЕ: это имело бы значение, если бы я указал, что оба read
а также close
только выкинуть IOExceptions? Так что мне кажется вероятным, что, если чтение не удастся, закрыть не удастся по той же причине.
11 ответов
Я бы всегда пошел за первым примером.
Если бы close выкинул исключение (на практике это никогда не произойдет для FileReader), разве стандартным способом обработки не будет выбрасывание исключения, соответствующего вызывающей стороне? Закрытое исключение почти наверняка превзойдет любую проблему, возникшую при использовании ресурса. Второй метод, вероятно, более уместен, если ваша идея обработки исключений заключается в вызове System.err.println.
Существует проблема того, как далеко должны быть выброшены исключения. ThreadDeath всегда должен быть переброшен, но любое исключение внутри наконец остановит это. Точно так же Ошибка должна бросить дальше, чем RuntimeException и RuntimeException дальше, чем проверенные исключения. Если вы действительно хотите, вы можете написать код, который следует этим правилам, а затем абстрагировать его с помощью выражения "выполнить вокруг".
Я боюсь, что есть большая проблема с первым примером, который заключается в том, что если исключение происходит во время или после чтения, finally
блок выполняется. Все идет нормально. Но что если fr.close()
затем вызывает другое исключение? Это "превзойдет" первое исключение (немного похоже на return
в finally
блок), и вы потеряете всю информацию о том, что на самом деле вызвало проблему с самого начала.
Ваш блок finally должен использовать:
IOUtil.closeSilently(fr);
где этот метод утилиты просто делает:
public static void closeSilently(Closeable c) {
try { c.close(); } catch (Exception e) {}
}
Я предпочитаю второй. Зачем? Если оба read()
а также close()
бросить исключения, один из них может быть потерян. В первой конструкции исключение из close()
отменяет исключение из read()
в то время как во втором исключение из close()
обрабатывается отдельно.
Начиная с Java 7, конструкция try-with-resources делает это намного проще. Читать без заботы об исключениях:
try (FileReader fr = new FileReader("SomeFile.txt")) {
fr.read();
// no need to close since the try-with-resources statement closes it automatically
}
С обработкой исключений:
try (FileReader fr = new FileReader("SomeFile.txt")) {
fr.read();
// no need to close since the try-with-resources statement closes it automatically
} catch (IOException e) {
// Do exception handling
log(e);
// If this catch block is run, the FileReader has already been closed.
// The exception could have come from either read() or close();
// if both threw exceptions (or if multiple resources were used and had to be closed)
// then only one exception is thrown and the others are suppressed
// but can still be retrieved:
Throwable[] suppressed = e.getSuppressed(); // can be an empty array
for (Throwable t : suppressed) {
log(suppressed[t]);
}
}
Требуется только одна попытка, и все исключения могут быть безопасно обработаны. Вы все еще можете добавить finally
блокируйте, если хотите, но нет необходимости закрывать ресурсы.
Если и чтение, и закрытие выдают исключение, исключение из чтения будет скрыто в варианте 1. Таким образом, второй вариант делает больше обработки ошибок.
Однако в большинстве случаев первый вариант все равно будет предпочтительным.
- Во многих случаях вы не можете иметь дело с исключениями в методе, который они генерируют, но вы все равно должны инкапсулировать обработку потока в этой операции.
- Попробуйте добавить писателя в код и посмотрите, насколько многословен второй подход.
Если вам нужно передать все сгенерированные исключения, это можно сделать.
Разница, насколько я вижу, состоит в том, что существуют разные исключения и причины в игре на разных уровнях, и
поймать (исключение е)
затемняет это. Единственная цель нескольких уровней - различать ваши исключения и то, что вы будете делать с ними:
try
{
try{
...
}
catch(IOException e)
{
..
}
}
catch(Exception e)
{
// we could read, but now something else is broken
...
}
Я обычно делаю следующее. Во-первых, определите класс на основе шаблона-метода, чтобы справиться с беспорядком try/catch
import java.io.Closeable;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
public abstract class AutoFileCloser {
private static final Closeable NEW_FILE = new Closeable() {
public void close() throws IOException {
// do nothing
}
};
// the core action code that the implementer wants to run
protected abstract void doWork() throws Throwable;
// track a list of closeable thingies to close when finished
private List<Closeable> closeables_ = new LinkedList<Closeable>();
// mark a new file
protected void newFile() {
closeables_.add(0, NEW_FILE);
}
// give the implementer a way to track things to close
// assumes this is called in order for nested closeables,
// inner-most to outer-most
protected void watch(Closeable closeable) {
closeables_.add(0, closeable);
}
public AutoFileCloser() {
// a variable to track a "meaningful" exception, in case
// a close() throws an exception
Throwable pending = null;
try {
doWork(); // do the real work
} catch (Throwable throwable) {
pending = throwable;
} finally {
// close the watched streams
boolean skip = false;
for (Closeable closeable : closeables_) {
if (closeable == NEW_FILE) {
skip = false;
} else if (!skip && closeable != null) {
try {
closeable.close();
// don't try to re-close nested closeables
skip = true;
} catch (Throwable throwable) {
if (pending == null) {
pending = throwable;
}
}
}
}
// if we had a pending exception, rethrow it
// this is necessary b/c the close can throw an
// exception, which would remove the pending
// status of any exception thrown in the try block
if (pending != null) {
if (pending instanceof RuntimeException) {
throw (RuntimeException) pending;
} else {
throw new RuntimeException(pending);
}
}
}
}
}
Обратите внимание на "ожидающее" исключение - оно учитывает случай, когда исключение, выбрасываемое во время закрытия, маскирует исключение, о котором мы могли бы действительно заботиться.
Наконец, в конце концов сначала пытается закрыться снаружи любого декорированного потока, поэтому, если у вас есть BufferedWriter, обертывающий FileWriter, мы сначала пытаемся закрыть BuffereredWriter, и если это не удается, все равно попробуйте закрыть сам FileWriter.
Вы можете использовать вышеуказанный класс следующим образом:
try {
// ...
new AutoFileCloser() {
@Override protected void doWork() throws Throwable {
// declare variables for the readers and "watch" them
FileReader fileReader = null;
BufferedReader bufferedReader = null;
watch(fileReader = new FileReader("somefile"));
watch(bufferedReader = new BufferedReader(fileReader));
// ... do something with bufferedReader
// if you need more than one reader or writer
newFile(); // puts a flag in the
FileWriter fileWriter = null;
BufferedWriter bufferedWriter = null;
watch(fileWriter = new FileWriter("someOtherFile"));
watch(bufferedWriter = new BufferedWriter(fileWriter));
// ... do something with bufferedWriter
}
};
// .. other logic, maybe more AutoFileClosers
} catch (RuntimeException e) {
// report or log the exception
}
Используя этот подход, вам никогда не придется беспокоиться о try/catch / finally, чтобы снова закрывать файлы.
Если это слишком тяжело для вашего использования, по крайней мере подумайте о том, чтобы придерживаться подхода try/catch и подхода "ожидающих" переменных, который он использует.
2-й подход.
В противном случае я не вижу, чтобы вы ловили исключение из конструктора FileReader
http://java.sun.com/j2se/1.5.0/docs/api/java/io/FileReader.html
public FileReader(String fileName) выдает исключение FileNotFoundException
Итак, у меня обычно есть конструктор внутри блока try. Блок finally проверяет, не является ли читатель нулевым, прежде чем пытаться закрыть его.
Этот же шаблон применяется для источника данных, соединения, оператора, ResultSet.
Стандартное соглашение, которое я использую, состоит в том, что вы не должны позволять исключениям выходить из блока finally.
Это связано с тем, что если исключение уже распространяется, исключение, выброшенное из блока finally, превзойдет исходное исключение (и, таким образом, будет потеряно).
В 99% случаев это не то, что вам нужно, поскольку исходное исключение, вероятно, является источником вашей проблемы (любые вторичные исключения могут быть побочными эффектами от первого, но затенят вашу способность найти источник исходного исключения и, следовательно, реальное проблема).
Итак, ваш основной код должен выглядеть так:
try
{
// Code
}
// Exception handling
finally
{
// Exception handling that is garanteed not to throw.
try
{
// Exception handling that may throw.
}
// Optional Exception handling that should not throw
finally()
{}
}
Мне нравится подход @Chris Marshall, но мне никогда не нравится, когда исключения молча поглощаются. Я думаю, что лучше регистрировать исключения, особенно если вы продолжаете в любом случае.
Я всегда использую служебный класс для обработки подобных распространенных исключений, но я бы немного изменил его ответ.
Я всегда использовал бы регистратор (log4j для меня), чтобы регистрировать ошибки и т.д.
IOUtil.close(fr);
Небольшая модификация служебного метода:
public static void close(Closeable c) {
try {
c.close();
} catch (Exception e) {
logger.error("An error occurred while closing. Continuing regardless", e);
}
}
Иногда вложенный try-catch не является предпочтением, учтите это:
try{
string s = File.Open("myfile").ReadToEnd(); // my file has a bunch of numbers
// I want to get a total of the numbers
int total = 0;
foreach(string line in s.split("\r\n")){
try{
total += int.Parse(line);
} catch{}
}
catch{}
Это, вероятно, плохой пример, но иногда вам понадобится вложенный try-cactch.
В некоторых случаях вложенный Try-Catch неизбежен. Например, когда код восстановления ошибки сам может бросить и исключение. Но чтобы улучшить читабельность кода, вы всегда можете извлечь вложенный блок в собственный метод. Посмотрите этот пост в блоге, чтобы увидеть больше примеров для вложенных блоков Try-Catch-finally.