Featured image of post 【Java8 Stream】ListをMapにした際に並び順が保証されない【LinkedHashMap】

【Java8 Stream】ListをMapにした際に並び順が保証されない【LinkedHashMap】

1500文字

Listの順番を担保したMapを作りたい

ソートされたListからMapを作ると順序が変わる

Java8以降でstreamAPIを使い、ListオブジェクトからMapオブジェクトを生成したいというケースは良くあると思います。

今回の例ではとてもシンプルなListオブジェクトですが、実際にはModelなどをListの要素として持つ形になると思います。

そんな時に、すでにソートされたList(DBからの取得結果etc...)をstreamで回してMapオブジェクトにしようとする際に以下のような書き方をする場合が多いと思います。

Sample.java
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オブジェクトが生成されてしまいます。

java Sample
=====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クラスで生成されます。

[adsense-responsive]

そしてご存知の通りHashMapクラスは要素の一意性をkeyのハッシュ値で管理するので元々順序を保証してくれません

勘違いしやすいケース

以下のようなとてもシンプルなListの場合はぱっと見順序を保持していそうですが、たまたまハッシュ値と順序が一緒なだけなので、要素がオブジェクトになった途端順序がバラバラになってしまいます。

Sample.java
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);
  }
}

java Sample
=====List=====
1
2
3
4
5
6
7
8
9
10
=====Map=====
1
2
3
4
5
6
7
8
9
10

試しにリストをハッシュとは逆順にすると、、、、

Sample.java
  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);
    }
  }

java Sample
=====List=====
10
9
8
7
6
5
4
3
2
1
=====Map=====
1
2
3
4
5
6
7
8
9
10

はい残念。

解決方法

なので、

toMap
public static <T, K, U>
    Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper)

ではなく、

toMap
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です。

[adsense-responsive]

Sample.java
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);
  }
}

確認

実行すると以下のような形になると思います。

java Sample
=====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でもいけるという記事をいくつか見ましたので確かめてみました、。

Sample.java
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);
  }
}

java Sample
=====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の値によってはうまく順序を保っているように見えてしまうので注意が必要です。

Sample.java
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);
  }
}

java Sample
=====List=====
1
2
3
4
5
6
7
8
9
10
=====Map=====
1
2
3
4
5
6
7
8
9
10

終わりに

以上のように簡単にList要素の順番に担保されたMapオブジェクトを生成することが可能になりました。

お困りの方は、こちらのやり方を参考にしてみてください♪