Initialize a HashMap in Java - Best practices

Updated:

The official documentation for HashMap is here https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/HashMap.html

Initialization

Initialization using the put() Method

One of the simplest ways to initialize a HashMap is to use the put() method. The put() method allows you to add key-value pairs to the HashMap.
You can add multiple key-value pairs to the HashMap by calling the put() method multiple times.

HashMap<String, Integer> map = new HashMap<>(); 
map.put("Apple", 1); 
map.put("Banana", 2); 
map.put("Cherry", 3); 

In the example above, we are creating a HashMap with String as the key type and Integer as the value type.
We are then using the put() method to add three key-value pairs to the map.

Initialization using the Constructor

Another way to initialize a HashMap is to use the HashMap constructor.
The HashMap constructor takes another Map as a parameter and creates a new HashMap that contains the same key-value pairs as the original Map.

HashMap<String, Integer> map1 = new HashMap<>(); 
map1.put("Apple", 1); 
map1.put("Banana", 2); 
 
HashMap<String, Integer> map2 = new HashMap<>(map1); 

In the example above, we are creating two HashMaps. The first HashMap, map1, is created using the put() method.

The second HashMap, map2, is created using the constructor, and it contains the same key-value pairs as map1.

Initialization using asList()

The asList() method is a static method of the Arrays class in Java that returns a List.

You can use this method to create a List of key-value pairs and then pass the List to the HashMap constructor to initialize the HashMap.

HashMap<String, Integer> map = new HashMap<>( 
  Arrays.asList( 
    new AbstractMap.SimpleEntry<>("Apple", 1), 
    new AbstractMap.SimpleEntry<>("Banana", 2), 
    new AbstractMap.SimpleEntry<>("Cherry", 3) 
  ) 
); 

In the example above, we are using the asList() method to create a List of key-value pairs.

We are then passing the List to the HashMap constructor to initialize the HashMap.

Initialization using of()

The of() method is a static method of the Map class in Java that returns a Map. This allow you to initialize a Map inline.

You can use this method to create a Map of key-value pairs and then assign the Map to a HashMap reference.

In the example below, we are using the of() method to create a Map of key-value pairs.

We are then assigning the Map to a HashMap reference

HashMap<String, Integer> map = new HashMap<>( 
  Map.of( 
    "Apple", 1, 
    "Banana", 2, 
    "Cherry", 3 
  ) 
); 

Create an unmodifiable Map using a factory method

Since Java 9 (so you are excused if you were using Java 8 until recently) there are some factory methods present in the Map class. Map.of, Map.ofEntries and Map.copyOf create unmodifiable maps. In the documentation you can find the characteristics of these maps.

The goal of these methods is to 'reduce the verbosity' of Java when immutable collections are created. You can read about the goals in the JEP 269.

Examples and use cases

Empty unmodifiable Map

We saw already how to use the .of() for the initialisation.

This method can be useful if you have to return an empty map:

Map emptyMap = Map.of(); 

In case someone tries to modify the object:

emptyMap.put(1, "test") 
|  Warning: 
|  unchecked call to put(K,V) as a member of the raw type java.util.Map 
|  emptyMap.put(1, "test") 
|  ^----------------^ 
|  Exception java.lang.UnsupportedOperationException 
|        at ImmutableCollections.uoe (ImmutableCollections.java:142) 
|        at ImmutableCollections$AbstractImmutableMap.put (ImmutableCollections.java:1072) 
|        at (#2:1) 

Unmodifiable Map initialization with Map.of

Here the example if you have to add some elements:

Map testMap = Map.of(1, "One", 2, "Two"); 

Java will generate a Map like this: testMap ==> {2=Two, 1=One}

As you can quickly see this solution has some major limitations, the initialization of the method is developer friendly for dynamic values and the number of entries is limited to 10. If you try to create a Map with 11 entries you will receive this error: (cannot infer type-variable(s) K,V (actual and formal argument lists differ in length)) .

The next solution is more flexible.

Unmodifiable Map initialization with Map.ofEntries

The JavaDoc contains an example:

import static java.util.Map.entry; 
 
Map<Integer,String> map = Map.ofEntries( 
    entry(1, "a"), 
    entry(2, "b"), 
    entry(3, "c"), 
    ... 
    entry(26, "z")); 

Don’t worry the signature of the method doesn’t limit to 26 entries. It can receive an array of Entry.

Create and initialize HashMap inline in a lambda stream using .toMap()

The Collector class offers us the .toMap() method that generates a Map from a Stream. Here the official documentation.

As described in the JavaDoc:

* Returns a {@code Collector} that accumulates elements into a 
     * {@code Map} whose keys and values are the result of applying the provided 
     * mapping functions to the input elements. 

The JavaDoc offers us an example:

Map<String, Student> studentIdToStudent 
    = students.stream().collect( 
    toMap(Student::getId, 
    Function.identity())); 
} 

