Javaにおける、効率の良い一覧データの回し方

こんにちは。 エキサイト株式会社の三浦です。

コードを書いている時、たまに「一覧データの中から条件に合ったものを持ってくる」ことが必要になる場合があります。 一覧データの件数自体が大したことがなかったり、1~2件程度を持ってくるのであれば filter を使えばいいですが、一覧データの件数や試行回数が多くなってくると、パフォーマンスが気になってきます。

今回は、高パフォーマンスで一覧データから合致データを持ってくる方法を紹介します。

一般的にどうするか

一般的に、一覧データの中から条件にあったものを持ってくる場合は以下のようにすると思います。

@Value
public class SampleModel {
   Integer id;
   String data;
}

public class Sample {
    private SampleModel doSample() {
        List<SampleModel> sampleModelList = List.of(
            new SampleModel(1, "aaa"),
            new SampleModel(2, "bbb"),
            new SampleModel(3, "ccc"),
            new SampleModel(4, "ddd"),
            new SampleModel(5, "eee")
        );
        Integer expectedId = 2;

        return sampleModelList
            .stream()
            .filter(sampleModel -> sampleModel.id == expectedId)
            .collect(Collectors.toList())
            .get(0);
    }
}

5件の SampleModel の中から、 id が2のものを取得してくる処理です。 この条件であれば、これで十分でしょう。

ですが、以下の場合はどうでしょうか?

@Value
public class SampleModel {
   Integer id;
   String data;
}

public class Sample {
    private List<SampleModel> doSample() {
        List<SampleModel> sampleModelList = List.of(
            new SampleModel(1, "aaa"),
            new SampleModel(2, "bbb"),
            new SampleModel(3, "ccc"),
            new SampleModel(4, "ddd"),
            new SampleModel(5, "eee"),
            // 大量にデータが入る
            new SampleModel(9999, "end")
        );
        List<Integer> expectedIdList = List.of(
            2,
            5,
            87,
            // 大量にデータが入る
            8799
        );

        return expectedIdList
            .stream()
            .map(expectedId -> doFilter(expectedId, sampleModelList))
            .collect(Collectors.toList());
    }

    private SampleModel doFilter(Integer expectedId, List<SampleModel> sampleModelList) {
        return sampleModelList
            .stream()
            .filter(sampleModel -> sampleModel.id == expectedId)
            .collect(Collectors.toList())
            .get(0);
    }
}

大量にある SampleModel の中から、指定された大量の id 一覧に合致するものを持ってくる処理です。 動作自体はこれで問題ありませんが、各 List が大量にある上に、 Listmap の中で Listfilter 処理を行っているため、パフォーマンス的に良くないことは容易に想像できます。

では、このパフォーマンスを改善するにはどうすれば良いでしょうか。

HashMapを使って改善する

一覧データを扱う上で、Javaには List 形式の他に Map 形式と呼ばれるものがあります。

List 形式が、

0 -> sampleModelA
1 -> sampleModelB
2 -> sampleModelC
...

のように、自動的に割り振られた数値キーの一覧であるのに対し、 Map 形式は

ID1 -> sampleModelA
ID2 -> sampleModelB
ID3 -> sampleModelC
...

のように、明示的に指定したキーを元にした一覧です。

Map の中でもその Map の保存形式ごとに種類があり、 HashMapハッシュ値を使って保存しているため、ランダムアクセスに強い保存形式の Map になります。

この「ハッシュ値を使っているためにランダムアクセスに強い」という HashMap の特性を使うことで、先程のコードのパフォーマンスを改善できます。

具体的には、以下のような処理を行います。

@Value
public class SampleModel {
   Integer Id;
   String data;
}

public class Sample {
    private List<SampleModel> doSample() {
        Map<Integer, SampleModel> sampleModelMap = new HashMap<Integer, SampleModel>() {
            {
                put(1, new SampleModel(1, "aaa"));
                put(2, new SampleModel(2, "bbb"));
                put(3, new SampleModel(3, "ccc"));
                // 大量にデータが入る
                put(9999, new SampleModel(9999, "end"));
            }
        };
        List<Integer> expectedIdList = List.of(
            2,
            5,
            87,
            // 大量にデータが入る
            8799,
        );

        return expectedIdList
            .stream()
            .map(expectedId -> sampleModelMap.get(expectedId))
            .collect(Collectors.toList());
    }
}

HashMap を使ってフィルタリングを行うようにした結果、ひたすら Listfilter を行っていたときに比べてパフォーマンスが上がったはずです。

最後に

扱うデータ量が少ないのであればパフォーマンスをそこまで考慮する必要はないですが、大量のデータになってくると、段々とパフォーマンスも考慮する必要が出てきます。 Javaには同じ一覧データを扱う上でも様々な選択肢があるので、一度確認してみてもいいかもしれません。