Programovanie

Diagnostika a riešenie chyby StackOverflowError

Nedávna správa vo fóre komunity JavaWorld (Stack Overflow po vytvorení inštancie nového objektu) mi pripomenula, že ľudia, ktorí sú v Jave noví, nie vždy dobre rozumejú základom StackOverflowError. Našťastie je StackOverflowError jednou z ľahších ladiacich chýb za behu a v tomto blogovom príspevku ukážem, aké ľahké je často diagnostikovať StackOverflowError. Upozorňujeme, že potenciál pretečenia zásobníka sa neobmedzuje iba na Javu.

Diagnostika príčiny StackOverflowError môže byť pomerne jednoduchá, ak bol kód kompilovaný so zapnutou voľbou ladenia, aby boli vo výslednom sledovaní zásobníka k dispozícii čísla riadkov. V takýchto prípadoch obvykle stačí nájsť opakujúci sa vzor čísel riadkov v stope zásobníka. Vzor opakujúcich sa čísel riadkov je užitočný, pretože StackOverflowError je často spôsobený neukončenou rekurziou. Opakujúce sa čísla riadkov označujú kód, ktorý sa volá priamo alebo nepriamo rekurzívne. Upozorňujeme, že existujú iné situácie ako neobmedzená rekurzia, v ktorých môže dôjsť k pretečeniu zásobníka, ale tento príspevok do blogu je obmedzený na StackOverflowError spôsobené nespútanou rekurziou.

Vzťah rekurzie sa zhoršil StackOverflowError je uvedené v popise Javadoc pre StackOverflowError, ktorý uvádza, že táto chyba je „Vyvolaná, keď dôjde k pretečeniu zásobníka, pretože sa aplikácia opakuje príliš hlboko.“ Je dôležité, že StackOverflowError končí slovom Chyba a je chybou (rozširuje java.lang.Error cez java.lang.VirtualMachineError) a nie kontrolovanou alebo runtime výnimkou. Rozdiel je značný. The Chyba a Výnimka sú každý špecializovaný Throwable, ale ich zamýšľané zaobchádzanie je úplne odlišné. Výukový program Java poukazuje na to, že chyby sú zvyčajne externé voči aplikácii Java, a preto ich aplikácia zvyčajne nemôže a nesmie chytiť alebo spracovať.

Ukážem, že narazím StackOverflowError prostredníctvom neobmedzenej rekurzie s tromi rôznymi príkladmi. Kód použitý v týchto príkladoch je obsiahnutý v troch triedach, z ktorých prvá (a hlavná trieda) sú zobrazené ďalej. Uvádzam všetky tri triedy ako celok, pretože čísla riadkov sú významné pri ladení súboru StackOverflowError.

StackOverflowErrorDemonstrator.java

