Correct idiom for managing multiple chained resources in try-with-resources block?
Asked Answered
G

8

190

The Java 7 try-with-resources syntax (also known as ARM block (Automatic Resource Management)) is nice, short and straightforward when using only one AutoCloseable resource. However, I am not sure what is the correct idiom when I need to declare multiple resources that are dependent on each other, for example a FileWriter and a BufferedWriter that wraps it. Of course, this question concerns any case when some AutoCloseable resources are wrapped, not only these two specific classes.

I came up with the three following alternatives:

1)

The naive idiom I have seen is to declare only the top-level wrapper in the ARM-managed variable:

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

This is nice and short, but it is broken. Because the underlying FileWriter is not declared in a variable, it will never be closed directly in the generated finally block. It will be closed only through the close method of the wrapping BufferedWriter. The problem is, that if an exception is thrown from the bw's constructor, its close will not be called and therefore the underlying FileWriter will not be closed.

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

Here, both the underlying and the wrapping resource are declared in the ARM-managed variables, so both of them will certainly be closed, but the underlying fw.close() will be called twice: not only directly, but also through the wrapping bw.close().

This should not be a problem for these two specific classes that both implement Closeable (which is a subtype of AutoCloseable), whose contract states that multiple calls to close are permitted:

Closes this stream and releases any system resources associated with it. If the stream is already closed then invoking this method has no effect.

However, in a general case, I can have resources that implement only AutoCloseable (and not Closeable), which doesn't guarantee that close can be called multiple times:

Note that unlike the close method of java.io.Closeable, this close method is not required to be idempotent. In other words, calling this close method more than once may have some visible side effect, unlike Closeable.close which is required to have no effect if called more than once. However, implementers of this interface are strongly encouraged to make their close methods idempotent.

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

This version should be theoretically correct, because only the fw represents a real resource that needs to be cleaned up. The bw doesn't itself hold any resource, it only delegates to the fw, so it should be sufficient to only close the underlying fw.

On the other hand, the syntax is a bit irregular and also, Eclipse issues a warning, which I believe is a false alarm, but it is still a warning that one has to deal with:

Resource leak: 'bw' is never closed


So, which approach to go for? Or have I missed some other idiom that is the correct one?

Godrich answered 23/9, 2012 at 13:47 Comment(6)
Your first example isn't really demonstrative. If FileWriter throws an exception, then the resource certainly isn't opened and need not to be closed.Unsuccessful
Of course, if the underlying FileWriter's constructor throws an exception, it doesn't get even opened and everything is OK. What the 1st example is about, is what happens if the FileWriter gets created, but the BufferedWriter's constructor throws an exception.Godrich
Its worth noting that BufferedWriter will not throw an Exception. Is there an example you can think of where this question not pure academic.Shunt
@PeterLawrey Yes, your're right that the BufferedWriter's constructor in this scenario most probably will not throw an exception, but as I've pointed out, this question concerns any decorator-style resources. But for example public BufferedWriter(Writer out, int sz) can throw an IllegalArgumentException. Also, I can extend BufferedWriter with a class that would throw something from its constructor or create a whatever custom wrapper that I need.Godrich
The BufferedWriter constructor can easily throw an exception. OutOfMemoryError is probably the most common one as it allocates a fair chunk of memory for the buffer (although may indicate you want to restart the entire process). / You need to flush your BufferedWriter if you don't close and want to keep the contents (generally only the non-exception case). FileWriter picks up whatever happens to be the "default" file encoding - it's better to be explicit.Hackman
@Godrich I wish all question in SO are as well researched and clear as this one is. I wish I could up vote this 100 times.Freestyle
H
82

Here's my take on the alternatives:

1)

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

For me, the best thing coming to Java from traditional C++ 15 years ago was that you could trust your program. Even if things are in the muck and going wrong, which they often do, I want the rest of the code to be on best behaviour and smelling of roses. Indeed, the BufferedWriter might throw an exception here. Running out of memory wouldn't be unusual, for instance. For other decorators, do you know which of the java.io wrapper classes throw a checked exception from their constructors? I don't. Doesn't do code understandability much good if you rely upon that sort of obscure knowledge.

