LoginSignup
10
11

More than 5 years have passed since last update.

Eclipse Collections Kata に挑む

Posted at

概要

Java の高機能 Collection Framework である Eclipse Collections には Kata というトレーニングマテリアルが用意されています。Kata という名前は武術の「型」が元だそうです。

Eclipse Collections がどういうものかについては @opengl-8080 さんの GS Collections 使い方メモ が詳しいです。なお、GS Collections は Eclipse Collections の旧称です。

Eclipse Collections Kata とはどんなものか?

未完成の UnitTest(JUnit 形式) のコードを修正していくことで、Eclipse Collections の機能や使い方を学習できるというトレーニングマテリアルです。GitHub で公開されており、いつでもダウンロードして始めることができます。学習後に、ケースごとの使い方を具体的な例で振り返る資料としても使えそうです。

導入

下記の @deaf_tadashi さんの記事が参考になります。
Eclipse Collections Kata をやるにあたって知っておくとよさげなこと

構成

2016/05/28(Sat) 時点では pet-kata と company-kata の2種類が用意されています。pet-kata の方が問題数が少なくシンプルな問題が多いです。

pet-kata

Class Tests
Exercise1Test 4
Exercise2Test 11
Exercise3Test 3
Exercise4Test 4
Exercise5Test 5

company-kata

Class Tests
Exercise1Test 3
Exercise2Test 9
Exercise3Test 3
Exercise4Test 5
Exercise5Test 2
Exercise6Test 5
Exercise7Test 3
Exercise8Test 6

解答

実際に不完全な UnitTest のコードを修正し、テストにパスするようにしていきます。修正したテストコードは Gradle で実行してもよいですし、 IDE で1つずつ実行してもよいでしょう。IDE だと進捗をグラフィカルに把握できるのが利点です。

私の解答例

感想

早くやっておくべきだったと思います。 company-kata の Exercise8Test を除くと、結構やさしめの問題でした。ライブラリを普及させるためにはこうした教育マテリアルがあった方が、使いたい側にとっては非常に有益に思います。

単純なEclipse Collectionsの学習にとどまらず、今まで使えていなかった flat○○ 操作やメソッド参照の使い方を理解できたのは大きいです。 Eclipse Collections では特に Bag の強力さがよくわかります。ArrayIterate や Multimap も設計がよく考えられた使いやすいクラスです。入門用の学習教材であるため、すべてのクラスについての問題が用意されているわけではありませんが、これをやっておくだけで使い方に幅が出るはずです。

本質とはまったく関係ありませんが、メソッド名のあとの中括弧を改行するスタイルだとメソッド名のコピペが楽だと気付きました。


以下、実際に解いてみた際のメモです。まだ解いていない方はご覧にならないことをお勧めします。


pet-kata

Exercise1

前提

下記3つのフィールドを持つ Person クラスがあります。

Person
private final String firstName;
private final String lastName;
private final MutableList<Pet> pets = FastList.newList();

そして、Person クラスの List である people が最初に与えられています。

getFirstNamesOfAllPeople

まあ、読んで字のごとしです。

getFirstNamesOfAllPeople
@Test
public void getFirstNamesOfAllPeople() {
    final MutableList<Person> people = this.people;
    final MutableList<String> firstNames = people.collect(person -> person.getFirstName());
    final MutableList<String> expectedFirstNames
         = Lists.mutable.with("Mary", "Bob", "Ted", "Jake", "Barry", "Terry", "Harry", "John");
    Assert.assertEquals(expectedFirstNames, firstNames);
}

getNamesOfMarySmithsPets

上のとそんなに変わりません。

getNamesOfMarySmithsPets
@Test
public void getNamesOfMarySmithsPets() {
    final Person person = this.getPersonNamed("Mary Smith");
    final MutableList<Pet> pets = person.getPets();
    //Replace null, with a transformation method on MutableList.
    final MutableList<String> names = person.getPets().collect(pet -> pet.getName()); 
    Assert.assertEquals("Tabby", names.makeString());
}

getPeopleWithCats & getPeopleWithoutCats

メソッドを1つ変えるだけで解けるのでまとめます。

select が Collection に残す要素を選ぶメソッドで、reject が条件に一致する要素を Collection から除外するメソッドです。条件式に否定形を書かなくてよい点が素晴らしいです。

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

