19 czerwca 2008

SCJP - Klasy opakowujące typów prostych

Dzisiejszy artykuł traktuje o klasach opakowujących dla typów prostych (ang. wrapper classes). Co prawda artykuł piszę z perspektywy przygotowań do SCJP – patrz "Przygotowania do SCJP czas zacząć" – ale coraz częściej zauważam, że seria czytana jest także przez osoby SCJP nie zainteresowane, więc może tak czy inaczej warto? Zapraszam.

Każdy typ prosty z języka Java posiada swój odpowiednik w postaci klasy w pakiecie 'java.lang'. Dla typu ‘int’ jest to ‘java.lang.Integer’, dla typu ‘char’ 'java.lang.Character' a dla pozostałych klasy nazywają się tak samo jak typ prosty, tyle, że nazwa klasy zaczyna się wielką literą. Dla przykładu, dla typu ‘float’ będzie to ‘java.lang.Float’. Do czego służą te klasy i dlaczego ich potrzebujemy? Z dwu powodów. Po pierwsze, aby móc używać wartości prymitywnych w operacjach, które wymagają obiektów. Zerknijmy na poniższy przykład.

public class Test {
public static void main(String[] args) {
Storage storage = new Storage();

int x = 1;

storage.addToStorage(new Integer(x));
}
}

class Storage {
private Set<Object> storage;

public void addToStorage(Object obj) {
this.storage.add(obj);
}
}

Deklarując kolekcję – zbiór – w klasie ‘Storage’ mieliśmy intencję taką, aby móc w tej kolekcji przechowywać dowolne byty. Tyle, że obiekty języka Java to nie są wszystkie byty jakie nas interesują – są jeszcze przecież wartości prymitywne, które obiektami nie są. Aby więc móc dodać wartość zmiennej ‘x’ typu ‘int’ do kolekcji musimy utworzyć obiekt, który tą wartość będzie reprezentował. W powyższym przykładzie jest to konstrukcja ‘new Integer(x)’. Co prawda odkąd mamy w Javie (a mamy począwszy od Javy 5) "in-boxing" i "out-boxing" (o których będzie mowa w kolejnych artykułach) nie musimy robić tego explicite, ale nie zmienia to faktu, że taka operacja musi być wykonywana.

Omawiane klasy – oprócz tego, że są obiektowymi opakowaniami dla typów prostych – udostępniają szereg przydatnych operacji konwersji. Jako operację konwersji możemy również postrzegać samą operację konstrukcji. Żadna z omawianych klas nie udostępnia konstruktora bez-argumentowego. Wszystkie za to udostępniają konstruktor jednoargumentowy z argumentem typu prostego odpowiadającego klasie, czyli np. ‘Boolean(boolean b)’ czy ‘Character(char c)’. Dodatkowo, wszystkie klasy oprócz ‘Character’ udostępniają konstruktor akceptujący obiekt typu ‘String’ – a więc jest to konwersja z typu napisowego. Wyjątkowo, klasa ‘Float’ implementuje jeszcze konstruktor, który akceptuje wartości typu ‘double’. Bardzo ważną rzeczą jest, że obiekty klas opakowujących reprezentują jedną konkretną wartość danego typu. Oznacza to, że wartość reprezentowana np. przez obiekt klasy ‘Integer’ nigdy nie może być zmieniona – nie ma operacji ‘setValue(int val)’ czy ‘increase()’. Z tego też powodu nie ma konstruktorów bezargumentowych – liczba czy wartość logiczna, albo litera nieposiadające wartości nie mają sensu, a późniejsze ustawienie tej wartości nie jest możliwe. Ilustruje to dobitnie poniższy przykład.

public static void main(String[] args) {
Integer i = new Integer(1);
Integer j = i;

if(i == j) // warunek jest spełniony
System.out.println("zmienne i oraz j reprezentują ten sam obiekt");

i = i - 1; // równoważne z instrukcją ‘i = new Integer(i.intValue() - 1)’

if(i == j) // warunek już nie jest spełniony
System.out.println("zmienne i oraz j nadal reprezentują ten sam obiekt");
}

Rezultatem uruchomienia tego programu będzie wypisanie tylko pierwszego ze zdań, albowiem po operacji ‘i = i - 1’ referencja ‘i’ nie odnosi się już do tego samego obiektu. Wartości reprezentowanej przez obiekt, do którego odnosi się zmienna ‘i’ nie da się zmienić, zatem trzeba utworzyć nowy obiekt. Instrukcja ‘i = i - 1‘ jest de facto równoważna instrukcji ‘i = new Integer(i.intValue() - 1)’. Na zakończenie tematu konstruktorów jeszcze słowo komentarza o konwersji z typu napisowego, a więc o konstruktorach akceptujących obiekt typu ‘String’. Wszystkie one – dla typów numerycznych, a więc z wyłączeniem klasy ‘Boolean’ – zgłaszają wyjątek 'NumberFormatException' w przypadku, gdy konwersja nie jest możliwa. Taki wyjątek otrzymamy np. jeśli spróbujemy uruchomić konstruktor ‘new Integer("jeden")’ – niestety, Java nie umie jeszcze czytać po polsku. W innych językach też nie umie, ogranicza się do czytania cyfr. Analogiczna konwersja dla typu ‘Boolean’ zawsze kończy się sukcesem, tj. konstruktor tej klasy nie zwraca wyjątków. Obiekt klasy ‘Boolean’ reprezentujący wartość ‘true’ uzyskamy z napisu "true", przy czym nie ważna jest wielkość liter, zaś obiekt reprezentujący wartość ‘false’ z każdego innego napisu, oraz gdy zamiast obiektu klasy ‘String’ przekażemy wartość ‘null’ (!). Uwaga! Instrukcja ‘new Boolean("1")’ także powoduje utworzenie obiektu reprezentującego wartość ‘false’.

Bliskim kuzynem konstruktorów są metody ‘valueOf(…)’. Są to statyczne metody-fabryki klas opakowujących, których zadaniem jest tworzenie instancji tychże klas. Jaka jest więc różnica między tymi metodami a konstruktorami? Różnica głównie tkwi w tym, że metody ‘valueOf(…)’ nie zawsze tworzą nowy obiekt. Zamiast tego – w celu optymalizacji – używane są istniejące, ale już nie wykorzystywane instancje. Szczegóły nie są dla nas ważne, ale trzeba zapamiętać, że jeśli nie zależy nam na utworzeniu NOWEGO obiektu to powinniśmy używać tych metod zamiast konstruktorów. Pomijając względy wydajnościowe – metody ‘valueOf(…)’ dają te same możliwości tworzenia obiektów, co konstruktory (poza tym, że nie ma metody ‘Float.valueOf(double d)’, ale to szczegół) a oprócz tego, pozwalają na parsowanie liczb zapisanych w systemie innym niż dziesiętny. Zerknijmy na poniższy przykład.

public static void main(String[] args) {
System.out.println(Integer.valueOf("100", 2));
}

Rezultatem uruchomienia tej metody będzie wypisanie liczby ‘4’, albowiem w systemie dziesiętnym taka jest właśnie wartość liczby ‘100’ zapisanej w systemie dwójkowym. Metody ‘valueOf(String value, int radix)’, służące do konwersji z jednoczesną zmianą systemu liczbowego są zaimplementowane tylko dla typów całkowitoliczbowych, a więc dla typów: Byte, Short, Integer i Long.

Bardzo podobne do metod ‘valueOf(…)’ są metody ‘parseXXX(…)’ gdzie ‘XXX’ to nazwa odpowiedniego typu prymitywnego. Są to również metody statyczne do konwersji ze String’ów, tyle, że do typów prymitywnych a nie instancji klas opakowujących. Metody ‘parseXXX(…)’ występują w dwu odmianach: jedno-argumentowej i dwu-argumentowej. Wariant jedno-argumentowy – z argumentem typu String – zaimplementowany jest we wszystkich klasach poza klasą ‘Character’; wariant dwu-argumentowy tylko w klasach reprezentujących typy całkowitoliczbowe. Pierwszy argument to liczba a drugi (typu ‘int’) to podstawa systemu liczbowego (ang. radix), w którym ta liczba jest zapisana.

Sprawę tworzenia obiektów z wartości prymitywnych oraz konwersji String’ów na obiekty i wartości prymitywne mamy już z głowy. Została jeszcze kwestia przejścia w drugą stronę, a więc pobieranie wartości prymitywnej z obiektu oraz konwersja wartości reprezentowanej przez obiekt na ‘String’. Aby przejść od typu prymitywnego do String’a można użyć statycznej metody ‘valueOf(…)’ z klasy ‘String’.

Aby pobrać wartość prymitywną reprezentowaną przez daną instancję klasy typu opakowującego należy użyć jednej z metod postaci ‘xxxValue()’. Naturalnie, w odróżnieniu od wcześniej omawianych metod są to metody instancji. W klasie ‘Boolean’ zdefiniowano tylko metodę ‘booleanValue()’, podobnie w klasie ‘Character’ jest tylko metoda ‘charValue()’ ale w pozostałych klasach, tj. w klasach reprezentujących typy numeryczne jest już komplet metod numerycznych, tj. ‘byteValue()’, ‘shortValue()’, ‘intValue()’, ‘longValue()’, ‘floatValue()’ oraz ‘doubleValue()’. Można zatem obiekt klasy, dajmy na to, ‘Byte’ przekonwertować wywołaniem jednej funkcji do zmiennej typu ‘float’.

Została jeszcze konwersja z obiektu na ‘String’ i będzie po wszystkim… a nie było lekko. Przede wszystkim, instancje klas opakowujących są obiektami, a więc mamy do dyspozycji metodę instancyjną toString(). Dodatkowo, wszystkie typy numeryczne implementują jedno argumentową statyczną metodę ‘toString(…)’ która akceptuje odpowiadającą typem (np. ‘int’ w klasie ‘Integer’, ‘float’ w klasie ‘Float’ itd.) wartość prymitywną a zwraca ‘String’. W klasie ‘Integer’ oraz ‘Long’ jest jeszcze dwu-argumentowy wariant statycznej metody ‘toString(…)’. Drugi, dodatkowy argument typu ‘int’ określa system liczbowy, w którym zapisana ma być liczba. Trochę jakby nadmiarowo, klasy ‘Integer’ oraz ‘Long’ implementują też jednoargumentowe metody ‘toBinaryString(…)’, ‘toOctalString(…)’ oraz ‘toHexString(…)’, które robią dokładnie to samo, co wspomniane przed chwilą metody dwuargumentowe ‘toString(…)’, tyle, że system liczbowy określony jest w nazwie metody a nie w postaci drugiego argumentu.

3 komentarze:

Anonimowy pisze...

Świetne! Dziękuję bardzo!

Anonimowy pisze...

Przepraszam ze przeszkadzam
ale
czy Pan moze zjadl lasiczke?
Bo nie ma jej na zdjeciu
jestem zaniepokojona
Bede szalenie wdzieczna za odpowiedz

Anonimowy pisze...

Ładne odkopanie po 9 latach :P