Viliam Búr

5. 8. 2008

Reálne čísla v jazyku Java

V jazyku Java máme dva primitívne typy určené na prácu s reálnymi číslami: float a double. Ich veľkosť je 4 alebo 8 bajtov, z toho je 1 bit znamienko, 8 alebo 11 bitov exponent, a 23 alebo 52 bitov mantisa. Reálne čísla sa zaokrúhľujú na určitý počet platných číslic, čiže čím väčšie číslo, tým väčšie zaokrúhlenie. Ak teda hovoríme o ich rozsahu, máme na mysli dve nezávislé veci: na akú presnosť sa hodnoty zaokrúhľujú, a aké sú najväčšie a najmenšie možné hodnoty.

Týmto primitívnym typom zodpovedajú dva objektové typy: java.lang.Float a java.lang.Double. Ich statické premenné "MIN_VALUE" a "MAX_VALUE" určujú najmenšie a najväčšie konečné kladné reálne čísla vyjadriteľné pomocou daného typu.

System.out.println("Float " + Float.MIN_VALUE + " " + Float.MAX_VALUE);
// Float 1.4E-45 3.4028235E38
System.out.println("Double " + Double.MIN_VALUE + " " + Double.MAX_VALUE);
// Double 4.9E-324 1.7976931348623157E308

Zápis "1.4E-45" znamená "1.4 × 10-45", čiže 0.000 000 000 000 000 000 000 000 000 000 000 000 000 000 001 4. Zápis "3.4028235E38" znamená "3.4028235 × 1038", čiže 340 282 350 000 000 000 000 000 000 000 000 000 000; záverečné nuly znamenajú, že číslice na týchto miestach sa zaokrúhľujú. Podobne zápisy "4.9E-324" a "1.7976931348623157E308" znamenajú "4.9 × 10-324" a "1.7976931348623157 × 10308", čo nebudem rozpisovať, pretože výsledné čísla majú vyše 300 číslic.

Čo sa stane, ak je výsledok výpočtu príliš veľké alebo príliš malé číslo, a nezmestí sa do daného rozsahu? Oba reálne typy majú pomocné konštanty, ktoré umožňujú vrátiť aspoň nejaké výsledky aj za hranicami rozsahu výpočtu. Príliš veľké kladné a záporné čísla vyjadrujeme pomocou konštánt "POSITIVE_INFINITY" a "NEGATIVE_INFINITY", doslova "kladné nekonečno" a "záporné nekonečno". Čísla príliš blízko nule sa zaokrúhľujú na nulu (existuje aj záporná nula "-0.0f", ktorá sa rovná obyčajnej nule, okrem špeciálnych situácií ako napríklad delenie nulou). Ak o výsledku nevieme povedať vôbec nič, napríklad nekonečno mínus nekonečno alebo nula delená nulou, vyjadrujeme to pomocou konštanty "NaN". (Na rozdiel od objektovej konštanty "null" sú tieto konštanty typu "float" alebo "double", takže môžu byť uložené aj do premennej primitívneho typu.)

Ak chceme zistiť, či náš výpočet vrátil jednu z takýchto hodnôt, použijeme metódy "isInfinite" a "isNaN".

Prečo sa pri reálnych číslach používajú takéto špeciálne konštanty, keď pri celých číslach nastane za podobných okolností obyčajné pretečenie alebo "ArithmeticException"? Jedným dôvodom je zaokrúhľovanie reálnych čísel. Ak pri výpočte s celými číslami delíme nulou, zvyčajne to znamená, že programátor zabudol ošetriť nejaký špeciálny prípad; je dobré, ak ho program na túto situáciu upozorní. Ak pri výpočte s reálnymi číslami delíme nulou, často to znamená, že sme chceli deliť nejakou nenulovou hodnotou, ktorá sa pri medzivýpočte zaokrúhlila na nulu. Ošetrovanie všetkých takýchto situácií by bolo náročné, a v podstate zbytočné. Dobrý programátor predsa vie, že pri práci s reálnymi číslami sa zaokrúhľuje, takže výsledok výpočtu je vždy do istej miery nespoľahlivý; záleží od konkrétneho vzorca, nakoľko môže zaokrúhľovacia chyba narásť. Druhým dôvodom je, že spôsob uloženia reálnych čísel v pamäti umožňuje medzi ne "prirodzeným" spôsobom doplniť tieto konštanty; v prípade celých čísel by bol podobný postup "umelý", a okrem iného by extrémne spomaľoval všetky programy.

