RAII на Java... это утилизация ресурсов всегда так безобразно?

Я просто играл с API-интерфейсом Java файловой системы и пришел со следующей функцией, используемой для копирования двоичных файлов. Исходный источник пришел из Интернета, но я добавил предложения try/catch/finally, чтобы убедиться, что, если что-то не так, буферизованные потоки будут закрыты (и, следовательно, освобождены мои операционные ресурсы), прежде чем выйти из функции.

Я урезал функцию, чтобы показать шаблон:

public static void copyFile(FileOutputStream oDStream, FileInputStream oSStream) throw etc...
{
   BufferedInputStream oSBuffer = new BufferedInputStream(oSStream, 4096);
   BufferedOutputStream oDBuffer = new BufferedOutputStream(oDStream, 4096);

   try
   { 
      try
      { 
         int c;

         while((c = oSBuffer.read()) != -1)  // could throw a IOException
         {
            oDBuffer.write(c);  // could throw a IOException
         }
      }
      finally
      {
         oDBuffer.close(); // could throw a IOException
      }
   }
   finally
   {
      oSBuffer.close(); // could throw a IOException
   }
}

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

Я знаю, что С# имеет шаблон Dispose, который обрабатывал бы это с помощью ключевого слова using.

Я даже лучше знаю, что код на С++ был бы чем-то вроде (с использованием Java-подобного API):

void copyFile(FileOutputStream & oDStream, FileInputStream & oSStream)
{
   BufferedInputStream oSBuffer(oSStream, 4096);
   BufferedOutputStream oDBuffer(oDStream, 4096);

   int c;

   while((c = oSBuffer.read()) != -1)  // could throw a IOException
   {
      oDBuffer.write(c);  // could throw a IOException
   }

   // I don't care about resources, as RAII handle them for me
}

Мне что-то не хватает, или мне действительно нужно производить уродливый и раздутый код на Java, чтобы обрабатывать исключения в методе close() буферизованного потока?

(Пожалуйста, скажи мне, что я где-то не прав))

РЕДАКТИРОВАТЬ: Это я, или когда обновляю эту страницу, я видел, как вопрос, и все ответы уменьшились на один пункт за пару минут? Является ли кто-то слишком много себя переживающим, но при этом оставаясь анонимным?

EDIT 2: McDowell предложила очень интересную ссылку, которую я чувствовал, что должен был упомянуть здесь: http://illegalargumentexception.blogspot.com/2008/10/java-how-not-to-make-mess-of-stream.html

EDIT 3: Следуя ссылке McDowell, я применил предложение Java 7 шаблона, похожего на шаблон С#: http://tech.puredanger.com/java7/#resourceblock, Моя проблема явно описана. По-видимому, даже с Java 7 do проблемы остаются.

Ответ 1

Шаблон try/finally - это правильный способ обработки потоков в большинстве случаев для Java 6 и ниже.

Некоторые выступают за молчание закрытия потоков. Будьте осторожны, делая это по следующим причинам: Java: как не создавать беспорядок обработки потока


Java 7 вводит try-with-resources:

/** transcodes text file from one encoding to another */
public static void transcode(File source, Charset srcEncoding,
                             File target, Charset tgtEncoding)
                                                             throws IOException {
    try (InputStream in = new FileInputStream(source);
         Reader reader = new InputStreamReader(in, srcEncoding);
         OutputStream out = new FileOutputStream(target);
         Writer writer = new OutputStreamWriter(out, tgtEncoding)) {
        char[] buffer = new char[1024];
        int r;
        while ((r = reader.read(buffer)) != -1) {
            writer.write(buffer, 0, r);
        }
    }
}

AutoCloseable будут автоматически закрыты:

public class Foo {
  public static void main(String[] args) {
    class CloseTest implements AutoCloseable {
      public void close() {
        System.out.println("Close");
      }
    }
    try (CloseTest closeable = new CloseTest()) {}
  }
}

Ответ 2

Есть проблемы, но код, который вы нашли, лежащий в Интернете, действительно низок.

Закрытие буферных потоков закрывает поток под ним. Вы действительно не хотите этого делать. Все, что вы хотите сделать, это очистить выходной поток. Также нет смысла указывать базовые потоки для файлов. Производительность отстой, потому что вы копируете один байт за раз (на самом деле, если вы используете java.io, использование может использовать transferTo/transferFrom, что немного быстрее). Пока мы об этом, имена переменных сосать. Итак:

public static void copy(
    InputStream in, OutputStream out
) throw IOException {
    byte[] buff = new byte[8192];
    for (;;) {
        int len = in.read(buff);
        if (len == -1) {
            break;
        }
        out.write(buff, 0, len);
    }
}

Если вы обнаружите, что используете try-finally много, тогда вы можете разделить его на "идем вокруг" идиомы.

На мой взгляд: Java должен иметь возможность закрытия ресурсов в конце области. Я предлагаю добавить private в качестве унарного оператора постфикса, чтобы закрыть его в конце закрывающего блока.

Ответ 3

К сожалению, этот тип кода имеет тенденцию немного раздуваться в Java.

Кстати, если один из вызовов oSBuffer.read или oDBuffer.write генерирует исключение, то вы, вероятно, захотите, чтобы это исключение пронизывало иерархию вызовов.

Наличие неохраняемого вызова для закрытия() внутри предложения finally приведет к тому, что исходное исключение будет заменено на другое, созданное вызовом close(). Другими словами, неудачный метод close() может скрыть исходное исключение, созданное read() или write(). Итак, я думаю, вы хотите игнорировать исключения, которые были выбраны close(), если и только если другие методы не выбрасывались.

Я обычно решаю это, включив явный закрытый вызов внутри внутренней попытки:

  try {
    while (...) {
      read...
      write...
    }
    oSBuffer.close(); // exception NOT ignored here
    oDBuffer.close(); // exception NOT ignored here
  } finally {
    silentClose(oSBuffer); // exception ignored here
    silentClose(oDBuffer); // exception ignored here
  }
  static void silentClose(Closeable c)  {
    try {
      c.close();
    } catch (IOException ie) {
      // Ignored; caller must have this intention
    }
  }

Наконец, для производительности код, вероятно, должен работать с буферами (несколько байтов на чтение/запись). Невозможно вернуть это по номерам, но меньшее количество вызовов должно быть более эффективным, чем добавление буферизованных потоков вверх.

Ответ 4

Да, это то, как работает java. Существует инверсия управления - пользователь объекта должен знать, как очистить объект, а не сам объект, очищающий сам по себе. Это, к сожалению, приводит к большому количеству кода очистки, разбросанного по всему вашему Java-коду.

С# имеет ключевое слово "using", чтобы автоматически вызывать Dispose, когда объект выходит из области видимости. Java не имеет такой вещи.

Ответ 5

Для обычных задач ввода-вывода, таких как копирование файла, код, такой как показанный выше, изобретает колесо. К сожалению, JDK не предоставляет никаких утилит более высокого уровня, но apache commons-io делает.

Например, FileUtils содержит различные утилиты для работы с файлами и каталогами (включая копирование). С другой стороны, если вам действительно нужно использовать поддержку IO в JDK, IOUtils содержит набор методов closeQuietly(), которые закрыть читателей, писателей, потоки и т.д., не исключая исключений.