LoginSignup
2
0

More than 3 years have passed since last update.

ScalaでThrowableを継承したobjectってどうなっちゃうんだろう

Last updated at Posted at 2021-03-23

はじめに

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 厳密にはシングルトンではないようです

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0