Androidの単体テストにあたり、privateなクラスやメソッドにアクセスするために使用するリフレクションについてまとめました。
【注意点】
この記事では、筆者がリフレクションを一緒くたに認識していた背景から、KotlinでJavaリフレクションを使用しています。
しかし実際はKotlinにはKotlinリフレクションが存在するため、あえてKotlinでJavaリフレクションを使うのであれば、その理由を明確にできることが望ましいはずです。
次回は、Kotlinリフレクションをまとめます。
##リフレクションとは
リフレクション(reflection)とは、反射、内省、熟考という意味の英単語です。
JavaやKotlinでは、クラスやコンストラクタ、メソッドなどの情報の取得、必要に応じて書き替えるための仕組みを指します。
Java:Trail: The Reflection API (The Java™ Tutorials)
リフレクションを使うことで、通常はアクセスできない可視性修飾子のついた要素にアクセスができます。
そのため通常ソースコード内での使用は推奨されませんが、単体テストなどではプロパティのset/getの確認や、privateクラスのテストに使用されます。
Kotlinの可視性修飾子について:可視性修飾子 - Kotlin Programming Language
今回はKotlinで書かれた以下のコードについて、Javaリフレクションを使用して要素を取得し、テストを作成します。
尚、今回使用する以下のコードはリフレクションの事例をまとめるために使用するもので、不完全な部分を含みますのでご了承ください。
//Curryクラス (publicコンストラクタを持つクラス)
class Curry(potato: Int) {
val publicP = "publicPotato: ${potato}kg"
private val privateP = "privatePotato: ${potato}kg"
fun publicM(carrot: Int): String{
return "publicCarrot: ${carrot}kg"
}
private fun privateM(carrot: Int): String{
return "privateCarrot: ${carrot}kg"
}
private class MakeCurry(onion: String, val meat: Int){
val nestPublicP = "nestPublic-$onion"
private val nestPrivateP = "nestPrivate-$onion"
fun nestPublicM(water: Int): String {
return "nestPublicMeat: ${meat+water}kg"
}
private fun nestPrivateM(water: Int): String {
return "nestPrivateMeat: ${meat+water}kg"
}
}
}
//Stewクラス (privateコンストラクタを持つクラス)
class Stew private constructor(val potato: Int, val carrot: Int) {
val publicP = "publicPotato: ${potato}kg"
private fun privateM(onion: Int): String {
return "${publicP}, privateNetWeight: ${potato+carrot+onion}kg"
}
}
##Javaのリフレクションを使用する
###publicクラスの要素
####publicプロパティの取得
publicプロパティを取得するにあたり、以下の3つのJavaリフレクションのメソッドを使用します。
-
getDeclaredField
Javaにおけるpublic以外のFieldを取得するためのメソッドです。
Kotlinではプロパティを取得するために利用できます。
インスタンスからJavaクラスを取得し、取得したいプロパティ名を引数に入れて使用します。
インスタンス::class.java.getDeclaredField("プロパティ名")
※ JavaのFieldからKotlinのプロパティを取得しようとする場合、プロパティがpublic設定でもprivate認識となるようです。
そのため、public Fieldを取得するためのgetField
は使用できず、getDeclaredField
を使用する必要があります。
-
isAccessible
取得した要素へのアクセスを許可するためのメソッドです。
プロパティを取得する場合、getDeclaredFieldの実行のみでは取得したプロパティへのアクセスが許可されません。
プロパティの値を設定、取得するためには、isAccessibleをtrueに設定する必要があります。
取得したフィールド.isAccessible = true
-
set
、get
プロパティに値を設定、取得するためのメソッドです。
通常、プロパティの値の設定はインスタンス.プロパティ名 = 値
、取得はインスタンス.プロパティ名
のように行いますが、リフレクションを使用する場合は以下のように行います。
set:フィールド.set(インスタンス, セットしたい値)
get:フィールド.get(インスタンス)
@Test
fun getPublicP() {
//①当該クラスのインスタンスを生成
val instance = Curry(2)
//②フィールド(プロパティ)を取得
val field = instance::class.java.getDeclaredField("publicP")
//③アクセスを許可
field.isAccessible = true
//④プロパティの値を取得し、検証
assertEquals("publicPotato: 2kg", field.get(instance))
}
####publicメソッドの取得
publicメソッドの取得では、getMethod
を使用します。
-
getMethod
publicメソッドを取得するためのメソッドです。
インスタンスからJavaクラスを取得し、第一引数に取得したいメソッド名、第二引数以降に取得したいメソッドが規定する引数のクラスを入れることで使用できます。
インスタンス::class.java.getMethod("プロパティ名", 引数1のクラス, 引数2のクラス, …)
また、以下の例では取得したメソッドを実行して検証しています。
通常、メソッドの実行はインスタンス.メソッド名(引数1, 引数2, …)
のように行いますが、リフレクションの場合は以下のように行います。
取得したメソッド(インスタンス, 引数1, 引数2, …)
もしくは取得したメソッド.invoke(インスタンス, 引数1, 引数2, …)
@Test
fun publicMTest() {
//①当該クラスのインスタンスを生成
val instance = Curry(4)
//②メソッドを取得
val method = instance::class.java.getMethod("publicM", Int::class.java)
//③メソッドを実行し、検証
assertEquals("publicCarrot: 20kg", method(instance, 20))
}
####privateプロパティの取得
privateプロパティを取得するにあたり、以下のメソッドを使用します。
- getDeclaredField、isAccessible:publicクラスのpublicプロパティの取得方法を参照
@Test
fun getPrivateP() {
//①当該クラスのインスタンスを取得
val instance = Curry(4)
//②フィールド(プロパティ)を取得
val field = instance::class.java.getDeclaredField("privateP")
//③アクセスを許可
field.isAccessible = true
//④プロパティの値を取得し、検証
assertEquals("privatePotato: 4kg", field.get(instance))
}
####privateメソッドの取得
privateメソッドを取得するにあたり、以下の2つのメソッドを使用します。
-
getDeclaredMethod
public以外のメソッドを取得するためのメソッドです。
インスタンスからJavaクラスを取得し、第一引数に取得したいメソッド名、第二引数以降に取得したいメソッドが規定する引数のクラスを入れて使用します。
インスタンス::class.java.getDeclaredMethod("プロパティ名", 引数1のクラス, 引数2のクラス, …)
-
isAccessible
プロパティ取得の際にも登場した、取得した要素へのアクセスを許可するためのメソッドです。
privateメソッド取得の際も、取得したメソッドにアクセスするためにisAccessibleをtrueに設定する必要があります。
取得したフィールド.isAccessible = true
@Test
fun privateMTest() {
//①当該クラスのインスタンスを取得
val instance = Curry(5)
//②メソッドを取得
val method = instance::class.java.getDeclaredMethod("privateM", Int::class.java)
//③アクセスを許可
method.isAccessible = true
//④メソッドを実行し、検証
assertEquals("privateCarrot: 5kg", method(instance, 5))
}
###privateネストクラスの要素
今回はpublicクラスの中にprivateクラスを入れる複雑な構造ですが、同様の方法でトップレベルに宣言したprivateクラスも取得できます。
####publicプロパティの取得
privateクラスの要素取得するにあたり、新たに使用するメソッドは次の3つです。
-
forName
引数に指定したクラスを取得するメソッドです。
引数にはJavaディレクトリ以下の完全指定の名称を設定します。
Class.forName("Javaディレクトリ以下のクラスパス")
-
getConstructor
publicなコンストラクタを取得するためのメソッドです。
引数にはコンストラクタ引数のクラスを設定します。
クラス.getConstructor(コンストラクタ引数1のクラス, コンストラクタ引数2のクラス, … )
-
newInstance
当該クラスのインスタンスを取得するためのメソッドです。
引数には通常のインスタンス生成時と同じように、コンストラクタ引数に渡す値を設定します。
コンストラクタ.newInstance(引数1, 引数2, … )
そのほか、今回使用するメソッドについては以下をご覧ください。
- getDeclaredField、isAccessible、get:publicクラスのpublicプロパティの取得方法を参照
@Test
fun getNestPublicP() {
//①クラスを取得 今回はネストクラスのため\$MakeCurryがつく
val nestClass = Class.forName("com.example.temp.Curry\$MakeCurry")
//②コンストラクタを取得 privateクラスだが、コンストラクタ自体はpublicのためgetConstructorを使用
val constructor = nestClass.getConstructor(String::class.java, Int::class.java)
//③インスタンスを取得
val instance = constructor.newInstance("onion", 2)
//④フィールド(プロパティ)を取得
val field = instance::class.java.getDeclaredField("nestPublicP")
//⑤アクセスを許可
field.isAccessible = true
//⑥プロパティの値を取得し、検証
assertEquals("nestPublic-onion", field.get(instance))
}
####publicメソッドの取得
publicメソッドを取得するにあたり、以下のメソッドを使用します。
- forName、getConstructor、newInstance:privateクラスのpublicプロパティ取得を参照
- getMethod、メソッドの実行:publicクラスのpublicメソッド取得を参照
@Test
fun nestPublicMTest() {
//①クラスを取得 今回はネストクラスのため\$MakeCurryがつく
val nestClass = Class.forName("com.example.temp.Curry\$MakeCurry")
//②コンストラクタを取得
val constructor = nestClass.getConstructor(String::class.java, Int::class.java)
//③インスタンスを取得
val instance = constructor.newInstance("onion", 5)
//④メソッドを取得
val method = instance::class.java.getMethod("nestPublicM", Int::class.java)
//⑤メソッドを実行し、検証
assertEquals("nestPublicMeat: 25kg", method(instance, 20))
}
####privateプロパティの取得
privateプロパティを取得するにあたり、以下のメソッドを使用します。
- forName、getConstructor、newInstance:privateクラスのpublicプロパティ取得を参照
- getDeclaredField、isAccessible、set/get:publicクラスのpublicプロパティ取得を参照
@Test
fun getNestPrivateP() {
//①クラスを取得 今回はネストクラスのため\$MakeCurryがつく
val nestClass = Class.forName("com.example.temp.Curry\$MakeCurry")
//②コンストラクタを取得
val constructor = nestClass.getConstructor(String::class.java, Int::class.java)
//③インスタンスを取得
val instance = constructor.newInstance("onion", 5)
//④フィールド(プロパティ)を取得
val field = instance::class.java.getDeclaredField("nestPrivateP")
//⑤プロパティのアクセスを許可
field.isAccessible = true
//⑥プロパティに値を設定
field.set(instance, "カレーが食べたい")
//⑦プロパティの値を取得し、検証
assertEquals("カレーが食べたい", field.get(instance))
}
####privateメソッドの取得
privateメソッドを取得するにあたり、以下のメソッドを使用します。
- forName、getConstructor、newInstance:privateクラスのpublicプロパティ取得を参照
- getDeclaredMethod、isAccessible:publicクラスのprivateメソッド取得を参照
- 取得したメソッドの実行:publicクラスのpublicメソッド取得を参照
@Test
fun nestPrivateMTest() {
//①当該クラスを取得 今回はネストクラスのため\$MakeCurryがつく
val nestClass = Class.forName("com.example.temp.Curry\$MakeCurry")
//②コンストラクタを取得
val constructor = nestClass.getConstructor(String::class.java, Int::class.java)
//③インスタンスを生成
val instance = constructor.newInstance("onion", 5)
//④メソッドを取得
val method = instance::class.java.getDeclaredMethod("nestPrivateM", Int::class.java)
//⑤メソッドへのアクセスを許可
method.isAccessible = true
//⑥メソッドを実行し、検証
assertEquals("nestPrivateMeat: 25kg", method(instance, 20))
}
###privateコンストラクタを持つpublicクラスの要素
####publicプロパティ
privateコンストラクタを持つクラスのプロパティを取得するにあたり、新しくgetDeclaredConstructor
を使用します。
-
getDeclaredConstructor
public以外のコンストラクタを取得するためのメソッドです。
publicなコンストラクタを取得する際に使用したgetConstructorと同じように、コンストラクタ引数のクラスを引数に設定して使用します。
クラス.getDeclaredConstructor(コンストラクタ引数1のクラス, コンストラクタ引数2のクラス)
そのほか、使用するメソッドの詳細については以下をご覧ください。
- forName、newInstance:privateクラスのpublicプロパティ取得を参照
- getDeclaredField、isAccessible、get:publicクラスのpublicプロパティ取得を参照
@Test
fun getPublicP() {
//①当該クラスを取得
val c = Class.forName("com.example.temp.Stew")
//②コンストラクタを取得
val constructor = c.getDeclaredConstructor(Int::class.java, Int::class.java)
//③コンストラクタへのアクセスを許可
constructor.isAccessible = true
//④インスタンスを生成
val instance = constructor.newInstance(3, 6)
//⑤フィールド(プロパティ)を取得
val field = instance::class.java.getDeclaredField("publicP")
//⑥プロパティへのアクセスを許可
field.isAccessible = true
//⑦プロパティを取得し、検証
assertEquals("publicPotato: 3kg", field.get(instance))
}
####privateメソッド
privateメソッドを取得するにあたり、以下のメソッドを使用します。
- forName、newInstance:privateクラスのpublicプロパティ取得を参照
- getDeclaredField、isAccessible、get:publicクラスのpublicプロパティ取得を参照
@Test
fun privateM() {
//①当該クラスを取得
val c = Class.forName("com.example.temp.Stew")
//②コンストラクタを取得
val constructor = c.getDeclaredConstructor(Int::class.java, Int::class.java)
//③コンストラクタへのアクセスを許可
constructor.isAccessible = true
//④インスタンスを生成
val instance = constructor.newInstance(10, 5)
//⑤メソッドを取得
val method = instance::class.java.getDeclaredMethod("privateM", Int::class.java)
//⑥メソッドのへのアクセスを許可
method.isAccessible = true
//⑦メソッドを実行し、検証
assertEquals("publicPotato: 10kg, privateNetWeight: 23kg", method(instance, 8))
}
##まとめ
- プロパティの取得は
getDeclaredField
- publicメソッドの取得は
getMethod
- publicコンストラクタの取得は
getConstructor
- それぞれ
Declared
のついたメソッドでpublic以外の要素を取得可能 - Declaredのついたメソッドは
.isAccessible = true
とセットで使用する
##参考
・Qiita: Junitでprivateメソッドのテスト方法
・Qiita: 入門Javaのリフレクション
・Qiita: 何かの時にスッと使える力技 - Reflection 編
・HatenaBlog: privateなインナークラスをリフレクションで触る
・stackoverflow: 別クラスからプライベートフィールドの値を読み込む方法
・SAMURAI ENGINEER Blog: リフレクションでメソッドの実行、フィールドの変更
・HatenaBlog: JavaのリフレクションAPIでコンストラクタ取得、インスタンス生成
・HatenaBlog: JavaのリフレクションAPIでフィールド取得