Understanding try with resource Grammatical Sugars on the Basis of Java

  • 2021-06-29 10:51:22
  • OfStack

background

It is well known that all open system resources, such as streams, files, or Socket connections, need to be manually closed by the developer. Otherwise, as the program continues to run, resource leaks will accumulate into major production accidents.

In the rivers and lakes of Java, there is a kind of work called finally, which can ensure that when you practice martial arts and get into the magic, you can also do some self-rescue operations.In ancient times, code to handle resource closures was usually written in the finally block.However, if you open multiple resources at the same time, you will experience nightmare scenarios:


public class Demo {
  public static void main(String[] args) {
    BufferedInputStream bin = null;
    BufferedOutputStream bout = null;
    try {
      bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
      bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")));
      int b;
      while ((b = bin.read()) != -1) {
        bout.write(b);
      }
    }
    catch (IOException e) {
      e.printStackTrace();
    }
    finally {
      if (bin != null) {
        try {
          bin.close();
        }
        catch (IOException e) {
          throw e;
        }
        finally {
          if (bout != null) {
            try {
              bout.close();
            }
            catch (IOException e) {
              throw e;
            }
          }
        }
      }
    }
  }
}

Oh My God!!!There are even more codes to close resources than business codes!!!That's because we don't just need to shut down BufferedInputStream You also need to make sure that if you turn it off BufferedInputStream An exception occurred. BufferedOutputStream It also needs to be closed properly.So we have to use the nested finally law in finally.You can imagine that the more resources you open, the deeper the nesting in finally will be!!!

The new try-with-resource grammar sugar added in Java 1.7 opens the resource without the coders writing the resource themselves to close the code.Don't worry about me breaking my handwriting anymore!Let's rewrite the example with try-with-resource:


public class TryWithResource {
  public static void main(String[] args) {
    try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
       BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
      int b;
      while ((b = bin.read()) != -1) {
        bout.write(b);
      }
    }
    catch (IOException e) {
      e.printStackTrace();
    }
  }
}

Hands-on practice

In order to be able to work with try-with-resource, resources must be implemented AutoClosable Interface.The implementation class of this interface needs to be overridden close Method:


public class Connection implements AutoCloseable {
  public void sendData() {
    System.out.println(" Sending data ");
  }
  @Override
  public void close() throws Exception {
    System.out.println(" Closing connection ");
  }
}

Call class:


public class TryWithResource {
  public static void main(String[] args) {
    try (Connection conn = new Connection()) {
      conn.sendData();
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
}

Output after running:

Sending data
Closing connection

principle

So how does this work?I believe you smart 1 must have guessed that, in fact, all these are compiler ghosts.Let's decompile the class file from the previous example:


package com.codersm.trywithresource;

public class TryWithResource {
  public TryWithResource() {
  }

  public static void main(String[] args) {
    try {
      Connection conn = new Connection();
      Throwable var2 = null;

      try {
        conn.sendData();
      } catch (Throwable var12) {
        var2 = var12;
        throw var12;
      } finally {
        if (conn != null) {
          if (var2 != null) {
            try {
              conn.close();
            } catch (Throwable var11) {
              var2.addSuppressed(var11);
            }
          } else {
            conn.close();
          }
        }

      }
    } catch (Exception var14) {
      var14.printStackTrace();
    }

  }
}

See, in lines 15-27, the compiler automatically generated the finally block for us and called the resource's close method in it, so the close method in the example will be executed when it runs.

Exception shielding

Carefully, you must find that the code you just decompiled (line 21) is one more addSuppressed than the code you wrote in ancient times.To understand the purpose of this code, we'll modify a little bit the example just now: We've changed the code back to the way exceptions were manually closed in ancient times and thrown exceptions in the sendData and close methods:


public class Connection implements AutoCloseable {
  public void sendData() throws Exception {
    throw new Exception("send data");
  }
  @Override
  public void close() throws Exception {
    throw new MyException("close");
  }
}

Modify the main method:


public class TryWithResource {
  public static void main(String[] args) {
    try {
      test();
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
  private static void test() throws Exception {
    Connection conn = null;
    try {
      conn = new Connection();
      conn.sendData();
    }
    finally {
      if (conn != null) {
        conn.close();
      }
    }
  }
}

After running, we found that:

basic.exception.MyException: close
at basic.exception.Connection.close(Connection.java:10)
at basic.exception.TryWithResource.test(TryWithResource.java:82)
at basic.exception.TryWithResource.main(TryWithResource.java:7)
......

Okay, here's the problem. Since we can only throw one exception at a time, what we see at the top is the last one thrown -- MyException thrown by the close method, while Exception thrown by sendData is ignored.This is called exception shielding.Due to the loss of anomaly information, anomaly screening may make some bugs extremely difficult to discover. Programmers have to work overtime to find bug, such a malignant tumor, can't help it!Fortunately, to solve this problem, starting with Java 1.7, the big guys added the addSuppressed method to the Throwable class, which supports attaching one exception to another to avoid anomaly shielding.So what format will the blocked exception information be output in?Let's run the main method we just wrapped in try-with-resource once more:


java.lang.Exception: send data

 at basic.exception.Connection.sendData(Connection.java:5)
 at basic.exception.TryWithResource.main(TryWithResource.java:14)
 ......
 Suppressed: basic.exception.MyException: close
 at basic.exception.Connection.close(Connection.java:10)
 at basic.exception.TryWithResource.main(TryWithResource.java:15)
 ... 5 more

You can see that there is one more tip from Suppressed in the exception information telling us that the exception actually consists of two exceptions, MyException being an exception by Suppressed.Congratulations!

Matters needing attention

In the process of using try-with-resource, 1 must understand the implementation logic within the resource's close method.Otherwise, it may cause a resource leak.

For example, in Java BIO a large number of decorator modes are used.When the decorator's close method is called, it essentially calls the close method of the stream wrapped inside the decorator.For example:


public class TryWithResource {
  public static void main(String[] args) {
    try (FileInputStream fin = new FileInputStream(new File("input.txt"));
        GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(new File("out.txt")))) {
      byte[] buffer = new byte[4096];
      int read;
      while ((read = fin.read(buffer)) != -1) {
        out.write(buffer, 0, read);
      }
    }
    catch (IOException e) {
      e.printStackTrace();
    }
  }
}

In the above code, we start with FileInputStream Read bytes and write to GZIPOutputStream Medium. GZIPOutputStream Actually FileOutputStream Decorator for.Due to the nature of try-with-resource, the actual compiled code is followed by an finally code block, in which the fin.close() method and the out.close() method are called.Let's see again GZIPOutputStream Class's close method:


public void close() throws IOException {
  if (!closed) {
    finish();
    if (usesDefaultDeflater)
      def.end();
    out.close();
    closed = true;
  }
}

As we can see, the out variable actually represents decorated FileOutputStream Class.Before calling the close method of the out variable, GZIPOutputStream An finish operation has also been made and will continue FileOutputStream Write compression information in, and if an exception occurs out.close() The method is skipped, but this is the lowest resource shutdown method.It is correct to declare the bottom-most resources separately in try-with-resource to ensure that the corresponding close method 1 can be called.In the previous example, we need to declare each one separately FileInputStream as well as FileOutputStream :


public class TryWithResource {
  public static void main(String[] args) {
    try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
       BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
      int b;
      while ((b = bin.read()) != -1) {
        bout.write(b);
      }
    }
    catch (IOException e) {
      e.printStackTrace();
    }
  }
}
0

Since the compiler automatically generates the code for fout.close(), this guarantees that the real stream will be closed.


Related articles: