JavaのMapをソートするとは、entrySet(キーと値の組)を Comparator で並べ替え、その順序を保てる LinkedHashMap か List に入れ直すことです。キーで並べるだけなら TreeMap に渡す方法が最短です。 HashMap 自体には順序がないため、「並べ替えた結果をどこに入れるか」までがセットになります。

この記事では、キー昇順・キー降順・値昇順・値降順・「値降順で同点ならキー昇順」の5パターンを、コピーして使えるコードと実行結果でまとめます。つまずきやすい comparingByValue().reversed() のコンパイルエラーの文面と直し方、toMap で順序が崩れる例、値が null のときの扱い、上位N件の取り出しまで、動かした結果つきで紹介します。

確認環境:Ubuntu 24.04 / OpenJDK 21.0.10。掲載コードはすべて java ファイル名.java で実行した結果です。

Mapソートの早見表(5パターン)

.sorted(...) に渡す Comparator を替えるだけで、5パターンを切り替えられます。

並べ方 sorted に渡す Comparator
キー昇順 Map.Entry.comparingByKey()
キー降順 Map.Entry.comparingByKey(Comparator.reverseOrder())
値昇順 Map.Entry.comparingByValue()
値降順 Map.Entry.comparingByValue(Comparator.reverseOrder())
値降順 → 同点はキー昇順 Map.Entry.<String, Integer>comparingByValue(Comparator.reverseOrder()).thenComparing(Map.Entry.comparingByKey())

並べ替えた結果の受け取り方は3つです。

受け取り方 書き方 向いている場面
List で受ける .sorted(...).toList() 順番に表示・処理するだけのとき
LinkedHashMap で受ける .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new)) 並べた順のまま Map として使い続けたいとき
TreeMap に渡す(キー順のみ) new TreeMap<>(map) キー昇順でよく、追加後も並びを保ちたいとき

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java Mapをキー・値でソートする方法 昇順・降順・複数条件」

5パターンのコードと実行結果

5パターンをまとめて動かせる完成形です。sorted というメソッドに、並べ替えから LinkedHashMap への入れ直しまでをまとめています。

import java.util.*;
import java.util.stream.Collectors;

public class Five {
    /** 並べ替えた順のまま LinkedHashMap に集める */
    static <K, V> Map<K, V> sorted(Map<K, V> map, Comparator<Map.Entry<K, V>> order) {
        return map.entrySet().stream()
                .sorted(order)
                .collect(Collectors.toMap(
                        Map.Entry::getKey, Map.Entry::getValue,
                        (a, b) -> a, LinkedHashMap::new));
    }

    public static void main(String[] args) {
        Map<String, Integer> score = new HashMap<>();
        score.put("sato", 82);
        score.put("suzuki", 95);
        score.put("takahashi", 70);
        score.put("tanaka", 95);
        score.put("ito", 82);
        score.put("watanabe", 61);
        System.out.println("元の HashMap        : " + score);

        // 1. キー昇順
        System.out.println("1 キー昇順          : " + sorted(score, Map.Entry.comparingByKey()));
        // 2. キー降順
        System.out.println("2 キー降順          : " + sorted(score, Map.Entry.comparingByKey(Comparator.reverseOrder())));
        // 3. 値昇順
        System.out.println("3 値昇順            : " + sorted(score, Map.Entry.comparingByValue()));
        // 4. 値降順
        System.out.println("4 値降順            : " + sorted(score, Map.Entry.comparingByValue(Comparator.reverseOrder())));
        // 5. 値降順 → 同点はキー昇順
        System.out.println("5 値降順→キー昇順   : " + sorted(score,
                Map.Entry.<String, Integer>comparingByValue(Comparator.reverseOrder())
                        .thenComparing(Map.Entry.comparingByKey())));
    }
}

実行結果:

元の HashMap        : {takahashi=70, ito=82, watanabe=61, tanaka=95, sato=82, suzuki=95}
1 キー昇順          : {ito=82, sato=82, suzuki=95, takahashi=70, tanaka=95, watanabe=61}
2 キー降順          : {watanabe=61, tanaka=95, takahashi=70, suzuki=95, sato=82, ito=82}
3 値昇順            : {watanabe=61, takahashi=70, ito=82, sato=82, tanaka=95, suzuki=95}
4 値降順            : {tanaka=95, suzuki=95, ito=82, sato=82, takahashi=70, watanabe=61}
5 値降順→キー昇順   : {suzuki=95, tanaka=95, ito=82, sato=82, takahashi=70, watanabe=61}