Exercise2Test

doAnyPeopleHaveCats

いつも Lambda で直接記述している Predicate を外で定義します。

doAnyPeopleHaveCats
    @Test
    public void doAnyPeopleHaveCats() {
        //replace null with a predicate which checks for PetType.CAT
        final Predicate<Person> predicate = person -> person.hasPet(PetType.CAT);
        Assert.assertTrue(this.people.anySatisfy(predicate));
    }

doAllPeopleHavePets

同上

doAllPeopleHavePets
    @Test
    public void doAllPeopleHavePets() {
        final Predicate<Person> predicate = person -> person.isPetPerson();
        //replace with something that checks if all people have pets
        final boolean result = people.allSatisfy(predicate);
        Assert.assertFalse(result);
    }

howManyPeopleHaveCats

条件に一致する要素数を数える count メソッドを使います。

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

findMarySmith

条件に一致する最初の要素を取り出すには detect メソッドを使います。IDE で最初にサジェストされます。

findMarySmith
    @Test
    public void findMarySmith() {
        final Person result = people.detect(
                person -> "Mary".equals(person.getFirstName()) && "Smith".equals(person.getLastName()));
        Assert.assertEquals("Mary", result.getFirstName());
        Assert.assertEquals("Smith", result.getLastName());
    }

getPeopleWithPets

Pet を所持していない Person を reject することで、 Pet を所持している Person の List を作ります。

getPeopleWithPets
    @Test
    public void getPeopleWithPets() {
        final MutableList<Person> petPeople = this.people.reject(person -> person.getPets().isEmpty()); //replace with only the pet owners
        Verify.assertSize(7, petPeople);
    }

なお Eclipse Collections の eclipse-collections-testutils では、 Collection のテストで便利な独自 assertion を実装しています。

Verify.assertSize(7, petPeople);

getAllPetsOfAllPeople

これ、本当に合っているのでしょうか……突然 flatCollect が出てきます。

getAllPetsOfAllPeople
    @Test
    public void getAllPetsOfAllPeople() {
        final Function<Person, Iterable<PetType>> function = person -> person.getPetTypes();
        // ???
        final MutableSet<PetType> petTypes = this.people.flatCollect(function).toSet();
        Assert.assertEquals(
                Sets.mutable.with(PetType.CAT, PetType.DOG, PetType.TURTLE, PetType.HAMSTER, PetType.BIRD, PetType.SNAKE),
                petTypes);
    }

getFirstNamesOfAllPeople

collect で変形させるだけのようです。

getFirstNamesOfAllPeople
    @Test
    public void getFirstNamesOfAllPeople() {
        final MutableList<String> firstNames = people.collect(person -> person.getFirstName());
        Assert.assertEquals(
                Lists.mutable.with("Mary", "Bob", "Ted", "Jake", "Barry", "Terry", "Harry", "John"),
                firstNames);
    }

doAnyPeopleHaveCatsRefactor

Eclipse Collections ではメソッド参照を使いやすくするために、それ用のメソッドが xxWith という名前で用意されています。

doAnyPeopleHaveCatsRefactor
    @Test
    public void doAnyPeopleHaveCatsRefactor() {
        final boolean peopleHaveCatsLambda = this.people.anySatisfy(person -> person.hasPet(PetType.CAT));
        Assert.assertTrue(peopleHaveCatsLambda);

        //use method reference, NOT lambdas, to solve the problem below
        final boolean peopleHaveCatsMethodRef = this.people.anySatisfyWith(Person::hasPet, PetType.CAT);
        Assert.assertTrue(peopleHaveCatsMethodRef);
    }

doAllPeopleHaveCatsRefactor

上に同じです。

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

        //use method reference, NOT lambdas, to solve the problem below
        final boolean peopleHaveCatsMethodRef = this.people.allSatisfyWith(Person::hasPet, PetType.CAT);
        Assert.assertFalse(peopleHaveCatsMethodRef);
    }

getPeopleWithCatsRefator

selectWith を使います。

getPeopleWithCatsRefator
    @Test
    public void getPeopleWithCatsRefator() {
        //use method reference, NOT lambdas, to solve the problem below
        final MutableList<Person> peopleWithCatsMethodRef
            = this.people.selectWith(Person::hasPet, PetType.CAT);
        Verify.assertSize(2, peopleWithCatsMethodRef);
    }

