<div dir="ltr"><div>Hi all,</div><div><br></div><div>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.</div><div><br></div><div>Thanks!</div><div>Brandon</div><div><br></div><div>Title: Enhanced Void return<br>Author: Brandon Mintern<br>Organization: Everlaw<br>Created: 2025/03/27<br>Type: Feature<br>State: Draft<br>Exposure: Open<br>Component: specification / language<br>Scope: SE<br>Template: 2.0<br><br>Summary<br>-------<br><br>Allow `Void`-returning methods to omit the `return null` statement. Method<br>implementations can still `return null` if desired, but they can also use a<br>plain `return` or omit the statement altogether.<br><br>Goals<br>-----<br><br>Improve ergonomics when using methods that accept a lambda and optionally return<br>a result. Enhance the compiler to:<br><br>- Automatically insert `return null` (or convert `return`) in `Void`-returning<br>  methods.<br>- Infer `Void` as the generic return type in method references and lambdas<br>  without a `return`.<br><br>Non-Goals<br>---------<br><br>- `Void`-returning methods are still free to explicitly state `return null`.<br>- The scope should be limited such that:<br>    - The compiler can do the heavy lifting, without any JVM changes.<br>    - No new lambda objects would be constructed implicitly.</div><div><br>Motivation<br>----------<br><br>Lambdas provide powerful abstractions for working with closeable resources or in<br>temporary contexts. For example, we can define the following abstraction:<br><br>    <R> R withBufferedReader(Path path, BufferedReaderFunction<R> f) {<br>        try (var reader = Files.newBufferedReader(path)) {<br>            return f.apply(reader);<br>        } catch (IOException e) {<br>            throw new UncheckedIOException(e);<br>        }<br>    }<br><br>    @FunctionalInterface<br>    interface BufferedReaderFunction<R> {<br>        R apply(BufferedReader reader) throws IOException;<br>    }<br><br>Callers can use this method as follows:<br><br>    var firstLine = withBufferedReader(path, reader -> reader.lines().findFirst());<br><br>When a caller doesn't need the return value, though, the caller must include an<br>awkward `return null`:<br><br>    withBufferedReader(path, reader -> {<br>        this.header = reader.readLine();<br>        this.data = reader.lines().toList();<br>        return null;<br>    });<br><br>These examples used `BufferedReader` for familiarity, but abstractions like this<br>are arguably more useful when reading from other persistent data stores (e.g.,<br>databases, search indexes) or when writing context-specific code. Using a<br>completely hypothetical `UserContextHolder`:<br><br>    <R> R inUserContext(User user, Supplier<R> supplier) {<br>        var oldContext = UserContextHolder.getContext();<br>        UserContextHolder.setUserContext(user);<br>        try {<br>            return supplier.get();<br>        } finally {<br>            UserContextHolder.setContext(oldContext);<br>        }<br>    }<br><br>Then we might have system code that looks like the following:<br><br>    interface Report {<br>        User getUser();<br>        GeneratedReport generate();<br>    }<br><br>    for (var report: pendingReports) {<br>        save(inUserContext(report.getUser(), report::generate));<br>    }<br><br>    // or:<br><br>    interface Job {<br>        User getUser();<br>        void execute();<br>    }<br><br>    for (var job : pendingJobs) {<br>        inUserContext(job.getUser(), () -> {<br>            job.execute();<br>            return null;<br>        });<br>        // inUserContext(job.getUser(), job::execute) with this proposal<br>    }<br><br>Description<br>-----------<br><br>When a method has a fully instantiated return type of `Void`, all of the<br>following would be valid:<br><br>- `return null`<br>- `return`<br>- omitted `return` (implicit `void` return)<br><br>`Void`-returning methods can continue using `return null` without warning. It<br>should be at least a warning, though, for a single method to mix `return null`<br>and any `void`-style return (implicit or explicit).<br><br>When a functional interface has a generic return type parameter, lambdas can use<br>`void`-style returns, and references to `void`-returning methods can be used,<br>where the compiler will infer the return type to be `Void` and logically insert</div><div>the necessary `return null` statements.<br><br>As with functional interfaces in general, an instance of one type cannot be<br>automatically converted to another. That is, a `Runnable` cannot be passed as a<br>`Supplier<T>`, inferring `T = Void`. `Runnable::run` could be used instead.<br><br>When a method returns a generic type, and when that type is inferred as `Void`<br>due to the use of an *enhanced `Void` return*, it would be an error to assign<br>the result to a variable. Returning to the `inUserContext` example:<br><br>    GeneratedReport result = inUserContext(report.getUser(), report::generate); // OK<br><br>    Void result = inUserContext(job.getUser(), () -> {<br>        job.execute();<br>        return null;<br>    }); // OK: result is null (backwards compatible)<br><br>    Void result = inUserContext(job.getUser(), job::execute); // ERROR<br><br>    Void result = inUserContext(job.getUser(), () -> job.execute()); // ERROR<br><br>This enhancement would be implemented entirely by the compiler, generating JVM<br>bytecode as if `return null` had been used where it's currently needed.<br><br>Alternatives<br>------------<br><br>Returning to the `withBufferedReader` example, without this proposal, we can<br>instead define sibling methods (and types) to avoid the need for `return null`:<br><br>    void withBufferedReader(Path path, BufferedReaderProcedure f) {<br>        withBufferedReader(path, reader -> {<br>            f.accept(reader);<br>            return null;<br>        });<br>    }<br><br>    interface BufferedReaderProcedure {<br>        void accept(BufferedReader reader) throws IOException;<br>    }<br><br>    withBufferedReader(path, reader -> {<br>        this.header = reader.readLine();<br>        this.data = reader.lines().toList();<br>    });<br><br>This works, sort of, but we quickly find that it doesn't actually work.<br>Returning to the first example:<br><br>    var firstLine = withBufferedReader(path, reader -> reader.lines().findFirst());<br><br>This now fails to compile, reporting "Ambiguous method call." We can work around<br>that issue, of course, by standardizing on verb pairs to use in these scenarios:<br><br>    R applyingBufferedReader(...)<br>    void acceptingBufferedReader(...)<br><br>In reality, though, this disambiguation is not the biggest issue. Rather, in<br>most codebases, it would be hard to justify defining duplicate methods and<br>types. Many development teams would instead use `return null` statements where<br>necessary, avoid the abstraction in cases where `return null` is needed, or<br>avoid defining the abstraction altogether.<br><br>Risks and Assumptions<br>---------------------<br><br>Some codebases may have sibling methods like those described above, where this<br>enhancement would cause new "Ambiguous method call." when using the<br>`void`-returning method. However:<br><br>- Already-compiled code would not be affected since there is no JVM change.<br>- Impacted code would likely benefit from this enhancement, with new compiler<br>  errors pointing to duplicate code that can likely be deleted.<br><br><br></div></div>