Search code examples
javaobjectoopfinal

Does a Java method exist that can "finalize" private state of Object?


I have an class that I would like to perform the following on:

  • Create an instance of the class
  • Alter the private properties of the object in a separate class
  • Freeze the properties of the object so they cannot be modified after I am done the initial setup

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.


Solution

  • No

    No, Java does not provide any such thawed/frozen feature.

    Builder

    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:

    • Achieves your goal of producing an object that is only immutable after twiddling with its intended values.
    • Ensures valid values. The calling programmer can verify the builder with a isValid method, and then correct the settings.
    • Allows for subclasses to be returned, where appropriate, depending on the values you set.

    You can see such builders in the API bundled with Java.

    Example

    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]]