クラスを定義する方法

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

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

ここでは、クラスを定義して使用する方法について説明します。

クラスとはなにか

クラスとはオブジェクト指向におけるインスタンスを作成するための設計図です。
クラスは、状態を保持する「フィールド」と、インスタンスを生成する際に呼び出されて内容の初期化などを行なう「コンストラクタ」、処理を行う「メソッド」からなります。
このフィールドとメソッドをまとめて「メンバー」と呼びます。

オブジェクト指向では、クラスからインスタンスを作成して、インスタンスに対してメッセージを送る(メソッドを呼び出す)ことで、プログラムを構築していきます。

クラスを定義する

次にクラスの定義方法を説明します。

クラスは、以下の構文で定義します。

class 識別子

定義したクラスから、newを使用してインスタンスを作成します。

new 識別子

クラスを定義するだけであれば、class 識別子のみで定義できます。
しかし、これだけだと何もすることはできませんね。

フィールドを定義する

次に説明するのはフィールドを定義する方法です。
フィールドとは状態を保持するものです。

フィールドを定義する方法には2つの方法があります。
1つはコンストラクタで値を設定する方法、もう1つはクラス定義内に定義する方法です。

コンストラクタで定義する

コンストラクタで値を設定する場合は、クラス名の後ろの()内にフィールドを定義します。

以下の例では、「x」「y」「z」「zz」の4つのフィールドをコンストラクタで定義しています。

class ClassExample(val x: Int, var y: Int, z: Int, zz: Int = 0)

valvarの有無や=の有無の違いがあるのには意味があります。
valvarがついている場合は、そのクラスの外からフィールドにアクセスすることができます。
また、valは不変であり、varは可変です。

フィールドへは、以下の例のようにアクセスします。

val instance = new ClassExample(1, 2, 3, 4) println(s"x=${instance.x},y=${instance.y}")

「x」「y」には、クラスの外からアクセスすることができましたが、val, varがついていない「z」「zz」はどうでしょうか?
val, varがつかないフィールドには、クラスの外からアクセスすることができません。
また、同じクラスであっても別のインスタンスのフィールドにアクセスすることもできません。
後ほど説明するprivate[this]のアクセス制限が付きます。

また、「zz」には、=の後に値を指定しています。
これは、省略値で省略値を指定したフォールドは、インスタンスを生成する際に省略することができます。

val instance_2 = new ClassExample(1, 2, 3)

クラス内で定義する

クラス内でフィールドを定義する場合は、以下のように定義します。
クラス内で定義する場合は、初期値を指定するかabstractを付ける必要があります。
このようなクラスを「抽象クラス」と呼びます。

  • 初期値を指定
class ClassExample: val x: Int = 0 var y: Int = 0
  • abstractを指定
abstract class ClassExample2: val x: Int var y: Int

抽象クラスは、そのままではインスタンスを作成できません。
抽象クラスからインスタンスを作成する場合は、抽象クラスを継承した派生クラスから作成する必要があります。
継承については、「クラスを拡張する」で説明します。

メソッドを定義する

次にメソッドを定義する方法を説明します。

メソッドは、以下の構文で定義します。

def メソッド名(パラメーターリスト): 戻り値の型 = 式(ブロック)

パラメーターリストと戻り値の型は省略可能です。
戻り値の型を省略した場合は、メソッド内で最後に処理した式の値から推論され戻り値の型が決まります。

以下の例では、ClassExampleクラスにメンバーをタプルで返すtoTupleメソッドを定義しています。

class ClassExample(val x: Int, val y: Int): def toTuple(): (Int, Int) = (x, y)

定義したメソッドを呼び出してみます。

val example = new ClassExample(100, 200) val (x, y) = example.toTuple() println(s"x=${x}, y=${y}")

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

x=100, y=200

コンストラクタを定義する

次にコンストラクタを定義する方法を説明します。

フィールドをコンストラクタで定義する方法は、「フィールドを定義する」で説明しました。
では、初期化処理を記述したい場合は、どのようにすれば良いでしょうか?

