デトフィア

プログラミング

JavaのCollectionUtils.getCardinalityMapを少しだけ速くする

CollectionUtils.getCardinalityMapこのメソッドは面白いです。

イテレート可能なオブジェクトの要素を全てkeyにしたMapを作ります。 このMapのvalueは数値の1です。keyが重複した場合はvalueをインクリメントさせます

以下のテストで動きを確認します

@Test
public void test_get_countmap(){
    List<String> strs = List.of("abc","def","ghi","abc");
    Map<Object, Integer> count = CollectionUtils.getCardinalityMap(strs);
    assertEquals(count.get("abc"),2);
    assertEquals(count.get("ghi"),1);
    assertEquals(count.size(),3);
}

ちなみに実装は以下のコードです

public static <O> Map<O, Integer> getCardinalityMap(final Iterable<? extends O> coll) {
    final Map<O, Integer> count = new HashMap<>();
    for (final O obj : coll) {
        final Integer c = count.get(obj);
        if (c == null) {
            count.put(obj, Integer.valueOf(1));
        } else {
            count.put(obj, Integer.valueOf(c.intValue() + 1));
        }
    }
    return count;
}

果たしてこれを改造するとしたらどうでしょうか そもそもこのコードはそこまで改造する必要はありません あえて言うならば、最初のcount.get(obj)以降動きが、if文を読むまでは理解できないことくらいでしょうか。 この場合はメソッド抽出でもできればキレイになりますが、Utilクラスにそんなことまで求めるのはナンセンスかもしれません。

無理矢理コードを書きました

public static <T> Map<T,Integer> getCardinalityMap(final Iterable<? extends T> coll){
    final Map<T, Integer> count = new HashMap<>();
    coll.forEach(item -> {
        final Integer c = count.get(item);
        if (c == null) {
            count.put(item, Integer.valueOf(1));
        } else {
            count.put(item, Integer.valueOf(c.intValue() + 1));
        }
    });
    return count;
}

foreachを使っているのに元々のメソッドとほとんど何も変わっているように見えません。 ここでメソッド抽出をして少しでもわかるように修正します

public static <T> Map<T,Integer> getCardinalityMap(final Iterable<? extends T> coll){
    final Map<T, Integer> count = new HashMap<>();
    coll.forEach(item -> {
        count.put(item, getCardinalityValue(item,count));
    });
    return count;
}

private static <T> int getCardinalityValue(T item, Map<T,Integer> count){
    final Integer c = count.get(item);
    if (c == null) {
        return 1;
    } else {
        return Integer.valueOf(c.intValue() + 1);
    }
}

countにputしているのはCardinalityな値なのねというのは抽象的に理解できるようになりました

速度が少しだけ早くなります。 また大量のオブジェクトを突っ込む処理を行って単純な速度を比較します

@Test
public void test_count(){
    class Item{
        public final int id;
        public final String name;
        public Item(int id, String name){
            this.id = id;
            this.name = name;
        }
    }
    // 大量のオブジェクトを仕込む
    List<Item> lista = new ArrayList<>();
    for(int i = 0; i < 300000; i++){
        Item item = new Item(i,"商品" + String.valueOf(i));
        lista.add(item);
    }

    long startTime = System.currentTimeMillis();
    Map<Object, Integer> counta = CollectionUtils.getCardinalityMap(lista);
    long endTime = System.currentTimeMillis();
    System.out.println("処理時間:" + (endTime - startTime) + " ms");

    startTime = System.currentTimeMillis();
    Map<Object, Integer> countb = MyCollectionUtils.getCardinalityMap(lista);
    endTime = System.currentTimeMillis();
    System.out.println("処理時間:" + (endTime - startTime) + " ms");

}

◆1回目 処理時間:100 ms 処理時間:60 ms

◆2回目 処理時間:110 ms 処理時間:80 ms

◆3回目 処理時間:110 ms 処理時間:72 ms

◆4回目 処理時間:105 ms 処理時間:75 ms

◆5回目 処理時間:116 ms 処理時間:83 ms

◆6回目 処理時間:100 ms 処理時間:80 ms

◆7回目 処理時間:105 ms 処理時間:71 ms

何度か実施しましたが、どれも既存の処理よりも速度が遅くなるということはありませんでした。