balíček dustin.examples.stackoverflow; import java.io.IOException; import java.io.OutputStream; / ** * Táto trieda demonštruje rôzne spôsoby, ako by mohlo dôjsť k StackOverflowError *. * / public class StackOverflowErrorDemonstrator {private static final String NEW_LINE = System.getProperty ("line.separator"); / ** Ľubovoľný dátový člen na základe reťazca. * / private String stringVar = ""; / ** * Jednoduchý prístupový objekt, ktorý zobrazí neúmyselnú rekurziu, sa pokazil. Po vyvolaní * sa táto metóda bude opakovane nazývať sama. Pretože neexistuje * zadaná podmienka ukončenia na ukončenie rekurzie, dá sa očakávať * StackOverflowError. * * @return Premenná reťazca. * / public String getStringVar () {// // UPOZORNENIE: // // Toto je ZLÉ! Toto sa bude rekurzívne volať, kým stack // nepretečie a nevyhodí sa StackOverflowError. Zamýšľaný riadok v // tomto prípade mal byť: // return this.stringVar; návrat getStringVar (); } / ** * Vypočítajte faktoriál poskytnutého celého čísla. Táto metóda sa spolieha na * rekurziu. * * @param number Číslo, ktorého faktoriál je požadovaný. * @return Faktorová hodnota poskytnutého čísla. * / public int countFactorial (konečné číslo int) {// UPOZORNENIE: Ak to bude mať číslo menšie ako nula, skončí sa to zle. // Lepší spôsob, ako to urobiť, je uvedený tu, ale je komentovaný. // návratové číslo <= 1? 1: číslo * vypočítaťFaktoriál (číslo-1); spiatočné číslo == 1? 1: číslo * vypočítaťFaktoriál (číslo-1); } / ** * Táto metóda ukazuje, ako neúmyselná rekurzia často vedie k * StackOverflowError, pretože pre * neúmyselnú rekurziu nie je poskytnutá žiadna podmienka ukončenia. * / public void runUnintentionalRecursionExample () {final String unusedString = this.getStringVar (); } / ** * Táto metóda demonštruje, ako môže neúmyselná rekurzia ako súčasť cyklickej * závislosti viesť k StackOverflowError, ak nebude dôsledne rešpektovaná. * / public void runUnintentionalCyclicRecusionExample () {final State newMexico = State.buildState ("Nové Mexiko", "NM", "Santa Fe"); System.out.println ("Novo vytvorený stav je:"); System.out.println (newMexico); } / ** * Ukazuje, ako aj zamýšľaná rekurzia môže mať za následok StackOverflowError *, keď nikdy nie je splnená konečná podmienka rekurzívnej funkcie. * / public void runIntentionalRecursiveWithDysfunctionalTermination () {final int numberForFactorial = -1; System.out.print ("Faktoriál" + numberForFactorial + "je:"); System.out.println (CalculateFactorial (numberForFactorial)); } / ** * Zapíšte hlavné možnosti tejto triedy do poskytnutého OutputStream. * * @param out OutputStream, do ktorého sa majú zapísať možnosti tejto testovacej aplikácie. * / public static void writeOptionsToStream (konečný výstup OutStream) {konečná voľba reťazca1 = "1. Neúmyselná (bez podmienky ukončenia) rekurzia jednej metódy"; final String option2 = "2. Neúmyselná (bez podmienky ukončenia) cyklická rekurzia"; final String option3 = "3. Chybná rekurzia ukončenia"; 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 ("(Nedá sa zapísať na poskytnutý OutputStream)"); System.out.println (možnosť1); System.out.println (možnosť2); System.out.println (možnosť3); }} / ** * Hlavná funkcia pre spustenie StackOverflowErrorDemonstrator. * / public static void main (final String [] argumenty) {if (argumenty.lenka <1) {System.err.println ("Musíte uviesť argument a ten jediný argument by mal byť"); System.err.println ("jedna z nasledujúcich možností:"); writeOptionsToStream (System.err); System.exit (-1); } int moznost = 0; try {option = Integer.valueOf (argumenty [0]); } catch (NumberFormatException notNumericFormat) {System.err.println ("Zadali ste nečíselnú (neplatnú) možnosť [" + argumenty [0] + "]"); writeOptionsToStream (System.err); System.exit (-2); } final StackOverflowErrorDemonstrator me = nový StackOverflowErrorDemonstrator (); switch (možnosť) {prípad 1: me.runUnintentionalRecursionExample (); prestávka; prípad 2: me.runUnintentionalCyclicRecusionExample (); prestávka; prípad 3: me.runIntentionalRecursiveWithDysfunctionalTermination (); prestávka; predvolené: System.err.println ("Poskytli ste neočakávanú možnosť [" + možnosť + "]"); }}} 

Trieda vyššie demonštruje tri typy neobmedzenej rekurzie: náhodná a úplne nechcená rekurzia, neúmyselná rekurzia spojená so zámerne cyklickými vzťahmi a zamýšľaná rekurzia s nedostatočnou podmienkou ukončenia. O každom z nich a ich výstupoch sa bude diskutovať ďalej.

Úplne neúmyselná rekurzia

Môžu nastať chvíle, kedy dôjde k rekurzii bez akéhokoľvek jej zámeru. Bežnou príčinou môže byť náhodná voľba metódy. Napríklad nie je príliš ťažké byť trochu neopatrný a zvoliť prvé odporúčanie IDE pre návratovú hodnotu pre metódu „get“, ktorá by mohla skončiť volaním tej istej metódy! Toto je v skutočnosti príklad uvedený v triede vyššie. The getStringVar () metóda sa opakovane volá až do StackOverflowError sa stretol. Výstup sa zobrazí takto:

Výnimka vo vlákne „main“ java.lang.StackOverflowError na adrese dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) na dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getstorErrorDemonstrator.getstorErrorDemonstrator.getErrorDemonstrator.getErrorDemonstrator.getErrorDemonstrator.getErrorDemonstrator.getErrorDemonstrator.getErrorDemonstrator.getErrorDemonstrator.getErrorDemonstrator.getErrorDemonstrator.getErrorDemonstrator.get Stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) na adrese dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) .stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) na adrese dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) atemint.OverStorstOverStorstOverStore n.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) o 

