StackOverflowErrorの診断と解決

最近のJavaWorldコミュニティフォーラムメッセージ(新しいオブジェクトをインスタンス化した後のStack Overflow)は、StackOverflowErrorの基本がJavaに不慣れな人々によって常によく理解されているとは限らないことを思い出させました。幸い、StackOverflowErrorはデバッグが簡単なランタイムエラーの1つであり、このブログ投稿では、StackOverflowErrorの診断がいかに簡単であるかを示します。スタックオーバーフローの可能性はJavaに限定されないことに注意してください。

デバッグオプションをオンにしてコードをコンパイルし、結果のスタックトレースで行番号を使用できるようにすると、StackOverflowErrorの原因の診断がかなり簡単になります。このような場合、通常は、スタックトレースで行番号の繰り返しパターンを見つけるだけです。 StackOverflowErrorは多くの場合、終了していない再帰によって発生するため、行番号を繰り返すパターンが役立ちます。繰り返し行番号は、直接的または間接的に再帰的に呼び出されているコードを示します。無制限の再帰以外にスタックオーバーフローが発生する可能性がある状況がありますが、このブログ投稿はStackOverflowError無制限の再帰が原因であることに限定されていることに注意してください。

再帰の関係が悪くなったのStackOverflowErrorは、StackOverflowErrorのJavadocの説明に記載されており、このエラーは「アプリケーションの再帰が深すぎるためにスタックオーバーフローが発生したときにスローされます」と記載されています。エラーStackOverflowErrorという単語で終わり、チェック済みまたは実行時の例外ではなく、エラー(java.lang.VirtualMachineErrorを介してjava.lang.Errorを拡張する)であることが重要です。違いは重要です。各AはThrowableの専門ですが、彼らの意図取り扱いはかなり異なっています。 Javaチュートリアルでは、エラーは通常Javaアプリケーションの外部にあるため、アプリケーションでキャッチまたは処理することはできず、処理すべきではないと指摘しています。ErrorException

StackOverflowError3つの異なる例を使用して、無制限の再帰を介して遭遇することを示します。これらの例で使用されているコードは3つのクラスに含まれており、最初のクラス(およびメインクラス)が次に示されています。をデバッグするときに行番号が重要であるため、3つのクラスすべてをまとめてリストしますStackOverflowError

StackOverflowErrorDemonstrator.java

