当記事の目的
JavaエンジニアがScalaの学習過程で得た学びをJavaとの違いに注目していきながら、クラス、ケースクラス、オブジェクト、およびコンパニオンオブジェクトについて解説する。
どのように記述するかというよりも、どのようなケースでこれらを使うことにより効率的にコードが書けるのかといった観点に焦点を当てていく。
Scalaオブジェクト指向プログラミング
Class
どのオブジェクト指向プログラミング言語なら、どの言語にも実装されているであろうClassにおけるJavaとScalaの実装の違いについて解説する。
コンストラクタのオーバーロードについて
まずJavaとの大きな違いはフィールドおよびコンストラクタの定義の仕方にある。
Javaであれば下記のように、自由にコンストラクタ内の処理が実装できるため、明示的にコンストラクタのオーバーロードが可能である
@Getter
class Hoge {
private String fieldA;
private String fieldB;
public Hoge() {
this.fieldA = null;
this.fieldB = null;
}
public Hoge(String fieldA) {
this.fieldA = fieldA;
this.fieldB = null;
}
public Hoge(String fieldA, String fieldB) {
this.fieldA = fieldA;
this.fieldB = fieldB;
}
}
Sacalの場合、valもしくはvarで宣言されているフィールドはコンストラクタによる値の代入は自動生成される。そうなるとオーバーロードをどうするのか困るかもしれないが、二つ方法がある。
一つは補助コンストラクタを使用する方法である。
def this()
の部分に定義することで実装可能である。
class Hoge(val fieldA: String, val fieldB: String) {
def this() = {
this(null, null)
}
def this(fieldA: String) = {
this(fieldA, null);
}
}
もう一つはデフォルト値を定義しておく方法である。デフォルト値があればコンストラクタから省略できるため、下記のようになる
class Hoge(val fieldA: String = null, val fieldB: String = null)
コンストラクタによる事前条件・事後条件の検証について
完全コンストラクタパターンのように事前条件と事後条件を定義したい場合、Javaであれば下記のようにただコンストラクタ内に定義すれば良い。
public class Hoge {
private String fieldA;
private String fieldB;
public Hoge(String fieldA, String fieldB) {
// 事前条件の検証
assert fieldA != null && !fieldA.isEmpty();
assert fieldB != null && !fieldB.isEmpty();
this.fieldA = fieldA;
this.fieldB = fieldB;
// 事後条件の検証
if ((fieldA + fieldB).length() >= 30) {
throw new IllegalArgumentException("The combined length of fieldA and fieldB must be 30 or more")
}
}
}
Scalaの場合Assertionをクラスブロック内に定義することで事前・事後条件の検証ができる。よって下記のようになる
class Hoge(val fieldA: String, val fieldB: String) {
// 事前条件の検証
require(fieldA != null && fieldA.nonEmpty, "fieldA must be non-null and non-empty")
require(fieldB != null && fieldB.nonEmpty, "fieldB must be non-null and non-empty")
// 事後条件の検証
(fieldA + fieldB).ensuring(_.length >= 30, "The combined length of fieldA and fieldB must be 30 or more")
}
ScalaにはさまざまなAssertionがあるので、確認して表現力が高まるだろう
コンストラクタにおけるロジックの実行
Javaの場合はコンストラクタに自由にロジックが記述できるが(良いか悪いかは置いておいて)、Scalaにおいては若干書き方が異なる。
例えばJavaで以下のように引数をフィールドに代入した後、標準出力するコードがあるとする。
public class Hoge {
private String fieldA;
private String fieldB;
private String fieldC;
public Hoge(String fieldA, String fieldB) {
this.fieldA = fieldA;
this.fieldB = fieldB;
this.fieldC = fieldA + fieldB;
System.out.println("Hello World");
}
}
一方Scalaではクラスブロック内にある処理は、コンストラクタが呼ばれた際に実行される。クラスのインスタンスが生成されるときに、クラスブロック内のコードがトップダウンで順に実行されるので下記のようにできる。
class Fuga2(val fieldA: String, val fieldB: String) {
val fieldC = fieldA + fieldB;
println("hello world")
}
Case Class
CaseClassとはJavaのRecordに近いデータ型であり、equalsや、hashcode,
toString,
apply(newを不要にしてオブジェクトを生成できる)などの便利なメソッドを自動生成してくれる。JavaのRecord型との一番の違いはImmutableではないvar型のデータをフィールドとして持つことができる点である。デフォルトではvalになり、何も宣言しないとvalのフィールドとして定義される。
case class Person(name: String, isAdult: boolan)
自動生成されるメソッドの詳細はこちらの記事が役立つかもしれない
Trait
Javaなど多くのオブジェクト指向言語で採用されているinterfaceと思って良い。オブジェクトにミックスイン(多重継承することができる)でき、Java8以降のInterfaceと同様にデフォルトの実装を定義することもできる。違いはプライベートな、初期ブロックを持てることと、フィールドを持てることである(ただしコンストラクタを持てないので、初期化する際には継承先のクラスのフィールドとして定義する必要がある)。
trait Person {
val firstName: String
val lastName: String
// メソッドの宣言
def sleep(): Unit
// デフォルト実装
def sayName(): Unit = {
println(firstName + " " + lastName)
}
}
class Employee(val id: UUID, val firstName: String, val lastName: String) extends Person {
override def sleep(): Unit = {
println("zzzzzzzzzzzz")
}
}
var e = new Employee(UUID.randomUUID(), "john", "doe")
e.sayName() // john doe
Abstract Class
Javaでは抽象クラスとインターフェースに大きな違いがあるが、Scalaではさほどないと考えて良い。主な違いは下記の二点である。
- 継承
- Trait
- 多重継承可能
- Abstract Class
- 多重継承不可
- Trait
- コンストラクタ引数
- Trait
- もてない
- Abstract Class
- もてる
- Trait
実装を比べた限り機能にそこまで大差なく、traitの方が記述は簡単なのでとりあえずtraitで実装し、ダメそうならAbstract Classを使えば良さそう
abstract class Person(val firstName: String, val lastName: String) {
// メソッドの宣言
def sleep(): Unit
// デフォルト実装
def sayName(): Unit = {
println(firstName + " " + lastName)
}
}
class Employee(val id: UUID, firstName: String, lastName: String) extends Person(firstName, lastName) {
override def sleep(): Unit = {
println("zzzzzzzzzzzz")
}
}
var e = new Employee(UUID.randomUUID(), "john", "doe")
e.sayName()
Companion Object
scalaにはstaticという概念がない。そのためオブジェクトではなくクラスに保持させるフィールドもしくはメソッドを定義したい場合はCompanion Objectを使うことで同様の機能を実装することができる。
またobject側の値をclass側から参照したい場合は、importを用いることで可能になる。
class Circle(val radius: Double) {
import Circle._
def area: Double = Pi * radius * radius
}
object Circle {
private val Pi = 3.14159
def apply(radius: Double): Circle = new Circle(radius)
}
val circle = Circle(5.0)
println(s"Area of the circle with radius 5.0: ${circle.area}")
Object(Singleton Object)
ScalaではObjectはシングルトンオブジェクトを意味し、コンストラクタを持たない。
基本的にはステートレスな実装を心がけるといいだろう
object Greeting {
val StartHourOfMorning = 6
val StartHourOfNoon = 12
val StartHourOfNight = 18
def exec(now: LocalDateTime): Unit = {
if (StartHourOfMorning <= now.getHour) {
println("Good Morning")
}
else if (StartHourOfNoon <= now.getHour && now.getHour < StartHourOfNight) {
println("Good After Noon")
}
else {
println("Good Night")
}
}
}
## まとめ(感想)
ScalaにはJavaとは違い独自の概念が多いほか、Classのメソッドの自動生成など言語使用によるマジックがJavaよりも多い印象を受けた。そのため適切なコードを書くためには言語使用について深く理解することが必須となると感じた