Știm (sau ar trebui să știm) că numerele zecimale sunt reprezentate cu ajutorul așa numitei virgule mobile. Un astfel de număr va avea un semn, un exponent și mantisă și există o formulă care ne permite să determinăm valoarea pe baza acestor trei componente. Există și cazuri speciale; toate detaliile legate de astfel de numere sunt specificate în standardul IEEE 754 (și acest standard este mai mult sau mai puțin respectat de către compilatoare).
Nu vom prezenta detalii legate de standard, ci ne vom referi doar asupra unor aspecte interesante, mai puțin cunoscute.
Reprezentarea
Pentru exemplificare vom folosi limbajul Java. Compilatoarele Java încearcă să respecte standardul "la sânge", deci riscul să avem surprize este mic. În Java avem tipurile float și double pentru a lucra cu numere zecimale. Pentru a nu avea reprezentări lungi, vom folosi float în exemplele noastre. E la fel pentru double, doar că avem mai mulți biți.
Clasa Float ne permite să convertim un număr float (reprezentat pe 32 de biți) într-un int care are exact aceeași reprezentare binară; metoda folosită este floatToIntBits. Iată câteva exemple:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Exemplu { private static int reprezentare(float value) { return Float.floatToIntBits(value); } public static void main (String[] args) { System.out.println(reprezentare(0f)); System.out.println(reprezentare(2457f)); System.out.println(reprezentare(4.7f)); System.out.println(reprezentare(-1.3f)); } } |
Dacă executăm acest program, numerele afișate sunt următoarele:
1 2 3 4 |
0 1159303168 108359843 -1079613850 |
Nu se înțelege mare lucru. Din fericire clasa Integer ne oferă posibilitatea să obținem o reprezentare binară cu ajutorul metodei toBinaryString. Putem transforma metoda noastră astfel încât să returneze un string care să conțină reprezentarea binară a numerelor.
1 2 3 |
private static String reprezentare(float value) { return Integer.toBinaryString(Float.floatToIntBits(value)); } |
Restul programului rămâne nemodificat. În urma executării sale am obține următoarele reprezentări:
1 2 3 4 |
0 1000101000110011001000000000000 1000000100101100110011001100110 10111111101001100110011001100110 |
E mai bine, dar lipsesc zerourile din față. Le putem adăuga folosind metoda format a clasei String; aceasta adaugă spații pe care le vom înlocui cu zerouri. Funcția noastră devine:
1 2 3 4 |
private static String reprezentare(float value) { return String.format("%32s", Integer.toBinaryString( Float.floatToIntBits(value))).replace(' ', '0'); } |
Avem acum reprezentări puțin mai clare:
1 2 3 4 |
00000000000000000000000000000000 01000101000110011001000000000000 01000000100101100110011001100110 10111111101001100110011001100110 |
Zerouri
Primul aspect interesant este existența a două valori pentru 0; una "pozitivă" și una "negativă". Diferă doar bitul de semn. Să modificăm metoda main astfel:
1 2 3 4 5 6 |
public static void main (String[] args) { System.out.println(reprezentare(-1.0f * 0f)); System.out.println(reprezentare(-0f)); System.out.println(reprezentare(0)); System.out.println(reprezentare(-0)); } |
Rezultatul execuției noului program este:
1 2 3 4 |
00000000000000000000000000000000 10000000000000000000000000000000 00000000000000000000000000000000 00000000000000000000000000000000 |
Poate v-ați fi așteptat ca rezultatele executării liniilor 3 și 5 să fie identice - nu este așa. În primul caz avem un zero "negativ" pentru care am specificat că avem un float (adăugând sufixul f). În al doilea caz avem un int; Cum în cazul numerele întregi nu avem mai multe reprezentări ale lui zero, reprezentarea este aceeași; abia apoi urmează conversia la float și obținem un zero "pozitiv".
Există mai multe posibilități de a obține zerouri negative. Una dintre ele (probabil cea mai simplă) este înmulțirea unui număr negativ cu un zero pozitiv.
Dar, sunt cele două zerouri egale? Să vedem...
1 2 3 4 5 |
public static void main (String[] args) { System.out.println(0.0f == -0.0f); System.out.println(new Float(0f) == new Float(-0f)); System.out.println(new Float(0f).equals(new Float(-0f))); } |
Pentru linia 2 se va afișa true, pentru liniile 3 și 4 se va afișa false. Pentru tipurile primitive, cele două zerouri sunt egale, așa cum ne-am aștepta.
Pentru Float e mai complicat; e simplu pentru linia 3: avem două obiecte diferite și operatorul == returnează true doar dacă ambii operanzi ar fi același obiect. Chiar dacă valorile ar fi fost cu adevărat egale, tot am fi avut rezultatul false; nu se schimbă nimic dacă ambele zerouri ar fi fost "pozitive".
Totuși, pentru linia 4 pare ciudat. Ar trebui ca equals să "facă ce trebuie". Dar... clasa Float folosește pe post de hash code reprezentarea pe biți a tipului primitiv, interpretată ca număr întreg. Cum cele două zerouri au reprezentările diferite, hash code-urile lor sunt și ele diferite. Pentru a vedea acest lucru, putem executa:
1 2 3 4 |
public static void main (String[] args) { System.out.println(new Float(0).hashCode()); System.out.println(new Float(-0f).hashCode()); } |
Valorile afișate sunt:
1 2 |
0 -2147483648 |
Două obiecte care au hash code-uri diferite nu pot fi egale (s-ar încălca un principiu de bază: două obiecte egale au întotdeauna același hash code). Dacă ar fi, colecțiile bazate pe hash code-uri nu ar mai funcționa corect.
Dar, dacă cele două zerouri nu sunt egale, care e mai mare? Niciunul! Să vedem...
1 2 3 4 5 6 7 8 9 10 |
public static void main (String[] args) throws java.lang.Exception { Float positiveZero = 0.0f; Float negativeZero = -0.0f; System.out.println(positiveZero < negativeZero); System.out.println(positiveZero > negativeZero); System.out.println(positiveZero <= negativeZero); System.out.println(positiveZero >= negativeZero); System.out.println(positiveZero != negativeZero); } |
Explicația e simplă pentru primele patru cazuri: pentru acești operatori are loc un unboxing înainte de comparare; practic se vor compara valorile primitive. Nu același lucru se întâmplă în cazul operatorului !=; dacă îl folosim pe acesta, se vor compara obiectele (care nu sunt identice în cazul nostru).
Va urma
În cadrul episodului următor ne vom ocupa cu adevărat de infinituri...