M.Hiroi's Home Page

Scala Programming

お気楽 Scala プログラミング入門

[ PrevPage | Scala | NextPage ]

オブジェクト指向の基礎知識

プログラミングに興味のある方ならば、オブジェクト指向という言葉は聞いたことがあると思います。よく使われているオブジェクト指向言語にC++ があります。C++ はオブジェクト指向プログラミングができるようにC言語を拡張したものですが、度重なる機能追加により複雑な言語仕様になってしまいました。このため、初心者がオブジェクト指向を学ぶには適していないと言われています。

Java のオブジェクト指向はC++ よりも簡単だといわれていますが、C++ と同じようにバージョンアップするたびに新しい機能が追加されるので、Java のオブジェクト指向機能もかなり複雑になりつつあります。初心者 (M.Hiroi を含む) からみると、どちらのオブジェクト指向も大変難しい、と思われている方が多いのではないでしょうか。初心者がオブジェクト指向を学ぶには Ruby のようなスクリプト言語のほうが適しているのかもしれません。

Scala のオブジェクト指向は Java と同様に高機能ですが、その中には Java よりも複雑で難しい機能もあります。ハードルは少々高めですが、Scala はオブジェクト指向言語なので、そこを避けて通るわけにもいきません。そこで、簡単なプログラムを作りながら、少しずつステップアップしていきましょう。まずは最初に、一般的なオブジェクト指向について簡単に説明します。

●オブジェクトとは?

プログラムを作る場合、全体を小さな処理に分割して、一つ一つの処理を作成し、それらを組み合わせて全体のプログラムを完成させます。このとき、基本的な部品となるのが関数です。つまり、処理を関数単位で分割して、それらを組み合わせてプログラムを作るわけです。もともと関数の役割は、入力されたデータを処理してその結果を返すことです。つまり、関数は機能を表しているのです。このため、全体を小さな処理に分割するにしても、機能単位で行われることが普通です。

オブジェクト指向プログラミングでは、関数ではなく「オブジェクト (object)」を部品として扱います。たとえば、えんぴつを考えてみましょう。えんぴつには、色、長さ、固さ、などいろいろな性質があります。そして、えんぴつを使って文字を書いたり、絵を描いたりすることができます。プログラムでは、このような性質をデータで表し、機能を関数で表すことになります。そしてオブジェクトとは、このデータと関数を結び付けたものなのです。

いままでのプログラミング言語では、データと関数を別々に定義するため、それを一つのオブジェクトとして表すことができません。えんぴつで文字を書くにも、えんぴつの種類をチェックして文字を書くようにプログラムしなければいけません。ところが、オブジェクトはデータと関数を結び付けたものなので、自分がなにをしたらよいかわかっています。えんぴつオブジェクトに文字を書けと命じれば、それが赤えんぴつのオブジェクトであれば文字は赤に、黒えんぴつのオブジェクトであれば黒い文字になるのです。

このように、オブジェクトはデータと関数を一つにまとめたものです。従来のプログラミングが全体を機能単位で分割するのに対し、オブジェクト指向プログラミングでは全体をオブジェクト単位に分割して、それを組み合わせることでプログラムを作成します。

ところで、データと関数を結び付けることは、従来のプログラミング言語でも可能です。オブジェクト指向はプログラミングの考え方の一つであり、C++のようなオブジェクト指向言語を使わなくても、たとえばC言語でもその考え方にしたがってプログラムを作れば、オブジェクト指向プログラミングになります。

実際、オブジェクト指向には様々な考え方があり、いろいろなオブジェクト指向プログラミング言語が存在します。ですが、データと関数を一つにまとめたものをオブジェクトとして扱うという基本的な考え方は、オブジェクト指向言語の元祖と言われる Smalltalk でも、C++, Java, Scala でも同じです。

●クラスとインスタンス

次は、一般的なオブジェクト指向機能について簡単に説明します。

「クラス (class)」はオブジェクトの振る舞いを定義したものです。ここでデータを格納するための変数や、それを操作する関数が定義されます。Java や Scala では、この変数を「フィールド変数」とか「フィールド」といいます。他の言語ではメンバ変数とかインスタンス変数と呼ぶことがあります。そして、クラスの中で定義された関数を「メソッド (method)」といいます。メソッドはあとで説明します。

クラスはオブジェクトの設計図にあたるもので、オブジェクトの「雛形」と呼ぶこともあります。クラスはオブジェクトの振る舞いを定義するだけで、アクセスできる実体はなにも生み出していない、ということに注意してください。ただし、プログラミング言語によってはクラスに実体を持たせていることもあります。

