Draft JEP: Enhanced Void return

Brandon Mintern mintern at everlaw.com
Thu Mar 27 19:16:18 UTC 2025


I'm sorry. I see that my prior email wraps poorly when rendered in the web
interface. Here's the same email wrapped to 76 characters for better
readability:

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, r -> r.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:

    var report = 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, r -> r.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.


On Thu, Mar 27, 2025 at 11:59 AM Brandon Mintern <mintern at everlaw.com>
wrote:

> 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/acc2f4d0/attachment-0001.htm>


More information about the compiler-dev mailing list