Collections ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-10-24

TreeMap, SortedMap and NavigableMap in Java

Swap new HashMap<>() for new TreeMap<>() to get keys printed in alphabetical order, and code that has worked for years suddenly fails on map.put(null, 0) with a NullPointerException. A HashMap simply puts the null key into bucket 0, while a TreeMap has to compare every key with compareTo(), and null cannot be compared. That is not the only difference: a sorted map has its own rules, its own exceptions and a set of search methods that HashMap does not have at all.

TreeMap is a Map implementation backed by a red-black tree that keeps key–value pairs sorted by key: in natural ordering (Comparable) or by a Comparator you pass in. TreeMap implements SortedMap (Java 1.2: first and last key, key ranges) and NavigableMap (Java 6: nearest-key lookups — floorKey(), ceilingKey(), lowerKey(), higherKey()). The get(), put(), remove() and containsKey() operations run in O(log n).

The TreeMap class: how it works and how to create one

A TreeMap in Java stores its entries in the nodes of a red-black tree — a self-balancing binary search tree whose height always stays around log n. That is why lookup, insertion and removal by key take O(log n), and iteration always goes in ascending key order, no matter in which order the entries were added.

Keys are compared not with equals() and hashCode(), but with compareTo() or the comparator's compare(): if the comparison returns 0, the keys are treated as the same key. The hierarchy looks like this:

Map<K,V>
└── SortedMap<K,V>        (Java 1.2; extends SequencedMap since Java 21)
    └── NavigableMap<K,V>  (Java 6)
        ├── TreeMap<K,V>
        └── ConcurrentSkipListMap<K,V>  (thread-safe)

The basic put(), get() and entrySet() methods and the ways to iterate are shared by all maps — they are covered in Map Interface in Java. Example: salaries added in random order come out of a TreeMap sorted by last name:

import java.util.Map;
import java.util.TreeMap;

public class TreeMapExample {
    public static void main(String[] args) {
        Map<String, Double> salaries = new TreeMap<>();

        salaries.put("Smith", 123.22);
        salaries.put("Taylor", 1378.00);
        salaries.put("Brown", 3434.34);

        salaries.forEach((key, value) -> System.out.println(key + ": " + value));
    }
}

Output:

Brown: 3434.34
Smith: 123.22
Taylor: 1378.0

Strings are compared by character code, so all uppercase letters come before lowercase ones: a key "adams" would be printed after "Taylor" (details in How compareTo compares strings).

TreeMap constructors

TreeMap has four constructors: an empty map with natural ordering, an empty map with a comparator, and a copy of another map — a plain one or a sorted one.

Constructor What it creates Edge case
TreeMap() Empty map, keys in natural ordering (Comparable) A key without Comparable — ClassCastException on the first put()
TreeMap(Comparator<? super K> comparator) Empty map ordered by the comparator Passing null as the comparator means natural ordering
TreeMap(Map<? extends K, ? extends V> m) A copy of any map, keys sorted in natural ordering The source map's comparator is not inherited, even if a TreeMap is passed through a variable of type Map
TreeMap(SortedMap<K, ? extends V> m) A copy of a sorted map with the same comparator Built in O(n): the data is already sorted

TreeMap with a Comparator

To sort a TreeMap in an order other than the natural one, pass a Comparator to the constructor: for example Comparator.reverseOrder() for descending keys or String.CASE_INSENSITIVE_ORDER for strings that ignore case. The comparator decides not only the iteration order but also which keys the map considers the same.

NavigableMap<Integer, String> desc = new TreeMap<>(Comparator.reverseOrder());
desc.put(1, "a");
desc.put(5, "e");
desc.put(3, "c");
System.out.println(desc);            // Output: {5=e, 3=c, 1=a}
System.out.println(desc.firstKey()); // Output: 5 - "first" according to the comparator

What does this code print?

import java.util.TreeMap;

public class CaseInsensitiveKeys {
    public static void main(String[] args) {
        TreeMap<String, Integer> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        map.put("Java", 1);
        map.put("JAVA", 2);
        map.put("python", 3);

        System.out.println(map);
        System.out.println(map.get("jAvA"));
        System.out.println(map.containsKey("PYTHON"));
    }
}