その場合は、クラス定義内に処理を記述します。
クラス定義内に記述した処理は、インスタンスを生成すると実行されます。

以下の例では、コンストラクタでフィールドの値を出力しています。

class ClassExample(val x: Int, val y: Int): println(s"x=${x}, y=${y}")

では、インスタンスを生成します。

new ClassExample(100, 200)

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

x=100, y=200

コンストラクタを複数定義する

コンストラクタを複数使用する場合はどのようにすれば良いでしょうか?

そのような場合は、補助コンストラクターを使用します。
補助コンストラクターは、以下の構文で定義します。

def this(パラメータリスト) = 式(ブロック)

補助コンストラクターでは、最初に同じクラスのコンストラクターを呼び出す必要があります。
最初に同じクラスのコンストラクターを呼び出さない場合は、コンパイルエラーとなります。

error: 'this' expected but identifier found.

以下の例では、Intの値を2つ指定するコンストラクタの他に、tupleを指定するコンストラクタを定義しています。

class ClassExample(val x: Int, val y: Int): println(s"x=${x}, y=${y}") def this(tuple: (Int, Int)) = this(tuple._1, tuple._2) println("called with tuple")

では、インスタンスを生成します。

val t: (Int, Int) = (100, 200) new ClassExample(t)

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

x=100, y=200 called with tuple

クラスを拡張する

クラスは継承して拡張することができます。
拡張する対象となるクラスのことを親クラス(スーパークラス)、親クラスを継承したクラスのことを派生クラス(サブクラス)といいます。

クラスを継承するには、派生クラスのクラス名の後ろにextendsを指定し、その後ろに継承する親クラスのクラス名を指定します。

class 派生クラス extends 親クラス

派生クラスは親クラスの非公開メンバー以外のメンバーを引き継ぎます。
以下の例では、SuperClassを継承してSubClassを定義しています。
SubClassからは、SuperClassmethodSuperを呼び出すことができます。

class SuperClass: def methodSuper(): Unit = println("called methodSuper") class SubClass extends SuperClass: def methodSub(): Unit = methodSuper() // SuperClassのメソッドを呼び出す

また、親クラスで定義したメンバーと同名のメンバーを派生クラスで再定義(オーバーライド)できます。

class SuperClass_2: def methodSuper(): Unit = println("called methodSuper") class SubClass_2 extends SuperClass_2: // SuperClassのメソッドをオーバーライドする override def methodSuper(): Unit = println("called methodSuper defined at SubClass") def methodSub(): Unit = methodSuper() // SubClassでオーバーライドしたメソッドを呼び出す

継承はすでにあるコード(クラス)を簡単に拡張できますが、同時に複雑さが増す要因になります。
安易に継承を使ってコードを拡張せずに、本当に継承が必要なケースなのかを慎重に検討しましょう。

アクセスを制限する

Scalaはデフォルトでは、メンバーへのアクセス制限はなく、すべての場所から参照できます。(publicメンバー)

メンバーに対して、以下の2種類のアクセス修飾子を指定してアクセス範囲を制限します。

  • private(非公開)
  • protected(限定公開)

また、それぞれにアクセス保護のスコープを指定することができます。

ここでは、メンバーに対してアクセスを制限する方法について説明します。

private(非公開)

privateをつけたメンバーは、そのメンバーを定義したクラスからのみアクセスできます。

以下の例では、フィールド「field1」、メソッド「method1」はどちらも、ClassExampleからのみアクセスできます。
他のクラスからアクセスした場合は、コンパイルエラーになります。

class ClassExample: // 自身だけアクセスできる private var field1: Int = 0 // 自身だけアクセスできる private def method1(): String = "called private method" def publicMethod(): Unit = println(s"private field1=${field1}") println(method1()) val instance = new ClassExample instance.publicMethod()

protected(限定公開)

protectedをつけたメンバーは、そのメンバーを定義したクラスか、そのクラスのサブクラスからのみアクセスできます。

以下の例では、SuperClassの定義したフィールド「field1」、メソッド「method1」はどちらも、SuperClassSuperClassのサブクラスSubClassからのみアクセスできます。

class SuperClass: // 自身とサブクラスからアクセスできる protected var field1: Int = 0 // 自身とサブクラスからアクセスできる protected def method1(): String = "called protected method" class SubClass extends SuperClass: def publicMethod2(): Unit = println(s"protected field1=${field1}") // SuperClassのフィールドにアクセス println(method1()) // SuperClassのメソッドを呼び出す

アクセス保護のスコープ

アクセス修飾子は、限定子を指定してアクセスできる範囲を広げることができます。
限定子は、private[X], protected[X]のようにアクセス修飾子の後ろの[]内に記述します。
Xにはクラス、パッケージを指定します。

例えば、Java言語のprotectedと同じアクセス範囲をScalaで定義するにはどうすれば良いでしょうか?
Java言語のprotectedは、同一パッケージ、派生クラスからアクセスできます。
Scalaでは、protected[パッケージ名]と定義することで、限定子で指定したパッケージ、派生クラスからアクセスできます。
したがって、限定子のパッケージ名をその定義が定義されているパッケージ名にすれば、Java言語のprotectedと同じアクセス範囲を定義できます。
以下の例では、ClassX1method1には、protected[X]を指定しているため、同一パッケージ内のClassX2と別のパッケージ内の派生クラスClassY1から呼び出すことができます。

package X: class ClassX1: // 同一パッケージないと派生クラスから呼び出すことができる protected[X] def method1(): Unit = println("called protected[X] method") class ClassX2: def method2(): Unit = new ClassX1().method1() // 同一パッケージ内のメソッドを呼び出す package Y: class ClassY1 extends X.ClassX1: def method2(): Unit = method1() // 親クラスのメソッドを呼び出す

もう1つ例をあげてみます。 クラスから生成したインスタンス自身だけアクセスできるようにすることはできるでしょうか?

private[this]を指定すると、クラスから生成したインスタンス自身からのみアクセスできるようにできます。

ただし [this] 修飾子はScala 3以降非推奨であることに注意してください。

以下の例では、objectPrivateメソッドに、private[this]をつけています。

class ObjectPrivate: // 同一インスタンス内からのみ呼び出すことができる @annotation.nowarn private[this] def objectPrivate(): Unit = println("called private[this] method") def method(): Unit = this.objectPrivate() // 自身のprivate[this]は呼び出すことができる

以下のように同一インスタンスから呼び出すことはできますが、他のインスタンスから呼び出すことはできません。

this.objectPrivate()

以下のように記述することはできません。
コンパイルエラーになります。

val other = new ObjectPrivate() other.objectPrivate()

newを使わずにインスタンスを生成する

これまでは、クラスからインスタンスを作成するには、newを使用してきました。
実は、newを使用しないでインスタンスを作成する方法があります。

ここでは、newを使わずにインスタンスを作成する方法を説明します。

ケースクラス

case修飾子をつけて定義したクラスは、ケースクラスと呼ばれ、newを使用せずにインスタンスを生成することができます。

case class Hoge(x: String)

上記クラスは、

val hoge = new Hoge("hogehoge")

と書くかわりに、以下のようにnewを省略して書くことができます。

val hoge_2 = Hoge("hogehoge")

applyメソッドを使用する

もう1つnewを使用しないでクラスからインスタンスを生成する方法を見てみます。

クラスと同じ名前のobjectを作成してapplyメソッドを定義すると、ケースクラスと同様に、インスタンスを生成する際にnewを省略できるようになります。
このobjectを「コンパニオンオブジェクト」と呼びます。
また、コンパニオンオブジェクトはクラスと同じファイルで定義する必要があります。

class Hoge(val hoge: String) object Hoge: def apply(hoge: String): Hoge = new Hoge(hoge)

上記クラスは、以下のようにnewを省略してインスタンスを生成できます。

val hoge = Hoge("hogehoge")

実は、ケースクラスを定義すると、コンパイラがapplyメソッドを自動的に追加しています。 そのため、インスタンスを生成する時に以下のように書くこともできます。

val hoge_3 = Hoge.apply("hogehoge")

サイト内検索