getPeopleWithoutCatsRefactor

rejectWith を使います。

getPeopleWithoutCatsRefactor
    @Test
    public void getPeopleWithoutCatsRefactor() {
        //use method reference, NOT lambdas, to solve the problem below
        final MutableList<Person> peopleWithoutCatsMethodRef
            = this.people.rejectWith(Person::hasPet, PetType.CAT);
        Verify.assertSize(6, peopleWithoutCatsMethodRef);
    }

Exercise3Test

メソッド全体を引用するのがいい加減つらくなってきたので、要点だけ書いていきます。

getCountsByPetType

要素ごとの重複数を数え上げる処理を下記のように書いていたとします。

なお、元のコードで PetType 一覧を作るのに flatCollect を使っているのに、数え上げで拡張 for 文を使っている点は特に気にしないでよいです。

Before
final MutableList<PetType> petTypes = this.people.flatCollect(Person::getPets).collect(Pet::getType);
// Try to replace MutableMap<PetType, Integer> with a Bag<PetType>
final MutableMap<PetType, Integer> petTypeCounts = Maps.mutable.empty();
for (final PetType petType : petTypes) {
    Integer count = petTypeCounts.get(petType);
    if (count == null) {
        count = 0;
    }
    petTypeCounts.put(petType, count + 1);
}

この長ったらしい処理が Bag を使うとたった2行で書けます。

After
final MutableBag<PetType> counts = Bags.mutable.empty();
petTypes.each(petType -> counts.add(petType));

便利すぎて涙が出ますね。

getPeopleByLastName

Before
final MutableMap<String, MutableList<Person>> lastNamesToPeople = Maps.mutable.empty();
        for (final Person person : this.people)
        {
            final String lastName = person.getLastName();
            MutableList<Person> peopleWithLastName = lastNamesToPeople.get(lastName);
            if (peopleWithLastName == null)
            {
                peopleWithLastName = Lists.mutable.empty();
                lastNamesToPeople.put(lastName, peopleWithLastName);
            }
            peopleWithLastName.add(person);
        }

この長ったらしい処理が Multimap (スペリングに注意、map は小文字)を使うとたった2行で書けます。

After
final MutableMultimap<String, Person> byLastNameMultimap = Multimaps.mutable.list.empty();
this.people.each(person -> byLastNameMultimap.put(person.getLastName(), person));

Multimap の factory は、まず mutable/immutable を選び、さらに list/set/sortedSet/bag を選択できます。

Eclipse Collections の Map は Pair(key-value 1組だけの Collection) との連携が意識されており、同じ処理を下記の通り書けます。

add
this.people.each(person -> byLastNameMultimap.add(Tuples.pair(person.getLastName(), person)));

getPeopleByTheirPets

set の MutableMultimap を使って改善する、というお題です。

Before
final MutableMap<PetType, MutableSet<Person>> peopleByPetType = Maps.mutable.empty();

        for (final Person person : this.people)
        {
            final MutableList<Pet> pets = person.getPets();
            for (final Pet pet : pets)
            {
                final PetType petType = pet.getType();
                MutableSet<Person> peopleWithPetType = peopleByPetType.get(petType);
                if (peopleWithPetType == null)
                {
                    peopleWithPetType = Sets.mutable.empty();
                    peopleByPetType.put(petType, peopleWithPetType);
                }
                peopleWithPetType.add(person);
            }
        }

ひとまずこのように書けます。

After
final MutableMultimap<PetType, Person> peopleByPetType = Multimaps.mutable.set.empty();
for (final Person person : this.people) {
    final MutableList<Pet> pets = person.getPets();
    for (final Pet pet : pets) {
        peopleByPetType.put(pet.getType(), person);
    }
}

Lambda in Lambda になってしまって具合は悪いですが、こんな風に書けなくもないです。

Lambda_in_Lambda
this.people.each(person -> {
    person.getPets().each(pet -> peopleByPetType.put(pet.getType(), person));
});

Exercise4Test

Collection の修正

Eclipse Collections には primitive の要素を保持できる Collection が用意されています。このテストでは IntList と IntSet を使います。