(C) 2008 Viliam Búr viliam-bur.blogspot.com

System.out.println(Float.MAX_VALUE * 2);  // Infinity
System.out.println(Float.MIN_VALUE / 2);  // 0.0
System.out.println(0.0f /  0.0f);  // NaN
System.out.println(1.0f /  0.0f);  // Infinity
System.out.println(1.0f / -0.0f);  // -Infinity
System.out.println(1.0f / Float.POSITIVE_INFINITY);  //  0.0
System.out.println(1.0f / Float.NEGATIVE_INFINITY);  // -0.0

Základné matematické operácie robíme znamienkami "+" sčítanie, "-" odčítanie, "*" násobenie, "/" delenie. Ak pri výpočtoch používame celé aj reálne čísla, znamienko "/" medzi dvoma celými číslami znamená celočíselné delenie, medzi dvoma reálnymi alebo reálnym a celým znamená reálne delenie.

System.out.println(1    / 3   );  // 0
System.out.println(1    / 3.0f);  // 0.333...
System.out.println(1.0f / 3   );  // 0.333...
System.out.println(1.0f / 3.0f);  // 0.333...

Čísla môžeme porovnávať pomocou operátorov "==" rovná sa, "!=" nerovná sa, "<" je menšie, ">" je väčšie, "<=" je menšie alebo rovné, ">=" je väčšie alebo rovné. Počas výpočtu sa reálne čísla zaokrúhľujú, takže výsledok operátora "==" môže závisieť od zaokrúhľovania. Pri dvoch reálnych číslach zvyčajne nezisťujeme ich rovnosť, ale či je ich rozdiel v danej tolerancii.

float a = 123456789f;
float b = 123000123f;
float c = 987987987f;
float x = a * b * c;
float y = c * b * a;
System.out.println(x);  // 1.5002796E25
System.out.println(y);  // 1.5002795E25

Trieda java.lang.Math ponúka množstvo metód na výpočty s celými aj reálnymi číslami: absolútna hodnota "abs" a znamienko "signum"; väčšie alebo menšie z dvoch čísel "min" a "max"; zaokrúhľovanie "floor", "ceil", "round"; mocniny "exp" a "pow", odmocniny "sqrt" a "cbrt", a logaritmy "log" a "log10"; premena uhlov zo stupňov na radiány "toRadians" a naopak "toDegrees"; goniometrické funkcie "sin", "cos", "tan", "asin", "acos", "atan", "atan2" a hyperbolické funkcie "sinh", "cosh", "tanh". Poskytuje aj matematické konštanty "E" a "PI".

Existuje aj trieda java.lang.StrictMath, ktorá ponúka rovnaké metódy, ale navyše so zárukou, že na rôznych platformách dá rovnaký výpočet vždy celkom rovnaký výsledok.

Náhodné čísla získame pomocou objektu java.util.Random. Metódy "nextFloat" a "nextDouble" vracajú náhodné reálne čísla rovnomerne rozložené medzi 0 a 1. Metóda "nextGaussian" vracia náhodné reálne čísla podľa Gaussovho normálneho rozloženia s priemerom 0 a smerodajnou odchýlkou 1; to znamená, že približne 68% hodnôt nebude od priemeru ďalej (ľubovoľným smerom) ako smerodajná odchýlka, 95% hodnôt nebude ďalej ako dve smerodajné odchýlky, a 99.7% hodnôt nebude ďalej ako tri smerodajné odchýlky. Napríklad pri IQ testoch je priemer 100 a smerodajná odchýlka 15, takže tu je simulátor výsledkov IQ testov náhodných jedincov v populácii:

java.util.Random r = new java.util.Random();
for (int i = 0; i < 100; i++) {
  System.out.println(Math.round(100 + 15 * r.nextGaussian()));
}

Súvisiace články:

Menovky:

2. 8. 2008

Celé čísla v jazyku Java

V jazyku Java máme štyri primitívne typy určené na prácu s celými číslami: byte, short, int, a long. Sú to celočíselné typy so znamienkom, s veľkosťou 1, 2, 4, 8 bajtov, čiže 8, 16, 32, 64 bitov. (Aj typ char určený na prácu s písmenami je v podstate 16-bitové celé číslo bez znamienka, a možno s ním robiť matematické operácie, ale kvôli prehľadnosti programu to neodporúčam robiť bezdôvodne.)