Here a more generic example that uses an array and can run in your IDE:

Map<Integer, String> mapGenerated = Stream.of(new Object[][] { 
     {1, "First Name"}, 
     {2, "City"} 
 }).collect(Collectors.toMap(mapper -> (Integer) mapper[0], 
                             mapper -> (String) mapper[1])); 

A more complex use case are the MessageHeaders in Spring Integration. The MessageHeaders object requires an HashMap as constructor parameter.

@Override 
public List<Message> handleFileRequest() { 
// we create a list from an enumerator 
List<Message> messageList = Arrays.stream(FileType.values()) 
// we read the file content from an external provider 
.map(externalProviderService::readFileFromExternalProvider) 
// for each object we create a new message 
.map(file -> MessageBuilder.createMessage(file, 
new MessageHeaders(new HashMap<String, Object>() { { 
put("fileName", file.getTitle()); 
} }))) 
.collect(Collectors.toList()); 
   
return messageList; 
} 

In detail:

new HashMap<String, Object>() { {put("valueText", Object} } 

The first brace ({ }) creates an Anonymous Inner Class, the second brace initializes a block inside the Anonymous Inner Class.

For more demanding implementation look at the documentation, there are other methods and different signatures in the Collectors class: toUnmodifiableMap, toConcurrentMap

The ‘old’ classic method

For more classical implementations you can initialize an HashMap in to steps, this implementation should be accessible to most of the developers:

Map<Integer, String> myClassicMap = new HashMap<Integer, String>(); 
myClassicMap.put(1, "one"); 
myClassicMap.put(2, "two"); 

Best practices: No argument constructor ...

In Java is a good practice to initialize the initial capacity of collections and maps.

Many developers (me included) have the habit to declare a new Collection or Map using the no-argument constructor, e.g.:

Map exampleMap = new HashMap();

With this instruction, Java initializes a new HashMap object with the attribute loadFactor at 0.75 and the DEFAULT_INITIAL_CAPACITY at 16.

The HashMap stores internally his values in an array of HashMap$Node objects (at least until when the size doesn't become too big).

The initialization doesn't create yet the array, it will be instantiated only with the first insert in the map (e.g. using put),

Java will create the internal array with something like: Node<K,V>[] tab = (Node<K,V>[])new Node[16].

... it will grow

Every time an entry is added into the Map, the HashMap instance checks that the number of values contained in the bucket array is not more than his capacity multiplied the load factor (default at 0.75).

In our case : 16 (capacity) * 0.75 (load factor) = 12 (threshold).

What happens when the 13th value is inserted in the array? The number of entries in the array is more than the threshold and the HashMap instance calls the method: final Node<K,V>[] resize().

This method creates a new array of Node with a capacity of the current store (16) * 2:

(Node<K,V>[])new Node[32]

The values of the current bucket array are 'transferred' in the new array, the new threshold is also multiplied * 2.

The table shows how the size of the bucket array grows adding new entries.

The rehashing is done in resize() requires computational power and should be avoided if possible.

of inserts .put(K,V)resize() callsbucket array sizeThreshold
00null0
111612
1323224
2536448
49412896
975256192
1936512384
38571 024768
76982 0481 536
1 53794 0963 072
............
98 30515262 144196 608
............

defining the initial size in the constructor

You have a defined number of entries or you know what should be the number of values that the map will contain, then it's recommended to set the 'initial capacity' accordingly.

Example you will have 100 entries and not one more? Is new HashMap(100) the optimum size for your map?

Unfortunately, no.

If the initial threshold is calculated using the following algorithm:

/** 
* Returns a power of two size for the given target capacity. 
*/ 
static final int tableSizeFor(int cap) { 
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1); 
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; 
} 

... the result is 128.

When you start to insert elements in your Map, HashMap resizes the Map and recalculates the threshold: threshold (128) * DEFAULT_LOAD_FACTOR (0.75) = new threshold (96) .

With a threshold of 96 and the Map will be re-hashed when you insert the 97th element.

If you want to optimize the size of the HashMap you can specify the load factor in the initialization:

new HashMap(100, 1f)

This will create a new HashMap with a threshold of 128, after the initialization, the threshold will be still at 128 (128 * 1 = 128).

To have a threshold of 100 you need a factor of 0.78125 (new HashMap(100, 0.78125f). A less suited alternative to avoid the re-hashing is to initialize the Map with a size of 129: new HashMap(129). This would generate a table with a threshold of 192 (256*0.75);

General tip from the code source

The expected number of entries in the map and its load factor should be taken into account when

setting its initial capacity, so as to minimize the number of rehash operations.

If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.


WebApp built by Marco using SpringBoot 3.2.4 and Java 21. Hosted in Switzerland (GE8).