パターン4では、同じ 95 点の2人が tanaka, suzuki の順に出ています。値だけで並べると、同点どうしの順番は元の HashMap の並びに左右されます。 表示のたびに同じ順にしたい場合は、パターン5のように thenComparing で第2条件(ここではキー昇順)を足します。

処理の流れ

flowchart LR
    A[Map] --> B[entrySet で キーと値の組にする]
    B --> C[stream の sorted で並べ替える]
    C --> D{受け取り方}
    D --> E[toList で List にする]
    D --> F[toMap の4引数で LinkedHashMap にする]
  • map.entrySet():キーと値の組(Map.Entry)の集まりを取り出します。
  • Map.Entry.comparingByKey()・comparingByValue():キーまたは値で比べる Comparator を返します。引数に別の Comparator を渡すと、その順で比べます。
  • Collectors.toMap(..., LinkedHashMap::new):並べ替えた順のまま LinkedHashMap に入れます。

comparingByValue().reversed() のコンパイルエラーと直し方

値の降順にしようとして comparingByValue() に .reversed() をつなげると、コンパイルエラーになります。

import java.util.*;
import java.util.stream.Collectors;

public class RevErr {
    public static void main(String[] args) {
        Map<String, Integer> score = Map.of("sato", 82, "suzuki", 95, "watanabe", 61);
        List<Map.Entry<String, Integer>> list = score.entrySet().stream()
                .sorted(Map.Entry.comparingByValue().reversed())
                .collect(Collectors.toList());
        System.out.println(list);
    }
}

コンパイル結果:

