Maps
The Map
interface is the base interface for collections which allows to store unique key/value pairs, no necessary in any particular order.
Table of contents
- Create Maps
- Hashtable
- HashMap
- Is
HashMap
the successor of theHashtable
? - LinkedHashMap
- TreeMap
- Can we store
null
s? - Which Map to Use?
- Map keys MUST BE immutable
- Double brace initialization
- Mutable and immutable maps
Create Maps
Java 9 added static methods to the Map interface Map.of(). Consider the following example.
package demo;
import java.util.Map;
public class App {
public static void main( final String[] args ) {
final Map<String, Integer> marks = Map.of(
"Aden", 82,
"Jade", 92,
"Peter", 74,
"Jane", 68
);
System.out.printf( "Marks: %s%n", marks );
}
}
The above example creates a map of students and their tests’ mark, and simply print the map’s elements (also referred to as entries), in no particular order.
Marks: {Aden=82, Jane=68, Peter=74, Jade=92}
Maps take a key and a value, the students’ name and their marks in the above example, and stores them as entries, as shown in the following image,
Maps can be seen as a collection of entries that use the entry’s key to determine where to save the entity. Maps can only contain unique keys. We cannot have two entries with the same key. The Map.of()
method will throw an IllegalArgumentException
if duplicate keys are provided. Consider the following example.
IllegalArgumentException
!! package demo;
import java.util.Map;
public class App {
public static void main( final String[] args ) {
/* ⚠️ Throws IllegalArgumentException!! */
final Map<String, Integer> marks = Map.of(
"Aden", 82,
"Aden", 92
);
System.out.printf( "Marks: %s%n", marks );
}
}
The above example will fail as expected.
Exception in thread "main" java.lang.IllegalArgumentException: duplicate key: Aden
at java.base/java.util.ImmutableCollections$MapN.<init>(ImmutableCollections.java:977)
at java.base/java.util.Map.of(Map.java:1328)
at demo.App.main(App.java:8)
Generally, maps do not fail when duplicate keys are added. Instead, the value associated with the duplicate key will simply replace the existing value. This is a unique behaviour of the Map.of()
methods.
Some implementations of Map
support null
keys and/or values. Unfortunately, the Map.of()
method does not support null
s, either as key or value and a NullPointerException
is thrown when null
is passed.
NullPointerException
!! package demo;
import java.util.Map;
public class App {
public static void main( final String[] args ) {
/* ⚠️ Throws NullPointerException!! */
final Map<String, Integer> marks = Map.of( "Aden", null );
System.out.printf( "Marks: %s%n", marks );
}
}
The above example will fail as expected.
Exception in thread "main" java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Objects.java:222)
at java.base/java.util.ImmutableCollections$Map1.<init>(ImmutableCollections.java:884)
at java.base/java.util.Map.of(Map.java:1308)
at demo.App.main(App.java:8)
Hashtable
Hashtable is an implementation of Map
interface based on hash functions and buckets, as shown in the following diagram.
The image shown above is very similar to another image shown in the sets page. Hash based sets, use hash-based maps as their underlying data structure.
A Hashtable
can be seen as a list of lists, where entities are placed in the bucket they belong. A hash function is used to determine the bucket the entities belongs to, as shown in the following diagram.
The Hashtable
will use the entry’s key hashCode()
method to determine the bucket to which the entry belongs, then the equals()
method to determine whether this already exists within the bucket, as shown in the following diagram.
The relation between these two methods is so strong that the Effective Java book has an item about this, Item 11: Always override hashCode when you override equals.
A Hashtable
can be created like any other object, as shown next.
package demo;
import java.util.Hashtable;
import java.util.Map;
public class App {
public static void main( final String[] args ) {
final Map<String, Integer> marksByName = new Hashtable<>();
marksByName.put( "Aden", 82 );
marksByName.put( "Jade", 92 );
marksByName.put( "Peter", 74 );
marksByName.put( "Jane", 68 );
System.out.printf( "Marks: %s%n", marksByName );
}
}
The above example creates a map and adds (puts) four entries to the map. We can provide hits to the Hashtable
constructor about its initial capacity and the load factor. The load factor is the relation between number of buckets and the size of the map. This is a trade-off between memory used and performance. In most cases the default load factor value works well, but there are cases where this needs to be tuned.
It is always recommended to provide an initial capacity when this is known as it minimises the number of times the Hashtable
has to resize its internal data structures, as shown in the following example.
package demo;
import java.util.Hashtable;
import java.util.Map;
public class App {
public static void main( final String[] args ) {
final Map<String, Integer> marksByName = new Hashtable<>( 4 );
marksByName.put( "Aden", 82 );
marksByName.put( "Jade", 92 );
marksByName.put( "Peter", 74 );
marksByName.put( "Jane", 68 );
System.out.printf( "Marks: %s%n", marksByName );
}
}
Both examples will print the same output.
Marks: {Jane=68, Peter=74, Jade=92, Aden=82}
The Hashtable
’s put()
method returns the previous value, if one exists and null
if no value exists.
null
as the entry's value. Therefore, a null
does not necessarily indicate that an entry did not exist for the given key.When adding (putting) an entry, which key already exists in the map, the put()
method will replace the existing entry with the new one and returns the previous entry’s value, as shown in the following example.
package demo;
import java.util.Hashtable;
import java.util.Map;
public class App {
public static void main( final String[] args ) {
final Map<String, Integer> marksByName = new Hashtable<>( 4 );
marksByName.put( "Aden", 82 );
marksByName.put( "Jade", 92 );
marksByName.put( "Peter", 74 );
marksByName.put( "Jane", 68 );
final Integer previousMark = marksByName.put( "Aden", 84 );
System.out.printf( "Previous mark: %s%n", previousMark );
}
}
The entry for the given key is replaced by the new entry and the previous entry’s values is returned, if one exists otherwise null
, as also indicated in the following output.
Previous mark: 82
In some of above examples, the output is always returned in particular order. This may give the wrong impression that the Hashtable
always returns the elements in a given order. The order in which the entries are returned is not guaranteed and may vary between different versions of the JVM and JRE. There are other map implementations, such as LinkedHashMap
and TreeMap
, that always return the elements in a specific order.
Hashtable
is one of the oldest Map
implementation. Java provides better implementations of Map
, such as HashMap
when concurrency is not an issue or ConcurrentHashMap
when we need to work with multiple threads.
HashMap
HashMap is another implementation of the Map
interface that works in a similar fashion as the Hashtable
described before. Consider the following example.
package demo;
import java.util.HashMap;
import java.util.Map;
public class App {
public static void main( final String[] args ) {
final Map<String, Integer> marksByName = new HashMap<>( 4 );
marksByName.put( "Aden", 82 );
marksByName.put( "Jade", 92 );
marksByName.put( "Peter", 74 );
marksByName.put( "Jane", 68 );
/* Update an existing entry */
marksByName.put( "Aden", 84 );
System.out.printf( "Marks: %s%n", a );
}
}
The order in which the items are returned is not guaranteed.
Marks: {John=91, Aden=72, Peter=74, Jane=68, Jade=92}
What’s the difference between the Hashtable
and HashMap
implementations?
These two Map
implementations are very similar.
Hashtable
(available since Java 1.0) came beforeHashMap
(added in Java 1.2)Hashtable
methods are synchronized, which means that only one thread can access each method at any point in time keeping the data within the map consistent. TheHashtable
, despite being thread-safe, is still susceptible to the check-then-act problem when multiple methods are used as one operation. TheHashMap
is not synchronized and thus cannot safely be used by multiple threads without additional safeguards.HashMap
supportsnull
s for both the key and the value.Hashtable
does not supportnull
s either as key or the value.Hashtable
does not supportnull
to mitigate the check-then-act problem. Theput()
method of theHashtable
will only returnnull
if the map does not contain an entry for the given key. This does not apply to theHashMap
as it can containnull
values.Hashtable
is slower when compared toHashMap
as synchronisation comes at a performance cost.
Is HashMap
the successor of the Hashtable
?
NO
HashMap
does not provide any concurrent safety and was never intended to. Concurrency adds complexity and slows things down. The HashMap
is ideal for situations where concurrency is not a requirement.
The ConcurrentHashMap
supersedes the Hashtable
as it provides a highly performant concurrent map.
LinkedHashMap
LinkedHashMap
is a HashMap
that also preserve the order in which entries are returned. Consider the following example.
package demo;
import java.util.LinkedHashMap;
import java.util.Map;
public class App {
public static void main( final String[] args ) {
final Map<String, Integer> a = new LinkedHashMap<>();
a.put( "Aden", 82 );
a.put( "Jade", 92 );
a.put( "Peter", 74 );
a.put( "Jane", 68 );
System.out.printf( "Marks: %s%n", a );
}
}
The entries will always be return in the same order these were added, as shown next.
Marks: {Aden=82, Jade=92, Peter=74, Jane=68}
LinkedHashMap
uses a doubly linked list to preserve the order in which the entries are added to the map.
TreeMap
TreeMap
is another Map
implementation that uses a tree data structure. The TreeMap
is based on the red–black self-balancing binary search tree implementation.
Similar to other map implementations, only the entry’s key take part in indexing and finding entries. In the above image, the keys are integers and the value can be any object.
The tree marks its nodes red or black, hence the name, and rebalances itself following an addition or deletion of elements, guaranteeing searches in O(log n)
time. This makes mutation more complex as the tree needs to be rebalanced every time elements are added or removed.
TreeMap
does not outperform the HashMap
when searching elements. In most case the `HashMap` finds elements faster than the TreeMap
.Consider the following example.
package demo;
import java.util.Map;
import java.util.TreeMap;
public class App {
public static void main( final String[] args ) {
final Map<String, Integer> marksByName = new TreeMap<>();
marksByName.put( "Aden", 82 );
marksByName.put( "Jade", 92 );
marksByName.put( "Peter", 74 );
marksByName.put( "Jane", 68 );
System.out.printf( "Marks: %s%n", marksByName );
}
}
In the above example, the TreeMap
stores the given entries in alphabetical order, using the entry’s key. We can control how entries are handled by the TreeMap
by providing a Comparator
instance, as shown next.
package demo;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
public class App {
public static void main( final String[] args ) {
final Map<String, Integer> marksByName = new TreeMap<>( Comparator.reverseOrder() );
marksByName.put( "Aden", 82 );
marksByName.put( "Jade", 92 );
marksByName.put( "Peter", 74 );
marksByName.put( "Jane", 68 );
System.out.printf( "Marks: %s%n", marksByName );
}
}
Different from the previous example, the map will return the elements in reverse order, as shown next.
Marks: {Peter=74, Jane=68, Jade=92, Aden=82}
The order in which the entries are sorted is governed by the provided Comparator
or by their natural ordering (the entry’s key implements Comparable
).
Note that adding entries to a TreeMap
which key does not support natural ordering (the key does not implement Comparable
) and without providing a Comparator
will throw a ClassCastException
at runtime.
ClassCastException
!! package demo;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Map;
import java.util.TreeMap;
public class App {
public static void main( final String[] args ) {
final Map<Student, Integer> marksByName = new TreeMap<>();
/* ⚠️ Throws ClassCastException!! */
marksByName.put( new Student( "Aden" ), 82 );
System.out.printf( "Marks: %s%n", marksByName );
}
}
@Data
@AllArgsConstructor
class Student {
private String name;
}
The Student
class does not implement the Comparable
interface, thus this type of object does not provide natural ordering. A Comparator
needs to be provided to the TreeMap
to be able to work with the Student
class, as shown in the following example.
package demo;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
public class App {
public static void main( final String[] args ) {
final Comparator<Student> comparator = Comparator.comparing( Student::getName );
final Map<Student, Integer> marksByName = new TreeMap<>( comparator );
/* ⚠️ Throws ClassCastException!! */
marksByName.put( new Student( "Aden" ), 82 );
System.out.printf( "Marks: %s%n", marksByName );
}
}
@Data
@AllArgsConstructor
class Student {
private String name;
}
We can work with any key, as long as we provide a Comparator
when the key being used do not implement Comparable
. The above will print.
Marks: {Student(name=Aden)=82}
The TreeMap
always store the elements sorted.
Can we store null
s?
Some map implementations accept null
s as both key and value while others do not, as shown in the following table.
Map | Allows null keys | Allows null values |
---|---|---|
Hashtable | NO | NO |
HashMap | YES | YES |
LinkedHashMap | YES | YES |
TreeMap | NO | YES |
Following are some basic example that tries to work with null
keys and values for each of the above implementations
Hashtable
(does not acceptnull
s)Null Key
⚠ The following example will compile but will throwNullPointerException
!!package demo; import java.util.Hashtable; import java.util.Map; public class App { public static void main( final String[] args ) { final Map<String, String> map = new Hashtable<>(); /* ⚠️ Throws NullPointerException!! */ map.put( "k", null ); System.out.printf( "Map %s%n", map ); } }
Null Value
⚠ The following example will compile but will throwNullPointerException
!!package demo; import java.util.Hashtable; import java.util.Map; public class App { public static void main( final String[] args ) { final Map<String, String> map = new Hashtable<>(); /* ⚠️ Throws NullPointerException!! */ map.put( null, "v" ); System.out.printf( "Map %s%n", map ); } }
Both examples will fail with a
NullPointerException
.HashMap
(acceptsnull
keys and values)package demo; import java.util.HashMap; import java.util.Map; public class App { public static void main( final String[] args ) { final Map<String, String> map = new HashMap<>(); map.put( null, "v" ); map.put( "k", null ); System.out.printf( "Map %s%n", map ); } }
HashMap
acceptsnull
keys and values. At most, there can be only onenull
key in a map, but there can be as manynull
values.LinkedHashMap
(acceptsnull
keys and values)package demo; import java.util.LinkedHashMap; import java.util.Map; public class App { public static void main( final String[] args ) { final Map<String, String> map = new LinkedHashMap<>(); map.put( null, "v" ); map.put( "k", null ); System.out.printf( "Map %s%n", map ); } }
LinkedHashMap
acceptsnull
keys and values. At most, there can be only onenull
key in a map, but there can be as manynull
values.TreeMap
(does not acceptnull
keys but acceptsnull
values)Null Key
⚠ The following example will compile but will throwNullPointerException
!!package demo; import java.util.Map; import java.util.TreeMap; public class App { public static void main( final String[] args ) { final Map<String, String> map = new TreeMap<>(); /* ⚠️ Throws NullPointerException!! */ map.put( null, "v" ); System.out.printf( "Map %s%n", map ); } }
TreeMap
does not work withnull
keys and aNullPointerException
will be thrown if we attempt to addnull
s.Exception in thread "main" java.lang.NullPointerException at java.base/java.util.TreeMap.compare(TreeMap.java:1291) at java.base/java.util.TreeMap.put(TreeMap.java:536) at demo.App.main(App.java:10)
Null Value
package demo; import java.util.Map; import java.util.TreeMap; public class App { public static void main( final String[] args ) { final Map<String, String> map = new TreeMap<>(); map.put( "k", null ); System.out.printf( "Map %s%n", map ); } }
TreeMap
can containnull
values and the above will print.Map {k=null}
Which Map to Use?
HashMap
is my first choice as it is very fast and can handle null
s. With that said, HashMap
consumes more space when compared to TreeMap
. LinkedHashMap
is a variant of HashMap
, where the entries’ order is preserved, at some extra space cost. The following table shows which map I prefer and a one sentence describing the motivation behind this decision.
Map | Motivation |
---|---|
HashMap | My default go-to map implementation |
LinkedHashMap | When I need to preserve the insertion order of the entries |
TreeMap | When ordering is important and no need to deal with null keys |
Hashtable | Never. I use ConcurrentHashMap instead when need to deal with concurrency |
Each map implementation is compared in more details next.
Performance
HashMap
performs faster thanTreeMap
. This comes to a surprise especially when searching element.Ordering
HashMap
provides no ordering guarantees.LinkedHashMap
preserves the order in which the entries are added whileTreemap
always contains the entries in an ordered manner (based on the entry’s key natural ordering or the providedComparator
).When an ordered map (a map of type
SortedMap
) is required, it is recommended to create aHashMap
and populate it with the entries first. Then create anTreeMap
from theHashMap
, as shown next.package demo; import java.util.HashMap; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; public class App { public static void main( final String[] args ) { final Map<String, Integer> temporary = new HashMap<>(); temporary.put( "Jade", 82 ); temporary.put( "Aden", 84 ); final SortedMap<String, Integer> ordered = new TreeMap<>( temporary ); System.out.printf( "Ordered: %s%n", ordered ); } }
This example takes advantage from the bulk population of the
TreeMap
and does not suffer the cost associated with the rebalancing with every entry addition.null
supportTreeMap
does not supportnull
keys, but supportnull
values.HashMap
andLinkedHashMap
supportnull
keys and values.ⓘ NoteThere can be at most onenull
key in a map.Comparison
The
HashMap
andLinkedHashMap
use the entry’s keyhashCode()
method to determine which bucket to use and the entry’s keyequals()
method to compare between entries within the same bucket.The
TreeMap
relies on the entry’s keycompareTo()
method for same purpose.The relation between the collections and the elements which they contain is discussed in more depth in the relation to objects section.
Concurrency
Only the
Hashtable
provide thread-safety. TheHashMap
,LinkedHashMap
and theTreeMap
are not thread-safe and provide no thread-safety.As mentioned before, prefer the
ConcurrentHashMap
over theHashtable
when dealing with concurrent situations.
Map keys MUST BE immutable
Modifying the entry’s key after adding them to the map may break the map. Consider the following example.
package demo;
import java.awt.*;
import java.util.HashMap;
import java.util.Map;
public class App {
public static void main( final String[] args ) {
final Point point = new Point( 1, 1 );
final Map<Point, String> points = new HashMap<>( 3 );
points.put( point, "Lower left corner" );
System.out.println( "-- Before modifying the key ----" );
System.out.printf( "The map contains %d points%n", points.size() );
System.out.printf( "Is point %s in map? %s%n", point, points.containsKey( point ) );
/* Modify the key */
point.y = 10;
System.out.println( "-- After modifying the key -----" );
System.out.printf( "The map contains %d points%n", points.size() );
System.out.printf( "Is point %s in map? %s%n", point, points.containsKey( point ) );
System.out.printf( "The map contains: %s%n", points );
}
}
The Point
class is mutable and thus not suitable to be used as a key in any Map
. Modifying the point’s state, as shown above example, will break the map. In the above example, the map is not able to locate the same key object after it is modified.
-- Before modifying the key ----
The map contains 1 points
Is point java.awt.Point[x=1,y=1] in map? true
-- After modifying the key -----
The map contains 1 points
Is point java.awt.Point[x=1,y=10] in map? false
The map contains: {java.awt.Point[x=1,y=10]=Lower left corner}
The strangest thing when debugging such problems is that the map seems to contain this key, as printed in the last line from the above output. The issue here happened because the element now belongs to a different bucket and that’s why the map is not able to find it.
How can we modify keys that are contained within a map?
If a key within a map is mutable and needs to be updated, then it should first be removed from the map, updated and then added back to the map, as shown in the following example.
package demo;
import java.awt.Point;
import java.util.HashMap;
import java.util.Map;
public class App {
public static void main( final String[] args ) {
final Point point = new Point( 1, 1 );
final Map<Point, String> points = new HashMap<>( 3 );
points.put( point, "Lower left corner" );
/* Remove, update and put back */
final String description = points.remove( point );
point.y = 10;
points.put( point, description );
System.out.printf( "The map contains %d points%n", points.size() );
System.out.printf( "Is point %s in map? %s%n", point, points.containsKey( point ) );
}
}
The order in which these three operations happen is quite important as if we update the entry’s key before removing it, the remove may not remove the element and then end up with two instances of the same object in the same map.
Mutable objects are not good candidates as map keys.
Double brace initialization
Consider the following example.
package demo;
import java.util.HashMap;
import java.util.Map;
public class App {
public static void main( final String[] args ) {
final Map<String, String> a = new HashMap<>() { {
put( "a", "A" );
put( "b", "B" );
put( "c", "C" );
} };
System.out.printf( "Map %s%n", a );
}
}
The above example makes use of double brace initialization. An inner anonymous class is created and the init block is used to add (put) the entries to the map. The above example is similar to the following.
package demo;
import java.util.HashMap;
public class MyStringMap extends HashMap<String, String> {
/* Initialisation block */
{
put( "a", "A" );
put( "b", "B" );
put( "c", "C" );
}
}
I’ve never used this pattern and prefer other constructs instead, such as Map.of()
, Guava Maps.newHashMap() method. I’ve added this example here as you may encounter this in code.
Mutable and immutable maps
Immutable (also referred to as unmodifiable) maps cannot be modified, while mutable (also referred to as modifiable) maps can be modified. Consider the following example.
UnsupportedOperationException
!! package demo;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class App {
public static void main( final String[] args ) {
final Map<String, String> modifiable = new HashMap<>( 3 );
modifiable.put( "a", "A" );
modifiable.put( "b", "B" );
modifiable.put( "c", "C" );
final Map<String, String> unmodifiable = Collections.unmodifiableMap( modifiable );
/* ⚠️ Throws UnsupportedOperationException!! */
unmodifiable.put( "d", "D" );
}
}
Changing the unmodifiable map will throw an UnsupportedOperationException
.
Exception in thread "main" java.lang.UnsupportedOperationException
at java.base/java.util.Collections$UnmodifiableMap.put(Collections.java:1473)
at demo.App.main(App.java:17)
Changes to the underlying map will also affect the immutable map
package demo;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class App {
public static void main( final String[] args ) {
final Map<String, String> modifiable = new HashMap<>( 3 );
modifiable.put( "a", "A" );
modifiable.put( "b", "B" );
modifiable.put( "c", "C" );
final Map<String, String> unmodifiable = Collections.unmodifiableMap( modifiable );
/* The immutable map will be modified too */
modifiable.put( "d", "D" );
System.out.printf( "Map: %s%n", unmodifiable );
}
}
The unmodifiable map uses the given map as its underlying data structure. Therefore, any changes to the underlying data structure will affect the unmodifiable map too, as shown next.
Map: {a=A, b=B, c=C, d=D}
Consider the following class.
package demo;
import java.util.Collections;
import java.util.Map;
public class Data {
private final Map<String, Integer> sample;
public Data( final Map<String, Integer> sample ) {
this.sample = Collections.unmodifiableMap( sample );
}
public Map<String, Integer> getSample() {
return sample;
}
@Override
public String toString() {
return String.format( "Data: %s", sample );
}
}
The Data
class contains an unmodifiable map, named sample
. We cannot add or remove data to/from the sample
map. Consider the following example.
UnsupportedOperationException
!! package demo;
import java.util.HashMap;
import java.util.Map;
public class App {
public static void main( final String[] args ) {
final Map<String, Integer> source = new HashMap<>( 3 );
source.put( "a", 7 );
source.put( "b", 4 );
source.put( "c", 11 );
final Data data = new Data( source );
/* ⚠️ Throws UnsupportedOperationException!! */
data.getSample().put( "d", 6 );
}
}
The above example compiles and fails whenever we try to modify the sample
map through the enclosing Data
class.
Exception in thread "main" java.lang.UnsupportedOperationException
at java.base/java.util.Collections$UnmodifiableMap.put(Collections.java:1473)
at demo.App.main(App.java:16)
The above example may give you the wrong impression that the sample map is immutable. Consider the following example.
package demo;
import java.util.HashMap;
import java.util.Map;
public class App {
public static void main( final String[] args ) {
final Map<String, Integer> source = new HashMap<>( 3 );
source.put( "a", 7 );
source.put( "b", 4 );
source.put( "c", 11 );
final Data data = new Data( source );
/* Modify the source */
source.put( "d", 6 );
/* The data is changed too as a side effect */
System.out.println( data );
}
}
The above example is modifying the map through the source
variable, which happens to be the underlying data structured of the immutable map, sample
. We are still able to modify the sample by modifying the underlying map.
Data: {a=7, b=4, c=11, d=6}
Defensive copying is a technique which mitigates the negative effects caused by unintentional (or intentional) modifications of shared objects. Instead of sharing the reference to the original map, we create a new map and use the reference to the newly created copy instead. Thus, any modification made to the source will not affect our map.
To address this problem, we need to change the following line
this.sample = Collections.unmodifiableMap( sample );
with (if you are working with Java 9 or above)
this.sample = Map.copyOf( sample );
or (if you are working with Java 8 or you need to handle null
s)
this.sample = Collections.unmodifiableSet( new HashMap<>( sample ) );
There are at least two ways to solve this problem, both options will achieve the same thing.
package demo;
import java.util.Map;
public class Data {
private final Map<String, Integer> sample;
public Data( final Map<String, Integer> sample ) {
/**/this.sample = Map.copyOf( sample );
}
public Map<String, Integer> getSample() {
return sample;
}
@Override
public String toString() {
return String.format( "Data: %s", sample );
}
}
Any changes made to the source map, will not affect our map. The above example is truly immutable.