try-with-resources 블록에서 여러 체인 리소스를 관리하기위한 올바른 관용구?


168

Java 7 try-with-resources 구문 (ARM 블록 ( Automatic Resource Management ) 이라고도 함 )은 하나의 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 메소드와 달리이 close 메소드는 dem 등성이 될 필요는 없습니다. 다시 말해,이 close 메소드를 두 번 이상 호출하면 Closeable.close와는 달리 한 번 이상 호출해도 아무런 영향을 미치지 않는 부작용이있을 수 있습니다. 그러나이 인터페이스의 구현자는 가까운 메소드를 dem 등원으로 만들 것을 강력히 권장합니다.

삼)

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의 생성자가 예외를 throw하면 열리지 않으며 모든 것이 정상입니다. 첫 번째 예는 FileWriter가 만들어 지지만 BufferedWriter의 생성자가 예외를 발생시키는 경우에 발생합니다.
Natix

6
BufferedWriter는 예외를 throw하지 않습니다. 이 질문이 순수한 학문이 아닌 곳을 생각할 수있는 예가 있습니까?
Peter Lawrey

10
@PeterLawrey 네,이 시나리오에서 BufferedWriter의 생성자가 예외를 throw하지 않을 것입니다. 그러나 내가 지적 했듯이이 질문은 모든 데코레이터 스타일 리소스와 관련이 있습니다. 그러나 예 public BufferedWriter(Writer out, int sz)를 들어을 던질 수 있습니다 IllegalArgumentException. 또한 생성자에서 무언가를 던지거나 필요한 사용자 정의 래퍼를 만드는 클래스로 BufferedWriter를 확장 할 수 있습니다.
Natix

5
BufferedWriter생성자는 쉽게 예외를 던질 수 있습니다. OutOfMemoryError아마도 버퍼에 적절한 메모리 청크를 할당 할 때 가장 일반적 일 것입니다 (전체 프로세스를 다시 시작하려고 할 수도 있음). / 당신은 필요 flush당신 BufferedWriter이 닫지 말고 내용 (일반적으로 유지하려는 경우 에만 이 아닌 예외 케이스). FileWriter"기본"파일 인코딩으로 발생하는 모든 것을 선택합니다. 명시 적으로 작성하는 것이 좋습니다.
Tom Hawtin-tackline

10
@Natix 나는 SO의 모든 질문이 이것처럼 연구되고 명확 해지기를 바랍니다. 나는이 투표를 100 번 할 수 있으면 좋겠다.
Geek

답변:


75

다음은 대안에 대한 것입니다.

1)

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

저에게, 15 년 전 전통적인 C ++에서 Java로 오는 가장 좋은 점은 프로그램을 신뢰할 수 있다는 것입니다. 일이 잘못되고 잘못되는 경우에도, 나머지 코드는 최상의 행동과 장미 냄새가 나기를 원합니다. 실제로 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리소스 릴리스를 막을 수 없습니다.

삼)

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는 여전히 부적절한 수정이며 Java SE 8에서 클로저를 사용하여 더 명확한 구문을 가져야하는 Execute Around idiom 을 사용해야합니다.


4
버전 3에서 bw가 close를 요구하지 않는다는 것을 어떻게 알 수 있습니까? 그리고 만약 당신이 그것을 확신 할 수 있더라도, 당신이 버전 1에 대해 언급 한 것처럼 "분명한 지식"도 아닌가?
TimK

3
" 소프트웨어 개발 이유는 2를 더 나은 선택으로 만듭니다. "이 내용을보다 자세히 설명 할 수 있습니까?
Duncan Jones

8
"폐쇄 된 관용구 관용구"의 예를들 수 있습니까
Markus

2
"Java SE 8에서 클로저가있는 더 명확한 구문"이 무엇인지 설명 할 수 있습니까?
petertc

1
"관용구 주변 실행"의 예는 다음과 같습니다. stackoverflow.com/a/342016/258772
mrts

20

첫 번째 스타일은 Oracle이 제안한 스타일입니다 . BufferedWriter확인 된 예외를 throw하지 않으므로 예외가 발생하면 프로그램에서 예외를 복구하지 않아도되므로 리소스를 거의 무질서하게 만듭니다.

스레드가 죽으면 서 스레드에서 발생할 수 있기 때문에 프로그램은 계속 진행됩니다. 예를 들어, 프로그램의 나머지 부분을 심각하게 손상시킬만큼 오래 걸리지 않은 일시적인 메모리 중단이있었습니다. 그러나 다소 모호한 경우이며 리소스 누수를 문제로 만들기에 충분할 경우 리소스 사용 시도가 가장 적은 문제입니다.


2
또한 효과적인 Java의 3 판에서 권장되는 접근 방법입니다.
shmosel

5

옵션 4

