Compact Main Method in Java: Modern Approaches to Application Entry Points

Java has evolved significantly in how we can define main methods and application entry points. This guide covers traditional approaches, modern compact syntax, and emerging alternatives for Java application startup.

Traditional Main Method

Classic Approach

public class TraditionalMain {
public static void main(String[] args) {
System.out.println("Hello, World!");
for (String arg : args) {
System.out.println("Argument: " + arg);
}
}
}

Compilation and Execution:

javac TraditionalMain.java
java TraditionalMain arg1 arg2

Modern Compact Approaches

1. Implicit Classes (Java 21+ Preview)

// No class declaration needed!
void main() {
System.out.println("Hello from implicit class!");
}

2. Compact Main Method (Java 21+)

// No public access modifier, no String[] parameter required
class CompactMain {
static void main() {
System.out.println("Compact main method!");
}
}

3. String[] Parameter Optional

class FlexibleMain {
// Both forms are valid
static void main() {
System.out.println("No args needed");
}
static void main(String[] args) {
System.out.println("With args: " + args.length);
}
}

Complete Examples by Java Version

Java 21+ Unnamed Classes and Instance Main Methods

// SimpleMain.java - Run directly with: java SimpleMain.java
void main() {
System.out.println("🎉 No class declaration needed!");
System.out.println("Running in " + java.time.LocalTime.now());
}
// With command-line arguments
void main(String[] args) {
System.out.println("Received " + args.length + " arguments:");
for (int i = 0; i < args.length; i++) {
System.out.println((i + 1) + ": " + args[i]);
}
}
// Using modern Java features
void main() {
var message = """
🚀 Compact Main Method Features:
• No explicit class declaration
• No public modifier required  
• String[] parameter optional
• Perfect for scripting and simple apps
""";
System.out.println(message);
}

Single-File Source-Code Programs

// Calculator.java - Run with: java Calculator.java 5 + 3
void main(String[] args) {
if (args.length != 3) {
System.out.println("Usage: java Calculator.java <num1> <operator> <num2>");
return;
}
double num1 = Double.parseDouble(args[0]);
String operator = args[1];
double num2 = Double.parseDouble(args[2]);
double result = switch (operator) {
case "+" -> num1 + num2;
case "-" -> num1 - num2;
case "*" -> num1 * num2;
case "/" -> num1 / num2;
default -> throw new IllegalArgumentException("Unknown operator: " + operator);
};
System.out.printf("%s %s %s = %.2f%n", num1, operator, num2, result);
}

Practical Use Cases

1. Quick Scripts and Utilities

// FileConverter.java
void main(String[] args) throws IOException {
if (args.length < 2) {
System.out.println("Usage: java FileConverter.java <input> <output>");
return;
}
String inputFile = args[0];
String outputFile = args[1];
String content = Files.readString(Path.of(inputFile));
String converted = content.toUpperCase();
Files.writeString(Path.of(outputFile), converted);
System.out.println("Converted " + inputFile + " to " + outputFile);
}

2. API Testing Client

// ApiTester.java
void main(String[] args) throws Exception {
String url = args.length > 0 ? args[0] : "https://api.github.com/users/java";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Accept", "application/json")
.build();
HttpResponse<String> response = client.send(request, 
HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + response.statusCode());
System.out.println("Body: " + response.body());
}

3. Data Processing Script

// DataProcessor.java
void main(String[] args) {
var data = """
Alice,25,Engineer
Bob,30,Designer
Charlie,35,Manager
""";
records Person(String name, int age, String job) {}
var people = data.lines()
.map(line -> line.split(","))
.map(parts -> new Person(parts[0], Integer.parseInt(parts[1]), parts[2]))
.toList();
// Average age
double avgAge = people.stream()
.mapToInt(Person::age)
.average()
.orElse(0);
System.out.printf("Average age: %.1f%n", avgAge);
people.forEach(p -> System.out.println(p.name() + " - " + p.job()));
}

Advanced Compact Patterns

1. Modular Compact Applications

// App.java - Self-contained modular application
void main() {
var app = new App();
app.start();
}
class App {
private final List<String> messages = new ArrayList<>();
void start() {
messages.add("Application started at " + java.time.LocalDateTime.now());
processData();
displayResults();
}
private void processData() {
// Simulate some processing
messages.add("Processed " + (int)(Math.random() * 100) + " items");
}
private void displayResults() {
System.out.println("=== APPLICATION RESULTS ===");
messages.forEach(System.out::println);
}
}

2. Interactive Console Application

// InteractiveApp.java
void main() {
var scanner = new java.util.Scanner(System.in);
System.out.println("🔧 Interactive Configuration");
System.out.print("Enter your name: ");
String name = scanner.nextLine();
System.out.print("Enter your age: ");
int age = scanner.nextInt();
var user = new User(name, age);
displayWelcome(user);
}
record User(String name, int age) {}
void displayWelcome(User user) {
var message = """
🎊 WELCOME %s!
Profile Summary:
• Name: %s
• Age: %d
• Birth Year: %d
Enjoy using the application!
""".formatted(user.name(), user.name(), user.age(), 
java.time.Year.now().getValue() - user.age());
System.out.println(message);
}

3. HTTP Server Example

