Skip to content

A lightweight, dependency-free utility library, laser-focused on improving the experience of working with Java's java.util.concurrent futures.

License

Notifications You must be signed in to change notification settings

futures4j/futures4j

futures4j - Enhancements for Java's Futures

Build Status codecov Contributor Covenant Javadoc License Changelog Maven Central

Feedback and high-quality pull requests are highly welcome!

  1. What is it?
  2. Java Compatibility
  3. Usage
  4. Building from sources
  5. License

What is it?

futures4j is a lightweight, dependency-free utility library designed to enhance and extend the functionality of Java's java.util.concurrent futures. It simplifies working with futures, providing convenient utilities to improve readability and efficiency when handling asynchronous operations.

Java Compatibility

  • futures4j 1.1.x requires Java 11 or newer.
  • futures4j 1.0.x requires Java 17 or newer.

Usage

Binaries

Latest release binaries are available on Maven Central, see https://central.sonatype.com/artifact/io.github.futures4j/futures4j

Add futures4j as a dependency in your pom.xml:

<project>

  <!-- ... -->

  <dependencies>
    <dependency>
      <groupId>io.github.futures4j</groupId>
      <artifactId>futures4j</artifactId>
      <version>[LATEST_VERSION]</version>
    </dependency>
  </dependencies>
</project>

Replace [LATEST_VERSION] with the latest release version.

The latest snapshot binaries are available via the mvn-snapshots-repo Git branch. You need to add this repository configuration to your Maven settings.xml:

<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd">
  <profiles>
    <profile>
      <id>futures4j-snapshot</id>
      <repositories>
        <repository>
          <id>futures4j-snapshot</id>
          <name>futures4j Snapshot Repository</name>
          <url>https://raw.githubusercontent.com/futures4j/futures4j/mvn-snapshots-repo</url>
          <releases><enabled>false</enabled></releases>
          <snapshots><enabled>true</enabled></snapshots>
        </repository>
      </repositories>
    </profile>
  </profiles>
  <activeProfiles>
    <activeProfile>futures4j-snapshot</activeProfile>
  </activeProfiles>
</settings>

The ExtendedFuture class

The io.github.futures4j.ExtendedFuture πŸ“˜ serves as a drop-in replacement and enhanced version of Java's java.util.concurrent.CompletableFuture class.

