結論

要件選ぶべき実装
順序が要らない・最速で動かしたいHashMap
入力した順をそのまま保ちたいLinkedHashMap
キー自体で自動ソートしたい・範囲検索したいTreeMap

Map<K, V> インターフェースで受けておけば、実装クラスの差し替えは1行で済む。用途に合わせて中身だけ入れ替える設計が定石。

詳しい使い方は JavaのMap — HashMapの使い方と代表メソッド を参照。本記事は 3つの実装クラスをどう使い分けるか に絞る。

動機 — イベントログを HashMap に入れたら時系列が壊れた

サーバー起動からの経過秒数とログメッセージを Map<Long, String> で管理する例を考える。

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Map<Long, String> eventLog = new HashMap<>();
        eventLog.put(0L,   "サーバー起動");
        eventLog.put(5L,   "DB接続確立");
        eventLog.put(90L,  "APIリクエスト処理");
        eventLog.put(135L, "エラー: タイムアウト");
        eventLog.put(180L, "リトライ成功");

        for (Long time : eventLog.keySet()) {
            System.out.println(time + "秒: " + eventLog.get(time));
        }
    }
}

期待としては 0, 5, 90, 135, 180 の時系列順。しかし実際の出力は:

0秒: サーバー起動
180秒: リトライ成功
5秒: DB接続確立
135秒: エラー: タイムアウト
90秒: APIリクエスト処理

ログとしてまったく読めない。これが HashMap の「順序保証なし」仕様。

なぜ崩れるか — HashMap のバケット計算

Long.hashCode(value) の中身は (int)(value ^ (value >>> 32))。今回のように 0〜180 のような小さい long であれば、上位32bitは0なので結果は (int)value と等しくなる。

デフォルト容量16の HashMap では、バケット位置は hash & 15(下位4bit)で決まる。

キー (long)hashCodeバケット (hash & 15)
000
555
909010
1351357
1801804

バケット番号順 0 → 4 → 5 → 7 → 10 で取り出されるので、出力順は 0, 180, 5, 135, 90 になる。これが「順序保証なし」の正体。

JDKバージョンや初期容量を変えれば順番は変わるが、いずれにせよ「挿入順とは別の何かに従って並ぶ」のは確定仕様。順序を期待してはいけない

解決策その1 — LinkedHashMap(挿入順を保つ)

put した順をそのまま維持したいなら LinkedHashMap に差し替える。コードの変更は 1行のみ

import java.util.LinkedHashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Map<Long, String> eventLog = new LinkedHashMap<>();
        eventLog.put(0L,   "サーバー起動");
        eventLog.put(5L,   "DB接続確立");
        eventLog.put(90L,  "APIリクエスト処理");
        eventLog.put(135L, "エラー: タイムアウト");
        eventLog.put(180L, "リトライ成功");

        for (Long time : eventLog.keySet()) {
            System.out.println(time + "秒: " + eventLog.get(time));
        }
    }
}

出力:

0秒: サーバー起動
5秒: DB接続確立
90秒: APIリクエスト処理
135秒: エラー: タイムアウト
180秒: リトライ成功

時系列順に並んだ。これは put した順 = タイムスタンプ昇順」が成立している から有効。LinkedHashMap は内部でハッシュ表に加えて 双方向リンクリスト を持ち、挿入順を覚えている。

落とし穴 — 「届いた順 ≠ 時系列順」のケース

非同期処理や複数のソースからログが集約される場合、put 順が時系列とずれることがある。

Map<Long, String> eventLog = new LinkedHashMap<>();
eventLog.put(180L, "リトライ成功");      // 後発のログが先に届いた
eventLog.put(0L,   "サーバー起動");
eventLog.put(90L,  "APIリクエスト処理");
eventLog.put(5L,   "DB接続確立");
eventLog.put(135L, "エラー: タイムアウト");

この場合の出力は put した順そのまま:

180秒: リトライ成功
0秒: サーバー起動
90秒: APIリクエスト処理
5秒: DB接続確立
135秒: エラー: タイムアウト

「入力順」と「キーの自然順」は別物。LinkedHashMap が保証するのは前者のみで、キー自体で並べたいなら次の TreeMap を使う

解決策その2 — TreeMap(キー昇順で自動ソート)

