Thành ngữ chính xác để quản lý nhiều tài nguyên chuỗi trong khối tài nguyên thử?


168

Cú pháp thử tài nguyên Java 7 (còn được gọi là khối ARM ( Quản lý tài nguyên tự động )) rất hay, ngắn gọn và đơn giản khi chỉ sử dụng một AutoCloseabletài nguyên. Tuy nhiên, tôi không chắc đâu là thành ngữ chính xác khi tôi cần khai báo nhiều tài nguyên phụ thuộc lẫn nhau, ví dụ a FileWritervà a BufferedWriterbao bọc nó. Tất nhiên, câu hỏi này liên quan đến bất kỳ trường hợp nào khi một số AutoCloseabletài nguyên được gói, không chỉ hai lớp cụ thể này.

Tôi đã đưa ra ba phương án sau:

1)

Thành ngữ ngây thơ mà tôi đã thấy là chỉ khai báo trình bao bọc cấp cao nhất trong biến do ARM quản lý:

static void printToFile1(String text, File file) {
    try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
}

Điều này là tốt đẹp và ngắn, nhưng nó bị hỏng. Bởi vì cơ sở FileWriterkhông được khai báo trong một biến, nó sẽ không bao giờ được đóng trực tiếp trong finallykhối được tạo . Nó sẽ được đóng chỉ thông qua các closephương pháp của gói BufferedWriter. Vấn đề là, nếu một ngoại lệ được ném ra từ hàm bwtạo của nó, thì nó closesẽ không được gọi và do đó phần bên dưới FileWriter sẽ không bị đóng .

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

Ở đây, cả tài nguyên gói và tài nguyên gói được khai báo trong các biến do ARM quản lý, vì vậy cả hai tài nguyên này chắc chắn sẽ bị đóng, nhưng phần bên dưới fw.close() sẽ được gọi hai lần : không chỉ trực tiếp mà còn thông qua gói bw.close().

Đây không phải là một vấn đề đối với hai lớp cụ thể mà cả hai thực hiện Closeable(là một kiểu con của AutoCloseable), có hợp đồng nói rằng nhiều cuộc gọi closeđược phép:

Đóng luồng này và giải phóng bất kỳ tài nguyên hệ thống nào được liên kết với nó. Nếu luồng đã bị đóng thì việc gọi phương thức này không có hiệu lực.

Tuy nhiên, trong trường hợp chung, tôi có thể có các tài nguyên chỉ thực hiện AutoCloseable(chứ không phải Closeable), điều này không đảm bảo closecó thể được gọi nhiều lần:

Lưu ý rằng không giống như phương thức đóng của java.io.Closizable, phương thức đóng này không bắt buộc phải là idempotent. Nói cách khác, gọi phương thức đóng này nhiều lần có thể có một số tác dụng phụ có thể nhìn thấy, không giống như Closizable.close được yêu cầu không có hiệu lực nếu được gọi nhiều lần. Tuy nhiên, những người triển khai giao diện này được khuyến khích mạnh mẽ để làm cho các phương thức gần gũi của họ trở nên bình thường.

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

Phiên bản này phải đúng về mặt lý thuyết, bởi vì chỉ có fwđại diện cho một tài nguyên thực sự cần được làm sạch. Bản bwthân nó không nắm giữ bất kỳ tài nguyên nào, nó chỉ ủy thác cho fw, vì vậy nó chỉ đủ để đóng cơ sở fw.

Mặt khác, cú pháp hơi bất thường và đồng thời, Eclipse đưa ra cảnh báo, mà tôi tin là cảnh báo sai, nhưng nó vẫn là một cảnh báo mà người ta phải xử lý:

Tài nguyên bị rò rỉ: 'bw' không bao giờ bị đóng


Vì vậy, cách tiếp cận nào để đi? Hoặc tôi đã bỏ lỡ một số thành ngữ khác đó là một thành ngữ chính xác ?


4
Tất nhiên, nếu hàm tạo của FileWriter bên dưới ném ra một ngoại lệ, thì nó thậm chí không được mở và mọi thứ đều ổn. Ví dụ đầu tiên nói về điều gì sẽ xảy ra nếu FileWriter được tạo, nhưng hàm tạo của BufferedWriter đưa ra một ngoại lệ.
Natix