Also there's the "destruction". If there is an error condition, then you probably don't want to be flushing rubbish to a file that needs deleting (code for that not shown). Although, of course, deleting the file is also another interesting operation to do as error handling.

Generally you want finally blocks to be as short and reliable as possible. Adding flushes does not help this goal. For many releases some of the buffering classes in the JDK had a bug where an exception from flush within close caused close on the decorated object not be called. Whilst that has been fixed for some time, expect it from other implementations.

2)

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

We're still flushing in the implicit finally block (now with repeated close - this gets worse as you add more decorators), but the construction is safe and we have two implicit finally blocks so even a failed flush doesn't prevent resource release.

3)

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

There's a bug here. Should be:

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

Some poorly implemented decorators are in fact resource and will need to be closed reliably. Also some streams may need to be closed in a particular way (perhaps they are doing compression and need to write bits to finish off, and can't just flush everything.

Verdict

Although 3 is a technically superior solution, software development reasons make 2 the better choice. However, try-with-resource is still an inadequate fix and you should stick with the Execute Around idiom, which should have a clearer syntax with closures in Java SE 8.

Hackman answered 30/9, 2012 at 21:27 Comment(5)
In version 3, how do you know that bw doesn't require its close to be called? And even if you can be sure if it, isn't that also "obscure knowledge" like you mention for version 1?Throwback
"software development reasons make 2 the better choice" Can you explain this statement in more detail?Offence
Can you give an example of the "Execute Around idiom with closures"Exhilarate
Can you explain what's "clearer syntax with closures in Java SE 8"?Hunchback
Example of "Execute Around idiom" is here: https://mcmap.net/q/56354/-what-is-the-quot-execute-around-quot-idiomAcquire
S
24

The first style is the one suggested by Oracle. BufferedWriter doesn't throw checked exceptions, so if any exception is thrown, the program is not expected to recover from it, making resource recover mostly moot.

Mostly because it could happen in a thread, with the thread dieing but the program still continuing -- say, there was a temporary memory outage that wasn't long enough to seriously impair the rest of the program. It's a rather corner case, though, and if it happens often enough to make resource leak a problem, the try-with-resources is the least of your problems.

Syllabub answered 28/8, 2013 at 19:18 Comment(1)
It's also the recommended approach in the 3rd edition of Effective Java.Exsanguine
L
5

Option 4

Change your resources to be Closeable, not AutoClosable if you can. The fact that the constructors can be chained implies it isn't unheard of to close the resource twice. (This was true before ARM too.) More on this below.

Option 5

Don't use ARM and code very carefully to ensure close() isn't called twice!

Option 6

Don't use ARM and have your finally close() calls in a try/catch themselves.

Why I don't think this problem is unique to ARM

In all these examples, the finally close() calls should be in a catch block. Left out for readability.

No good because fw can be closed twice. (which is fine for FileWriter but not in your hypothetial example):

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

No good because fw not closed if exception on constructing a BufferedWriter. (again, can't happen, but in your hypothetical example):

FileWriter fw = null;
BufferedWriter bw = null;
try {
  fw = new FileWriter(file);
  bw = new BufferedWriter(fw);
  bw.write(text);
} finally {
  if ( bw != null ) bw.close();
}
Letendre answered 23/9, 2012 at 18:1 Comment(0)
S
3

To concur with earlier comments: simplest is (2) to use Closeable resources and declare them in order in the try-with-resources clause. If you only have AutoCloseable, you can wrap them in another (nested) class that just checks that close is only called once (Facade Pattern), e.g. by having private bool isClosed;. In practice even Oracle just (1) chains the constructors and doesn't correctly handle exceptions partway through the chain.

Alternatively, you can manually create a chained resource, using a static factory method; this encapsulates the chain, and handle cleanup if it fails part-way:

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

You can then use it as a single resource in a try-with-resources clause:

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

The complexity comes from handling multiple exceptions; otherwise it's just "close resources that you've acquired so far". A common practice seems to be to first initialize the variable that holds the object that holds the resource to null (here fileWriter), and then include a null check in the cleanup, but that seems unnecessary: if the constructor fails, there's nothing to clean up, so we can just let that exception propagate, which simplifies the code a little.

You could probably do this generically:

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

Similarly, you can chain three resources, etc.

As a mathematical aside, you could even chain three times by chaining two resources at a time, and it would be associative, meaning you would get the same object on success (because the constructors are associative), and same exceptions if there were a failure in any of the constructors. Assuming you added an S to the above chain (so you start with a V and end with an S, by applying U, T, and S in turn), you get the same either if you first chain S and T, then U, corresponding to (ST)U, or if you first chained T and U, then S, corresponding to S(TU). However, it would be clearer to just write out an explicit three-fold chain in a single factory function.

Selfanalysis answered 30/8, 2015 at 23:57 Comment(2)
Am I gathering correctly that one still needs to use try-with-resource, as in try (BufferedWriter writer = <BufferedWriter, FileWriter>createChainedResource(file)) { /* work with writer */ }?Consueloconsuetude
@Consueloconsuetude Yes, you would still need to use try-with-resources, but you would only need to use a single function for the chained resource: the factory function encapsulates the chaining. I've added an example of use; thanks!Selfanalysis
R
2

Since your resources are nested, your try-with clauses should also be:

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
}
Raseta answered 26/9, 2012 at 15:28 Comment(1)
This is pretty much similar to my 2nd example. If no exception occurs, the FileWriter's close will be called twice.Godrich
N
2

I just wanted to build on Jeanne Boyarsky's suggestion of not using ARM but making sure the FileWriter is always closed exactly once. Don't think there are any problems here...

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

I guess since ARM is just syntactic sugar, we can't always use it to replace finally blocks. Just like we can't always use a for-each loop to do something that is possible with iterators.

Newly answered 30/9, 2012 at 21:33 Comment(1)
If both your try and finally blocks throw exceptions, this construct will lose the first (and potentially more useful) one.Ciscaucasia
F
0

I would say don't use ARM and go on with Closeable. Use method like,

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

}