It offers several improvements:

  1. Supports Thread Interruption on Cancellation

    Unlike CompletableFuture, ExtendedFuture supports task thread interruption when you cancel the future with cancel(true).

    var myFuture = ExtendedFuture.supplyAsync(...);
    
    myFuture.cancel(true); // Will attempt to interrupt the async task
    
    var myFutureNonInterruptible = myFuture.asNonInterruptible();
    myFutureNonInterruptible.cancel(true); // Behaves like CompletableFuture.cancel(true), which ignores the boolean value

    This functionality addresses issues discussed in:

  2. Allows Dependent Stages to Cancel Preceding Stages

    With ExtendedFuture, you can enable cancellation of preceding stages by dependent stages using asCancellableByDependents(true).

    var myFuture = ExtendedFuture.supplyAsync(...);
    
    var myCancellableChain = myFuture
        .thenApply(...)   // Step 1
        .asCancellableByDependents(true) // allows subsequent stages cancel the previous stage
        .thenRun(...)     // Step 2
        .thenApply(...)   // Step 3
        .thenAccept(...); // Step 4
    
    if (shouldCancel) {
      // Cancels steps 1 to 4
      myCancellableChain.cancel(true);
    }

    This even works for nested chains:

    var myFuture = ExtendedFuture.supplyAsync(...);
    
    var myCancellableChain = myFuture
        .thenApply(...)   // Step 1
        .asCancellableByDependents(true) // allows subsequent stages cancel the previous stage
        .thenRun(...)     // Step 2
        .thenApply(...)   // Step 3
        .thenCompose(x -> {
          var innerFuture = ExtendedFuture
              .runAsync(...)
              .asCancellableByDependents(true);
          return innerFuture;
        })
        .thenAccept(...); // Step 4
    
    if (shouldCancel) {
      // Cancels steps 1 to 4 and the innerFuture
      myCancellableChain.cancel(true);
    }

    This addresses issues discussed in:

  3. Allows Running Tasks That Throw Checked Exceptions

    With ExtendedFuture, you can write code that throws checked exceptions without wrapping them in UncheckedIOException or similar.

    I.e. instead of

    CompletableFuture.runAsync(() -> {
      try (var writer = new FileWriter("output.txt")) {
        writer.write("Hello, World!");
      } catch (IOException ex) {
        throw new UncheckedIOException(ex);
      }
    });

    You can now do:

    ExtendedFuture.runAsync(() -> {
      try (var writer = new FileWriter("output.txt")) {
        writer.write("Hello, World!");
      }
    });
  4. Provides Read-Only Views of Futures

    You can create read-only views of futures via asReadOnly(ReadOnlyMode):

    ExtendedFuture<?> myFuture = ExtendedFuture.supplyAsync(...);
    
    var readOnly1 = myFuture.asReadOnly(ReadOnlyMode.THROW_ON_MUTATION); // returns a delegating future that throws an UnsupportedException when a modification attempt is made
    var readOnly2 = myFuture.asReadOnly(ReadOnlyMode.IGNORE_MUTATION); // returns a delegating future that silently ignore modification attempts
  5. Backports of Future Methods from Newer Java Versions to Java 11

    ExtendedFuture makes Future methods available in Java 11 that were only introduced in later Java versions, providing equivalent functionality.

    Backported from Java 12:

    Backported from Java 19:

  6. Allows Defining a Default Executor

    You can specify a default executor for a future and its subsequent stages using ExtendedFuture.createWithDefaultExecutor(Executor) or ExtendedFuture.withDefaultExecutor(Executor):

    // Use myDefaultExecutor for all subsequent stages by default
    var myFuture = ExtendedFuture.createWithDefaultExecutor(myDefaultExecutor);
    
    // Use myDefaultExecutor2 for all subsequent stages by default
    var myFuture2 = myFuture.withDefaultExecutor(myDefaultExecutor2);
    
    // Use myDefaultExecutor3 for execution and subsequent stages by default
    var myOtherFuture = ExtendedFuture.runAsyncWithDefaultExecutor(runnable, myDefaultExecutor3);
  7. Additional Convenience Methods

    ExtendedFuture<String> myFuture = ...;
    
    // Returns true if the future completed normally
    boolean success = myFuture.isSuccess();
    
    // Returns the completion state (CANCELLED, SUCCESS, FAILED, INCOMPLETE)
    CompletionState state = myFuture.getCompletionState();
    
    // Returns the completed value or the fallbackValue; never throws an exception
    T result = myFuture.getNowOrFallback(fallbackValue);
    
    // Returns the completed value or the fallbackValue after waiting; never throws an exception
    T result = myFuture.getOrFallback(10, TimeUnit.SECONDS, fallbackValue);
    
    // Complete a future with the same value/exception of another future
    CompletableFuture<String> otherFuture = ...;
    myFuture.completeWith(otherFuture);

The Futures utility class

The io.github.futures4j.Futures πŸ“˜ class provides a set of utility methods for managing and combining Java Future and CompletableFuture instances. It simplifies handling asynchronous computations by offering functionality to cancel, combine, and retrieve results from multiple futures.

  1. Cancelling Futures

    • Cancel multiple futures:

      Futures.combine(future1, future2, future3).toList().cancelAll(true);
    • Propagate cancellations from one CompletableFuture to others:

      var cancellableFuture = new CompletableFuture<>();
      
      var dependentFuture1 = new CompletableFuture<>();
      var dependentFuture2 = new CompletableFuture<>();
      
      // Propagate cancellation from cancellableFuture to dependent futures
      Futures.forwardCancellation(cancellableFuture, dependentFuture1, dependentFuture2);
      
      // Cancelling the primary future also cancels the dependent futures
      cancellableFuture.cancel(true);
  2. Combining Futures

    • Combine futures into lists or sets:

      CompletableFuture<String> future1 = CompletableFuture.completedFuture("Result 1");
      CompletableFuture<String> future2 = CompletableFuture.completedFuture("Result 2");
      CompletableFuture<String> future3 = CompletableFuture.completedFuture("Result 2");
      
      Futures.combine(List.of(future1, future2, future3))
        .toList()
        .thenAccept(results -> System.out.println(results)); // Outputs: [Result 1, Result 2, Result 2]
      
      Futures.combine(List.of(future1, future2, future3))
        .toSet()
        .thenAccept(results -> System.out.println(results)); // Outputs: [Result 1, Result 2]
    • Combine and flatten futures:

      CompletableFuture<List<String>> future1 = CompletableFuture.completedFuture(List.of("A", "B"));
      CompletableFuture<List<String>> future2 = CompletableFuture.completedFuture(List.of("C", "D"));
      
      Futures.combineFlattened(List.of(future1, future2))
        .toList()
        .thenAccept(flattenedResults -> System.out.println(flattenedResults)); // Outputs: [A, B, C, D]
  3. Retrieving Results

    • Get future result with timeout and fallback:

      Future<String> future = ...;
      
      // Returns "Fallback Result" if timeout occurs, future is cancelled or completes exceptionally
      String result = Futures.getOrFallback(future, 1, TimeUnit.SECONDS, "Fallback Result");