Before
MutableList<Integer> petAges = this.people
                .stream()
                .flatMap(person -> person.getPets().stream())
                .map(pet -> pet.getAge())
                .collect(Collectors.toCollection(FastList::new));

collectInt で IntList に変換できます。

After
final MutableIntList petAges = this.people
                .flatCollect(person -> person.getPets())
                .collectInt(pet -> pet.getAge());
Before
Set<Integer> uniqueAges = petAges.toSet();

toSet で変換できます。

After
IntSet uniqueAges = petAges.toSet();
Before
IntSummaryStatistics stats = petAges.stream().mapToInt(i -> i).summaryStatistics();

空の IntSummaryStatistics インスタンスを生成し、 petAges の要素を全部入れます。

After
final IntSummaryStatistics stats = new IntSummaryStatistics();
petAges.forEach(i -> {stats.accept(i);});

IntSets の使用

もちろん primitive collection 用の factory も用意されています。

Before
Assert.assertEquals(Sets.mutable.with(1, 2, 3, 4), uniqueAges);
After
Assert.assertEquals(IntSets.mutable.with(1, 2, 3, 4), uniqueAges);

便利な要素検査

Before
Assert.assertEquals(stats.getMin(), petAges.stream().mapToInt(i -> i).min().getAsInt());
Assert.assertEquals(stats.getMax(), petAges.stream().mapToInt(i -> i).max().getAsInt());
Assert.assertEquals(stats.getSum(), petAges.stream().mapToInt(i -> i).sum());
Assert.assertEquals(stats.getAverage(), petAges.stream().mapToInt(i -> i).average().getAsDouble(), 0.0);

これらを計算するメソッドが全部用意されています。

After
Assert.assertEquals(stats.getMin(), uniqueAges.min());
Assert.assertEquals(stats.getMax(), uniqueAges.max());
Assert.assertEquals(stats.getSum(), petAges.sum());
Assert.assertEquals(stats.getAverage(), petAges.average(), 0.0);

Match = Satisfy

Before
Assert.assertTrue(petAges.stream().allMatch(i -> i > 0));
Assert.assertFalse(petAges.stream().anyMatch(i -> i == 0));
Assert.assertTrue(petAges.stream().noneMatch(i -> i < 0));
After
Assert.assertTrue(petAges.allSatisfy(i -> i > 0));
Assert.assertFalse(petAges.anySatisfy(i -> i == 0));
Assert.assertTrue(petAges.noneSatisfy(i -> i < 0));

streamsToECRefactor1

Eclipse Collections を使えば余計なメソッドが少なくなるという例です。

Before
//find Bob Smith
final Person person =
                this.people.stream()
                        .filter(each -> each.named("Bob Smith"))
                        .findFirst().get();

//get Bob Smith's pets' names
final String names =
                person.getPets().stream()
                        .map(Pet::getName)
                        .collect(Collectors.joining(" & "));
After
//find Bob Smith
final Person person =
                this.people
                        .select(each -> each.named("Bob Smith"))
                        .getFirst();

//get Bob Smith's pets' names
final String names =
                person.getPets()
                        .collect(Pet::getName)
                        .makeString(" & ");

streamsToECRefactor2

Before
final Map<PetType, Long> countsStream =
                Collections.unmodifiableMap(
                        this.people.stream()
                                .flatMap(person -> person.getPets().stream())
                                .collect(Collectors.groupingBy(Pet::getType,
                                        Collectors.counting())));
After
final Bag<PetType> countsStream =
                this.people
                        .flatCollect(person -> person.getPets())
                        .collect(pet -> pet.getType())
                        .toBag();

なお、前の問題にもあったように Bag からの数値取り出しには occurrencesOf メソッドを使います。 Assertion の書き換えも必要でした。

streamsToECRefactor3

引き続き Bag の力を思い知りましょう。

Before
final List<Map.Entry<PetType, Long>> favoritesStream =
                this.people.stream()
                        .flatMap(p -> p.getPets().stream())
                        .collect(Collectors.groupingBy(Pet::getType, Collectors.counting()))
                        .entrySet()
                        .stream()
                        .sorted(Comparator.comparingLong(e -> -e.getValue()))
                        .limit(3)
                        .collect(Collectors.toList());

