細かい粒度でコードの再利用を可能とするメソッド内メソッドのJava言語への導入 理学部 情報科学科 07-22331 平松 俊樹 指導教員 千葉 滋 教授
巨大メソッドの一部の再利用 一部だけ上書きしたい メソッドに切り出す ローカル変数を参照して いたら? 変更 class Parser { private Object parse(TokenStr in) { while (..) { Symbol token = .. ; short act = .. ; if (..) { : } else { report.syntaxError(token); recoverFromError(token, in); 一部だけ上書きしたい メソッドに切り出す ローカル変数を参照して いたら? 変更 プログラミングの際に、巨大なメソッドの一部だけを上書きして変更し、 残りの部分を再利用したい場合があります。 例えば巨大なループの一部を変更したい場合などです。 このようなときに、その変更したい部分を別のメソッドとして切り出し、 その切り出されたメソッドを、サブクラスにおいてオーバーライドするという方法が考えられます。 しかしこの方法では、その変更したい部分がローカル変数を参照していたときに問題が起きます。 この例では・・・ 1’
メソッドに切り出すことは困難 大量の引数 変数への代入は? 切り出されたメソッド class Parser { private Object parse(TokenStr in) { while (..) { Symbol token = .. ; short act = .. ; if (..) {..} else {elseM(token, in);} } void elseM(Symbol token,TokenStr in){ report.syntaxError(token); recoverFromError(token, in); 大量の引数 変数への代入は? 変更したい部分を別のメソッドとして切り分けた場合には、 その切り分けられた部分から元のメソッドのローカル変数にアクセスすることは不可能です。 このため、メソッドに切り分ける際には、必要なローカル変数を全て引数として渡す等の 作業が必要になり、場合によっては大量の引数を扱わなければならなくなります。 引数に渡すことで、切り分けられたメソッドで 元のメソッドのローカル変数を使うことが可能となりますが、 切り分けられたメソッドから元のメソッドのローカル変数への代入を行うことができません。 2’ 切り出されたメソッド
クロージャを用いた場合 ローカル変数にアクセスできる アクセスできない 上書きするとアクセスできない class Parser{ Closure elseM; private Object parse(TokenStr in) { while (..) { Symbol token = .. ; short act = .. ; if (..) {..} else { elseM = {report.syntaxError(token); recoverFromError(token, in); } elseM(); }}}}} class SubParser extends Parser{ private Object parse(TokenStr in) { elseM = { report.syntaxError(token); act = 0; } super.parse(in); アクセスできない 上書き 先ほどの、ローカル変数にアクセスできないという問題は、クロージャを用いることによって解決できます。 これを例を使って説明します。 なお、現在のJavaにはクロージャが無いため、クロージャの記述は仮のものです。 左のコードがスーパークラスです。 フィールドとしてクロージャを保持し、これをメソッド内で呼び出しています。 このとき、定義されたクロージャはメソッドのローカル変数にアクセスすることができます。 これに対して、右のコードがサブクラスであり、 メソッド内で別のクロージャを定義し、フィールドへ代入してクロージャを上書きしたうえで、 元のメソッドを呼び出しています。 このようにすると、メソッドの一部を変更することができますが、 サブクラスで定義したクロージャからは元のメソッドのローカル変数にアクセスすることができず、 この例では右のコードのクロージャからスーパークラスのメソッド内の token, actを参照できず、実行が行えません。 3’
提案:上書き可能なメソッド内メソッド サブクラスでオーバーライド可能 class Parser { private Object parse(TokenStr in) { while (..) { Symbol token = .. ; short act = .. ; if (..) {..} else { void elseM() { report.syntaxError(token); recoverFromError(token, in); } elseM(); }}}} class Session { public void buy(Item item) { int count = 0; public int numItem = 0; public int totalAmount = 0; boolean inService = false; : void service() { if (inService) { numItem++; count++; } service(); class SubParser extends Parser{ parse(TokenStr).elseM() { report.syntaxError(token); act = 0; } class Discount extends Session{ int limit; public void buy(Item).void service(){ if (numItem > limit) { totalAmount *= 0.8; } elseMのみを上書き これらの問題を解決するために本研究が提案するのは 上書き可能なメソッド内メソッドです。 本システムの概要を述べさせていただきます。 まず、メソッドの内部にメソッドを定義することが可能です。 左のコードがメソッド内にメソッドを定義した例です。 赤字の部分がメソッド内メソッドの定義です。 この例では通常のメソッドparseの内部にメソッド内メソッドelseMを定義しています。 このメソッド内メソッドからは、 自身が定義されたメソッドのローカル変数と引数が参照可能です。 変数の値を得るだけでなく、変数への代入も可能です。 これによって、メソッドの一部を別のメソッドとして切り分けた際の、 引数の受け渡しや、ローカル変数に代入ができないという問題を解決できます。 この例では、メソッド内メソッドelseMの内部から、 メソッドparseのローカル変数token, 引数inを参照しています。 メソッドの定義だけでは実行はされないため、改めてメソッド内メソッドを呼び出す必要があります。 また、メソッド内メソッドはサブクラスでオーバーライドすることができます。 この例では右のサブクラスで、メソッド内メソッドelseMをオーバーライドしています。 オーバーライドするメソッド内メソッドからも、もとのメソッドのローカル変数を参照できます。 これによって、クロージャを用いた際の、 クロージャを上書きするとローカル変数にアクセスできなくなるという問題を解決できます。 4’30’’
ローカル変数へのアクセス public宣言されたローカル変数、引数 サブクラスのメソッド内メソッドから参照可能 カプセル化を破壊しない class Parser { Object parse(public TokenStr in) { public Symbol token; short act; void elseM() { : } parse in token act アクセス可 メソッド内メソッドを定義すると、そのクラスにおいては、メソッド内メソッドからは、 元のメソッドの全てのローカル変数にアクセス可能です。 しかし、サブクラスでオーバーライドしたメソッド内メソッドからも 全てのローカル変数にアクセスできてしまうと、 これはカプセル化に反すことになります。 このため、本研究ではメソッド内メソッドをオーバーライドした場合には、 サブクラスからアクセスできるローカル変数、引数は public宣言されたもののみという制限を設けています。 この例では・・・ 5’30’’ class SubParser extends Parser { Object parse(TokenStr).elseM() { : } elseM アクセス不可
実装方法 JastAddを用いて実装 メソッド内メソッドで参照される変数を集めた オブジェクトを作る 今回採用した方法 それを引数で渡す クロージャの実装方法と類似 今回採用した方法 メソッドに対応するクラスを作成 コード変換が簡単 thisの扱いが複雑 コード量 約8000行を読み、1100行を記述 本システムの実装方法について説明します。 本システムはJastAddを用いて実装を行いました。 一般的には、メソッド内メソッドで参照される変数を集めたオブジェクトを作り、 それを引数で渡すという クロージャの実装方法と類似した手法をとりますが、 コード変換を簡便にするため、今回は メソッドに対応するクラスを作成し、 メソッドのローカル変数を対応するクラスのフィールドに変換するという方法をとりました。 この方法を用いると、新たにクラスが作成されるため、 メソッド内のThisが指すものを正しいオブジェクトに戻すための変換が必要になります。 6’30’’
コード変更の例 class C { int outerM(int arg) { return new C$outerM(this).run$$(arg); } void innerM(C$outerM $outer) { $outer.new C$innerM($outer).run$$(); class C$outerM { public C $this; private int arg; protected int localVar; : public int run$$(int arg$arg) { arg = arg$arg; localVar = 0; $this.innerM(this); class C$innerM { public C$outerM $outer; public void run$$() { localVar = arg; }}}} コード変更の例 class C { int outerM(int arg) { public int localVar = 0; void innerM() { localVar = arg; } InnerM(); : オーバーライド class Child extends C { void innerM(C$outerM $outer) { $outer.new C$innerM($outer) { public void run$$() { $outer.localVar = 0; } }.run$$(); class Child extends C { int outerM(int).void innerM(){ localVar = 0; } 7’30’’
実験:マイクロベンチマーク 実行時間の比較 代入するローカル変数の個数を変えて実験 メソッド内メソッド 手動で切り分けたメソッド 実験環境 OS: Windows 7 CPU: Intel Core i5 2.67GHz メモリ: 4.00GB JVM 1.6.0_20
実験1: 代入するローカル変数の個数=1 結果 メソッド内メソッドを呼ぶたび にオブジェクト作成 手動の場合はオブジェクトを 作らない public void method1() { int result = 0; public int method2() { int r1 = i + j; int r2 = i - j; int r3 = i * j; int r4 = i / j; return r1 + r2 + r3 + r4; } for (int i = 1; i <= 10000; i++) { for (int j = 1; j <= 10000; j++) { result = method2(); 結果 メソッド内メソッドを呼ぶたび にオブジェクト作成 手動の場合はオブジェクトを 作らない プログラム 実行時間(ms) 比 メソッド内 メソッド 1694 4.8 手動で分けたメソッド 352 1 プログラム 実行時間(ms) メソッド内メソッド 1694 手動で分けたメソッド 352 1694 352 8’
実験2: 代入するローカル変数の個数=4 結果 手動で書いたコードの変 数の渡し方が悪い プログラム 実行時間(ms) 比 メソッド内 public void method1() { int localVar1 = 0; int localVar2 = 0; int localVar3 = 0; int localVar4 = 0; public void method2() { localVar1 = i + j; localVar2 = i - j; localVar3 = i * j; localVar4 = i / j; } for (int i = 1; i <= 10000; i++) { for (int j = 1; j <= 10000; j++) { method2(); }}} 結果 手動で書いたコードの変 数の渡し方が悪い 効率よく書くのが難しい オーバーヘッドは許容範囲 プログラム 実行時間(ms) 比 メソッド内 メソッド 1261 1 手動で分けたメソッド 1515 1.2 プログラム 実行時間(ms) メソッド内メソッド 1261 手動で分けたメソッド 1515 public ReturnValue method2(int i, int j) { int r1 = i + j; int r2 = i – j; int r3 = i * j; int r4 = i / j; return new ReturnValue(r1, r2, r3, r4); } 手で切り分けたコードが必ず速いわけではない ローカル変数が複数ある場合を想定しているので、 先ほどのオーバーヘッドは許容範囲 8’30’’ 手動で書いたmehtod2
関連研究 Regioncut [Akaiら ‘09] Closure Joinpoints [Bodden ‘11] コード領域をジョインポイントとして選択可能 コード領域に対する変更が可能 ローカル変数への代入が不可能 Closure Joinpoints [Bodden ‘11] コードブロックをジョインポイントとして選択可能 Beta [Knudsenら ‘94] オブジェクト指向言語 上書き可能なインナープロシージャ メソッド内メソッドと類似 スーパークラスの振る舞いが取り除けない
まとめ メソッド内メソッド 今後の課題 JastAddJを拡張してコンパイラを実装 オーバーライド可能 ローカル変数を参照可能 実装の改善 オーバーライドの記述の簡素化