Despre infinit și alte (ne)numere (I)

Ș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:

Dacă executăm acest program, numerele afișate sunt următoarele:

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.

Restul programului rămâne nemodificat. În urma executării sale am obține următoarele reprezentări:

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:

Avem acum reprezentări puțin mai clare:

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:

Rezultatul execuției noului program este:

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...

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:

Valorile afișate sunt:

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...

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...

Te-ar putea interesa și:

  • cdman83

    Astept cu nerabdare episoadele urmatoare depsre valori normalizate vs. nenormalizate, signaling vs. non-signaling NaN, etc.

    PS. Delphi-ul avea un obicei de a reseta flag-ul de semnalizare la operatii floating point, pe cand Visual C++ se astepta sa nu fie setat. Asa ca daca incarcai un DLL Delphi intr-o aplicatie Visual C++ (de exemplu ai scris un add-on pentru Explorer), aveai sanse bune ca aplicatia (Explorer-ul in cazul de fata) sa crape cu o eroare misterioasa :-).