Proposal: Automatic Resource Management

Reinier Zwitserloot reinier at zwitserloot.com
Thu Apr 2 05:24:42 PDT 2009


I remember quite a bit of discussion on that. Some of the problems no  
one really solved:

  - What if one object ends up implementing multiple interfaces that  
are all AutomaticResource subtypes? Call all of them? Compile-time  
error? What if that situation happens at runtime due to updating a  
library? Class Validation? Close all of them in that case, but compile- 
time error if the compiler detects such a situation? Runtime exception?

  - What if one implements AutomaticResource but has multiple methods?  
Who is responsible for warning? Compiler? What if I construct a class  
file with this 'bug' in it? Its easy even without ASM: Use jruby,  
jython, or just an older version of javac. Piece of pie. The JVM can't  
just up and crash when it encounters such a type, so some sort of  
behaviour must be specified.


You also seem to mistaken about the ARM proposal's 'exception hiding'  
principle. As I understand it, this is relevant ONLY if the guarded  
scope  (=contents of the try block) threw an exception of its own. You  
can't throw 2 exceptions simultaneously in java, so one of them has to  
win out. Normally, in try/finally blocks, whatever the finally block  
does wins, but for ARM, the idea is that the try block's exception  
wins, because often .close() will fail if some method on the resource  
itself also failed, and the problem is probably better described with  
a more useful stack trace on the original.

If a try block ends normally, but during resource cleanup the .close()  
causes an exception, that exception will propagate out, it will NOT be  
ignored!

There is no need to catch exceptions wafting off of  
InputStream.close() because they basically never happen. The thing  
that is strongly preferable is to take the annoyance of having to  
worry about InputStream.close() away from the programmer, which ARM  
essentially does. The problem isn't IOExceptions being thrown by  
close(), the problem is InputStream.close()'s declaration that it  
throws this exception, which forces you to handle it. This isn't the  
only erroneous checked exception in java (new String(somebytes,  
"UTF-8") declares that it can throw the checked  
UnsupportedEncodingException, eventhough it actually cant, because  
UTF-8 is guaranteed by the JVM spec to exist. The appropriate way to  
handle this is to catch the UEncEx, and rethrow it as an  
InternalError("JVM Bug"). But I digress - at any rate,  
InputStream.close() in practice doesn't throw exceptions unless  
there's a good reason for it, whereas the UEncEx from String.<init> is  
*defined* that it MUST NOT throw that exception, which puts it in a  
different ballpark).


Your proposal is vague to me: What, exactly, are e1 and e2? Will one  
of them be non-null if there is an exception? What happens if I leave  
my finally body empty, which is what I imagine most people who are in  
a hurry will do? Just get ignored? That would be very bad.

Having to remember the order of the try-block's parameters because  
these are applied to the finally parameters also strikes me as  
particularly bad design; making mistakes in ordering seems like a  
great way to make a puzzler.


  --Reinier Zwitserloot



On Apr 2, 2009, at 13:59, Peter Levart wrote:

> Hello Coiners!
>
> The debate about ARM proposal has calmed down lately and I was  
> thinking
> about it last few days. Initially I submitted a comment that got  
> lost by
> list processor and was about using a "marker" interface as a  
> supertype of
> sigle-method disposable interfaces like this:
>
>
> package java.lang;
>
> public interface AutomaticResource {}
>
>
> ... and for example, retroffiting java.io.Closeable:
>
>
> package java.io;
>
> public interface Closeable extends AutomaticResource {
>    void close() throws IOException;
> }
>
>
> ... this way even the name of the method would not be "coined".  
> There would
> have to be some rules that compiler will enforce to make sure it can
> uniquely identify the method to be called on resource disposal (for  
> example:
> the static type of the resource should not directly or indirectly  
> implement
> or extend two different single-method interfaces marked with
> AutomaticResource marker superinterface).
>
> But then I thought. Why would this language feature even have to be  
> tied to
> a particular interface. The "foreach" loop did take this approach with
> java.lang.Iterable, but this had nothing to do with exceptions and  
> catching
> them and ignoring them silently. A language construct that hides  
> this often
> cited "bad practice" under the carpet is no good!
>
> For example, this snippet of code, written to the proposed ARM spec.,
> submited by Bob Lee in this thread a month ago, asking Neal Gafter to
> demonstrate how the equivalent BGGA version would look like:
>
>  try (InputStream in = new FileInputStream(src);
>      OutputStream out = new FileOutputStream(dest)) {
>    byte[] buf = new byte[8 * 1024];
>    int n;
>    while ((n = in.read(buf)) >= 0)
>      out.write(buf, 0, n);
>  } catch (IOException e) {
>    showDialog("Copy failed.");
>  }
>
> ... is flawed. This code does not guarantee that in the absence of  
> shown
> exception the file has been copied in it's intirety.
>
> Ignoring exception thrown on an InputStream.close() might be  
> desireable, but
> ignoring exception thrown on OutputStream.close() migh mean that you  
> don't
> mind missing writing some final bytes into the file. Closing  
> OutputStream
> should not be part of automatic resource disposal but part of main  
> code
> block. This brings us to the question how is ARM supposed to be able  
> do
> differentiate InputStream from OutputStream if both implement  
> Closeable?
>
> So I thought how to circumvent these two weeknesses. My take on this  
> is
> something like the following:
>
>
>        try (InputStream in = new FileInputStream("inputFile");
>             OutputStream out = new FileOutputStream("outputFile"))
>        {
>          // try body
>        }
>        catch (IOException e)
>        {
>          // catch body
>        }
>        finally (IOException e1 : in.close();
>                 IOException e2 : out.close())
>        {
>          // finally body
>        }
>
>
> ... would be translated to ...
>
>
>        {
>            IOException e1 = null;
>            IOException e2 = null;
>
>            try
>            {
>                InputStream in = new FileInputStream("inputFile");
>
>                try
>                {
>                    OutputStream out = new  
> FileOutputStream("outputFile");
>
>                    try
>                    {
>                        // try body
>                    }
>                    finally
>                    {
>                        try
>                        {
>                            out.close();
>                        }
>                        catch (IOException $$e)
>                        {
>                            e2 = $$e;
>                        }
>                    }
>                }
>                finally
>                {
>                    try
>                    {
>                        in.close();
>                    }
>                    catch (IOException $$e)
>                    {
>                        e1 = $$e;
>                    }
>                }
>            }
>            catch (IOException e)
>            {
>                // catch body
>            }
>            finally
>            {
>                // finally body
>            }
>        }
>
>
>
> ... no special interfaces are used here. The code to dispose of  
> resources is
> clearly visible (it has to be writen, yes, but that also means that  
> it can
> be read, which is a good thing).
>
> The above example is the "full monty" version. The thing to note is  
> that a
> try (...; ...; ...) construct is to be matched by an optional  
> finally (...;
> ...; ...) construct in that both, if the second is specified, must  
> have an
> equal number of semicolons. Each "statement" in the try construct  
> gets it's
> own slot that must be matched by the coresponding slot in the  
> finally (...;
> ...; ...) construct. Any slot in finally construct can be left empty  
> (like
> any of the 3 slots in the for (;;) statement).
>
> The following minimal version is also possible (in this example no  
> catching
> of exceptions on resource cleanup is attempted):
>
> int i, j;
> ReadWriteLock iLock = ...;
> ReadWriteLock jLock = ...;
>
> // ...
>
> try (jLock.readLock().lock(); iLock.writeLock().lock()) {
>  i += j;
> }
> finally (jLock.readLock().unlock(); iLock.writeLock().unlock())
>
>
> ... gets translated to:
>
>
> {
>    jLock.readLock().lock();
>    try {
>      iLock.writeLock().lock();
>      try {
>        i += j;
>      }
>      finally {
>        iLock.writeLock().unlock();
>      }
>    }
>    finally {
>      jLock.readLock().unlock();
>    }
> }
>
>
> All this will have to be formalized. This is just an idea to keep  
> the debate
> going.
>
> Regards, Peter
>




More information about the coin-dev mailing list