Output:

{Java=2, python=3}
2
true

The comparator returned 0 for “Java” and “JAVA”, so the second put() did not add a new entry — it replaced the value of the existing one. The key itself stayed “Java”, not “JAVA”. For dictionaries, logins and HTTP headers that is exactly what you want; for data where case matters it is a way to lose entries silently. Why this happens and how to write a comparator that is consistent with equals() is explained in Comparable and Comparator.

The SortedMap interface and its methods

SortedMap is the interface of a sorted map: it guarantees iteration in ascending key order and adds to Map methods for the first and last key and for cutting out a key range (headMap(), subMap(), tailMap()). The lower bound of a range is included, the upper bound is not.

Method What it returns Edge case
Comparator<? super K> comparator() The map's comparator null if natural ordering is used
K firstKey() The smallest key Empty map — NoSuchElementException
K lastKey() The largest key Empty map — NoSuchElementException
SortedMap<K,V> headMap(K toKey) Keys strictly less than toKey toKey is not part of the result
SortedMap<K,V> subMap(K fromKey, K toKey) Keys from fromKey inclusive to toKey exclusive fromKey > toKey — IllegalArgumentException: fromKey > toKey
SortedMap<K,V> tailMap(K fromKey) Keys greater than or equal to fromKey fromKey is included if the map contains it

Example: the keys are added in random order, and the SortedMap methods treat them as the sorted sequence 1, 2, 3, 4, 5, 100:

import java.util.SortedMap;
import java.util.TreeMap;

public class SortedMapExample {
    public static void main(String[] args) {
        SortedMap<Integer, Double> treeMap = new TreeMap<>();

        treeMap.put(3, 3434.34);
        treeMap.put(2, 1378.00);
        treeMap.put(1, 123.22);
        treeMap.put(4, 444.00);
        treeMap.put(5, 555.00);
        treeMap.put(100, 123.22);

        System.out.println("treeMap: " + treeMap);
        System.out.println("firstKey: " + treeMap.firstKey());
        System.out.println("lastKey: " + treeMap.lastKey());
        System.out.println("headMap(3): " + treeMap.headMap(3));
        System.out.println("subMap(3, 5): " + treeMap.subMap(3, 5));
        System.out.println("tailMap(4): " + treeMap.tailMap(4));
    }
}

Output:

treeMap: {1=123.22, 2=1378.0, 3=3434.34, 4=444.0, 5=555.0, 100=123.22}
firstKey: 1
lastKey: 100
headMap(3): {1=123.22, 2=1378.0}
subMap(3, 5): {3=3434.34, 4=444.0}
tailMap(4): {4=444.0, 5=555.0, 100=123.22}

headMap(3) cuts off the beginning of the map up to key 3, without including it. subMap(3, 5) takes the middle: key 3 is in, key 5 is out. tailMap(4) returns the “tail” from key 4 inclusive to the end. The same bound rules apply to sets — headSet(), subSet() and tailSet() in the lesson SortedSet, NavigableSet and TreeSet.

headMap, subMap and tailMap are live views

The range methods do not copy data: they return a view of the original map. Changes to the original map show up in the view immediately and vice versa, and you cannot put a key outside the view's range into it.

TreeMap<Integer, String> map = new TreeMap<>(Map.of(10, "ten", 20, "twenty", 30, "thirty"));
SortedMap<Integer, String> head = map.headMap(25);   // keys less than 25

map.put(15, "fifteen");      // added to the original map
System.out.println(head);

head.remove(10);             // removed through the view
System.out.println(map);

head.put(40, "forty");       // 40 is outside the range < 25

Output:

{10=ten, 15=fifteen, 20=twenty}
{15=fifteen, 20=twenty, 30=thirty}
Exception in thread "main" java.lang.IllegalArgumentException: key out of range

If you need an independent copy of a range, wrap the view: new TreeMap<>(map.headMap(25)). The same view behavior applies to keySet(), values() and entrySet() of any map.

