Writing your first program in Java can have quite a learning curve.
Java has been trying in recent releases to make it easier to write the first simple program.
Here are some changes that simplify your first Java program:
JEP 222: jshell: The Java Shell (Read-Eval-Print Loop) in JDK 9
JEP 330: Launch Single-File Source-Code Programs in JDK 11
JEP 445: Unnamed Classes and Instance Main Methods in preview in JDK 21
JEP 458: Launch Multi-File Source-Code Programs. Not released yet.
In this article, we’ll write a program (The Reloader) to easily play with Java code without the need to know how to compile or run the code or print the result.
The code will be automatically (re)compiled, (re)loaded, (re)executed and (re)printed to the output when the .java file is saved.
JShellJava 11+Java 21 (–enable-preview)ReloaderExecute java files(.jsh)No class neededNo complex main methodNo System.out.println neededRe-execute on file changed
What we’ll need:
Source code to play with
Code to detect when the source code has been modified
A compiler to recompile the code when the file has changed
A way to reload the generated class file after the compilation
A method to call when the new code is reloaded
The source code
import java.util.Random;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class PlayWithNumbers implements Supplier<String> {
@Override
public String get() {
IntStream numbers = new Random().ints(20, 0, 100);
return numbers.sorted().mapToObj(Integer::toString).collect(Collectors.joining(“, “));
}
}
The Reloader
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
/**
* Live Reload and execution of another java file.
* The other Java file needs to implements the Supplier interface.
* Public domain code.
*
* @author Anthony Goubard – japplis.com
*/
public class Reloader {
private String fileName;
public Reloader(String fileName) {
this.fileName = fileName;
try {
monitorForReload();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
private void monitorForReload() throws IOException {
Path file = Path.of(fileName);
if (Files.notExists(file)) throw new FileNotFoundException(file + ” not found”);
if (!file.toString().endsWith(“.java”)) throw new IllegalArgumentException(file + ” is not a .java file”);
Runnable compileAndRun = () -> { // statement executed each time the file is saved
try {
boolean compiled = compile(file);
if (compiled) execute(file);
} catch (Exception ex) {
System.err.println(ex.getClass().getSimpleName() + “: ” + ex.getMessage());
}
};
compileAndRun.run(); // First execution at launch
monitorFile(file, compileAndRun);
}
private boolean compile(Path file) throws IOException, InterruptedException {
String javaHome = System.getenv(“JAVA_HOME”);
String javaCompiler = javaHome + File.separator + “bin” + File.separator + “javac”;
Process compilation = Runtime.getRuntime().exec(new String[] {javaCompiler, file.toString()});
compilation.getErrorStream().transferTo(System.err); // Only show compilation errors
compilation.waitFor(1, TimeUnit.MINUTES); // Wait for max 1 minute for the compilation
boolean compiled = compilation.exitValue() == 0;
if (!compiled) System.err.println(“Compilation failed”);
return compiled;
}
private void execute(Path file) throws Exception {
// Execution is done in a separate class loader that doesn’t use the current class loader as parent
URL classpath = file.toAbsolutePath().getParent().toUri().toURL();
URLClassLoader loader = new URLClassLoader(new URL[] { classpath }, ClassLoader.getPlatformClassLoader());
String supplierClassName = file.toString().substring(file.toString().lastIndexOf(File.separator) + 1, file.toString().lastIndexOf(“.”));
// Create a new instance of the supplier with the class loader and call the get method
Class<?> stringSupplierClass = Class.forName(supplierClassName, true, loader);
Object stringSupplier = stringSupplierClass.getDeclaredConstructor().newInstance();
if (stringSupplier instanceof Supplier) {
Object newResult = ((Supplier) stringSupplier).get();
System.out.println(“> ” + newResult);
}
}
public static void monitorFile(Path file, Runnable onFileChanged) throws IOException {
// Using WatchService was more code and less reliable
Runnable monitor = () -> {
try {
long lastModified = Files.getLastModifiedTime(file).to(TimeUnit.SECONDS);
while (Files.exists(file)) {
long newLastModified = Files.getLastModifiedTime(file).to(TimeUnit.SECONDS);
if (lastModified != newLastModified) {
lastModified = newLastModified;
onFileChanged.run();
}
Thread.sleep(1_000);
}
} catch (Exception ex) {
ex.printStackTrace();
}
};
Thread monitorThread = new Thread(monitor, file.toString());
monitorThread.start();
}
public static void main(String[] args) {
if (args.length == 0) {
System.err.println(“Missing Java file to execute argument.”);
System.exit(-1);
}
new Reloader(args[0]);
}
}
Now start it with java Reloader.java PlayWithNumbers.java and edit the PlayWithNumbers.java file as you wish.
Going further
In this example, we created a minimal version (100 lines of code) that could easily be extended with the following features:
Provide multiple java files in the arguments
Provide a directory instead of a Java file
Detect if there is a pom.xml, gradle.properties or build.xml and build with Apache Maven, Gradle or Apache Ant instead of javac
Provide external libraries to the URLClassLoader
Detect the return type of the Supplier (String, List, Array, JComponent, …) and show the result accordingly
Open a JShell file (.jsh), create a Java file from it and execute it
Use JBang so that installing and running Java get even easier
Some of these ideas, I’ve implemented in my Applet Runner IDE plug-in where I recently added support for local java files for URLs.
The post Live (re)compile, (re)load, (re)execute Java code in 100 LoC appeared first on foojay.