- JAVA
Java Mapをキー・値でソートする方法 昇順・降順・複数条件
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つです。
- 最初の
comparingのラムダには型を書く:(Map.Entry<String, Item> e) -> ...。ここで型が決まるので、続くthenComparingのラムダはe -> ...で書けます。 - 降順にしたい条件だけ、第2引数に
Comparator.reverseOrder()を渡す:thenComparing(e -> e.getValue().price(), Comparator.reverseOrder())。 - 値のクラス用の 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期連続の黒字決算。