【入門編】基礎から学ぶScala④:クラスとオブジェクト

言語学習

今回はScalaでオブジェクト指向プログラミングをする際によく使う「class」と「object」を学んでいきます。

どちらも似たような機能なので、違いが分かりづらいですよね。

本記事では、Scalaのclassとobjectの機能をまとめつつ、両者の違いとユースケースにも言及していきます👋

クラス(class)

クラス(class)は、オブジェクトにメンバー(属性)とメソッド(ふるまい)を持たせることで、効率よくプログラムを作成するために使われます。

後述するobjectは複数インスタンスを作成するということはできないため、一般的なオブジェクト指向で利用したいケースにはclassを使います。

クラスの最小定義

Scalaでクラスを定義するのはかんたん。

クラス定義とインスタンス化の最小定義は以下のようになります。
よくある言語のクラス定義と違って、クラスの中身がなければ波括弧({ })は必要じゃないです。

クラスのインスタンスをprintlnで出力してみると、そのインスタンスへの文字列表現が出力されます。

  class Person
  
  val person = new Person
  println(person) // packages.oop.OOP$Person@4f9ff63c

メンバーとメソッド

最小定義のクラスに、メンバーとメソッドを追加してみます。

メンバーは通常の変数と同様にval, varを使って定義できます。

Scalaにはクラス定義の中に明示的なコンストラクタがありません。
代わりに、メンバーの初期化はインスタンス化する際に{}内の各式が評価される形で行われます。
また、valで定義した値はインスタンス後に変更することができず、変更したい場合はvarで定義します。

メソッドは通常の関数と同様にdefを使って定義します。
メソッドからインスタンスのメンバーを参照したいときにはthisを利用できます。

  class Person(_name: String, _age: Int) {
    // インスタンスの初期化時にパラメータをメンバーに代入する
    var name = _name
    val age = _age

    def getProfile = s"I'm ${this.name}. ${this.age} years old."

    def setName(_name: String): Unit = {
      this.name = _name
    }
  }

  val person = new Person("Jonny", 20)
  println(person.getProfile) // I'm Jonny. 20 years old.

  person.setName("James")
  println(person.getProfile) // I'm James. 20 years old.

クラス定義の引数にvar, valキーワードを付与すると、引数をそのままインスタンスのメンバーにすることができます。

  // var, valを引数に付与すると、メンバーとしてそのまま参照できる
  class Person(var name: String, val age: Int) {
    def getProfile = s"I'm ${this.name}. ${this.age} years old."

    def setName(_name: String): Unit = {
      this.name = _name
    }
  }
  val person = new Person("Marry", 25)
  println(person.name)  // Marry

オーバーロード

メソッドの「オーバーロード」は、同じ名前で異なるシグネチャ(関数名、引数の数、引数の型の組み合わせ)の複数のメソッドを定義することができる機能です。

この点は、ほかの言語のオーバーロードと同じなのであまり気にならないですね。

  class Person(val name: String, val age: Int) {
    def greet(name: String): Unit = println(s"${this.name} says: Hi, $name")
    // 同じ名前のメソッドだけど引数がない
    def greet(): Unit = println(s"Hi, I am ${this.name}")
  }

  val person1 = new Person("Jack", 30)
  person1.greet("Becky")
  person1.greet()        // 呼び出し時の引数で異なるgreetメソッドを実行できる

補助コンストラクタ

Scalaでは、ほかの言語では見かけない「補助コンストラクタ」というちょっと変わったメソッドが存在します。

インスタンス初期化のコンストラクタをオーバーロードすることができると考えると、わかりやすいかもしれません。

  class Person(val name: String, val age: Int) {
    def getProfile = s"${name}(${age})"

    // 補助コンストラクタは複数定義可能
    def this(name: String) = this(name, 0)
    def this() = this("Nameless Person")
  }

  val person1 = new Person("Jack", 30) // 通常のコンストラクタを使用
  val person2 = new Person("John")      // 一つ目の補助コンストラクタを使用
  val person3 = new Person()           // 二つ目の補助コンストラクタを使用
  println(person1.getProfile) // Jack(30)
  println(person2.getProfile) // John(0)
  println(person3.getProfile) // Nameless Person(0)

アクセス修飾子(private/protected)

ScalaにもJavaのようにアクセス修飾子があります。

異なる点として、Scalaのメンバーはデフォルトではpublicとなり、そもそも修飾子が存在しません。

private

クラスのメンバーにprivate修飾子を付与すると、そのメンバーはクラスのインスタンスからのみ参照可能となります。(非公開メンバー)

  
  class Person(
    _name: String,
    _age: Int,
    private val realAge: Int, // クラスの引数に修飾子を付与することも可能
  ) {
    val name = _name
    private val age = _age
  }

  val person = new Person("Marry", 20, 30)
  println(person.name)  // Marry
  println(person.age)   // [error] private value age can only be accessed from class Person ~
  println(person.realAge)   // [error] private value realAge can only be accessed from class Person ~

protected

protected修飾子は、そのクラスか、クラスのサブクラスからのみ参照可能にします。(限定公開メンバー)

  class SuperClass {
    protected val protectedValue = "Protected Value"
    val publicValue = "Public Value"
  }

  class SubClass extends SuperClass {
    def getProtectedValue = protectedValue
  }

  val sample = new SubClass
  println(sample.publicValue)       // Public Value
  println(sample.getProtectedValue) // Protected Vaule
  println(sample.protectedValue)    // [error] protected value protectedValue can only be accessed from class SuperClass in object OOP or one of its subclasses

オブジェクト(object)

Scalaには、Javaなどで提供されているようなstaticな変数やメソッドを定義する機能がなく、同等のことをやりたい場合は、「object」を使います。

すなわち、Scalaでobjectがよくつかわれるケースとしては以下の3つとなります。

  • ヘルパー関数や、プログラム内で共通の状態の管理
  • シングルトンオブジェクトとしての利用
  • ファクトリメソッドの定義

シングルトンオブジェクト

シングルトン」というのは、そのクラスのインスタンスがメモリ上で常に1つしか存在しないということを保証するプログラミングの実装パターンです。

以下は公式からの引用です。

An object is a class that has exactly one instance. It is created lazily when it is referenced, like a lazy val.

As a top-level value, an object is a singleton.

Singleton Objects | Tour of Scala | Scala Documentation (scala-lang.org)

(オブジェクトは、正確に1つのインスタンスを持つクラスである。オブジェクトは、遅延valのように、参照されたときに遅延的に生成されます。 トップレベルの値として、オブジェクトはシングルトンです。)

クラスとは異なり値が参照されるときに自動的にインスタンス化され、メモリ上で一意であることが保証されているので、Staticのように使うことができるんですね。

  object IdGenerator {
    var id = 0

    def next = {
      id += 1
      id
    }
  }

  val id1 = IdGenerator.next
  val id2 = IdGenerator.next
  val id3 = IdGenerator.next
  println(id1) // 1
  println(id2) // 2
  println(id3) // 3

コンパニオンオブジェクト

クラスと同じ名前を持つオブジェクトのことを「コンパニオンオブジェクト」と言います。
また、対応するクラスの方は「コンパニオンクラス」と言います。

  class Database  // コンパニオンクラス
  object Database // コンパニオンオブジェクト

コンパニオンオブジェクトとコンパニオンクラスはお互いのプライベートなメンバーにアクセスすることが可能

Scalaにおいては、コンパニオンクラスを初期化するための「ファクトリメソッド」を提供する実装で使われることが多いようです。
(つまり、objectをインスタンス生成をするためのファクトリクラスとして利用する)

  // privateを付与することでコンパニオンオブジェクト内でしかインスタンス化ができなくなる
  class Animal private(val name: String, val kind: String) {
    private def profile: String = {
      s"This ${kind}'s name is ${name}"
    }
  }

  object Animal {
    // DogとCat用の異なるファクトリ関数を定義する
    def makeDog(name: String): Animal = {
      new Animal(name, "DOG")
    }
    def makeCat(name: String): Animal = {
      new Animal(name, "CAT")
    }

    // Animalクラスのprivateメソッドを使える
    def getProfile(animal: Animal): String = {
      animal.profile 
    }
  }

 // 直接newはできない
  // val hoge = new Animal("Hoge", "HOGE") 

  // インスタンス化したければobjectのファクトリメソッドを使う
  val dog = Animal.makeDog("Pochi")
  val cat = Animal.makeCat("Tama")
  println(Animal.getProfile(dog))
  println(Animal.getProfile(cat))

継承

クラスの継承はJavaと同じくextendsキーワードを使用して行います。

この辺もほかの言語と似ているのであまり違和感なく入っていけるかと思います。

  class Animal
  class Dog extends Animal

オーバーライド(override)

オーバーライドは、スーパークラスのメソッド定義などサブクラスで再定義することを指します。

ちなみに、クラスの引数をスーパークラスに渡す場合は、その引数にもoverrideキーワードを付与する必要があります。

  class Animal(val name: String) {
    def bark = println(s"${name} barks: '...'")
  }

  // このoverrideキーワードは必須
  class Dog(override val name: String) extends Animal(name) {
    override def bark = println(s"${name} barks: 'bow wow'")
  }

  val dog = new Dog("Pochi")
  dog.bark // Pochi barks: 'bow wow'

ちなみに、冒頭でクラスインスタンスをprintlnするとインスタンスの文字列表現が出力されましたが、クラスに標準定義されているtoStringメソッドをオーバーライドすることで、printlnした際の文字列表現を変更できます

  class Stringer {
    override def toString = "This is stringer instance"
  }

  val instance = new Stringer
  println(instance)

継承・オーバーライド禁止(final/sealed)

一方で、スーパークラス側でそれ以上オーバーライドしてほしくないメソッドなどがある場合は、「final」キーワードや「sealed」キーワードを付与します。

この辺りも、Javaと似ていますね。

  class Animal(_name: String) {
    final val name = _name 
    final def bark = println(s"${name} barks: '...'")
  }

  class Dog(_name: String) extends Animal(_name) {
    // スーパークラスでfinal指定されているのでコンパイルエラー
    override val name = _name
    override def bark = println(s"${name} barks: 'bow wow'")
  }
  
  // 同一のファイルからのみ継承が可能となる
  sealed class Person

Scalaで定義できるメソッド

演算子オーバーロード

Scalaでは、「+ * -」 などのような演算子をオーバーロードすることが可能です。

というのも、数値に対する加減乗除などの演算処理も数値オブジェクトのメソッドとして定義されているためで、ユーザー定義のクラスにもこれらのメソッドを定義してあげると簡単に演算子オーバーロードができます。

  class Vec2d(val x: Double, val y: Double) {
    def +(other: Vec2d): Vec2d = {
      new Vec2d(x + other.x, y + other.y)
    }
    def print = s"${x}:${y}"
  }

  val vec1 = new Vec2d(10, 20)
  val vec2 = new Vec2d(40, 50)
  val vec3 = vec1 + vec2
  val vec4 = vec1.+(vec2) // メソッドなのでこういう呼び出しも可能
  println(vec3.print)     // 50.0:70.0
  println(vec4.print)     // 50.0:70.0

単項演算子

Scalaではメソッドとして単項演算子を定義することも可能。

単項演算子の定義には「unary_」というプリフィックスをつける必要があり、定義できるメソッドは「! + - ~」だけのようです。(unaryは「単項」の意味)

  class NumberLike(val value: Int) {
    def unary_! = {
      if (value == 0) true
      else false
    }
    def unary_- = -value
  }

  val n = new NumberLike(10)
  println(n.value) // 10
  println(-n)      // -10
  println(!n)      // false

apply

先ほどから何気なく使っているapplyメソッドですが、Scalaではclass/objectのメソッド定義においてちょっと特殊な立ち位置を持っています。

それは、applyメソッドを実装しているclass/objectは、「オブジェクト名(引数)」という形式でapplyメソッドを実行することができるということ

クラスのファクトリメソッドなどとして定義する際に、記述がシンプルにできるのでScalaでは重宝されます。

 object ApplyTest {
   def apply(name: String): String = s"Hello ${name} !!"
 }

 val sample = ApplyTest.apply("Taro") // 普通にapplyメソッドを実行
 val sample = ApplyTest("Taro")       // applyを省略したこっちも可能

まとめ

  • classは属性やふるまいを定義して、一般的なオブジェクト指向の用途に使う
  • objectはstaticの代わりやコンパニオンオブジェクトとしてファクトリクラスの作成に使う
  • Scalaでは演算子オーバーロードや単項演算子の定義が簡単にできる

classとobjectはかなり学習内容が多い機能なので、この記事に記載した情報だけではその魅力を網羅しきれていないです。

他にも、case classやabstract classなど、classを応用した機能があるので、そちらも別記事にて紹介していきます!

コメント

タイトルとURLをコピーしました