Programavimas

„StackOverflowError“ diagnozavimas ir sprendimas

Neseniai paskelbtas „JavaWorld“ bendruomenės forumo pranešimas („Stack Overflow“, sukūrus naują objektą) man priminė, kad „StackOverflowError“ pagrindai ne visada gerai suprantami žmonėms, naujiems „Java“. Laimei, „StackOverflowError“ yra viena iš lengviau paleidžiamų vykdymo laiko klaidų ir šiame tinklaraščio įraše aš pademonstruosiu, kaip dažnai lengva diagnozuoti „StackOverflowError“. Atkreipkite dėmesį, kad kamino perpildymas yra ne tik „Java“.

„StackOverflowError“ priežasties diagnozavimas gali būti gana paprastas, jei kodas buvo sukompiliuotas įjungus derinimo parinktį, kad gautame kamino pėdsakyje būtų prieinami eilutės numeriai. Tokiais atvejais paprastai reikia surasti pasikartojantį eilučių skaičių šūsnies pėdsakuose. Eilių skaičių kartojimas yra naudingas, nes „StackOverflowError“ dažnai sukelia nenutrūkstama rekursija. Pasikartojantys eilutės numeriai nurodo kodą, kuris yra tiesiogiai ar netiesiogiai rekursiškai vadinamas. Atminkite, kad yra ne tik neribotos rekursijos atvejų, kai gali pasitaikyti kamino perpildymas, tačiau šis tinklaraščio įrašas apsiriboja „StackOverflowError“ sukeltas neriboto rekurso.

Rekursijos santykiai blogi „StackOverflowError“ yra pažymėtas „Javadoc“ apraše, skirtame „StackOverflowError“, kuriame teigiama, kad ši klaida yra „išmesta, kai įvyksta kamino perpildymas, nes programa pasikartoja per giliai“. Reikšminga tai „StackOverflowError“ baigiasi žodžiu Klaida ir yra klaida (pratęsia java.lang.Error per java.lang.VirtualMachineError), o ne patikrinta ar vykdymo laiko išimtis. Skirtumas yra didelis. Klaida ir Išimtis kiekvienas yra specializuotas mėtomas, tačiau jų numatytas tvarkymas yra gana skirtingas. „Java Tutorial“ pažymi, kad klaidos paprastai yra išorinės „Java“ programos išorės, todėl paprastai jų negalima ir nereikia gaudyti ar tvarkyti.

Pademonstruosiu, kad įvažiuoju „StackOverflowError“ per neribotą rekursiją su trimis skirtingais pavyzdžiais. Šiems pavyzdžiams naudojamas kodas yra trijose klasėse, iš kurių pirmoji (ir pagrindinė) rodoma toliau. Išvardiju visas tris klases visas, nes derinant eilutę eilutės numeriai yra reikšmingi „StackOverflowError“.

„StackOverflowErrorDemonstrator.java“

