Java でマッピング・フィルタリング・畳み込み - FunctionalJava, totallylazy, Commons Collections, Guava, Java 8 Lambda

Java でコレクションのマッピング(map)・フィルタリング(filter)・畳み込み(foldLeft)のような処理を使いたいケースがありますが、今のところ標準で用意されていません。

そこで、以下のようなライブラリを使って試してみました。

ついでに、Java の次期バージョンである Java 8 の Early Access 版も試してみました。

サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20121027/

はじめに

下記 ProductItem クラスを使って以下の (1) 〜 (3) の処理を実装してみます。

  • (1) name からなるコレクションを作成(マッピング)
  • (2) price が 1500 以上のものを抽出(フィルタリング)
  • (3) 合計金額(price × qty の合計)を算出(畳み込み)
ProductItem.java
import java.math.BigDecimal;

public class ProductItem {
    private String id;
    private String name;
    private BigDecimal price;
    private int qty;

    public ProductItem(String id, String name, BigDecimal price, int qty) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.qty = qty;
    }

    public String getId() {
        return id;
    }
    ・・・ 各種 getter メソッド ・・・
}

まず Groovy での実装は以下のようになります。(collect・findAll・inject を使用)

GroovySample.groovy
def items = [
    new ProductItem("id1", "商品1", new BigDecimal("1000"), 1),
    new ProductItem("id2", "商品2", new BigDecimal("3000"), 2),
    new ProductItem("id3", "商品3", new BigDecimal("1500"), 3)
]
// (1) マッピング
def names = items.collect { it.name }

names.each { println it }

println "-----"

// (2) フィルタリング
def highItems = items.findAll { it.getPrice() >= 1500 }

highItems.each { println it.name }

println "-----"

// (3) 畳み込み
def total = items.inject(0) {a, b ->
    a + b.price * b.qty
}

println total
実行結果
> groovy GroovySample.groovy
商品1
商品2
商品3
-----
商品2
商品3
-----
11500

ついでに、Scala だと以下のようになります。

ScalaSample.scala
import scala.math.BigDecimal._

val items = List(
    new ProductItem("id1", "商品1", BigDecimal("1000").bigDecimal, 1),
    //以下でも可
    //new ProductItem("id1", "商品1", 1000.bigDecimal, 1),
    new ProductItem("id2", "商品2", BigDecimal("3000").bigDecimal, 2),
    new ProductItem("id3", "商品3", BigDecimal("1500").bigDecimal, 3)
)
// (1) マッピング
val names = items.map { _.getName() }

names.foreach { println }

println("-----")

// (2) フィルタリング
val highItems = items.filter { _.getPrice() >= 1500 }

highItems.foreach { it => println(it.getName()) }

println("-----")

// (3) 畳み込み
def total = items.foldLeft(0: BigDecimal) {(a, b) =>
    a + b.getPrice() * b.getQty()
}

println(total)
実行結果
> scala -cp .; ScalaSample.scala
商品1
商品2
商品3
-----
商品2
商品3
-----
11500

Functional Java

それでは Functional Java から実装してみます。

Functional Java では fj.data.List の map・filter・foldLeft メソッドが使えます。

ちなみに、要素の出力に foreach とか使ってますが普通に拡張 for 文を使えます。(Functional Java 以降のサンプルも同様)

FunctionalJavaSample.java
・・・
import fj.Effect;
import fj.F;
import fj.F2;
import fj.data.List;
import static fj.data.List.*;

