Правильная идиома для управления несколькими связанными ресурсами в блоке try-with-resources?

168

Синтаксис пробного использования ресурсов Java 7 (также известный как блок ARM ( Автоматическое управление ресурсами )) хорош, короток и понятен при использовании только одного AutoCloseableресурса. Однако я не уверен, какова правильная идиома, когда мне нужно объявить несколько ресурсов, которые зависят друг от друга, например a FileWriterи a, BufferedWriterкоторые обертывают его. Конечно, этот вопрос касается любого случая, когда некоторые AutoCloseableресурсы упакованы, а не только эти два конкретных класса.

Я придумал три следующих варианта:

1)

Наивная идиома, которую я видел, состоит в объявлении только оболочки верхнего уровня в переменной, управляемой ARM:

static void printToFile1(String text, File file) {
    try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
}

Это красиво и коротко, но оно сломано. Поскольку базовый FileWriterобъект не объявлен в переменной, он никогда не будет закрыт непосредственно в сгенерированном finallyблоке. Он будет закрыт только через closeметод упаковки BufferedWriter. Проблема заключается в том, что если исключение выдается из bwконструктора, оно closeне будет вызываться и, следовательно, базовое FileWriter не будет закрыто .

2)

static void printToFile2(String text, File file) {
    try (FileWriter fw = new FileWriter(file);
            BufferedWriter bw = new BufferedWriter(fw)) {
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
}

Здесь и базовый, и ресурс обёртывания объявляются в переменных, управляемых ARM, поэтому они оба обязательно будут закрыты, но базовый fw.close() будет вызываться дважды : не только напрямую, но и через обтекание bw.close().

Это не должно быть проблемой для этих двух конкретных классов, которые оба реализуют Closeable(что является подтипом AutoCloseable), чей контракт гласит, что closeразрешены множественные вызовы :

Закрывает этот поток и освобождает любые системные ресурсы, связанные с ним. Если поток уже закрыт, то вызов этого метода не имеет никакого эффекта.

Однако в общем случае у меня могут быть ресурсы, которые реализуют только AutoCloseable(и не реализуют Closeable), что не гарантирует, что их closeможно будет вызывать несколько раз:

Обратите внимание, что в отличие от метода закрытия java.io.Closeable, этот метод закрытия не обязательно должен быть идемпотентным. Другими словами, вызов этого метода close более одного раза может иметь некоторый видимый побочный эффект, в отличие от Closeable.close, который не должен иметь эффекта при вызове более одного раза. Однако разработчикам этого интерфейса настоятельно рекомендуется сделать их близкие методы идемпотентными.

3)

static void printToFile3(String text, File file) {
    try (FileWriter fw = new FileWriter(file)) {
        BufferedWriter bw = new BufferedWriter(fw);
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
}

Эта версия должна быть теоретически правильной, потому что только она fwпредставляет реальный ресурс, который необходимо очистить. Сам по bwсебе ресурс не содержит никаких ресурсов, он только делегирует его fw, поэтому его должно быть достаточно только для закрытия базового объекта fw.

С другой стороны, синтаксис немного нерегулярный, а также, Eclipse выдает предупреждение, которое я считаю ложной тревогой, но это все еще предупреждение, с которым нужно иметь дело:

Утечка ресурсов: «bw» никогда не закрывается


Итак, какой подход пойти? Или я пропустил какую-то другую идиому, которая является правильной ?

Natix
источник
4
Конечно, если базовый конструктор FileWriter выдает исключение, оно даже не открывается и все в порядке. Первый пример - это то, что происходит, если FileWriter создается, но конструктор BufferedWriter выдает исключение.
Натикс,
6
Стоит отметить, что BufferedWriter не будет генерировать исключение. Есть ли пример, который вы можете придумать, где этот вопрос не чисто академический.
Питер Лоури
10
@PeterLawrey Да, вы правы, что конструктор BufferedWriter в этом сценарии, скорее всего, не будет генерировать исключение, но, как я уже отмечал, этот вопрос касается любых ресурсов в стиле декоратора. Но например public BufferedWriter(Writer out, int sz)могу бросить IllegalArgumentException. Кроме того, я могу расширить BufferedWriter классом, который будет генерировать что-то из его конструктора, или создать любую пользовательскую оболочку, которая мне нужна.
Natix,
5
BufferedWriterКонструктор может легко бросить исключение. OutOfMemoryErrorвероятно, является наиболее распространенным, поскольку он выделяет достаточный объем памяти для буфера (хотя это может означать, что вы хотите перезапустить весь процесс). / Вы должны flushсвой контекстуальный, BufferedWriterесли вы не близко и хотите сохранить содержание ( как правило , только случае , не исключение). FileWriterПодбирает то, что происходит в кодировке файла по умолчанию - лучше быть явным.
Том Хотин - tackline
10
@Natix Я бы хотел, чтобы все вопросы в SO были так же хорошо изучены и ясны, как и этот. Я хотел бы проголосовать до 100 раз.
Компьютерщик

Ответы:

75

Вот мой взгляд на альтернативы:

1)