NavigableMap in Java is an interface that extends SortedMap with navigation methods: find the nearest key that is less than, at most, at least or greater than a given key, take and remove the first or last entry, view the map in reverse order, and get ranges with explicit flags for including each bound. It was added in Java 6 and is implemented by TreeMap and ConcurrentSkipListMap.

The key difference from get(): navigation methods do not need an exact key match. If there is no key 35, floorKey(35) returns the nearest key below it instead of null.

NavigableMap method What it returns If no matching key exists
lowerKey(K key) / lowerEntry(K key) The greatest key (entry) strictly less than key: < null
floorKey(K key) / floorEntry(K key) The greatest key (entry) less than or equal to key: ≤ null
ceilingKey(K key) / ceilingEntry(K key) The least key (entry) greater than or equal to key: ≥ null
higherKey(K key) / higherEntry(K key) The least key (entry) strictly greater than key: > null
firstEntry() / lastEntry() The first / last entry, without removing it null for an empty map (unlike firstKey())
pollFirstEntry() / pollLastEntry() The first / last entry, removed from the map null for an empty map
headMap(K toKey, boolean inclusive) Keys less than toKey (or equal, if inclusive) An empty view
tailMap(K fromKey, boolean inclusive) Keys greater than fromKey (or equal, if inclusive) An empty view
subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) Keys between the bounds, each bound included by its own flag fromKey > toKey — IllegalArgumentException
descendingMap() A view of the map in reverse order —
navigableKeySet() / descendingKeySet() The keys as a NavigableSet in ascending / descending order —

The entries returned by firstEntry(), floorEntry() and the other *Entry methods of TreeMap are immutable snapshots: calling entry.setValue(...) throws UnsupportedOperationException. To change the value, use map.put(entry.getKey(), newValue).

The example below runs all four lookup methods on one map, including key 35, which is not in the map, and the keys at both ends:

import java.util.NavigableMap;
import java.util.TreeMap;

public class NavigableMapExample {
    public static void main(String[] args) {
        NavigableMap<Integer, String> map = new TreeMap<>();
        map.put(30, "c");
        map.put(10, "a");
        map.put(50, "e");
        map.put(20, "b");
        map.put(40, "d");

        System.out.println("lowerKey(30) = " + map.lowerKey(30));
        System.out.println("floorKey(30) = " + map.floorKey(30));
        System.out.println("ceilingKey(30) = " + map.ceilingKey(30));
        System.out.println("higherKey(30) = " + map.higherKey(30));

        System.out.println("floorKey(35) = " + map.floorKey(35));
        System.out.println("ceilingKey(35) = " + map.ceilingKey(35));
        System.out.println("lowerKey(10) = " + map.lowerKey(10));
        System.out.println("higherKey(50) = " + map.higherKey(50));
        System.out.println("floorEntry(35) = " + map.floorEntry(35));
    }
}

Output:

lowerKey(30) = 20
floorKey(30) = 30
ceilingKey(30) = 30
higherKey(30) = 40
floorKey(35) = 30
ceilingKey(35) = 40
lowerKey(10) = null
higherKey(50) = null
floorEntry(35) = 30=c

When the key is in the map, floorKey() and ceilingKey() return the key itself, while lowerKey() and higherKey() return its neighbors. When the key is missing, the strict and non-strict methods give the same result. Beyond the ends of the map all four return null, so check the result before using it. The set versions of these methods are called lower(), floor(), ceiling() and higher() — see SortedSet, NavigableSet and TreeSet.

In NavigableMap, headMap(), tailMap() and subMap() have overloads with inclusive flags: true includes the bound in the result, false excludes it. This removes the main inconvenience of SortedMap, where the upper bound is always excluded. To iterate from the largest key to the smallest, use descendingMap().

For the same map {10=a, 20=b, 30=c, 40=d, 50=e}:

System.out.println(map.headMap(30));                    // {10=a, 20=b}
System.out.println(map.headMap(30, true));              // {10=a, 20=b, 30=c}
System.out.println(map.tailMap(30, false));             // {40=d, 50=e}
System.out.println(map.subMap(20, false, 40, true));    // {30=c, 40=d}
System.out.println(map.descendingMap());                // {50=e, 40=d, 30=c, 20=b, 10=a}
System.out.println(map.descendingKeySet());             // [50, 40, 30, 20, 10]

The pollFirstEntry() and pollLastEntry() methods turn the map into a queue sorted by key: each call returns the entry at one end and removes it.

NavigableMap<Integer, String> queue = new TreeMap<>(map);

System.out.println("firstEntry: " + queue.firstEntry());
System.out.println("pollFirstEntry: " + queue.pollFirstEntry());
System.out.println("lastEntry: " + queue.lastEntry());
System.out.println("pollLastEntry: " + queue.pollLastEntry());
System.out.println("queue: " + queue);

Output:

firstEntry: 10=a
pollFirstEntry: 10=a
lastEntry: 50=e
pollLastEntry: 50=e
queue: {20=b, 30=c, 40=d}

firstEntry() and lastEntry() only read the end entries, while the poll methods return the same entries and remove them, so keys 10 and 50 are gone from the final map.

TreeMap in Java 21

Since Java 21 (JEP 431), SortedMap extends SequencedMap, so TreeMap gained a reversed() method that does the same as descendingMap(). But putFirst() and putLast() on a TreeMap throw UnsupportedOperationException: a key's position is decided by the sort order, not by the programmer. Where these methods actually work is shown in LinkedHashMap in Java.

NavigableMap examples from real code

A NavigableMap fits any task where a value depends on the range a key falls into: grade scales and discount tiers, rates by weight or amount, time schedules. The key is the lower bound of each range, and floorEntry() finds the right range in O(log n) instead of a chain of if-else statements.

A grade scale with floorEntry

import java.util.NavigableMap;
import java.util.TreeMap;

public class GradeScale {
    public static void main(String[] args) {
        NavigableMap<Integer, String> grades = new TreeMap<>();
        grades.put(0, "F");
        grades.put(60, "D");
        grades.put(70, "C");
        grades.put(80, "B");
        grades.put(90, "A");

        for (int score : new int[]{95, 80, 79, 12}) {
            System.out.println(score + " -> " + grades.floorEntry(score).getValue());
        }
    }
}

Output:

95 -> A
80 -> B
79 -> C
12 -> F

For 80, floorEntry() finds an exact match; for 79, the nearest bound below it, 70. But for a negative score such as -5 there is no key ≤ -5 in the map: floorEntry(-5) returns null, and getValue() throws a NullPointerException. So the scale must start at the lowest possible value, or the result must be checked for null.

A schedule: events in a time window

import java.time.LocalTime;
import java.util.NavigableMap;
import java.util.TreeMap;

public class Schedule {
    public static void main(String[] args) {
        NavigableMap<LocalTime, String> schedule = new TreeMap<>();
        schedule.put(LocalTime.of(9, 0), "Stand-up");
        schedule.put(LocalTime.of(10, 30), "Code review");
        schedule.put(LocalTime.of(13, 0), "Lunch");
        schedule.put(LocalTime.of(15, 0), "Demo");
        schedule.put(LocalTime.of(17, 30), "Retro");

        LocalTime now = LocalTime.of(12, 0);
        System.out.println("From 10:00 to 15:00: "
                + schedule.subMap(LocalTime.of(10, 0), true, LocalTime.of(15, 0), true));
        System.out.println("Next: " + schedule.ceilingEntry(now));
        System.out.println("Previous: " + schedule.lowerEntry(now));
    }
}

Output:

From 10:00 to 15:00: {10:30=Code review, 13:00=Lunch, 15:00=Demo}
Next: 13:00=Lunch
Previous: 10:30=Code review

LocalTime implements Comparable, so no comparator is needed. The true flag on the upper bound included the event at exactly 15:00 — with subMap(from, to) from SortedMap, “Demo” would have been left out.