RevErr.java:8: error: no suitable method found for sorted(Comparator<Entry<Object,V#1>>)
                .sorted(Map.Entry.comparingByValue().reversed())
                ^
    method Stream.sorted() is not applicable
      (actual and formal argument lists differ in length)
    method Stream.sorted(Comparator<? super Entry<String,Integer>>) is not applicable
      (argument mismatch; Comparator<Entry<Object,V#2>> cannot be converted to Comparator<? super Entry<String,Integer>>)
  where V#1,V#2 are type-variables:
    V#1 extends Comparable<? super V#1>
    V#2 extends Comparable<? super V#2>
1 error
error: compilation failed

comparingByValue() は、キーと値の型を前後の文脈から推論するメソッドです。単独で sorted(...) に渡せば「Map.Entry<String, Integer> 用の Comparator が必要」と分かるので推論できます。ところが .reversed() をつなげると、comparingByValue() の時点では受け取り先が分からず、キーの型が Object と推論されます。その結果、エラー文にある Comparator<Entry<Object,V>> ができあがり、sorted に渡せなくなります。

ラムダ式で Comparator.comparing(e -> e.getValue()).reversed() と書いた場合も、同じ理由で別のエラーになります。

import java.util.*;
import java.util.stream.Collectors;

public class RevErr2 {
    public static void main(String[] args) {
        Map<String, Integer> score = Map.of("sato", 82, "suzuki", 95, "watanabe", 61);
        List<Map.Entry<String, Integer>> list = score.entrySet().stream()
                .sorted(Comparator.comparing(e -> e.getValue()).reversed())
                .collect(Collectors.toList());
        System.out.println(list);
    }
}
RevErr2.java:8: error: cannot find symbol
                .sorted(Comparator.comparing(e -> e.getValue()).reversed())
                                                   ^
  symbol:   method getValue()
  location: variable e of type Object
1 error
error: compilation failed

直し方は2通り

import java.util.*;

public class RevFix {
    public static void main(String[] args) {
        Map<String, Integer> score = Map.of("sato", 82, "suzuki", 95, "watanabe", 61);

        // 直し方1: 型を明示する(型引数を comparingByValue の前に書く)
        System.out.println("型を明示        : " + score.entrySet().stream()
                .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
                .toList());

        // 直し方2: reversed() を使わず、Comparator.reverseOrder() を渡す
        System.out.println("reverseOrder    : " + score.entrySet().stream()
                .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
                .toList());

        // 参考: Collections.reverseOrder で全体を包む
        System.out.println("全体を包む      : " + score.entrySet().stream()
                .sorted(Collections.reverseOrder(Map.Entry.comparingByValue()))
                .toList());

        // 参考: ラムダの引数に型を書く
        System.out.println("ラムダに型を書く: " + score.entrySet().stream()
                .sorted(Comparator.comparing((Map.Entry<String, Integer> e) -> e.getValue()).reversed())
                .toList());
    }
}

実行結果:

型を明示        : [suzuki=95, sato=82, watanabe=61]
reverseOrder    : [suzuki=95, sato=82, watanabe=61]
全体を包む      : [suzuki=95, sato=82, watanabe=61]
ラムダに型を書く: [suzuki=95, sato=82, watanabe=61]
直し方 書き方 特徴
1. 型を明示する Map.Entry.<String, Integer>comparingByValue().reversed() reversed をそのまま使える。型引数はメソッド名の前に書く
2. reverseOrder を渡す Map.Entry.comparingByValue(Comparator.reverseOrder()) 型を書かなくてよく、短い
参考:全体を包む Collections.reverseOrder(Map.Entry.comparingByValue()) Comparator 全体を逆順にする
参考:ラムダに型を書く Comparator.comparing((Map.Entry<String, Integer> e) -> e.getValue()).reversed() ラムダ式で書きたいとき

単純な値の降順なら「2. reverseOrder を渡す」が手軽です。thenComparing をつなげる複数条件では、同じ理由で型の明示が必要になるため、早見表のパターン5では Map.Entry.<String, Integer>comparingByValue(...) と書いています。

なお、reversed() は「それまでに組み立てた Comparator 全体」を逆にします。A.thenComparing(B).reversed() は A も B も逆順になるので、「値は降順、キーは昇順」のように向きが混ざるときは、条件ごとに Comparator.reverseOrder() を渡す書き方が意図どおりになります。

toMapの4引数でLinkedHashMapに集めないと順序が崩れる

並べ替えた結果を Map に戻すときは、Collectors.toMap の引数の数に注意します。

import java.util.*;
import java.util.stream.Collectors;

public class ToMap {
    public static void main(String[] args) {
        Map<String, Integer> score = Map.of(
                "sato", 82, "suzuki", 95, "takahashi", 70, "tanaka", 88, "ito", 77, "watanabe", 61);

        // 並べ替えた直後(List)は正しい順
        System.out.println("sorted 直後の List : " + score.entrySet().stream()
                .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
                .toList());

        // 2 引数の toMap → HashMap に入るので順序が崩れる
        Map<String, Integer> two = score.entrySet().stream()
                .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
        System.out.println("toMap 2引数        : " + two + "  ← " + two.getClass().getSimpleName());

        // 4 引数の toMap → LinkedHashMap に入るので順序が残る
        Map<String, Integer> four = score.entrySet().stream()
                .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
                        (a, b) -> a, LinkedHashMap::new));
        System.out.println("toMap 4引数        : " + four + "  ← " + four.getClass().getSimpleName());

        // LinkedHashMap を HashMap や Map.copyOf に渡し直すと、また崩れる
        System.out.println("new HashMap<>(four): " + new HashMap<>(four));
        System.out.println("new TreeMap<>(four): " + new TreeMap<>(four));
    }
}

実行結果:

sorted 直後の List : [suzuki=95, tanaka=88, sato=82, ito=77, takahashi=70, watanabe=61]
toMap 2引数        : {takahashi=70, ito=77, watanabe=61, tanaka=88, suzuki=95, sato=82}  ← HashMap
toMap 4引数        : {suzuki=95, tanaka=88, sato=82, ito=77, takahashi=70, watanabe=61}  ← LinkedHashMap
new HashMap<>(four): {watanabe=61, tanaka=88, suzuki=95, sato=82, takahashi=70, ito=77}
new TreeMap<>(four): {ito=77, sato=82, suzuki=95, takahashi=70, tanaka=88, watanabe=61}

