Правильна ідіома для керування кількома ланцюговими ресурсами в блоці "пробних ресурсів"?


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конструктора 's, його 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, цей метод закриття не повинен бути ідентичним. Іншими словами, виклик цього методу закриття не один раз може мати деякий видимий побічний ефект, на відміну від 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" ніколи не закривається


Отже, до якого підходу скористатися? Або я пропустив якусь іншу ідіому, яка є правильною ?


4
Звичайно, якщо основний конструктор FileWriter кидає виняток, він навіть не відкривається і все гаразд. Що стосується першого прикладу, це те, що відбувається, якщо FileWriter буде створений, але конструктор BufferedWriter кидає виняток.
Натікс

6
Варто зазначити, що BufferedWriter не кине виняток. Чи є приклад, який ви можете придумати, де це питання не є чимось академічним.
Пітер Лорі

10
@PeterLawrey Так, ви праві, що конструктор BufferedWriter в цьому сценарії, швидше за все, не викине виключення, але, як я вже зазначив, це питання стосується будь-яких ресурсів стилю декораторів. Але наприклад public BufferedWriter(Writer out, int sz)можна кинути IllegalArgumentException. Також я можу розширити BufferedWriter класом, який би викинув щось із його конструктора або створив будь-яку власну обгортку, яка мені потрібна.
Натікс

5
BufferedWriterКонструктор може легко кинути виняток. OutOfMemoryErrorце, мабуть, найпоширеніший, оскільки він виділяє неабиякий фрагмент пам'яті для буфера (хоча це може означати, що ви хочете перезапустити весь процес). / Ви повинні flushсвій контекстуальний, BufferedWriterякщо ви не близько і хочете зберегти зміст ( як правило , тільки випадку , не виняток). FileWriterпідбирає те, що відбувається з кодуванням файлів «за замовчуванням» - краще бути явним.
Том Хотін - тайклін

10
@Natix Я б хотів, щоб усі питання в ТА були так само досліджені і зрозумілі, як це. Я хотів би, щоб я міг проголосувати за це 100 разів.
Geek

Відповіді:


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);
}

