Looking at Java 22: Multi-File Source-Code Programs

 ยท 6 min
AI-generated by DALL-E

Software development is a dynamic process, especially in the early stages of a project or when experimenting with new ideas. During these phases, files and overall structures can change frequently. Java, traditionally known for its strict organizational requirements, has made impressive strides to accommodate this fluidity and become more beginner-friendly.

The introduction of JEP 330 allowed developers to run single-file programs directly from source code. Building on this, Java 22 introduces JEP 458, which extends this functionality to multi-file source-code programs, simplifying the process even further.


How to run Java Source Code (before Java 22)

Previously, Java required a two-step process to run code: compiling with javac and then running with java:

shell
# COMPILE
javac MyAwesomeApp.java

# RUN
java MyAwesomeApp

In “full” projects, these steps are usually handled by build tools like Gradle or Maven. For tinkering around and small-scale experimentation with a few files, that’s quite a hassle and can be cumbersome.

Java 11 and its JEP 330 made it a little bit easier by adding the ability to the java command to directly run Java source code, which is compiled in-memory “behind the scenes”:

shell
java --source 11 MyAwesomeApp.java

This is known as “source-code programs”.

However, this was limited to single-file Java programs.

The Java Language Specification allows for multiple classes in a single file, but if we want to organize our code just a little bit, we’d need to compile any additional classes beforehand.

Multiple classes make tinkering around a little easier, but as soon as the code starts to grow to more than a few lines or simple classes, we still need to branch out and set up a project with a build tool to get back control over the process of compiling and running our code.

An experienced developer expects that to happen, but it still shifts the code from simply tinkering and experimenting to a more rigid form. For beginners, though, it means that besides learning Java, they now need to deal with javac, build tools, and/or IDEs, which can be quite daunting at first.

Wouldn’t it be nice to extend the feasibility of the more straightforward tinker-phase of Java projects without relying on extra tooling?


Running Multi-File Source-Code Programs

In essence, the source-code mode of the java command has gained the capability of working with multiple files:

java
// MyAwesomeApp.java
class MyAwesomeApp {

  public static void main(String... args) {
    new AwesomeFeature().doTheWork(args);
  }
}


// AwesomeFeature.java
class AwesomeFeature {

  void doTheWork(String... args) {
    // ...
  }
}

The java launcher looks for referenced types based on their name/package and compiles them, too, if necessary.

There a few rules regarding the process, but they are what you’d expect from such a feature.

How To Find Source Files

Referenced files must be organized in the typical Java package-based directory hierarchy for the java launcher to find them.

The root directory of the source files is also the root of the package hierarchy. Any type located must be declared in the unnamed package. Other types residing in subfolders must have a matching package structure:

java
// MyAwesomeApp.java
class MyAwesomeApp {

  public static void main(String... args) {
    new features.AwesomeFeature().doTheWork(args);
  }
}


// ./features/awesome/AwesomeFeature.java
package features.awesome;

class AwesomeFeature {

  void doTheWork(String... args) {
    // ...
  }
}

Even though we can finally split up classes into multiple files, declaring more than one class in a single file can still be beneficial. It keeps all the moving parts closer together and, therefore, reduces the overall cognitive load required to grasp the bigger picture.

The java launcher seems to agree and prioritizes co-declared classes over separate files.

On-Demand Compilation

Only files of referenced types are actually compiled.

That means we can have multiple entry points using different sets of classes, and only the required minimum of compilation is done.

It also allows for easier tinkering and experimentation, as we don’t need to make sure all of our code is actually in a compilable state; only the used types need to be!

The order or timing of compiling the files isn’t guaranteed. Some files might get compiled beforehand, other parts get compiled lazily when needed.

If a .java file contains more than one class, all of them are compiled together.

Working with Pre-Compiled Code

Want to use other code in your experiments?
Just include the JARs!

Throwing a dependency as a JAR file directly in the project and including it with java --class-path '*' MyAwesomeApp.java is a way more satisfying experience than needing to set up Gradle or Maven to manage them, especially for beginners.

Even if the program becomes more complex, with multiple dependencies and package structures, moving it all to a libs directory and using --class-path 'libs/*' makes for a more straightforward alternative and extends the possible tinkering phase even more.

Keep in mind, though, that it’s a stated non-goal of the JEP to make using external dependencies easier. Nevertheless, the existing mechanisms are already quite straightforward and “good enough” for many use cases.

Java Modules

The root of the source tree might contain a module-info.java, making the resulting multi-file source-code program reside in that named module.

The --module-path or -p option works as with any other modular Java code:

shell
java --module-path . pkg/MyAwesomeApp.java

Conclusion

As expected, there are significant differences between compiling your code beforehand with javac and using the java launcher to run and compile source files on demand.

For example, a major downside of incremental compilation is deferring any compile issues until the actual file is loaded during execution, which might not be in the order you’d expect, and then, the program crashes.

Other class-based features work in both variants, like Reflection. Package annotations queried via Package::getAnnotations are also available if a module-info.java is present, so it can be compiled in-memory.

Two restrictions are currently present that might be mitigated in the future:

  • No annotation processing, similar to calling javac --proc:none
  • No source-code program with files belonging to multiple modules.

A few things are still missing to make Java on par with other languages regarding ease-of-use and the ability to experiment without requiring a full-fledged project setup or additional tooling.

Nevertheless, looking at these new capabilities, the improvements of the java launcher supporting more use cases is a significant shift towards a more tinker- and beginner-friendly Java.


A Functional Approach to Java Cover Image
Interested in using functional concepts and techniques in your Java code?
Check out my book!
Available in English, Polish, Korean, and soon, Chinese.

Resources

Looking at Java 22