こうしたややこしい処理が

After
        final List<ObjectIntPair<PetType>> favoritesStream =
                this.people
                        .flatCollect(p -> p.getPets())
                        .collect(pet -> pet.getType())
                        .toBag()
                        .topOccurrences(3);

Eclipse Collections を使うとまったく必要なく、toBag() と topOccurrences(3) で解決します。

Assertion の修正

Before
Verify.assertContains(new AbstractMap.SimpleEntry<>(PetType.CAT, Long.valueOf(2)), favoritesStream);
Verify.assertContains(new AbstractMap.SimpleEntry<>(PetType.DOG, Long.valueOf(2)), favoritesStream);
Verify.assertContains(new AbstractMap.SimpleEntry<>(PetType.HAMSTER, Long.valueOf(2)), favoritesStream);

PrimitiveTuples で書き直します。

After
Verify.assertContains(PrimitiveTuples.pair(PetType.CAT, 2), favoritesStream);
Verify.assertContains(PrimitiveTuples.pair(PetType.DOG, 2), favoritesStream);
Verify.assertContains(PrimitiveTuples.pair(PetType.HAMSTER, 2), favoritesStream);

Exercise5Test

partitionPetAndNonPetPeople

partition を使うと selected と rejected 2つの Collection が生成され、それぞれ参照できます。

partition
PartitionMutableList<Person> partitionMutableList
            = people.partition(person -> 0 < person.getPets().size());

getOldestPet

flatCollect で Pet の一覧を作り、属性を指定して最大値を持つ Pet を取り出します。

maxBy
people.flatCollect(Person::getPets).maxBy(pet -> pet.getAge());

getAveragePetAge

flatCollect で Pet の一覧を作り、さらに年齢の List に変換して平均年齢を出します。

average
people.flatCollect(Person::getPets)
                .collectInt(pet -> pet.getAge())
                .average();

addPetAgesToExistingSet

すでにある Collection に要素を追加します。 Set で持っている addAll を使えばよいでしょう。

add
final MutableIntSet petAges = IntSets.mutable.with(5);
petAges.addAll(people.flatCollect(Person::getPets).collectInt(Pet::getAge));

refactorToEclipseCollections

従来の Collection を Eclipse Collections で置き換えます。

List

Before
final List<Person> people = new ArrayList<>();
people.add(new Person("Mary", "Smith").addPet(PetType.CAT, "Tabby", 2));
people.add(new Person("Bob", "Smith")
    .addPet(PetType.CAT, "Dolly", 3).addPet(PetType.DOG, "Spot", 2));
people.add(new Person("Ted", "Smith").addPet(PetType.DOG, "Spike", 4));
people.add(new Person("Jake", "Snake").addPet(PetType.SNAKE, "Serpy", 1));
people.add(new Person("Barry", "Bird").addPet(PetType.BIRD, "Tweety", 2));
people.add(new Person("Terry", "Turtle").addPet(PetType.TURTLE, "Speedy", 1));
people.add(new Person("Harry", "Hamster")
    .addPet(PetType.HAMSTER, "Fuzzy", 1).addPet(PetType.HAMSTER, "Wuzzy", 1));
people.add(new Person("John", "Doe"));

Collection を of メソッドで生成します。

After
final MutableList<Person> people = Lists.mutable.of(
    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),
    new Person("John", "Doe")
);

Set

Before
final Set<Integer> petAges = new HashSet<>();
for (final Person person : people)
{
    for (final Pet pet : person.getPets())
    {
        petAges.add(pet.getAge());
    }
}
After
final MutableSet<Integer> petAges
            = people.flatCollect(Person::getPets).collect(Pet::getAge).toSet();

完了

最後に $ gradle test ですべての UnitTest を実行しましょう。達成感に浸れます。

pet-kata.png


company-kata

前提

Company というクラスのオブジェクトが1つ(company)与えられています。

Fields

suppliers の型が配列なのが気になるところです。

Type Name
MutableList customers
Supplier[] suppliers

Methods

フィールドの getter が用意されており、メソッド参照で活用できます。また、Customer の持つ Order をすべて取得する getOrders() メソッドも用意されています。

Exercise1Test

pet-kata を解いていれば特に難しいこともないでしょう。

