Mendiagnosis dan Menyelesaikan StackOverflowError

Mesej forum Komuniti JavaWorld baru-baru ini (Stack Overflow setelah membuat objek baru) mengingatkan saya bahawa asas-asas StackOverflowError tidak selalu difahami dengan baik oleh orang yang baru menggunakan Java. Nasib baik, StackOverflowError adalah salah satu kesilapan runtime yang lebih mudah untuk disahpijat dan dalam catatan blog ini saya akan menunjukkan betapa mudahnya mendiagnosis StackOverflowError. Perhatikan bahawa potensi limpahan tumpukan tidak terbatas pada Java.

Mendiagnosis penyebab StackOverflowError boleh dilakukan secara langsung jika kod telah disusun dengan pilihan debug dihidupkan sehingga nombor baris tersedia dalam jejak timbunan yang dihasilkan. Dalam kes seperti itu, biasanya hanya untuk mencari corak nombor garis yang berulang dalam jejak timbunan. Corak mengulang nombor baris sangat membantu kerana StackOverflowError sering disebabkan oleh pengulangan yang tidak diselesaikan. Nombor baris yang berulang menunjukkan kod yang dipanggil secara berulang atau tidak secara berulang. Perhatikan bahawa terdapat situasi selain pengulangan tanpa batasan di mana tumpahan tumpukan mungkin berlaku, tetapi pengeposan blog ini terhad StackOverflowErrordisebabkan oleh pengulangan tanpa had.

Hubungan rekursi menjadi buruk StackOverflowErrordicatat dalam keterangan Javadoc untuk StackOverflowError yang menyatakan bahawa Ralat ini "Dilemparkan ketika tumpahan tumpahan berlaku kerana aplikasi berulang terlalu dalam." Ini penting yang StackOverflowErrordiakhiri dengan kata Error dan merupakan Error (memanjangkan java.lang.Error melalui java.lang.VirtualMachineError) daripada pengecualian yang dicentang atau runtime. Perbezaannya adalah ketara. Yang Errordan Exceptionmasing-masing adalah khusus dilontar, tetapi pengendalian yang dimaksudkan adalah agak berbeza. Tutorial Java menunjukkan bahawa Kesalahan biasanya adalah luaran untuk aplikasi Java dan dengan demikian biasanya tidak dapat dan tidak boleh ditangkap atau ditangani oleh aplikasi.

Saya akan menunjukkan jalan keluar StackOverflowErrormelalui pengulangan tanpa had dengan tiga contoh berbeza. Kod yang digunakan untuk contoh ini terdapat dalam tiga kelas, yang pertama (dan kelas utama) ditunjukkan seterusnya. Saya menyenaraikan ketiga-tiga kelas secara keseluruhan kerana nombor garis adalah penting semasa menyahpepijat 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 + "]"); } } } 

Kelas di atas menunjukkan tiga jenis rekursi tanpa had: rekursi yang tidak disengajakan dan benar-benar tidak disengajakan, pengulangan yang tidak disengajakan yang berkaitan dengan hubungan siklik yang sengaja, dan pengulangan yang dimaksudkan dengan keadaan penamatan yang tidak mencukupi. Setiap ini dan keluarannya dibincangkan seterusnya.

Pengulangan Sepenuhnya Tidak Disengajakan

Ada kalanya rekursi berlaku tanpa niat sama sekali. Sebab biasa mungkin ada kaedah yang tidak sengaja memanggilnya sendiri. Sebagai contoh, tidak terlalu sukar untuk terlalu cuai dan memilih cadangan pertama IDE mengenai nilai pengembalian untuk kaedah "get" yang mungkin menjadi panggilan kepada kaedah yang sama! Ini sebenarnya contoh yang ditunjukkan dalam kelas di atas. The getStringVar()Cara berulang kali panggilan sendiri sehingga StackOverflowErrorditemui. Keluaran akan muncul seperti berikut:

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 

Jejak timbunan yang ditunjukkan di atas sebenarnya jauh lebih lama daripada yang saya letakkan di atas, tetapi corak pengulangan yang sama. Oleh kerana coraknya berulang, mudah didiagnosis bahawa baris 34 kelas adalah penyebab masalah. Apabila kita melihat garis itu, kita melihat bahawa memang pernyataan return getStringVar()yang akhirnya berulang kali menyebut dirinya. Dalam kes ini, kita dapat dengan cepat menyadari bahawa tingkah laku yang dimaksudkan adalah sebaliknya return this.stringVar;.

Pengulangan yang Tidak Diingini dengan Hubungan Siklik

Terdapat risiko tertentu untuk mempunyai hubungan kitaran antara kelas. Salah satu risiko ini adalah kemungkinan besar berlakunya rekursi yang tidak disengajakan di mana ketergantungan siklik terus dipanggil antara objek sehingga timbunan meluap. Untuk menunjukkan ini, saya menggunakan dua kelas lagi. The Statekelas dan Citykelas mempunyai relationshiop kitaran kerana Statecontoh mempunyai rujukan kepada modal Citydan Citymempunyai rujukan kepada Statedi mana ia terletak.

Negeri.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; } } 

Bandar.java