// SimpleServer.java
void main() throws IOException {
int port = 8080;
var server = com.sun.net.httpserver.HttpServer.create(
new java.net.InetSocketAddress(port), 0);
server.createContext("/", exchange -> {
String response = """
{
"message": "Hello from compact Java!",
"timestamp": "%s",
"version": "Java %s"
}
""".formatted(java.time.Instant.now(), 
System.getProperty("java.version"));
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(200, response.getBytes().length);
try (var os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
});
server.start();
System.out.println("🚀 Server running on http://localhost:" + port);
System.out.println("Press Ctrl+C to stop...");
}

Build Tool Integration

Maven with Exec Plugin

<!-- pom.xml -->
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<mainClass>CompactMain</mainClass>
<commandlineArgs>arg1 arg2</commandlineArgs>
</configuration>
</plugin>
</plugins>
</build>
mvn compile exec:java

Gradle Application Plugin

// build.gradle
plugins {
id 'application'
}
application {
mainClass = 'CompactMain'
}
run {
args = ['arg1', 'arg2']
}
gradle run

Migration Strategies

From Traditional to Compact

// BEFORE: Traditional approach
public class TraditionalApp {
public static void main(String[] args) {
if (args.length > 0) {
System.out.println("Arguments: " + String.join(", ", args));
} else {
System.out.println("No arguments provided");
}
}
}
// AFTER: Compact approach
class CompactApp {
static void main(String[] args) {
var message = args.length > 0 
? "Arguments: " + String.join(", ", args)
: "No arguments provided";
System.out.println(message);
}
// Or even more compact:
static void main() {
System.out.println("Running without arguments");
}
}

Best Practices and Patterns

1. Error Handling in Compact Mains

// RobustMain.java
void main(String[] args) {
try {
if (args.length == 0) {
throw new IllegalArgumentException("No arguments provided");
}
int result = processArguments(args);
System.out.println("Result: " + result);
} catch (NumberFormatException e) {
System.err.println("Error: Invalid number format");
System.exit(1);
} catch (IllegalArgumentException e) {
System.err.println("Error: " + e.getMessage());
System.exit(1);
} catch (Exception e) {
System.err.println("Unexpected error: " + e.getMessage());
e.printStackTrace();
System.exit(2);
}
}
int processArguments(String[] args) {
return Arrays.stream(args)
.mapToInt(Integer::parseInt)
.sum();
}

2. Configuration Pattern

// ConfigurableApp.java
void main(String[] args) {
var config = loadConfig(args);
runApplication(config);
}
record AppConfig(String inputFile, String outputFile, boolean verbose) {}
AppConfig loadConfig(String[] args) {
String inputFile = "input.txt";
String outputFile = "output.txt";
boolean verbose = false;
for (int i = 0; i < args.length; i++) {
switch (args[i]) {
case "-i" -> inputFile = args[++i];
case "-o" -> outputFile = args[++i];
case "-v" -> verbose = true;
}
}
return new AppConfig(inputFile, outputFile, verbose);
}
void runApplication(AppConfig config) {
if (config.verbose()) {
System.out.println("Starting application with config: " + config);
}
// Application logic here
System.out.println("Processing " + config.inputFile() + 
" to " + config.outputFile());
}

3. Command Pattern for Complex Applications

// CommandApp.java
void main(String[] args) {
if (args.length == 0) {
showHelp();
return;
}
var command = args[0];
var commandArgs = Arrays.copyOfRange(args, 1, args.length);
switch (command) {
case "greet" -> greet(commandArgs);
case "calculate" -> calculate(commandArgs);
case "help" -> showHelp();
default -> System.out.println("Unknown command: " + command);
}
}
void greet(String[] args) {
String name = args.length > 0 ? args[0] : "World";
System.out.println("Hello, " + name + "!");
}
void calculate(String[] args) {
if (args.length != 3) {
System.out.println("Usage: calculate <num1> <operator> <num2>");
return;
}
double a = Double.parseDouble(args[0]);
String op = args[1];
double b = Double.parseDouble(args[2]);
double result = switch (op) {
case "+" -> a + b;
case "-" -> a - b;
case "*" -> a * b;
case "/" -> a / b;
default -> throw new IllegalArgumentException("Unknown operator: " + op);
};
System.out.println(a + " " + op + " " + b + " = " + result);
}
void showHelp() {
System.out.println("""
Available commands:
greet [name]    - Greet someone
calculate a op b - Perform calculation
help           - Show this help
""");
}

Testing Compact Main Methods

JUnit Testing

// Test for compact main methods
class CompactMainTest {
@Test
void testMainWithoutArgs() {
// Capture output
var outputStream = new ByteArrayOutputStream();
System.setOut(new PrintStream(outputStream));
// Run main method
CompactMain.main();
assertThat(outputStream.toString()).contains("Compact main method");
}
@Test
void testMainWithArgs() {
var outputStream = new ByteArrayOutputStream();
System.setOut(new PrintStream(outputStream));
CompactMain.main(new String[]{"test", "args"});
assertThat(outputStream.toString()).contains("With args: 2");
}
}

Performance Considerations

Startup Time Optimization

// Optimized for fast startup
class FastStartupApp {
static void main() {
// Minimal class loading
var startTime = System.nanoTime();
// Core logic
performTask();
var duration = (System.nanoTime() - startTime) / 1_000_000.0;
System.out.printf("Task completed in %.2f ms%n", duration);
}
private static void performTask() {
// Use primitives and avoid heavy initialization
int result = 0;
for (int i = 0; i < 1000; i++) {
result += i;
}
System.out.println("Result: " + result);
}
}

Conclusion

Compact main methods in Java provide:

  • Reduced boilerplate for simple applications and scripts
  • Improved readability with less ceremonial code
  • Faster prototyping and experimentation
  • Better scripting support for single-file programs

When to use compact main methods:

  • Simple utilities and scripts
  • Prototyping and experimentation
  • Educational examples
  • Single-file applications

When to stick with traditional main methods:

  • Large, complex applications
  • Enterprise applications with frameworks
  • Libraries and APIs
  • Applications requiring specific access modifiers

The compact main method syntax represents Java's evolution towards more developer-friendly syntax while maintaining backward compatibility and enterprise readiness.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper