クラス

これからScalaにおけるクラス定義に関して説明します。Javaのクラスがわかっている事を前提にしますが、ご了承ください。

クラス定義

Scalaにおけるクラスは、記法を除けばJava言語のクラスと大して変わりません。Scalaのクラス定義はおおまかには次のような形を取ります。

class ClassName(parameter1: Type1, parameter2: Type2, ...) {
  (a field or method definition)の0回以上の繰り返し
}

たとえば、点を表すクラスPointを定義したいとします。Pointはx座標を表すフィールドxInt型)とフィールドyInt型)からなるとします。このクラスPointをScalaで書くと次のようになります。

class Point(_x: Int, _y: Int) {
  val x = _x
  val y = _y
}

コンストラクタの引数と同名のフィールドを定義し、それを公開する場合は、以下のように短く書くこともできます。

class Point(val x: Int, val y: Int)
  • クラス名の直後にコンストラクタ引数の定義がある
  • val/varによって、コンストラクタ引数をフィールドとして公開することができる

点に注目してください。まず、最初の点ですが、Scalaでは1クラスに付き、基本的には1つのコンストラクタしか使いません。このコンストラクタを、Scalaではプライマリコンストラクタとして特別に扱っています。文法上は複数のコンストラクタを定義できるようになっていますが、実際に使うことはほとんどありません。複数のオブジェクトの生成方法を提供したい場合、objectのapplyメソッドとして定義することが多いです。

2番目の点ですが、プライマリコンストラクタの引数にval/varをつけるとそのフィールドは公開され、外部からアクセスできるようになります。なお、プライマリコンストラクタの引数のスコープはクラス定義全体におよびます。そのため、

class Point(val x: Int, val y: Int) {
  def +(p: Point): Point = {
    new Point(x + p.x, y + p.y)
  }
  override def toString(): String = "(" + x + ", " + y + ")"
}

のように、メソッド定義の中から直接コンストラクタ引数を参照できます。

メソッド定義

先ほど既にメソッド定義の例として+メソッドの定義が出てきましたが、一般的には、

(private[this/package名]/protected[package名]) def methodName(parameter1: Type1, parameter2: Type2, ...): ReturnType = ???

という形をとります。ただし、実際には {} 式を使った以下の形式を取ることが多いでしょう。

(private[this/package名]/protected[package名]) def methodName(parameter1: Type1, parameter2: Type2, ...): ReturnType = {
   ???
   ???
   ???
   ...
}

ここで、単に、メソッド本体が {} 式からなる場合にこうなる、というだけであって、メソッド定義を {} で囲む専用の構文があるわけではないことに注意しましょう。

返り値の型は省略しても特別な場合以外型推論してくれますが、読みやすさのために、返り値の型は明記する習慣を付けるようにしましょう。privateを付けるとそのクラス内だけから、 protectedを付けると派生クラスからのみアクセスできるメソッドになります。 private[this] をつけると、同じオブジェクトからのみアクセス可能になります。また、 private[package名]を付けると同一パッケージに所属しているものからのみ、 protected[package名] をつけると、派生クラスに加えて追加で同じパッケージに所属しているもの全てからアクセスできるようになります。privateprotectedも付けない場合、そのメソッドはpublicとみなされます。

先ほど定義したPointクラスをREPLから使ってみましょう。

scala> class Point(val x: Int, val y: Int) {
     |   def +(p: Point): Point = {
     |     new Point(x + p.x, y + p.y)
     |   }
     |   override def toString(): String = "(" + x + ", " + y + ")"
     | }
defined class Point

scala> val p1 = new Point(1, 1)
p1: Point = (1, 1)

scala> val p2 = new Point(2, 2)
p2: Point = (2, 2)

scala> p1 + p2
res0: Point = (3, 3)

複数の引数リストを持つメソッド

メソッドは以下のように複数の引数リストを持つように定義することができます。

def methodName(parameter11: Type11, parameter12: Type12, ...)(...)(parameterN1: TypeN1, ..., parameterNM: TypeNM): RerurnType = ???

複数の引数リストを持つメソッドには、Scalaの糖衣構文と組み合わせて流暢なAPIを作ったり、後述するimplicit parameterのために必要になったり、型推論を補助するために使われたりといった用途があります。何はともあれ、複数の引数リストを持つ加算メソッドを定義してみましょう。

scala> class Adder {
     |   def add(x: Int)(y: Int): Int = x + y
     | }
defined class Adder

scala> val adder = new Adder()
adder: Adder = Adder@7bc9f09

scala> adder.add(2)(3)
res1: Int = 5

scala> val fun = adder.add(2) _
fun: Int => Int = $$Lambda$6158/1588831364@7fdc4316

scala> fun(3)
res2: Int = 5

複数の引数リストを持つメソッドはobj.m(x, y)の形式でなくobj.m(x)(y)の形式で呼びだすことになります。また、一番下の例のように最初の引数だけを適用して新しい関数を作る(部分適用)こともできます。

