Overloop en onderstroom in Java

1. Inleiding

In deze zelfstudie kijken we naar de overloop en onderstroom van numerieke gegevenstypen in Java.

We zullen niet dieper ingaan op de meer theoretische aspecten - we zullen ons alleen concentreren op wanneer het gebeurt in Java.

Eerst kijken we naar gegevenstypen met gehele getallen en vervolgens naar gegevenstypen met drijvende komma. Voor beide zullen we ook zien hoe we kunnen detecteren wanneer over- of underflow optreedt.

2. Overloop en onderstroom

Simpel gezegd, overflow en underflow gebeuren wanneer we een waarde toewijzen die buiten het bereik van het gedeclareerde gegevenstype van de variabele valt.

Als de (absolute) waarde te groot is, noemen we het overloop, als de waarde te klein is, noemen we het onderloop.

Laten we eens kijken naar een voorbeeld waarin we proberen de waarde toe te wijzen 101000 (een 1 met 1000 nullen) naar een variabele van het type int of dubbele. De waarde is te groot voor een int of dubbele variabele in Java, en er zal een overflow zijn.

Laten we als tweede voorbeeld zeggen dat we proberen de waarde toe te kennen 10-1000 (die heel dicht bij 0 ligt) naar een variabele van het type dubbele. Deze waarde is te klein voor een dubbele variabele in Java, en er zal een onderstroom zijn.

Laten we eens in meer detail kijken wat er in Java gebeurt.

3. Geheel getal gegevenstypen

De gegevenstypen met gehele getallen in Java zijn byte (8 bits), kort (16 bits), int (32 bits), en lang (64 bits).

Hier zullen we ons concentreren op de int data type. Hetzelfde gedrag is van toepassing op de andere gegevenstypen, behalve dat de minimum- en maximumwaarden verschillen.

Een geheel getal van het type int in Java kan negatief of positief zijn, wat betekent dat we met zijn 32 bits waarden kunnen toewijzen tussen -231 (-2147483648) en 231-1 (2147483647).

De wrapper-klasse Geheel getal definieert twee constanten die deze waarden bevatten: Geheel getal.MIN_VALUE en Geheel getal.MAX_VALUE.

3.1. Voorbeeld

Wat gebeurt er als we een variabele definiëren? m van het type int en probeer een waarde toe te wijzen die te groot is (bijv. 21474836478 = MAX_VALUE + 1)?

Een mogelijke uitkomst van deze opdracht is dat de waarde van m zal ongedefinieerd zijn of dat er een fout zal zijn.

Beide zijn geldige resultaten; in Java is de waarde van m zal zijn -2147483648 (de minimumwaarde). Aan de andere kant, als we proberen een waarde toe te kennen van -2147483649 (= MIN_VALUE - 1), m zal zijn 2147483647 (de maximale waarde). Dit gedrag wordt integer-wraparound genoemd.

Laten we het volgende codefragment bekijken om dit gedrag beter te illustreren:

int waarde = Geheel getal.MAX_VALUE-1; voor (int i = 0; i <4; i ++, waarde ++) {System.out.println (waarde); }

We krijgen de volgende uitvoer, die de overloop laat zien:

2147483646 2147483647 -2147483648 -2147483647 

4. Omgaan met onder- en overloop van gegevenstypen met gehele getallen

Java genereert geen uitzondering wanneer er een overflow optreedt; daarom kan het moeilijk zijn om fouten te vinden die het gevolg zijn van een overflow. We hebben ook geen directe toegang tot de overloopvlag, die beschikbaar is in de meeste CPU's.

Er zijn echter verschillende manieren om met een mogelijke overloop om te gaan. Laten we eens kijken naar een aantal van deze mogelijkheden.

4.1. Gebruik een ander gegevenstype

Als we waarden groter dan willen toestaan 2147483647 (of kleiner dan -2147483648), kunnen we gewoon de lang gegevenstype of een BigInteger in plaats daarvan.

Hoewel variabelen van het type lang kan ook overstromen, de minimum- en maximumwaarden zijn veel groter en zijn waarschijnlijk voldoende in de meeste situaties.

Het waardebereik van BigInteger is niet beperkt, behalve door de hoeveelheid geheugen die beschikbaar is voor de JVM.

