Listの順番を担保したMapを作りたい
ソートされたListからMapを作ると順序が変わる
Java8以降でstreamAPIを使い、ListオブジェクトからMapオブジェクトを生成したいというケースは良くあると思います。
今回の例ではとてもシンプルなListオブジェクトですが、実際にはModelなどをListの要素として持つ形になると思います。
そんな時に、すでにソートされたList(DBからの取得結果etc...)をstreamで回してMapオブジェクトにしようとする際に以下のような書き方をする場合が多いと思います。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Sample
{
public static void main( String... args)
{
System.out.println("=====List=====");
List<String> numberList = Arrays.asList("One","Two", "three", "For", "Five", "Six", "Seven", "Eight", "Nine", "Ten");
numberList.stream()
.forEach(System.out::println);
System.out.println("=====Map=====");
numberList.stream()
.collect(Collectors.toMap(
number -> number,
number -> number
)
)
.keySet()
.forEach(System.out::println);
}
}
しかし、この実装方法で作成したMapからkeySet()を呼び出してをさらにstreamでループ処理しようとした際に、Listの順序が担保されないMapオブジェクトが生成されてしまいます。
=====List=====
One
Two
three
For
Five
Six
Seven
Eight
Nine
Ten
=====Map=====
Eight
Five
Six
One
Nine
For
Seven
Ten
Two
three
原因
toMapを使う場合、引数にMapクラスを何も指定しないとHashMapクラスで生成されます。
そしてご存知の通りHashMapクラスは要素の一意性をkeyのハッシュ値で管理するので元々順序を保証してくれません。
勘違いしやすいケース
以下のようなとてもシンプルなListの場合はぱっと見順序を保持していそうですが、たまたまハッシュ値と順序が一緒なだけなので、要素がオブジェクトになった途端順序がバラバラになってしまいます。
import java.util.Arrays;
import java.util.List;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class Sample
{
public static void main( String... args)
{
System.out.println("=====List=====");
List<Integer> numberList = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
numberList.stream()
.forEach(System.out::println);
System.out.println("=====Map=====");
numberList.stream()
.collect(Collectors.toMap(
number -> number,
number -> number)
)
.keySet()
.forEach(System.out::println);
}
}
=====List=====
1
2
3
4
5
6
7
8
9
10
=====Map=====
1
2
3
4
5
6
7
8
9
10
試しにリストをハッシュとは逆順にすると、、、、
import java.util.Arrays;
import java.util.List;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class Sample
{
public static void main( String... args)
{
System.out.println("=====List=====");
List<Integer> numberList = Arrays.asList(10,9,8,7,6,5,4,3,2,1);
numberList.stream()
.forEach(System.out::println);
System.out.println("=====Map=====");
numberList.stream()
.collect(Collectors.toMap(
number -> number,
number -> number)
)
.keySet()
.forEach(System.out::println);
}
}
=====List=====
10
9
8
7
6
5
4
3
2
1
=====Map=====
1
2
3
4
5
6
7
8
9
10
はい残念。
解決方法
なので、
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper)
ではなく、
public static <T, K, U, M extends Map<K, U>>
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapFactory)
を使い、第四引数にてLinkedHashMapクラスを渡し、明示的に使用するMapクラス指定することで解決が可能となります。
ちなみに、LinkedHashMapクラスとは追加した順番を保持してくれる便利なMapクラスとなっています。
今回は実際にこちらを使って追加した順番を担保するサンプルを作ってみようと思います。
手順
collect関数の引数を修正
修正方法はとても簡単です。
以下のようにCollectors.toMapの第三引数と第四引数を以下のように追加するだけでOKです。
import java.util.Arrays;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.stream.Collectors;
public class Sample
{
public static void main( String... args)
{
System.out.println("=====List=====");
List<String> numberList = Arrays.asList("One","Two", "three", "For", "Five", "Six", "Seven", "Eight", "Nine", "Ten");
numberList.stream()
.forEach(System.out::println);
System.out.println("=====Map=====");
numberList.stream()
.collect(Collectors.toMap(
number -> number,
number -> number,
(u, v) -> v, // 追加
LinkedHashMap::new // 追加
)
)
.keySet()
.forEach(System.out::println);
}
}
確認
実行すると以下のような形になると思います。
=====List=====
One
Two
three
For
Five
Six
Seven
Eight
Nine
Ten
=====Map=====
One
Two
three
For
Five
Six
Seven
Eight
Nine
Ten
完璧ですね♪
TreeMapでは怪しい
LinkedHashMapではなく、TreeMapでもいけるという記事をいくつか見ましたので確かめてみました、。
import java.util.Arrays;
import java.util.List;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class Sample
{
public static void main( String... args)
{
System.out.println("=====List=====");
List<String> numberList = Arrays.asList("One","Two", "three", "For", "Five", "Six", "Seven", "Eight", "Nine", "Ten");
numberList.stream()
.forEach(System.out::println);
System.out.println("=====Map=====");
numberList.stream()
.collect(Collectors.toMap(
number -> number,
number -> number,
(u, v) -> v,
TreeMap::new
)
)
.keySet()
.forEach(System.out::println);
}
}
=====List=====
One
Two
three
For
Five
Six
Seven
Eight
Nine
Ten
=====Map=====
Eight
Five
For
Nine
One
Seven
Six
Ten
Two
three
むむっ。
TreeMapクラスは通常のHashMapクラスと同様にハッシュによる順序に自動ソートされるのでうまくいきませんでした。
しかし、引数を渡さないtoMapと同様に、Listの値によってはうまく順序を保っているように見えてしまうので注意が必要です。
import java.util.Arrays;
import java.util.List;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class Sample
{
public static void main( String... args)
{
System.out.println("=====List=====");
List<Integer> numberList = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
numberList.stream()
.forEach(System.out::println);
System.out.println("=====Map=====");
numberList.stream()
.collect(Collectors.toMap(
number -> number,
number -> number,
(u, v) -> v,
TreeMap::new
)
)
.keySet()
.forEach(System.out::println);
}
}
=====List=====
1
2
3
4
5
6
7
8
9
10
=====Map=====
1
2
3
4
5
6
7
8
9
10
終わりに
以上のように簡単にList要素の順番に担保されたMapオブジェクトを生成することが可能になりました。
お困りの方は、こちらのやり方を参考にしてみてください♪
