概要
表題のとおり、companion object に定義した protected メソッドをコールすると、下記の実行時例外が発生しました。
java.lang.IllegalAccessError: tried to access method com.sample.Parent$Companion.parentStaticProtected()Ljava/lang/String; from class com.Child$Companion
at com.Child$Companion.parentFromChild(Child.kt:10)
at another.ChildTest.call_test(ChildTest.kt:10)
詳細
ソースコードの構成は下記のようになっています。
.
└── com
├── Child.kt
└── sample
├── Parent.kt
Child クラスから、Parent クラスをコールします。
package com
import com.sample.Parent
class Child: Parent() {
companion object {
fun parentFromChild(): String {
return parentStaticProtected()
}
}
}
package com.sample
abstract class Parent {
companion object {
@JvmStatic
protected fun parentStaticProtected(): String {
return "IllegalAccessError!!"
}
}
}
Parent の parentStaticProtected についている @JvmStatic アノテーション
は、companion object の protected メソッドには、このアノテーションをつけるようコンパイラがエラーを出すので、コンパイルエラーを解決するためにつけています。
原因
Kotlin には、パッケージプライベートの概念はないのですが、どうやら companion object の protected メソッドは、同一パッケージ内でからのみコールできるようです。
試しに、Child クラスのパッケージを com から、com.sample に変更したところ、正常終了しました。
package com.sample
OptIn アノテーションで回避
パッケージの移動で回避できるものの、2 つの課題を感じました。
- 同一パッケージ外に companion object に protecte メソッドをもったサブクラスをおきたい時があるだろう
- 例外の意味がよくわからないので実行時例外前にコンパイルエラーで気付きたい
この挙動を変更してくれればよいのですが、いつになるかがよくわからないので、対処策として、Opt-In Requirements の機能を使って回避してみました。
この方法ですと、後述の @OptIn
のアノテーションを、利用したくなったら都度メソッドにつける方法でコールできてしまいます。不適切なスコープからコールされる可能性はあるので、せめてそうならないよう、それらしい名前のアノテーションで定義します。
package com.annotation
/**
* このアノテーションは、実質 @RequiresOptIn アノテーションですが、
* 役割を明確にするため別途アノテーション定義しています。
*/
@RequiresOptIn
annotation class SubClassCanUse
公式のページによると、@RequiresOptIn
アノテーション をつけたメソッドをコールするために、特殊な制限をかけることができます。
companion object {
@SubClassCanUse
fun parentStaticProtected(): String {
return "now we can call this... because scope is public."
}
}
このメソッドをコールしようとすると、
This declaration is experimental and its usage must be marked with '@com.annotation.SubClassCanUse' or '@OptIn(com.annotation.SubClassCanUse::class)'
とコンパイルエラーが発生します。コンパイルエラーを解決するためには、メッセージのとおり、呼び出し元に、
@SubClassCanUse
@OptIn
のどちらかのアノテーションを書く必要あります。@SubClassCanUse アノテーション
でコンパイルエラーを解決する場合、parentStaticProtected をコールしているメソッドだけでなく、間接的にコールするメソッドを含めて、parentStaticProtected メソッドに到達するまでに利用されるすべてのメソッドに @SubClassCanUse アノテーション
を設定しないといけません。
この方法だと、非常に多くの場所にアノテーションを記述するハメになり、ノイズになってしまうので、@OptIn アノテーション
を使用してコンパイルエラーを解決します。このアノテーションでコンパイルエラーを解決する場合は、直接 @SubClassCanUse アノテーション
がついたメソッドをコールしているメソッドにだけ記述すれば良いです。
package com
import com.annotation.SubClassCanUse
import com.sample.Parent
class Child: Parent() {
companion object {
@OptIn(SubClassCanUse::class)
fun parentFromChild(): String {
return parentStaticProtected()
}
}
}