Java HashMap Initialization: Syntax and Pitfalls
Learn how to initialize a HashMap in Java, including constructors, capacity and load factor, copy constructors, Map.of, and common pitfalls.
When you write new HashMap<>() in Java, you get an empty map configured with a default initial capacity of 16 and a default load factor of 0.75. That is the most common way to initialize a HashMap, but it is not always the best choice. The constructor you pick affects future resizing, memory usage, and readability. This article walks through the available initialization options, explains what happens underneath, and points out mistakes that can cause subtle bugs.
The Default Constructor and Its Behavior
The no-argument constructor is the simplest way to create a map:
Map<String, Integer> scores = new HashMap<>();
This configures the map with an initial table capacity of 16 and a load factor of 0.75. The backing table is not allocated until the first put call, but once the map is populated the defaults work as follows: the map resizes when inserting another entry would make the size exceed capacity * loadFactor, or 12 entries for the default capacity. In other words, the 13th entry triggers a resize. Resizing rebuilds the internal table and rehashes every entry, which costs time and temporarily uses extra memory.
For small maps up to about a dozen entries, the default constructor is fine. If you know the map will hold thousands of entries, starting with 16 buckets forces several resizes before the map reaches its final size. Each resize is an O(n) operation, so the cumulative cost can become visible in performance-sensitive code.
Setting Initial Capacity and Load Factor
The HashMap(int initialCapacity) constructor lets you request an initial table size:
Map<String, Integer> scores = new HashMap<>(1000);
HashMap rounds the requested capacity up to a power of two, and the load factor determines when the table grows. With the default 0.75 load factor, new HashMap<>(1000) will use a 1024-bucket table when first populated and will resize once the map exceeds 768 entries. If you expect to insert 1000 entries and want to avoid a resize, pass a larger initial capacity, such as 1334 or 2000.
You can also set the load factor explicitly:
Map<String, Integer> scores = new HashMap<>(1000, 0.8f);
A higher load factor (for example, 0.8 or 0.9) can reduce memory usage but usually increases the chance of hash collisions, which can slow down lookups. A lower load factor (for example, 0.5) can speed up lookups but uses more memory. The default 0.75 is a reasonable tradeoff for most applications. Only change it after measuring a specific bottleneck.
A common mistake is to pass the exact number of entries you expect as the initial capacity. Because HashMap rounds the requested capacity up to a power of two, new HashMap<>(100) actually uses 128 buckets and resizes on the 97th insertion (the threshold is 96), not at 75 entries. To avoid resizing while inserting N entries, use (N / loadFactor) + 1 as the initial capacity; for N = 100 and the default load factor, that is (int)(100 / 0.75) + 1 = 134. Using N * 2 is a simpler margin that also avoids resizing.
Initializing with a Map Using the Copy Constructor
If you already have a map and want a new independent copy, use the copy constructor:
Map<String, Integer> original = new HashMap<>(); original.put("alice", 90); original.put("bob", 85); Map<String, Integer> copy = new HashMap<>(original);
The copy constructor creates a new map with the same mappings. The new map has its own internal table, so changes to copy do not affect original. This is useful when you need to modify a map without changing the source, or when you receive a map from a library and want to avoid unintended side effects.
Note that the copy constructor does not copy the load factor or the original capacity. It uses the default load factor and chooses a capacity based on the source map's size. If the source map is large, the new map will be sized appropriately, but you cannot control the exact capacity through this constructor.
Creating a HashMap with a Fixed Set of Entries
For a small, static set of key-value pairs, Java 9 introduced Map.of():
Map<String, Integer> scores = new HashMap<>(Map.of("alice", 90, "bob", 85));
Map.of() returns an immutable map with the given entries. Wrapping it in a HashMap creates a mutable copy. This is concise and avoids the boilerplate of multiple put calls.
There are a few limitations. Map.of() accepts at most 10 key-value pairs. For more entries, use Map.ofEntries():
Map<String, Integer> scores = new HashMap<>(Map.ofEntries( Map.entry("alice", 90), Map.entry("bob", 85), Map.entry("carol", 78) ));
Map.of() and Map.ofEntries() do not allow null keys or null values. If your data may contain nulls, you cannot use this approach. Also, the immutable map returned by these methods has an unspecified iteration order, and HashMap itself does not guarantee order, so do not rely on the entry order after copying.
Avoiding Double Brace Initialization
A pattern you may see in older code is double brace initialization:
Map<String, Integer> scores = new HashMap<>() {{ put("alice", 90); put("bob", 85); }};
The outer braces create an anonymous subclass of HashMap, and the inner braces are an instance initializer that runs the put calls. This works, but it has serious downsides:
- The expression compiles to an extra anonymous class, and each execution creates an instance of that subclass.
- In a non-static context, the anonymous subclass also keeps a reference to the enclosing object, which can cause a memory leak if the map outlives that object.
- It is harder to read and debug than a simple sequence of
putcalls.
Use Map.of() or a static initializer block instead. If you need a mutable map with a fixed set of entries, the Map.of() copy approach is cleaner and safer.
Performance and Memory Considerations During Initialization
The most important performance factor during initialization is the number of resizes. Each resize allocates a new backing array and rehashes every existing entry. A map that grows from 16 to 1024 buckets passes through several intermediate resizes; sizing it closer to the final size avoids that repeated work.
If you know the approximate number of entries, set the initial capacity accordingly. This is especially relevant when you are building a map from a large dataset, such as reading a file or processing a database result set. Pre-sizing the map can reduce initialization time by a noticeable margin.
Memory usage is also affected by the load factor. A lower load factor means more empty buckets, which increases the memory footprint of the map. For maps that hold many entries, the difference between 0.75 and 0.5 can be significant. Measure your actual memory usage before tuning these parameters.
Another subtle point: the HashMap constructor does not allocate the bucket array until the first put call. So new HashMap<>(1000) does not immediately consume memory for 1000 buckets. The requested capacity is stored as an initial table-size threshold, and the array is created lazily. Pre-sizing a map that never receives entries therefore does not waste memory.
Thread Safety and Initialization Patterns
HashMap is not thread-safe. If multiple threads access the same map concurrently, and at least one thread modifies it, you must synchronize access. This applies during initialization as well. If you build a map in one thread and publish it to other threads for read-only use, prefer publishing an immutable map, such as the result of Map.of(). If updates can happen after publication, use a thread-safe map.
A common pattern is to construct a map and then wrap it in an unmodifiable view:
Map<String, Integer> scores = Collections.unmodifiableMap(new HashMap<>(Map.of("alice", 90)));
This prevents callers from modifying the exposed map, but the wrapper does not make the underlying HashMap thread-safe. If you keep the original mutable reference and update it, readers can still see inconsistent state. For concurrent read and write access, use ConcurrentHashMap instead:
Map<String, Integer> scores = new ConcurrentHashMap<>();
ConcurrentHashMap does not allow null keys or null values, and its initial-capacity constructor is useful when you know the expected size. For most concurrent read/write scenarios, ConcurrentHashMap is the right choice.
Choosing the Right Initialization Approach
The following table summarizes the common initialization options and their typical use cases:
| Approach | Best For | Limitations |
|---|---|---|
new HashMap<>() | Small maps, unknown size | Resizes often for large maps |
new HashMap<>(capacity) | Known size, performance-sensitive code | Must account for load factor and power-of-two rounding |
new HashMap<>(otherMap) | Copying an existing map | Cannot set capacity directly |
new HashMap<>(Map.of(...)) | Small static entry sets | Max 10 pairs, no nulls |
new HashMap<>(Map.ofEntries(...)) | Larger static entry sets | No nulls, verbose |
new ConcurrentHashMap<>() | Concurrent access | No null keys/values |
For most application code, the default constructor is fine. When you know the map will grow large, pre-size it. When you need a fixed set of entries, use Map.of() or Map.ofEntries() and copy into a HashMap if mutability is required. Avoid double brace initialization entirely, and always consider thread safety before sharing the map across threads.
One final detail: if you are initializing a map from a stream, you can use Collectors.toMap() to avoid an intermediate map. This is a different initialization path worth knowing, but it requires a stream source and careful handling of duplicate keys. For direct initialization, the constructors and Map.of() methods cover most real-world needs.