Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating immutable Records having mutable fields

I want to create an immutable record which has 2 mutable fields Date and a HashMap

public record ImmutableRecord(String name, LocalDate admissionDate, Date dateOfBirth, Map<String, Integer> metaData) {

    public ImmutableRecord{
        // Date is a mutable field
        dateOfBirth = new Date(dateOfBirth.getTime());

        // HashMap is a mutable field
        Map<String, Integer> tempMap = new HashMap<>();
        for(Map.Entry<String, Integer> entry: metaData.entrySet()){
            tempMap.put(entry.getKey(), entry.getValue());
        }
        metaData = tempMap;

        //Can I use following instead of above for deep copying the map?
        metaData = Map.copyOf(metaData);
    }

}

Which of the following approach is correct using forEach to deep clone every field or using Map.copyOf

like image 921
user2173372 Avatar asked Sep 18 '25 08:09

user2173372


1 Answers

First of all, in terms of design, generally the responsibility for copying data should fall upon the calling method that instantiates the record. The constructor should not be doing this copying, commonly.

Let’s change the name of this record, as “ImmutableRecord” is redundant, and fails to reflect your problem domain. Apparently you intend to represent students, given the admission date field. So your entire class definition could be:

public record Student ( String name, LocalDate admitted, LocalDate dateOfBirth, Map<String, Integer> metaData ) {}

The modern and immutable LocalDate class should always be used instead of the mutable legacy class java.sql.Date class.

The calling method should do the copying, as seen further below.

Regarding your attempt to clone the map:

    Map<String, Integer> tempMap = new HashMap<>();
    for(Map.Entry<String, Integer> entry: metaData.entrySet()){
        tempMap.put(entry.getKey(), entry.getValue());
    }

That copying is not a deep clone. You did not copy the content of the keys. Nor did you copy the content of the values. You did create another Map object. But both the old map and the new map have entries holding references to the very same key & value objects.

Your copying is effectively the same as merely passing the old map to the constructor of a new map. Both old and new map have entries that point to the same key objects and the same value objects.

Map < String , Integer > metaData = new HashMap <> ( oldMetaData ) ;

Your copying is nearly the same as Map.copyOf( oldMap ) except that copyOf produces an unmodifiable map of some unspecified class implementing Map rather than a modifiable HashMap. But like your code above, both the old and new maps point to the same key objects and the same value objects.

So the calling code would look something like this:

Student someStudent = new Student( "Alice" , … ) ;
…
Student twinStudent = 
    new Student( 
        "Bob" , 
        LocalDate.of( 2021, Month.MARCH, 23 ), 
        someStudent.dateOfBirth, 
        Map.copyOf( someStudent.metaData ) 
    ) 
;

The purpose of the records feature is to concisely define a class whose main purpose is to communicate data transparently and immutably. So ideally a record should contain immutable content. That is another reason to prefer Map.copyOf over a new HashMap.

As commented, another reason to prefer Map.copyOf is that if the passed map is already unmodifiable, that passed map is returned directly. Less memory used, and less CPU used.

As commented, one possible limitation to using Map.copyOf is that nulls are not tolerated in the map reference, in any of the keys, nor in any of the values. Map.copyOf is a “null-free zone”, my own newly coined term.

If nulls are tolerable in your scheme, an alternative would be Collections.unmodifiableMap. And, for defensive programming, make a copy of the map being wrapped by Collections.unmodifiableMap.

metaData = Collections.unmodifiableMap( new HashMap<>( oldMetaData ) ) ;
like image 73
Basil Bourque Avatar answered Sep 19 '25 22:09

Basil Bourque