2引数の toMap は結果を HashMap に入れるため、せっかく並べ替えた順序が崩れます。 公式ドキュメント(Collectors)でも、2引数・3引数の toMap が返す Map の型は保証しないと書かれています。順序を保つには、4引数の toMap で LinkedHashMap::new を指定します。

引数 役割 このコードでの値
第1引数 キーの取り出し方 Map.Entry::getKey
第2引数 値の取り出し方 Map.Entry::getValue
第3引数 キーが重なった時の合わせ方 (a, b) -> a(先に来た値を残す)
第4引数 結果を入れる Map の作り方 LinkedHashMap::new

元が Map ならキーは重ならないので、第3引数は実際には呼ばれません。第4引数を指定するために必要な引数、と考えると覚えやすくなります。

実行結果の最後の2行のとおり、並べ替え済みの LinkedHashMap を new HashMap<>(...) や new TreeMap<>(...) に渡し直すと、順序はその Map の性質に戻ります。 メソッドの戻り値や引数の型は Map のままで構いませんが、途中でコピーし直さないようにします。

キーで並べるならTreeMapが最短

キーの順に並べるだけなら、Stream を使わずに TreeMap へ渡す方法が最も短く書けます。

import java.util.*;

public class KeySort {
    public static void main(String[] args) {
        Map<String, Integer> score = new HashMap<>(Map.of("sato", 82, "Suzuki", 95, "ito", 82, "Tanaka", 61));

        // TreeMap に渡すだけでキー昇順
        System.out.println("new TreeMap<>(map)      : " + new TreeMap<>(score));

        // キー降順
        Map<String, Integer> desc = new TreeMap<>(Comparator.reverseOrder());
        desc.putAll(score);
        System.out.println("reverseOrder            : " + desc);

        // 昇順の TreeMap から降順のビューを得る
        System.out.println("descendingMap()         : " + new TreeMap<>(score).descendingMap());

        // 大文字小文字を区別しない
        Map<String, Integer> ci = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        ci.putAll(score);
        System.out.println("CASE_INSENSITIVE_ORDER  : " + ci);

        // 数字の文字列キーは辞書順になる
        Map<String, String> no = new HashMap<>(Map.of("1", "a", "2", "b", "10", "c", "9", "d", "100", "e"));
        System.out.println("文字列の数字キー         : " + new TreeMap<>(no));
        Map<String, String> num = new TreeMap<>(Comparator.comparingInt(Integer::parseInt));
        num.putAll(no);
        System.out.println("数値として比べる         : " + num);

        // 日本語のキーは文字コード順
        Map<String, Integer> ja = new TreeMap<>(Map.of("山田", 1, "青木", 2, "井上", 3, "あおき", 4, "アオキ", 5));
        System.out.println("日本語キー(文字コード順): " + ja);
    }
}

実行結果:

new TreeMap<>(map)      : {Suzuki=95, Tanaka=61, ito=82, sato=82}
reverseOrder            : {sato=82, ito=82, Tanaka=61, Suzuki=95}
descendingMap()         : {sato=82, ito=82, Tanaka=61, Suzuki=95}
CASE_INSENSITIVE_ORDER  : {ito=82, sato=82, Suzuki=95, Tanaka=61}
文字列の数字キー         : {1=a, 10=c, 100=e, 2=b, 9=d}
数値として比べる         : {1=a, 2=b, 9=d, 10=c, 100=e}
日本語キー(文字コード順): {あおき=4, アオキ=5, 井上=3, 山田=1, 青木=2}
やりたいこと 書き方
キー昇順 new TreeMap<>(map)
キー降順 new TreeMap<>(Comparator.reverseOrder()) を作って putAll(map)
昇順の TreeMap を逆順に見る treeMap.descendingMap()
大文字小文字を区別しない順 new TreeMap<>(String.CASE_INSENSITIVE_ORDER)
数字の文字列を数値の順に new TreeMap<>(Comparator.comparingInt(Integer::parseInt))