Laten we eens kijken hoe we ons bovenstaande voorbeeld kunnen herschrijven met BigInteger:

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

We zullen de volgende output zien:

2147483647 2147483648 2147483649 2147483650

Zoals we in de uitvoer kunnen zien, is er hier geen overloop. Ons artikel BigDecimal en BigInteger in Java-omslagen BigInteger meer gedetailleerd.

4.2. Gooi een uitzondering

Er zijn situaties waarin we geen grotere waarden willen toestaan, noch willen we dat er een overflow optreedt, en we willen in plaats daarvan een uitzondering genereren.

Vanaf Java 8 kunnen we de methoden gebruiken voor exacte rekenkundige bewerkingen. Laten we eerst naar een voorbeeld kijken:

int waarde = Geheel getal.MAX_VALUE-1; for (int i = 0; i <4; i ++) {System.out.println (waarde); waarde = Math.addExact (waarde, 1); }

De statische methode addExact () voert een normale optelling uit, maar genereert een uitzondering als de bewerking resulteert in een overloop of onderloop:

2147483646 2147483647 Uitzondering in thread "main" java.lang.ArithmeticException: integer overflow op java.lang.Math.addExact (Math.java:790) op baeldung.underoverflow.OverUnderflow.main (OverUnderflow.java:115)

In aanvulling op addExact (), de Wiskunde pakket in Java 8 biedt overeenkomstige exacte methoden voor alle rekenkundige bewerkingen. Zie de Java-documentatie voor een lijst van al deze methoden.

Verder zijn er exacte conversiemethoden, die een uitzondering opleveren als er een overflow is tijdens de conversie naar een ander datatype.

Voor de conversie van een lang aan een int:

openbare statische int toIntExact (lange a)

En voor de ombouw van BigInteger aan een int of lang:

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

4.3. Voordat Java 8

De exacte rekenmethoden zijn toegevoegd aan Java 8. Als we een eerdere versie gebruiken, kunnen we deze methoden eenvoudig zelf maken. Een optie om dit te doen is om dezelfde methode te implementeren als in Java 8:

openbare statische int addExact (int x, int y) {int r = x + y; if (((x ^ r) & (y ^ r)) <0) {throw new ArithmeticException ("int overflow"); } retourneer r; }

5. Gegevenstypen die geen geheel getal zijn

De niet-gehele getallen vlotter en dubbele gedragen zich niet op dezelfde manier als de gegevenstypen met gehele getallen als het gaat om rekenkundige bewerkingen.

Een verschil is dat rekenkundige bewerkingen op getallen met drijvende komma kunnen resulteren in een NaN. We hebben een speciaal artikel over NaN in Java, dus daar gaan we in dit artikel niet verder op in. Bovendien zijn er geen exacte rekenkundige methoden zoals addExact of vermenigvuldigen Exact voor niet-integer typen in de Wiskunde pakket.

Java volgt de IEEE Standard for Floating-Point Arithmetic (IEEE 754) voor zijn vlotter en dubbele gegevenstypen. Deze standaard is de basis voor de manier waarop Java omgaat met over- en underflow van drijvende-kommagetallen.

In de onderstaande secties zullen we ons concentreren op de over- en onderstroom van de dubbele gegevenstype en wat we kunnen doen om de situaties aan te pakken waarin ze zich voordoen.

5.1. Overloop

Wat betreft de gegevenstypen met gehele getallen, zouden we kunnen verwachten dat:

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

Dat is echter niet het geval voor variabelen met een drijvende komma. Het volgende is waar:

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

Dit komt doordat een dubbele waarde heeft slechts een beperkt aantal significante bits. Als we de waarde van een large verhogen dubbele waarde met slechts één, veranderen we geen van de significante bits. Daarom blijft de waarde hetzelfde.

Als we de waarde van onze variabele verhogen zodat we een van de significante bits van de variabele verhogen, krijgt de variabele de waarde ONEINDIGHEID:

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

en NEGATIVE_INFINITY voor negatieve waarden:

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

We kunnen zien dat er, in tegenstelling tot gehele getallen, geen omhullende is, maar twee verschillende mogelijke uitkomsten van de overflow: de waarde blijft hetzelfde, of we krijgen een van de speciale waarden, POSITIVE_INFINITY of NEGATIVE_INFINITY.