Stopa zásobníka uvedená vyššie je v skutočnosti mnohokrát dlhšia ako tá, ktorú som umiestnil vyššie, ale je to jednoducho ten istý opakujúci sa vzor. Pretože sa vzor opakuje, je ľahké diagnostikovať, že linka 34 triedy je pôvodcom problému. Keď sa pozrieme na tento riadok, zistíme, že ide skutočne o výrok návrat getStringVar () ktorá sa opakovane nazýva sama sebou. V takom prípade si rýchlo uvedomíme, že zamýšľaným správaním bolo namiesto toho vrátiť this.stringVar;.

Neúmyselná rekurzia s cyklickými vzťahmi

Cyklistické vzťahy medzi triedami sú spojené s určitými rizikami. Jedným z týchto rizík je vyššia pravdepodobnosť spustenia do neúmyselnej rekurzie, keď sa medzi objektmi neustále volajú cyklické závislosti, kým sa zásobník nepreleje. Na demonštráciu toho používam ďalšie dve triedy. The Štát triedy a Mesto triedy majú cyklický vzťahshiop, pretože a Štát inštancia má odkaz na svoj kapitál Mesto a a Mesto má odkaz na Štát v ktorej sa nachádza.

State.java

balíček dustin.examples.stackoverflow; / ** * Trieda, ktorá predstavuje štát a je zámerne súčasťou cyklického * vzťahu medzi mestom a štátom. * / verejná trieda State {private static final String NEW_LINE = System.getProperty ("line.separator"); / ** Názov štátu. * / súkromné ​​meno reťazca; / ** Dvojpísmenová skratka pre štát. * / súkromná skratka reťazca; / ** Mesto, ktoré je hlavným mestom štátu. * / súkromné ​​Mesto capitalCity; / ** * Statická metóda vytvárania, ktorá je určená na vytvorenie inštancie mňa. * * @param newName Názov novovytvoreného štátu. * @param newAbbreviation Dvojpísmenová skratka štátu. * @param newCapitalCityName Názov hlavného mesta. * / public static State buildState (final String newName, final String newAbbreviation, final String newCapitalCityName) {final State instance = new State (newName, newAbbreviation); instance.capitalCity = nové mesto (newCapitalCityName, inštancia); návratová inštancia; } / ** * Parametrizovaný konštruktor prijímajúci údaje na vyplnenie novej inštancie State. * * @param newName Názov novovytvoreného štátu. * @param newAbbreviation Dvojpísmenová skratka štátu. * / private State (konečný reťazec newName, konečný reťazec newAbbreviation) {this.name = newName; this.abbreviation = newAbbreviation; } / ** * Poskytnite reťazcové znázornenie inštancie štátu. * * @return Reprezentácia môjho reťazca. * / @Override public String toString () {// UPOZORNENIE: Toto sa skončí zle, pretože implicitne volá metódu City toString () // a metóda City toString () volá túto metódu // State.toString (). return "StateName:" + this.name + NEW_LINE + "StateAbbreviation:" + this.abbreviation + NEW_LINE + "CapitalCity:" + this.capitalCity; }} 

City.java

$config[zx-auto] not found$config[zx-overlay] not found