キーの並びで気をつけたい点が3つあります。

  • 文字列は文字コード順です。大文字が小文字より先に並ぶため、Suzuki, Tanaka, ito, sato の順になります。
  • 数字の文字列キーは辞書順になり、"10" や "100" が "2" より前に来ます。数値として比べる Comparator を渡すか、キーの型を Integer にします。
  • 日本語のキーも文字コード順です。ひらがな・カタカナ・漢字の順に並び、漢字は読みの五十音順にはなりません。読みの順に並べたいときは、読みがなを別に持ってそれをキーにします。

値がnullのときのNullPointerExceptionとnullsLast

値に null が混ざっている Map を comparingByValue() で並べ替えると、NullPointerException になります。Comparator.nullsLast・nullsFirst で null の置き場所を決めると回避できます。

import java.util.*;
import java.util.stream.Collectors;

public class NullVal {
    public static void main(String[] args) {
        Map<String, Integer> score = new HashMap<>();
        score.put("sato", 82);
        score.put("suzuki", null);      // まだ採点していない
        score.put("tanaka", 95);
        score.put("ito", null);

        // 1. そのまま値で並べ替える
        try {
            score.entrySet().stream().sorted(Map.Entry.comparingByValue()).toList();
        } catch (NullPointerException e) {
            System.out.println("1 comparingByValue()  : " + e.getMessage());
        }

        // 2. nullsLast で null を末尾へ(昇順)
        System.out.println("2 nullsLast 昇順      : " + score.entrySet().stream()
                .sorted(Map.Entry.comparingByValue(Comparator.nullsLast(Comparator.naturalOrder())))
                .toList());

        // 3. nullsLast で null を末尾へ(降順)
        System.out.println("3 nullsLast 降順      : " + score.entrySet().stream()
                .sorted(Map.Entry.comparingByValue(Comparator.nullsLast(Comparator.reverseOrder())))
                .toList());

        // 4. nullsFirst で null を先頭へ
        System.out.println("4 nullsFirst 昇順     : " + score.entrySet().stream()
                .sorted(Map.Entry.comparingByValue(Comparator.nullsFirst(Comparator.naturalOrder())))
                .toList());

        // 5. null を除いてから並べ替える
        System.out.println("5 null を除く         : " + score.entrySet().stream()
                .filter(e -> e.getValue() != null)
                .sorted(Map.Entry.comparingByValue())
                .toList());

        // 6. null の値を toMap で集める
        try {
            score.entrySet().stream()
                    .sorted(Map.Entry.comparingByValue(Comparator.nullsLast(Comparator.naturalOrder())))
                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
                            (a, b) -> a, LinkedHashMap::new));
        } catch (NullPointerException e) {
            System.out.println("6 toMap に null の値  : " + e);
        }

        // 7. null の値を残したまま LinkedHashMap に集める
        Map<String, Integer> kept = score.entrySet().stream()
                .sorted(Map.Entry.<String, Integer>comparingByValue(Comparator.nullsLast(Comparator.naturalOrder()))
                        .thenComparing(Map.Entry.comparingByKey()))
                .collect(LinkedHashMap::new, (m, e) -> m.put(e.getKey(), e.getValue()), Map::putAll);
        System.out.println("7 3引数の collect     : " + kept);

        // 8. null のキーを TreeMap に入れる
        Map<String, Integer> withNullKey = new HashMap<>(Map.of("b", 2, "a", 1));
        withNullKey.put(null, 0);
        try {
            new TreeMap<>(withNullKey);
        } catch (NullPointerException e) {
            System.out.println("8 new TreeMap(nullキー): " + e);
        }
        Map<String, Integer> tm = new TreeMap<>(Comparator.nullsFirst(Comparator.naturalOrder()));
        tm.putAll(withNullKey);
        System.out.println("9 nullsFirst の TreeMap: " + tm);
    }
}

実行結果:

1 comparingByValue()  : Cannot read field "value" because "anotherInteger" is null
2 nullsLast 昇順      : [sato=82, tanaka=95, ito=null, suzuki=null]
3 nullsLast 降順      : [tanaka=95, sato=82, ito=null, suzuki=null]
4 nullsFirst 昇順     : [ito=null, suzuki=null, sato=82, tanaka=95]
5 null を除く         : [sato=82, tanaka=95]
6 toMap に null の値  : java.lang.NullPointerException
7 3引数の collect     : {sato=82, tanaka=95, ito=null, suzuki=null}
8 new TreeMap(nullキー): java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" because "k1" is null
9 nullsFirst の TreeMap: {null=0, a=1, b=2}