paketas dustin.examples.stackoverflow; importuoti java.io.IOException; importuoti java.io.OutputStream; / ** * Ši klasė parodo skirtingus būdus, kaip gali atsirasti „StackOverflowError“. * / public class StackOverflowErrorDemonstrator {private static final String NEW_LINE = System.getProperty ("line.separator"); / ** Savavališkas eilučių duomenų narys. * / privati ​​eilutė stringVar = ""; / ** * Paprasta prieiga, kuri parodys netyčinį rekursiją. Kai bus iškviestas *, šis metodas pakartotinai pasikvies. Kadangi rekursijai nutraukti nėra * nurodytos nutraukimo sąlygos, reikia tikėtis * StackOverflowError. * * @return String kintamasis. * / public string getStringVar () {// // ĮSPĖJIMAS: // // Tai blogai! Tai rekursyviai vadinsis tol, kol kaminas // perpildys ir „StackOverflowError“ bus išmestas. Numatyta eilutė // šiuo atveju turėjo būti: // return this.stringVar; grįžti getStringVar (); } / ** * Apskaičiuokite pateikto sveiko skaičiaus faktorialą. Šis metodas remiasi * rekursija. * * @param number Skaičius, kurio faktoriaus pageidaujama. * @return Pateikto skaičiaus faktoriaus vertė. * / public int calcFactorial (galutinis int skaičius) {// ĮSPĖJIMAS: Tai baigsis blogai, jei nurodomas mažesnis nei nulis skaičius. // Čia parodytas geresnis būdas tai padaryti, bet pakomentuotas. // grąžinimo numeris <= 1? 1: skaičius * apskaičiuotiFaktorius (skaičius-1); grąžinimo numeris == 1? 1: skaičius * apskaičiuotiFaktorius (skaičius-1); } / ** * Šis metodas parodo, kaip nenumatytas rekursija dažnai sukelia * StackOverflowError, nes nenumatyta * nenumatyto rekurso nutraukimo sąlyga. * / public void runUnintentionalRecursionExample () {final String unusedString = this.getStringVar (); } / ** * Šis metodas parodo, kaip netyčinis pasikartojimas kaip ciklinės * priklausomybės dalis gali sukelti „StackOverflowError“, jei jo nebus kruopščiai laikomasi. * / public void runUnintentionalCyclicRecusionExample () {final State newMexico = State.buildState ("Naujoji Meksika", "NM", "Santa Fe"); System.out.println ("Naujai sukurta būsena yra:"); System.out.println (newMexico); } / ** * Parodo, kaip net numatytas rekursija gali sukelti „StackOverflowError *“, kai rekurzinės funkcijos pabaigos sąlyga niekada nėra * patenkinta. * / public void runIntentionalRecursiveWithDysfunctionalTermination () {galutinis int skaičiusForFactorial = -1; System.out.print ("" + numberForFactorial + "faktorius yra:"); System.out.println (apskaičiuotiFactorial (numberForFactorial)); } / ** * Parašykite pagrindines šios klasės parinktis į pateiktą „OutputStream“. * * @param out OutputStream, kur rašyti šios bandomosios programos parinktis. * / public static void writeOptionsToStream (final OutputStream out) {final String option1 = "1. Netyčinis (be nutraukimo sąlygos) vieno metodo rekursija"; galutinis eilutės variantas2 = "2. Netyčinis (be nutraukimo sąlygų) ciklinis rekursija"; galutinis eilutės variantas3 = "3. Netinkamas nutraukimo rekursija"; pabandykite {out.write ((option1 + NEW_LINE) .getBytes ()); out.write ((parinktis2 + NEW_LINE) .getBytes ()); out.write ((parinktis3 + NEW_LINE) .getBytes ()); } gaudyti (IOException ioEx) {System.err.println ("(Nepavyko parašyti į pateiktą OutputStream)"); System.out.println (1 parinktis); System.out.println (2 parinktis); System.out.println (3 parinktis); }} / ** * Pagrindinė „StackOverflowErrorDemonstrator“ paleidimo funkcija. * / public static void main (final String [] argumentai) {if (argumentai.length <1) {System.err.println ("Jūs turite pateikti argumentą ir tas vienintelis argumentas turėtų būti"); System.err.println ("viena iš šių parinkčių:"); writeOptionsToStream (System.err); System.exit (-1); } int variantas = 0; pabandykite {option = Integer.valueOf (argumentai [0]); } catch (NumberFormatException notNumericFormat) {System.err.println ("Įvedėte ne skaitmeninę (neteisingą) parinktį [" + argumentai [0] + "]"); writeOptionsToStream (System.err); System.exit (-2); } final StackOverflowErrorDemonstrator me = naujas StackOverflowErrorDemonstrator (); jungiklis (parinktis) {atvejis 1: me.runUnintentionalRecursionExample (); pertrauka; 2 atvejis: me.runUnententionalCyclicRecusionExample (); pertrauka; 3 atvejis: me.runIntentionalRecursiveWithDysfunctionalTermination (); pertrauka; numatytasis: System.err.println ("Pateikėte netikėtą parinktį [" + parinktis + "]"); }}} 

Aukščiau paminėta klasė rodo tris neriboto rekurso tipus: atsitiktinis ir visiškai nenumatytas rekursija, nenumatytas rekursija, susijęs su tyčia cikliškais santykiais, ir numatytas rekursija, esant nepakankamai nutraukimo sąlygai. Kiekvienas iš jų ir jų rezultatai aptariami toliau.

Visiškai nenumatytas rekursija

Gali būti atvejų, kai rekursija įvyksta be jokio tikslo. Dažna priežastis gali būti netyčia paskambinęs metodas. Pavyzdžiui, nėra labai sunku pasidaryti per daug neatsargus ir pasirinkti pirmąją IDE rekomendaciją dėl „get“ metodo grąžos vertės, kuri gali būti raginimas naudoti tą patį metodą! Iš tikrųjų tai yra pavyzdys, parodytas aukščiau pateiktoje klasėje. „getStringVar“ () metodas pakartotinai vadinasi, kol „StackOverflowError“ yra susiduriama. Rezultatas pasirodys taip:

Išimtis temoje „main“ java.lang.StackOverflowError at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowEstackStarDover.StackOverowator stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) ne dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) ne dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) esant dustin.examples .stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) ne dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) ne dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) esant dusti n.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) 

