BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル 実例で学ぶGS Collections – Part 2

実例で学ぶGS Collections – Part 2

ブックマーク

原文(投稿日:2014/11/10)へのリンク

実例で学ぶGS Collections – Part 1では、GS Collectionsを使ってさまざまなコレクションをフィルタする方法をお見せしました。

selectメソッドを呼ぶ方法では、Predicateをラムダとして引数に、selectWithではPredicate2をメソッド参照として渡しました。

GS Collectionsにはselect、reject、detect、anySatisfy、allSatisfy、noneSatisfy、count、partitionのようにPredicateを引数にとるメソッドが数多く存在します。

同様にselectWith、rejectWith、detectWith、anySatisfyWith、allSatisfyWith、noneSatisfyWith、countWith、partitionWithはPredicate2を引数とします。

Part2では、これらのPredicatesをとる2種類のメソッドの詳細を紹介した後に、collect、flatCollect、groupBy、groupByEachなどの、Functionと呼ばれる引数をとり別のコレクションに変換するメソッドの例を紹介していきます。

そして、オブジェクトのコンテナからプリミティブ型のコンテナに変換する方法と、プリミティブ型のコンテナに備わっているAPIの活用法をお見せします。いくつかの実例では下図に示したシンプルなドメインを使って説明します。

サンプルコードはユニットテストとして書かれており、実行にはJava 8が必要です。

[図をクリックして拡大]

本稿の例を読み終わるころには、GS Collectionsで開発された豊富で完成されたAPIをさらに探求したいと興味を持っていただけるかと思います。

例2: コレクション内の要素がひとつでも条件に一致するかどうかを判別する

anySatisfy/anySatisfyWithメソッドを使用して下記のように2通りの方法で書けます。

@Test
public void doAnyPeopleHaveCats()
{
    Predicate<Person> predicate = person -> person.hasPet(PetType.CAT);
    boolean result =
	this.people.anySatisfy(predicate);
    Assert.assertTrue(result);

    boolean result1 =
	this.people.anySatisfyWith(Person::hasPet, PetType.CAT);
    Assert.assertTrue(result1);
}

例3: コレクション内のすべての要素が条件に一致するかどうかを判別する

allSatisfy/allSatisfyWithメソッドを使用して下記のように2通りの方法で書けます。

@Test
public void doAllPeopleHaveCats()
{    
    boolean result =        
        this.people.allSatisfy(person -> person.hasPet(PetType.CAT));    
    Assert.assertFalse(result);    

    boolean result1 =        
        this.people.allSatisfyWith(Person::hasPet, PetType.CAT);    
    Assert.assertFalse(result1);
}

例4: 条件に一致する要素がコレクション内に全くないかどうかを判別する

noneSatisfy/noneSatisfyWithメソッドを使用して下記のように2通りの方法で書けます。

@Test
public void doNoPeopleHaveCats()
{
    boolean result =
	this.people.noneSatisfy(person -> person.hasPet(PetType.CAT));
    Assert.assertFalse(result);

    boolean result1 =
        this.people.noneSatisfyWith(Person::hasPet, PetType.CAT);
    Assert.assertFalse(result1);
}

例5: 条件に一致する要素を数える

count/countWithメソッドを使用して下記のように2通りの方法で書けます。

@Test
public void howManyPeopleHaveCats()
{
    int count =
	this.people.count(person -> person.hasPet(PetType.CAT));
    Assert.assertEquals(2, count);

    int count1 =
        this.people.countWith(Person::hasPet, PetType.CAT);
    Assert.assertEquals(2, count1);
}

例6: 条件に一致する一番最初の要素を見つける

detect/detectWithメソッドを使用して下記のように2通りの方法で書けます。

@Test
public void findPersonNamedMarySmith()
{
    Person result =
        this.people.detect(person -> person.named("Mary Smith"));
    Assert.assertEquals("Mary", result.getFirstName());
    Assert.assertEquals("Smith", result.getLastName());

    Person result1 =
        this.people.detectWith(Person::named, "Mary Smith");
    Assert.assertEquals("Mary", result1.getFirstName());
    Assert.assertEquals("Smith", result1.getLastName());
}

