Preljev i preljev u Javi

1. Uvod

U ovom ćemo uputstvu pogledati preljev i preljev numeričkih vrsta podataka u Javi.

Nećemo zalaziti dublje u teoretske aspekte - usredotočit ćemo se samo na to kada se to dogodi na Javi.

Prvo ćemo pogledati cjelobrojne tipove podataka, a zatim vrste podataka s pomičnom zarezom. I za jedno i za drugo vidjet ćemo kako možemo otkriti kada se dogodi prekomjerno ili prekomjerno preljevanje.

2. Preljev i podlijevanje

Jednostavno rečeno, preljev i preljev se događaju kada dodijelimo vrijednost koja je izvan dosega deklarirane vrste podataka varijable.

Ako je (apsolutna) vrijednost prevelika, nazivamo je preljevom, ako je vrijednost premala, nazivamo je premalom.

Pogledajmo primjer gdje pokušavamo dodijeliti vrijednost 101000 (a 1 s 1000 nule) na varijablu tipa int ili dvostruko. Vrijednost je prevelika za int ili dvostruko varijabla u Javi i doći će do prelijevanja.

Kao drugi primjer, recimo da pokušavamo dodijeliti vrijednost 10-1000 (što je vrlo blizu 0) varijabli tipa dvostruko. Ova je vrijednost premala za dvostruko varijabla u Javi i doći će do podlijevanja.

Pogledajmo što se u tim slučajevima događa u Javi detaljnije.

3. Cijeli tipovi podataka

Cjelobrojni tipovi podataka u Javi su bajt (8 bitova), kratak (16 bitova), int (32 bita) i dugo (64 bita).

Ovdje ćemo se usredotočiti na int vrsta podataka. Isto se ponašanje odnosi i na ostale vrste podataka, osim što se minimalna i maksimalna vrijednost razlikuju.

Cijeli broj tipa int u Javi može biti negativan ili pozitivan, što znači da sa svojih 32 bita možemo dodijeliti vrijednosti između -231 (-2147483648) i 231-1 (2147483647).

Klasa omota Cijeli broj definira dvije konstante koje sadrže ove vrijednosti: Cijeli broj.MIN_VALUE i Cijeli broj.MAX_VALUE.

3.1. Primjer

Što će se dogoditi ako definiramo varijablu m tipa int i pokušati dodijeliti preveliku vrijednost (npr., 21474836478 = MAX_VALUE + 1)?

Mogući ishod ovog zadatka je da vrijednost m bit će nedefinirano ili da će doći do pogreške.

Oba su valjani ishodi; međutim, u Javi vrijednost m bit će -2147483648 (minimalna vrijednost). S druge strane, ako pokušamo dodijeliti vrijednost -2147483649 (= MIN_VRIJEDNOST - 1), m bit će 2147483647 (maksimalna vrijednost). To se ponašanje naziva integer-wraparound.

Razmotrimo sljedeći isječak koda da bismo bolje ilustrirali ovo ponašanje:

int vrijednost = Integer.MAX_VALUE-1; za (int i = 0; i <4; i ++, vrijednost ++) {System.out.println (vrijednost); }

Dobit ćemo sljedeći izlaz, koji pokazuje preljev:

2147483646 2147483647 -2147483648 -2147483647 

4. Rukovanje preljevom i preljevom cijelih vrsta podataka

Java ne donosi iznimku kada dođe do prelijevanja; zato može biti teško pronaći pogreške koje nastaju zbog prelijevanja. Niti možemo izravno pristupiti zastavi preljeva, koja je dostupna u većini procesora.

Međutim, postoje različiti načini za rješavanje mogućeg preljeva. Pogledajmo nekoliko ovih mogućnosti.

4.1. Upotrijebite drugu vrstu podataka

Ako želimo dopustiti vrijednosti veće od 2147483647 (ili manje od -2147483648), možemo jednostavno koristiti dugo tip podataka ili a BigInteger umjesto toga.

Iako varijable tipa dugo može se i prelijevati, minimalne i maksimalne vrijednosti su puno veće i vjerojatno su dovoljne u većini situacija.

Raspon vrijednosti od BigInteger nije ograničen, osim količinom memorije dostupne JVM-u.

Pogledajmo kako prepisati naš gornji primjer sa BigInteger:

BigInteger largeValue = novi BigInteger (Integer.MAX_VALUE + ""); za (int i = 0; i <4; i ++) {System.out.println (largeValue); largeValue = largeValue.add (BigInteger.ONE); }

Vidjet ćemo sljedeći izlaz:

2147483647 2147483648 2147483649 2147483650

Kao što možemo vidjeti u izlazu, ovdje nema preljeva. Naš članak BigDecimal i BigInteger u Java pokrovima BigInteger detaljnije.

4.2. Bacite iznimku

Postoje situacije u kojima ne želimo dopustiti veće vrijednosti, niti želimo da dođe do prelijevanja, a umjesto toga želimo izbaciti iznimku.

Od Jave 8, metode možemo koristiti za točne aritmetičke operacije. Pogledajmo prvo primjer:

int vrijednost = Integer.MAX_VALUE-1; za (int i = 0; i <4; i ++) {System.out.println (vrijednost); vrijednost = Math.addExact (vrijednost, 1); }

Statička metoda addExact () izvodi normalno dodavanje, ali baca iznimku ako operacija rezultira preljevom ili podlijevanjem:

2147483646 2147483647 Iznimka u niti "main" java.lang.ArithmeticException: cjelobrojni preljev na java.lang.Math.addExact (Math.java:790) na baeldung.underoverflow.OverUnderflow.main (OverUnderflow.java:115)

Pored addExact (), Matematika paket u Javi 8 pruža odgovarajuće točne metode za sve aritmetičke operacije. Popis dokumentacije za Java potražite u popisu svih ovih metoda.

Nadalje, postoje točne metode pretvorbe, koje izuzimaju iznimku ako dođe do prelijevanja tijekom pretvorbe u drugu vrstu podataka.

Za pretvorbu iz a dugo do an int:

javni statički int toIntExact (dugi a)

I za pretvorbu iz BigInteger do an int ili dugo:

BigInteger largeValue = BigInteger.TEN; long longValue = largeValue.longValueExact (); int intValue = largeValue.intValueExact ();

4.3. Prije Jave 8

Točne aritmetičke metode dodane su u Javu 8. Ako koristimo stariju verziju, te metode možemo jednostavno sami stvoriti. Jedna od mogućnosti za to je implementacija iste metode kao u Javi 8:

javni statički int addExact (int x, int y) {int r = x + y; if (((x ^ r) & (y ^ r)) <0) {baciti novo ArithmeticException ("int overflow"); } povratak r; }

5. Necjeloviti tipovi podataka

Tipovi koji nisu cijeli plutati i dvostruko ne ponašaju se na isti način kao cjelobrojni tipovi podataka kada su u pitanju aritmetičke operacije.

Jedna je razlika u tome što aritmetičke operacije nad brojevima s pomičnom zarezom mogu rezultirati a NaN. Imamo posvećeni članak o NaN-u na Javi, pa ga u ovom članku nećemo dalje istraživati. Nadalje, ne postoje egzaktne aritmetičke metode poput addExact ili pomnožiEgzaktno za necjelobrojne tipove u Matematika paket.

Java slijedi IEEE standard za aritmetiku s pomičnom zarezom (IEEE 754) plutati i dvostruko vrste podataka. Ovaj je standard osnova za način na koji Java obrađuje prekomjerni i nedovoljni protok brojeva s pomičnom zarezom.

U odjeljcima u nastavku usredotočit ćemo se na prekomjerni i nedovoljni protok dvostruko tip podataka i što možemo učiniti za rješavanje situacija u kojima se javljaju.

5.1. Prelijevanje

Što se tiče cjelobrojnih vrsta podataka, mogli bismo očekivati ​​sljedeće:

assertTrue (Double.MAX_VALUE + 1 == Double.MIN_VALUE);

Međutim, to nije slučaj s varijablama s pomičnom zarezom. Sljedeće je točno:

assertTrue (Double.MAX_VALUE + 1 == Double.MAX_VALUE);

To je zato što a dvostruko vrijednost ima samo ograničeni broj značajnih bitova. Povećamo li vrijednost velike dvostruko vrijednost samo jedan, ne mijenjamo nijedan bitni bit. Stoga vrijednost ostaje ista.

Ako vrijednost naše varijable povećamo tako da povećamo jedan od značajnih bitova varijable, varijabla će imati vrijednost BESKONAČNOST:

assertTrue (Double.MAX_VALUE * 2 == Double.POSITIVE_INFINITY);

i NEGATIVNA_BESKRAJNOST za negativne vrijednosti:

assertTrue (Double.MAX_VALUE * -2 == Double.NEGATIVE_INFINITY);

Možemo vidjeti da, za razliku od cijelih brojeva, ne postoji zaobilaženje, već dva različita moguća ishoda prelijevanja: vrijednost ostaje ista ili dobivamo jednu od posebnih vrijednosti, POZITIVNA_BESKRAJNOST ili NEGATIVNA_BESKRAJNOST.

5.2. Podlijevanje

Dvije su konstante definirane za minimalne vrijednosti a dvostruko vrijednost: MIN_VALUE (4.9e-324) i MIN_NORMALNO (2.2250738585072014E-308).

IEEE standard za aritmetiku s pomičnom zarezom (IEEE 754) detaljnije objašnjava pojedinosti o razlici između njih.

Usredotočimo se na to zašto nam uopće treba minimalna vrijednost za brojeve s pomičnim zarezom.

A dvostruko vrijednost ne može biti proizvoljno mala, jer imamo samo ograničeni broj bitova koji predstavljaju vrijednost.

Poglavlje o vrstama, vrijednostima i varijablama u specifikaciji jezika Java SE opisuje kako su predstavljeni tipovi s pomičnom zarezom. Minimalni eksponent za binarni prikaz a dvostruko daje se kao -1074. To znači da je najmanja pozitivna vrijednost koju dvojnik može imati Math.pow (2, -1074), što je jednako 4.9e-324.

Kao posljedica toga, preciznost a dvostruko u Javi ne podržava vrijednosti između 0 i 4.9e-324, ili između -4,9e-324 i 0 za negativne vrijednosti.

Dakle, što se događa ako varijabli tipa pokušamo dodijeliti premalu vrijednost dvostruko? Pogledajmo primjer:

za (int i = 1073; i <= 1076; i ++) {System.out.println ("2 ^" + i + "=" + Math.pow (2, -i)); }

S izlazom:

2 ^ 1073 = 1,0E-323 2 ^ 1074 = 4,9E-324 2 ^ 1075 = 0,0 2 ^ 1076 = 0,0 

Vidimo da ako dodijelimo premalu vrijednost, dobit ćemo podlijevanje, a rezultirajuća vrijednost je 0.0 (pozitivna nula).

Slično tome, za negativne vrijednosti, premali će rezultirati vrijednošću od -0.0 (negativna nula).

6. Otkrivanje podljeva i preljeva vrsta podataka s pomičnom zarezom

Budući da će prelijevanje rezultirati pozitivnom ili negativnom beskonačnošću, a nedovoljno pozitivnom ili negativnom nulom, ne trebaju nam točne aritmetičke metode kao za cjelobrojne tipove podataka. Umjesto toga, možemo provjeriti postoje li ove posebne konstante za otkrivanje prekomjernog i prekomjernog preljeva.

Ako u ovoj situaciji želimo izuzeti, možemo implementirati pomoćnu metodu. Pogledajmo kako to može tražiti potenciranje:

javni statički dvostruki powExact (dvostruka baza, dvostruki eksponent) {if (baza == 0,0) {return 0,0; } dvostruki rezultat = Math.pow (baza, eksponent); if (rezultat == Double.POSITIVE_INFINITY) {baciti novo ArithmeticException ("Dvostruki preljev rezultirajući POSITIVE_INFINITY"); } else if (rezultat == Double.NEGATIVE_INFINITY) {baciti novo ArithmeticException ("Dvostruki preljev rezultirajući NEGATIVE_INFINITY"); } else if (Double.compare (-0.0f, rezultat) == 0) {throw new ArithmeticException ("Dvostruki preljev rezultirajući negativnom nulom"); } else if (Double.compare (+ 0.0f, rezultat) == 0) {throw new ArithmeticException ("Dvostruko prelijevanje rezultiralo pozitivnom nulom"); } vratiti rezultat; }

U ovoj metodi trebamo se koristiti metodom Double.compored (). Normalni operateri usporedbe (< i >) ne razlikuju pozitivnu i negativnu nulu.

7. Pozitivno i negativno Nula

Na kraju, pogledajmo primjer koji pokazuje zašto trebamo biti oprezni u radu s pozitivnom i negativnom nulom i beskonačnošću.

Definirajmo nekoliko varijabli koje ćemo pokazati:

dvostruko a = + 0f; dvostruki b = -0f;

Jer pozitivno i negativno 0 smatraju se jednakima:

assertTrue (a == b);

Dok se pozitivna i negativna beskonačnost smatraju različitim:

assertTrue (1 / a == Double.POSITIVE_INFINITY); assertTrue (1 / b == Double.NEGATIVE_INFINITY);

Međutim, sljedeća je tvrdnja točna:

assertTrue (1 / a! = 1 / b);

Što se čini proturječjem našoj prvoj tvrdnji.

8. Zaključak

U ovom smo članku vidjeli što se prelijeva i prelijeva, kako se to može dogoditi u Javi i koja je razlika između vrsta podataka s cijelim brojem i s pomičnim zarezom.

Također smo vidjeli kako možemo otkriti prekomjerno i prekomjerno slijevanje tijekom izvršavanja programa.

Kao i obično, cjeloviti izvorni kod dostupan je na Githubu.