「タイムスタンプ自体で並べたい」なら TreeMapput した順に関係なく、常にキー昇順で並ぶ。

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

public class Main {
    public static void main(String[] args) {
        Map<Long, String> eventLog = new TreeMap<>();
        eventLog.put(180L, "リトライ成功");      // 後発が先でも
        eventLog.put(0L,   "サーバー起動");
        eventLog.put(90L,  "APIリクエスト処理");
        eventLog.put(5L,   "DB接続確立");
        eventLog.put(135L, "エラー: タイムアウト");

        for (Long time : eventLog.keySet()) {
            System.out.println(time + "秒: " + eventLog.get(time));
        }
    }
}

出力はキー昇順:

0秒: サーバー起動
5秒: DB接続確立
90秒: APIリクエスト処理
135秒: エラー: タイムアウト
180秒: リトライ成功

put 順に依存せず、常に「経過秒数の小さい順」になる。ログが順不同で届くシステムでも安心して使える。

TreeMap だけが持つ追加機能

キーで並んでいる前提があるので、範囲検索系のメソッド が使える(SortedMap / NavigableMap インターフェース由来)。

TreeMap<Long, String> log = new TreeMap<>();
// ... put 略 ...

// 90秒以降のログだけ取り出す
SortedMap<Long, String> after90 = log.tailMap(90L);

// 60〜150秒のログを取り出す
SortedMap<Long, String> middle = log.subMap(60L, 150L);

// 最初・最後のログ
Long firstTime = log.firstKey();
Long lastTime  = log.lastKey();

// 指定キー以下で最も近いエントリ
Map.Entry<Long, String> floor = log.floorEntry(100L);  // 90秒のログが返る

これらは HashMap/LinkedHashMap にはない。「キーの順序が意味を持つ」用途では TreeMap 一択 という場面が多い。

3クラスの比較

観点HashMapLinkedHashMapTreeMap
順序保証なし挿入順キー昇順(Comparator 指定可)
内部構造ハッシュ表ハッシュ表 + 双方向リンクリスト赤黒木
get/put 計算量O(1) 平均O(1) 平均O(log N)
キー型制約なしなしComparable 実装 or Comparator 指定
メモリ消費中(リンクリスト分)
範囲検索不可不可可(subMap/tailMap/firstKey/lastKey 等)
null キー1個まで可1個まで可不可NullPointerException

性能だけ見れば HashMap/LinkedHashMap が勝つが、TreeMap「キーの順序を活かした便利機能」 が独自に揃っている。

選択フロー

順序が必要か?
├─ NO  → HashMap (デフォルト・最速)
└─ YES
    ├─ 入力順を保てばOK
    │   → LinkedHashMap
    └─ キー自体で並べたい
        ├─ 範囲検索 (subMap など) も使う?
        │   → TreeMap (一択)
        └─ ソートだけで十分
            → TreeMap (簡潔・自動)

実務での使い分けシナリオ

シナリオ適した実装理由
ID → ユーザー情報の引き当てHashMap順序不要、頻繁な検索、最速
ユーザー設定の保存HashMap順序不要、最小メモリ
操作履歴の Undo/RedoLinkedHashMap操作順そのままで保持
LRUキャッシュLinkedHashMapアクセス順序モード(accessOrder=true)を利用
ソート済みイベントログTreeMap順不同入力でも自動ソート
時間範囲での集計TreeMapsubMap で範囲抽出が自然
スコアランキング(点数→ユーザー名)TreeMapキー昇順・降順で順位算出が容易

まとめ

  • HashMap は最速だが順序保証なし。順序が要件なら別を選ぶ
  • 「入力順 = 期待出力順」なら LinkedHashMap(1行差し替えで対応可)
  • 「入力順がバラバラでもキー自体で並べたい」なら TreeMap
  • TreeMap だけが範囲検索系メソッド(subMap/tailMap/firstKey/lastKey)を持つ
  • 計算量は HashMap/LinkedHashMap が O(1)、TreeMap が O(log N)。性能と機能のトレードオフを判断する
  • 変数の型は Map<K, V> で宣言しておけば、実装クラスの差し替えは無痛で済む

関連記事:

変更履歴

  • 2026-05-05: 初版公開