Java HashMap Initialization: Load Factor and Initial Capacity

Java HashMap class provides key-value pairs storage with constant-time performance for get and put operations. Java allows to initialize a HashMap in different ways and each serves a specific purpose.

This Java Collections tutorial explores various initialization techniques for HashMap, including empty maps, pre-populated maps, immutable maps, and collecting Stream items to maps.

// 1. Empty maps
HashMap<String, String> map1 = new HashMap<>();
HashMap<String, String> map2 = new HashMap<>(10);
HashMap<String, String> map3 = HashMap.newHashMap(10);

// 2. Pre-populated maps
HashMap<String, String> map4 = new HashMap<>(
    Map.ofEntries(
        Map.entry("key1", "value1"),
        Map.entry("key2", "value3"))
);

Map<String, String> map5  = new HashMap<>() {{
  put("key1", "value1");
  put("key2", "value2");
}};

HashMap<String, String> map6 = new HashMap<>(
    Map.of("key1", "value1", "key2", "value3")
);

// 3. Collecting Stream to Map
// More examples: https://howtodoinjava.com/java8/collect-stream-to-map/
Stream<Item> strem = ...;
Map<Long, String> mapWithValue = stream.collect(Collectors.toMap(Item::id, Item::name));

1. Quick Overview of Load Capacity and Load Factor

When we look into the internal working of HashMap class, we see two important parameters it has i.e. ‘initialCapacity‘ and ‘loadFactor‘.

  • The initial capacity represents the number of buckets that the HashMap can accommodate when it is first created. It determines the size of the internal array used to store key-value pairs. By default, the initial capacity of a HashMap is 16.
  • When we keep adding the key-value pairs, the load factor determines how full the HashTable can be before it needs to be resized. In HashMap class, the default load factor is 0.75.

When the number of elements in the HashMap exceeds the product of the load factor and the current capacity, the HashMap is resized to accommodate more elements. During resizing, JVM creates a new internal array with a larger capacity (calculated) and stores all existing key-value pairs in the new array.

This resize operation is expensive in terms of time and resources. This is why it is recommended to specify an approximate number of entries, the map will store. This enables JVM to allocate a sufficiently large array beforehand and thus avoid the resize operation.

2. Initialize an Empty HashMap

An empty map does not contain key-value pairs. It is initialized without any entries.

The best way to create an empty HashMap is by using its default constructor. It initializes an instance of HashMap with an initial capacity of 16, and a load factor of 0.75.

HashMap<String, String> map = new HashMap<>();

Alternatively, we can specify the initial capacity in the constructor. This is useful when we know the approximate number of elements the map will hold. It can improve the performance, in runtime, by reducing the number of resize operations.

// Initial capacity is 10, but resizing triggers on storing the 8th element.
HashMap<String, String> map = new HashMap<>(10);  

A similar method newHashMap() has been introduced in Java 19, which calculates the approximate initial capacity by utilizing the method input. This is a ‘static‘ method so we do need to invoke the constructor.

// Initial capacity is 14 and resizing triggers on storing the 11th element
HashMap<String, String> map = HashMap.newHashMap(10);

3. Difference between ‘new HashMap()‘ Constructor and ‘newHashMap()‘ Method

Both methods look the same, so what’s the difference?

The major difference between ‘new HashMap(capacity)’ constructor and ‘newHashMap(capacity)’ method is in the role of load factor. Suppose, you want to create an ArrayList of 180 elements, you could use the constructor ‘new HashMap(180)‘ and hope that it will store 180 pairs without resizing. But it does not happen. In reality, due to the default load factor of 0.75, resizing is triggered as soon as it is 75% filled (180 × 0.75 = 135 pairs). Ouch!!

To fix this behavior, the new factory method newHashMap recalculates the capacity by dividing the number of required pairs by the load factor. For example, when we use ‘newHashMap(180)‘, it creates the Map with the initial capacity of 240 (180 ÷ 0.75 = 240). Thus the resizing will not trigger until we store then 180 elements. [Read More]

After the HashMap has been initialized, we can add and remove entries from the map.

map.put("key1", "value1");
map.put("key2", "value2");

String value1 = map.get("key1");
String value2 = map.get("key2");

4. Initialize HashMap with Key-Value Pairs

Several times, we already have the key-value pairs that have to be stored in the HashMap. In this case, it makes sense to use the corresponding constructor for initializing and populating the pairs in one statement.

The following constructor takes an existing Map and copies all of its entries into a new HashMap object.

HashMap<String, String> map = new HashMap<>(
  Map.ofEntries(
    Map.entry("key1", "value1"),
    Map.entry("key2", "value3"))
);

Alternatively, we can use a similar approach using the Map.of() methods which allows us to create an immutable Map with a maximum of 10 key-value pairs.

HashMap<String, String> map = new HashMap<>(
    Map.of("key1", "value1", "key2", "value3")
);

A similar syntax is using the double-brace initialization which is not recommended. In this approach, an anonymous subclass of HashMap is created which holds a reference to the enclosing instance of the outer class. This extra instance may lead to a memory leak if the outer map object is not garbage collected.

Map<String, String> map  = new HashMap<>() {{
  put("key1", "value1");
  put("key2", "value2");
}}

5. Collecting Stream Elements into HashMap

Sometimes, we may have the key-value pairs beforehand. Rather we will get the pairs from a dynamic source such as Stream. The Stream API provides a convenient API to process and store all stream elements in a Map.

Stream<Item> stream = Stream.of(

  new Item(1, "Item1"),
  new Item(2, "Item2"),
  new Item(3, "Item3")
);

Map<Long, String> mapWithValue = stream
	.collect(Collectors.toMap(Item::id, Item::name));

The above Map instance is immutable. We cannot add more key-value pairs to it. If we want an instance of HashMap, so we can modify it later, we can do as follows:

Map<Long, String> mapWithValue = stream
	.collect(Collectors.toMap(Item::id, Item::name, (old, new) -> old, HashMap::new));

This is very important to know beforehand if the Stream elements will have a distinct value for the map key field or not. If map keys are duplicates and we use Collectors.toMap() method, we will get the IllegalStateException.

If duplicate keys are present in the Stream, then we can use Collectors.groupingBy() to collect the stream elements.

Stream<Item> streamWithDuplicates = Stream.of(

  new Item(1, "Item1"),
  new Item(2, "Item2"),
  new Item(3, "Item3-1"),
  new Item(3, "Item3-2"),
  new Item(3, "Item3-3")
);

Map<Long, List<Item>> mapWithList = streamWithDuplicates
	.collect(Collectors.groupingBy(Item::id));

6. Summary

In this Java tutorial, we learned to initialize a HashMap object in different ways such as empty map, pre-polated map and even collecting stream elements into map. We learned these methods with examples and understood when we can use these methods based on requirements.

Happy Learning !!

Source Code on Github

Comments

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.