15 października 2008

SCJP - Klasy StringBuffer i StringBuilder

Kilka dni temu – w artykule „SCJP - Klasa String” – pisałem o klasie String. Dziś będzie o dwu kolejnych klasach używanych do operacji na napisach; klasie StringBuffer i StringBuilder.

Klasa String reprezentuje sekwencję znaków, typ napisowy. Jak napisałem w poprzednim artykule, obiekty klasy String są niemodyfikowalne (ang. immutable); każda operacja, w wyniku której chcemy otrzymać inny napis na bazie istniejącego wymaga utworzenia nowego obiektu, do reprezentowania tego nowego napisu. Jest to coś, czego chcielibyśmy uniknąć. Potrzebujemy więc typu, który reprezentował by modyfikowalną sekwencję znaków i właśnie tym jest klasa StringBuffer. Aby zapewnić bezpieczne przetwarzanie wielowątkowe (ang. thread-safe) operacje klasy StringBuffer są synchronizowane. Ułatwia to znakomicie programowanie ale w sytuacji gdy wiemy, że nasza aplikacja nie będzie wykonywana przez więcej niż jeden wątek synchronizacja jest zbędna i wprowadza bezsensowne straty wydajnościowe. Z tego powodu do specyfikacji Java SE w wersji 5 dodano klasę StringBuilder, która różni się od klasy StringBuffer tylko tym, że jej metody synchronizowane już nie są. Poniżej przedstawiam metody tych klas – te ważne z perspektywy egzaminu SCJP – na przykładzie klasy StringBuilder, ale jak wiemy klasa StringBuffer ma metody analogiczne, tyle, że synchronizowane.

public StringBuilder append(String str) - Metoda ta zmodyfikuje łańcuch znaków reprezentowany przez obiekt dla którego ją wywołano w ten sposób, że do jego końca doklei napis przekazany jako argument. Metoda ta ma wiele wariantów różniących się typem parametru; istnieją między innymi wersje dla parametrów typu: int, char, boolean czy Object. Metoda ta zwraca obiekt dla którego została wywołana (nie nowy obiekt jak w klasie String) aby umożliwić wywołania łańcuchowe (ang. chained invocations). Uruchomienie metody
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder();

System.out.println(stringBuilder.append("mam ").append(5).append(" lat"));
}
spowoduje wyświetlenie napisu ‘mam 5 lat’.

public StringBuilder delete(int start, int end) - Metoda ta zmodyfikuje obiekt dla którego ją wywołano w ten sposób, że usunie ciąg znaków określony argumentami wywołania. Pierwszy argument to pozycja pierwszego znaku który ma być usunięty (jak zawsze liczymy od 0-ra) a drugi to pozycja pierwszego znaku który usunięty nie będzie. Metoda ta także zwraca obiekt dla którego została uruchomiona. Uruchomienie metody
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder("mam 55 lat");

System.out.println(stringBuilder.delete(4, 5));
}
spowoduje wyświetlenie napisu ‘mam 5 lat’.

public StringBuilder insert(int offset, String str) - Metoda ta zmodyfikuje obiekt dla którego ją wywołano w ten sposób, że na pozycji określonej pierwszym argumentem wywołania zostanie wklejony String przekazany jako drugi argument. Analogicznie jak metoda append(..) metoda insert(…) występuje w wielu odmianach, akceptujących jako drugi argument typy takie jak (między innymi): int, float, char, Object. Zwrócony obiekt to obiekt dla którego wywołano metodę. Uruchomienie programu
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder("mam 5 lat");

System.out.println(stringBuilder.delete(4, 5).insert(4, "niewiele"));
}
spowoduje wyświetlenie napisu ‘mam niewiele lat’.

public StringBuilder reverse() – Metoda ta odwraca kolejność znaków w obiekcie dla którego ją wywołano. Tak jak dla innych metod, zwrócony obiekt to obiekt dla którego wywołano metodę. Uruchomienie programu
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder("abcdef");

System.out.println(stringBuilder.reverse());
}
spowoduje wyświetlenie napisu ‘fedcba’.

13 października 2008

SCJP - Klasa String

Kolejny zastrzyk wiedzy przed egzaminem SCJP – kontynuacja dzieła zapowiedzianego w artykule „Przygotowania do SCJP czas zacząć”. Dziś będzie o typie napisowym reprezentowanym przez klasę String.

Zacznijmy od rozwiania ewentualnych wątpliwości natury fundamentalnej. Otóż typ napisowy w języku Java nie jest typem prostym. Co prawda przywykliśmy do tego, że obiekty tworzymy z użyciem operatora ‘new’, a więc inaczej niż zazwyczaj tworzone są String’i, ale nie zmienia to faktu, że każdy String jest obiektem. Zerknijmy na poniższy kod.
public class MyClass {
public static void main(String[] args) {
String a = new String("Ola ma kota"); // oczywiście można tak

String b = "A ula psa"; // ale możemy użyć także skróconej formy

System.out.print(b.getClass());
}
}

Uruchomienie tego programu spowoduje wyświetlenie napisu „class java.lang.String”. Czy jednak obydwa pokazane powyżej sposoby utworzenia obiektu napisu są równoważne? Otóż nie. Konstrukcja ‘new String(…)’ zawsze skutkuje utworzeniem nowego obiektu, natomiast forma skrócona, z bezpośrednim użyciem literału spowoduje utworzenie nowego obiektu tylko wówczas, gdy obiekt reprezentujący dany napis jeszcze nie istnieje. A gdzie ma istnieć? Otóż Wirtualna Maszyna Javy utrzymuje pulę String’ów – w specjalnym, przeznaczonym do tego celu obszarze pamięci – z nadzieją, że przyczyni się to do optymalizacji czasu wykonania i ilości potrzebnej pamięci. Pierwsze użycie literału napisowego spowoduje utworzenie nowego obiektu oraz umieszczenie go w owej puli, zaś każde następne użycie odnosić się będzie do tegoż właśnie obiektu z puli (kolejny obiekt nie jest tworzony). Przekonajmy się o tym analizując poniższy program,
public class MyClass {
public static void main(String[] args) {
String a = "Ola";
String b = "Ola";

System.out.println("a == b : " + (a == b));

String c = new String("Ola");
String d = new String("Ola");

System.out.println("c == d : " + (c == d));
}
}

który uruchomiony wyświetli napis
a == b : true
c == d : false

Zatem zgadza się. Referencja ‘a’ i ‘b’ wskazują na ten sam obiekt (wyrażenie ‘a == b’ ma wartość ‘true’) zaś ‘c’ i ‘d’ już nie (wyrażenie ‘c == d’ ma wartość ‘false’).

Kolejnym ważnym faktem odnośnie obiektów klasy String jest, że są one niemodyfikowalne (ang. immutable). Obiekt klasy String reprezentuje pewien stały, dokładnie jeden napis. Jeśli potrzebujemy obiektu, który reprezentuje inny napis, to najzwyczajniej w świecie potrzebujemy nowego obiektu klasy String. Zerknijmy na poniższy przykład,
public class MyClass {
public static void main(String[] args) {
String a = "Ola";
String b = a;

System.out.println("a == b : " + (a == b));

a += " ma kota";

System.out.println("a == b : " + (a == b));
}
}

który uruchomiony wyświetli napis
a == b : true
a == b : false

Prześledźmy teraz, jak to się dzieje. W pierwszej linii deklarujemy referencję ‘a’ i przypisujemy jej nowo utworzony obiekt reprezentujący napis „Ola”. Obiekt ten ląduje w puli obiektów o której mówiliśmy przed chwilą. Następnie deklarujemy referencję ‘b’ i przypisujemy jej ten sam obiekt, który przypisaliśmy do zmiennej ‘a’. Nic więc dziwnego, że pierwszą linią wyświetloną przez program jest ‘a == b : true’. Następnie, do zmiennej ‘a’ chcemy dodać sufiks „ ma kota”. Wiemy, że obiekty klasy String są niemodyfikowalne a więc operator ‘+=’ nie może zmienić wartości obiektu wskazywanego przez zmienną ‘a’. Co zatem się dzieje? Otóż tworzony jest nowy obiekt inicjowany wartością „Ola ma kota” i to ten właśnie obiekt jest przypisywany do zmiennej ‘a’. Zmienna ‘a’ wskazuje zatem teraz na nowy, zupełnie inny obiekt niż zmienna ‘b’, stąd porównanie ‘a == b’ ma wartość ‘false’.

String to klasa zupełnie podstawowa, stąd niezbędna jest – także z perspektywy egzaminu SCJP – znajomość jej najpopularniejszych metod. Poniżej zaprezentowano listę tychże, wraz z opisem i przykładem wywołania.

public char charAt(int index) - zwraca znak znajdujący się na zadanej argumentem wywołania pozycji w String’u, przy czym należy pamiętać, że tak jak tablice, indeks zaczyna się od liczby 0, tj. wyrażenie
"abcdef".charAt(1) 
ma wartość ‘b’

public String concat(String sufix) - zwraca nowy obiekt typu String, który ma wartość taką jak obiekt dla którego wywołano metodę z doklejonym jako sufiks argumentem wywołania, np. wyrażenie
"abcdef".concat("ghij")
ma wartość ‘abcdefghij’

public boolean equalsIgnoreCase(String str) – metoda pozwala stwierdzić, czy porównywane napisy (ten, dla którego wywołano metodę i ten przekazany jako argument) są takie same (leksykograficznie), przy czym przy porównywaniu ignoruje się „wielkość liter” (litery małe i duże są utożsamiane), np. wyrażenie
"abc".equalsIgnoreCase("AbC")
ma wartość ‘true’

public int length() - metoda ta zwraca długość napisu reprezentowanego przez obiekt, tj. ilość jego znaków, np. wyrażenie
"abc".length()
ma wartość ‘3’

public String replace(char old, char new) - metoda zwraca nowy obiekt typu String, powstały poprzez zastąpienie (w String’u dla którego wywołano metodę) każdego wystąpienia znaku przekazanego jako pierwszy argument, przez znak przekazany jako drugi argument, np. wyrażenie
"ababab".replace('b', 'c') 
ma wartość ‘acacac’

public String substring(int begin) - zwraca String’a powstałego po odrzuceniu początkowych znaków ze String’a dla którego wywołano metodę; pierwszym znakiem zwracanego String’a będzie ten, który znajduje się na pozycji przekazanej jako argument wywołania, np. wyrażenie
"abcdef".substring(1) 
ma wartość ‘bcdef’

public String substring(int begin, int end) - zachowuje się jak metoda opisana powyżej, z tym, że obcina także końcówkę napisu; wartość przekazana jako drugi argument wywołania to indeks pierwszego znaku który nie znajdzie się w zwracanym String’u, np. wyrażenie
"abcdef".substring(1, 3) 
ma wartość ‘bc’

public String toLowerCase() - zwraca String’a, który powstał po zamienieniu wszystkich dużych liter String’a dla którego wywołano metodę na odpowiadające litery małe, np. wyrażenie
"aBcDe".toLowerCase()
ma wartość ‘abcde’

public String toUpperCase() - metoda analogiczna do metody opisanej powyżej, z tym, że litery małe zamienia na duże a nie na odwrót, np. wyrażenie
"aBcDe".toUpperCase()
ma wartość ‘ABCDE’

public String trim() - zwraca napis powstały ze Stinga użytego do wywołania metody po obcięciu białych znaków znajdujących się na początku i końcu tegoż Stringa, np. wyrażenie
"    przed tabulacja, po spacja ".trim()
ma wartość ‘przed tabulacja, po spacja’

8 października 2008

SCJP - Metody equals() i hashCode()

W ostatnim artykule z serii o SCJP – „SCJP - Serializacja” – zapowiedziałem powrót do nieco intensywniejszego trybu pracy… no i jestem. Ostatnio publikowałem 28 września, także zgodnie z obietnicą jest częściej niż co miesiąc. Dziś będzie o metodach equals() i hashCode().

Zacznijmy od podstaw, a mianowicie – do czego właściwie służy metoda equals()? Przecież mamy operator równości ‘==’? Mówiąc najprościej, operator równości służy do sprawdzenia, czy dwa obiekty są dokładnie tym samym (tym samym obiektem), zaś metoda equals() odpowiada na pytanie, czy dwa obiekty są „takie same”. Sformułowanie „takie same” ująłem w cudzysłów, bowiem co ono oznacza zależy tylko od naszego uznania, a mówiąc ściślej, właśnie od implementacji metody equals(). Metoda equals() jest zdefiniowana w klasie java.lang.Object w ten sposób, że jest tożsama z operatorem równości ‘==’, ale w wielu przypadkach nie jest to implementacja właściwa. Dla przykładu, w klasie java.lang.Integer metodę tę nadpisano w ten sposób, że dwa obiekty ‘x’ i ‘y’ są uznawane za „takie same” (‘x.equals(y) == true’), jeśli reprezentują tę samą wartość liczbową, tj. ‘x.intValue() == y.intValue()’.

Powiedzieliśmy sobie przed chwilą, że znaczenie sformułowania „takie same”, a więc implementacja metody equals(), mogą być dowolne i zależą wyłącznie od naszego uznania, ale nie do końca jest to prawdą. Metodę equals() obowiązuje bowiem kontrakt, którego musimy przestrzegać. Oto on:

- Relacja wyznaczona metodą equals() musi być zwrotna, tj. dla każdej zmiennej referencyjnej ‘x’ (różnej od ‘null’) wyrażenie ‘x.equals(x)’ ma wartość ‘true’.

- Relacja wyznaczona metodą equals() musi być symetryczna, tj. dla każdej pary zmiennych referencyjnych ‘x’ i ‘y’, wyrażenie ‘x.equals(y)’ ma wartość ‘true’ wtedy i tylko wtedy gdy ‘y.equals(x)’ ma wartość ‘true’. Chciałoby się powiedzieć, że ‘x.equals(y)’ musi dawać dokładnie ten sam wynik co ‘y.equals(x)’, ale tak nie jest. Jeśli bowiem ‘x’ wskazuje na pewien obiekt a ‘y’ ma wartość ‘null’, to ‘x.equals(y)’ ma wartość ‘false’ (musi być ‘false’, co jest kolejnym punktem kontraktu) a ‘y.equals(x)’ wywołuje wyjątek NullPointerException.

- Relacja wyznaczona metodą equals() musi być przechodnia, tj. dla dowolnych zmiennych referencyjnych ‘x’, ‘y’ i ‘z’, jeśli ‘x.equals(y)’ ma wartość ‘true’ oraz ‘y.equals(z)’ ma wartość ‘true’ to także ‘x.equals(z)’ musi mieć wartość ‘true’.

- Przy założeniu, że porównywane obiekty się w międzyczasie nie zmieniają, każdorazowe wywołanie funkcji ‘x.equals(y)’ musi dawać taki sam wynik, tj. albo zawsze ‘true’ albo zawsze ‘false’.

- Każdy obiekt musi być różny od wartości ‘null’, tj wywołanie ‘x.equals(null)’ musi zawsze zwracać wartość ‘false’.

Ściśle związaną z operacją equals() jest operacja hashCode(). Operację hashCode() również obowiązuje pewien kontrakt i to taki, który definiuje jej zachowanie w zależności od zachowania metody equals(). Stąd w typowej sytuacji, ilekroć nadpisujemy jedną z tych metod musimy nadpisać też i drugą. Oto kontrakt dla metody hashCode():

- Przy założeniu, że obiekt się w międzyczasie nie zmienia, każdorazowe wywołanie funkcji hashCode() musi zwracać, w ramach pojedynczego uruchomienia aplikacji, taką samą wartość. Co prawda wtrącenie, że „w ramach pojedynczego uruchomienia aplikacji” nie ma specjalnie sensu (inne uruchomienie, a więc inny obiekt), ale jest to wtrącone także w „oficjalnej” dokumentacji więc niech zostanie.

- Jeśli dwa obiekty ‘x’ i ‘y’ są „takie same”, tj. ‘x.equals(y)’ ma wartość ‘true’, to muszą one mieć takie same wartości skrótu (ang. hash code), tj. musi zachodzić ‘x.hashCode() == y.hashCode()’. Zauważmy, że implikacja działa tylko w jedną stronę, tj. jeśli obiekty „są różne” (tj. nie są „takie same”), to ich wartości skrótu wcale nie muszą być różne.

I na koniec jeszcze przykład. Zwróćmy szczególną uwagę na sygnatury metod. Upewnijmy się, że to co nam pokażą na egzaminie SCJP, to będą rzeczywiście nadpisania a nie przeciążenia.
public class MyClass {
private int someValue;

public int hashCode() {
return someValue;
}

public boolean equals(Object obj) {
if (obj == null || getClass() != obj.getClass())
return false;

if (someValue != ((MyClass) obj).someValue)
return false;

return true;
}
}

28 września 2008

SCJP – Serializacja

Wakacje się skończyły; czas zakasać rękawy i wrócić do pracy w zwyczajowym tempie (tj. więcej niż jeden artykuł na miesiąc). Zregenerowałem nieco siły i z nowym zapasem energii "wznawiam" serię o SCJP, zapoczątkowaną artykułem "Przygotowania do SCJP czas zacząć". Dziś będzie o serializacji.

Mechanizm serializacji służy do zapisywania obiektów do strumienia danych, oraz w drugą stronę, do rekonstruowania obiektów z ciągu bitów odczytanych ze strumienia. W typowej sytuacji, strumienie te "podłączone są" do plików dyskowych, a więc z pewnym uproszczeniem można powiedzieć, że serializacja służy do zapisywania obiektów do plików i odczytywania tychże z powrotem. Zacznijmy od przykładu kodu, który serializuje (zapisuje) obiekt do pliku a następnie go odczytuje.

class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
saveObject(new DataObject(123), "db.bin");

System.out.println(readObject("db.bin").value);
}

public static void saveObject(DataObject obj, String fileName) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream(fileName);

ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
outputStream.writeObject(obj);

outputStream.close();
}

public static DataObject readObject(String fileName)
throws IOException, ClassNotFoundException {

FileInputStream fileInputStream = new FileInputStream(fileName);

ObjectInputStream inputStream = new ObjectInputStream(fileInputStream);
DataObject obj = (DataObject) inputStream.readObject();

inputStream.close();

return obj;
}
}

class DataObject implements Serializable {
int value;

public DataObject(int value) {
this.value = value;
}
}

Uruchomienie powyższego programu spowoduje wyświetlenie liczby "123", co pozwala nam przypuszczać, że obiekt rzeczywiście został zapisany i następnie poprawnie odczytany. Zastanówmy się teraz, co to dokładnie znaczy "zapisać obiekt". W powyższym przykładzie sprawa jest oczywista, ale co się stanie, gdy nasz obiekt będzie zawierał referencje do innych obiektów? Czy one także zostaną zapisane? Tak, zostaną zapisane! Serializacja zapisuje de facto cały graf obiektów, nie tylko obiekt zapisywany explicite.

Aby obiekt mógł być serializowany, jego klasa musi implementować interfejs java.io.Serializable. Ale uwaga! Nie tylko obiekt, który serializujemy explicite musi być serializowalny. Serializowalne muszą być wszystkie zapisywane obiekty, a więc także te, na które wskazują referencje obiektu, który zapisujemy. Zmodyfikujmy nieco nasz przykład, jak pokazano poniżej.

class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
saveObject(new DataObject(123), "db.bin");

System.out.println(readObject("db.bin").valueObject.value);
}

// metody saveObject(…) oraz readObject(…) bez zmian
}

class DataObject implements Serializable {
ValueObject valueObject;

public DataObject(int value) {
this.valueObject = new ValueObject(value);
}
}

class ValueObject {
int value;

public ValueObject(int value) {
this.value = value;
}
}

Kod się skompiluje, ale otrzymamy błąd czasu wykonania. Próba uruchomienia programu zakończy się wyjątkiem java.io.NotSerializableException.

Exception in thread "main" java.io.NotSerializableException: my.pckg.ValueObject
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1081)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1375)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1347)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1290)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1079)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:302)
at my.pckg.Test.saveObject(Test.java:21)
at my.pckg.Test.main(Test.java:12)

Przyczyną jest naturalnie brak deklaracji, że klasa ValueObject jest serializowalna – dodanie deklaracji ‘implements Serializable’ rozwiązuje problem.

Serializacja obiektu oznacza zapisanie stanu obiektu, a więc "wartości" zmiennych instancyjnych, przy czym "wartość" zmiennej referencyjnej to referowany obiekt. Ważne jest, aby uświadomić sobie, że stan obiektu to tylko zmienne instancji, w odróżnieniu od zmiennych klasowych, tj. statycznych, których serializacja nie dotyczy. Mamy ponadto możliwość wykluczenia poszczególnych zmiennych z serializacji, za pomocą modyfikatora ‘transient’. Wystarczy zmienną oznaczyć jako ‘transient’ i z punktu widzenia serializacji będzie tak, jakby jej nie było.

Zastanówmy się teraz, jak wygląda deserializacja, a więc jak tworzone są obiekty i jak odtwarzany jest ich zserializowany stan. Zerknijmy na kolejny przykład.

class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
DataObject obj = new DataObject();

obj.value = 1;
obj.middleValue = 2;
obj.topValue = 3;

saveObject(obj, "db.bin");
obj = readObject("db.bin");

System.out.print(obj.value + " " + obj.middleValue
+ " " + obj.topValue + " " + obj.tValue);
}

// metody saveObject(…) oraz readObject(…) bez zmian
}

class TopDataObject {
int topValue = 345;
}

class MiddleDataObject extends TopDataObject implements Serializable {
int middleValue = 234;
}

class DataObject extends MiddleDataObject {
int value = 123;

transient int tValue = 999;
}

Uruchomienie tego programu spowoduje wyświetlenie liczb "1 2 345 0”. Klasa ‘MiddleDataObject’ deklaruje wprost, że jest serializowalna. Serializowalna jest także klasa ‘DataObject’, jako że dziedziczy z klasy ‘MiddleDataObject’, która jest serializowalna. Natomiast nie jest serializowalna klasa ‘TopDataObject’. Co się zatem będzie działo z zadeklarowaną w tej klasie, i odziedziczoną przez klasę ‘DataObject’ zmienną ‘topValue’? A cóż specjalnego miałoby się dziać? – Będzie ona zwyczajnie ignorowana przez mechanizm serializacji. Zmienna ‘topValue’ jest ignorowana, bo została zadeklarowana w klasie, która nie jest serializowalna. Zmienna ‘tValue’ jest ignorowana, bo została zadeklarowana jako ‘transient’; zatem w wyniku serializacji zostanie zapisana tylko wartość zmiennych ‘value’ i ‘middleValue’.

Zerknijmy teraz do artykułu "SCJP - Konstruktory i inicjalizacja", aby przypomnieć sobie (o ile nie pamiętamy), jak inicjalizowane są nowo tworzone obiekty. Generalnie rzecz biorąc, uruchamiane są konstruktory, począwszy od konstruktora klasy Object a skończywszy na konstruktorze klasy, której instancję tworzymy (w tym inicjalizowane są zmienne instancyjne, którym przypisaliśmy wartość w ramach deklaracji). I teraz pytanie. Co się dzieje, gdy obiekt tworzony jest w wyniku deserializacji a nie z użyciem operatora ‘new’? Otóż coś zupełnie innego. Odtworzenie stanu zserializowanego obiektu i inicjalizacja obiektu to dwie zupełnie różne rzeczy. Jak zatem wygląda odtworzenie obiektu w wyniku deserializacji? Przede wszystkim trzeba zapamiętać, że nie jest uruchamiany konstruktor i zmienne nie są inicjalizowane na wartości przypisane w trakcie deklaracji. Wartości zmiennych są przecież ustawiane na wartości odczytane ze strumienia a więc uruchamianie konstruktora nie ma sensu. No dobrze. A co z wartościami zmiennych, które przez serializacje są ignorowane, jak ‘topValue’ czy ‘tValue’ z powyższego przykładu? No więc:

- Jeśli pewna nadklasa klasy deserializowanego obiektu nie jest serializowalna, to w celu zainicjowania wartości zmiennych zadeklarowanych w tej klasie zostanie uruchomiony jej konstruktor. Tak więc w powyższym przykładzie, uruchomiony będzie konstruktor klasy ‘TopDataObject’ który ustawi wartość zmiennej ‘topValue’ na ‘345’ (inicjalizacja zmiennych w trakcie deklaracji to de facto część konstruktora). Nie zmienia to oczywiście faktu, że w żadnym wypadku nie zostanie uruchomiony konstruktor klas serializowalnych, a więc ‘MiddleDataObject’ czy ‘DataObject’.

- Zmienne ‘transient’ zadeklarowane w klasach serializowalnych nie są inicjalizowane ani wartościami odczytanymi ze strumienia (bo tych wartości tam nie ma), ani przez konstruktor (bo ten nie jest uruchamiany). Mają one zatem wartość domyślną dla danego typu, czyli np. 0 dla typu całkowitoliczbowego, ‘null’ w przypadku referencji. Więcej na temat wartości domyślnych w artykule "SCJP - Wartości domyślne zmiennych".

Wyobraźmy sobie teraz, że chcemy serializować obiekty naszej klasy, które mają referencję do obiektów klasy, którą zaimplementował ktoś inny, której nie możemy zmodyfikować i która nie jest serializowalna. Oczywiście nic nie stoi na przeszkodzie. Możemy oznaczyć taką referencję jako ‘transient’ i wszystko będzie działać… z tym tylko wyjątkiem, że możemy akurat nie chcieć ignorować tego obiektu. Chcielibyśmy jednak zapisywać stan także tej "cudzej" klasy, której musimy (bądź chcemy) używać. Otóż i to da się zrobić. Wystarczy zaimplementować metody wywołania zwrotnego (ang. callback):

private void writeObject(ObjectOutputStream os) throws IOException {
// kod, który wykona serializację "ręcznie"
}

oraz

private void readObject(ObjectInputStream os)
throws IOException, ClassNotFoundException {

// kod, który wykona deserializację "ręcznie"
}

w klasie, która ma się serializować inaczej niż standardowo. Poniżej przykład. Algorytm postępowania wygląda tak, że zmienną referencyjną, która nie może być serializowana w zwykły sposób oznaczamy jako ‘transient’, a jej stan zapisujemy "ręcznie", po czym wywołujemy metodę ‘defaultWriteObject()’ (może być też w odwrotnej kolejności, ale kolejność musi być ta sama), która wykona standardową serializację całej reszty.

class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
saveObject(new DataObject(123), "db.bin");

System.out.println(readObject("db.bin").valueObject.value);
}

// metody saveObject(…) oraz readObject(…) bez zmian
}

class DataObject implements Serializable {
transient ValueObject valueObject;

public DataObject(int value) {
this.valueObject = new ValueObject(value);
}

private void writeObject(ObjectOutputStream os) throws IOException {
os.defaultWriteObject();
os.writeInt(valueObject.value);
}

private void readObject(ObjectInputStream os)
throws IOException, ClassNotFoundException {

os.defaultReadObject();
valueObject = new ValueObject(os.readInt());
}
}

class ValueObject {
int value;

public ValueObject(int value) {
this.value = value;
}
}