Looking at Java 22: Multi-File Source-Code Programs
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.
Table of Contents
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
:
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”:
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:
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:
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:
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.