any/all/noneSatisyとdetectは短絡評価で実装されたメソッドの例で、場合によってはコレクション内のすべての要素をループし終わる前に結果を返します。たとえば、anySatisfyは論理和の演算と同様に、Predicateの条件を満たす要素を見つけるとすぐにtrueを返し、すべてのコレクションをイテレートし終わった場合にfalseを返します。

これから挙げる例も同様にPredicate/Predicate2を引数にとりますが、短絡評価は行いません。

例7: コレクション内の要素を選択する

本稿のPart 1ですでにコレクションをフィルタする例を多数挙げました。ここではPerson/Petドメインを用いて復習してみましょう。コレクションをフィルタするメソッドはselect/selectWithです。

@Test
public void getPeopleWithCats()
{
    MutableList<Person> peopleWithCats =
        this.people.select(person -> person.hasPet(PetType.CAT))
    Verify.assertSize(2, peopleWithCats);

    MutableList<Person> peopleWithCats1 =
        this.people.selectWith(Person::hasPet, PetType.CAT);
    Verify.assertSize(2, peopleWithCats1);
}

例8: 条件に一致しない要素をコレクションから見つける

reject/rejectWithメソッドを使用して下記のように2通りの方法で書けます。

@Test
public void getPeopleWhoDontHaveCats()
{
    MutableList<Person> peopleWithNoCats =
        this.people.reject(person -> person.hasPet(PetType.CAT));
    Verify.assertSize(5, peopleWithNoCats);
   
    MutableList<Person> peopleWithNoCats1 =
        this.people.rejectWith(Person::hasPet, PetType.CAT);
    Verify.assertSize(5, peopleWithNoCats1);
}

例9: 条件に一致する要素と一致しない要素にコレクションを分割する

partition/partitionWithメソッドを使用して下記のように2通りの方法で書けます。

@Test
public void partitionPeopleByCatOwnersAndNonCatOwners()
{
    PartitionMutableList<Person> catsAndNoCats =
	this.people.partition(person -> person.hasPet(PetType.CAT));
    Verify.assertSize(2, catsAndNoCats.getSelected());
    Verify.assertSize(5, catsAndNoCats.getRejected());

    PartitionMutableList<Person> catsAndNoCats1 =
        this.people.partitionWith(Person::hasPet, PetType.CAT);
    Verify.assertSize(2, catsAndNoCats1.getSelected());
    Verify.assertSize(5, catsAndNoCats1.getRejected());
}

上記の例では、PartitionMutableListと呼ばれる特別な型が返されます。このインターフェースの親インターフェースはPartitionIterableと呼ばれ、getSelected()とgetRejected()の2つのメソッドを持ちます。これらのメソッドはPartitionIterableのサブクラス内で共変な型を返すように実装されています。例えば、PartitionMutableListでは両メソッドともにMutableListを返します。親インターフェースのPartitionIterableでは、返り値の型はRichIterableとして定義されています。

以上がPredicate/Predicate2を引数にとるAPIの紹介です。ここからは、Function/Function2を引数にとるAPIを紹介していきます。

例10: コレクションを元に別のコレクションに変換する

collect/collectWithメソッドを使用して下記のように2通りの方法で書けます。

@Test
public void getTheNamesOfBobSmithPets()
{
    Person person =
	this.people.detectWith(Person::named, "Bob Smith");
    MutableList<Pet> pets = person.getPets();
    MutableList<String> names =
        pets.collect(Pet::getName);
    Assert.assertEquals("Dolly, Spot", names.makeString());
}

上記の例では”Bob Smith”という名前の人を探し、彼が飼っているペットのリストを得て最終的にMutableList<String>に変換しています。ペットのリストから名前のリストをcollectしているのです。

例11: コレクションのコレクションを要素ごとに展開する

型がコレクションである要素に対してcollectして、最終的に展開されたコレクションを得たい場合は、flatCollectが使えます。

