ごあいさつ
こんにちは。meviyのWebシステムを開発しています、大崎です。
今回は、Scalaでコードを書く中でよく使うことになるであろう、コレクションの.collectメソッドについて、その紹介と実装例、Javaとの比較について書いていきたいと思います。
.collectメソッドのあらまし
collectメソッドは、Scalaのコレクションにおいて、特定の条件に一致する要素を変換して新たなコレクションを作成するために用いられます。
これは、PartialFunction(部分関数)を引数として受け取り、その部分関数に合致する要素のみを変換するような動きをします。
実装例
.collectなしでの実装例
まずは、collectメソッドを使わないで、1〜5までの数値から偶数だけを抽出して文字列に変換する方法を見ていきましょう。
val numbers = List(1, 2, 3, 4, 5) val evenStrings = numbers .filter(_ % 2 == 0) .map(_.toString) println(evenStrings) // 出力: List(2, 4)
このコードでは、数列からまずfilterを使って偶数を抽出し、その後mapで文字列に変換するという2段構えの処理をしています。
ちなみに、Javaで同じ処理を行う場合は以下のようになります。Scalaと同様ですね。
List<Integer> numbers = List.of(1, 2, 3, 4, 5); List<String> evenStrings = numbers.stream() .filter(x -> x % 2 == 0) .map(Object::toString) .collect(Collectors.toList()); System.out.println(evenStrings); // 出力: [2, 4]
.collectを使った実装例
次に、collectメソッドを使って同じ処理を実装します。
val numbers = List(1, 2, 3, 4, 5) val evenStrings = numbers.collect { case x if x % 2 == 0 => x.toString } println(evenStrings) // 出力: List(2, 4)
filterもmapもcollectに一体化しております。xが偶数の場合のみ、xを文字列に変換するという処理をする関数をcollectに渡しています。条件に該当しない要素はコレクションから除外されます。
このように、collectではフィルタリングとマッピングをまとめて定義できるので、コードがより簡潔になります。
Sealedクラスとの組み合わせ例
次に、Sealedクラスと組み合わせて.collectメソッドを使った実装例を見ていきましょう。
Scalaでの実装
sealed traitを使って特定の型を処理する例です。
sealed trait Animal case class Dog(name: String) extends Animal case class Cat(name: String) extends Animal case class Bird(name: String) extends Animal val animals: List[Animal] = List(Dog("Buddy"), Cat("Whiskers"), Bird("Tweety")) val names = animals.collect { case Dog(name) => "Dog_" + name case Cat(name) => "Cat_" + name } println(names) // 出力: List(Dog_Buddy, Cat_Whiskers)
DogクラスとCatクラスのnameを抽出し加工しています。
このように、Sealedとcollectを組み合わせることで、特定の型に対する安全な処理を簡潔に記述することができます。
Javaでの実装
Javaも12からSwitchの拡張が始まり、17からSealedクラスが正式に追加されましたが、参考までにここではSealedクラスと拡張されたswitchを使ったJava 21での例を示します。
import java.util.List; import java.util.stream.Collectors; public sealed class Animal permits Dog, Cat, Bird {} public final class Dog extends Animal { private String name; public Dog(String name) { this.name = name; } public String getName() { return name; } } public final class Cat extends Animal { private String name; public Cat(String name) { this.name = name; } public String getName() { return name; } } public final class Bird extends Animal { private String name; public Bird(String name) { this.name = name; } public String getName() { return name; } } public class SwitchExample { public static void main(String[] args) { List<Animal> animals = List.of(new Dog("Buddy"), new Cat("Whiskers"), new Bird("Tweety")); List<String> names = animals.stream() .map(animal -> switch (animal) { case Dog dog -> "Dog_" + dog.getName(); case Cat cat -> "Cat_" + cat.getName(); default -> null; }) .filter(Objects::nonNull) .collect(Collectors.toList()); System.out.println(names); // 出力: [Dog_Buddy, Cat_Whiskers] } }
この例では、animalsリストにDogとCatとBirdを含め、Stream APIを使用してDogとCatの名前を抽出し加工しています。
Scalaのcollectと同様のメソッドはないので、代わりにswitch式を使ってマッピングした後、filterでnull(この例ではBirdのインスタンスだった要素)を除外しています。
Java8と比較するととても仕様が充実し簡潔に書けるようになりましたが、依然としてScalaの方がより簡潔かつ安全に書けるのではないでしょうか。
おまけ
.collectFirst
というメソッドもあり、collectと同様に部分関数を受け取りますが、最初にマッチした要素だけを取得します。何にもマッチしなかった場合はNoneを返します。
val numbers = List(1, 2, 3, 4, 5) val evenString = numbers.collectFirst { case x if x % 2 == 0 => x.toString } println(evenString) // 出力: Some(2) val numbers2 = List(1, 3, 5) val evenString2 = numbers2.collectFirst { case x if x % 2 == 0 => x.toString } println(evenString2) // 出力: None
まとめ
Scalaのcollectメソッドを使うと、フィルタリングとマッピングを一度に行うような簡素な書き方ができますし、
さらにSealedクラスと組み合わせることで、コレクション内の特定の型や条件に一致する値に対するマッピング処理を簡潔に書けます。
おわりに
今回はScalaのコレクションのcollectメソッドについて、その紹介と実装例について書いてみました。
本ブログでは今後もScalaをはじめとする、DTダイナミクスで用いられている技術やプロダクトなどの魅力を発信していきますので、興味を持っていただけると幸いです。
ほかにも、Scalaに関する記事を書いていますので、ぜひご覧ください。