トレイトと抽象クラスについて解説

Scala 3.3.4
最終更新:2023年12月7日

[AD] Scalaアプリケーションの開発・保守は合同会社ミルクソフトにお任せください

Scalaにはオブジェクトを抽象化する方法に「トレイト(trait)」と「抽象クラス(abstract class)」を使用する方法があります。
ではトレイトと抽象クラスのどちらを使用するのがよいのでしょうか?
この記事ではトレイトと抽象クラスについて解説します。

抽象クラスは実装を持たない抽象メンバーを持つクラス

「抽象クラス」は、実装を持たない「抽象メンバー」を持つクラスです。
以下のようにabstraceキーワードをclassの前につけて定義します。

abstract class AbstractClass

抽象クラスには以下の特徴があります。

  • 直接インスタンスを作ることはできない
  • 抽象メンバーはサブクラスで実装する

直接インスタンスを作ることはできない

抽象クラスから直接インスタンスを作ることはできません。

抽象クラスをnewするとコンパイルエラーになります。

new AbstractClass ^ error: class AbstractClass is abstract; cannot be instantiated

抽象クラスを継承してサブクラスを作り、サブクラスからインスタンスを作成します。
以下のように抽象クラスを継承して作成したサブクラスを具象クラスと呼びます。

class SubClass extends AbstractClass val obj: AbstractClass = new SubClass

抽象メンバーはサブクラスで実装する

最初に説明したとおり、抽象クラスは実装を持たいない抽象メンバーを持つクラスです。
以下の例では、motherTonguegreetが抽象メンバーです。

abstract class Person: val motherTongue: String def greet(): String

サブクラスで抽象メンバーを具体的に定義します。
overrideキーワードは省略可能ですが、抽象メンバーの具体的な実装であることを表すためにつけたほうが良いです。

class Japanese extends Person: override val motherTongue: String = "Japanese" override def greet(): String = "こんにちは" class Chinese extends Person: override val motherTongue: String = "Chinese" override def greet(): String = "你好啊"

定義した具象クラスからインスタンスを作成して、メンバー変数とメソッドにアクセスしてみます。

val person1: Person = new Japanese() val person2: Person = new Chinese() println(s"person1: 母国語: ${person1.motherTongue}, 挨拶: ${person1.greet()}") println(s"person2: 母国語: ${person2.motherTongue}, 挨拶: ${person2.greet()}")

実行結果は以下のようになります。

person1: 母国語: Japanese, 挨拶: こんにちは person2: 母国語: Chinese, 挨拶: 你好啊

トレイトはメソッドとフィールドの定義をカプセル化したもの

「トレイト」はメソッドとフィールドをカプセル化したものであり、コードを再利用するために利用します。
また、「抽象クラス」と同様に「抽象メンバー」を持つこともできます。

トレイトは以下のようにtraitキーワードを使用して定義します。

trait TraitA

トレイトには以下の特徴があります。

  • 直接インスタンスを作成することはできない
  • 抽象メンバーを持つことができる
  • パラメータを持つことができない
  • 複数のトレイトを1つのクラスやトレイトにミックスインできる

直接インスタンスを作成することはできない

トレイトから直接インタンスを作成できません。
トレイトをnewすると抽象クラスと同様にコンパイルエラーになります。

new TraitA ^ error: trait TraitA is abstract; cannot be instantiated

トレイトはクラスやトレイトにミックスインして使用します。

トレイトをミックスインするには、抽象クラスと同様にextendsキーワードを使用します。

class ClassA extends TraitA

抽象メンバーを持つことができる

トレイトは抽象クラスと同様に抽象メンバーを持つことができます。

抽象クラスで定義したPersonクラスをトレイトを使って定義し直してみます。
抽象クラスとの違いは、abstract classtraitのみであることがわかります。

trait Person: val motherTongue: String def greet(): String

では、トレイトをミックスインしたクラスを定義してみます。
抽象クラスを継承した場合と全く同じコードになりました。

class Japanese extends Person: override val motherTongue: String = "Japanese" override def greet(): String = "こんにちは" class Chinese extends Person: override val motherTongue: String = "Chinese" override def greet(): String = "你好啊"

定義したクラスの実行結果も抽象クラスを使用した場合と同じです。

val person1: Person = new Japanese() val person2: Person = new Chinese() println(s"person1: 母国語: ${person1.motherTongue}, 挨拶: ${person1.greet()}") println(s"person2: 母国語: ${person2.motherTongue}, 挨拶: ${person2.greet()}")
person1: 母国語: Japanese, 挨拶: こんにちは person2: 母国語: Chinese, 挨拶: 你好啊

(Scala 2系まで)パラメータを持つことができない

Scala 2系ではトレイトはクラスとは違いパラメータを持つことができません。

クラスは以下のようにパラメータを持てますが、

class ClassA(val param: String)

トレイトはパラメータを持てません。
以下のコードはコンパイルエラーになります。

trait TraitA(val param: String)

トレイトでは以下のように抽象メンバーを定義してミックスインしたクラスでパラメータを定義します。

trait TraitA: val param: String class ClassA(val param: String) extends TraitA

この制限はScala3でなくなりました。

複数のトレイトを1つのクラスやトレイトにミックスインできる

トレイトは複数のトレイトをミックスインすることができます。
複数のトレイトをミックスインする場合は、withキーワードを使用します。

class ClassA extends TraitA with TraitB

また、インスタンスを作成する際にトレイトをミックスインすることもできます。

class ClassA val obj: TraitA = new ClassA() with TraitA

トレイトには抽象クラスと違った使い方がある

次にトレイトの抽象クラスとは違う使い方として以下の2つの方法を紹介します。

  • クラスにインターフェースを追加する
  • ミックスインしたクラスのメソッドに変更を加える

クラスにインターフェースを追加する

トレイトを使用してクラスにインターフェースを追加することができます。
以下の例ではPersonalityOfBloodTypeトレイトを使用してpersonalityメソッドをJapaneseクラスに追加します。

trait PersonalityOfBloodType: val bloodType: String def personality(): String = bloodType match case "O" => "おおらか" case "A" => "几帳面" case "B" => "わがまま" case "AB" => "天才" class Japanese(override val bloodType: String) extends Person with PersonalityOfBloodType: override val motherTongue: String = "Japanese" override def greet(): String = "こんにちは"

Japaneseクラスのインスタンスに対してPersonalityOfBloodTypeトレイトで定義したメソッドを呼び出すことができます。

val person = new Japanese("AB") println(s"あなたの性格は、「${person.personality()}」です。")

実行結果は以下のとおりです。

あなたの性格は、「天才」です。

ミックスインしたクラスのメソッドに変更を加える

次にトレイトを使用してクラスのメソッドに変更を加える方法を紹介します。
以下の例ではLoggerトレイトを使用してJapaneseクラスのgreetメソッドが呼び出されたときにログメッセージを表示するようにしています。

trait Logger extends Person: abstract override def greet(): String = println("greet is called") super.greet() class JapaneseWithLogger extends Japanese with Logger

LoggerトレイトはPersonクラスを継承しています。
これは、LoggerトレイトをミックスインできるのはPersonクラスの子クラスのみであることを意味します。

greetメソッドをabstract overrideキーワードを付けて定義し、その中でsuperで親のメソッドを呼び出しています。

実行結果は以下のとおりです。

val person = new JapaneseWithLogger() println(person.greet())
greet is called こんにちは

Loggerトレイトからsuper.greet()を呼び出すと何が実行されるのでしょうか?

Loggerトレイトが継承したPersonクラスのgreetメソッドは抽象メンバーです。
そのため、Personクラスの子クラスではこのような実装はできません。

トレイトの場合、トレイト内でのsuper呼び出しは、トレイトを定義した時点で決定するのではなく動的に束縛されます。
そのため、トレイトをミックスインしたクラスの具象メンバーを呼び出すことになります。

トレイトと抽象クラスの共通点と相違点

ここまでの説明でトレイトと抽象クラスには以下の共通点があることがわかります。

  • それ自身から直接インスタンスを作成することができない
  • 抽象メンバーを持てる
  • 継承またはミックスインして具象クラスを作成して使用する

また、以下の違いがあることがわかります。

  • 複数のトレイトを1つのクラスにミックスインできるが、複数のクラスを継承したクラスを作ることはできない
  • (2系まで)トレイトはパラメータを持てないが、抽象クラスはパラメータを持つことができる

トレイトと抽象クラスを使い分ける

抽象クラスとトレイトはどちらもオブジェクトを抽象化するために利用することができます。
ではトレイトと抽象クラスのどちらを使用するのが良いでしょうか?

抽象クラスとトレイトは以下のように持つ意味に違いがあります。

  • 抽象クラス: それが何であるかを表現する
  • トレイト: それがどういった振る舞いをするのかを表現する

抽象クラスには、クラスは1つのスーパークラスしか持てないという制約がありますので、それが何であるか(is-aの関係性)を明確に示したい場合に使用しましょう。
そうでなければトレイトを使うのが良いでしょう。

scala3ではパラメータをもつトレイトを使用できる

前述のトレイトと抽象クラスの違いの説明でトレイトはパラメータを持つことができないと説明しました。
これはScala2の制限であり、Scala3ではトレイトがパラメータを持てるように変更されました。

以下のコードはScala2ではコンパイルエラーになりますが、Scala3ではコンパイルできます。

trait TraitA(val param: String) class ClassA(override val param: String) extends TraitA(param) val obj: TraitA = new ClassA("value x")

詳細は以下の記事を参照してください。

サイト内検索