はじめに
scalaにはobjectなるシングルトンな値を定義できます。 *1
下のようにThrowableを継承させる定義もできます。
object ObjectThrowable extends Throwable
意気揚々と下のように
sealed abstract class UserError extends Throwable
object NameFormatError extends UserError
object InvalidAgeError extends UserError
なんて定義をしてましたが、objectでThrowableを定義するのはよくないな。という結論になりました。
なぜそんなことをしたの? といいますとobject NameFormatError
にはフィールドがないし、classにする価値なさそう。という発想でした。
これがよくなかったわけです。
何が起きるのか
object ObjectThrowable extends Throwable // 1行目
object ThrowableTest extends App {
def testObjectThrow() = {
try {
throw ObjectThrowable // 5行目
} catch {
case t: Throwable => t.printStackTrace()
}
try {
throw ObjectThrowable / /11行目
} catch {
case t: Throwable => t.printStackTrace()
}
}
testObjectThrow()
}
これを実行すると以下のようにコンソールに出てきます。
ObjectThrowable$
at ObjectThrowable$.<clinit>(ThrowableTest.scala:1)
at ThrowableTest$.testObjectThrow(ThrowableTest.scala:5)
at ThrowableTest$.delayedEndpoint$ThrowableTest$1(ThrowableTest.scala:17)
at ThrowableTest$delayedInit$body.apply(ThrowableTest.scala:2)
~~~略~~~
ObjectThrowable$
at ObjectThrowable$.<clinit>(ThrowableTest.scala:1)
at ThrowableTest$.testObjectThrow(ThrowableTest.scala:5)
at ThrowableTest$.delayedEndpoint$ThrowableTest$1(ThrowableTest.scala:17)
at ThrowableTest$delayedInit$body.apply(ThrowableTest.scala:2)
~~~略~~~
二度objectをthrowしてprintStackTrace
を実行しているので、スタックトレースが二度出ていていますがスタックトレースの内容が妙ですね。
二度目のprintStackTrace
を呼び出されているオブジェクトは11行目でthrowされているので、本来下記のようなスタックトレースが出て欲しいですよね。
ObjectThrowable$
at ObjectThrowable$.<clinit>(ThrowableTest.scala:1)
at ThrowableTest$.testObjectThrow(ThrowableTest.scala:11)
下記のようなコードで試してみるとわかりやすく、
object ObjectThrowable extends Throwable
object ThrowableTest extends App {
def testObjectThrow() = {
ObjectThrowable // 4行目
try {
throw ObjectThrowable
} catch {
case t: Throwable => t.printStackTrace()
}
}
testObjectThrow()
}
4行目でobjectが最初に評価された場所がスタックトレースに記録されます。
ObjectThrowable$
at ObjectThrowable$.<clinit>(ThrowableTest.scala:1)
at ThrowableTest$.testObjectThrow(ThrowableTest.scala:4)
at ThrowableTest$.delayedEndpoint$ThrowableTest$1(ThrowableTest.scala:19)
at ThrowableTest$delayedInit$body.apply(ThrowableTest.scala:2)
これはあたり前の挙動で、Throwableは生成時にスタックトレースを埋めるからです。
class Throwable {
// from https://github.com/AdoptOpenJDK/openjdk-jdk/blob/master/src/java.base/share/classes/java/lang/Throwable.java#L255-L257
public Throwable() {
fillInStackTrace();
}
public synchronized Throwable fillInStackTrace() {
// from https://github.com/AdoptOpenJDK/openjdk-jdk/blob/master/src/java.base/share/classes/java/lang/Throwable.java#L795-L802
if (stackTrace != null ||
backtrace != null /* Out of protocol state */ ) {
fillInStackTrace(0);
stackTrace = UNASSIGNED_STACK;
}
return this;
}
}
objectが評価されるタイミングで、親クラスにあたるThrowableのコンストラクタも呼ばれてスタックトレースが埋められます。
ということで
objectでThrowableを継承するのは混乱を生むかもしれませんね。
意図的にスタックトレースをobjectを最初に評価した場所に固定したい場合以外は、
そもそも、Throwableは生成するタイミングのスタックトレースを自身の状態として保持する。
という事情を考えるとobjectではなくclassで定義したほうが良い。という考え方をした方がいいですね。
*1 厳密にはシングルトンではないようです