@Test
public void getAllPets()
{
    Function<Person, Iterable<PetType>> function = person ->
person.getPetTypes();
    Assert.assertEquals(
        UnifiedSet.newSetWith(PetType.values()),
        this.people.flatCollect(function).toSet()
    );
    Assert.assertEquals(
        UnifiedSet.newSetWith(PetType.values()),
        this.people.flatCollect(Person::getPetTypes).toSet()
    );
}

上記のひとつめの例では、flatCollectが引数にとる型を明確にするために、ラムダを変数functionに抽出しています。collectメソッドがFunction<? super T, ? extends V>をとるのに対し、flatCollectメソッドはFunction<? super T, ? extends Iterable<V>>をとります。言い換えると、flatCollectに渡されるFunctionはなんらかのIterable型を返す必要があります。

例12: Functionの返り値でコレクションをグルーピングする

groupByメソッドを使用して下記のように書けます。

@Test
public void groupPeopleByLastName()
{
    Multimap<String, Person> byLastName =
this.people.groupBy(Person::getLastName);
    Verify.assertIterableSize(3, byLastName.get("Smith"));
}

上記の例では、peopleコレクションが苗字によってグルーピングされます。テストでは苗字がSmithの人が3人いることがわかります。groupByの結果はMultimapで返されます。MultimapはMap<K, Iterable<V>>とほとんど同様なものと考えて差し支えありません。GS Collectionsにはそれぞれのコンテナ(List、Set、Bag、SortedBag、SortedSet)に特化したMultimapがあり、それぞれミュータブル、イミュータブルな型が存在しています。この例では親インターフェースであるMultimapを使用していますが、ListMultimapもしくはMutableListMultimapと特化した型で書くこともできます。

例13: 複数のキーを返すFunctionに基づいてコレクションをグルーピングする

groupByEachメソッドを使用して下記のように書けます。

@Test
public void groupPeopleByTheirPets()
{
    Multimap<PetType, Person> peopleByPets =
        this.people.groupByEach(Person::getPetTypes);
    RichIterable<Person> catPeople = peopleByPets.get(PetType.CAT);
    Assert.assertEquals(
        "Mary, Bob",
        catPeople.collect(Person::getFirstName).makeString()
    );
    RichIterable<Person> dogPeople = peopleByPets.get(PetType.DOG);
    Assert.assertEquals(
        "Bob, Ted",
        dogPeople.collect(Person::getFirstName).makeString()
    );
}

flatCollectの場合と同様に、groupByEachメソッドはFunction<? super T, ? extends Iterable<V>>を引数にとります。必然的にMultimap<K, T>は、値が複数のキーに対応し得るマルチインデックスの形式になります。この例ではpeopleコレクションをそれぞれの人が飼っているペットのタイプによってグルーピングしています。ひとりの人が飼っているペットは複数いることもあるので、例えばBobはそれぞれのテスト結果に現れています。この例ではcollectを使ってそれぞれのPersonから名前を集めた後、makeStringを使ってカンマで区切られた文字列に変換しています。このmakeStringにはオーバーロードされたメソッドが存在し、区切り文字や、最初と最後の文字をパラメータで指定することができます。

例14: プリミティブ型の要素の和を計算する

RichIterableにある4つのsumOfメソッド(sumOfInt、sumOfFloat、sumOfLong、sumOfDouble)のうちのひとつを使用して書けます。

@Test
public void getTotalNumberOfPets()
{
    long numberOfPets = this.people.sumOfInt(Person::getNumberOfPets);
    Assert.assertEquals(9, numberOfPets);
}

上記の例では、それぞれの人が飼っているペットの数を足し合わせ、すべての人が飼っているペットの総数を計算しています。intやfloatを足し合わせる場合、より広範囲の型であるlongまたはdoubleを返すように実装されています。sumOfIntメソッドはFunctionの特別な型であるIntFunctionを引数にとります。

public interface IntFunction
        extends Serializable
{
    int intValueOf(T anObject);
}

