Using Java 8 Streams to Find Open Ports

In the last time we saw how to use lambdas as predicates, specifically with the Java 8 Collection#removeIf method in order to remove elements from a map based on the predicate. In this article, we will use a predicate to filter elements from a stream, and combine it with a generator to find the first open port in a specific range.

Use Case

The use case is a (micro)service-based environment where each new service binds to the first open port it finds in a specific port range (49152 to 65535, as defined by IANA). We want to choose a port at random in this range and bind to that port if it is open. If it is not open, we will repeat the process until we find an open port or until we reach a pre-defined number of attempts.

Original Pre-Java 8 Code

Integer findOpenPort(int maxAttempts) {
    Integer assignedPort = null;
    int count = 0;
    while (count < maxAttempts) {
        int port = randomPort();
        if (portChecker.isAvailable(port)) {
            assignedPort = port;
            break;
        }
        count++;
    }
    return assignedPort;
}

Observations

  1. Returns an Integer indicating the port or null if none is found.
  2. Utilizes mutable variables assignedPort and count to store state and track attempts.
  3. Executes a loop while the maximum attempts haven’t been exceeded.
  4. Uses a port checker to determine availability and breaks if a port is found.
  5. Uses a ternary expression for return value.

Refactoring with Java 8 Stream API

To refactor using Java 8 streams:

  1. Generate a sequence of random ports.
  2. Filter for open ports and return the first open port encountered.
  3. Return an empty value that indicates no open port was found, represented as an OptionalInt.

Here is the refactored code:

OptionalInt findOpenPortStream(int maxAttempts) {
    return IntStream.generate(this::randomPort)
                    .filter(portChecker::isAvailable)
                    .limit(maxAttempts)
                    .findFirst();
}

Explanation

  • The method returns OptionalInt, providing a clear indication of presence or absence of a value.
  • Generates an infinite sequence of random integers and limits it to maxAttempts.
  • The filter method uses method reference to the isAvailable method.
  • findFirst effectively “short-circuits,” terminating once a valid port is found.

Advantages of the Functional Approach

  • No mutable variables involved, simplifying the logic.
  • Declarative nature improves readability.
  • Easier to compose with generic methods.

Example of a Re-Usable Method

You could implement a method to find the first random number above two billion within 10 attempts:

OptionalInt result = findFirst(randomSupplier, n -> n > 2000000000, 10).orElse(42);

Conclusion

The functional approach allows composability and keeps the code concise, focusing on the business logic rather than the mechanism. This style can significantly improve readability and maintainability.