RAII в Java ... утилизация ресурсов всегда так безобразна? - PullRequest
15 голосов
/ 11 октября 2008

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

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

Я даже лучше знаю, что код C ++ был бы чем-то вроде (используя 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() буферизованного потока?

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

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

РЕДАКТИРОВАТЬ 2: Макдауэлл предложил очень интересную ссылку, я чувствовал, что я должен был упомянуть здесь: http://illegalargumentexception.blogspot.com/2008/10/java-how-not-to-make-mess-of-stream.html

РЕДАКТИРОВАТЬ 3: Следуя ссылке Макдауэлла, я наткнулся на предложение для Java 7 шаблона, аналогичного шаблону C #, используя шаблон: http://tech.puredanger.com/java7/#resourceblock. Моя проблема подробно описана. Видимо, даже с Java 7 do проблемы остаются.

Ответы [ 5 ]

18 голосов
/ 11 октября 2008

Шаблон 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()) {}
  }
}
4 голосов
/ 11 октября 2008

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

Закрытие потоков буфера закрывает поток внизу. Вы действительно не хотите этого делать. Все, что вы хотите сделать, это очистить поток вывода. Также нет смысла указывать базовые потоки для файлов. Производительность отстой, потому что вы копируете один байт за раз (на самом деле, если вы используете 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, вы можете выделить его с помощью идиомы "execute around".

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

3 голосов
/ 11 октября 2008

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

C # имеет ключевое слово «using» для автоматического вызова Dispose, когда объект выходит из области видимости. У Java такого нет.

3 голосов
/ 11 октября 2008

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

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

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

Обычно я решаю эту проблему путем включения явного вызова 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
    }
  }

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

2 голосов
/ 11 октября 2008

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

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

...