Programování

Diagnostika a řešení StackOverflowError

Nedávná zpráva ve fóru JavaWorld Community (přetečení zásobníku po vytvoření instance nového objektu) mi připomněla, že lidé, kteří jsou v Javě noví, základům StackOverflowError nerozumí vždy dobře. Naštěstí je StackOverflowError jednou z jednodušších ladicích chyb za běhu a v tomto příspěvku na blogu ukážu, jak snadné je často diagnostikovat StackOverflowError. Všimněte si, že potenciál přetečení zásobníku není omezen na Javu.

Diagnostika příčiny StackOverflowError může být docela přímočará, pokud byl kód zkompilován s možností ladění zapnutou, takže ve výsledném trasování zásobníku jsou k dispozici čísla řádků. V takových případech obvykle stačí najít opakující se vzor čísel řádků ve stopě zásobníku. Vzor opakujících se čísel řádků je užitečný, protože StackOverflowError je často způsoben neukončenou rekurzí. Opakující se čísla řádků označují kód, který je přímo nebo nepřímo rekurzivně volán. Všimněte si, že existují situace jiné než neomezená rekurze, ve kterých může dojít k přetečení zásobníku, ale tento příspěvek do blogu je omezen na StackOverflowError způsobeno neomezenou rekurzí.

Vztah rekurze se zhoršil StackOverflowError je uvedeno v popisu Javadoc pro StackOverflowError, který uvádí, že tato chyba je „Vyvolána, když dojde k přetečení zásobníku, protože aplikace se opakuje příliš hluboko.“ Je důležité, že StackOverflowError končí slovem Chyba a je chybou (rozšiřuje java.lang.Error přes java.lang.VirtualMachineError) spíše než kontrolovanou nebo runtime výjimku. Rozdíl je značný. The Chyba a Výjimka jsou každý specializovaný Throwable, ale jejich zamýšlené zacházení je zcela odlišné. Výukový program Java poukazuje na to, že chyby jsou obvykle externí vůči aplikaci Java, a proto je běžně nemohou a neměly by být aplikací zachyceny nebo zpracovány.

Ukážu narážku StackOverflowError prostřednictvím neomezené rekurze se třemi různými příklady. Kód použitý pro tyto příklady je obsažen ve třech třídách, z nichž první (a hlavní třída) jsou zobrazeny dále. Uvádím všechny tři třídy jako celek, protože čísla řádků jsou významná při ladění StackOverflowError.

StackOverflowErrorDemonstrator.java

balíček dustin.examples.stackoverflow; import java.io.IOException; importovat java.io.OutputStream; / ** * Tato třída ukazuje různé způsoby, jak může nastat StackOverflowError *. * / public class StackOverflowErrorDemonstrator {private static final String NEW_LINE = System.getProperty ("line.separator"); / ** Libovolný datový člen na základě řetězce. * / private String stringVar = ""; / ** * Jednoduchý přístupový objekt, který zobrazí neúmyslnou rekurzi. Jakmile je * vyvolána, bude se tato metoda opakovaně nazývat sama. Protože neexistuje * specifikovaná podmínka ukončení pro ukončení rekurze, lze očekávat * StackOverflowError. * * @return Proměnná řetězce. * / public String getStringVar () {// // UPOZORNĚNÍ: // // Toto je ŠPATNÉ! To se rekurzivně zavolá, dokud stack // nepřeteče a není vyvolána StackOverflowError. Zamýšlený řádek v // v tomto případě měl být: // return this.stringVar; návrat getStringVar (); } / ** * Vypočítejte faktoriál zadaného celého čísla. Tato metoda závisí na * rekurzi. * * @param number Číslo, jehož faktoriál je požadován. * @return Faktoriální hodnota poskytnutého čísla. * / public int countFactorial (konečné int číslo) {// UPOZORNĚNÍ: Pokud bude zadáno číslo menší než nula, skončí to špatně. // Zde je zobrazen lepší způsob, jak to udělat, ale komentován. // návratové číslo <= 1? 1: number * countFactorial (number-1); návratové číslo == 1? 1: number * countFactorial (number-1); } / ** * Tato metoda ukazuje, jak neúmyslná rekurze často vede k * StackOverflowError, protože pro * nezamýšlenou rekurzi není poskytována žádná podmínka ukončení. * / public void runUnintentionalRecursionExample () {final String unusedString = this.getStringVar (); } / ** * Tato metoda ukazuje, jak může neúmyslná rekurze jako součást cyklické * závislosti vést k StackOverflowError, pokud nebude pečlivě respektována. * / public void runUnintentionalCyclicRecusionExample () {final State newMexico = State.buildState ("New Mexico", "NM", "Santa Fe"); System.out.println ("Nově vytvořený stát je:"); System.out.println (newMexico); } / ** * Ukazuje, jak i zamýšlená rekurze může mít za následek StackOverflowError *, když není nikdy * splněna podmínka ukončení rekurzivní funkce. * / public void runIntentionalRecursiveWithDysfunctionalTermination () {final int numberForFactorial = -1; System.out.print ("Faktoriál" + numberForFactorial + "je:"); System.out.println (CalculateFactorial (numberForFactorial)); } / ** * Napište hlavní možnosti této třídy do poskytnutého OutputStream. * * @param out OutputStream, do kterého se mají zapsat možnosti této testovací aplikace. * / public static void writeOptionsToStream (konečný výstup OutStream) {final String option1 = "1. Neúmyslná (žádná podmínka ukončení) rekurze jedné metody"; final String option2 = "2. Neúmyslná (bez podmínky ukončení) cyklická rekurze"; final String option3 = "3. Vadná rekurze ukončení"; 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 ("(Nelze zapisovat na poskytnutý OutputStream)"); System.out.println (volba1); System.out.println (volba2); System.out.println (volba3); }} / ** * Hlavní funkce pro spuštění StackOverflowErrorDemonstrator. * / public static void main (final String [] argumenty) {if (argumenty.lenka <1) {System.err.println ("Musíte uvést argument a ten jediný argument by měl být"); System.err.println ("jedna z následujících možností:"); writeOptionsToStream (System.err); System.exit (-1); } volba int = 0; try {option = Integer.valueOf (argumenty [0]); } catch (NumberFormatException notNumericFormat) {System.err.println ("Zadali jste nečíselnou (neplatnou) možnost [" + argumenty [0] + "]"); writeOptionsToStream (System.err); System.exit (-2); } final StackOverflowErrorDemonstrator me = nový StackOverflowErrorDemonstrator (); switch (option) {case 1: me.runUnintentionalRecursionExample (); přestávka; případ 2: me.runUnintentionalCyclicRecusionExample (); přestávka; případ 3: me.runIntentionalRecursiveWithDysfunctionalTermination (); přestávka; výchozí: System.err.println ("Poskytli jste neočekávanou možnost [" + volba + "]"); }}} 

Třída výše ukazuje tři typy neomezené rekurze: náhodná a zcela nezamýšlená rekurze, nezamýšlená rekurze spojená se záměrně cyklickými vztahy a zamýšlená rekurze s nedostatečnou podmínkou ukončení. Každá z nich a jejich výstupy jsou diskutovány dále.

Zcela nezamýšlená rekurze

Mohou nastat situace, kdy dojde k rekurzi bez jakéhokoli záměru. Běžnou příčinou může být náhodná volání metody. Například není příliš obtížné být trochu příliš neopatrný a vybrat první doporučení IDE pro návratovou hodnotu pro metodu „get“, která by mohla skončit jako volání téže stejné metody! Toto je ve skutečnosti příklad uvedený ve třídě výše. The getStringVar () metoda se opakovaně volá až do StackOverflowError došlo. Výstup se zobrazí takto:

Výjimka v podprocesu „hlavní“ java.lang.StackOverflowError na adrese dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) na Dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getErrorDemonstrator.getErrorDemonstrator.get String stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) ve společnosti dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) .stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) v souboru dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) n.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) v 

Stopa zásobníku zobrazená výše je ve skutečnosti mnohokrát delší než ta, kterou jsem umístil výše, ale je to prostě stejný opakující se vzor. Protože se vzor opakuje, je snadné diagnostikovat, že řádek 34 třídy je původcem problému. Když se podíváme na tento řádek, zjistíme, že jde skutečně o tvrzení návrat getStringVar () který se opakovaně volá sám. V tomto případě si můžeme rychle uvědomit, že zamýšleným chováním bylo místo toho vrátit this.stringVar;.

Neúmyslná rekurze s cyklickými vztahy

Existují určitá rizika pro cyklické vztahy mezi třídami. Jedním z těchto rizik je větší pravděpodobnost spuštění do nezamýšlené rekurze, kde jsou cyklické závislosti nepřetržitě volány mezi objekty, dokud přetečení zásobníku. K prokázání toho používám další dvě třídy. The Stát třída a Město třídy mají cyklický relačníshiop, protože a Stát instance má odkaz na svůj kapitál Město a Město má odkaz na Stát ve kterém se nachází.

State.java

balíček dustin.examples.stackoverflow; / ** * Třída, která představuje stát a je záměrně součástí cyklického * vztahu mezi městem a státem. * / public class State {private static final String NEW_LINE = System.getProperty ("line.separator"); / ** Název státu. * / soukromé jméno řetězce; / ** Dvoupísmenná zkratka pro stát. * / soukromá zkratka řetězce; / ** Město, které je hlavním městem státu. * / soukromé město capitalCity; / ** * Static builder method that is the intended method for instantiation of me. * * @param newName Název nově vytvořeného státu. * @param newAbbreviation Dvoupísmenná zkratka státu. * @param newCapitalCityName Název hlavního města. * / public static State buildState (final String newName, final String newAbbreviation, final String newCapitalCityName) {final State instance = new State (newName, newAbbreviation); instance.capitalCity = nové město (newCapitalCityName, instance); návratová instance; } / ** * Parametrizovaný konstruktor přijímající data k naplnění nové instance státu. * * @param newName Název nově vytvořeného státu. * @param newAbbreviation Dvoupísmenná zkratka státu. * / private State (final String newName, final String newAbbreviation) {this.name = newName; this.abbreviation = newAbbreviation; } / ** * Poskytněte řetězcovou reprezentaci instance státu. * * @return My String zastoupení. * / @Override public String toString () {// UPOZORNĚNÍ: To skončí špatně, protože implicitně volá metodu City toString () // a metoda City toString () volá tuto metodu // State.toString (). návrat "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