Modern Java Constructs

2022 10 02 head

Modern Java is quite a departure from the traditional Java available at the beginning of the millennium.

The most visible change is certainly lambda expressions and stream, enabling a more functional programming approach in Java.

Functional programming is a paradigm that supports developers to think in terms of functions instead of classes. Functions are stateless and without side effects. Avoiding state can lead to better software, especially in high-performance, concurrent applications [1]. Functional programming is also a natural fit for multicore parallel programming. The paradigm is the best way to squeeze more performance out of modern CPUs.

The Java Streams library allows algorithms to be coded declaratively meaning the developer specifies what they want, not how they want it. This improves code quality because all the plumbing code is handled by the library itself. The fluent interface style is just a way to express streams algorithm in one continuous line.

Additional modern constructs reduce boiler code and often eliminate known potential errors.

Here a set of extensions we use daily in our code.

You can find additional examples in the introductory book Java by Comparison [1] and the more advanced Effective Java [2].

Try with Closeable Resources

Automatic resource management was introduced in Java 7 in 2011. Better handling of final variables was added in Java 9 in 2017. Local variable type inference with the var keyword was added to Java 10 in March 2018.

The AutoCloseable interface is the API extension you should use to declare any resource type in your solution.

A nice feature of the solution is the support of suppressed exception hierarchy to access all exceptions potentially thrown during the closing operations.

    try (Writer writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {   (1)
            writer.write(features.toString(4));
    }                                                                               (2)
1 Declares an auto-closeable resource in the block context.
2 The auto-closeable resource is closed by Java runtime when leaving the block either through regular flow or when an exception is thrown. The programmer can never forget to close the resource.

Streams

You shall always use streams to manipulate data collections. Stream operators support similar functionality as SQL dialects.

public String firstname() {
    return card.getProperties(Property.Id.FN).stream()
            .map(Property::getValue)
            .map(o -> o.split(";")[1])
            .findAny()
            .orElse(null);
    }

Sequences of values are also streams. Simply use the Stream.iterate(…​) method. Therefore, a for loop is easily transformed into a stream of operations.

Most imperative operations, such as conditional and loop statements, can be rewritten as stream operators.

Streams are often more legible and compact. The approach moves the developers from imperative programming to a more functional programming style.

Modern Java code has seldom loops and conditional statements.

Functional Programming

Null values are acknowledged as a language design fault in modern software engineering.

You should never return a null value from any method. Either you give back an empty collection or an optional object for single values.

Optional<Organization> findOrganizationById(String identifier) {                            (1)
    return (identifier != null) ?
                Provider.findById(realm.organizations(), identifier) : Optional.empty();
}

Collection<Split> splites{
        this.splits = (splits != null) ? List.copyOf(splits) : Collections.emptyList();     (2)
    }
1 Returns an optional with a value if found or an empty optional. The optional can be processed as a stream instead of using a conditional statement.
2 Returns either a list of items or an empty list. The result can be processed as a stream instead of using a conditional statement.

Streams provide the flatmap operator to elegantly handle optional values in collection processing.

Remember that Optional<T> is almost an implementation of a monad. Time to study the concepts of functional programming and lambda calculus [3].

You are really starting to program using functional programming idioms if you:

  • Pass lambda expressions or single abstract method SAM instances as parameters of some methods.

  • Return lambda expressions or single abstract method SAM instances in some methods.

Just scan your code and find out how often you write such constructs.

Explore the java.util.function package to increase your functional programming skills.

Pattern Matching

Pattern matching is another approach for functional programming. New versions of Java support more related constructs.

The extension of the switch statement into a powerful switch expression was a game changer.

Pattern matching supports:

  • Select a transformation based on the variable type using the instanceof operator. The simplified and type-aware variable declaration is provided.

  • Refine the selection though when clause to differentiate the transformation.

  • Record deconstruction gives access to field variables without having to call accessor methods.

Records and Sealed Types

Sealed types perfectly model a closed inheritance hierarchy. You can control your bounded domain and inhibit any client to change it.

Records are immutable objects. Records are the perfect construct to implement value objects as described in domain-driven design.

Business logic is implemented as additional methods. The fact that the value objects are also immutable makes the business operations both thread-safe and side effect free.

public sealed interface LList<T> permits LList.Nil, LList.ImmutableList {
    boolean isEmpty();
    T first();
    LList<T> rest();
}

record ImmutableList<T>(@NotNull T first, @NotNull LList<T> rest) implements LList<T> {
        @Override
        public boolean isEmpty() {
            return false;
        }

        @Override
        public String toString() {
            return first() + (rest().isEmpty() ? "" : ", " + rest());
        }
}

Factory methods can automatically be created using Lombok annotations.

Modules

Modules were introduced with Java 9, which was released in September 2017.

A module is a group of closely related packages and resources along with a new module descriptor file.

When we create a module, we include a descriptor file that defines several aspects of our new module [4]:

Name

the name of our module.

Dependencies

a list of other modules that this module depends on.

Public Packages

a list of all packages we want accessible from outside the module.

Services Offered

we can provide service implementations that can be consumed by other modules.

Services Consumed

allow the current module to be a consumer of a service.

Reflection Permissions

explicitly allows other classes to use reflection to access the private members of a package.

The approach describes in plain Java the coupling and cohesion principles.

module net.tangly.fsm {
    exports net.tangly.fsm;
    exports net.tangly.fsm.dsl;
    exports net.tangly.fsm.utilities;
    exports net.tangly.fsm.eventbus;
    exports net.tangly.fsm.eventbus.imp;

    requires org.apache.logging.log4j;
    requires static transitive org.jetbrains.annotations;
}

Goodies

Loom Threads

Java 19 introduces lightweight threads in the library. You can now program massively multithreaded applications with hundreds of thousands of threads without taxing the JVM or the operating system.

JavaDoc code snippets

Code examples in your java documentation are declared as a code snippet in a regular Java class. Your unit tests ensure that all your documented code snippets compile and run without trouble.

JShell

You have access to a Read-Evaluate-Print Loop REPL to test new Java code.

Vector Optimizations

You have access to the SIMD modules in your CPU directly from your Java code. Speed-ups can be tremendous. The code is still portable for all supported Java platforms.

References

[1] S. Harrer, J. Lenhard, and L. Dietz, Java By Comparison. Pragmatic Bookshelf, 2018 [Online]. Available: https://www.amazon.com/dp/B07CLFTVZS

[2] J. Bloch, Effective Java, Third. Addison-Wesley Professional, 2017 [Online]. Available: https://www.amazon.com/dp/B078H61SCH

[3] V. Subramaniam, Functional Programming In Java Harnessing The Power Of Java 8 Lambda Expressions. The Pragmatic Programmers, 2014 [Online]. Available: https://www.amazon.com/dp/B0CJL7VKFL

[4] S. Mak, Java 9 Modularity. O’Reilly Media, 2017 [Online]. Available: https://www.amazon.com/dp/1491954167


1. The record construct was added to Java 15 in March 2020. Records are shallow immutable objects. You can use them in pure functions to avoid unwanted side effects.