Compile-time annotation processor for generating immutable value objects with builder patterns and compile-time validation.
—
Advanced attribute behavior annotations for flexible object modeling including default values, lazy computation, derived values, parameter configuration, and auxiliary attributes.
Specify default values for attributes that are optional during construction.
/**
* Annotates accessor that should be turned into settable generated attribute
* that is non-mandatory to set via builder. Default value obtained by calling
* the annotated method.
*/
@interface Value.Default { }Usage Examples:
@Value.Immutable
public interface Server {
String host();
@Value.Default
default int port() { return 8080; }
@Value.Default
default boolean ssl() { return false; }
@Value.Default
default List<String> tags() { return List.of(); }
}
// Builder usage - default values are optional
Server server1 = ImmutableServer.builder()
.host("localhost")
.build(); // port=8080, ssl=false, tags=[]
Server server2 = ImmutableServer.builder()
.host("example.com")
.port(443)
.ssl(true)
.addTags("production", "web")
.build();Thread-safe lazy computation of expensive attribute values.
/**
* Lazy attributes cannot be set during building but are computed lazily
* once and only once in a thread-safe manner. Computed from other attributes.
* Always act as auxiliary (excluded from equals, hashCode, toString).
*/
@interface Value.Lazy { }Usage Examples:
@Value.Immutable
public interface Order {
List<Item> items();
@Value.Lazy
default BigDecimal totalCost() {
return items().stream()
.map(item -> item.price().multiply(BigDecimal.valueOf(item.quantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
@Value.Lazy
default String summary() {
return String.format("Order with %d items, total: $%.2f",
items().size(), totalCost());
}
}
// Usage - lazy values computed on first access
Order order = ImmutableOrder.builder()
.addItems(item1, item2, item3)
.build();
// First access triggers computation and caches result
BigDecimal cost = order.totalCost(); // Computes and caches
BigDecimal sameCost = order.totalCost(); // Returns cached valueEagerly computed attributes that are calculated from other attributes during construction.
/**
* Derived attributes cannot be set during building but are eagerly computed
* from other attributes and stored in field. Should be applied to non-abstract
* method that serves as the attribute value initializer.
*/
@interface Value.Derived { }Usage Examples:
@Value.Immutable
public interface Rectangle {
double width();
double height();
@Value.Derived
default double area() {
return width() * height();
}
@Value.Derived
default double perimeter() {
return 2 * (width() + height());
}
@Value.Derived
default String dimensions() {
return width() + "x" + height();
}
}
// Derived values are computed during construction
Rectangle rect = ImmutableRectangle.builder()
.width(10.0)
.height(5.0)
.build();
// Values are already computed and stored
double area = rect.area(); // Returns pre-computed 50.0
String dims = rect.dimensions(); // Returns pre-computed "10.0x5.0"Control which attributes become constructor parameters and their ordering.
/**
* Mark abstract accessor method to be included as constructor parameter.
* For constructable objects, all non-default and non-derived attributes
* should be annotated with @Value.Parameter.
*/
@interface Value.Parameter {
/**
* Specify order of constructor argument. Defaults to -1 (unspecified).
* Arguments are sorted ascending by this order value.
*/
int order() default -1;
/**
* Whether to include as parameter. Set false to cancel out parameter
* (useful with Style.allParameters flag).
*/
boolean value() default true;
}Usage Examples:
@Value.Immutable
public interface Person {
@Value.Parameter(order = 1)
String firstName();
@Value.Parameter(order = 2)
String lastName();
@Value.Parameter(order = 3)
int age();
@Value.Default
default String email() { return ""; }
}
// Generated constructor: ImmutablePerson.of(firstName, lastName, age)
Person person = ImmutablePerson.of("John", "Doe", 30);
// Builder still available
Person person2 = ImmutablePerson.builder()
.firstName("Jane")
.lastName("Smith")
.age(25)
.email("jane@example.com")
.build();
// Exclude from parameters while using allParameters style
@Value.Immutable
@Value.Style(allParameters = true)
public interface Coordinate {
double x();
double y();
@Value.Parameter(false) // Exclude from constructor
@Value.Default
default String label() { return ""; }
}
// Constructor: ImmutableCoordinate.of(x, y) - label excludedAttributes that are stored and accessible but excluded from equals, hashCode, and toString methods.
/**
* Annotate attribute as auxiliary - will be stored and accessible but
* excluded from generated equals(), hashCode() and toString() methods.
* Lazy attributes are always auxiliary.
*/
@interface Value.Auxiliary { }Usage Examples:
@Value.Immutable
public interface CacheEntry {
String key();
Object value();
@Value.Auxiliary
long createdTimestamp();
@Value.Auxiliary
int accessCount();
@Value.Auxiliary
@Value.Default
default String debugInfo() { return ""; }
}
// Auxiliary attributes don't affect equality
CacheEntry entry1 = ImmutableCacheEntry.builder()
.key("user:123")
.value(userData)
.createdTimestamp(System.currentTimeMillis())
.accessCount(0)
.build();
CacheEntry entry2 = ImmutableCacheEntry.builder()
.key("user:123")
.value(userData)
.createdTimestamp(System.currentTimeMillis() + 1000) // Different timestamp
.accessCount(5) // Different access count
.build();
// These are considered equal (auxiliary attributes ignored)
assert entry1.equals(entry2); // true
assert entry1.hashCode() == entry2.hashCode(); // true
// But auxiliary values are still accessible
long timestamp = entry1.createdTimestamp();
int count = entry1.accessCount();Mark methods as non-attributes to prevent them from becoming generated attributes.
/**
* Mark some abstract no-argument methods in supertypes as regular,
* non-attribute methods. Prevents annotation processor from generating
* field, accessor, and builder initializer.
*/
@interface Value.NonAttribute { }Usage Examples:
// Base interface with methods that shouldn't be attributes
public interface Validatable {
@Value.NonAttribute
default boolean isValid() {
// Custom validation logic
return true;
}
@Value.NonAttribute
default List<String> validate() {
// Return validation errors
return List.of();
}
}
@Value.Immutable
public interface User extends Validatable {
String name();
String email();
// Override validation methods
@Override
default boolean isValid() {
return !name().isEmpty() && email().contains("@");
}
@Override
default List<String> validate() {
List<String> errors = new ArrayList<>();
if (name().isEmpty()) errors.add("Name is required");
if (!email().contains("@")) errors.add("Invalid email format");
return errors;
}
}
// isValid() and validate() are regular methods, not attributes
User user = ImmutableUser.builder()
.name("John")
.email("john@example.com")
.build();
boolean valid = user.isValid(); // Method call, not attribute access
List<String> errors = user.validate(); // Method call, not attribute accessInstall with Tessl CLI
npx tessl i tessl/maven-org-immutables--value