降順でも null を末尾に置きたいときは、nullsLast の中に reverseOrder() を入れます(3番)。nullsLast(naturalOrder()).reversed() と書くと null の位置まで逆になり、先頭に来ます。

nullの値はtoMapで集められない

6番の結果にも注目してください。並べ替えが通っても、値に null があると Collectors.toMap が NullPointerException になります。 null の値を残したまま LinkedHashMap に集めたいときは、7番のように3引数の collect を使います。

.collect(LinkedHashMap::new, (m, e) -> m.put(e.getKey(), e.getValue()), Map::putAll)

8番・9番はキー側の null です。null のキーを含む Map を new TreeMap<>(map) に渡すと NullPointerException になり、Comparator.nullsFirst(Comparator.naturalOrder()) を渡した TreeMap なら先頭に入ります。

上位N件を取り出す

並べ替えのあとに limit(N) を足すと、上位N件になります。

import java.util.*;
import java.util.stream.Collectors;

public class TopN {
    public static void main(String[] args) {
        Map<String, Integer> sales = Map.of(
                "ノート", 120, "ペン", 340, "付箋", 85, "クリップ", 340, "のり", 40, "はさみ", 95);

        Comparator<Map.Entry<String, Integer>> byValueDesc =
                Map.Entry.<String, Integer>comparingByValue(Comparator.reverseOrder())
                        .thenComparing(Map.Entry.comparingByKey());

        // 上位3件を Map で
        Map<String, Integer> top3 = sales.entrySet().stream()
                .sorted(byValueDesc)
                .limit(3)
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
                        (a, b) -> a, LinkedHashMap::new));
        System.out.println("上位3件(Map)   : " + top3);

        // 上位3件のキーだけ
        List<String> top3Keys = sales.entrySet().stream()
                .sorted(byValueDesc).limit(3).map(Map.Entry::getKey).toList();
        System.out.println("上位3件のキー  : " + top3Keys);

        // 下位2件
        System.out.println("下位2件        : " + sales.entrySet().stream()
                .sorted(Map.Entry.comparingByValue()).limit(2).toList());

        // 4〜6位(ページ送り)
        System.out.println("4〜6位         : " + sales.entrySet().stream()
                .sorted(byValueDesc).skip(3).limit(3).toList());

        // 1件だけなら sorted は不要
        System.out.println("最大の1件      : " + Collections.max(sales.entrySet(), Map.Entry.comparingByValue()));
        System.out.println("最小の1件      : " + sales.entrySet().stream().min(Map.Entry.comparingByValue()).orElseThrow());

        // 順位つきで表示
        int rank = 1;
        for (Map.Entry<String, Integer> e : top3.entrySet()) {
            System.out.printf("%d位 %s %d個%n", rank++, e.getKey(), e.getValue());
        }
    }
}

実行結果:

上位3件(Map)   : {クリップ=340, ペン=340, ノート=120}
上位3件のキー  : [クリップ, ペン, ノート]
下位2件        : [のり=40, 付箋=85]
4〜6位         : [はさみ=95, 付箋=85, のり=40]
最大の1件      : クリップ=340
最小の1件      : のり=40
1位 クリップ 340個
2位 ペン 340個
3位 ノート 120個
やりたいこと 書き方
上位N件 .sorted(値の降順).limit(N)
下位N件 .sorted(値の昇順).limit(N)
ページ送り(4〜6位など) .sorted(...).skip(3).limit(3)
キーだけほしい .map(Map.Entry::getKey).toList()
最大・最小の1件 Collections.max(map.entrySet(), Map.Entry.comparingByValue())、stream().min(...)

同じ 340 個の「クリップ」と「ペン」は、第2条件のキー昇順で順番が決まっています。上位N件の境目に同点があると、第2条件がない場合はどちらが入るかが元の並びに左右されるため、ランキングでは thenComparing を付けておくと結果が安定します。1件だけほしいときは、全体を並べ替えない Collections.max や min のほうが処理が軽く済みます。

値がオブジェクトのときの複数条件ソート