getCustomerNames

final MutableList<String> customerNames = customers.collect(Customer::getName);

getCustomerCities

final MutableList<String> customerCities = customers.collect(Customer::getCity);

getLondonCustomers

final MutableList<Customer> customersFromLondon
            = customers.select(customer -> "London".equals(customer.getCity()));

Exercise2Test

最初に Predicate を作り、それを使って基本的な Eclipse Collections のメソッドを動かす問題で構成されています。

事前準備

private static final Predicate<Customer> CUSTOMER_FROM_LONDON
     = customer -> "London".equals(customer.getCity());

Customer クラスの TO_CITY も実装する必要があります。

public static final Function<Customer, String> TO_CITY = Customer::getCity;

customerFromLondonPredicate

事前準備をしていると終わります。

doAnyCustomersLiveInLondon

anySatisfy を使います。

doAllCustomersLiveInLondon

allSatisfy を使います。

howManyCustomersLiveInLondon

count を使います。

getLondonCustomers

select を使います。

getCustomersWhoDontLiveInLondon

reject を使います。

getCustomersWhoDoAndDoNotLiveInLondon

select と reject の両方が必要な場合は partition を使うといい具合に書けます。

final PartitionMutableList<Customer> partition
     = this.company.getCustomers().partition(CUSTOMER_FROM_LONDON);
final MutableList<Customer> customersFromLondon    = partition.getSelected();
final MutableList<Customer> customersNotFromLondon = partition.getRejected();

findMary & findPete

Company#getCustomerNamed(String) を実装するとパスするようになります。

customers.detect(customer -> name.equals(customer.getName()));

Exercise3Test

flatCollect の使い方を学びます。

improveGetOrders

Company#getOrders() を実装します。

Before
final MutableList<Order> orders = this.customers.flatCollect(Customer::getOrders);
for (final Customer customer : this.customers)
{
    orders.addAll(customer.getOrders());
}
return orders;

これがたった1行で書ける素晴らしさ……

After
return this.customers.flatCollect(Customer::getOrders);

findItemNames

findItemNames
final MutableList<LineItem> allOrderedLineItems
     = this.company.getOrders().flatCollect(Order::getLineItems);
final MutableSet<String> actualItemNames
     = allOrderedLineItems.collect(LineItem::getName).toSet();

findCustomerNames

final MutableList<String> names = this.company.getCustomers().collect(Customer::getName);

Exercise4Test

なぜか配列になっていた Supplier を使って、配列を Eclipse Collections でどう処理するかを学ぶ問題です。 Supplier とは供給業者の意で、 Java8 から追加された Supplier ではありません。

findSupplierNames

company.getSuppliers() の戻り型を変えずに MutableList へ変換するには ArrayIterate クラスを使います。第0引数に配列、第1引数に Predicate を受け取ります。

final MutableList<String> supplierNames
            = ArrayIterate.collect(company.getSuppliers(), Supplier::getName);

countSuppliersWithMoreThanTwoItems

ArrayIterate#count を使います。

final Predicate<Supplier> moreThanTwoItems = supplier -> 2 < supplier.getItemNames().length;
final int suppliersWithMoreThanTwoItems = ArrayIterate.count(company.getSuppliers(), moreThanTwoItems);

whoSuppliesSandwichToaster

supplier.getItemNames() の戻り型が array なので ArrayIterate を使って Predicate を作ります。

final Predicate<Supplier> suppliesToaster = supplier -> ArrayIterate.anySatisfy(
                    supplier.getItemNames(), itemName -> "sandwich toaster".equals(itemName));

filterOrderValues

orders が java.util.List なので、 factory の ofAll メソッドを使って変換します。
この問題ではテンプレートに従い、説明的な変数を用意して1段ずつ変形します。

final MutableList<Double> orderValues
            = Lists.mutable.ofAll(orders).collect(Order::getValue);
final MutableList<Double> filtered = orderValues.select(value -> 1.5 < value);

ただ、私は下記のように1つにまとめるのが好きです。

final MutableList<Double> filtered
            = Lists.mutable.ofAll(orders)
                .collect(Order::getValue)
                .select(value -> 1.5 < value);

変換をしない場合は下記のようにすれば同じ値を求められます。