このクラスから実体として作り出されるのが「インスタンス (instance)」です。このインスタンスを「オブジェクト」と考えてください。インスタンスを生成する方法は、当然ですがプログラミング言語によって違います。たとえば Java や Scala は new を使います。次の図を見てください。


               図 : クラスとインスタンスの関係

クラスはオブジェクトの定義を表すものですから、Foo というクラスは一つしかありません。これに対し、インスタンスはクラスから生み出されるオブジェクトです。たとえば、クラス Foo に new を適用することで、いくつでもインスタンスを生み出すことができるのです。クラスは設計図であり、それに従って作られるオブジェクトがインスタンスと考えるとわかりやすいでしょう。

●メソッド

メソッドはオブジェクトと結びついた関数です。オブジェクト指向プログラミングでは、ほかの関数から直接オブジェクトを操作することはせず、メソッドを呼び出すことで行います。メソッドは、クラスが異なっていれば同じ名前のメソッドを定義することができます。たとえば、クラス Foo1 にメソッド bar が定義されていても、クラス Foo2 に同名のメソッド bar を定義することができます。

そして、ここからが重要なのですが、あるオブジェクトに対してメソッド bar を呼び出した場合、それが Foo1 から作られたオブジェクトであれば、Foo1 で定義された bar が実行され、Foo2 から作られたオブジェクトであれば、Foo2 で定義された bar が実行されるのです。このように、オブジェクトが属するクラスによって、実行されるメソッドが異なるのです。この機能を「ポリモーフィズム (polymorphism)」と呼びます。これにより、オブジェクトは自分が行うべき適切な処理を実行できるわけです。

クラス、インスタンス、メソッドの関係は図に示すと次のようになります。


         図 : クラス、インスタンス、メソッドの関係

クラスという設計図が中心にあり、そこからインスタンスが生み出され、メソッドを使ってインスタンスを操作する、という関係になります。

●継承

「継承 (inheritance : インヘリタンス)」は簡単に言うとクラスに「親子関係」を持たせる機能です。子供のクラスは親クラスの性質を受け継ぐことができます。プログラミング言語の場合、引き継ぐ性質は定義されたインスタンス変数やメソッドになります。プログラムを作る場合、今まで作ったプログラムと同じような機能が必要になることがありますが、継承を使うことでその機能を受け継ぎ、新規の機能や変更される機能だけプログラムする、いわゆる「差分プログラミング」が可能になります。

クラスを継承する場合、その元になるクラスを「スーパークラス」とか「ベースクラス」と呼びます。そして、継承したクラスを「サブクラス」と呼びます。この呼び方は言語によってまちまちで統一されていません。C++の場合は、元になるクラスを基本クラスといい、継承するクラスを派生クラスとか導出クラスといいます。

たとえば、クラス Foo1 を継承してクラス Foo2 を定義しましょう。クラス Foo1 にはメソッド bar が定義されています。クラス Foo2 にメソッド bar は定義されていませんが、Foo2 のオブジェクトに対して bar を呼び出すと、スーパークラス Foo1 のメソッド bar が実行されるのです。

メソッドの選択は次のように行われます。まず、オブジェクトが属するクラス Foo2 にメソッド bar が定義されているか調べます。ところが、Foo2 には bar が定義されていないので、スーパークラスである Foo1 に bar が定義されているか調べます。ここでメソッド bar が見つかり、それを実行するのです。このように、メソッドが見つかるまで順番にスーパークラスを調べていきますが、最上位のスーパークラスまで調べてもメソッドが見つからない場合はエラーになります。

継承したクラスのメソッドとは違う働きをさせたい場合、同名のメソッドを定義することで、そのクラスのメソッドを設定することができます。これを「オーバーライド (over ride)」といいます。メソッドを選択する仕組みから見た場合、オーバーライドは必然の動作です。メソッドはサブクラスからスーパークラスに向かって探索されるので、スーパークラスのメソッドよリサブクラスのメソッドが先に選択されるわけです。

●Scala のクラス

さて、一般的な話はここまでにして、Scala のオブジェクト指向機能に目を向けてみましょう。Scala は class 文でクラスを定義します。class 文の基本的な構文を図に示します。

class 名前 extends スーパークラス {

    ...

}

      図 : class 文の構文

class の次に名前を指定し、その次の extends で他のクラスを指定すると、そのクラスの機能を「継承」することができます。継承は次回以降で詳しく説明します。extends を省略すると暗黙のうちに AnyRef というクラスが継承されます。AnyRef はいわゆる「参照型データ」のスーパークラスで、AnyRef のスーパークラスが Any になります。Scala のすべてのクラスは直接的もしくは間接的に Any を継承しています。そのあとのブロック { ... } の中でフィールド変数やメソッドを定義します。

Scala の場合、class で定義されたクラスは Java の public で宣言したクラスと同じで、そのクラスは他の任意のクラスからアクセスすることできます。

一番簡単なクラス定義を示しましょう。次の例を見てください。

scala> class Foo
class Foo

scala> val a = new Foo
val a: Foo = Foo@18b74ea

一般に、クラス名は英大文字から始めることが多いので、名前は Foo としました。Foo はクラス名しかありませんが、これでも立派なクラスなのです。Scala は new を付けて Foo と呼び出すと、そのクラスのインスタンスを生成して返します。Foo(...) を「コンストラクタ」といって、呼び出すときカッコの中に引数を指定することができます。これはあとで説明します。インスタンスの型はクラス名で表すことができます。変数 a の型は Foo になります。

●Scala のインスタンス

Scala はクラス内で宣言された変数をフィールド変数として扱います。他の言語ではインスタンス変数とかメンバ変数といいます。フィールド変数の実体はインスタンスに割り当てられます。

Scala のフィールド変数は protected, private, 指定無し という 3 通りのアクセス制御を行うことができます。指定無しは Java の public と同じで、どのクラスからでもアクセスすることできます。protected は同じクラスとそれを継承したクラス (サブクラス) からアクセスすることができ、private は同じクラスからしかアクセスすることができません。

フィールド変数のアクセスは次の形式で行います。

obj.variable

インスタンス obj の後ろにドット ( . ) を付けて、その後ろにフィールド変数名を指定します。同じクラス内であれば、obj を省略してフィールド変数名だけでアクセスすることができます。

簡単な例を示しましょう。

scala> class Foo {
     | var x = 1
     | }
class Foo

scala> val a = new Foo
val a: Foo = Foo@70025b99

scala> val b = new Foo
val b: Foo = Foo@5f61371d

scala> a.x
val res0: Int = 1

scala> b.x
val res1: Int = 1

scala> a.x = 10
// mutated a.x

scala> b.x = 100
// mutated b.x

scala> a.x
val res2: Int = 10

scala> b.x
val res3: Int = 100

クラス Foo のインスタンスを生成して変数 a と b にセットします。インスタンス a, b にあるフィールド変数 x は、それぞれ a.x, b.x でアクセスすることができます。a.x = 10 とすると a の x に 10 が代入され、b.x = 100 とすると b の x に 100 が代入されます。

var の前に private を付けると、外部からアクセスすることができなくなります。

scala> class Foo1 {
     | private var x = 1
     | }
class Foo1

scala> val c = new Foo1
val c: Foo1 = Foo1@70e8c019

scala> c.x
         ^
       error: variable x in class Foo1 cannot be accessed as a member of Foo1 from class

●Scala のメソッド

Scala はクラス内で定義された関数をメソッドとして扱います。Scala のメソッドは protected, private, 指定無し という 3 通りのアクセス制御を行うことができます。これはフィールド変数のアクセス制御と同じです。

簡単な例として、クラス Foo にフィールド変数 x のリーダーメソッドとライターメソッドを定義しましょう。次の例を見てください。

scala> class Foo {
     | private var x = 1
     | def getX(): Int = x
     | def putX(n: Int): Unit = x = n
     | }
class Foo

scala> val a = new Foo
val a: Foo = Foo@441fbe89

scala> a.getX()
val res0: Int = 1

scala> a.putX(10)

scala> a.getX()
val res2: Int = 10

scala> val b = new Foo
val b: Foo = Foo@47fa3671

scala> b.getX()
val res3: Int = 1

scala> b.putX(100)

scala> b.getX()
val res5: Int = 100

フィールド変数 x は private ですが、メソッド getX と putX が public なので、これらのメソッドを使って x にアクセスすることができます。メソッド getX がリーダーで、putX がライターです。メソッド名の付け方は適当でかまいませんが、リーダーの場合は変数名の前に get や read を、ライターの場合は put, set, write などを付けるとわかりやすいと思います。

これらのメソッドは次の形式で呼び出します。

obj.method(args, ...)

インスタンス obj の後ろにドット ( . ) を付けて、メソッド名と引数を続けて書きます。同じクラス内であれば、obj を省略してメソッド名だけで呼び出すことができます。