가능하면 자동 닫기가 아닌 닫기 가능한 리소스로 변경하십시오. 생성자가 연결될 수 있다는 사실은 리소스를 두 번 닫는 것을 들어 본 적이 없다는 것을 의미합니다. (이것은 ARM 이전에도 마찬가지였습니다.) 이에 대한 자세한 내용은 아래를 참조하십시오.

옵션 5

close ()가 두 번 호출되지 않도록 ARM과 코드를 매우 신중하게 사용하지 마십시오!

옵션 6

ARM을 사용하지 말고 try / catch 자체에서 최종 close () 호출을 수행하십시오.

이 문제가 ARM에 고유하지 않다고 생각하는 이유

이 모든 예제에서 finally close () 호출은 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();
}

BufferedWriter를 생성 할 때 예외가 발생하면 fw가 닫히지 않아 좋지 않습니다. (다시 일어날 수는 없지만 가상의 예에서) :

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

나는 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 블록을 대체하기 위해 항상 그것을 사용할 수는 없습니다. 반복자 (iterator)로 가능한 것을하기 위해 항상 for-each 루프를 사용할 수있는 것처럼


5
귀하 tryfinally블록 모두 예외를 던지면이 구조는 첫 번째 (그리고 잠재적으로 더 유용한) 구조를 잃습니다.
rxg 2016 년

3

이전 주석과 일치하려면 : 가장 간단한 방법은 (2)Closeable 자원 을 사용 하고 try-with-resources 절에서 순서대로 선언하는 것입니다. 만 가지고 있다면 AutoCloseable, close한 번만 호출 되는 것을 확인하는 다른 (중첩) 클래스로 랩핑 할 수 있습니다 (예 : Facade Pattern) private bool isClosed;. 실제로 오라클조차도 (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 , TS 를 차례로 적용 하면) 먼저 ST를 체인 한 다음 U , 에 해당하는 (ST) U , 또는 먼저 체인 경우 TU 다음,S (TU)에 해당합니다 . 그러나 단일 팩토리 함수에서 명시 적 3 중 체인을 작성하는 것이 더 명확합니다.


에서와 같이 여전히 try-with-resource를 사용해야한다는 것을 올바르게 수집하고 try (BufferedWriter writer = <BufferedWriter, FileWriter>createChainedResource(file)) { /* work with writer */ }있습니까?
ErikE

@ErikE 예, 리소스를 사용하여 시도해야하지만 체인 된 리소스에는 단일 함수 만 사용해야합니다. 팩토리 함수 는 체인을 캡슐화 합니다. 사용 예를 추가했습니다. 감사!
닐스 폰 바스

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

}

또한 BufferedWriterclose를 위임하는 것이 아니라 close FileWriter와 같은 정리 작업을 수행 하므로 close 호출을 고려해야 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경우 try 블록 내의 (암시 적 또는 명시 적) 명령문 보다 먼저 호출해야합니다 . 현재 발신자가이 작업을 수행 할 수있는 방법이 없으므로 문서화해야합니다 writeFileWriter.

다시 업데이트

위의 업데이트는 @SuppressWarning호출자에게 리소스를 반환하는 함수가 필요하기 때문에 불필요하므로 자체적으로 닫을 필요는 없습니다. 불행하게도, 이것은 우리를 상황의 시작으로 되돌려 놓는다 : 경고는 이제 호출자 측으로 다시 이동된다.

따라서이 문제를 올바르게 해결하려면 AutoClosable닫을 때마다 밑줄 BufferedWriter이 표시되도록 사용자 정의 해야합니다 flush(). 실제로 이것은 경고를 우회하는 다른 방법을 보여줍니다 BufferWriter.


경고의 의미는 다음과 같습니다. 여기서 bw실제로 데이터를 기록 할 수 있습니까? 이것은 된다 에만 (버퍼 풀 및 / 또는 경우에 때때로 디스크에 기록해야하므로 Afterall는 버퍼링 flush()close()방법). flush()메소드를 호출해야한다고 생각합니다 . 그러나 한 번에 한 묶음으로 즉시 작성하는 경우에는 버퍼링 된 작성자를 사용할 필요가 없습니다. 코드를 수정하지 않으면 파일에 잘못된 순서로 기록 된 데이터가 있거나 파일에 전혀 기록되지 않을 수 있습니다.
Petr Janeček

flush()전화가 필요한 경우 발신자가 전화를 끊기로 결정하기 전에는 항상 전화해야합니다 FileWriter. 따라서 이것은 try 블록의 s printToFile바로 전에 발생해야합니다 return. 이것은 그 일부 writeFileWriter가 아니므로 그 함수 내부에있는 것이 아니라 그 함수의 호출자에 대한 경고입니다. 주석이 있으면 @LiftWarningToCaller("wanrningXXX")이 경우와 비슷한 경우에 도움이됩니다.
어스 엔진
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.