I have an class that I would like to perform the following on:
I understand there are some workarounds, such as not providing any setter methods and only allowing the properties to be set in the constructor. I could implement this no problem but it got me wondering if there was some simpler way to just 'freeze' the object's properties in place. I believe there is an Object.freeze()
method in JavaScript that does something similar.
No, Java does not provide any such thawed/frozen feature.
You can likely meet your needs with a “builder”. The idea is that you define a second class in charge of producing instances of your first class. This builder class has setter methods for all the various properties you want to tweak. When all the setting is done, you call .build()
to produce an instance of the desired class. That desired instance may be immutable if you so choose.
A builder should return a reference to itself from its setters, to provide method-chaining.
A builder may set default values for some of the settings, if appropriate in your problem domain.
The builder class provides a few benefits:
isValid
method, and then correct the settings.You can see such builders in the API bundled with Java.
Locale.Builder
has methods for setting the language, region, script, and such. You eventually call Locale.Builder#build
to produce a Locale
object.DateTimeFormatterBuilder
has many setters for nuanced control of the many aspects of a desired date-time formatter. Call toFormatter
(same idea as a build
method) to produce a DateTimeFormatter
.Here is a brief example of a builder.
If an immutable object is desired, then a record
may be in order. In Java 16+, a record is a brief way to define a class whose main purpose is to communicate data transparently and immutably. You merely declare the type and name of its member fields. The compiler implicitly creates default constructor, getters, equals
& hashCode
, and toString
.
public record Employee( UUID id , String name , LocalDate hired ) {}
While a record is a special kind of class, it is still a class. So we can nest another class, a static
builder class.
public record Employee( UUID id , String name , LocalDate hired ) {
public static class Builder { … }
}
Here is the entire example class.
package work.basil.building;
import java.time.LocalDate;
import java.util.Objects;
import java.util.UUID;
public record Employee( UUID id , String name , LocalDate hired ) {
public static class Builder {
// ----------- Members
private UUID id;
private String name;
private LocalDate hired;
// ----------- Constructor
public Builder () {
this.id = UUID.randomUUID();
}
// ------- Accessors
public UUID getId () {
return id;
}
public Employee.Builder setId ( UUID id ) {
this.id = Objects.requireNonNull( id );
return this;
}
public String getName () {
return name;
}
public Employee.Builder setName ( String name ) {
Objects.requireNonNull( name );
if ( name.isBlank() ) {
throw new IllegalStateException( "Name must have some text, cannot be blank. Message # 346624fd-cb97-447a-9f56-e09ccf2e97f3." );
} else {
this.name = name;
}
return this;
}
public LocalDate getHired () {
return hired;
}
public Employee.Builder setHired ( LocalDate hired ) {
Objects.requireNonNull( hired );
if ( hired.isAfter( LocalDate.now() ) ) {
throw new IllegalStateException( "Hired date cannot be after today. Message # 181717b8-e2b0-4b5c-9fd2-ee45a2339b09." );
} else {
this.hired = hired;
}
return this;
}
// -------- Logic
public boolean isValid () {
return Objects.nonNull( this.id ) && Objects.nonNull( this.name ) && Objects.nonNull( this.hired );
}
public Employee build () {
if ( this.isValid() ) {
return new Employee( this.id , this.name , this.hired );
} else {
throw new IllegalStateException( "Builder is not valid, so cannot build new object. Message # c0021179-243c-4da5-b265-85208aaaf072" );
}
}
}
}
Example usage.
List <Employee> employees =
List.of(
new Employee.Builder().setName( "Alice" ).setHired( LocalDate.of( 2018 , Month.MARCH, 23) ).build() ,
new Employee.Builder().setName( "Bob" ).setHired( LocalDate.of( 2014 , Month.JANUARY, 28) ).build() ,
new Employee.Builder().setName( "Carol" ).setHired( LocalDate.of( 2013 , Month.JUNE, 17) ).build()
);
When run.
employees = [Employee[id=9736cb4c-1b32-4924-976b-7340f7f2fdc4, name=Alice, hired=2018-03-23], Employee[id=0ac4ff54-51b6-45c9-bb57-59f6efe40cd5, name=Bob, hired=2014-01-28], Employee[id=52cc9d03-3846-464a-bbed-49f022175bee, name=Carol, hired=2013-06-17]]