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
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 ) ) ;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With