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

Синтаксис Java 7 try-with-resources (также известный как блок ARM (автоматическое управление ресурсами)) хорош, короток и прост при использовании только одного ресурса AutoCloseable. Однако я не уверен, что является правильной идиомой, когда мне нужно объявить несколько ресурсов, которые зависят друг от друга, например, FileWriter и 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 можно вызвать несколько раз:

Обратите внимание, что в отличие от метода close из java.io.Closeable этот метод закрытия не требуется идемпотентным. Другими словами, вызов этого закрытого метода несколько раз может иметь некоторый видимый побочный эффект, в отличие от 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" никогда не закрывается


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

Ответ 1

Вот мои варианты:

1)

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

Для меня лучшее, что приходит на Java с традиционного С++ 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 - это становится все хуже, когда вы добавляете больше декораторов), но конструкция безопасна, и мы должны подразумевать, наконец, блоки, чтобы даже сбой 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.

Ответ 2

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

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

Ответ 3

Вариант 4

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

Вариант 5

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

Вариант 6

Не используйте ARM и вызовы finally() в try/catch сами.

Почему я не думаю, что эта проблема уникальна для 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();
}

Ответ 4

Я просто хотел построить на Jeanne Boyarsky предложение не использовать 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, чтобы сделать что-то, что возможно с помощью итераторов.

Ответ 5

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

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

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 (TU). Однако было бы яснее просто написать явную трехкратную цепочку в одной функции factory.

Ответ 6

Поскольку ваши ресурсы вложены, ваши предложения 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
}

Ответ 7

Я бы сказал, не используйте 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, поскольку он не просто делегирует закрытие FileWriter, но выполняет некоторую очистку, например flushBuffer.

Ответ 8

Мое решение - сделать рефакторинг методом "extract method" следующим образом:

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 будет работать лучше с механизмом передачи прав собственности.

UPDATE

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

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

UPDATE AGAIN

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

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