JDK-8343768 : Inflater and Deflater should implement AutoCloseable
  • Type: CSR
  • Component: core-libs
  • Sub-Component: java.util.jar
  • Priority: P4
  • Status: Closed
  • Resolution: Approved
  • Fix Versions: 25
  • Submitted: 2024-11-07
  • Updated: 2025-03-04
  • Resolved: 2025-01-14
Related Reports
CSR :  
Description
Summary
-------

`java.util.zip.Deflater` and `java.util.zip.Inflater` classes will be enhanced to implement `java.lang.AutoCloseable`.

Problem
-------

The `Deflater` and `Inflater` classes have APIs which allow applications to compress and decompress data, respectively. The implementation in these classes uses the ZLIB native library. Compression and decompression operations using this library require the use of native resources whose lifetime is controlled by the corresponding Deflater/Inflater instance. The application must release these resources by calling the `end()` method. 

Conventionally, Java APIs use the `close()` method for releasing resources. The use of the `end()` method for this purpose is unusual and is easy for developers to miss. The `Deflater` and `Inflater` classes have no `close()` methods. They thus do not implement the `AutoCloseable` interface, and in turn they cannot be used with the try-with-resources statement. This situation is inconvenient and error-prone.

Making `Inflater` and `Deflater` implement the `AutoCloseable` will make it more prominent that the instances of these classes hold on to resources that are expected to be cleaned up when the instances are no longer need by the application. Furthermore, various static analysis tools, including Integrated Development Environments (IDEs) have the ability to warn application developers if instances of `AutoCloseable` aren't closed. Thus, having `Inflater` and `Deflater` implement `AutoCloseable` will increase the chances of the application developer closing these instances at the right places.

Solution
--------

The `Inflater` and `Deflater` classes will be enhanced to implement the `AutoCloseable` interface. The newly introduced `close()` method will be specified to invoke the current existing `end()` method that is already specified to clean up resources. The `close()` method will be allowed to be invoked more than once and each time it will invoke the `end()` method. The specification of the `end()` method will be updated to state that it allows multiple invocations to it but the subsequent invocations after the first one will act as no-op.

The API documentation of `Inflater` and `Deflater` will also be improved to make it clear to subclasses that any resources they allocate must be freed by overriding the `end()` method.

While working on these changes, it was noticed that the current implementation of both the `Inflater` and `Deflater` throw a (unspecified) `NullPointerException` if certain methods are invoked on the instances after the Inflater/Deflater has been closed. These methods will be updated to specify that they will throw an `java.lang.IllegalStateException` if they are invoked on closed instances. The implementation of these methods will thus be updated to throw an `IllegalStateException` instead of a `NullPointerException`, for such cases.

