[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)
val
やvar
の有無や=
の有無の違いがあるのには意味があります。
val
とvar
がついている場合は、そのクラスの外からフィールドにアクセスすることができます。
また、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
からは、SuperClass
のmethodSuper
を呼び出すことができます。
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」はどちらも、SuperClass
とSuperClass
のサブクラス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
と同じアクセス範囲を定義できます。
以下の例では、ClassX1
のmethod1
には、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")