try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
    bw.write(text);
}

Для меня лучшее, что пришло на Java из традиционного C ++ 15 лет назад, это то, что вы можете доверять своей программе. Даже если что-то пошло не так, как это часто случается, я хочу, чтобы остальная часть кода работала лучше и пахла розами. В самом деле, здесь BufferedWriterможет быть исключение. Например, нехватка памяти не будет необычной. Для других декораторов, вы знаете, какой из java.ioклассов-оболочек выдает проверенное исключение из своих конструкторов? Я не. Понимание кода не очень хорошо, если вы полагаетесь на такие неясные знания.

Также есть «разрушение». Если возникает ошибка, то вы, вероятно, не хотите сбрасывать мусор в файл, который необходимо удалить (код для этого не показан). Хотя, конечно, удаление файла - еще одна интересная операция, связанная с обработкой ошибок.

Обычно вы хотите, чтобы finallyблоки были максимально короткими и надежными. Добавление флешей не помогает этой цели. Во многих выпусках некоторые из классов буферизации в JDK имели ошибку, из- flushза которой не closeвызывалось исключение изнутри, вызванное closeдля декорированного объекта. Хотя это было исправлено в течение некоторого времени, ожидайте его от других реализаций.

2)

try (
    FileWriter fw = new FileWriter(file);
    BufferedWriter bw = new BufferedWriter(fw)
) {
    bw.write(text);
}

Мы все еще очищаем неявный блок finally (теперь с повторением close- это ухудшается, когда вы добавляете больше декораторов), но конструкция безопасна, и мы должны неявно блокировать блоки finally, чтобы даже сбой flushне препятствовал освобождению ресурса.

3)

try (FileWriter fw = new FileWriter(file)) {
    BufferedWriter bw = new BufferedWriter(fw);
    bw.write(text);
}

Здесь есть ошибка. Должно быть:

try (FileWriter fw = new FileWriter(file)) {
    BufferedWriter bw = new BufferedWriter(fw);
    bw.write(text);
    bw.flush();
}

