Ohjelmointi

StackOverflowErrorin diagnosointi ja ratkaiseminen

Viimeaikainen JavaWorld-yhteisöfoorumiviesti (Stack Overflow uuden objektin luomisen jälkeen) muistutti minua siitä, että Java-uudet ihmiset eivät aina ymmärrä StackOverflowErrorin perusteita. Onneksi StackOverflowError on yksi helpommista ajonaikaisista virheistä virheenkorjaamiseen, ja tässä blogikirjoituksessa osoitan, kuinka helppoa on usein diagnosoida StackOverflowError. Huomaa, että pinon ylivuoto ei ole rajoitettu Java-sovellukseen.

StackOverflowError-syyn diagnosointi voi olla melko suoraviivaista, jos koodi on käännetty virheenkorjausasetuksella, jotta rivinumerot ovat käytettävissä tuloksena olevassa pinon jäljityksessä. Tällaisissa tapauksissa on tyypillisesti kyse vain toistuvien viivojen numeroiden löytämisestä pinon jäljityksestä. Toistuvien rivinumeroiden kuvio on hyödyllinen, koska StackOverflowError johtuu usein loputtomasta rekursiosta. Toistuvat rivinumerot osoittavat koodin, jota kutsutaan suoraan tai epäsuorasti rekursiivisesti. Huomaa, että muissa tilanteissa kuin rajattomassa rekursiossa voi esiintyä pinon ylivuotoa, mutta tämä blogikirjoitus on rajoitettu StackOverflowError johtuu rajattomasta rekursiosta.

Recursion suhde on mennyt huonoon StackOverflowError on mainittu StackOverflowErrorin Javadoc-kuvauksessa, jossa todetaan, että tämä virhe on "heitetty, kun pinon ylivuoto tapahtuu, koska sovellus toistuu liian syvälle". On merkittävää, että StackOverflowError päättyy sanaan Virhe ja on virhe (laajentaa java.lang.Error kautta java.lang.VirtualMachineError) pikemminkin kuin tarkistettu tai ajonaikainen poikkeus. Ero on merkittävä. Virhe ja Poikkeus ovat kukin erikoistuneita heitettäviä, mutta niiden suunniteltu käsittely on melko erilaista. Java-opetusohjelma huomauttaa, että virheet ovat tyypillisesti Java-sovelluksen ulkopuolisia, joten sovellus ei tavallisesti voi eikä pidä tarttua niihin.

Näytän törmäämisen StackOverflowError rajoittamattoman rekursion kautta kolmella eri esimerkillä. Näissä esimerkeissä käytetty koodi sisältyy kolmeen luokkaan, joista ensimmäinen (ja pääluokka) esitetään seuraavaksi. Luettelon kaikki kolme luokkaa kokonaisuudessaan, koska rivinumerot ovat merkittäviä StackOverflowError.

StackOverflowErrorDemonstrator.java

paketti dustin.examples.stackoverflow; tuo java.io.IOException; tuo java.io.OutputStream; / ** * Tämä luokka osoittaa erilaisia ​​tapoja, joilla StackOverflowError * voi esiintyä. * / public class StackOverflowErrorDemonstrator {private static final String NEW_LINE = System.getProperty ("line.separator"); / ** Mielivaltainen merkkijono-pohjainen tietojäsen. * / yksityinen merkkijono stringVar = ""; / ** * Yksinkertainen lisävaruste, joka näyttää tahattoman rekursio on mennyt pieleen. Kun * kutsutaan, tämä menetelmä kutsuu itseään toistuvasti. Koska rekursiota ei voida lopettaa * määritetyllä päättämisehdolla, on odotettavissa * StackOverflowError. * * @return String -muuttuja. * / public String getStringVar () {// // VAROITUS: // // Tämä on huono! Tämä kutsuu itseään rekursiivisesti, kunnes pino // ylittää ja StackOverflowError heitetään. Tarkoitettu rivi // tässä tapauksessa olisi pitänyt olla: // return this.stringVar; palauta getStringVar (); } / ** * Laske toimitetun kokonaisluvun kerroin. Tämä menetelmä perustuu * rekursioon. * * @param numero Luku, jonka tekijä on haluttu. * @return Annetun luvun kerroinarvo. * / public int calcFactorial (lopullinen int-numero) {// VAROITUS: Tämä loppuu huonosti, jos annetaan alle nolla. // Parempi tapa tehdä tämä on esitetty tässä, mutta kommentoitu. // palautusluku <= 1? 1: numero * calcFactorial (luku-1); palautusluku == 1? 1: numero * calcFactorial (luku-1); } / ** * Tämä menetelmä osoittaa, kuinka tahaton rekursio johtaa usein * StackOverflowErroriin, koska * tahattomalle rekursiolle ei määritetä lopetusehtoja. * / public void runUnintentionalRecursionExample () {final String unusedString = this.getStringVar (); } / ** * Tämä menetelmä osoittaa, kuinka tahaton rekursio osana syklistä * riippuvuutta voi johtaa StackOverflowErroriin, ellei sitä noudateta huolellisesti. * / public void runUnintentionalCyclicRecusionExample () {final State newMexico = State.buildState ("New Mexico", "NM", "Santa Fe"); System.out.println ("Äskettäin rakennettu tila on:"); System.out.println (uusiMeksiko); } / ** * Osoittaa, kuinka edes suunniteltu rekursio voi johtaa StackOverflowError * -tekniikkaan, kun rekursiivisen toiminnon loppuehtoja ei koskaan * täytetä. * / public void runIntentionalRecursiveWithDysfunctionalTermination () {lopullinen int numeroForFactorial = -1; System.out.print ("" + numberForFactorial + "-kerroin on:"); System.out.println (calcFactorial (numberForFactorial)); } / ** * Kirjoita tämän luokan päävaihtoehdot toimitettuun OutputStreamiin. * * @param out OutputStream, johon kirjoitetaan tämän testisovelluksen vaihtoehdot. * / public static void writeOptionsToStream (final OutputStream out) {final String option1 = "1. Tahaton (ei lopetusehtoa) yhden menetelmän rekursio"; final String option2 = "2. Tahaton (ei lopetustilaa) syklinen rekursio"; final String option3 = "3. Virheellinen lopetusrekursio"; kokeile {out.write ((option1 + NEW_LINE) .getBytes ()); out.write ((vaihtoehto2 + NEW_LINE) .getBytes ()); out.write ((vaihtoehto3 + NEW_LINE) .getBytes ()); } catch (IOException ioEx) {System.err.println ("(ei voida kirjoittaa toimitettuun OutputStreamiin" "); System.out.println (vaihtoehto 1); System.out.println (vaihtoehto 2); System.out.println (vaihtoehto 3); }} / ** * Päätoiminto StackOverflowErrorDemonstratorin suorittamiseen. * / public static void main (viimeiset merkkijono [] argumentit) {if (argumentit.pituus <1) {System.err.println ("Sinun on annettava argumentti ja kyseisen yksittäisen argumentin tulisi olla"); System.err.println ("yksi seuraavista vaihtoehdoista:"); writeOptionsToStream (System.err); System.exit (-1); } int vaihtoehto = 0; kokeile {option = Integer.valueOf (argumentit [0]); } catch (NumberFormatException notNumericFormat) {System.err.println ("Annoit ei-numeerisen (virheellisen) vaihtoehdon [" + argumentit [0] + "]"); writeOptionsToStream (System.err); System.exit (-2); } final StackOverflowErrorDemonstrator me = uusi StackOverflowErrorDemonstrator (); kytkin (vaihtoehto) {tapaus 1: me.runUnintentionalRecursionExample (); tauko; tapaus 2: me.runUnententionalCyclicRecusionExample (); tauko; tapaus 3: me.runIntentionalRecursiveWithDysfunctionalTermination (); tauko; oletus: System.err.println ("Annoit odottamattoman vaihtoehdon [" + vaihtoehto + "]"); }}} 

Yllä oleva luokka osoittaa kolmea tyyppistä rajoittamatonta rekursiota: vahingossa tapahtuvaa ja täysin tahatonta rekursiota, tahattomaa rekursiota, joka liittyy tahallisesti syklisiin suhteisiin, ja tarkoitettua rekursiota, jonka lopetustila on riittämätön. Kutakin näistä ja niiden tuotosta keskustellaan seuraavaksi.

Täysin tahaton rekursio

Voi olla aikoja, jolloin rekursio tapahtuu ilman mitään tarkoitusta. Yleinen syy voi olla se, että menetelmä tapaa itsensä vahingossa. Esimerkiksi ei ole liian vaikeaa päästä hieman liian huolimattomaksi ja valita IDE: n ensimmäinen suositus palautusarvosta "get" -menetelmälle, joka saattaa päätyä kutsuksi juuri samalle menetelmälle! Tämä on itse asiassa esimerkki, joka on esitetty yllä olevassa luokassa. getStringVar () menetelmä kutsuu itseään toistuvasti, kunnes StackOverflowError on kohdattu. Tulos näkyy seuraavasti:

Poikkeus säikeessä "main" java.lang.StackOverflowError at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflatorEstackOverDownloader stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) on dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) on dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) on dustin.examples .stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) on dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) on dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) on dusti n.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar (StackOverflowErrorDemonstrator.java:34) osoitteessa 

Yllä esitetty pinon jälki on itse asiassa monta kertaa pidempi kuin mitä laitoin edellä, mutta se on yksinkertaisesti sama toistuva kuvio. Koska malli toistuu, on helppo diagnosoida, että luokan viiva 34 on ongelman aiheuttaja. Kun tarkastelemme sitä linjaa, näemme, että se on todellakin lausunto palauta getStringVar () joka lopulta kutsuu itseään toistuvasti. Tässä tapauksessa voimme nopeasti ymmärtää, että aiottu käyttäytyminen oli sen sijaan palauta tämä.merkkijonoVar;.

Tahaton rekursio syklisillä suhteilla

Syklisillä suhteilla luokkien välillä on tiettyjä riskejä. Yksi näistä riskeistä on suurempi todennäköisyys törmätä tahattomaan rekursioon, jossa syklisiä riippuvuuksia kutsutaan jatkuvasti objektien välillä, kunnes pino täyttyy. Tämän osoittamiseksi käytän vielä kahta luokkaa. Osavaltio luokka ja Kaupunki luokalla on sykliset suhteet, koska a Osavaltio esimerkiksi viittaus sen pääomaan Kaupunki ja a Kaupunki on viittaus Osavaltio jossa se sijaitsee.

Osavaltio.java

paketti dustin.examples.stackoverflow; / ** * Luokka, joka edustaa valtiota ja on tarkoituksellisesti osa syklistä * suhdetta kaupungin ja valtion välillä. * / public class State {private static final String NEW_LINE = System.getProperty ("line.separator"); / ** Valtion nimi. * / yksityinen merkkijono; / ** Kaksikirjaiminen lyhenne tilalle. * / yksityinen merkkijono lyhenne; / ** Kaupunki, joka on valtion pääkaupunki. * / yksityinen kaupungin pääkaupunki; / ** * Staattinen rakentaja -menetelmä, joka on tarkoitettu menetelmä minua ilmentämään. * * @param newName Äskettäin osoitetun valtion nimi. * @param newAbbreviation Kaksikirjaiminen valtion lyhenne. * @param newCapitalCityName Pääkaupungin nimi. * / public static State buildState (viimeinen merkkijono uusiNimi, viimeinen merkkijono uusiAbbreviation, lopullinen merkkijono newCapitalCityName) {lopullinen valtion instanssi = new State (uusiNimi, newAbbreviation); instance.capitalCity = uusi kaupunki (newCapitalCityName, instanssi); palautusilmentymä; } / ** * Parametroitu konstruktori, joka hyväksyy tietoja uuden valtion esiintymän täyttämiseksi. * * @param newName Äskettäin osoitetun valtion nimi. * @param newAbbreviation Kaksikirjaiminen valtion lyhenne. * / yksityinen tila (viimeinen merkkijono uusi nimi, viimeinen merkkijono uusi lyhenne) {this.name = newName; tämä. lyhenne = uusi lyhenne; } / ** * Anna merkkijono edustus valtion instanssille. * * @return My String -esitys. * / @Override public String toString () {// VAROITUS: Tämä loppuu huonosti, koska se kutsuu Cityn toString () // -metodia implisiittisesti ja City's toString () -metodi kutsuu tätä // State.toString () -metodiksi. palauta "StateName:" + this.name + NEW_LINE + "StateAbbreviation:" + this.abbreviation + NEW_LINE + "CapitalCity:" + this.capitalCity; }} 

Kaupunki.java