Základné matematické operácie robíme znamienkami "+" sčítanie, "-" odčítanie, "*" násobenie, "/" celočíselné delenie, "%" zvyšok po celočíselnom delení.

int a = 100;
int b = 6;

System.out.println(a + b);  // 106
System.out.println(a - b);  //  94
System.out.println(a * b);  // 600
System.out.println(a / b);  //  16
System.out.println(a % b);  //   4

Znamienko "+" okrem sčítania znamená aj spojenie reťazcov, a má rovnakú prioritu ako matematické "+" a "-". Ak pri vypisovaní výsledku výpočtu pripájame aj textový popis, pozor na prioritu znamienok; radšej okolo výpočtu pridajme zátvorky.

int x = 2;
int y = 3;

System.out.println("Spolu " + x + y);    // Spolu 23
System.out.println("Spolu " + (x + y));  // Spolu 5

Čísla môžeme porovnávať pomocou operátorov "==" rovná sa, "!=" nerovná sa, "<" je menšie, ">" je väčšie, "<=" je menšie alebo rovné, ">=" je väčšie alebo rovné.

int x = 2;
int y = 3;

if (x == y) {
 System.out.println("X sa rovna Y");   
}
if (x != y) {
 System.out.println("X sa nerovna Y");   
}
if (x < y) {
 System.out.println("X je mensie ako Y");   
}
if (x > y) {
 System.out.println("X je vacsie ako Y");   
}
if (x <= y) {
 System.out.println("X je mensie alebo rovne Y");   
}
if (x >= y) {
 System.out.println("X je vacsie alebo rovne Y");   
}

Každému z týchto primitívnych typov zodpovedá objektový typ: java.lang.Byte, java.lang.Short, java.lang.Integer, java.lang.Long; tieto typy sú odvodené od abstraktného typu java.lang.Number. Vďaka objektovým typom môžeme celé čísla používať tam, kde sa vyžadujú objekty, napríklad pri množinách a zoznamoch. Objektové typy poskytujú aj niekoľko užitočných statických metód. Z primitívnej hodnoty vyrobíme objekt pomocou statickej metódy valueOf; z objektu vyrobíme primitívnu hodnotu pomocou metód byteValue, shortValue, intValue, longValue.

int i = 5;
Integer j = Integer.valueOf(i);
int k = j.intValue();

System.out.println(i);  // 5
System.out.println(j);  // 5
System.out.println(k);  // 5

Jazyk Java umožňuje takzvaný autoboxing, čiže automatické dopĺňanie metód na premenu primitívnej hodnoty na objektovú a naopak. Predchádzajúci príklad by sa dal napísať aj jednoduchšie, ale znamenalo by to to isté.

int i = 5;
Integer j = i;
int k = j;

System.out.println(i);  // 5
System.out.println(j);  // 5
System.out.println(k);  // 5

S autoboxingom treba zaobchádzať opatrne. Lepšie je používať metódy explicitne, môžeme sa tak vyhnúť hľadaniu veľmi nenápadných chýb. Premenná typu "Integer" môže obsahovať hodnotu null, premenná typu "int" vždy obsahuje číslo; ak na to zabudneme, môže pri premene nastať výnimka NullPointerException.

Pri používaní typov "Integer" a "int" sa líši aj význam znamienka "==". Pre primitívne typy znamienko "==" porovnáva čísla. Pre objektové typy znamienko "==" testuje, či je to ten istý objekt. Ak teda máme dva rôzne objekty, ktoré vyjadrujú rovnaké číslo, znamienko "==" vráti, že sú rôzne; rovnosť obsahu zistíme pomocou metódy "equals". Pri dvoch rôznych primitívnych typoch, napríklad "int" a "long" znamienko "==" porovná, či obsahujú rovnaké číslo; pri objektoch "Integer" a "Long" bude metóda "equals" vždy vracať negatívny výsledok. Pri práci s číslami ako objektmi treba dávať veľký pozor, čo vlastne porovnávame.

(C) 2008 Viliam Búr viliam-bur.blogspot.com