The CompletionState enum

The io.github.futures4j.CompletionState πŸ“˜ enum allows switching on a future's completion state:

Future<String> future = ...;

switch (CompletionState.of(future)) {
  case INCOMPLETE -> /* ... */;
  case SUCCESS    -> /* ... */;
  case CANCELLED  -> /* ... */;
  case FAILED     -> /* ... */;
}

This is similar to the Future.State enum introduced in Java 19, which allows switching like so:

Future<String> future = ...;

switch (future.state()) {
  case RUNNING   -> /* ... */;
  case SUCCESS   -> /* ... */;
  case CANCELLED -> /* ... */;
  case FAILED    -> /* ... */;
}

Building from Sources

To ensure reproducible builds, this Maven project inherits from the vegardit-maven-parent project, which declares fixed versions and sensible default settings for all official Maven plugins.

The project also uses the maven-toolchains-plugin, which decouples the JDK used to execute Maven and its plugins from the target JDK used for compilation and unit testing. This ensures full binary compatibility of the compiled artifacts with the runtime library of the required target JDK.

To build the project, follow these steps:

  1. Install Java 11 AND Java 17 JDKs

    Download and install the JDKs from e.g.:

  2. Configure Maven Toolchains

    In your user home directory, create the file .m2/toolchains.xml with the following content:

    <?xml version="1.0" encoding="UTF8"?>
    <toolchains xmlns="http://maven.apache.org/TOOLCHAINS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/TOOLCHAINS/1.1.0 https://maven.apache.org/xsd/toolchains-1.1.0.xsd">
      <toolchain>
        <type>jdk</type>
        <provides>
          <version>11</version>
          <vendor>default</vendor>
        </provides>
        <configuration>
          <jdkHome>[PATH_TO_YOUR_JDK_11]</jdkHome>
        </configuration>
      </toolchain>
      <toolchain>
        <type>jdk</type>
        <provides>
          <version>17</version>
          <vendor>default</vendor>
        </provides>
        <configuration>
          <jdkHome>[PATH_TO_YOUR_JDK_17]</jdkHome>
        </configuration>
      </toolchain>
    </toolchains>

    Replace [PATH_TO_YOUR_JDK_11] and [PATH_TO_YOUR_JDK_17] with the path to your respective JDK installations.

  3. Clone the Repository

    git clone https://github.com/futures4j/futures4j.git
  4. Build the Project

    Run mvnw clean verify in the project root directory. This will execute compilation, unit testing, integration testing, and packaging of all artifacts.

License

If not otherwise specified (see below), files in this repository fall under the Eclipse Public License 2.0.

Individual files contain the following tag instead of the full license text:

SPDX-License-Identifier: EPL-2.0

This enables machine processing of license information based on the SPDX License Identifiers that are available here: https://spdx.org/licenses/.

An exception is made for:

  1. files in readable text which contain their own license information, or
  2. files in a directory containing a separate LICENSE.txt file, or
  3. files where an accompanying file exists in the same directory with a .LICENSE.txt suffix added to the base-name of the original file. For example foobar.js is may be accompanied by a foobar.LICENSE.txt license file.

About

A lightweight, dependency-free utility library, laser-focused on improving the experience of working with Java's java.util.concurrent futures.

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •