23 grudnia 2011

Kod uzupełnień do dwóch w Javie (reprezentacja binarna liczb całkowitych)

Wszystko zaczęło się od mojego przedstawienia zmian w Javie 7, w pakiecie java.util.concurrent podczas 84 spotkania Warszawa JUG - Warszawski Eclipse DemoCamp 2011 - Java 7, JavaFX i Eclipse (prezentacja do pobrania jako JacekLaskowski-EclipseDemoCamp2011-ConcurrencyUtilitiesJava7-2011.11.08.pdf).

Wtedy zaczęła się moja przygoda ze szkieletem Fork/Join. Rozwiązywanie problemu przez zrównoleglanie jego mniejszych składowych wymaga właściwego sposobu podziału (zwykle po połowie) i tak do ustalonego, minimalnego poziomu jego złożoności, a właściwie braku, po którym rozwiązanie można obliczyć "siłowo" (element po elemencie, liniowo).

Kwestią, z którym zwykle zmagają się programiści korzystający z Fork/Join (czy dowolnego problemu rozwiązywanego przez algorytmy typu "dziel i zwycieżaj") to, w jaki sposób dzielić, aby samo dzielenie nie było na tyle skomplikowane, że zysk ze zrównoleglenia zostanie przez niego skonsumowany i ostatecznie wyjdziemy na przysłowiowe "zero".

I tu pojawia się przyczynek do tego wpisu - operator przesunięcia bez znaku w prawo >>> (ang. unsigned right shift) w Javie.

Nie jest to niczym odkrywczym w Javie 7, ale przy Fork/Join nabrał większego znaczenia. Mówiąc wprost, ja na niego zwróciłem uwagę właśnie przy Fork/Join. I tak kończy się w zasadzie rola Fork/Join, które nie będzie już przywoływane, bo posłużył wyłącznie jako tło do rozpoznania operatora przesunięcia i jak się później okazało reprezentacji liczb całkowitych w Javie.

Weźmy następujący problem: W jaki sposób wyznaczyć połowę pewnej liczby nieujemnej x?

Liczba dowolna, acz ustalona, x (teraz dopiero dojrzałem, aby dostrzec piękno tych słów) może być liczbą elementów w tablicy, w którym znajdują się elementy do przetworzenia.

Operator przesunięcia w prawo bez znaku >>> jest złożeniem << i >> w zależności od znaku lewego argumentu - dodatni to >>, a ujemny...cóż...tu sprawa się komplikuje i wygląda (n>>s)+(2<<~s) przy założeniu, że rozważamy n>>>s.

Pamiętam, kiedy dostrzegłem (a właściwie pokazano mi palcem), że n << 1 to po prostu n * 2. Innych zastosowań przesunięcia w lewo jeszcze nie odkryłem, ale pewnie są równie zabójcze dla mojego serca :)

Wtedy zacząłem zgłębiać reprezentację liczb całkowitych w Javie, bo w końcu działanie tych operatorów polega na przesunięciach binarnych.

Zadanie wprowadzające: Czy znasz zapis binarny ujemnej liczby całkowitej w Javie? Niech to będzie -1 lub Integer.MIN_VALUE.

Właśnie wtedy zreflektowałem się, jak niewiele wiem na ten temat. Niewiele?! Delikatnie powiedziane. Nic nie wiem! Zabrałem się za lekturę specyfikacji języka Java (tutaj niewiele znalazłem poza ogólnikami), aby skończyć na pojęciach reprezentacjach liczb ze znakiem i bez oraz kod uzupełnień do dwóch (ang. two's complement).

I tak od rozpoznawania Fork/Join przeszedłem do operatora przesunięcia, aby skończyć na reprezentacji liczb całkowitych w Javie.

W ten sposób powstała moja implementacja zamiany binarnego ciągu znaków (bez wiodącego 0b, które doszło w Javie 7) do postaci liczby całkowitej w zapisie dziesiętnym. Klasa nie jest krótka, ale starałem się dobierać nazwy do ich przeznaczenia, więc sądzę, że stosunkowo łatwo będzie zorientować się, co miałem na myśli.

Przede mną analiza kodów źródłowych java.lang.Integer, która dostarcza większości z poniższych metod. Niedługo więcej w temacie, bo nie czuję, abym go wyczerpał (a mam wrażenie, że jedynie zdrapałem wierzchnią warstwę). Oczekuję uwag i wskazówek od życzliwego czytelnika (w czasie świąt niegodnym nie podzielić się miłym słówkiem).

Gdybyśmy już się nie widzieli, życzę najlepszego z okazji nadchodzących Świąt Bożego Narodzenia. Baw się i świętuj, aby wypoczęty wrócić do dalszych prac poznawczych.
package pl.japila.java7;

public class BinaryToDecimalIntDemo {

    static final char ZERO = '0';
    static final char ONE = '1';

    public static void main(String[] args) {
        for (int number = Integer.MIN_VALUE; number <= Integer.MIN_VALUE + 0xFFFF; number++) {
            String negativeNumber = Integer.toBinaryString(number);
            int intInDecimal = BinaryToDecimalIntDemo.getIntInDecimal(negativeNumber);
            assert number == intInDecimal;
        }
        for (int number = Integer.MAX_VALUE; number >= Integer.MAX_VALUE - 0xFFFF; number--) {
            String negativeNumber = Integer.toBinaryString(number);
            int intInDecimal = BinaryToDecimalIntDemo.getIntInDecimal(negativeNumber);
            assert number == intInDecimal;
        }
    }

    static int getIntInDecimal(String binary) {
        int sum = 0;
        boolean negative = false;
        char[] bits = binary.toCharArray();
        if (bits.length == Integer.SIZE) {
            if (isNegative(bits)) {
                negative = true;
                bits = subtractOne(invertBits(removeSignBit(bits)));
            }
        }
        for (int i = bits.length - 1, j = 0; i >= 0; i--, j++) {
            sum += calculateValueInBinaryRepAt(j, bits[i]);
        }
        return negative ? (sum == 0 ? Integer.MIN_VALUE : -sum) : sum;
    }

    private static char[] subtractOne(final char[] bits) {
        char[] newBits = new char[bits.length];
        System.arraycopy(bits, 0, newBits, 0, newBits.length);
        int i = newBits.length - 1;
        if (newBits[i] == ONE) {
            newBits[i] = ZERO;
        } else {
            newBits[i] = ONE;
            for (int j = i - 1; j >= 0; j--) {
                if (newBits[j] == ZERO) {
                    newBits[j] = ONE;
                } else {
                    newBits[j] = ZERO;
                    break;
                }
            }
        }
        return newBits;
    }

    private static char[] invertBits(char[] bits) {
        char[] newBits = new char[bits.length];
        System.arraycopy(bits, 0, newBits, 0, newBits.length);
        for (int i = 0; i < newBits.length; i++) {
            newBits[i] = (newBits[i] == ZERO ? ONE : ZERO);
        }
        return newBits;
    }

    private static char[] removeSignBit(char[] bits) {
        char[] newBits = new char[bits.length - 1];
        System.arraycopy(bits, 1, newBits, 0, newBits.length);
        return newBits;
    }

    private static boolean isNegative(char[] bits) {
        return bits.length == Integer.SIZE && bits[0] == ONE;
    }

    private static double calculateValueInBinaryRepAt(int i, char c) {
        return Math.pow(2, i) * Character.getNumericValue(c);
    }

}

7 komentarzy:

  1. Cos to sie Jacku slabo kompiluje. W ostatniej chwili chyba przemianowales ShiftOperatorDemo na BinaryToDecimalIntDemo ale w metodach zostalo.

    Post w sam raz zeby rozruszac umysl zabity cholesterolem w Swieta. No wlasnie, Swieta, Swieta.. i po Swietach. Tymczasem zycze udanego Sylwestra i wielu wspanialych postow (na blogu) w Nowym Roku :)

    OdpowiedzUsuń
  2. Ech, faktycznie. Dzięki za zwrócenie uwagi! Już poprawiam.

    Jako uzupełnienie i próba zakamuflowania wpadki wieczorem nowy wpis na temat 2C. W końcu i mnie udało się zrozumieć niskopoziomowe rozwiązania reprezentacji bitowej liczb całkowitych w Javie, więc w nagrodę dla Ciebie, i Ty będziesz miał okazję :)

    Zachęcam do dalszych komentarzy.

    OdpowiedzUsuń
  3. Coś chyba jest nie tak, ponieważ w przypadku liczb ujemnych number nie jest równe intInDecimal.

    Poza tym mimo tego, że "starałeś się dobierać nazwy do ich przeznaczenia", to nazwy nie oddają chyba Twoich intencji.

    Przykład zmiennej:

    String negativeNumber = Integer.toBinaryString(number);
    ten napis zawiera binarną postać zmiennej number to dlaczego nazywasz zmienną negativeNumber a nie np: binaryString lub binaryNumber?

    Inny przykład, nazwy metod:

    subtractOne (po przeanalizowaniu metody wiem co robi, ale lepiej by było jakbym wiedział co robi przed tą analizą) oraz calculateValueInBinaryRepAt (tutaj nie wiem wcale, jaki jest związek nazwy metody z tym co ona robi :)).

    OdpowiedzUsuń
  4. Uwaga z nazewnictwem jak najbardziej słuszna - pewnie wynik zbyt małej dbałości o szczegóły, kiedy przygotowywałem ten przykład. Zawsze coś wyjdzie w praniu :(

    W przypadku negativeNumber w drugim wydaniu powinna brzmieć po prostu number, albo jak napisałeś binaryNumber, a subtractOne wynika z jej celu - odjęcia jedynki, ale skoro Tobie nic nie mówiła, to pewnie wielu innym również. Co proponujesz w zamian?

    calculate... to zmiany systemu binarnego na dziesiętny. Może calculateDecimalValueFromBinaryAt? Sugestie mile widziane.

    OdpowiedzUsuń
  5. Metoda "subtractOne" ma w argumencie tablice bitów, więc nazwa skojarzyła mi się z odjęciem jednego bitu na początku (lub końcu) tej tablicy. Może gdybyś nazwał argument "numberInBits" a nie bits to działanie metody byłoby bardziej jasne. Szukałem chwilę innej nazwy dla metody ale substractOne pasuje dobrze, tyle że w kontekście liczby a nie tablicy. Jak widać nazywanie zmiennych i metod nie jest trywialne :).

    Co do ostatniej metody "calculateValueInBinaryRepAt", to ona po prostu oblicza wartość liczby w systemie dziesiętnym dla podanej cechy i mantysy (a nie tajemniczych i oraz c), więc może by zapisać tą metodę tak:

    private static double calculateDecimalValue(int characteristic, char mantissa) {
    return Math.pow(2, characteristic) * Character.getNumericValue(mantissa);
    }

    to wszystko staje się jasne!


    Korzystając z okazji jeszcze się trochę przyczepię :)

    1. Dlaczego char[] bits a nie byte[] bits? Byte mniej zajmuje oraz nie wymaga konwersji w ostatniej metodzie (Character.getNumericValue(c)). Co prawda należy by napisać szybko własną metodę zmiany String na byte[] ale to wydaje się dość proste (wystarczy "przekopiować warunkowo" elementy binary.toCharArray()).

    2. Dlaczego metody i pola mają domyślny (pakietowy), zamiast prywatnego, modyfikator dostępu?


    Wiem, że Twój post nie miał na celu pokazania jak powinno pisać się czysty kod, ale patrząc codziennie na taki właśnie zagmatwany kod, człowiek marnuje mnóstwo czasu na zrozumienie o co w tym wszystkim chodzi. Z tego co kojarzę, Twoją stronę odwiedza sporo osób, pewnie część początkujących, więc przy okazji opisywania ciekawego zagadnienia, pokaż im jak należy pisać czysty i ładny kod ;)

    OdpowiedzUsuń
  6. Cześć Jacku,

    Przepraszam, że nie na temat, ale nurtuje mnie kilka pytań.

    1). Czy podchodziłeś do wersji beta egzaminu Oracle?

    2). Kiedy pojawi się książka 'cookbook', której jesteś recenzentem?

    3). Pytanie odbiegające od pozostałych: kiedy na Twoim blogu pojawi się takie logo:
    http://tinyurl.com/73er2le
    :).

    Pozdrawiam.

    OdpowiedzUsuń
  7. Cześć Łukasz,

    Ad 1. Nie. I chyba się nie wyrobię w czasie - właśnie przygotowuję się do wyjazdu do Słowenii, aby poprowadzić 5-dniowe szkolenie z BPM 7.5 i mnie to za bardzo angażuje czasowo. Mówi się trudno.

    Ad 2. Rozumiem, że pytasz o Java 7 New Features Cookbook? Jeśli tak, to oby nie za wcześnie - miałem tyle uwag, że modlę się, aby autorzy jeszcze nad tym popracowali. Mnie ten styl nie przypadł do gustu, ale rozumiem, że innym wręcz przeciwnie.

    Ad 3. Tutaj to trzeba mieć znajomości, a mnie daleko do posiadania ich właściwej liczby :) Uważam, że ludzie posiadający to logo mają duże rozpoznanie w świecie, a mnie do niego (ponownie) daleko. Nie te progi.

    OdpowiedzUsuń