int i = 5;
int j = 5;
if (i == j) {  // ano
 System.out.println("I a J sa rovnaju");
}

Integer I = new Integer(5);
Integer J = new Integer(5);
if (I == J) {  // nie
 System.out.println("I a J su ten isty objekt");
}
if (I.equals(J)) {  // ano
 System.out.println("I a J su rovnake");
}

int x = 5;
long y = 5;
if (x == y) {  // ano
 System.out.println("X a Y sa rovnaju");
}

Number X = new Integer(5);
Number Y = new Long(5);
if (X == Y) {  // nie
 System.out.println("X a Y su ten isty objekt");
}
if (X.equals(Y)) {  // nie!
 System.out.println("X a Y su rovnake");
}
if (X.longValue() == Y.longValue()) {  // ano
 System.out.println("X a Y sa rovnaju");
}

Rozsah jednotlivých celočíselných typov si môžeme vypočítať podľa počtu ich bitov, alebo použijeme ich statické premenné "MIN_VALUE" a "MAX_VALUE".

System.out.println("Byte " + Byte.MIN_VALUE + " " + Byte.MAX_VALUE);
// Byte -128 127
System.out.println("Short " + Short.MIN_VALUE + " " + Short.MAX_VALUE);
// Short -32768 32767
System.out.println("Integer " + Integer.MIN_VALUE + " " + Integer.MAX_VALUE);
// Integer -2147483648 2147483647
System.out.println("Long " + Long.MIN_VALUE + " " + Long.MAX_VALUE);
// Long -9223372036854775808 9223372036854775807

Ak chceme číslo zapísať do reťazca, môžeme použiť metódu "toString" (existuje aj statická) alebo pomocou znamienka "+" pripojiť číslo k reťazcu (napríklad prázdnemu). Ak to chceme urobiť v inej ako desiatkovej sústave, použijeme statickú metódu "toString" s dvoma parametrami. Základ číselnej sústavy nesmie byť väčší ako "Character.MAX_RADIX", ale väčšinou potrebujeme základy od 2 do 16, a tie sú v poriadku. Pre najčastejšie základy existujú aj statické metódy "toBinaryString", "toOctalString", "toHexString" ktoré konvertujú parameter ako celé číslo bez znamienka.

int x = 123456789;
System.out.println(Integer.toHexString(x));   //  75bcd15
System.out.println(Integer.toString(x, 16));  //  75bcd15

int y = -123456789;
System.out.println(Integer.toHexString(y));   // f8a432eb
System.out.println(Integer.toString(y, 16));  // -75bcd15

Ak chceme naopak z reťazca získať číslo, použijeme statickú metódu "parseInt" ("parseByte",...) alebo "valueOf", podľa toho, či chceme dostať primitívny typ alebo objekt. (Ak reťazec obsahuje číslo bez znamienka, získané napríklad pomocou metódy "toHexString", môže nastať prekročenie rozsahu a výnimka "NumberFormatException".)

System.out.println(Integer.parseInt("cafe", 16));  // 51966

S celými číslami môžeme robiť aj bitové operácie: "&" logický súčin (obidva bity), "|" logický súčet (aspoň jeden bit), "^" exkluzívny logický súčet (práve jeden bit). Bity môžeme posúvať pomocou operátorov: "<<" posun doľava, ">>" posun doprava so zachovaním znamienka, ">>>" posun doprava s doplnením núl.

int i = Integer.parseInt("0011", 2);
int j = Integer.parseInt("0101", 2);

System.out.println(Integer.toBinaryString(i & j));  // 0001
System.out.println(Integer.toBinaryString(i | j));  // 0111
System.out.println(Integer.toBinaryString(i ^ j));  // 0110

int k = -100;

System.out.println(Integer.toBinaryString(k));
// 11111111111111111111111110011100
System.out.println(Integer.toBinaryString(k << 1));
// 11111111111111111111111100111000
System.out.println(Integer.toBinaryString(k >> 1));
// 11111111111111111111111111001110
System.out.println(Integer.toBinaryString(k >>> 1));
// 01111111111111111111111111001110

Náhodné celé čísla získame pomocou objektu java.util.Random. Tu máme simuláciu hádzania kockou:

java.util.Random r = new java.util.Random();
for (int i = 0; i < 100; i++) {
 System.out.println(1 + r.nextInt(6));
}

Súvisiace články:

Menovky: