Draft JEP: Enhanced Void return

Brandon Mintern mintern at everlaw.com
Thu Mar 27 18:59:52 UTC 2025


Hi all,

I'm interested in allowing `return null` to be omitted from Void-returning
methods. Apologies if I'm doing this wrong, but I've drafted the following
JEP. I'd love to get feedback and discuss the feasibility of a change like
this.

Thanks!
Brandon

Title: Enhanced Void return
Author: Brandon Mintern
Organization: Everlaw
Created: 2025/03/27
Type: Feature
State: Draft
Exposure: Open
Component: specification / language
Scope: SE
Template: 2.0

Summary
-------

Allow `Void`-returning methods to omit the `return null` statement. Method
implementations can still `return null` if desired, but they can also use a
plain `return` or omit the statement altogether.

Goals
-----

Improve ergonomics when using methods that accept a lambda and optionally
return
a result. Enhance the compiler to:

- Automatically insert `return null` (or convert `return`) in
`Void`-returning
  methods.
- Infer `Void` as the generic return type in method references and lambdas
  without a `return`.

Non-Goals
---------

- `Void`-returning methods are still free to explicitly state `return null`.
- The scope should be limited such that:
    - The compiler can do the heavy lifting, without any JVM changes.
    - No new lambda objects would be constructed implicitly.

Motivation
----------

Lambdas provide powerful abstractions for working with closeable resources
or in
temporary contexts. For example, we can define the following abstraction:

    <R> R withBufferedReader(Path path, BufferedReaderFunction<R> f) {
        try (var reader = Files.newBufferedReader(path)) {
            return f.apply(reader);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    @FunctionalInterface
    interface BufferedReaderFunction<R> {
        R apply(BufferedReader reader) throws IOException;
    }

Callers can use this method as follows:

    var firstLine = withBufferedReader(path, reader ->
reader.lines().findFirst());

When a caller doesn't need the return value, though, the caller must
include an
awkward `return null`:

    withBufferedReader(path, reader -> {
        this.header = reader.readLine();
        this.data = reader.lines().toList();
        return null;
    });

These examples used `BufferedReader` for familiarity, but abstractions like
this
are arguably more useful when reading from other persistent data stores
(e.g.,
databases, search indexes) or when writing context-specific code. Using a
completely hypothetical `UserContextHolder`:

    <R> R inUserContext(User user, Supplier<R> supplier) {
        var oldContext = UserContextHolder.getContext();
        UserContextHolder.setUserContext(user);
        try {
            return supplier.get();
        } finally {
            UserContextHolder.setContext(oldContext);
        }
    }

Then we might have system code that looks like the following:

    interface Report {
        User getUser();
        GeneratedReport generate();
    }

    for (var report: pendingReports) {
        save(inUserContext(report.getUser(), report::generate));
    }

    // or:

    interface Job {
        User getUser();
        void execute();
    }

    for (var job : pendingJobs) {
        inUserContext(job.getUser(), () -> {
            job.execute();
            return null;
        });
        // inUserContext(job.getUser(), job::execute) with this proposal
    }

Description
-----------

When a method has a fully instantiated return type of `Void`, all of the
following would be valid:

- `return null`
- `return`
- omitted `return` (implicit `void` return)

`Void`-returning methods can continue using `return null` without warning.
It
should be at least a warning, though, for a single method to mix `return
null`
and any `void`-style return (implicit or explicit).

When a functional interface has a generic return type parameter, lambdas
can use
`void`-style returns, and references to `void`-returning methods can be
used,
where the compiler will infer the return type to be `Void` and logically
insert
the necessary `return null` statements.

As with functional interfaces in general, an instance of one type cannot be
automatically converted to another. That is, a `Runnable` cannot be passed
as a
`Supplier<T>`, inferring `T = Void`. `Runnable::run` could be used instead.

When a method returns a generic type, and when that type is inferred as
`Void`
due to the use of an *enhanced `Void` return*, it would be an error to
assign
the result to a variable. Returning to the `inUserContext` example:

    GeneratedReport result = inUserContext(report.getUser(),
report::generate); // OK

    Void result = inUserContext(job.getUser(), () -> {
        job.execute();
        return null;
    }); // OK: result is null (backwards compatible)

    Void result = inUserContext(job.getUser(), job::execute); // ERROR

    Void result = inUserContext(job.getUser(), () -> job.execute()); //
ERROR

This enhancement would be implemented entirely by the compiler, generating
JVM
bytecode as if `return null` had been used where it's currently needed.

Alternatives
------------

Returning to the `withBufferedReader` example, without this proposal, we can
instead define sibling methods (and types) to avoid the need for `return
null`:

    void withBufferedReader(Path path, BufferedReaderProcedure f) {
        withBufferedReader(path, reader -> {
            f.accept(reader);
            return null;
        });
    }

    interface BufferedReaderProcedure {
        void accept(BufferedReader reader) throws IOException;
    }

    withBufferedReader(path, reader -> {
        this.header = reader.readLine();
        this.data = reader.lines().toList();
    });

This works, sort of, but we quickly find that it doesn't actually work.
Returning to the first example:

    var firstLine = withBufferedReader(path, reader ->
reader.lines().findFirst());

This now fails to compile, reporting "Ambiguous method call." We can work
around
that issue, of course, by standardizing on verb pairs to use in these
scenarios:

    R applyingBufferedReader(...)
    void acceptingBufferedReader(...)

In reality, though, this disambiguation is not the biggest issue. Rather, in
most codebases, it would be hard to justify defining duplicate methods and
types. Many development teams would instead use `return null` statements
where
necessary, avoid the abstraction in cases where `return null` is needed, or
avoid defining the abstraction altogether.

Risks and Assumptions
---------------------

Some codebases may have sibling methods like those described above, where
this
enhancement would cause new "Ambiguous method call." when using the
`void`-returning method. However:

- Already-compiled code would not be affected since there is no JVM change.
- Impacted code would likely benefit from this enhancement, with new
compiler
  errors pointing to duplicate code that can likely be deleted.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/compiler-dev/attachments/20250327/f4e8e493/attachment-0001.htm>


More information about the compiler-dev mailing list