実をいうと、フィールド変数のアクセスも、参照用と更新用のメソッドが自動的に定義され、それが呼び出されています。フィールド変数を参照するメソッドは、フィールド変数と同じ名前になります。更新用のメソッドは "名前_=(引数)" となります。たとえば、a.x = 10 は a.x_=(10) でも x の値を 10 に書き換えることができます。

●コンストラクタ

インスタンスを生成するとき、フィールド変数に初期値を代入できると便利です。Scala の場合、インスタンスを生成するとき、class 定義のブロック内に書かれている処理が実行されます。これを「基本コンストラクタ」といいます。また、次のようにクラス名の後ろにカッコを付け、その中でコンストラクタに渡す引数を指定することができます。

class className([val or var] 引数: 型, ...) { ... }

引数の前に val または var を付けると、それらの引数は public なフィールド変数として扱われます。val または var の指定がないと、関数の引数のように immutable な変数として扱われます。コンストラクタの引数は、クラス内の処理から参照することができます。

簡単な例を示しましょう。

scala> class Foo(x: Int) {
     | def getX: Int = x
     | }
class Foo

scala> val a = new Foo(1)
val a: Foo = Foo@55cc4c61

scala> a.x
         ^
       error: value x is not a member of Foo

scala> a.getX
val res7: Int = 1

この場合、Foo には必ず引数を指定して呼び出してください。引数の個数が合わないとエラーになります。引数 x に var を付けていないので、インスタンスを生成して a.x とアクセスしてもエラーになります。メソッド getX はコンストラクタ引数 x を参照することができるので、x の値を返すことができます。

引数 x に var (または val) を指定すると、x は public なフィールド変数になります。簡単な例を示します。

scala> class Foo(var x: Int)
class Foo

scala> val a = new Foo(1)
val a: Foo = Foo@5ce1ec7

scala> a.x
val res8: Int = 1

scala> a.x = 10
// mutated a.x

scala> a.x
val res9: Int = 10

●補助コンストラクタ

Scala の場合、補助コンストラクタ this を定義することができます。

def this(引数: 型, ...) = { 式, ... }

補助コンストラクタは「多重定義 (overload)」ができるので、引数の個数が異なる補助コンストラクタを定義することができます。多重定義はあとで説明します。補助コンストラクタの本体では、最初に他のコンストラクタを呼ぶようにしてください。また、Java と同様に this は自分自身 (インスタンス) を表すキーワードとして使用することができます。

簡単な例を示します。

scala> class Foo(var x: Int){
     | def this() = this(1)
     | def this(a: Int, b: Int) = this(a + b)
     | def getX(): Int = x
     | def putX(n: Int): Unit = x = n
     | }
class Foo

scala> val a = new Foo
val a: Foo = Foo@3b9d5218

scala> a.getX()
val res10: Int = 1

scala> val b = new Foo(10, 20)
val b: Foo = Foo@64c189d8

scala> b.getX()
val res11: Int = 30

引数なしの補助コンストラクタと引数が 2 つの補助コンストラクタを定義します。引数なしのコンストラクタでは、基本コンストラクタ this(1) を呼び出して、フィールド変数 x を 1 に初期化します。引数が 2 つのコンストラクタでは、基本コンストラクタ this(a + b) を呼び出して、フィールド変数 x を a + b の値に初期化します。メソッドの局所変数とフィールド変数名が同じ場合、Scala は局所変数のアクセスを優先します。フィールド変数にアクセスする場合は this を付けてください。

なお、コンストラクタの引数は、関数と同様に「デフォルト引数」と「名前付き引数」を使うことができます。次の例を見てください。

scala> class Foo(val a: Int = 1, val b: Int = 10, val c: Int = 100){
     | def display(): Unit = print(s"$a, $b, $c")
     | }
class Foo

scala> (new Foo(2)).display()
2, 10, 100
scala> (new Foo).display()
1, 10, 100
scala> (new Foo(b = 100)).display()
1, 100, 100

デフォルトの値を指定できる場合は、補助コンストラクタを多重定義するよりもこちらのほうが簡単だと思います。

●メソッドの多重定義

Scala は同じクラス内で同名の関数 (メソッド) を複数定義することができます。これを「多重定義 (overload)」といいます。ただし、引数のデータ型、個数、並び方などが異なる必要があります。これらがまったく同じメソッドを多重定義することはできません。次の例を見てください。

リスト : 多重定義

object sample0801 {
  def max(a: Int, b: Int): Int = if (a > b) a else b
  def max(a: Double, b: Double): Double = if (a > b) a else b

  def main(args: Array[String]): Unit = {
    println(max(1, 2))
    println(max(1.1, 2.2))
  }
}
$ scalac sample0801.scala
$ scala sample0801
2
2.2

関数 max は引数 a と b で大きいほうを返します。同じ名前の関数が 2 つ定義されていますが、仮引数と返り値の型が異なっています。実引数に Int を渡すと最初に定義された max が呼び出されます。Double を渡すと 2 番目に定義された max が呼び出されます。このように、同じ名前の関数を定義することができます。

なお、Scala の場合、max はメソッドとして定義されているので、次のように呼び出すことができます。

scala> 1.max(2)
val res0: Int = 2

scala> (2.2).max(1.1)
val res1: Double = 2.2

scala> 1 max 2
val res2: Int = 2

scala> 1.1 min 2.2
val res3: Double = 1.1

max, min は演算子のように使うこともできます。

●Point クラス

それでは簡単な例題として、点を表すクラスを作ってみましょう。名前は Point にしました。x 座標をフィールド変数 x に、y 座標を変数 y に格納します。次の例を見てください。

scala> class Point(val x: Double = 0, val y: Double = 0) {
     | def distance(p: Point): Double = {
     | val dx = x - p.x
     | val dy = y - p.y
     | Math.sqrt(dx * dx + dy * dy)
     | }
     | }
class Point

scala> val p1 = new Point
val p1: Point = Point@ff4b223

scala> val p2 = new Point(10, 10)
val p2: Point = Point@3ec7eb5

scala> p1.distance(p2)
val res4: Double = 14.142135623730951

メソッド distance は Point クラスのインスタンス p を受け取り、その距離を計算します。sqrt は平方根を求める関数です。このほかにも Scala のライブラリ scala.math (別名 Math) には便利な数学関数が多数用意されています。次に、Point のインスタンスを生成して変数 p1, p2 にセットします。あとは、メソッド distance を呼び出すと、p1 と p2 の距離を求めることができます。

ここで、メソッドの呼び出しは、インスタンスによって適切なメソッドが選択されることに注意してください。たとえば、3 次元の座標を表す Point3D クラスを考えてみましょう。次の例を見てください。

scala> class Point3D(val x: Double = 0, val y: Double = 0, val z: Double = 0) {
     | def distance(p: Point3D): Double = {
     | val dx = x - p.x
     | val dy = y - p.y
     | val dz = z - p.z
     | Math.sqrt(dx * dx + dy * dy + dz * dz)
     | }
     | }
class Point3D

クラス Point3D は Point を 3 次元に拡張しただけです。Point でも Point3D でも距離を計算するメソッド distance が定義されていることに注目してください。それでは、メソッド distance を呼び出してみましょう。

scala> val p3 = new Point3D
val p3: Point3D = Point3D@147097ad

scala> val p4 = new Point3D(10, 10, 10)
val p4: Point3D = Point3D@72c175f1

scala> p3.distance(p4)
val res5: Double = 17.320508075688775

ドットの左側のインスタンス p1, p3 によって適切なメソッドが呼び出され、ポリモーフィズムが働いているようにみえます。Perl, Python, Ruby などのように、変数を使用するときデータ型の宣言が不要なプログラミング言語を「動的型付け言語」といい、Jva や Scala などのように変数を使用するときデータ型の宣言が必要なプログラミング言語を「静的型付け言語」といいます。

たとえば Ruby の場合、p1.distance(p3) で呼び出されるメソッド distance は、プログラムを実行するとき変数 p1 に格納されているオブジェクトの型 (クラス) によって決定されます。p1 が Point のインスタンスであれば、Point で定義されたメソッド distance() が呼び出されます。つまり、動的型付け言語のメソッド呼び出しはポリモーフィズムが働いている、と考えることができます。

これに対して、Java や Scala などの静的型付け言語は、コンパイルの時点で呼び出すメソッドを可能な限り決定します。たとえば、p1.distance(p2) の呼び出しは Point クラスのメソッドで、p3.distance(p4) は Point3D クラスのメソッドと決めることが可能です。この場合、動的型付け言語のようなポリモーフィズムは働いていませんが、オブジェクトのクラスによって呼び出すメソッドが決定される、ということにかわりはありません。

Scala でプログラムの実行時にポリモーフィズムを働かせるには「継承」を使います。これは次回以降に説明する予定です。

●toString メソッド

