ScalaのCollectionの.collectを使って要素をフィルタリング&変換する

ごあいさつ

こんにちは。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に関する記事を書いていますので、ぜひご覧ください。