新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。気づけば社会人3年目に突入。

学習記録:Scala逆引きレシピ

第4章:オブジェクト指向プログラミング

クラス・メンバ

クラス・メソッドの定義と呼び出し

クラスはclassキーワードを、メソッドdefキーワードを使用して定義する。
クラスのインスタンスnew演算子で生成する。

// クラスの定義
class HelloWorld {
  // メソッドの定義。=を忘れないように注意
  def hello(arg: String): Unit = {
    ...
  }
  // 式が1つなら一行で書ける
  // 戻り値が推論できれば省略可能
  def hello2(arg: String) = "Hello, " + arg
  // 引数なしの場合
  def printHello ( ) = println("Hello")
  // 引数なしかつ副作用がない場合()を省略するのが慣習
  def hello3 = "Hello"
}

// インスタンスの生成
val instance = new HelloWorld

// メソッドの呼び出し。.は省略可
instance.hello("name")
instance hello("name")
// 引数が1つなら()を省略したり{}を使っても良い
instance hello "name"
instance.hello{ if(flag) "name" else "guest" }
// 引数なしの場合()は付けてもつけなくても良い
// ただし、メソッド定義で()を省略した場合は必ず()を省略する。
instance.printHello()
instance.hello3

なお、メソッドの引数はvalなため代入はできない。
また、メソッドの戻り値は最後の式の値となる。
returnも使えるが、戻り値の型を省略できなくなるなど制約があるためなるべく使わないのが一般的。

可変長引数は、引数の型の後に*を付ける。
Javaと同様最後の引数のみ可変長引数を定義でき、また内部ではSeq型として扱われる。

def args(values: String*) = ...

args()
args("a")
args("a", "b")
引数名指定・デフォルト値

引数名を指定した呼び出しや、デフォルト値を設定することもできる。

// 引数thirdArgにデフォルト値を設定
def method(firstArg: String, secondArg: String, thirdArg: String = "default") = ...
// 引数名を指定した呼び出し
method(firstArg = "a", thirdArg = "c", secondArg = "b")
// デフォルト値を使用
method("a", "b")
遅延評価

引数を遅延評価させたい場合、引数名: => 引数の型 と記述する。

// 遅延評価しない場合
def hello(execute: Boolean, f: Unit) = if (execute) f

// executeがfalse/trueに関わらず、helloが出力される
// 通常、引数はメソッドを呼びだす前に評価され、結果が渡されるため、
// 呼び出し時にprint("hello")が実行されてしまう
hello(false, print("hello")) // hello
hello(true, print("hello")) // hello

// 引数fを遅延評価する
def hello(execute: Boolean, f: => Unit) = if (execute) f

// 引数fには、その引数にアクセスしたときに評価される
hello(true, print("hello")) // hello
hello(false, print("hello")) // 何も出力されない
フィールド・コンストラクタ

フィールドはvalまたはvarキーワードで定義する。
コンストラクタはクラスの本体に直接記述し(基本コンストラクタ)、複数定義する場合はdef thisに記述する(補助コンストラクタ)。

// 引数なし
class HelloWorld {
  // 基本コンストラクタ
  val value = "Hello World"
  println(value)
}

// 引数ありの場合
// 基本コンストラクタのアクセス修飾子はクラス名と引数の間
class HelloWorld2 private (x: Int, y: Int) {
  // 基本コンストラクタ
  println(x + y)

  // 補助コンストラクタ
  // 補助コンストラクタのアクセス修飾子はdefキーワードの前
  private def this(x: Int) = {
    // 先頭で自分より前に定義されたコンストラクタを必ず呼びだす必要がある
    this(z, 0)
    println("補助です")
  }
}