5.2. Onderstroom

Er zijn twee constanten gedefinieerd voor de minimumwaarden van a dubbele waarde: MIN_VALUE (4.9e-324) en MIN_NORMAL (2.2250738585072014E-308).

IEEE Standard for Floating-Point Arithmetic (IEEE 754) legt de details voor het verschil daartussen in meer detail uit.

Laten we ons concentreren op waarom we überhaupt een minimumwaarde nodig hebben voor getallen met drijvende komma.

EEN dubbele waarde kan niet willekeurig klein zijn, aangezien we maar een beperkt aantal bits hebben om de waarde weer te geven.

Het hoofdstuk over typen, waarden en variabelen in de Java SE-taalspecificatie beschrijft hoe typen met drijvende komma worden weergegeven. De minimale exponent voor de binaire weergave van a dubbele wordt gegeven als -1074. Dat betekent dat de kleinste positieve waarde die een dubbel kan hebben, is Math.pow (2, -1074), wat gelijk is aan 4.9e-324.

Als gevolg hiervan is de precisie van a dubbele in Java ondersteunt geen waarden tussen 0 en 4.9e-324, of tussen -4.9e-324 en 0 voor negatieve waarden.

Dus wat gebeurt er als we proberen een te kleine waarde toe te wijzen aan een variabele van het type dubbele? Laten we naar een voorbeeld kijken:

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

Met output:

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

We zien dat als we een waarde toekennen die te klein is, we een onderstroom krijgen, en de resulterende waarde is 0.0 (positief nul).

Evenzo zal voor negatieve waarden een underflow resulteren in een waarde van -0.0 (negatief nul).

6. Detectie van onder- en overloop van drijvende-kommagegevenstypen

Omdat overflow resulteert in positieve of negatieve oneindigheid en underflow in een positieve of negatieve nul, hebben we geen exacte rekenkundige methoden nodig zoals voor de gegevenstypen met gehele getallen. In plaats daarvan kunnen we controleren op deze speciale constanten om over- en underflow te detecteren.

Als we in deze situatie een uitzondering willen maken, kunnen we een hulpmethode implementeren. Laten we eens kijken hoe dat kan zoeken naar de machtsverheffing:

public static double powExact (double base, double exponent) {if (base == 0.0) {return 0.0; } dubbel resultaat = Math.pow (grondtal, exponent); if (result == Double.POSITIVE_INFINITY) {throw new ArithmeticException ("Double overflow resulteert in POSITIVE_INFINITY"); } else if (result == Double.NEGATIVE_INFINITY) {throw new ArithmeticException ("Double overflow resulteert in NEGATIVE_INFINITY"); } else if (Double.compare (-0.0f, result) == 0) {throw new ArithmeticException ("Double overflow resulteert in een negatieve nul"); } else if (Double.compare (+ 0.0f, result) == 0) {throw new ArithmeticException ("Double overflow resulteert in een positieve nul"); } resultaat teruggeven; }

Bij deze methode moeten we de methode gebruiken Double.compare (). De normale vergelijkingsoperatoren (< en >) maken geen onderscheid tussen positieve en negatieve nul.

7. Positief en negatief Nul

Laten we tot slot eens kijken naar een voorbeeld dat laat zien waarom we voorzichtig moeten zijn bij het werken met positieve en negatieve nul en oneindigheid.

Laten we een aantal variabelen definiëren om te demonstreren:

dubbel a = + 0f; dubbele b = -0f;

Omdat positief en negatief 0 worden als gelijk beschouwd:

assertTrue (a == b);

Terwijl positieve en negatieve oneindigheid als verschillend worden beschouwd:

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

De volgende bewering is echter correct:

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

Dat lijkt in tegenspraak met onze eerste bewering.

8. Conclusie

In dit artikel hebben we gezien wat over- en underflow is, hoe het kan voorkomen in Java en wat het verschil is tussen de gegevenstypen integer en floating-point.

We zagen ook hoe we over- en underflow konden detecteren tijdens de uitvoering van het programma.

Zoals gewoonlijk is de volledige broncode beschikbaar op Github.