Default parameter values for Java via annotation processing.
Java doesn't natively support default parameter values like Scala, Kotlin, or Python. This library fills that gap by generating helper code at compile time.
- Features
- Comparison with Other Libraries
- Installation
- Quick Start
- Usage Guide
- Supported Types
- Annotation Reference
- Compile-Time Validation
- How It Works
- Requirements
- Building from Source
- License
- Default parameters for methods — call
greet()instead ofgreet("World", "Hello") - Named parameters — set only what you need:
.host("prod").timeout(30).call() - Constructor & record factories —
UserDefaults.create("Alice")with sensible defaults - Works with external types — generate defaults for third-party classes you can't modify
- Extensive compile-time validation — catch errors early with helpful messages and typo suggestions
- Compile-time only — no runtime dependencies, no reflection, just plain Java
| Feature | default4j | Lombok | Immutables | AutoValue | record-builder |
|---|---|---|---|---|---|
| Primary Focus | Default values | Boilerplate reduction | Immutable objects | Value types | Record builders |
| Method Defaults | ✅ Full support | ❌ | ❌ | ❌ | ❌ |
| Constructor Defaults | âś… Full support | ||||
| Named Parameters | ✅ Built-in | ❌ | ✅ Via builder | ✅ Via builder | ✅ Via builder |
| Record Support | ✅ Native | ✅ | ❌ | ✅ Native | |
| External Types | âś… @IncludeDefaults |
❌ | ❌ | ❌ | ✅ |
| Factory Methods | âś… @DefaultFactory |
❌ | ❌ | ❌ | ❌ |
| Field References | âś… @DefaultValue(field=...) |
❌ | ❌ | ❌ | ❌ |
| Aspect | default4j | Lombok | Immutables | AutoValue | record-builder |
|---|---|---|---|---|---|
| Approach | Annotation Processing | Bytecode Modification | Annotation Processing | Annotation Processing | Annotation Processing |
| Runtime Dependency | None | None | Optional | None | None |
| IDE Plugin Required | No | Yes | No | No | No |
| Compile-time Validation | âś… Extensive | âś… | âś… | âś… | |
| Debuggable Output | âś… Plain Java | âś… Plain Java | âś… Plain Java | âś… Plain Java | |
| Java Version | 17+ | 8+ | 8+ | 8+ | 16+ |
| Library | Best For |
|---|---|
| default4j | Adding default parameters to existing code, method defaults, Python/Kotlin-like defaults |
| Lombok | Reducing boilerplate (getters, setters, equals, toString), quick prototyping |
| Immutables | Complex immutable objects with many optional fields, serialization support |
| AutoValue | Simple value types, Google ecosystem integration |
| record-builder | Adding builders to records, withers for records |
default4j can work alongside other libraries:
// With Lombok - use Lombok for boilerplate, default4j for defaults
@Getter @Setter
public class Config {
@WithDefaults
public Config(@DefaultValue("localhost") String host) { ... }
}
// With record-builder - use record-builder for withers, default4j for factory defaults
@RecordBuilder // Generates withHost(), withPort() etc.
@WithDefaults // Generates ConfigDefaults.create() with defaults
public record Config(
@DefaultValue("localhost") String host,
@DefaultValue("8080") int port) {}default4j is unique in providing:
- Method-level defaults — No other library supports default values for regular method parameters
- Unified syntax — Same
@DefaultValueworks for methods, constructors, and records - True default values — Unlike builders, you get actual default parameters (omit trailing args)
- Factory method defaults —
@DefaultFactoryfor computed/lazy default values - Field reference defaults —
@DefaultValue(field="CONSTANT")for static constants - External type defaults —
@IncludeDefaultsfor third-party classes you can't modify
Java allows inline field initialization, but constructor defaults solve problems inline initialization can't:
| Use Case | Inline Defaults | default4j |
|---|---|---|
| Records & Immutables | ❌ Not possible | ✅ Full support |
| Factory Methods | ❌ Manual boilerplate | ✅ Auto-generated |
| Third-Party Classes | ❌ Can't modify source | ✅ @IncludeDefaults |
| Computed Defaults | ❌ No method calls | ✅ @DefaultFactory |
| Builder Pattern | ❌ Manual implementation | ✅ named=true |
| Self-Documenting API | ❌ Defaults hidden in code | ✅ Visible in annotations |
Best for: Records, immutable classes, factory patterns, external types, computed/dynamic values.
When inline is simpler: Mutable classes with simple literal defaults that don't need factory methods.
<dependency>
<groupId>io.github.reugn</groupId>
<artifactId>default4j</artifactId>
<version>${version}</version>
</dependency>implementation 'io.github.reugn:default4j:${version}'
annotationProcessor 'io.github.reugn:default4j:${version}'import io.github.reugn.default4j.annotation.*;
@WithDefaults
public class Config {
public Config(
@DefaultValue("localhost") String host,
@DefaultValue("8080") int port) {
// ...
}
}
// Usage - generated ConfigDefaults class:
Config c1 = ConfigDefaults.create(); // host="localhost", port=8080
Config c2 = ConfigDefaults.create("example.com"); // host="example.com", port=8080Generate overloaded static methods that omit trailing parameters with defaults.
public class Greeter {
@WithDefaults
public String greet(
@DefaultValue("World") String name,
@DefaultValue("Hello") String greeting) {
return greeting + ", " + name + "!";
}
}Generated usage:
Greeter g = new Greeter();
GreeterDefaults.greet(g); // "Hello, World!"
GreeterDefaults.greet(g, "Alice"); // "Hello, Alice!"
GreeterDefaults.greet(g, "Alice", "Hi"); // "Hi, Alice!"Use named = true to generate a fluent builder that allows skipping any parameter, not just trailing ones.
public class Database {
@WithDefaults(named = true)
public Connection connect(
@DefaultValue("localhost") String host,
@DefaultValue("5432") int port,
@DefaultValue("postgres") String user) {
return createConnection(host, port, user);
}
}Generated usage:
Database db = new Database();
// Skip port, set only host and user
DatabaseDefaults.connect(db)
.host("prod.example.com")
.user("admin")
.call();
// Use all defaults
DatabaseDefaults.connect(db).call();Generate factory methods for constructors with default parameters.
public class User {
@WithDefaults
public User(
String name,
@DefaultValue("user@example.com") String email,
@DefaultValue("USER") String role) {
// ...
}
}Generated usage:
User u1 = UserDefaults.create("Alice"); // Default email & role
User u2 = UserDefaults.create("Bob", "bob@test.com"); // Default role
User u3 = UserDefaults.create("Carol", "c@x.com", "ADMIN"); // All specifiedNamed mode for constructors:
public class User {
@WithDefaults(named = true)
public User(String name, @DefaultValue("USER") String role) {
// ...
}
}
// Skip to any parameter
User u = UserDefaults.create()
.name("Alice")
.build(); // role uses defaultApply @WithDefaults to a class to generate helpers for all constructors and methods that have @DefaultValue or
@DefaultFactory parameters.
@WithDefaults
public class Service {
// Constructor with defaults -> factory methods generated
public Service(
@DefaultValue("default") String name,
@DefaultValue("100") int value) {
// ...
}
// Method with defaults -> helper methods generated
public void process(@DefaultValue("INFO") String level) {
// ...
}
}Generated usage:
Service s = ServiceDefaults.create(); // All constructor defaults
ServiceDefaults.process(s); // Method with default levelWith options:
@WithDefaults(named = true, methodName = "builder")
public class AppConfig {
public AppConfig(
@DefaultValue("localhost") String host,
@DefaultValue("8080") int port) {
// ...
}
}
// Usage:
AppConfigDefaults.builder().port(3000).build();Annotation Precedence:
Method/constructor-level @WithDefaults takes precedence over class-level settings:
@WithDefaults(named = true) // Class uses builder mode
public class Api {
// Uses class-level named=true -> builder
public void fetch(@DefaultValue("/api") String path) { }
// Overrides with named=false -> static overloads
@WithDefaults(named = false)
public void send(@DefaultValue("POST") String method) { }
}Works with Java records — place @DefaultValue directly on record components.
@WithDefaults
public record ServerConfig(
@DefaultValue("localhost") String host,
@DefaultValue("8080") int port,
@DefaultValue("false") boolean ssl) {}Generated usage:
ServerConfig c1 = ServerConfigDefaults.create(); // All defaults
ServerConfig c2 = ServerConfigDefaults.create("api.com"); // Custom host
ServerConfig c3 = ServerConfigDefaults.create("api.com", 443); // Custom host + portNamed mode with records:
@WithDefaults(named = true)
public record DatabaseConfig(
@DefaultValue("localhost") String host,
@DefaultValue("5432") int port,
@DefaultValue("postgres") String database) {}
// Skip any component:
DatabaseConfig cfg = DatabaseConfigDefaults.create()
.host("prod.example.com")
.database("myapp")
.build(); // port uses defaultUse @DefaultValue(field = ...) to reference static constants:
public class Service {
static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30);
static final String DEFAULT_HOST = "localhost";
@WithDefaults
public void connect(
@DefaultValue(field = "DEFAULT_HOST") String host,
@DefaultValue(field = "DEFAULT_TIMEOUT") Duration timeout) {
// ...
}
}External class reference:
public class Defaults {
public static final Duration TIMEOUT = Duration.ofSeconds(30);
public static final String HOST = "localhost";
}
public class Client {
@WithDefaults
public void connect(
@DefaultValue(field = "Defaults.HOST") String host,
@DefaultValue(field = "Defaults.TIMEOUT") Duration timeout) {
// ...
}
}Note
For classes outside the current package, use the fully qualified class name (e.g., com.example.Defaults.TIMEOUT).
Use @DefaultFactory for computed values:
public class Logger {
static LocalDateTime timestamp() {
return LocalDateTime.now();
}
@WithDefaults
public void log(
String message,
@DefaultFactory("timestamp") LocalDateTime time) {
// ...
}
}Note
Evaluation Timing:
- Non-named mode: Factory called on each helper method invocation
- Named mode (builder): Factory called once at builder creation, not at
call()/build()
External class factory:
// Same package - simple class name works
@DefaultFactory("Factories.defaultConfig") Config config
// Different package - use fully qualified name
@DefaultFactory("java.util.UUID.randomUUID") UUID idNote
The annotation processor cannot access import statements. For classes outside the current package, use the fully qualified class name.
Mix all annotation types:
@WithDefaults
public void request(
String url,
@DefaultValue("GET") String method,
@DefaultValue(field = "DEFAULT_TIMEOUT") Duration timeout,
@DefaultFactory("createHeaders") Map<String, String> headers) {
// ...
}The following types can be used with @DefaultValue string literals:
| Type | Example |
|---|---|
String |
@DefaultValue("hello") |
int / Integer |
@DefaultValue("42") |
long / Long |
@DefaultValue("100") |
double / Double |
@DefaultValue("3.14") |
float / Float |
@DefaultValue("2.5") |
boolean / Boolean |
@DefaultValue("true") |
byte / Byte |
@DefaultValue("10") |
short / Short |
@DefaultValue("100") |
char / Character |
@DefaultValue("x") |
null (for objects) |
@DefaultValue("null") |
For other types, use @DefaultFactory or @DefaultValue(field=...).
Marks a method, constructor, or class for code generation.
| Option | Type | Default | Applies To | Description |
|---|---|---|---|---|
named |
boolean |
false |
All | Generate fluent builder instead of overloads |
methodName |
String |
"create" |
Constructors/Classes | Custom factory method name |
Specifies the default value for a parameter or record component.
| Option | Type | Default | Description |
|---|---|---|---|
value |
String |
"" |
Default as a string literal |
field |
String |
"" |
Reference to a static field |
@DefaultValue("hello") // String literal
@DefaultValue("42") // int literal
@DefaultValue("null") // null reference
@DefaultValue(field = "TIMEOUT") // Same-class field
@DefaultValue(field = "Cfg.TIMEOUT") // External class fieldNote: Use value or field, not both.
Specifies a factory method for computed default values.
@DefaultFactory("createConfig") // Same-class method
@DefaultFactory("Factories.defaultConfig") // External class methodRequirements:
- Method must be
staticwith no parameters - Must be accessible (package-private or public)
- Return type must be assignable to the parameter type
Generates default helpers for external classes you cannot modify (third-party libraries, generated code, etc.).
| Option | Type | Default | Description |
|---|---|---|---|
value |
Class<?>[] |
required | Classes to generate defaults for |
named |
boolean |
false |
Generate fluent builder |
methodName |
String |
"create" |
Factory method name |
Use case: External records/immutables with many parameters that you construct repeatedly with similar values (e.g., test fixtures, configuration objects).
// External record from a library (you can't modify this)
public record ExternalConfig(String host, int port, Duration timeout) {}
// Your code - define defaults via convention
@IncludeDefaults(ExternalConfig.class)
public class Defaults {
// Convention: DEFAULT_{COMPONENT_NAME} for fields
public static final String DEFAULT_HOST = "localhost";
public static final int DEFAULT_PORT = 8080;
// Convention: default{ComponentName}() for methods
public static Duration defaultTimeout() {
return Duration.ofSeconds(30);
}
}
// Generated: ExternalConfigDefaults with factory methods
ExternalConfig cfg = ExternalConfigDefaults.create(); // All defaults
ExternalConfig cfg2 = ExternalConfigDefaults.create("prod.com"); // Custom hostMatching conventions:
DEFAULT_HOSTmatcheshost(case-insensitive, underscores ignored)DEFAULT_FIRST_NAMEmatchesfirstNamedefaultTimeout()matchestimeout
Note
Constructor Selection: When a class has multiple public constructors, the processor
selects the one that best matches your defined defaults. For example, if a class has both
Foo(int port) and Foo(String host, int port) constructors, and you define DEFAULT_HOST
and DEFAULT_PORT, the two-parameter constructor is chosen. A warning is issued for any
defined defaults that don't match parameters in the selected constructor.
default4j performs extensive compile-time validation to catch errors early, before your code runs. All errors include helpful messages with suggestions when possible.
| Validation | Error Message |
|---|---|
Both @DefaultValue and @DefaultFactory on same parameter |
"Cannot have both @DefaultValue and @DefaultFactory" |
Both value and field in @DefaultValue |
"Cannot specify both 'value' and 'field'" |
Empty @DefaultValue("") on non-String type |
"Empty value is only valid for String type, not int" |
| Unparseable literal value | "'abc' is not a valid int" |
null for primitive types |
"'null' is not a valid default for primitive type int" |
| Validation | Error Message |
|---|---|
| Method not found | "Factory method 'foo' not found. Did you mean 'fooBar()'?" |
| Method not static | "Factory method 'create' must be static" |
| Method has parameters | "Factory method 'create' must have no parameters" |
| Method returns void | "Factory method 'create' cannot return void" |
| Incompatible return type | "Factory method 'create' returns String which is not assignable to Duration" |
| Validation | Error Message |
|---|---|
| Field not found | "Field 'TMEOUT' not found. Did you mean 'TIMEOUT'?" |
| Field not static | "Field 'timeout' must be static" |
| Incompatible field type | "Field 'COUNT' has type int which is not assignable to String" |
| Validation | Error Message |
|---|---|
| Non-consecutive defaults (non-named mode) | "Parameter without default found after parameter with default" |
| Private method/constructor | "Elements annotated with @WithDefaults cannot be private" |
| @WithDefaults on interface | "Can only be applied to methods, constructors, classes, or records" |
| Builder name conflict | "Builder name conflict: 'PersonBuilder' would be generated for both constructor and method" |
| Validation | Error Message |
|---|---|
| Including an interface | "Cannot include interface 'MyInterface'" |
| Including abstract class | "Cannot include abstract class 'BaseConfig'" |
| No public constructor | "ExternalClass has no public constructor" |
| No defaults defined | Warning: "No defaults found for ExternalConfig" |
| Unused defaults | Warning: "Default for 'hostname' does not match any parameter in selected constructor" |
| Non-consecutive defaults | "Non-consecutive defaults for ExternalConfig: 'database' has no default" |
When a field or method reference contains a typo, the error message suggests similar names:
// Your code
@DefaultValue(field = "TMEOUT") // typo
// Error message
Field 'TMEOUT' not found in Service. Did you mean 'TIMEOUT'?
If no similar name is found, available options are listed:
// Error message
Factory method 'foo' not found in Service.
Available static no-arg methods: createConfig(), defaultHost().
- Compile time: The annotation processor scans for
@WithDefaultsannotations - Code generation: For each annotated element, it generates a
{ClassName}Defaultsclass - Zero runtime cost: Generated code is plain Java with no reflection
Example generated code:
// For: @WithDefaults public void greet(@DefaultValue("World") String name)
public final class GreeterDefaults {
public static void greet(Greeter instance) {
instance.greet("World");
}
public static void greet(Greeter instance, String name) {
instance.greet(name);
}
}- Java 17 or higher
- Maven 3.6+
git clone https://github.com/reugn/default4j.git
cd default4j
mvn clean installLicensed under the Apache License 2.0.