public class FunctionalJavaSample {
    public static void main(String[] args) {
        List<ProductItem> items = list(
            new ProductItem("id1", "商品1", new BigDecimal("1000"), 1),
            new ProductItem("id2", "商品2", new BigDecimal("3000"), 2),
            new ProductItem("id3", "商品3", new BigDecimal("1500"), 3)
        );
        // (1) マッピング
        List<String> names = items.map(new F<ProductItem, String>() {
            public String f(ProductItem it) {
                return it.getName();
            }
        });

        names.foreach(new Effect<String>() {
            public void e(String n) {
                System.out.println(n);
            }
        });
        //以下でも可
        /*
        for (String n : names) {
            System.out.println(n);
        }
        */

        System.out.println("-----");

        // (2) フィルタリング
        List<ProductItem> highItems = items.filter(new F<ProductItem, Boolean>() {
            public Boolean f(ProductItem it) {
                return it.getPrice().compareTo(new BigDecimal("1500")) >= 0;
            }
        });

        highItems.foreach(new Effect<ProductItem>() {
            public void e(ProductItem it) {
                System.out.println(it.getName());
            }
        });

        System.out.println("-----");

        // (3) 畳み込み
        BigDecimal total = items.foldLeft(
            new F2<BigDecimal, ProductItem, BigDecimal>() {
                public BigDecimal f(BigDecimal a, ProductItem b) {
                    return a.add(b.getPrice().multiply(new BigDecimal(b.getQty())));
                }
            },
            BigDecimal.ZERO
        );

        System.out.println(total);
    }
}
実行結果
> javac -cp .;functionaljava-3.1.jar FunctionalJavaSample.java
> java -cp .;functionaljava-3.1.jar FunctionalJavaSample
商品1
商品2
商品3
-----
商品2
商品3
-----
11500

totallylazy

totallylazy では Sequence の map・filter・foldLeft で実装できます。インターフェース名などが異なるだけで Functional Java とほとんど同じです。

TotallylazySample.java
・・・
import com.googlecode.totallylazy.Callable1;
import com.googlecode.totallylazy.Callable2;
import com.googlecode.totallylazy.Predicate;
import com.googlecode.totallylazy.Sequence;
import static com.googlecode.totallylazy.Sequences.*;

public class TotallylazySample {
    public static void main(String[] args) {
        Sequence<ProductItem> items = sequence(
            new ProductItem("id1", "商品1", new BigDecimal("1000"), 1),
            new ProductItem("id2", "商品2", new BigDecimal("3000"), 2),
            new ProductItem("id3", "商品3", new BigDecimal("1500"), 3)
        );
        // (1) マッピング
        Sequence<String> names = items.map(new Callable1<ProductItem, String>() {
            public String call(ProductItem it) {
                return it.getName();
            }
        });

        names.each(new Callable1<String, Void>() {
            public Void call(String n) {
                System.out.println(n);
                return null;
            }
        });
        //以下でも可
        /*
        for (String n : names) {
            System.out.println(n);
        }
        */

        System.out.println("-----");

        // (2) フィルタリング
        Sequence<ProductItem> highItems = items.filter(new Predicate<ProductItem>() {
            public boolean matches(ProductItem it) {
                return it.getPrice().compareTo(new BigDecimal("1500")) >= 0;
            }
        });

        highItems.each(new Callable1<ProductItem, Void>() {
            public Void call(ProductItem it) {
                System.out.println(it.getName());
                return null;
            }
        });

        System.out.println("-----");

        // (3) 畳み込み
        BigDecimal total = items.foldLeft(BigDecimal.ZERO,
            new Callable2<BigDecimal, ProductItem, BigDecimal>() {
                public BigDecimal call(BigDecimal a, ProductItem b) {
                    return a.add(b.getPrice().multiply(new BigDecimal(b.getQty())));
                }
            }
        );

        System.out.println(total);
    }
}
実行結果
> javac -cp .;totallylazy-850.jar TotallylazySample.java
> java -cp .;totallylazy-850.jar TotallylazySample
商品1
商品2
商品3
-----
商品2
商品3
-----
11500

Commons Collections

Commons Collections では CollectionUtils の collect・select を使ってマッピング・フィルタリングを実装できますが、畳み込みは用意されていないようです。

なお、ジェネリクスに対応していない点にも注意が必要です。

CommonsCollectionsSample.java
・・・
import org.apache.commons.collections.Closure;
import org.apache.commons.collections.Predicate;
import static org.apache.commons.collections.TransformerUtils.*;
import static org.apache.commons.collections.CollectionUtils.*;