GS CollectionsのすべてのProcedure、Function、PredicateはSerializableを継承しています。おかげでエンジニアが自前のSerializable実装を用意しなくても、ディスクに保存したりネットワーク越しに送る際に安全にシリアライズされます。

例15: オブジェクトのコレクションとプリミティブ型のコレクション間での容易な変換

オブジェクトのコレクションからプリミティブ型のコレクションに変換したい場合、8つのプリミティブ型に特化したcollectメソッド(collectInt/Float/Long/Double/Byte/Short/Char/Boolean)が使えます。プリミティブコレクションからオブジェクトのコレクションに変換する場合は単純にcollectを使えます。おかげでAPIを使う際に流暢さを保つことができます。

@Test
public void getAgesOfPets()
{
    IntList sortedAges =
        this.people
            .asLazy()
            .flatCollect(Person::getPets)
            .collectInt(Pet::getAge)
            .toSortedList();
    IntSet uniqueAges = sortedAges.toSet();
    IntSummaryStatistics stats = new IntSummaryStatistics();
    sortedAges.forEach(stats::accept);
    Assert.assertTrue(sortedAges.allSatisfy(IntPredicates.greaterThan(0)));
    Assert.assertTrue(sortedAges.allSatisfy(i -> i > 0));
    Assert.assertFalse(sortedAges.anySatisfy(i -> i == 0));
    Assert.assertTrue(sortedAges.noneSatisfy(i -> i < 0));
    Assert.assertEquals(IntHashSet.newSetWith(1, 2, 3, 4), uniqueAges);
    Assert.assertEquals(2.0d, sortedAges.median(), 0.0);
    Assert.assertEquals(stats.getMin(), sortedAges.min());
    Assert.assertEquals(stats.getMax(), sortedAges.max());
    Assert.assertEquals(stats.getSum(), sortedAges.sum());
    Assert.assertEquals(stats.getAverage(), sortedAges.average(), 0.0);
    Assert.assertEquals(stats.getCount(), sortedAges.size());
}

上記の例では、peopleリストの人々によって飼われているペットの年齢に関する様々な問いに答えています。まず、最初にasLazy()が呼ばれています。これは中間生成されるコレクションの量を減らすためにあえて呼んでいますが、このコードではasLazy()を取り除いても問題ないでしょう。この場合、asLazy()は単にメモリーの最適化をしているに過ぎません。flatCollect()はすべての人が飼っているペットを集めてひとつのコレクション上に展開しています。次にcollectInt()はLazyIterable<Pet>をIntIterableに変換しています。それぞれのPetがgetAge()メソッドを通じて年齢に変換されます。もしasLazy()を使わなかった場合は、MutableList<Pet>からIntListに変換されます。最後にtoSortedList()メソッドを呼び、IntIterableをIntListに変換してソートしています。次の行ではtoSet()を呼んで重複のない年齢のSetを作成しています。

そこからさらに、GS Collectionsのプリミティブ型コレクションの豊富な機能の実例が挙げられています。プリミティブ型のコレクションでは、min、max、sum、average、medianなどの統計メソッドを直接扱うことができます。ここでは、Java 8から導入された新しい統計クラスのひとつであるIntSummaryStatisticsも用いました。年齢を格納したIntListに対し、intをとるメソッド参照のIntSummaryStatistics::acceptを適用して結果確認用の計算をしてます。また、前の例で紹介したany/all/nonSatisfyメソッドも、プリミティブ型のコレクションに対して使われています。

例16: コレクション内のそれぞれの要素の出現回数を数える

もしコレクション内のそれぞれの要素をすばやく数えたいならば、コレクションをBagに変換することで実現できます。Bagは、Kをある要素、Integerをその出現回数としたときのMap<K, Integer>とほぼ同様のものと考えられます。Bagには他のコレクションと同様add、removeなどの基本的なメソッドが備わっています。また、Bagには効率的に出現回数を検索したり足したり引いたりといった操作をする特別なメソッドもあります。Bagは重複をゆるして出現回数を保持するSetのようなものと考えてもよいでしょう。