package dustin.examples.stackoverflow; import java.io.IOException; import java.io.OutputStream; /** * This class demonstrates different ways that a StackOverflowError might * occur. */ public class StackOverflowErrorDemonstrator { private static final String NEW_LINE = System.getProperty("line.separator"); /** Arbitrary String-based data member. */ private String stringVar = ""; /** * Simple accessor that will shown unintentional recursion gone bad. Once * invoked, this method will repeatedly call itself. Because there is no * specified termination condition to terminate the recursion, a * StackOverflowError is to be expected. * * @return String variable. */ public String getStringVar() { // // WARNING: // // This is BAD! This will recursively call itself until the stack // overflows and a StackOverflowError is thrown. The intended line in // this case should have been: // return this.stringVar; return getStringVar(); } /** * Calculate factorial of the provided integer. This method relies upon * recursion. * * @param number The number whose factorial is desired. * @return The factorial value of the provided number. */ public int calculateFactorial(final int number) { // WARNING: This will end badly if a number less than zero is provided. // A better way to do this is shown here, but commented out. //return number <= 1 ? 1 : number * calculateFactorial(number-1); return number == 1 ? 1 : number * calculateFactorial(number-1); } /** * This method demonstrates how unintended recursion often leads to * StackOverflowError because no termination condition is provided for the * unintended recursion. */ public void runUnintentionalRecursionExample() { final String unusedString = this.getStringVar(); } /** * This method demonstrates how unintended recursion as part of a cyclic * dependency can lead to StackOverflowError if not carefully respected. */ public void runUnintentionalCyclicRecusionExample() { final State newMexico = State.buildState("New Mexico", "NM", "Santa Fe"); System.out.println("The newly constructed State is:"); System.out.println(newMexico); } /** * Demonstrates how even intended recursion can result in a StackOverflowError * when the terminating condition of the recursive functionality is never * satisfied. */ public void runIntentionalRecursiveWithDysfunctionalTermination() { final int numberForFactorial = -1; System.out.print("The factorial of " + numberForFactorial + " is: "); System.out.println(calculateFactorial(numberForFactorial)); } /** * Write this class's main options to the provided OutputStream. * * @param out OutputStream to which to write this test application's options. */ public static void writeOptionsToStream(final OutputStream out) { final String option1 = "1. Unintentional (no termination condition) single method recursion"; final String option2 = "2. Unintentional (no termination condition) cyclic recursion"; final String option3 = "3. Flawed termination recursion"; try { out.write((option1 + NEW_LINE).getBytes()); out.write((option2 + NEW_LINE).getBytes()); out.write((option3 + NEW_LINE).getBytes()); } catch (IOException ioEx) { System.err.println("(Unable to write to provided OutputStream)"); System.out.println(option1); System.out.println(option2); System.out.println(option3); } } /** * Main function for running StackOverflowErrorDemonstrator. */ public static void main(final String[] arguments) { if (arguments.length < 1) { System.err.println( "You must provide an argument and that single argument should be"); System.err.println( "one of the following options:"); writeOptionsToStream(System.err); System.exit(-1); } int option = 0; try { option = Integer.valueOf(arguments[0]); } catch (NumberFormatException notNumericFormat) { System.err.println( "You entered an non-numeric (invalid) option [" + arguments[0] + "]"); writeOptionsToStream(System.err); System.exit(-2); } final StackOverflowErrorDemonstrator me = new StackOverflowErrorDemonstrator(); switch (option) { case 1 : me.runUnintentionalRecursionExample(); break; case 2 : me.runUnintentionalCyclicRecusionExample(); break; case 3 : me.runIntentionalRecursiveWithDysfunctionalTermination(); break; default : System.err.println("You provided an unexpected option [" + option + "]"); } } } 

上記のクラスは、3種類の無制限の再帰を示しています。偶発的で完全に意図しない再帰、意図的に循環する関係に関連する意図しない再帰、および終了条件が不十分な意図的な再帰です。次に、これらのそれぞれとその出力について説明します。

完全に意図しない再帰

再帰がまったく意図せずに発生する場合があります。一般的な原因は、メソッドが誤って自分自身を呼び出すことである可能性があります。たとえば、少し不注意になって、「get」メソッドの戻り値についてIDEの最初の推奨事項を選択することはそれほど難しくありません。これは、まったく同じメソッドの呼び出しになる可能性があります。これは実際、上記のクラスに示されている例です。getStringVar()までの方法は、繰り返し自分自身を呼び出すStackOverflowError遭遇しています。出力は次のように表示されます。

Exception in thread "main" java.lang.StackOverflowError at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at 

上に示したスタックトレースは、実際には上に配置したものより何倍も長くなっていますが、それは単に同じ繰り返しパターンです。パターンが繰り返されているため、クラスの34行目が問題の原因であると簡単に診断できます。その行を見ると、それが実際にreturn getStringVar()繰り返し自分自身を呼び出すことになるステートメントであることがわかります。この場合、意図された動作が代わりにであったことがすぐにわかりますreturn this.stringVar;

循環関係を伴う意図しない再帰

クラス間に循環的な関係を持つことには一定のリスクがあります。これらのリスクの1つは、スタックがオーバーフローするまで循環依存関係がオブジェクト間で継続的に呼び出される、意図しない再帰に遭遇する可能性が高くなることです。これを示すために、さらに2つのクラスを使用します。Stateクラス及びCityためのクラスは、環状relationshiopを有するStateインスタンスは、その資本への参照を持つCityCity参照有しState、それが配置されています。

State.java

package dustin.examples.stackoverflow; /** * A class that represents a state and is intentionally part of a cyclic * relationship between City and State. */ public class State { private static final String NEW_LINE = System.getProperty("line.separator"); /** Name of the state. */ private String name; /** Two-letter abbreviation for state. */ private String abbreviation; /** City that is the Capital of the State. */ private City capitalCity; /** * Static builder method that is the intended method for instantiation of me. * * @param newName Name of newly instantiated State. * @param newAbbreviation Two-letter abbreviation of State. * @param newCapitalCityName Name of capital city. */ public static State buildState( final String newName, final String newAbbreviation, final String newCapitalCityName) { final State instance = new State(newName, newAbbreviation); instance.capitalCity = new City(newCapitalCityName, instance); return instance; } /** * Parameterized constructor accepting data to populate new instance of State. * * @param newName Name of newly instantiated State. * @param newAbbreviation Two-letter abbreviation of State. */ private State( final String newName, final String newAbbreviation) { this.name = newName; this.abbreviation = newAbbreviation; } /** * Provide String representation of the State instance. * * @return My String representation. */ @Override public String toString() { // WARNING: This will end badly because it calls City's toString() // method implicitly and City's toString() method calls this // State.toString() method. return "StateName: " + this.name + NEW_LINE + "StateAbbreviation: " + this.abbreviation + NEW_LINE + "CapitalCity: " + this.capitalCity; } } 

City.java