Specification
-------------

     diff --git a/src/java.base/share/classes/java/util/zip/Deflater.java b/src/java.base/share/classes/java/util/zip/Deflater.java
      * This class deflates sequences of bytes into ZLIB compressed data format.
    - * The input byte sequence is provided in either byte array or byte buffer,
    + * The input byte sequence is provided in either a byte array or a {@link ByteBuffer},
      * via one of the {@code setInput()} methods. The output byte sequence is
    - * written to the output byte array or byte buffer passed to the
    + * written to the output byte array or {@code ByteBuffer} passed to the
      * {@code deflate()} methods.
      * <p>
    - * The following code fragment demonstrates a trivial compression
    - * and decompression of a string using {@code Deflater} and
    - * {@code Inflater}.
    - * {@snippet id="compdecomp" lang="java" class="Snippets" region="DeflaterInflaterExample"}
    + * To release the resources used by a {@code Deflater}, an application must close it
    + * by invoking its {@link #end()} or {@link #close()} method.
      *
      * @apiNote
    - * To release resources used by this {@code Deflater}, the {@link #end()} method
    - * should be called explicitly. Subclasses are responsible for the cleanup of resources
    - * acquired by the subclass. Subclasses that override {@link #finalize()} in order
    - * to perform cleanup should be modified to use alternative cleanup mechanisms such
    - * as {@link java.lang.ref.Cleaner} and remove the overriding {@code finalize} method.
    + * This class implements {@link AutoCloseable} to facilitate its usage with
    + * {@code try}-with-resources statement. The {@linkplain Deflater#close() close() method} simply
    + * calls {@code end()}.
    + *
    + * <p>
    + * The following code fragment demonstrates a trivial compression
    + * and decompression of a string using {@code Deflater} and {@code Inflater}.
    + * {@snippet id="compdecomp" lang="java" class="Snippets" region="DeflaterInflaterExample"}
      *
      * @see         Inflater
      * @author      David Connelly
      * @since 1.1
      */
     
    -public class Deflater {
    +public class Deflater implements AutoCloseable {
     
         private final DeflaterZStreamRef zsRef;
         private ByteBuffer input = ZipUtils.defaultBuf;
    @@ -269,6 +270,7 @@ public void setInput(ByteBuffer input) {
          * @param dictionary the dictionary data bytes
          * @param off the start offset of the data
          * @param len the length of the data
    +     * @throws IllegalStateException if the Deflater is closed
          * @see Inflater#inflate
          * @see Inflater#getAdler()
          */
    @@ -287,6 +289,7 @@ public void setDictionary(byte[] dictionary, int off, int len) {
          * in order to get the Adler-32 value of the dictionary required for
          * decompression.
          * @param dictionary the dictionary data bytes
    +     * @throws IllegalStateException if the Deflater is closed
          * @see Inflater#inflate
          * @see Inflater#getAdler()
          */
    @@ -305,6 +308,7 @@ public void setDictionary(byte[] dictionary) {
          * return, its position will equal its limit.
          *
          * @param dictionary the dictionary data bytes
    +     * @throws IllegalStateException if the Deflater is closed
          * @see Inflater#inflate
          * @see Inflater#getAdler()
          *
    @@ -437,6 +441,7 @@ public boolean finished() {
          * @param len the maximum number of bytes of compressed data
          * @return the actual number of bytes of compressed data written to the
          *         output buffer
    +     * @throws IllegalStateException if the Deflater is closed
          */
         public int deflate(byte[] output, int off, int len) {
             return deflate(output, off, len, NO_FLUSH);
    @@ -456,6 +461,7 @@ public int deflate(byte[] output, int off, int len) {
          * @param output the buffer for the compressed data
          * @return the actual number of bytes of compressed data written to the
          *         output buffer
    +     * @throws IllegalStateException if the Deflater is closed
          */
         public int deflate(byte[] output) {
             return deflate(output, 0, output.length, NO_FLUSH);
    @@ -476,6 +482,7 @@ public int deflate(byte[] output) {
          * @return the actual number of bytes of compressed data written to the
          *         output buffer
          * @throws ReadOnlyBufferException if the given output buffer is read-only
    +     * @throws IllegalStateException if the Deflater is closed
          * @since 11
          */
         public int deflate(ByteBuffer output) {
    @@ -531,6 +538,7 @@ public int deflate(ByteBuffer output) {
          *         the output buffer
          *
          * @throws IllegalArgumentException if the flush mode is invalid
    +     * @throws IllegalStateException if the Deflater is closed
          * @since 1.7
          */
         public int deflate(byte[] output, int off, int len, int flush) {
    @@ -657,6 +665,7 @@ public int deflate(byte[] output, int off, int len, int flush) {
          *
          * @throws IllegalArgumentException if the flush mode is invalid
          * @throws ReadOnlyBufferException if the given output buffer is read-only
    +     * @throws IllegalStateException if the Deflater is closed
          * @since 11
          */
         public int deflate(ByteBuffer output, int flush) {
    @@ -783,6 +792,7 @@ public int deflate(ByteBuffer output, int flush) {
     
         /**
          * {@return the ADLER-32 value of the uncompressed data}
    +     * @throws IllegalStateException if the Deflater is closed
          */
         public int getAdler() {
             synchronized (zsRef) {
    @@ -802,6 +812,7 @@ public int getAdler() {
          * @deprecated Use {@link #getBytesRead()} instead
          *
          * @return the total number of uncompressed bytes input so far
    +     * @throws IllegalStateException if the Deflater is closed
          */
         @Deprecated(since = "23")
         public int getTotalIn() {
    @@ -812,6 +823,7 @@ public int getTotalIn() {
          * Returns the total number of uncompressed bytes input so far.
          *
          * @return the total (non-negative) number of uncompressed bytes input so far
    +     * @throws IllegalStateException if the Deflater is closed
          * @since 1.5
          */
         public long getBytesRead() {
    @@ -832,6 +844,7 @@ public long getBytesRead() {
          * @deprecated Use {@link #getBytesWritten()} instead
          *
          * @return the total number of compressed bytes output so far
    +     * @throws IllegalStateException if the Deflater is closed
          */
         @Deprecated(since = "23")
         public int getTotalOut() {
    @@ -842,6 +855,7 @@ public int getTotalOut() {
          * Returns the total number of compressed bytes output so far.
          *
          * @return the total (non-negative) number of compressed bytes output so far
    +     * @throws IllegalStateException if the Deflater is closed
          * @since 1.5
          */
         public long getBytesWritten() {
    @@ -854,6 +868,7 @@ public long getBytesWritten() {
         /**
          * Resets deflater so that a new set of input data can be processed.
          * Keeps current compression level and strategy settings.
    +     * @throws IllegalStateException if the Deflater is closed
          */
         public void reset() {
             synchronized (zsRef) {
    @@ -868,23 +883,45 @@ public void reset() {
         }
     
         /**
    -     * Closes the compressor and discards any unprocessed input.
    +     * Closes and releases the resources held by this {@code Deflater}
    +     * and discards any unprocessed input.
    +     * <p>
    +     * If the {@code Deflater} is already closed then invoking this method has no effect.
    +     *
    +     * @implSpec Subclasses should override this method to clean up the resources
    +     * acquired by the subclass.
          *
    -     * This method should be called when the compressor is no longer
    -     * being used. Once this method is called, the behavior of the
    -     * Deflater object is undefined.
    +     * @see #close()
          */
         public void end() {
     
    +    /**
    +     * Closes and releases the resources held by this {@code Deflater}
    +     * and discards any unprocessed input.
    +     *
    +     * @implSpec This method calls the {@link #end()} method.
    +     * @since 25
    +     */
    +    @Override
    +    public void close() {

    diff --git a/src/java.base/share/classes/java/util/zip/Inflater.java b/src/java.base/share/classes/java/util/zip/Inflater.java
      * This class inflates sequences of ZLIB compressed bytes. The input byte
    - * sequence is provided in either byte array or byte buffer, via one of the
    + * sequence is provided in either a byte array or a {@link ByteBuffer}, via one of the
      * {@code setInput()} methods. The output byte sequence is written to the
    - * output byte array or byte buffer passed to the {@code inflate()} methods.
    + * output byte array or {@code ByteBuffer} passed to the {@code inflate()} methods.
      * <p>
    - * The following code fragment demonstrates a trivial compression
    - * and decompression of a string using {@code Deflater} and
    - * {@code Inflater}.
    - * {@snippet id="compdecomp" lang="java" class="Snippets" region="DeflaterInflaterExample"}
    + * To release the resources used by an {@code Inflater}, an application must close it
    + * by invoking its {@link #end()} or {@link #close()} method.
      *
      * @apiNote
    - * To release resources used by this {@code Inflater}, the {@link #end()} method
    - * should be called explicitly. Subclasses are responsible for the cleanup of resources
    - * acquired by the subclass. Subclasses that override {@link #finalize()} in order
    - * to perform cleanup should be modified to use alternative cleanup mechanisms such
    - * as {@link java.lang.ref.Cleaner} and remove the overriding {@code finalize} method.
    + * This class implements {@link AutoCloseable} to facilitate its usage with
    + * {@code try}-with-resources statement. The {@linkplain Inflater#close() close() method} simply
    + * calls {@code end()}.
    + *
    + * <p>
    + * The following code fragment demonstrates a trivial compression
    + * and decompression of a string using {@code Deflater} and {@code Inflater}.
    + * {@snippet id="compdecomp" lang="java" class="Snippets" region="DeflaterInflaterExample"}
      *
      * @see         Deflater
      * @author      David Connelly
    @@ -71,7 +72,7 @@
      *
      */
     
    -public class Inflater {
    +public class Inflater implements AutoCloseable {
     
         private final InflaterZStreamRef zsRef;
         private ByteBuffer input = ZipUtils.defaultBuf;
    @@ -192,6 +193,7 @@ public void setInput(ByteBuffer input) {
          * @param dictionary the dictionary data bytes
          * @param off the start offset of the data
          * @param len the length of the data
    +     * @throws IllegalStateException if the Inflater is closed
          * @see Inflater#needsDictionary
          * @see Inflater#getAdler
          */
    @@ -210,6 +212,7 @@ public void setDictionary(byte[] dictionary, int off, int len) {
          * indicating that a preset dictionary is required. The method getAdler()
          * can be used to get the Adler-32 value of the dictionary needed.
          * @param dictionary the dictionary data bytes
    +     * @throws IllegalStateException if the Inflater is closed
          * @see Inflater#needsDictionary
          * @see Inflater#getAdler
          */
    @@ -227,6 +230,7 @@ public void setDictionary(byte[] dictionary) {
          * return, its position will equal its limit.
          *
          * @param dictionary the dictionary data bytes
    +     * @throws IllegalStateException if the Inflater is closed
          * @see Inflater#needsDictionary
          * @see Inflater#getAdler
          * @since 11
    @@ -331,6 +335,7 @@ public boolean finished() {
          * @param len the maximum number of uncompressed bytes
          * @return the actual number of uncompressed bytes
          * @throws DataFormatException if the compressed data format is invalid
    +     * @throws IllegalStateException if the Inflater is closed
          * @see Inflater#needsInput
          * @see Inflater#needsDictionary
          */
    @@ -437,6 +442,7 @@ public int inflate(byte[] output, int off, int len)
          * @param output the buffer for the uncompressed data
          * @return the actual number of uncompressed bytes
          * @throws DataFormatException if the compressed data format is invalid
    +     * @throws IllegalStateException if the Inflater is closed
          * @see Inflater#needsInput
          * @see Inflater#needsDictionary
          */
    @@ -474,6 +480,7 @@ public int inflate(byte[] output) throws DataFormatException {
          * @return the actual number of uncompressed bytes
          * @throws DataFormatException if the compressed data format is invalid
          * @throws ReadOnlyBufferException if the given output buffer is read-only
    +     * @throws IllegalStateException if the Inflater is closed
          * @see Inflater#needsInput
          * @see Inflater#needsDictionary
          * @since 11
    @@ -600,6 +607,7 @@ public int inflate(ByteBuffer output) throws DataFormatException {
     
         /**
          * {@return the ADLER-32 value of the uncompressed data}
    +     * @throws IllegalStateException if the Inflater is closed
          */
         public int getAdler() {
             synchronized (zsRef) {
    @@ -619,6 +627,7 @@ public int getAdler() {
          * @deprecated Use {@link #getBytesRead()} instead
          *
          * @return the total number of compressed bytes input so far
    +     * @throws IllegalStateException if the Inflater is closed
          */
         @Deprecated(since = "23")
         public int getTotalIn() {
    @@ -629,6 +638,7 @@ public int getTotalIn() {
          * Returns the total number of compressed bytes input so far.
          *
          * @return the total (non-negative) number of compressed bytes input so far
    +     * @throws IllegalStateException if the Inflater is closed
          * @since 1.5
          */
         public long getBytesRead() {
    @@ -649,6 +659,7 @@ public long getBytesRead() {
          * @deprecated Use {@link #getBytesWritten()} instead
          *
          * @return the total number of uncompressed bytes output so far
    +     * @throws IllegalStateException if the Inflater is closed
          */
         @Deprecated(since = "23")
         public int getTotalOut() {
    @@ -659,6 +670,7 @@ public int getTotalOut() {
          * Returns the total number of uncompressed bytes output so far.
          *
          * @return the total (non-negative) number of uncompressed bytes output so far
    +     * @throws IllegalStateException if the Inflater is closed
          * @since 1.5
          */
         public long getBytesWritten() {
    @@ -670,6 +682,7 @@ public long getBytesWritten() {
     
         /**
          * Resets inflater so that a new set of input data can be processed.
    +     * @throws IllegalStateException if the Inflater is closed
          */
         public void reset() {
             synchronized (zsRef) {
    @@ -684,14 +697,22 @@ public void reset() {
         }
     
         /**
    -     * Closes the decompressor and discards any unprocessed input.
    +     * Closes and releases the resources held by this {@code Inflater}
    +     * and discards any unprocessed input.
    +     * <p>
    +     * If the {@code Inflater} is already closed then invoking this method has no effect.
    +     *
    +     * @implSpec Subclasses should override this method to clean up the resources
    +     * acquired by the subclass.
          *
    -     * This method should be called when the decompressor is no longer
    -     * being used. Once this method is called, the behavior of the
    -     * Inflater object is undefined.
    +     * @see #close()
          */
         public void end() {
     
    +    /**
    +     * Closes and releases the resources held by this {@code Inflater}
    +     * and discards any unprocessed input.
    +     *
    +     * @implSpec This method calls the {@link #end()} method.
    +     * @since 25
    +     */
    +    @Override
    +    public void close() {





Comments
Hello Joe [~darcy], before integrating the PR, there were a couple of suggestions. One was to move the sentence that provided guidance to subclasses, from the class level doc to a "@implSpec" of the "end()" method of the Inflater/Deflater. The other was to remove the "After the {@code Deflater} has been closed, subsequent calls to several methods of the {@code Deflater} will throw an {@link IllegalStateException}." sentence from the class level doc since the relevant methods on Inflater/Deflater have already been updated (as part of this PR) to specify which ones throw the IllegalStateException when closed. The PR has been updated with these changes and has been approved. I've now updated (only) the specification section of this CSR to match the approved text from the PR.
15-01-2025

Moving to Approved.
14-01-2025

The PR has been reviewed and approved. No further updates are proposed. I'll move this CSR to Finalized now.
13-01-2025

> To get extra notice of this change, I recommend a note to quality-discuss when it is available in a build. I'll follow up with the quality outreach team once this is integrated. Thank you Joe.
09-01-2025

Moving to Provisional, not Approved. To get extra notice of this change, I recommend a note to quality-discuss when it is available in a build.
09-01-2025

I've now moved this CSR to "Proposed" state. We decided not to mark the newly proposed "close()" method on Inflater/Deflater as final. No other semantic changes from the previous iteration.
08-01-2025

Agreed. I am pursuing this further, but I don't plan to target this for 24 anymore.
03-12-2024

The CSR is currently in Draft. That said, given the timeline for JDK 24 and not-quite-concluded discussions, I think it is best if this work target JDK 25 instead.
02-12-2024

Given Alan's note, I'm moving this back to Draft and will work on making the `close()` method non-final. A major part of the changes we did as part of this proposal were done considering `close()` as non-final. It's only after we have settled on the semantics that we considered and decided to mark `close()` `final`. I'll go back to the state where this was non-final and pursue it further. I think some additional guidance/text would perhaps have to added to help subclasses understand which method to override for resource clean up.
27-11-2024

The change is both source and binary incompatible so comes with risk. I realise that corpus analysis has been done but I think it is just too risky to be adding a final close method to non-final APIs that date from JDK 1.1. I think we have to re-visit the original proposal to add a non-final close method, or just drop the proposal completely.
27-11-2024

I've now moved it Finalized.
27-11-2024

The CSR needs to be finalized.
26-11-2024