orders.stream()
            .map(Order::getValue)
            .filter(value -> 1.5 < value).collect(Collectors.toList())

filterOrders

やはり ofAll で変換してから select します。

final MutableList<Order> filtered
            = Lists.mutable.ofAll(orders).select(order -> 2.0 < order.getValue());

変換をしない場合は下記のようにすれば同じ値を求められます。

orders.stream().filter(order -> 2.0 < order.getValue()).collect(Collectors.toList())

Exercise5Test

先ほどと同じ問題かと思ったら、今度は Static Utilities を使ってはいけないようです。

filterOrderValues

orders の 型を MutableList にして、Customer#getOrders の戻り型を MutableList にする(ためにフィールドの型も変えた)のですが、これでいいのでしょうか……

Customer
private final MutableList<Order> orders = Lists.mutable.empty();

......

public MutableList<Order> getOrders()
final MutableList<Order> orders = this.company.getMostRecentCustomer().getOrders();
final MutableList<Double> orderValues = orders.collect(Order::getValue);

Predicates.greaterThan(doubleValue)

より可読性が高まる Predicate 集があるらしいです。

filterOrders

先ほどの変更をしてあれば、あとは1行修正するだけです。

final MutableList<Order> filtered = orders.select(order -> 2.0 < order.getValue());

Exercise6Test

sort や max などを使います。

sortedTotalOrderValue

total を求めるメソッドはすでに用意されているので、total に変形させて、昇順を指定して sort するだけでよいです。

final MutableList<Double> sortedTotalValues = company.getCustomers()
                .collect(Customer::getTotalOrderValue)
                .sortThis(Comparators.naturalOrder());

maximumTotalOrderValue

totalOrderValue の List を作って max() メソッドを呼び出します。

final Double maximumTotalOrderValue = company.getCustomers()
                .collect(Customer::getTotalOrderValue).max();

customerWithMaxTotalOrderValue

sort を書いて最初の要素を取り出す、などということをしなくても maxBy メソッドを使えばよいです。

final Customer customerWithMaxTotalOrderValue = company.getCustomers()
                .maxBy(Customer::getTotalOrderValue);

supplierNamesAsTildeDelimitedString

ArrayIterate#collect で name だけにしてから makeString で連結します。 makeString メソッドの引数で delimiter を指定できます。省略すると「, 」が使われます。

final String tildeSeparatedNames
            = ArrayIterate.collect(company.getSuppliers(), Supplier::getName).makeString("~");

deliverOrdersToLondon

London 行きの Order だけ deliver します。forEach を使えとの指示だが、それだと曖昧だと言われるので each を使ってみます。

this.company.getCustomers().select(customer -> "London".equals(customer.getCity()))
            .flatCollect(Customer::getOrders).each(Order::deliver);

もちろん指示違反なので、ちゃんと forEach を使います。曖昧だと言われているのでキャストで型を指定してやればよく、それをヒントにある通り Procedures#cast を使えば実現できます。

this.company.getCustomers().select(customer -> "London".equals(customer.getCity()))
            .flatCollect(Customer::getOrders).forEach(Procedures.cast(Order::deliver));

Exercise7Test

Multimap の使い方を学びます。

customersByCity

groupBy でキーとなる要素を指定するだけで Multimap に変換できてしまいます。強力ですね。

final MutableListMultimap<String, Customer> multimap
            = company.getCustomers().groupBy(Customer::getCity);

mapOfItemsToSuppliers

従来のやり方から Multimap に適した形に直します。

Before
final MutableMap<String, List<Supplier>> itemsToSuppliers = UnifiedMap.newMap();

        for (final Supplier supplier : this.company.getSuppliers())
        {
            for (final String itemName : supplier.getItemNames())
            {
                List<Supplier> suppliersForItem;
                if (itemsToSuppliers.containsKey(itemName))
                {
                    suppliersForItem = itemsToSuppliers.get(itemName);
                }
                else
                {
                    suppliersForItem = FastList.newList();
                    itemsToSuppliers.put(itemName, suppliersForItem);
                }

                suppliersForItem.add(supplier);
            }
        }

配列を使っている都合上、 flatCollect できずに Iterate in Iterate になって微妙だが、こんな感じになります。

After
final MutableMultimap<String, Supplier> itemsToSuppliers = Multimaps.mutable.list.empty();