対話モード (REPL) や print, println は値 (オブジェクト) を表示するとき、そのオブジェクトに toString メソッドが定義されている場合は、それを呼び出して文字列に変換して表示します。そうでない場合はデフォルトの表示処理を行います。toString をオーバーライドすると、独自の表示処理を行うことができます。

簡単な例を示します。

scala> class Foo(val x: Int = 0, val y: Int = 0){
     | override def toString: String = s"Foo($x, $y)"
     | }
class Foo

scala> val a = new Foo
val a: Foo = Foo(0, 0)

scala> println(a)
Foo(0, 0)

scala> new Foo(1, 2)
val res1: Foo = Foo(1, 2)

Scala でメソッドをオーバーライドするときは def の前にキーワード override を必ず付けてください。

●equals メソッド

new で生成されたインスタンスを演算子 == で比較する場合、演算子 eq と同じ動作、つまりオブジェクトのアドレスを比較します。次の例を見てください。

scala> a
val res2: Foo = Foo(0, 0)

scala> val b = new Foo
val b: Foo = Foo(0, 0)

scala> a == b
val res3: Boolean = false

scala> a eq b
val res4: Boolean = false

scala> a eq a
val res5: Boolean = true

インスタンスが異なる場合、フィールド変数 a, b の値が同じでも演算子 == で比較すると false になります。フィールド変数の値を比較して等値を判定したい場合はメソッド equals をオーバーライドします。

scala> class Foo(val x: Int = 0, val y: Int = 0){
     | override def toString: String = s"Foo($x, $y)"
     | override def equals(other: Any): Boolean = other match {
     | case that: Foo => that.x == this.x && that.y == this.y
     | case _ => false
     | }
     | }
class Foo

scala> val a = new Foo
val a: Foo = Foo(0, 0)

scala> val b = new Foo
val b: Foo = Foo(0, 0)

scala> a == b
val res6: Boolean = true

scala> val c = new Foo(1, 2)
val c: Foo = Foo(1, 2)

scala> a == c
val res8: Boolean = false

メソッド equals の引数 other の型は Any になるので、まず最初に other の型が Foo であることをチェックする必要があります。型チェックはメソッド isInstanceOf[型] や match 式で行うことができます。match 式の場合、case 節で型を指定します。

式0 match {
  case 変数1: 型1 => 式1
  case 変数2: 型2 => 式2
    ...
  case 変数n: 型n => 式n
}

case の後ろに 変数 + ":" + 型 を指定します。式0 の返り値が指定した型と一致した場合、その case 節が選択され、式0 の返り値は変数にセットされます。引数 other の型が Foo であることを確認したら、あとはお互いのフィールド変数 x, y が等しいことを確かめるだけです。

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

class で宣言したクラスの場合、そこから複数のインスタンスを生成することができますが、object で宣言されたクラスからは、ひとつのインスタンスしか生成することができません。これを「シングルトンオブジェクト」といいます。シングルトンオブジェクトの実体 (インスタンス) は Scala が自動的に生成するので、私たちが new でインスタンスを生成することはできません。object の構文を示します。

object クラス名 extends スーパークラス {

    ...

}

      図 : object の構文

object は他のクラスを継承することができます。ただし、他のクラスが object を継承することはできません。class と同様に、フィールド変数やメソッドを定義することはできますが、クラス名のあとにコンストラクタの引数を定義することや、補助コンストラクタを定義することはできません。

簡単な例を示します。

scala> object Bar {
     | private var x = 1
     | def getX(): Int = x
     | def putX(n: Int): Unit = x = n
     | }
object Bar

scala> Bar.getX()
val res9: Int = 1

scala> Bar.putX(10)

scala> Bar.getX()
val res11: Int = 10

scala> val a = Bar
val a: Bar.type = Bar$@d4df4ce

scala> a.getX()
val res12: Int = 10

scala> a.putX(100)

scala> a.getX()
val res14: Int = 100

scala> Bar.getX()
val res15: Int = 100

シングルトンオブジェクトは object で指定した名前でアクセスすることができます。たとえば、名前が Bar であれば、Bar.getX, Bar.putX(10) でメソッドを呼び出すことができます。また、シングルトンオブジェクトは「値」なので、変数に代入することができます。この場合、型は object の名前の後ろに .type を付けたものになります。Bar の場合は Bar.type になります。

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

object にはクラスと同じ名前をつけることができます。これを「コンパニオンオブジェクト」といいます。クラスで定義された private 変数はコンパニオンオブジェクトからでもアクセスすることができ、逆に、コンパニオンオブジェクトの private 変数もクラスからアクセスすることができます。

簡単な例を示しましょう。