public class CommonsCollectionsSample {
    public static void main(String[] args) {
        List<ProductItem> items = Arrays.asList(
            new ProductItem("id1", "商品1", new BigDecimal("1000"), 1),
            new ProductItem("id2", "商品2", new BigDecimal("3000"), 2),
            new ProductItem("id3", "商品3", new BigDecimal("1500"), 3)
        );
        // (1) マッピング
        Collection names = collect(items, invokerTransformer("getName"));

        forAllDo(names, new Closure() {
            public void execute(Object n) {
                System.out.println(n);
            }
        });
        // 以下でも可
        /*
        for (Object n : names) {
            System.out.println(n);
        }
        */

        System.out.println("-----");

        // (2) フィルタリング
        Collection highItems = select(items, new Predicate() {
            public boolean evaluate(Object obj) {
                ProductItem it = (ProductItem)obj;
                return it.getPrice().compareTo(new BigDecimal("1500")) >= 0;
            }
        });

        forAllDo(highItems, new Closure() {
            public void execute(Object obj) {
                ProductItem it = (ProductItem)obj;
                System.out.println(it.getName());
            }
        });

        System.out.println("-----");
        ・・・
    }
    ・・・
}
実行結果
> javac -cp .;commons-collections-3.2.1.jar CommonsCollectionsSample.java
> java -cp .;commons-collections-3.2.1.jar CommonsCollectionsSample
商品1
商品2
商品3
-----
商品2
商品3
-----

Guava

Guava では Collections2 の transform・filter を使ってマッピング・フィルタリングを実装できますが、Commons Collections と同様に畳み込みは用意されていないようです。

GuavaSample.java
・・・
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import static com.google.common.collect.Collections2.*;

public class GuavaSample {
    public static void main(String[] args) {
        List<ProductItem> items = Arrays.asList(
            new ProductItem("id1", "商品1", new BigDecimal("1000"), 1),
            new ProductItem("id2", "商品2", new BigDecimal("3000"), 2),
            new ProductItem("id3", "商品3", new BigDecimal("1500"), 3)
        );

        // (1) マッピング
        Collection<String> names = transform(items, new Function<ProductItem, String>() {
            public String apply(ProductItem it) {
                return it.getName();
            }
        });

        for (String n : names) {
            System.out.println(n);
        }

        System.out.println("-----");

        // (2) フィルタリング
        Collection<ProductItem> highItems = filter(items, new Predicate<ProductItem>() {
            public boolean apply(ProductItem it) {
                return it.getPrice().compareTo(new BigDecimal("1500")) >= 0;
            }
        });

        for (ProductItem it : highItems) {
            System.out.println(it.getName());
        }

        System.out.println("-----");
        ・・・
    }
    ・・・
}
実行結果
> javac -cp .;guava-13.0.1.jar GuavaSample.java
> java -cp .;guava-13.0.1.jar GuavaSample
商品1
商品2
商品3
-----
商品2
商品3
-----

Java SE 8 Early Access with Lambda Support (b62)

現時点の Java 8 では Stream の map・filter・fold を使う事ができ、ラムダ式によって実装がすっきりします。

ちなみに、Stream は今のところ Iterable では無いようです。*1

Java8Sample.java
・・・
import java.util.streams.Stream;

public class Java8Sample {
    public static void main(String[] args) {
        List<ProductItem> items = Arrays.asList(
            new ProductItem("id1", "商品1", new BigDecimal("1000"), 1),
            new ProductItem("id2", "商品2", new BigDecimal("3000"), 2),
            new ProductItem("id3", "商品3", new BigDecimal("1500"), 3)
        );
        // (1) マッピング
        Stream<String> names = items.stream().map(it -> it.getName());

        names.forEach(n -> System.out.println(n));

        System.out.println("-----");

        // (2) フィルタリング
        Stream<ProductItem> highItems = items.stream().filter(it -> it.getPrice().compareTo(new BigDecimal("1500")) >= 0);

        highItems.forEach(it -> System.out.println(it.getName()));

        System.out.println("-----");

        // (3) 畳み込み
        BigDecimal total = items.stream().fold(
            () -> BigDecimal.ZERO, 
            (a, b) -> a.add(b.getPrice().multiply(new BigDecimal(b.getQty()))), 
            null
        );

        System.out.println(total);
    }
}
実行結果
> javac Java8Sample.java
> java Java8Sample
商品1
商品2
商品3
-----
商品2
商品3
-----
11500

*1:そのため、Stream を拡張 for 文に直接指定できません