// コンストラクタの引数にvalまたはvarを付けると、以下を自動生成
// privateフィールド、getter、setter(varのみ)
class HelloWorld3 (val count: Int, var name: String { ... }

val sample = new HelloWorld3(1, "hoge")

sample.name // => "hoge"
sample.name_=("huga")  // nameにhugaを設定
sample.name = "huga"   // 糖衣構文で代入式のようにも書ける

アクセス修飾子にはprivateとprotectedがあり、protectedはサブクラスからも、privateは自クラスのみから参照可能。
また、アクセス修飾子がない場合、どこからでも参照可能であることに注意。

継承と抽象クラス

継承・シールド・抽象クラス

Javaと同様、extendsキーワードによりクラスの継承が可能。多重継承は不可。
また、sealedキーワードを付与するとシールドクラスになり、このクラスを継承できるのは同一ソースファイル内のクラスのみに制限される。
これにより存在しうるサブクラスがコンパイル時に確定し、パターンマッチの網羅性をコンパイラがチェックできる。
抽象クラスにはabstractキーワードをつける。
Javaと異なり抽象メンバにabstractキーワードは不要(実装がなければ抽象メンバになる)で、メソッドだけでなくフィールドも抽象化できる。

sealed abstract class Message

case class TextMessage () extends Message
case class ObjectMessage () extends Message
...
オーバーライド・final

フィールドやメソッドのオーバーライドにはoverrideキーワードをつける。
Javaと異なり、オーバーライドする場合は必須であり、記述しないとコンパイラエラーになる。
また、フィールドやメソッドをfinalにするとオーバーライドができなくなる。

class SuperClass {
  val id = 1
  var key = "key01"
  def name = "SuperClass"
}

class SubClass extends SuperClass {
  // フィールドのオーバーライド
  override val id = 100
  // varのオーバーライドにはoverrideは不要
  key = "hoge"
  // メソッドのオーバーライド
  override def name = "SubClass" + super.name
}

トレイト・ミックスイン

トレイトは、Javaでいうインターフェイスのようなもので、Java8と同じくメソッドを実装できる。
トレイトはtraitキーワードを使って定義し、ミックスイン(Javaでいうimplements)にはwithを使用する。

trait TraitSample {
  val value: String
  def name = "TraitSample"
}

// トレイトのミックスイン
class Class1 extends SuperClass with TraitSample { ... }
// 継承クラスがない場合、最初のトレイトのみextendsを使う
class Class2 extends TraitSample with Trait2 with Trait3 { ... }

オブジェクトとケースクラス

シングルトン

objectキーワードにより、シングルトンを定義できる。
シングルトンのインスタンスは自動的に生成されるため、コンストラクタに引数を指定できない。

object Format {
  def format(input: String) = ...
}

Format.format("hoge")
コンパニオンオブジェクト

同名のクラスとシングルトンを1つのソースファイルに定義すると、そのシングルトンはコンパニオンオブジェクトになる。
クラスとコンパニオンオブジェクトは互いのprivateメンバにアクセス可能で、主にクラスのファクトリとして使用される。
ファクトリメソッドは、applyメソッド(オブジェクト/インスタンス名(引数)で呼び出せる特殊なメソッド)に定義する。

// クラス
class DBAccess private (user: String, pass: String) { ... }

// シングルトン(コンパニオンオブジェクト)
object DBAccess {
  // ファクトリメソッド
  def apply(user: String, pass: String) = new DBAccess(user, pass)
}

// ファクトリの使用
val db = DBAccess("test", "pass")
抽出子

unapplyメソッドを定義したオブジェクトを抽出子と呼ぶ。
unapplyメソッドには、applyメソッド(ファクトリメソッド)と逆のことを行う処理を記述する。

// クラス
class DBAccess private (user: String, pass: String) { ... }

// コンパニオンオブジェクト
object DBAccess {
  // ファクトリメソッド
  def apply(user: String, pass: String) = new DBAccess(user, pass)
  // 抽出メソッド
  def unapply(dba: DBAccess): Option[(String, String)] = Some(dba.user, dba.pass)
}

// ファクトリの使用
val db = DBAccess("test", "pass")
// 抽出子でインスタンスから情報を抽出
// user, passを変数として扱える
val DBAccess(user, pass) = dba
ケースクラス

ケースクラスとは、いくつかのメソッドやコンパニオンオブジェクトを自動生成するクラスである。
以下のメソッドを自動で生成する。

ケースクラスは、case classキーワードを用いて生成する。
主にパターンマッチで利用するためのクラスや、DTO、アクターへ送信するメッセージの定義に使用される。

case class Message(id: Int, payload: String)
パッケージオブジェクト

パッケージオブジェクトとは、クラス、メソッド、型の別名などを任意のパッケージのメンバとする構文である。
例えば、jp.co.hogeパッケージにLogクラスがあり、これをjp.cp.hoge.newに移動したとすると、Logクラスの参照元すべてに変更が発生してしまう。
この場合、パッケージオブジェクトに別名を定義し、パッケージ内ではその別名を使って参照することで参照元を変更しなくて済む。
パッケージオブジェクトは、package.scalaファイルに定義するのが慣習。

package jp.co.hoge

package object oop {
  type Log = jp.co.hoge.new.Log
}

ジェネリクス

パラメータ化された型

Scalaでは、ジェネリクスパラメータ化された型と呼び、[]の中に型パラメータを記述する。
ワイルドカードは[_]のように記述する(Javaのに相当)。
また、JavaではEやTをつけることが多いが、ScalaではAからアルファベット順につけるのが慣習。

class HelloWorld[A] {
  def hello[A](a: A) = ...
}
変位

Scalaの型パラメータには、変位と呼ばれるものがある。
[A]は通常のJavaジェネリクスと同様、指定した型のみ代入できる(非変)。
[+A]は指定した型とそのサブクラスを代入でき(共変)、[-A]は指定した型とそのスーパークラスを代入できる(反変)。
ただし、変位指定のある型パラメータをクラスで使用する場合には、型の安全性を保障するため制約がある。

// 共変クラス
class Cov[+A] {
  // 戻り値(出力)のみ型として利用できる
  def head: A = ...
}
// 変数にはサブクラスも代入できる
val v: Cov[AnyRef] = new Cov[String]

// 反変クラス
class Con[-A] {
  // 引数(入力)にのみ型として利用できる
  def put(arg: A) = ...
}
// 変数にはスーパークラスも代入できる
val v: Cov[String] = new Cov[AnyRef]

Listのようなイミュータブルな型では、一度インスタンスを生成すると後から変更できないため、戻り値でのみ利用可能な共変が適している。
一方、ListBufferのようにミュータブルな型では設定と取得の両方が必要であるため、非変のみ許容される。

型の操作

型の別名

型に別名をつけたい場合、typeキーワードを使用する。

type M[A, B] = scala.collection.mutable.Map[A, B]
type Two[A] = Tuple2[A, A]
ダックタイピング

構造的部分型を使うことで、ダックタイピングに相当する機能を実現できる。

// {def close()}の部分が構造的部分型で、引数resにはclose()メソッドを含む任意の型を渡せる
// Connectionなどclose()メソッドを持つ型を渡すと、処理後にcloseしてくれる
def using[T <: {def close( )], E](res: T)(f: T => E) = try {
  f(res)
} finally {
  res.close
}

アノテーション

アノテーションは、クラス・型・メソッド・変数・式に付与することができる。

アノテーション 説明 付与する箇所
@deprecated 非推奨APIを表す クラス、メソッド
@clonable クローン作成が可能なことを示す クラス
@SerialVersionUID シリアライズにおけるUIDを定義 クラス
@unchecked caseが足りない場合の警告メッセージを制御 match式
@scala.annotation.tailrec 末尾再帰でない場合コンパイルエラーに メソッド
@throws チェック例外がスローされることを示す(Java側でキャッチ可能) メソッド