ArrayIterate.forEach(this.company.getSuppliers(), supplier -> {
    ArrayIterate.forEach(supplier.getItemNames(), itemName -> {
        itemsToSuppliers.put(itemName, supplier);
    });
});

reminder

super クラス(CompanyDomainForKata) の setUpCustomersAndOrders をリファクタリングします。

/**
         * TODO 7: Refactor Order and its API so this repetition is not necessary.
         */
        // TODO 7: Add 3 cups at 1.5 each to the order
        fredOrder.addLineItem(new LineItem("cup", 1.5));
        fredOrder.addLineItem(new LineItem("cup", 1.5));
        fredOrder.addLineItem(new LineItem("cup", 1.5));

        // TODO 7: Add 3 saucers at 1.0 each to the order
        fredOrder.addLineItem(new LineItem("saucer", 1.0));
        fredOrder.addLineItem(new LineItem("saucer", 1.0));
……省略……

上記のような雑なコピペを Interval で書き直す。oneTo を使うとよい。

Interval.oneTo(3).each(i -> fredOrder.addLineItem(new LineItem("cup", 1.5)));

もちろん、for ループも書き換え可能です。

for (int i = 0; i < 43; i++) {
    billOrder1.addLineItem(new LineItem("gnome", 7.50));
}

は下記のようにできます。

Interval.oneTo(43).each(i -> billOrder1.addLineItem(new LineItem("gnome", 7.50)));

このリファクタリングに失敗すると次の問題が通らなくなるので注意しましょう。

Exercise8Test

aggregateBy 等の少し難しめの問題が多いです。

totalOrderValuesByCity

zeroValueFactory と aggregator を使って aggregateBy メソッドを呼び出します。

final MutableMap<String, Double> map
            = company.getCustomers().aggregateBy(Customer::getCity, zeroValueFactory, aggregator);

totalOrderValuesByItem

zeroValueFactory と aggregator を使って aggregateBy メソッドを呼び出します。

final MutableMap<String, Double> map
            = company.getOrders().flatCollect(Order::getLineItems)
                .aggregateBy(LineItem::getName, zeroValueFactory, aggregator);

sortedOrders

Javadoc コメントにやることが書いてあります。

Find all customers' line item values greater than 7.5 and sort them by highest to lowest price.

1つずつ書いていくと

  1. すべての Customer を取り出す
  2. すべての LineItem を取り出す
  3. value が 7.5 より上の要素を選択
  4. 高い順にソート

flatCollect の 2連発& collect でなかなか遠回り感がありますが、1段ずつやった方が可読性ではよさそうです。 toSortedBag では sort アルゴリズムを選択できます。

final MutableSortedBag<Double> orderedPrices
            = company.getCustomers()
                .flatCollect(Customer::getOrders)
                .flatCollect(Order::getLineItems)
                .collect(LineItem::getValue)
                .select(Predicates.greaterThan(7.5))
                .toSortedBag(Comparators.reverseNaturalOrder());

whoOrderedSaucers

"saucer" を注文した Customer を取り出します。Customer を持っていないといけないのでこうなったのですが、微妙ですね……

final MutableList<Customer> customersWithSaucers
            = company.getCustomers()
                .select(customer -> {
                    return customer.getOrders()
                            .flatCollect(Order::getLineItems)
                            .collect(LineItem::getName)
                            .anySatisfy(Predicates.in("saucer"));
                });

Predicates.in("saucer") は地味に便利です。

ordersByCustomerUsingAsMap

MutableList#toMap(Function, Function) を使います。引数の2つの Function はそれぞれ key と value を取り出す操作を指定します。

final MutableMap<String, MutableList<Order>> customerNameToOrders =
                this.company.getCustomers().toMap(Customer::getName, Customer::getOrders);

mostExpensiveItem

groupBy で Multimap を作ります。key に最高の商品価格、 value に Customer を入れます。

final MutableListMultimap<Double, Customer> multimap
            = company.getCustomers().groupBy(
                    customer -> customer.getOrders()
                                    .flatCollect(Order::getLineItems)
                                    .collect(LineItem::getValue)
                                    .max()
                                );

完了

$ gradle test して達成感に浸ります。

company-kata.png

10
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
11