概要
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 クラスがあります。
private final String firstName;
private final String lastName;
private final MutableList<Pet> pets = FastList.newList();
そして、Person クラスの List である people が最初に与えられています。
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
上のとそんなに変わりません。
@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 から除外するメソッドです。条件式に否定形を書かなくてよい点が素晴らしいです。
@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);
}
@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 を外で定義します。
@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
同上
@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 メソッドを使います。
@Test
public void howManyPeopleHaveCats() {
final int count = people.count(person -> person.hasPet(PetType.CAT));
Assert.assertEquals(2, count);
}
findMarySmith
条件に一致する最初の要素を取り出すには detect メソッドを使います。IDE で最初にサジェストされます。
@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 を作ります。
@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 が出てきます。
@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 で変形させるだけのようです。
@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 という名前で用意されています。
@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
上に同じです。
@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 を使います。
@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 を使います。
@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 文を使っている点は特に気にしないでよいです。
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行で書けます。
final MutableBag<PetType> counts = Bags.mutable.empty();
petTypes.each(petType -> counts.add(petType));
便利すぎて涙が出ますね。
getPeopleByLastName
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行で書けます。
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) との連携が意識されており、同じ処理を下記の通り書けます。
this.people.each(person -> byLastNameMultimap.add(Tuples.pair(person.getLastName(), person)));
getPeopleByTheirPets
set の MutableMultimap を使って改善する、というお題です。
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);
}
}
ひとまずこのように書けます。
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 になってしまって具合は悪いですが、こんな風に書けなくもないです。
this.people.each(person -> {
person.getPets().each(pet -> peopleByPetType.put(pet.getType(), person));
});
Exercise4Test
Collection の修正
Eclipse Collections には primitive の要素を保持できる Collection が用意されています。このテストでは IntList と IntSet を使います。
MutableList<Integer> petAges = this.people
.stream()
.flatMap(person -> person.getPets().stream())
.map(pet -> pet.getAge())
.collect(Collectors.toCollection(FastList::new));
collectInt で IntList に変換できます。
final MutableIntList petAges = this.people
.flatCollect(person -> person.getPets())
.collectInt(pet -> pet.getAge());
Set<Integer> uniqueAges = petAges.toSet();
toSet で変換できます。
IntSet uniqueAges = petAges.toSet();
IntSummaryStatistics stats = petAges.stream().mapToInt(i -> i).summaryStatistics();
空の IntSummaryStatistics インスタンスを生成し、 petAges の要素を全部入れます。
final IntSummaryStatistics stats = new IntSummaryStatistics();
petAges.forEach(i -> {stats.accept(i);});
IntSets の使用
もちろん primitive collection 用の factory も用意されています。
Assert.assertEquals(Sets.mutable.with(1, 2, 3, 4), uniqueAges);
Assert.assertEquals(IntSets.mutable.with(1, 2, 3, 4), uniqueAges);
便利な要素検査
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);
これらを計算するメソッドが全部用意されています。
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
Assert.assertTrue(petAges.stream().allMatch(i -> i > 0));
Assert.assertFalse(petAges.stream().anyMatch(i -> i == 0));
Assert.assertTrue(petAges.stream().noneMatch(i -> i < 0));
Assert.assertTrue(petAges.allSatisfy(i -> i > 0));
Assert.assertFalse(petAges.anySatisfy(i -> i == 0));
Assert.assertTrue(petAges.noneSatisfy(i -> i < 0));
streamsToECRefactor1
Eclipse Collections を使えば余計なメソッドが少なくなるという例です。
//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(" & "));
//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
final Map<PetType, Long> countsStream =
Collections.unmodifiableMap(
this.people.stream()
.flatMap(person -> person.getPets().stream())
.collect(Collectors.groupingBy(Pet::getType,
Collectors.counting())));
final Bag<PetType> countsStream =
this.people
.flatCollect(person -> person.getPets())
.collect(pet -> pet.getType())
.toBag();
なお、前の問題にもあったように Bag からの数値取り出しには occurrencesOf メソッドを使います。 Assertion の書き換えも必要でした。
streamsToECRefactor3
引き続き Bag の力を思い知りましょう。
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());
こうしたややこしい処理が
final List<ObjectIntPair<PetType>> favoritesStream =
this.people
.flatCollect(p -> p.getPets())
.collect(pet -> pet.getType())
.toBag()
.topOccurrences(3);
Eclipse Collections を使うとまったく必要なく、toBag() と topOccurrences(3) で解決します。
Assertion の修正
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 で書き直します。
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 が生成され、それぞれ参照できます。
PartitionMutableList<Person> partitionMutableList
= people.partition(person -> 0 < person.getPets().size());
getOldestPet
flatCollect で Pet の一覧を作り、属性を指定して最大値を持つ Pet を取り出します。
people.flatCollect(Person::getPets).maxBy(pet -> pet.getAge());
getAveragePetAge
flatCollect で Pet の一覧を作り、さらに年齢の List に変換して平均年齢を出します。
people.flatCollect(Person::getPets)
.collectInt(pet -> pet.getAge())
.average();
addPetAgesToExistingSet
すでにある Collection に要素を追加します。 Set で持っている addAll を使えばよいでしょう。
final MutableIntSet petAges = IntSets.mutable.with(5);
petAges.addAll(people.flatCollect(Person::getPets).collectInt(Pet::getAge));
refactorToEclipseCollections
従来の Collection を Eclipse Collections で置き換えます。
List
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 メソッドで生成します。
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
final Set<Integer> petAges = new HashSet<>();
for (final Person person : people)
{
for (final Pet pet : person.getPets())
{
petAges.add(pet.getAge());
}
}
final MutableSet<Integer> petAges
= people.flatCollect(Person::getPets).collect(Pet::getAge).toSet();
完了
最後に $ gradle test ですべての UnitTest を実行しましょう。達成感に浸れます。

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() を実装します。
final MutableList<Order> orders = this.customers.flatCollect(Customer::getOrders);
for (final Customer customer : this.customers)
{
orders.addAll(customer.getOrders());
}
return orders;
これがたった1行で書ける素晴らしさ……
return this.customers.flatCollect(Customer::getOrders);
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 にする(ためにフィールドの型も変えた)のですが、これでいいのでしょうか……
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 に適した形に直します。
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 になって微妙だが、こんな感じになります。
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つずつ書いていくと
- すべての Customer を取り出す
- すべての LineItem を取り出す
- value が 7.5 より上の要素を選択
- 高い順にソート
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 して達成感に浸ります。