@Test
public void getCountsByPetType()
{
    Bag<PetType> counts =
        this.people
            .asLazy()
            .flatCollect(Person::getPets)
            .collect(Pet::getType)
            .toBag();
    Assert.assertEquals(2, counts.occurrencesOf(PetType.CAT));
    Assert.assertEquals(2, counts.occurrencesOf(PetType.DOG));
    Assert.assertEquals(2, counts.occurrencesOf(PetType.HAMSTER));
    Assert.assertEquals(1, counts.occurrencesOf(PetType.SNAKE));
    Assert.assertEquals(1, counts.occurrencesOf(PetType.TURTLE));
    Assert.assertEquals(1, counts.occurrencesOf(PetType.BIRD));
}

現在JDKにはBagに相当する実装は存在しません。PartitionIterableやMultimapも同様です。Java 8のCollectorsクラスのメソッドでは、これらの型はそれぞれMap<K, Integer>(Collectors.counting())、Map<Boolean, List<V>>>(Collectors.partitioning())、Map<K, List<V>>(Collectors.groupingBy())としてシミュレートされています。

例17: プリミティブ型のコレクション内のそれぞれの要素の出現回数を数える

プリミティブ型のコレクションからそれぞれの要素を数えたいならば、プリミティブ用のBagを使うことができます。

@Test
public void getCountsByPetAge()
{
    IntBag counts =
        this.people
            .asLazy()
            .flatCollect(Person::getPets)
            .collectInt(Pet::getAge)
            .toBag();
    Assert.assertEquals(4, counts.occurrencesOf(1));
    Assert.assertEquals(3, counts.occurrencesOf(2));
    Assert.assertEquals(1, counts.occurrencesOf(3));
    Assert.assertEquals(1, counts.occurrencesOf(4));
    Assert.assertEquals(0, counts.occurrencesOf(5));
}

この例では、すべてのペットの年齢からIntBagを作っています。こうすることで、それぞれの年齢のペットが何匹いるのかを数えることができます。これがPart 2の最後の例になります。

以上さまざまな例を挙げてきましたが、これらはGS CollectionsのAPIを使用してできることのほんの一例にすぎません。現在RichIterableインターフェース上には100を越えるメソッドが定義されています。これらはJavaのエンジニアがコレクションを扱う際に非常に豊富な機能を与えてくれます。

今年のJavaOne 2014で、Craig Motlinと私は「 GS Collections and Java8: Functional, Fluent, Friendly and Fun!」と題したセッションに登壇しました。本セッションではJava 8のStream APIを使用した場合とGS Collectionsを使用した場合を対比する例をいくつか挙げました。スライドはJavaOne 2014のサイトもしくはGS Collections GitHub wikiから見ることが可能です。

参考として、以下に本稿で使用したテストのセットアップとドメインクラスを挙げておきます。

import com.gs.collections.api.RichIterable;
import com.gs.collections.api.bag.Bag;
import com.gs.collections.api.bag.MutableBag;
import com.gs.collections.api.bag.primitive.IntBag;
import com.gs.collections.api.block.function.Function;
import com.gs.collections.api.block.predicate.Predicate;
import com.gs.collections.api.list.MutableList;
import com.gs.collections.api.list.primitive.IntList;
import com.gs.collections.api.multimap.Multimap;
import com.gs.collections.api.partition.list.PartitionMutableList;
import com.gs.collections.api.set.primitive.IntSet;
import com.gs.collections.impl.bag.mutable.HashBag;
import com.gs.collections.impl.block.factory.Predicates2;
import com.gs.collections.impl.block.factory.primitive.IntPredicates;
import com.gs.collections.impl.list.mutable.FastList;
import com.gs.collections.impl.set.mutable.UnifiedSet;
import com.gs.collections.impl.set.mutable.primitive.IntHashSet;
import com.gs.collections.impl.test.Verify
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import java.util.IntSummaryStatistics;
 
public class PersonTest
{
    MutableList<Person> people;
 
    @Before
    public void setUp() throws Exception
    {
        this.people = FastList.newListWith(
            new Person("Mary", "Smith").addPet(PetType.CAT, "Tabby", 2),
            new Person("Bob", "Smith").addPet(PetType.CAT, "Dolly",
3).addPet(PetType.DOG, "Spot", 2),
            new Person("Ted", "Smith").addPet(PetType.DOG, "Spike", 4),
            new Person("Jake", "Snake").addPet(PetType.SNAKE, "Serpy", 1),
            new Person("Barry", "Bird").addPet(PetType.BIRD, "Tweety", 2),
            new Person("Terry", "Turtle").addPet(PetType.TURTLE, "Speedy",
1)
            new Person("Harry", "Hamster").addPet(PetType.HAMSTER, "Fuzzy",
1).addPet(PetType.HAMSTER, "Wuzzy", 1)
        );
    }

    public class Person
    {
        private String firstName;
        private String lastName;
        private MutableList<Pet> pets = FastList.newList();
 
        private Person(String firstName, String lastName)
        {
            this.firstName = firstName;
            this.lastName = lastName;
       
 
        public String getFirstName()
        {
            return this.firstName;
        }
 
        public String getLastName()
        {
            return this.lastName;
        }

        public boolean named(String name)
        {
            return name.equals(this.getFirstName() + " " +
this.getLastName());
        }
 
        public boolean hasPet(PetType petType)
        {
            return
this.pets.anySatisfyWith(Predicates2.attributeEqual(Pet::getType), petType);
        }
 
        public MutableList getPets()
        {
            return this.pets;
        }
 
        public MutableBag getPetTypes()
        {
            return this.pets.collect(Pet::getType, HashBag.newBag());
        }
 
        public Person addPet(PetType petType, String name, int age)
        {
            this.pets.add(new Pet(petType, name, age));
            return this;
        }
 
        public int getNumberOfPets()
        {
            return this.pets.size();
        }
    }
 
    public class Pet
    {
        private PetType type;
        private String name;
        private int age;
 
        public Pet(PetType type, String name, int age)
       
            this.type = type;
            this.name = name;
            this.age = age;
        }
 
        public PetType getType()
        {
            return this.type;
        }
 
        public String getName()
        {
            return this.name;
        }
 
        public int getAge()
        {
            return this.age;
        }
    }
 
    public enum PetType
    {
        CAT, DOG, HAMSTER, TURTLE, BIRD, SNAKE
    }
}

著者について

Donald Raab ゴールドマン・サックス テクノロジー部Enterprise PlatformsグループにてJVMアーキテクチャチームを統括。JSR 335 エキスパートグループ(Lambda Expressions for the Java Programming Language)の一員であり、JCP Executive Committeeのゴールドマン・サックス共同代表を務める。2001年PARAチームのテクニカルアーキテクトとしてゴールドマン・サックスに入社。2007年テクノロジー・フェロー、2013年マネージング・ディレクター就任。

翻訳者について

伊藤博志 ゴールドマン・サックス テクノロジー部Enterprise PlatformsグループのJVMアーキテクチャチームに所属するJavaエンジニア。2005年ゴールドマン・サックス入社。PARAアジアパシフィックチームのテクニカルアーキテクトを経て、現在ヴァイス・プレジデント。

GS Collectionsやゴールドマン・サックスのエンジニアリングに関する詳しい情報は、 www.gs.com/engineeringをご参照下さい。

© 2014 Goldman, Sachs & Co.

本掲載記事は、Goldman, Sachs & Coのテクノロジー部において入手した情報をもとに作成されたもので、情報の提供を目的としております。本記事によるなんらかの行動を勧誘または推奨するものではありません。本記事における意見は明示されていない限りGoldman, Sachs & Coの意見と異なるかもしれません。本記事は信頼できると思われる情報に基づいて作成されていますが、当社はその正確性、完全性に関する責任を負いません。 ご利用に際しては、ご自身の判断にてお願いします。本記事資料の一部又は全部を本ディスクレーマーなしに第三者に転送することを禁じます。

この記事に星をつける

おすすめ度
スタイル

BT