値が record やクラスの場合は、e.getValue().項目() を取り出して比べます。カテゴリ昇順 → 価格降順 → キー昇順の3条件で並べる例です。

import java.util.*;
import java.util.stream.Collectors;

public class ObjVal {
    record Item(String category, int price, int stock) {}

    public static void main(String[] args) {
        Map<String, Item> items = Map.of(
                "desk", new Item("家具", 12000, 3),
                "chair", new Item("家具", 8000, 10),
                "pen", new Item("文具", 150, 200),
                "note", new Item("文具", 300, 200),
                "lamp", new Item("家電", 4500, 0));

        // カテゴリ昇順 → 価格降順 → キー昇順
        Comparator<Map.Entry<String, Item>> order = Comparator
                .comparing((Map.Entry<String, Item> e) -> e.getValue().category())
                .thenComparing(e -> e.getValue().price(), Comparator.reverseOrder())
                .thenComparing(Map.Entry::getKey);

        Map<String, Item> sorted = items.entrySet().stream()
                .sorted(order)
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
                        (a, b) -> a, LinkedHashMap::new));
        sorted.forEach((k, v) -> System.out.println(k + " -> " + v));

        // 値側の Comparator を別に用意して comparingByValue に渡す書き方
        Comparator<Item> byStockDescThenPrice =
                Comparator.comparingInt(Item::stock).reversed().thenComparingInt(Item::price);
        System.out.println("在庫降順→価格昇順: " + items.entrySet().stream()
                .sorted(Map.Entry.comparingByValue(byStockDescThenPrice))
                .map(Map.Entry::getKey).toList());
    }
}

実行結果:

desk -> Item[category=家具, price=12000, stock=3]
chair -> Item[category=家具, price=8000, stock=10]
lamp -> Item[category=家電, price=4500, stock=0]
note -> Item[category=文具, price=300, stock=200]
pen -> Item[category=文具, price=150, stock=200]
在庫降順→価格昇順: [pen, note, chair, desk, lamp]

書き方のポイントは次の3つです。

  1. 最初の comparing のラムダには型を書く:(Map.Entry<String, Item> e) -> ...。ここで型が決まるので、続く thenComparing のラムダは e -> ... で書けます。
  2. 降順にしたい条件だけ、第2引数に Comparator.reverseOrder() を渡す:thenComparing(e -> e.getValue().price(), Comparator.reverseOrder())。
  3. 値のクラス用の Comparator を先に作り、comparingByValue(...) に渡す書き方もできます。値の並び順を他の場所でも使い回すときに向いています。

Streamを使わない書き方とTreeMapの注意点

entrySet を ArrayList に写して sort する書き方でも、同じ結果になります。

import java.util.*;

public class ListWay {
    public static void main(String[] args) {
        Map<String, Integer> score = new HashMap<>(Map.of("sato", 82, "suzuki", 95, "tanaka", 95, "ito", 61));

        // 1. entrySet を List に写して sort(Stream を使わない書き方)
        List<Map.Entry<String, Integer>> list = new ArrayList<>(score.entrySet());
        list.sort(Map.Entry.<String, Integer>comparingByValue(Comparator.reverseOrder())
                .thenComparing(Map.Entry.comparingByKey()));
        System.out.println("List で sort    : " + list);

        // 2. その順で LinkedHashMap に入れ直す
        Map<String, Integer> result = new LinkedHashMap<>();
        for (Map.Entry<String, Integer> e : list) {
            result.put(e.getKey(), e.getValue());
        }
        System.out.println("LinkedHashMap へ: " + result);

        // 3. 並べ替えたキーだけほしいとき
        List<String> keys = new ArrayList<>(score.keySet());
        keys.sort(Comparator.<String, Integer>comparing(score::get, Comparator.reverseOrder())
                .thenComparing(Comparator.naturalOrder()));
        System.out.println("キーの List     : " + keys);

        // 4. 値を見て比べる TreeMap は、同じ値のキーが 1 件にまとまる
        Map<String, Integer> byValueTree = new TreeMap<>(Comparator.comparing(score::get));
        byValueTree.putAll(score);
        System.out.println("値で比べる TreeMap: " + byValueTree + " (" + byValueTree.size() + "件 / 元は " + score.size() + "件)");

        // 5. 並べ替え後に元の Map へ追加しても、結果には入らない
        score.put("yamada", 100);
        System.out.println("元に追加した後   : " + result);
    }
}

