Mendiagnosis dan Menyelesaikan StackOverflowError

Pesan forum Komunitas JavaWorld baru-baru ini (Stack Overflow setelah membuat instance objek baru) mengingatkan saya bahwa dasar-dasar StackOverflowError tidak selalu dipahami dengan baik oleh orang yang baru mengenal Java. Untungnya, StackOverflowError adalah salah satu kesalahan runtime yang lebih mudah untuk di-debug dan dalam posting blog ini saya akan menunjukkan betapa mudahnya mendiagnosis StackOverflowError. Perhatikan bahwa potensi stack overflow tidak terbatas pada Java.

Mendiagnosis penyebab StackOverflowError bisa dilakukan dengan mudah jika kode telah dikompilasi dengan opsi debug diaktifkan sehingga nomor baris tersedia dalam pelacakan tumpukan yang dihasilkan. Dalam kasus seperti itu, biasanya hanya masalah menemukan pola berulang nomor baris dalam pelacakan tumpukan. Pola nomor baris berulang sangat membantu karena StackOverflowError sering kali disebabkan oleh rekursi yang tidak diselesaikan. Nomor baris yang berulang menunjukkan kode yang secara langsung atau tidak langsung dipanggil secara rekursif. Perhatikan bahwa ada situasi selain rekursi tak terbatas di mana luapan tumpukan mungkin terjadi, tetapi entri blog ini dibatasi StackOverflowErrorkarena disebabkan oleh rekursi tak terbatas.

Hubungan rekursi menjadi buruk StackOverflowErrordicatat dalam deskripsi Javadoc untuk StackOverflowError yang menyatakan bahwa Kesalahan ini adalah "Dilempar saat tumpukan overflow terjadi karena aplikasi berulang terlalu dalam." Ini penting yang StackOverflowErrordiakhiri dengan kata Error dan merupakan Error (memperluas java.lang.Error melalui java.lang.VirtualMachineError) daripada Exception yang dicentang atau runtime. Perbedaannya signifikan. The Errordan Exceptionmasing-masing adalah khusus Throwable, tetapi penanganan mereka dimaksudkan sangat berbeda. Tutorial Java menunjukkan bahwa Kesalahan biasanya berada di luar aplikasi Java dan oleh karena itu biasanya tidak dapat dan tidak boleh ditangkap atau ditangani oleh aplikasi.

Saya akan menunjukkan menjalankan StackOverflowErrormelalui rekursi tak terbatas dengan tiga contoh berbeda. Kode yang digunakan untuk contoh-contoh ini terdapat dalam tiga kelas, yang pertama (dan kelas utama) akan ditampilkan berikutnya. Saya mencantumkan ketiga kelas secara keseluruhan karena nomor baris signifikan saat men-debug 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 tidak terbatas: rekursi tidak disengaja dan benar-benar tidak disengaja, rekursi tidak disengaja yang terkait dengan hubungan siklik yang disengaja, dan rekursi yang dimaksudkan dengan kondisi penghentian yang tidak memadai. Masing-masing dan keluarannya akan didiskusikan selanjutnya.

Rekursi yang Benar-Benar Tidak Disengaja

Ada kalanya rekursi terjadi tanpa maksud apa pun. Penyebab umum mungkin karena metode tidak sengaja memanggil dirinya sendiri. Misalnya, tidak terlalu sulit untuk menjadi terlalu ceroboh dan memilih rekomendasi pertama IDE pada nilai kembalian untuk metode "get" yang mungkin akan menjadi panggilan ke metode yang sama! Ini sebenarnya adalah contoh yang ditunjukkan di kelas di atas. The getStringVar()Metode berulang kali menyebut dirinya sampai StackOverflowErrorditemui. Outputnya akan muncul sebagai 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 tumpukan yang ditunjukkan di atas sebenarnya berkali-kali lebih lama dari yang saya tempatkan di atas, tetapi itu hanyalah pola berulang yang sama. Karena polanya berulang, mudah untuk mendiagnosis bahwa baris 34 dari kelas adalah penyebab masalah. Ketika kita melihat baris itu, kita melihat bahwa memang pernyataan return getStringVar()itu akhirnya berulang kali memanggil dirinya sendiri. Dalam hal ini, kita dapat dengan cepat menyadari bahwa perilaku yang dimaksudkan adalah sebaliknya return this.stringVar;.

Rekursi Tidak Disengaja dengan Hubungan Siklik

Ada risiko tertentu untuk memiliki hubungan siklik antar kelas. Salah satu risiko ini adalah kemungkinan yang lebih besar untuk mengalami rekursi yang tidak disengaja di mana dependensi siklik terus dipanggil di antara objek hingga tumpukan meluap. Untuk mendemonstrasikan ini, saya menggunakan dua kelas lagi. The Statekelas dan Citykelas memiliki relationshiop siklik karena Statemisalnya memiliki referensi ke modal Citydan Citymemiliki referensi ke Statedi mana ia berada.

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