Ми все ще перебуваємо в неявному блоку, нарешті (зараз з повторним 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 кращим вибором. Однак випробування з ресурсом все ще є недостатнім виправленням, і вам слід дотримуватися ідіоми Execute Around , яка повинна мати чіткіший синтаксис із закриттями в Java SE 8.


4
У версії 3, як ви знаєте, що bw не вимагає називати його закриття? І навіть якщо ви можете бути впевнені, чи це не так, чи це теж "неясні знання", як ви згадуєте для версії 1?
TimK

3
" Причини розробки програмного забезпечення роблять 2 кращим вибором " Чи можете ви пояснити це твердження більш докладно?
Данкан Джонс

8
Чи можете ви навести приклад "Виконання ідіоми навколо",
Маркус

2
Чи можете ви пояснити, що "чіткіший синтаксис із закриттями в Java SE 8"?
petertc

1
Приклад «Виконати Around ідіому» тут: stackoverflow.com/a/342016/258772
MRTS

20

Перший стиль - це той, який запропонував Oracle . BufferedWriterне викидає перевірені винятки, тому якщо викинуто якесь виняток, програма не очікує відновлення з неї, завдяки чому ресурс відновлює в основному moot.

Переважно тому, що це може статися в потоці, при цьому потік відмирає, але програма все ще триває - скажімо, стався тимчасовий відключення пам’яті, який був недостатньо довгим, щоб серйозно погіршити решту програми. Хоча це досить кутовий випадок, і якщо це трапляється досить часто, щоб змусити витік ресурсів проблемою, спроба з ресурсами є найменшою з ваших проблем.


2
Це також рекомендований підхід у 3-му виданні «Ефективна Java».
шмосель

5

Варіант 4

Змініть свої ресурси, щоб вони були закритими, а не автозакритими, якщо можете. Той факт, що конструктори можуть бути ланцюжком, означає, що невідомо закрити ресурс двічі. (Це було правдою і до ARM.) Детальніше про це нижче.

Варіант 5

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

Варіант 6

Не використовуйте ARM, а ваші нарешті закриваються () дзвінки, намагаючись впіймати себе.

Чому я не думаю, що ця проблема є унікальною для ARM

У всіх цих прикладах, нарешті закриття () дзвінків повинно бути в блоці спіймання. Залишив для читабельності.

Недоцільно, тому що 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 - це просто синтаксичний цукор, ми не завжди можемо використовувати його для заміни, нарешті, блоків. Так само, як ми не завжди можемо використовувати цикл для кожного, щоб зробити щось, що можливо за допомогою ітераторів.


5
Якщо і ваші, tryі finallyблоки кидають винятки, ця конструкція втратить перший (і, можливо, більш корисний).
rxg

3

Для узгодження з попередніми коментарями: найпростіше (2) використовувати Closeableресурси та оголошувати їх у порядку, зазначеному в пункті про використання ресурсів. Якщо у вас є тільки AutoCloseable, ви можете їх обернути в інший (вкладений) клас, який просто перевіряє, що closeвикликається лише один раз (Фасадний шаблон), наприклад, маючи 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 (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) . Однак було б зрозуміліше просто виписати явний триразовий ланцюг в одній заводській функції.


Я правильно констатую, що ще потрібно використовувати пробний ресурс, як у try (BufferedWriter writer = <BufferedWriter, FileWriter>createChainedResource(file)) { /* work with writer */ }?
ЕрікЕ

@ErikE Так, вам все-таки потрібно використовувати пробні ресурси, але вам потрібно буде використовувати лише одну функцію для ланцюгового ресурсу: заводська функція інкапсулює ланцюжок. Я додав приклад використання; Дякую!
Нілс фон Барт

2

Оскільки ваші ресурси вкладені, вами також слід:

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
          }
    }

}

Крім того, ви повинні розглянути можливість закриття закриття, BufferedWriterоскільки це не просто делегування близького до FileWriter, але це також робить деяке очищення, як flushBuffer.


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(неявними або явними) заявами всередині блоку спробу. В даний час немає способу забезпечити того, хто телефонує, що це робить, я думаю, тому це потрібно документувати writeFileWriter.

ОНОВЛЕННЯ ПРОТИ

Вищеописане оновлення робить @SuppressWarningнепотрібним, оскільки вимагає функції повернення ресурсу абоненту, тому його не потрібно закривати. На жаль, це поверне нас до початку ситуації: попередження перенесено назад на сторону, що телефонує.

Отже, щоб правильно вирішити цю проблему, нам потрібна спеціальна відповідь, AutoClosableщо коли вона закриється, підкреслення BufferedWriterмає бути flush()змінено. Насправді, це показує нам інший спосіб обійти попередження, оскільки BufferWriterніколи не закривається.


Попередження має своє значення: чи можемо ми бути впевнені в тому, що bwфактично буде виписано дані? Це є буферним Afterall, тому вона має тільки писати на диск іноді (коли буфер заповнений , і / або на , flush()і close()методах). Я здогадуюсь, flush()метод повинен бути названий. Але тоді не потрібно використовувати захищений письменник, у будь-якому випадку, коли ми пишемо це безпосередньо в одній партії. І якщо ви не виправите код, ви можете отримати дані, записані у файл у неправильному порядку, або взагалі не записані у файл.
Петро Янечек

Якщо flush()потрібно зателефонувати, це має відбуватися кожного разу, перш ніж абонент вирішив закрити FileWriter. Отже, це має відбутися printToFileпрямо перед будь-яким returns у блоці спробу. Це не повинно бути частиною writeFileWriterі, таким чином, попередження стосується не нічого, що знаходиться всередині цієї функції, а абонента цієї функції. Якщо у нас є анотація, @LiftWarningToCaller("wanrningXXX")це допоможе у цій справі тощо.
Земляний двигун
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.