リスト : コンパニオンオブジェクト

class Foo {
  private var x = 1
  def getX(): Int = x
  def putX(n: Int): Unit = x = n
  def getY(): Int = Foo.y
  def putY(n: Int): Unit = Foo.y = n
}

object Foo {
  private var y = 2
  def getX(p: Foo): Int = p.x
  def putX(p: Foo, n: Int): Unit = p.x = n
  def getY(): Int = y
  def putY(n: Int): Unit = y = 2
}

object sample0802 {
  def main(args: Array[String]): Unit = {
    val a = new Foo
    println(a.getX())
    println(Foo.getX(a))
    println(a.getY())
    println(Foo.getY())
    Foo.putX(a, 10)
    a.putY(20)
    println(a.getX())
    println(Foo.getX(a))
    println(a.getY())
    println(Foo.getY())
  }
}
$ scalac sample0802.scala
$ scala sample0802
1
1
2
2
10
10
20
20

REPL でコンパニオンオブジェクトを定義する場合はコマンド :paste を使ってください。コンパニオンオブジェクト Foo の変数 y は private ですが、クラス Foo のメソッド getY, putY で Foo.y とすると、変数 y にアクセスすることができます。また、クラス Foo の変数 x が private でも、コンパニオンオブジェクトのメソッド getX, putX では p.x でアクセスすることができます。

フィールド変数は個々のインスタンスに割り当てられる変数です。その値はインスタンスによって変わります。クラスで共通の変数や定数を使いたい場合、Java では class 文の中で static 変数を定義します。これを「クラス変数」といいます。

同様に、メソッドは個々のインスタンスを操作する関数です。一般に、ユーザが定義するメソッドは引数のインスタンスを操作対象とし、クラスの動作にかかわることはありません。インスタンスを操作するメソッドを「インスタンスメソッド」といいます。これに対し、クラスの動作にかかわるメソッドを考えることができます。これを「クラスメソッド」といいます。Java はメソッドを static 宣言するとクラスメソッドになります。

Scala には static 宣言がないので、Java のようなクラス変数やクラスメソッドを定義することができませんが、コンパニオンオブジェクトを使って同様なことを行うことは可能です。

●apply メソッド

Scala の場合、コンパニオンオブジェクトで特別なメソッド apply を定義すると、new を使わないでインスタンスを生成することができます。次の例を見てください。

scala> :paste
// Entering paste mode (ctrl-D to finish)

class Foo(val x: Int)
object Foo {
def apply(a: Int) = new Foo(a)
}

// Exiting paste mode, now interpreting.

class Foo
object Foo

scala> val a = Foo(10)
val a: Foo = Foo@66223d94

scala> a.x
val res0: Int = 10

scala> val b = new Foo(10)
val b: Foo = Foo@2f4d32bf

scala> b.x
val res1: Int = 10

scala> val c = Foo.apply(10)
val c: Foo = Foo@19d27c27

scala> c.x
val res2: Int = 10

コンパニオンオブジェクトのメソッド apply で Foo のインスタンスを生成して返しています。メソッド apply を呼び出すとき、普通は Foo.apply(10) としますが、apply に限って apply を省略して Foo(10) とすることができます。これで Foo(10) とするだけで、Foo のインスタンスを生成することができます。もちろん、new Foo(10) でも、Foo.apply(10) でもインスタンスを生成することができます。

new によるインスタンスの生成を禁止したい場合は、Foo のコンストラクタを privete に指定します。次の例を見てください。

scala> :paste
// Entering paste mode (ctrl-D to finish)

class Foo private(val x: Int)
object Foo {
def apply(a :Int) = new Foo(a)
}

// Exiting paste mode, now interpreting.

class Foo
object Foo

scala> val a = Foo(10)
val a: Foo = Foo@4fef4f96

scala> val b = new Foo(100)
               ^
       error: constructor Foo in class Foo cannot be accessed in class $iw from class

クラス名と引数の間に private を指定すると、そのクラスのコンストラクタは private に設定されます。コンパニオンオブジェクトは同名クラスの private 変数やメソッドにアクセスできるので、apply メソッドで Foo のコンストラクタを呼び出すことができます。したがって、Foo(10) でインスタンスを生成することはできますが、new Foo(100) とするとエラーになります。

●upapply メソッド

パターンマッチングでフィールド変数の値を取り出したいときは、コンパニオンオブジェクトにメソッド unapply を定義します。unapply のことを「抽出子」といいます。次の例を見てください。

リスト : unapply メソッドの使用例