もちろん、複数の引数リストを使わずに、単に複数の引数を持つメソッドも作ることができます(以下)。

scala> class Adder {
     |   def add(x: Int, y: Int): Int = x + y
     | }
defined class Adder

scala> val adder = new Adder()
adder: Adder = Adder@5a03aac0

scala> adder.add(2, 3)
res3: Int = 5

scala> val fun: Int => Int = adder.add(2, _)
fun: Int => Int = $$Lambda$6159/609122394@5032b7e2

scala> fun(3)
res4: Int = 5

フィールド定義

フィールド定義は

(private[this/package名]/protected[package名]) (val/var) fieldName: Type = Expression

という形を取ります。valの場合は変更不能、varの場合は変更可能なフィールドになります。また、privateを付けるとそのクラス内だけから、protectedを付けるとそのクラスの派生クラスからのみアクセスできるフィールドになります。 private[this] を付加すると、同じオブジェクトからのみアクセス可能になります。さらに、private[package名]を付けると同一パッケージからのみ、 protected[package名] をつけると、派生クラスに加えて同じパッケージに所属しているもの全てからアクセスできるようになります。 privateprotectedも付けない場合、そのフィールドはpublicとみなされます。

private[this]を付けたフィールドへのアクセスは一般にJVMレベルでのフィールドへの直接アクセスになるため、若干高速です。細かいレベルでのパフォーマンスチューニングをする際は意識すると良いでしょう。

抽象メンバー

その時点では実装を書くことができず、後述する継承の際に、メソッドやフィールドの実装を与えたいということがあります。このような場合に対応するため、Scalaでは抽象メンバーを定義することができます。抽象メンバーは、メソッドの場合とフィールド場合があり、メソッドの定義は、次のようになります。

(private[this/package名]/protected[package名]) def methodName(parameter1: Type1, parameter2: Type2, ...): ReturnType

フィールドの定義は次のようになります。

(private[this/package名]/protected[package名]) (val/var) fieldName: Type

メソッドやフィールドの中身がない以外は、通常のメソッドやフィールド定義と同じです。また、抽象メソッドを一個以上持つクラスは、抽象クラスとして宣言する必要があります。たとえば、x座標とy座標を持つ、抽象クラスXYは次のようにして定義します。クラスの前にabstract 修飾子をつける必要があるのがポイントです。

scala> abstract class XY {
     |   def x: Int
     |   def y: Int
     | }
defined class XY

継承

Scalaのクラスは、Javaのクラスと同様、継承することができます。継承には2つの目的があります。1つは継承によりスーパークラスの実装をサブクラスでも使うことで実装を再利用することです。もう1つは複数のサブクラスが共通のスーパークラスのインタフェースを継承することで処理を共通化することです1

実装の継承には複数の継承によりメソッドやフィールドの名前が衝突する場合の振舞いなどに問題があることが知られており、 Javaでは実装継承が1つだけに限定されています。Java 8ではインタフェースにデフォルトの実装を持たせられるようになりましたが、変数は持たせられないという制約があります。Scalaではトレイトという仕組みで複数の実装の継承を実現していますが、トレイトについては別の節で説明します。

ここでは通常のScalaのクラスの継承について説明します。 Scalaでのクラスの継承は次のような構文になります。

class SubClass(....) extends SuperClass {
  ....
}

継承のはたらきはJavaのクラスと同様ですが、既存のメソッドをオーバーライドするときはoverrideキーワードを使わなければならない点が異なります。たとえば、

scala> class APrinter() {
     |   def print(): Unit = {
     |     println("A")
     |   }
     | }
defined class APrinter

scala> class BPrinter() extends APrinter {
     |   override def print(): Unit = {
     |     println("B")
     |   }
     | }
defined class BPrinter

scala> new APrinter().print
A

scala> new BPrinter().print
B

のようにすることができます。ここでoverrideキーワードをはずすと、

scala> class BPrinter() extends APrinter {
     |   def print(): Unit = {
     |     println("B")
     |   }
     | }
<console>:14: error: overriding method print in class APrinter of type ()Unit;
 method print needs `override' modifier
         def print(): Unit = {
             ^

のようにメッセージを出力して、コンパイルエラーになります。Javaではしばしば、気付かずに既存のメソッドをオーバーライドするつもりで新しいメソッドを定義してしまうというミスがありましたが、Scalaではoverrideキーワードを使って言語レベルでこの問題に対処しているのです。

1. このように継承などにより型に親子関係を作り、複数の型に共通のインタフェースを持たせることをサブタイピング・ポリモーフィズムと呼びます。Scalaでは他にも構造的部分型というサブタイピング・ポリモーフィズムの機能がありますが、実際に使われることが少ないため、このテキストでは説明を省略しています。

results matching ""

    No results matching ""