TreeMap vs HashMap: which one to choose

HashMap is the default choice: its operations take O(1) on average, but key order is not guaranteed. TreeMap is slower — O(log n) per operation — but it keeps keys sorted, can find the nearest key and can cut out key ranges. If you only need insertion order rather than sorting, use LinkedHashMap. And if you searched for “TreeHashMap”: there is no such class in Java — it is either TreeMap or HashMap.

Property HashMap TreeMap
Iteration order Not guaranteed Ascending by key, or by the comparator
Inside Hash table (an array of buckets) Red-black tree
get(), put(), remove() O(1) on average O(log n) guaranteed
Key requirements Correct equals() and hashCode() Comparable or a Comparator, ideally consistent with equals()
null key One allowed NullPointerException with natural ordering; allowed with a comparator such as Comparator.nullsFirst(...)
null values Allowed Allowed
Nearest-key lookup, ranges No floorKey(), ceilingEntry(), subMap() and the other NavigableMap methods
Thread safety No; the concurrent replacement is ConcurrentHashMap No; the concurrent replacement is ConcurrentSkipListMap
When to choose Lookup by key, order does not matter Sorted output, minimum/maximum, range queries

Why HashMap runs in O(1) and what happens on collisions is covered in How HashMap Works Internally, and a summary table of all Map implementations is in Map Interface in Java. If O(1) and O(log n) are new to you, see Big O notation. A common pattern: collect data in a HashMap and sort it once before output with new TreeMap<>(hashMap).

Common TreeMap mistakes

Most TreeMap bugs come from two things: keys are compared with compareTo()/compare() rather than equals(), and range bounds are included differently depending on the method. Here are the usual mistakes and their fixes:

What you did What happens How to fix it
put(null, value) on new TreeMap<>() NullPointerException Avoid null keys or pass Comparator.nullsFirst(Comparator.naturalOrder())
A key of your own class without Comparable and without a comparator ClassCastException: class ... cannot be cast to class java.lang.Comparable on the first put() Implement Comparable or pass a Comparator to the constructor
Changed a key field used in the comparison after put() The key ends up in the wrong place in the tree: get() and remove() cannot find it Use immutable keys: String, Integer, LocalDate, a record
The comparator returns 0 for different keys Entries merge: the value is overwritten, the first key stays Add a second criterion with thenComparing()
Expected headMap(k) or subMap(a, k) to include k Key k is not in the result headMap(k, true), subMap(a, true, k, true)
put() into a view with a key outside its range IllegalArgumentException: key out of range Add to the original map

Frequently Asked Questions

Why does firstKey() throw an exception on an empty TreeMap while firstEntry() returns null?

It is a matter of history: firstKey() and lastKey() come from SortedMap (Java 1.2) and throw NoSuchElementException on an empty map, while firstEntry(), lastEntry() and the poll methods were added in NavigableMap (Java 6) and return null. If the map may be empty, firstEntry() with a null check is the easier option.

Is TreeMap thread-safe?

No. For shared access from several threads use ConcurrentSkipListMap: it also implements NavigableMap with the same methods, works without a global lock and allows null neither as a key nor as a value. The wrapper Collections.synchronizedNavigableMap(new TreeMap<>()) (Java 8) also works, but it locks the whole map on every operation, and iteration must be synchronized manually. Other concurrent collections are compared in Thread-safe collections in Java.

Can I get a TreeMap element by index?

No, TreeMap has no get(int index): the tree does not store node positions. You find the n-th key by iterating (map.keySet().stream().skip(n).findFirst()) in O(n). The position of a key can be found with map.headMap(key).size(), but the size of a view is also computed by iterating. If you need such queries often, keep a separate sorted list of keys.

How do I iterate over a TreeMap in reverse order?

Use map.descendingMap() — it is a view without copying, and both forEach() and entrySet() work on it; on Java 21 map.reversed() does the same. If you always need reverse order, create the map with Comparator.reverseOrder() from the start. An example with output is in Ranges and reverse order in NavigableMap.

Comments

Please log in or register to have a possibility to add comment.