Некоторые плохо реализованные декораторы фактически являются ресурсом и должны быть надежно закрыты. Кроме того, некоторые потоки, возможно, должны быть закрыты определенным образом (возможно, они выполняют сжатие и должны записывать биты для завершения, и не могут просто очистить все.

решение суда

Хотя 3 - технически превосходное решение, причины разработки программного обеспечения делают 2 лучшим выбором. Однако try-with-resource все еще неадекватное исправление, и вам следует придерживаться идиомы Execute Around , которая должна иметь более четкий синтаксис с замыканиями в Java SE 8.

Том Хотин - Tackline
источник
4
В версии 3, откуда вы знаете, что bw не требует, чтобы его close вызывали? И даже если вы можете быть уверены в этом, разве это не «неясное знание», как вы упомянули для версии 1?
TimK
3
« Причины разработки программного обеспечения делают выбор 2 лучшим » Можете ли вы объяснить это утверждение более подробно?
Дункан Джонс
8
Можете ли вы привести пример «Выполнить вокруг идиомы с замыканиями»
Маркус
2
Можете ли вы объяснить, что такое «более понятный синтаксис с замыканиями в Java SE 8»?
Петр
1
Пример «Выполнить вокруг идиомы» можно
найти
20

Первый стиль - тот, который предложен Oracle . BufferedWriterне генерирует проверенные исключения, поэтому, если выдается какое-либо исключение, программа не должна восстанавливаться после него, что делает восстановление ресурса в основном спорным.

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

Даниэль С. Собрал
источник
2
Это также рекомендуемый подход в 3-й редакции Effective Java.
Шмосель
5

Вариант 4

Измените ваши ресурсы, чтобы они были закрываемыми, а не автоматически закрываемыми, если вы можете. Тот факт, что конструкторы могут быть объединены в цепочку, подразумевает, что нет ничего удивительного в том, чтобы дважды закрыть ресурс. (Это было верно и до ARM.) Подробнее об этом ниже.

Вариант 5

Не используйте ARM и код очень осторожно, чтобы close () не вызывался дважды!

Вариант 6

Не используйте ARM и сделайте ваши вызовы close () в попытке / поймать себя.

Почему я не думаю, что эта проблема уникальна для ARM

Во всех этих примерах вызовы finally () должны находиться в блоке catch. Оставлено для удобства чтения.

Ничего хорошего, потому что fw можно закрыть дважды. (что хорошо для FileWriter, но не в вашем гипотетическом примере):

FileWriter fw = null;
BufferedWriter bw = null;
try {
  fw = new FileWriter(file);
  bw = new BufferedWriter(fw);
  bw.write(text);
} finally {
  if ( fw != null ) fw.close();
  if ( bw != null ) bw.close();
}

Не хорошо, потому что fw не закрывается, если исключение при создании BufferedWriter. (опять не может произойти, но в вашем гипотетическом примере):

FileWriter fw = null;
BufferedWriter bw = null;
try {
  fw = new FileWriter(file);
  bw = new BufferedWriter(fw);
  bw.write(text);
} finally {
  if ( bw != null ) bw.close();
}
Жанна Боярская
источник
3

Я просто хотел основываться на предложении Жанны Боярской не использовать ARM, но убедиться, что FileWriter всегда закрывается ровно один раз. Не думайте, что здесь есть какие-то проблемы ...

FileWriter fw = null;
BufferedWriter bw = null;
try {
    fw = new FileWriter(file);
    bw = new BufferedWriter(fw);
    bw.write(text);
} finally {
    if (bw != null) bw.close();
    else if (fw != null) fw.close();
}

Я думаю, поскольку ARM - это просто синтаксический сахар, мы не всегда можем использовать его для замены блоков finally. Так же, как мы не всегда можем использовать цикл for-each, чтобы сделать что-то, что возможно с итераторами.

AmyGamy
источник
5
Если ваш tryи finallyблоки генерируют исключения, эта конструкция потеряет первую (и потенциально более полезную).
rxg
3

Чтобы согласиться с более ранними комментариями: простейшим является (2) использовать Closeableресурсы и объявлять их по порядку в предложении try-with-resources. Если у вас есть только AutoCloseable, вы можете обернуть их в другой (вложенный) класс, который просто проверяет, что closeвызывается только один раз (Facade Pattern), например, имея private bool isClosed;. На практике даже Oracle просто (1) объединяет в цепочку конструкторы и неправильно обрабатывает исключения во время цепочки.

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

static BufferedWriter createBufferedWriterFromFile(File file)
  throws IOException {
  // If constructor throws an exception, no resource acquired, so no release required.
  FileWriter fileWriter = new FileWriter(file);
  try {
    return new BufferedWriter(fileWriter);  
  } catch (IOException newBufferedWriterException) {
    try {
      fileWriter.close();
    } catch (IOException closeException) {
      // Exceptions in cleanup code are secondary to exceptions in primary code (body of try),
      // as in try-with-resources.
      newBufferedWriterException.addSuppressed(closeException);
    }
    throw newBufferedWriterException;
  }
}

Затем вы можете использовать его как отдельный ресурс в предложении try-with-resources:

try (BufferedWriter writer = createBufferedWriterFromFile(file)) {
  // Work with writer.
}

Сложность возникает из-за обработки нескольких исключений; в противном случае это просто «закрыть ресурсы, которые вы приобрели до сих пор». Обычная практика заключается в том, чтобы сначала инициализировать переменную, которая содержит объект, который содержит ресурс дляnull (здесь fileWriter), а затем включить нулевую проверку в очистке, но это кажется ненужным: если конструктор завершается ошибкой, нечего очищать, поэтому мы можем просто позволить этому исключению распространяться, что немного упрощает код.

Вы могли бы сделать это в общем:

static <T extends AutoCloseable, U extends AutoCloseable, V>
    T createChainedResource(V v) throws Exception {
  // If constructor throws an exception, no resource acquired, so no release required.
  U u = new U(v);
  try {
    return new T(u);  
  } catch (Exception newTException) {
    try {
      u.close();
    } catch (Exception closeException) {
      // Exceptions in cleanup code are secondary to exceptions in primary code (body of try),
      // as in try-with-resources.
      newTException.addSuppressed(closeException);
    }
    throw newTException;
  }
}

Точно так же вы можете связать три ресурса и т. Д.

Если не считать математики, вы могли бы даже трижды объединить в цепочку, объединив два ресурса за раз, и это было бы ассоциативно, то есть вы получите один и тот же объект при успехе (потому что конструкторы ассоциативны), и те же исключения, если произошел сбой в любом из конструкторов. Предполагая, что вы добавили S в вышеуказанную цепочку (поэтому вы начинаете с V и заканчиваете S , поочередно применяя U , T и S ), вы получите то же самое, если сначала соедините S и T , а затем U , соответствует (ST) U , или если вы сначала приковали цепью T и U , тоS, соответствующий S (ТУ) . Однако было бы яснее просто записать явную тройную цепочку в одной фабричной функции.

Нильс фон Барт
источник
Правильно ли я понимаю, что все еще нужно использовать try-with-resource, как в try (BufferedWriter writer = <BufferedWriter, FileWriter>createChainedResource(file)) { /* work with writer */ }?
ErikE
@ErikE Да, вам все равно нужно будет использовать try-with-resources, но вам нужно будет использовать только одну функцию для связанного ресурса: функция factory инкапсулирует цепочку. Я добавил пример использования; Спасибо!
Нильс фон Барт
2

Так как ваши ресурсы вложены, ваши предложения try-with также должны быть:

try (FileWriter fw=new FileWriter(file)) {
    try (BufferedWriter bw=new BufferedWriter(fw)) {
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
} catch (IOException ex) {
    // handle ex
}
яд
источник
5
Это очень похоже на мой второй пример. Если исключение не происходит, FileWriter closeбудет вызываться дважды.
Natix,
0

Я бы сказал, не используйте ARM и продолжайте с Closeable. Используйте метод, как,

public void close(Closeable... closeables) {
    for (Closeable closeable: closeables) {
       try {
           closeable.close();
         } catch (IOException e) {
           // you can't much for this
          }
    }

}

Также вы должны рассмотреть возможность вызова close, так BufferedWriterкак это не просто делегирование close FileWriter, но и некоторая очистка, например flushBuffer.

sakthisundar
источник
0

Мое решение состоит в том, чтобы сделать рефакторинг «метод извлечения», как показано ниже:

static AutoCloseable writeFileWriter(FileWriter fw, String txt) throws IOException{
    final BufferedWriter bw  = new BufferedWriter(fw);
    bw.write(txt);
    return new AutoCloseable(){

        @Override
        public void close() throws IOException {
            bw.flush();
        }

    };
}

printToFile может быть написано либо

static void printToFile(String text, File file) {
    try (FileWriter fw = new FileWriter(file)) {
        AutoCloseable w = writeFileWriter(fw, text);
        w.close();
    } catch (Exception ex) {
        // handle ex
    }
}

или

static void printToFile(String text, File file) {
    try (FileWriter fw = new FileWriter(file);
        AutoCloseable w = writeFileWriter(fw, text)){

    } catch (Exception ex) {
        // handle ex
    }
}

Для дизайнеров классов lib я предложу им расширить AutoClosable интерфейс дополнительным методом для подавления закрытия. В этом случае мы можем вручную контролировать поведение закрытия.

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

ОБНОВИТЬ

Первоначально код выше требует, @SuppressWarningтак как BufferedWriterвнутри функции требуетсяclose() .

Как следует из комментария, если flush()он вызывается до закрытия программы записи, мы должны делать это перед любыми return(неявными или явными) инструкциями внутри блока try. Я думаю, что в настоящее время нет способа гарантировать, что вызывающий абонент делает это, так что это должно быть задокументировано writeFileWriter.

ОБНОВЛЕНИЕ СНОВА

Приведенное выше обновление делает @SuppressWarningненужным, поскольку требует, чтобы функция возвращала ресурс вызывающей стороне, поэтому само закрытие не обязательно. К сожалению, это возвращает нас к началу ситуации: предупреждение теперь перенесено обратно на сторону звонящего.

Таким образом , чтобы правильно решить эту проблему, нам нужно настроить , AutoClosableчто всякий раз , когда он закрывается, подчеркивание BufferedWriterдолжно быть под flush()ред. На самом деле, это показывает нам еще один способ обойти предупреждение, поскольку BufferWriterни в коем случае не закрывается.

Земля Двигатель
источник
Предупреждение имеет значение: можем ли мы быть уверены, что на bwсамом деле эти данные будут записаны? После этого он буферизуется, поэтому ему иногда нужно только записать на диск (когда буфер заполнен и / или включен, flush()и close()методы). Я думаю, что flush()метод должен быть вызван. Но тогда нет необходимости использовать буферизованный писатель, так как мы записываем его сразу в одном пакете. И если вы не исправите код, вы можете получить данные, записанные в файл в неправильном порядке, или вообще не записанные в файл.
Петр Янечек
Если flush()требуется вызвать, это должно произойти всякий раз, когда вызывающий абонент решил закрыть FileWriter. Так что это должно произойти printToFileпрямо перед любымreturn s в блоке try. Это не должно быть частью, writeFileWriterи, следовательно, предупреждение касается не чего-то внутри этой функции, а вызывающей стороны этой функции. Если у нас есть аннотация, @LiftWarningToCaller("wanrningXXX")то поможет этот случай и тому подобное.
Земля Двигатель