Aukščiau pateiktas kamino pėdsakas iš tikrųjų yra daug kartų ilgesnis nei tas, kurį įdėjau aukščiau, bet tai tiesiog tas pats pasikartojantis modelis. Kadangi modelis kartojasi, lengva diagnozuoti, kad 34 klasės eilutė yra problemos priežastis. Pažvelgę ​​į tą eilutę matome, kad tai tikrai teiginys grąžinti „getStringVar“ () tai galiausiai pakartotinai save vadina. Tokiu atveju galime greitai suvokti, kad vietoj to buvo siekiama grąžink tai.stringVar;.

Nenumatytas rekursija su cikliškais santykiais

Yra tam tikra rizika, kad cikliniai santykiai tarp klasių yra susiję. Viena iš šių rizikų yra didesnė tikimybė patekti į nenumatytą rekursiją, kai ciklinės priklausomybės nuolat vadinamos tarp objektų, kol kaminas perpildomas. Norėdami tai parodyti, naudoju dar dvi klases. Valstija klasė ir Miestas klasės turi ciklinius santykius, nes a Valstija pavyzdžiui, turi nuorodą į savo kapitalą Miestas ir a Miestas turi nuorodą į Valstija kurioje jis yra.

Valstybė.java

paketas dustin.examples.stackoverflow; / ** * Valstybę reprezentuojanti klasė, kuri sąmoningai yra cikliško * miesto ir valstijos santykio dalis. * / public class State {private static final String NEW_LINE = System.getProperty ("line.separator"); / ** Valstybės pavadinimas. * / asmeninės eilutės pavadinimas; / ** Dviejų raidžių valstybės santrumpa. * / privati ​​stygų santrumpa; / ** Miestas, kuris yra valstybės sostinė. * / privatus miesto sostinės miestas; / ** * Statinio kūrimo metodas, kuris yra skirtas man išaiškinti. * * @param newName Naujai išrinktos valstybės pavadinimas. * @param newAbbreviation Dviejų raidžių valstybės santrumpa. * @param newCapitalCityName Sostinės pavadinimas. * / public static State buildState (final String newName, final String newAbbreviation, final String newCapitalCityName) {final State instance = new State (newName, newAbbreviation); instancija.kapitalCity = naujas miestas (newCapitalCityName, egzempliorius); grąžinimo instancija; } / ** * Parametruotas konstruktorius, priimantis duomenis naujam valstybės egzemplioriui užpildyti. * * @param newName Naujai išrinktos valstybės pavadinimas. * @param newAbbreviation Dviejų raidžių valstybės santrumpa. * / privati ​​valstybė (final String newName, final String newAbbreviation) {this.name = newName; this.abbreviation = newAbbreviation; } / ** * Pateikite eilutę valstybės instancijai. * * @return Mano eilutė. * / @Paisyti viešąją eilutę toString () {// ĮSPĖJIMAS: Tai baigsis blogai, nes jis netiesiogiai iškviečia „City toString () //“ metodą, o metodas „City toString ()“ vadina šį metodą // State.toString (). grąžinti "StateName:" + this.name + NEW_LINE + "StateAbbreviation:" + this.abbreviation + NEW_LINE + "CapitalCity:" + this.capitalCity; }} 

Miestas.java