6
Đáng chú ý là BufferedWriter sẽ không ném Ngoại lệ. Có một ví dụ bạn có thể nghĩ về nơi câu hỏi này không thuần túy hàn lâm.
Peter Lawrey

10
@PeterLawrey Vâng, bạn có quyền rằng nhà xây dựng của BufferedWriter trong kịch bản này có lẽ sẽ không đưa ra một ngoại lệ, nhưng như tôi đã chỉ ra, câu hỏi này liên quan đến bất kỳ tài nguyên kiểu trang trí nào. Nhưng ví dụ public BufferedWriter(Writer out, int sz)có thể ném một IllegalArgumentException. Ngoài ra, tôi có thể mở rộng BufferedWriter với một lớp sẽ ném thứ gì đó từ hàm tạo của nó hoặc tạo bất kỳ trình bao bọc tùy chỉnh nào tôi cần.
Natix

5
Các nhà BufferedWriterxây dựng có thể dễ dàng ném một ngoại lệ. OutOfMemoryErrorcó lẽ là phổ biến nhất vì nó phân bổ một đoạn bộ nhớ hợp lý cho bộ đệm (mặc dù có thể cho thấy bạn muốn khởi động lại toàn bộ quá trình). / Bạn cần flushcủa bạn BufferedWriternếu bạn không gần gũi và muốn giữ lại các nội dung (thường chỉ những trường hợp không ngoại lệ). FileWriterchọn bất cứ điều gì xảy ra để mã hóa tệp "mặc định" - tốt hơn là nên rõ ràng.
Tom Hawtin - tackline

10
@Natix Tôi muốn tất cả các câu hỏi trong SO cũng được nghiên cứu và rõ ràng như câu hỏi này. Tôi ước tôi có thể bỏ phiếu này 100 lần.
Geek

Câu trả lời:


75

Đây là của tôi về các lựa chọn thay thế:

1)

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

Đối với tôi, điều tốt nhất đến với Java từ C ++ truyền thống 15 năm trước là bạn có thể tin tưởng vào chương trình của mình. Ngay cả khi mọi thứ ở trong tình trạng khó khăn và trở nên sai lầm, điều họ thường làm, tôi muốn phần còn lại của mã sẽ có hành vi tốt nhất và ngửi thấy mùi hoa hồng. Thật vậy, BufferedWritercó thể ném một ngoại lệ ở đây. Hết bộ nhớ sẽ không bình thường, ví dụ. Đối với các trình trang trí khác, bạn có biết java.iolớp nào trong lớp bao bọc ném ngoại lệ được kiểm tra từ các hàm tạo của chúng không? Tôi không. Không hiểu mã tốt lắm nếu bạn dựa vào loại kiến ​​thức tối nghĩa đó.

Ngoài ra còn có sự "hủy diệt". Nếu có một điều kiện lỗi, thì có lẽ bạn không muốn xả rác vào một tệp cần xóa (mã cho điều đó không được hiển thị). Mặc dù, tất nhiên, xóa tập tin cũng là một thao tác thú vị khác để xử lý lỗi.

Nói chung, bạn muốn finallycác khối càng ngắn và đáng tin cậy càng tốt. Thêm các lần xả không giúp ích gì cho mục tiêu này. Đối với nhiều bản phát hành, một số lớp đệm trong JDK có một lỗi trong đó một ngoại lệ từ flushbên trong closegây ra closetrên đối tượng được trang trí không được gọi. Trong khi điều đó đã được sửa chữa trong một thời gian, hãy chờ đợi nó từ các triển khai khác.

2)

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

Chúng tôi vẫn đang trong khối cuối cùng ẩn (bây giờ lặp đi lặp lại close- điều này trở nên tồi tệ hơn khi bạn thêm nhiều trang trí), nhưng việc xây dựng vẫn an toàn và chúng tôi phải ẩn cuối cùng để chặn ngay cả việc flushkhông giải phóng tài nguyên.

3)

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

Có một lỗi ở đây. Nên là:

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

Một số trang trí được thực hiện kém trong thực tế là tài nguyên và sẽ cần phải được đóng lại đáng tin cậy. Ngoài ra, một số luồng có thể cần phải được đóng theo một cách cụ thể (có lẽ chúng đang thực hiện nén và cần ghi bit để kết thúc và không thể xóa mọi thứ.

Bản án

Mặc dù 3 là một giải pháp vượt trội về mặt kỹ thuật, lý do phát triển phần mềm làm cho 2 trở thành sự lựa chọn tốt hơn. Tuy nhiên, try-with-resource vẫn là một sửa chữa không đầy đủ và bạn nên gắn bó với thành ngữ Execute Xung quanh , cần có một cú pháp rõ ràng hơn với các bao đóng trong Java SE 8.


4
Trong phiên bản 3, làm thế nào để bạn biết rằng bw không yêu cầu phải gọi nó gần? Và ngay cả khi bạn có thể chắc chắn nếu nó, đó cũng không phải là "kiến thức tối nghĩa" như bạn đề cập cho phiên bản 1 sao?
TimK

3
" Lý do phát triển phần mềm làm cho 2 sự lựa chọn tốt hơn " Bạn có thể giải thích chi tiết hơn về tuyên bố này không?
Duncan Jones

8
Bạn có thể đưa ra một ví dụ về "Thực hiện xung quanh thành ngữ với việc đóng cửa"
Markus

2
Bạn có thể giải thích "cú pháp rõ ràng hơn với các bao đóng trong Java SE 8" không?
petertc

1
Ví dụ về "Thực thi xung quanh thành ngữ" ở đây: stackoverflow.com/a/342016/258772
mrts

20

Phong cách đầu tiên là phong cách được đề xuất bởi Oracle . BufferedWriterkhông ném các ngoại lệ được kiểm tra, vì vậy nếu có bất kỳ ngoại lệ nào được ném, chương trình dự kiến ​​sẽ không phục hồi từ nó, làm cho việc khôi phục tài nguyên chủ yếu là mô phỏng.

Chủ yếu là vì nó có thể xảy ra trong một luồng, với luồng chết nhưng chương trình vẫn tiếp tục - giả sử, có một sự cố mất bộ nhớ tạm thời không đủ lâu để làm suy giảm nghiêm trọng phần còn lại của chương trình. Tuy nhiên, đây là một trường hợp khá khó khăn và nếu nó xảy ra thường xuyên đủ để làm rò rỉ tài nguyên, thì tài nguyên thử là vấn đề nhỏ nhất của bạn.


2
Đó cũng là cách tiếp cận được đề xuất trong phiên bản thứ 3 của Java hiệu quả.
shmosel 18/03/18

5

Lựa chọn 4

Thay đổi tài nguyên của bạn thành Có thể đóng, không thể AutoClosable nếu bạn có thể. Thực tế là các nhà xây dựng có thể bị xiềng xích ngụ ý rằng việc đóng tài nguyên hai lần không phải là chưa từng nghe thấy. (Điều này cũng đúng trước cả ARM.) Thêm về điều này dưới đây.

Lựa chọn 5

Đừng sử dụng ARM và mã rất cẩn thận để đảm bảo đóng () không được gọi hai lần!

Lựa chọn 6

Không sử dụng ARM và tự mình thực hiện các cuộc gọi gần () trong một lần thử / bắt.

Tại sao tôi không nghĩ vấn đề này là duy nhất đối với ARM

Trong tất cả các ví dụ này, các lệnh gọi close () cuối cùng sẽ nằm trong một khối bắt. Còn lại để dễ đọc.

Không tốt vì fw có thể bị đóng hai lần. (điều này tốt cho FileWriter nhưng không phải trong ví dụ giả thuyết của bạn):

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

Không tốt vì fw không đóng nếu ngoại lệ khi xây dựng BufferedWriter. (một lần nữa, không thể xảy ra, nhưng trong ví dụ giả thuyết của bạn):

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

Tôi chỉ muốn xây dựng dựa trên đề xuất của Jeanne Boyarsky về việc không sử dụng ARM nhưng đảm bảo FileWriter luôn được đóng chính xác một lần. Đừng nghĩ rằng có bất kỳ vấn đề nào ở đây ...

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

Tôi đoán vì ARM chỉ là đường cú pháp, chúng ta không thể luôn sử dụng nó để thay thế các khối cuối cùng. Giống như chúng ta không thể luôn luôn sử dụng một vòng lặp for-every để làm điều gì đó có thể với các trình vòng lặp.


5
Nếu cả khối tryfinallykhối của bạn ném ngoại lệ, cấu trúc này sẽ mất cái đầu tiên (và có khả năng hữu dụng hơn).
rxg

3

Để đồng tình với các nhận xét trước đó: đơn giản nhất là (2) sử dụng Closeabletài nguyên và khai báo chúng theo thứ tự trong mệnh đề try-with-resource. Nếu bạn chỉ có AutoCloseable, bạn có thể gói chúng trong một lớp khác (lồng nhau) mà chỉ kiểm tra xemclose chỉ được gọi một lần (Mẫu mặt tiền), ví dụ bằng cách có private bool isClosed;. Trong thực tế, ngay cả Oracle chỉ (1) xâu chuỗi các nhà xây dựng và không xử lý chính xác các ngoại lệ giữa các chuỗi.

Ngoài ra, bạn có thể tự tạo tài nguyên chuỗi, sử dụng phương thức tĩnh nhà máy; cái này đóng gói chuỗi và xử lý dọn dẹp nếu nó bị hỏng một phần:

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

Sau đó, bạn có thể sử dụng nó làm tài nguyên đơn lẻ trong mệnh đề try-with-resource:

try (BufferedWriter writer = createBufferedWriterFromFile(file)) {
  // Work with writer.
}

Sự phức tạp đến từ việc xử lý nhiều ngoại lệ; mặt khác, nó chỉ là "tài nguyên gần gũi mà bạn có được cho đến nay". Một thực tế phổ biến dường như là trước tiên khởi tạo biến chứa đối tượng chứa tài nguyênnull (ở đây fileWriter), sau đó bao gồm kiểm tra null trong quá trình dọn dẹp, nhưng điều đó dường như không cần thiết: nếu hàm tạo không thành công, không có gì để dọn sạch, vì vậy chúng ta có thể để ngoại lệ đó lan truyền, đơn giản hóa mã một chút.

Bạn có thể có thể làm điều này một cách khái quát:

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

Tương tự, bạn có thể xâu chuỗi ba tài nguyên, v.v.

Về mặt toán học, bạn thậm chí có thể xâu chuỗi ba lần bằng cách xâu chuỗi hai tài nguyên cùng một lúc và nó sẽ mang tính kết hợp, nghĩa là bạn sẽ có được cùng một đối tượng thành công (vì các nhà xây dựng là liên kết) và ngoại lệ tương tự nếu có lỗi trong bất kỳ nhà xây dựng. Giả sử bạn đã thêm S vào chuỗi trên (vì vậy bạn bắt đầu bằng V và kết thúc bằng S , bằng cách áp dụng lần lượt U , TS ), bạn sẽ nhận được tương tự nếu bạn lần đầu tiên chuỗi ST , sau đó là U , tương ứng với (ST) U hoặc nếu lần đầu tiên bạn xâu chuỗi T U , thìS , tương ứng với S (TU) . Tuy nhiên, sẽ rõ ràng hơn nếu chỉ viết ra một chuỗi ba lần rõ ràng trong một chức năng của nhà máy.


Tôi có thu thập chính xác rằng người ta vẫn cần sử dụng tài nguyên thử với tài nguyên try (BufferedWriter writer = <BufferedWriter, FileWriter>createChainedResource(file)) { /* work with writer */ }không?
ErikE

@ErikE Có, bạn vẫn sẽ cần sử dụng tài nguyên thử, nhưng bạn chỉ cần sử dụng một chức năng duy nhất cho tài nguyên chuỗi: chức năng của nhà máy đóng gói chuỗi. Tôi đã thêm một ví dụ về việc sử dụng; cảm ơn!
Nils von Barth

2

Vì tài nguyên của bạn được lồng nhau, các mệnh đề thử của bạn cũng phải là:

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
Điều này khá giống với ví dụ thứ 2 của tôi. Nếu không có ngoại lệ xảy ra, FileWriter closesẽ được gọi hai lần.
Natix

0

Tôi sẽ nói không sử dụng ARM và tiếp tục với Closizable. Sử dụng phương thức như,

public void close(Closeable... closeables) {
    for (Closeable closeable: closeables) {
       try {
           closeable.close();
         } catch (IOException e) {
           // you can't much for this
          }
    }

}

Ngoài ra, bạn nên xem xét việc gọi gần BufferedWritervì nó không chỉ ủy thác cho gần FileWriter, mà nó còn có một số dọn dẹp như thế nào flushBuffer.


0

Giải pháp của tôi là thực hiện tái cấu trúc "phương pháp trích xuất", như sau:

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 có thể được viết

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

hoặc là

static void printToFile(String text, File file) {
    try (FileWriter fw = new FileWriter(file);
        AutoCloseable w = writeFileWriter(fw, text)){

    } catch (Exception ex) {
        // handle ex
    }
}

Đối với các nhà thiết kế lib lớp, tôi sẽ đề nghị họ mở rộng AutoClosable giao diện với một phương thức bổ sung để triệt tiêu sự đóng. Trong trường hợp này, sau đó chúng ta có thể điều khiển thủ công hành vi đóng.

Đối với các nhà thiết kế ngôn ngữ, bài học là việc thêm một tính năng mới có thể có nghĩa là thêm nhiều tính năng khác. Trong trường hợp Java này, rõ ràng tính năng ARM sẽ hoạt động tốt hơn với cơ chế chuyển quyền sở hữu tài nguyên.

CẬP NHẬT

Ban đầu mã ở trên yêu cầu @SuppressWarningBufferedWriterbên trong hàm yêu cầuclose() .

Theo đề xuất của một nhận xét, nếu flush()được gọi trước khi đóng trình soạn thảo, chúng ta cần thực hiện trước bất kỳ returncâu lệnh (ẩn hoặc tường minh) nào bên trong khối thử. Hiện tại không có cách nào để đảm bảo người gọi làm điều này tôi nghĩ, vì vậy điều này phải được ghi lại writeFileWriter.

CẬP NHẬT

Bản cập nhật trên làm cho @SuppressWarningkhông cần thiết vì nó yêu cầu chức năng trả lại tài nguyên cho người gọi, do đó bản thân nó không cần thiết phải đóng. Thật không may, điều này kéo chúng ta trở lại điểm bắt đầu của tình huống: cảnh báo hiện được chuyển trở lại phía người gọi.

Vì vậy, để giải quyết vấn đề này một cách chính xác, chúng ta cần một tùy chỉnh AutoClosablerằng bất cứ khi nào nó đóng, phần gạch chân BufferedWritersẽ được chỉnh sửa flush(). Trên thực tế, điều này cho chúng ta thấy một cách khác để bỏ qua cảnh báo, vì BufferWriternó không bao giờ bị đóng theo bất kỳ cách nào.


Cảnh báo có ý nghĩa của nó: Chúng ta có thể chắc chắn ở đây rằng bwthực sự sẽ ghi dữ liệu ra không? Nó được đệm sau đó, vì vậy đôi khi nó chỉ phải ghi vào đĩa (khi bộ đệm đầy và / hoặc bật flush()close()phương thức). Tôi đoán flush()phương pháp nên được gọi. Nhưng sau đó, không cần sử dụng trình soạn thảo được đệm, dù sao, khi chúng ta viết nó ngay lập tức trong một đợt. Và nếu bạn không sửa mã, bạn có thể kết thúc với dữ liệu được ghi vào tệp theo thứ tự sai hoặc thậm chí không được ghi vào tệp.
Petr Janeček

Nếu flush()được yêu cầu gọi, nó sẽ xảy ra bất cứ khi nào trước khi người gọi quyết định đóng FileWriter. Vì vậy, điều này sẽ xảy ra printToFilengay trước khi bất kỳ returns trong khối thử. Đây không phải là một phần của writeFileWritervà do đó cảnh báo không phải là bất cứ điều gì bên trong chức năng đó, mà là người gọi của chức năng đó. Nếu chúng ta có một chú thích, @LiftWarningToCaller("wanrningXXX")nó sẽ giúp trường hợp này và tương tự.
Động cơ Trái đất
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.