実行結果:

List で sort    : [suzuki=95, tanaka=95, sato=82, ito=61]
LinkedHashMap へ: {suzuki=95, tanaka=95, sato=82, ito=61}
キーの List     : [suzuki, tanaka, sato, ito]
値で比べる TreeMap: {ito=61, sato=82, tanaka=95} (3件 / 元は 4件)
元に追加した後   : {suzuki=95, tanaka=95, sato=82, ito=61}

4番は、値の順に並ぶ TreeMap を作ろうとして Comparator.comparing(score::get) を渡した例です。TreeMap は Comparator が「同じ」と判定したキーを1件にまとめるため、同点の suzuki と tanaka が1件になり、4件が3件に減りました。 値で並べたいときは TreeMap ではなく、List か LinkedHashMap に入れ直す方法を使います。

5番のとおり、並べ替えた結果は元の Map とは別物です。元の Map にあとから追加しても、並べ替え済みの結果には反映されません。追加のたびに並んでいてほしいのがキー順なら TreeMap、値の順なら表示の直前に並べ替える形にします。

つまずきやすい点のチェック表

起きること 原因 直し方
並べ替えたのに順序がばらばら 2引数の toMap で HashMap に入った 4引数で LinkedHashMap::new を指定
reversed() でコンパイルエラー 型を推論できない 型を明示するか Comparator.reverseOrder() を渡す
並べ替えで NullPointerException 値に null がある nullsLast・nullsFirst を使うか filter で除く
toMap で NullPointerException toMap は null の値を受け付けない 3引数の collect で LinkedHashMap に put する
同点の順番が実行環境やデータで変わる 第2条件がない thenComparing でキーなどを足す
キーも降順になってしまった reversed() が全体を逆にした 条件ごとに reverseOrder() を渡す
"10" が "2" より前に来る 文字列キーは辞書順 数値で比べる Comparator を渡す
TreeMap の件数が減った Comparator が同点を同じキーとみなした 値の順は List・LinkedHashMap で扱う

よくある質問

Q. HashMap そのものを並べ替えることはできますか? A. HashMap は順序を持たないので、その場で並べ替えることはできません。並べ替えた結果を LinkedHashMap・TreeMap・List のいずれかに入れ直します。

Q. 並べ替えた結果は List と LinkedHashMap のどちらで受けるのがよいですか? A. 順番に表示するだけなら List(toList())が簡単です。並べた順を保ちつつ、あとからキーで値を引きたい場合は LinkedHashMap にします。

Q. comparingByKey・comparingByValue は Java のどのバージョンから使えますか? A. どちらも Java 8 で Map.Entry に追加されました。Java 8 以降であれば、この記事の Comparator の書き方はそのまま使えます。

Q. 値がクラスで Comparable を実装していないときは? A. 引数なしの comparingByValue() は値が Comparable であることを求めるため、コンパイルエラーになります。comparingByValue(Comparator.comparingInt(Item::price)) のように、値用の Comparator を渡します。

まとめ

  • Mapのソートは「entrySet を並べ替えて、順序を保てる入れ物に入れ直す」。キー順だけなら new TreeMap<>(map) が最短。
  • 5パターンは Comparator の差し替えで書ける:comparingByKey・comparingByValue と Comparator.reverseOrder()、thenComparing。
  • comparingByValue().reversed() はコンパイルエラー。型を明示するか、comparingByValue(Comparator.reverseOrder()) と書く。
  • Mapに戻すときは toMap の4引数で LinkedHashMap::new。2引数では HashMap になり順序が崩れる。
  • 値が null なら nullsLast・nullsFirst。null の値を残して集めるときは3引数の collect。
  • 上位N件は sorted のあとに limit(N)。同点に備えて第2条件を付ける。
  • 値の順に並ぶ TreeMap は作らない。同点のキーが1件にまとまるため、List か LinkedHashMap を使う。

参考・出典

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java Mapをキー・値でソートする方法 昇順・降順・複数条件」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?