class Foo private (val x: String, val y: Int)
object Foo {
  def apply(a: String, b: Int) = new Foo(a, b)
  def unapply(p: Foo) = Some((p.x, p.y))
}

object sample0803 {
  def test(p: Foo) =
    p match {
      case Foo("hello", n) => println(n)
      case _ => println("oops!")
    }

  def main(args: Array[String]): Unit = {
    val a = Foo("hello", 10)
    val b = Foo("world", 20)
    test(a)
    test(b)
  }
}
$ scalac sample0803.scala
$ scala sample0803
10
oops!

unapply の引数はインスタンスです。返り値は Option で、フィールド変数の値を Some に格納して返します。複数のフィールド変数とマッチングさせたい場合は、それらの値をタプルに格納し、それを Some に格納して返します。

Foo には String と Int を格納するフィールド変数 x, y があるので、unapply はタプル (p.x, p.y) を Some に格納して返します。関数 test は match 式で引数 p とパターンマッチングし、Foo("hello", n) とマッチングしたら、変数 n の値を表示します。それ以外の値は oops! と表示します。変数 a のインスタンスは "hello" と 10 を格納しているので、test(a) は 10 と表示します。変数 b のインスタンスは "world" と 20 を格納していますが、文字列が "hello" ではないので、マッチングに失敗して oops! と表示されます。

●case クラス

toString, equals, apply, unapply などのメソッドは大変便利ですが、いちいち定義するのは面倒ですね。Scala の場合、case クラスを使うと toString, equals やコンパニオンオブジェクト, apply, unapply など便利なメソッドを自動的に生成してくれます。

case クラスの定義は class の前に case を付けるだけです。次の例を見てください。

scala> case class Foo(x: String, y: Int)
class Foo

scala> val a = Foo("hello", 10)
val a: Foo = Foo(hello,10)

scala> a.x
val res0: String = hello

scala> a.y
val res1: Int = 10

scala> val b = Foo("world", 20)
val b: Foo = Foo(world,20)

scala> val c = a.copy()
val c: Foo = Foo(hello,10)

scala> a == b
val res2: Boolean = false

scala> a == c
val res3: Boolean = true

scala> a match {
     | case Foo("hello", n) => n
     | }
       ^
       warning: match may not be exhaustive.
       It would fail on the following input: Foo((x: String forSome x not in "hello"), _)
val res4: Int = 10

scala> val Foo(d, e) = a
val d: String = hello
val e: Int = 10

case クラスの場合、apply メソッドが自動的に定義されるので、new を使わなくてもインスタンスを生成することができます。コンストラクタの引数に val や var を付けなくても、自動的に同じ名前のフィールド変数が定義されます。指定が無い場合、フィールド変数は immutable になります。また、値を文字列に変換するメソッド toString が定義されるので、インスタンスの表示が Foo("world", 20) になります。

メソッド copy は同じクラスのインスタンスを生成します。このとき、フィールド変数の値がコピーされます。名前付き引数を使って、フィールド変数の値を変更したインスタンスを生成することもできます。また、メソッド equals が定義されるので、フィールド変数の値を比較して等値を判定することができます。

case クラスは unapply メソッドも定義されるので、パターンマッチングでフィールド変数の値を取り出すことができます。match 式だけではなく、val や var で変数を定義するときにも使用することができます。たとえば、val Foo(d, e) = a とすれば、変数 d の値は "hello" に、変数 e の値は 10 になります。

もうひとつ、簡単な例として Point クラスを case クラスで書き直してみましょう。

scala> case class Point(val x: Double = 0, val y: Double = 0){
     | def distance(that: Point): Double = {
     | val dx = x - that.x
     | val dy = y - that.y
     | Math.sqrt(dx * dx + dy * dy)
     | }
     | }
class Point

scala> val a = new Point
val a: Point = Point(0.0,0.0)

scala> val b = new Point(10, 10)
val b: Point = Point(10.0,10.0)

scala> a.distance(b)
val res5: Double = 14.142135623730951

scala> val c = a.copy()
val c: Point = Point(0.0,0.0)

scala> a == c
val res6: Boolean = true

scala> val d = a.copy(y = 10)
val d: Point = Point(0.0,10.0)

scala> b == d
val res7: Boolean = false

このほかにも case クラスには便利な機能がありますが、実際に使うときに説明することにしましょう。


初版 2014 年 8 月 10 日
改訂 2021 年 3 月 7 日

Copyright (C) 2014-2021 Makoto Hiroi
All rights reserved.

[ PrevPage | Scala | NextPage ]