Initialize a HashMap in Java - Best practices


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

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

Empty unmodifiable Map

This 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, Java 17, Mustache, Markdown and in Azure