Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I prevent an object from being modified after it’s created in Java?

I’m trying to make sure an object can’t be modified after it’s created in Java. For example, in the class below, I can change the values of model and year using setters. But I want to prevent that and make the object immutable.

public class Car {
private String model;
private int year;

public Car(String model, int year) {
    this.model = model;
    this.year = year;
}

public void setModel(String model) {
    this.model = model;  // Allows modification of the model
}

public void setYear(int year) {
    this.year = year;  // Allows modification of the year
}

public void display() {
    System.out.println("Model: " + model + ", Year: " + year);
}

public static void main(String[] args) {
    Car car = new Car("Toyota", 2020);
    car.display();
    
    car.setModel("Honda");  // Modifying the model
    car.setYear(2021);      // Modifying the year
    car.display();
}

}

like image 346
Hind27 Avatar asked Oct 29 '25 17:10

Hind27


1 Answers

I know of three ways for you to produce immutable objects in Java.

No setters

If your class offers only getters to access private fields, with no setters, the resulting objects are rendered immutable. Pass all values in the constructor.

Doing this makes shallowly-immutable objects. By “shallow” we mean the member fields cannot be assigned a different object reference. But the objects referenced from a member field may itself be mutable. Your class cannot constrain such “deep” mutability.

You could also mark your member fields as final. This would ensure your internal code in that class/subclass does not inadvertently mutate a field that you intended to remain immutable.

public class Car 
{
    final private String model;
    final private Year year;

    public Car( final String model, final Year year) 
    {
        this.model = model;
        this.year = year;
    }

    public String getModel() { return this.model ; }
    public Year getYear() { return this.year ; }
}

(In real work, I would define an enum for all possible “Model” domain values. The type of our model field would be that enum rather than String. But that’s beside the point here.)

If the object referenced in your member fields might be mutable, you may want your getter methods to return a copy. Unnecessary here as both String and Year are immutable types.

Records

By definition, a record is shallowly immutable. You can set values, or nulls, only by passing to the constructor.

record Car ( String model , Year year ) {}

Every member field in a record is implicitly final.

Records can optionally have methods. But those methods cannot alter the member fields after construction.

Choose record where the main purpose of your class is to transparently communicate data. For more complicated purposes, choose a conventional class.

Builder

Passing a bunch of values to a constructor all at once may be awkward or inconvenient. The Builder pattern provides a more flexible approach.

A builder is an object of an adjacent class that temporarily holds inputs intended for the construction of another class. The builder method for each field can be called repeatedly to change an already-held value. So the calling programmer will make a series of calls to the builder, optionally in fluent syntax. These calls may set, clear, or change any of the inputs.

Once the calling programmer is happy with the configuration, they call a validate method to verify the configuration is in a valid state, meeting all business rules. (Tip: Jakarta Validation, formerly known as Bean Validation.)

Then the programmer calls build to produce an instance of the desired shallowly-immutable class. The instance returned by the build method will be of either kind discussed above: a conventional class without setters, or a record.

import java.time.Year;

public class Car {
    private final String model;
    private final Year year;

    // Private constructor to enforce use of builder
    private Car( final Builder builder ) {
        this.model = builder.model;
        this.year = builder.year;
    }

    // Getters
    public String getModel() { return model; }
    public Year getYear() { return year; }

    // Builder class
    public static class Builder {
        private String model;
        private Year year;

        public Builder model( final String model ) {
            this.model = model;
            return this;
        }

        public Builder year( final Year year ) {
            this.year = year;
            return this;
        }

        public Car build() {
            return new Car(this);
        }
    }
}

Usage:

Car car = 
     new Car.Builder()
        .model( "Toyota Camry" )
        .year( Year.of( 2023 ) )
        .build();

Caveat: This Answer is true for “normal” Java programming. But be aware that, by dipping into Java’s facility for reflection and introspection, otherwise immutable objects can be mutated. You can think of reflection in Java as a “back-door” trick.


A bonus feature appears if your objects are immutable: thread-safety in field access. To ensure that your object’s fields never get re-assigned, mark the member fields of a conventional class as final. For a record, the fields are already implicitly final. After construction, you can reliably access final fields across threads. Of course the objects referenced by those fields may not themselves be thread-safe, so be careful there. If the contents are deeply-immutable, then your object’s contents are fully thread-safe. (The behavior of their methods may not be thread-safe, so be careful there too. Concurrency is hard!)

like image 85
Basil Bourque Avatar answered Nov 01 '25 05:11

Basil Bourque