Also you should consider calling close of BufferedWriter as it is not just delegating the close to FileWriter , but it does some cleanup like flushBuffer.

Fredel answered 27/9, 2012 at 10:58 Comment(0)
B
0

My solution is to do a "extract method" refactoring, as following:

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 can be written either

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

or

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

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

For class lib designers, I will suggest them extend the AutoClosable interface with an additional method to suppress the close. In this case we can then manually control the close behavior.

For language designers, the lesson is that adding a new feature could mean adding a lot others. In this Java case, obviously ARM feature will work better with a resource ownership transfer mechanism.

UPDATE

Originally the code above requires @SuppressWarning since the BufferedWriter inside the function requires close().

As suggested by a comment, if flush() to be called before close the writer, we need to do so before any return (implicit or explicit) statements inside the try block. There is currently no way to ensure the caller doing this I think, so this must be documented for writeFileWriter.

UPDATE AGAIN

The above update makes @SuppressWarning unnecessary since it require the function to return the resource to the caller, so itself does not necessary being closed. Unfortunately, this pull us back to the beginning of the situation: the warning is now moved back to the caller side.

So to properly solve this, we need a customised AutoClosable that whenever it closes, the underline BufferedWriter shall be flush()ed. Actually, this shows us another way to bypass the warning, since the BufferWriter is never closed in either way.

Brynhild answered 26/4, 2013 at 6:23 Comment(2)
The warning has its meaning: Can we be sure here that the bw will actually write tha data out? It is buffered afterall, so it only has to write to disk sometimes (when the buffer is full and/or on flush() and close() methods). I guess the flush() method should be called. But then there's no need to use the buffered writer, anyway, when we're writing it out immediatelly in one batch. And if you don't fix the code, you could end up with data written to the file in a wrong order, or even not written to the file at all.Alcantar
If flush() is required to be called, it shall happen whenever before the caller decided to close the FileWriter. So this should happen in printToFile right before any returns in the try block. This shall not be a part of writeFileWriter and thus the warning is not about anything inside that function, but the caller of that function. If we have an annotation @LiftWarningToCaller("wanrningXXX") it will help this case and similar.Brynhild

© 2022 - 2024 — McMap. All rights reserved.