クラス

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

クラス定義

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

class <クラス名> '(' (<引数名1> : <引数型1>, <引数名2>: <引数型2> ...)? ')' {
  (<フィールド定義> | <メソッド定義> )*
}

たとえば、点を表すクラス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 | <パッケージ名>])? | protected([<パッケージ名>])?)? def <メソッド名> '('
  (<引数名> : 引数型 (, 引数名 : <引数型>)*)?
')': <返り値型> = <本体>

実際にはブロック式を使った以下の形式を取ることが多いでしょう。

(private([this | <パッケージ名>])? | protected([<パッケージ名>])?)? def <メソッド名> '('
  (<引数名> : 引数型 (, 引数名 : <引数型>)*)?
')': <返り値型> = {
  (<式> (; | <改行>)?)*
}

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

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

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

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 + ")"
}

val p1 = new Point(1, 1)
// p1: Point = (1, 1)

val p2 = new Point(2, 2)
// p2: Point = (2, 2)

p1 + p2
// res0: Point = (3, 3)

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

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

(private([this | <パッケージ名>])? | protected([<パッケージ名>])?)? def <メソッド名> '('
  (<引数名> : 引数型 (, 引数名 : <引数型>)*)?
')'( '('
  (<引数名> : 引数型 (, 引数名 : <引数型>)*)?
')' )* : <返り値型> = <本体式>

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

class Adder {
  def add(x: Int)(y: Int): Int = x + y
}

val adder = new Adder()
// adder: Adder = repl.MdocSession$App$Adder$1@525f0900

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

val fun = adder.add(2) _
// fun: Int => Int = <function1>
fun(3)
// res2: Int = 5

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

次のように、複数の引数リストを使わずに単に複数の引数を持つメソッドも作ることができます。

class Adder {
  def add(x: Int, y: Int): Int = x + y
}

val adder = new Adder()
// adder: Adder = repl.MdocSession$App$Adder$2@10b54ff9

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

val fun: Int => Int = adder.add(2, _)
// fun: Int => Int = <function1>
fun(3)
// res4: Int = 5

フィールド定義

フィールド定義は

(private([this | <パッケージ名>])? | protected([<パッケージ名>])?)? (val | var) <フィールド名>: <フィールド型> = <初期化式>

という形を取ります。valの場合は変更不能、varの場合は変更可能なフィールドになります。また、privateを付けるとそのクラス内だけから、protectedを付けるとそのクラスの派生クラスからのみアクセスできるフィールドになります。 private[this] を付加すると、同じオブジェクトからのみアクセス可能になります。さらに、private[<パッケージ名>]を付けると同一パッケージからのみ、 protected[パッケージ名] をつけると、派生クラスに加えて同じパッケージに所属しているもの全てからアクセスできるようになります。 privateprotectedも付けない場合、そのフィールドはpublicとみなされます。 private[this]を付けたフィールドへのアクセスは一般にJVMレベルでのフィールドへの直接アクセスになるため、若干高速です。細かいレベルでのパフォーマンスチューニングをする際は意識すると良いでしょう。

抽象メンバー

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

(private([this | <パッケージ名>])? | protected([<パッケージ名>])?)? def <メソッド名> '('
  (<引数名> : 引数型 (, 引数名 : <引数型>)*)?
')': <返り値型>

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

(private([this | <パッケージ名>])? | protected([<パッケージ名>])?)? (val | var) <フィールド名>: <フィールド型>

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

abstract class XY {
  def x: Int
  def y: Int
}

継承

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

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

class <クラス名> <クラス引数> (extends <スーパークラス>)? (with <トレイト名>)* {
  (<フィールド定義> | <メソッド定義>)*
}

トレイト名はここでは使われませんが、後で出てくるトレイトの節で説明を行います。継承のはたらきはJavaのクラスと同様ですが、既存のメソッドをオーバーライドするときはoverrideキーワードを使わなければならない点が異なります。 たとえば、次のようにすることができます。

class APrinter() {
  def print(): Unit = {
    println("A")
  }
}

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

new APrinter().print
// A

new BPrinter().print
// B

ここでoverrideキーワードをはずすと、次のようにメッセージを出力して、コンパイルエラーになります。

class BPrinter() extends APrinter {
  def print(): Unit = {
    println("B")
  }
}

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

練習問題

全てが Int 型の xyz という名前を持った、3次元座標を表す Point3D クラスを定義してください。 Point3D クラスは次のようにして使うことができなければいけません。

val p = new Point3D(10, 20, 30)
println(p.x) // 10
println(p.y) // 20
println(p.z) // 30

練習問題

次の抽象クラス Shape を継承して、 Rectangle クラス(長方形クラス)とCircle クラス(円クラス)を定義してください。また、area メソッドをオーバーライドして、ただしく面積が計算できるように定義してください。なお、長方形の面積は幅をw、高さをhとすると、w * hで求めることができます。円の面積は、半径をrとすると、r * r * math.Pi で求めることができます。

abstract class Shape {
  def area: Double
}
/*
 * RectangleとCircleの定義
 */
var shape: Shape = new Rectangle(10.0, 20.0)
println(shape.area)
shape = new Circle(2.0)
println(shape.area)
1. このように継承などにより型に親子関係を作り、複数の型に共通のインタフェースを持たせることをサブタイピング・ポリモーフィズムと呼びます。Scalaでは他にも構造的部分型というサブタイピング・ポリモーフィズムの機能がありますが、実際に使われることが少ないため、このテキストでは説明を省略しています。

results matching ""

    No results matching ""