środa, 8 grudnia 2010

Serializacja w Javie na potrzeby SCJP 6

Serializacja w Javie, a SCJP

W tym krótkim poście chciałbym opisać serializację w Javie na potrzeby egzaminu SCJP 6. Mówiąc najprościej serializacja to przekształcenie aktualnego stanu obiektu w strumień bajtów, który może być zapisany na dysku twardym. Odwrotny proces to deserializacja. Wspominajac o zachowaniu stanu obiektu mam namyśli oczywiście zachowanie wszystkich albo prawie wszystkich zmiennych instancji obiektu. Na początek kilka reguł:

Każdy obiekt, który ma być serializowany musi implementować interfejs Serializable.

Aby zapisać i odczytać stan obiektu korzystamy z klas: ObjectOutputStream i ObjectInputStream.

Prosty przykład na początek:

class Animal implements Serializable
{
public int age;

public Animal(int age)
{
this.age = age;
}

public String toString()
{
return "My age is: " + age;
}

public static void main(String[] args)
{
Animal a1 = new Animal(12);
Animal a2 = new Animal(2);

try
{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("my_animals.dat"));
oos.writeObject(a1);
oos.writeObject(a2);
oos.close();
}catch(Exception ex)
{
ex.printStackTrace();
}

a1 = null;
a2 = null;

try
{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("my_animals.dat"));
a1 = (Animal) ois.readObject();
a2 = (Animal) ois.readObject();
ois.close();
}catch(Exception ex)
{
ex.printStackTrace();
}

System.out.println(a1.toString() + " " + a2.toString());
}

}



Do pliku zapisujemy stan dwóch obiektów klasy Animal, a następnie je odczytujemy. Trzeba oczywiście pamiętać o łapaniu wyjątków, w tym przykładzie nieelegancko łapię wszystkie wyjątki za jednym razem.



Na stan obiektu składają się: zmienne typu referencyjnego – wskazują obiekty w pamięci komputera, oraz zmienne przechowujące wartości typu prymitywnego np. int, char, long. Jeżeli dany obiekt posiada w sobie dowiązanie do innego obiektu np. Owner – Animal to klasa tego obiektu musi również implementować interfejs Serializable


class Animal implements Serializable
{
public int age;

public Animal(int age)
{
this.age = age;
}

public String toString()
{
return "My age is: " + age;
}
}

public class Owner implements Serializable
{
private String name;
private Animal animal;

public Owner(String name, Animal animal)
{
this.name = name;
this.animal = animal;
}
}


Sytuacja jest bajecznie prosta gdy sami piszemy każdą z klas. Co zrobić w przypadku, gdy korzystamy już z istniejącej klasy, która nie implementuje ważnego interfejsu? Z pomocą przychodzi słowo kluczowe transient. Pole klasy nim tak opatrzone nie jest serializowane. To oznacza, że ten obiekt lub wartość nie zostanie zapisana. Po deserializacji pole to będzie przyjmować null-a, gdy jest to zmienna referencyjna lub domyślną wartość dla danego typu np. 0 lub false. Warto zaznaczyć, że podczas desarializacji nie wykonuje się konstruktor klasy, do której dany obiekt należy, dlatego np. pole: cpuName będzie null-em.



public class Computer implements Serializable
{

private transient String cpuName = "CPU from China 66.6 MHz";

public String toString()
{
return "My CPU is: " + cpuName;
}

}


W przypadku próby zapisania obiektu, który nie jest serializowalny (nie implementuje Serializable) otrzymamy wyjątek: NotSerializableException.

Jeżeli bardzo nam zależy na zachowaniu stanu pól możemy zdefiniować odpowiednie metody, które pozwolą prócz samych obiektów na zachowanie innych wartości.



public class Home implements Serializable
{

private String name = "Home1";
private transient Window window = new Window(14, 34);

public String toString()
{
return "Window's size: " + window.height +" x "+ window.width;
}

private void writeObject(ObjectOutputStream oos) throws IOException
{
oos.defaultWriteObject();
oos.writeInt(window.width);
oos.writeInt(window.height);
}

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException
{
ois.defaultReadObject();
window = new Window(ois.readInt(), ois.readInt());
}



public static void main(String[] args)
{
Home h = new Home();


try
{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("my_home.dat"));
oos.writeObject(h);
oos.close();
}catch(Exception ex)
{
ex.printStackTrace();
}

h = null;


try
{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("my_home.dat"));
h = (Home) ois.readObject();
ois.close();
}catch(Exception ex)
{
ex.printStackTrace();
}



}

}

class Window
{
public int width, height;

public Window(int w, int h)
{
width = w;
height = h;
}
}


Klasa Window nie jest serializowalna. Obiekty klasy Home posiadają pola, które są referencjami do obiektów klasy Window. Definiując metody writeObject i readObject możemy prócz obiektów zapisać również dwa int-y które są polami klasy Window. Przywrócenie odpowiednich wartości łączy się z odczytaniem wartości tych int-ów (w odpowiedniej kolejności) i wywołaniu konstruktora.


Co się dzieje w przypadku, gdy rozszerzamy daną klasę która nie implementuje Serializable?

Odpowiedź jest prosta: Jedynie te pola zostają serializowane, które należą do podklasy. Natomiast jakie wartości zostają nadane polom nad klasy po deserializacji?

Zanim odpowiem na to pytanie warto zaznaczyć, że w tym przypadku wykonują się konstruktory należące do nadklas. Pola będą przyjmować wartości nadane przez konstruktory zdefiniowane w nadklasach. Oto przykład:


public class Foo
{
String name = "Foo";
}

public class Bar extends Foo implements Serializable
{
String barName = "BarName";

public static void main(String[] args)
{
Bar bar = new Bar();
bar.name = "newFooName";

try{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("my_bar.dat"));
oos.writeObject(bar);
oos.close();
}catch(Exception ex)
{
ex.printStackTrace();
}

bar = null;

try
{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("my_bar.dat"));
bar = (Bar) ois.readObject();
ois.close();
}catch(Exception ex)
{
ex.printStackTrace();
}

System.out.println(bar.barName +" "+bar.name);
//Zostanie wypisany tekst: BarName Foo

}
}


Ostatnią rzeczą jaką warto powiedzieć to, że zapisywane są jedynie pola obiektów, a nie pola klasy. Więc wszystko co opatrzone słowem static nie zostanie zachowane.

niedziela, 17 października 2010

Co jest nie tak z PHP?

Do napisania tego tekstu zabierałem się dość długo. W końcu się odważyłem. Mam zamiar podzielić się z Wami moimi przemyśleniami na temat języka PHP. Wszystkie jednak mogę podsumować następująco: PHP to najgorszy język programowania wysokiego poziomu w jakim miałem okazję pisać. Zacznijmy może od początku. Skąd się wziął pomysł na stworzenie PHP? Otóż miał to być prosty język skryptowy wymyślony po to, aby sprawić by proste strony internetowe były dynamiczne. To założenie się udało. Na tym polu pisanie prostych kilkulinijkowych skryptów jest bajecznie proste i do tego PHP nadaje się wyśmienicie. Powinno tak zostać. Niestety w PHP powstają spore aplikacje klasy Enterprise. W pisaniu większych rzeczy przeszkadza sporo rzeczy, które poniekąd wynikają z historii PHP, który nagle stał się językiem, w którym pisze się coś więcej niż prostą stronę internetową. Oto lista rzeczy, które mnie najbardziej denerwują:

- dynamiczne typowanie ze słabą typizacją – dla niektórych wygoda, a dla mnie porażka jakich mało, rozwiązanie to uniemożliwia skuteczne debugowanie. Wytłumaczę to na przykładzie: piszę sobie pewien większy kawałek kodu, uruchamiam go, a moim oczom ukazuje się biała strona zamiast wyniku. Zastanawiam się co jest źle. Nie mam żadnego wyjątku informującego mnie o tym, że przekazałem do jakiejś funkcji argument o złym typie. Aplikacja w jakimś momencie wykonała coś czego nie przewidziałem, funkcja zwróciła zamiast Stringa wartość 0, którą następnie przekazałem do innej funkcji. Idąc dalej okazuje się, że kod wykonał się bez zarzutu, a jednak wyniku nie ma. Porównam tą sytuację do przykładu w Ruby. Otrzymując nil-a z funkcji zamiast stringa,  a następnie wykonując na nim metodą np. length otrzymam taki wyjątek, że mało co nie spadam z krzesła. Druga sytuacja mająca częste miejsce to literówka w nazwie zmiennej. W PHP robię gdzieś literówkę, kod się wykona, a ja otrzymam nie poprawne dane. Ruby mnie poinformuje, że nic nie wie o nowo powstałym czymś, co się nazywa jak zmienna z literówką. No to już mamy 2 do 0 dla Ruby. Ruby to język dynamicznie typowany z silną typizacją. To oznacza, że jeżeli gdzieś potrzebujemy String-a, a podamy np. liczbę całkowitą to nie zadziała. Sami musimy się zatroszczyć o to jaki typ ma nasza zmienna. Zdecydowanie ułatwia to szukanie błędów, a konwersja pomiędzy typami jest bardzo prosta.

- programowanie obiektowe – PHP miał być prostym językiem skryptowym, pewnie dla tego nie zaimplementowali w nim od samego początku obiektowość przynajmniej na takim poziomie jak w Javie, w której jednak nie wszystko jest obiektem. Chyba każdy przyzna mi rację, że zdecydowanie lepiej jest wywołać metodę na obiekcie niż pisać w sposób proceduralny. Nienawidzę wykonywać funkcji na String-ach. W zasadzie praktycznie wszystko w PHP jest funkcjami. Dlaczego nie mogły być one od samego początku powkładane pięknie do klas. Gdy powstał PHP była końcówka XX wieku. W tym czasie było już kilka świetnych języków obiektowych, a twórcy PHP pozostali w latach 70. Ruby jest obiektowym językiem programowania. Wszystko w nim jest obiektem, do tego stopnia i nawet dalej, że można wykonać coś takiego s = 1.to_s(), co oznacza, że jedynka jest obiektem i wywołujemy na nim metodę. Tak więc mamy 3 do 0

- brak konwencji – wygląda na to, że PHP projektowało 100 osób i każda robiła po swojemu. Dzięki temu mamy między innym niejednolitą konwencję nazywania funkcji np. strstr, str_replace, ucfirst. Wszystkie trzy funkcje operują na String-ach, raz nazywają się z podkreśleniem, a raz bez, raz z przedrostkiem, a raz bez. Każdy musiał robić po swojemu. Takich perełek znajdzie się więcej. W Rubym konwencja nazewnictwa jest raczej zachowana. Chociaż podobnie jak w PHP w podstawowych klasach znajdują się metody o nazwach po których ciężko rozszyfrować do czego służą. Jak dla mnie jest 4 do 0.

- obsługa wyjątków – każdy szanujący się język programowania powinien mieć coś takiego, Dobrze by było, aby również to działało. W PHP z tego co wiem nie zawsze chce to działać. Co z tego, że wrzuciłem cały kod do bloku try catch jak i tak otrzymałem fatal error.

- PHP czyli jak to spotkało się na początku 3 programistów. Z moich obserwacji wynika, że podczas grubej imprezy w akademikach musiało wpaść na pomysł trzech programistów: Javy, C i Perla, aby zrobić nowy język programowania. Zabrali się oni do pracy zaraz gdy się obudzili i byli na dużym kacu. Mi nie podoba się składnia PHP, choć muszę przyznać, że przypadnie ona raczej do gustu programistom C bardziej niż składnia Rubego. Jak dla mnie dalej jest 4 do 0. Nikt nie dostaje punktu.

Czas na podsumowanie. Każdy w życiu ma raczej taki etap, w którym pisze w PHP. Ja ten etap mam za sobą i mam nadzieję, że nigdy nie powróci. Pomimo tego, że sam jestem negatywnie nastawiony do PHP muszę przyznać mu kilka punktów. 

Pierwszy punkt za możliwość wdrożenia aplikacji PHP. Ta technologia dostępna jest za darmo, prócz tego nie ma najmniejszego problemu ze znalezieniem serwera nawet za kilka złotych miesięcznie, który ma wbudowaną obsługę PHP. Jeden punkt dla PHP.

Drugi to wsparcie społeczności. Społeczność PHP jest spora. Bez problemu znajdziemy fora dla PeHaPowców, w miarę dobrą dokumentację i sporo bibliotek. Dla Ruby jest ciut gorzej. Często zdarza się, że bibliotekę dla Ruby znajdziemy i jest ona całkiem dobra ale gorzej już np. z dobrą dokumentacją. Nie ma co, punkt dla PHP

Wydajność. Nie rozumiem jak można było stworzyć wydajny i dość dobry interpreter PHP, który stoi zdecydowanie na wyższym poziomie niż sama koncepcja PHP, a wykombinować tak słaby język na poziomie konceptualnym.  W ostatnim czasie dla Ruby zrobiono sporo, aby pod względem wydajności zbliżyć się do czołówki, w tym do PHP. Tak więc w tym pojedynku nie ma punktów.

Wynik końcowy 4 do 2.

Jeżeli uważacie, że PHP to dobry język programowania bo łatwiej i szybciej można coś sensownego w nim napisać to polecam poczytać trochę o Ruby. Jestem fanem tego języka i polubiłem go bardzo szybko. Podczas pisania pierwszej większej aplikacji w Ruby przekonałem się, że jest on bardziej dla mnie intuicyjny niż PHP z którym znam się od liceum, chociaż moją przygodę z nim zacząłem kilka miesięcy wstecz.




poniedziałek, 4 października 2010

Własna baza danych SQLite w Android

System operacyjny Android stworzony dla urządzeń mobilnych takich jak: telefony komórkowe, tablety czy netbooki pozwala na skorzystanie z bazy danych SQLite. Sporo materiałów w Internecie pokazuje jak krok po kroku utworzyć bazę danych, a następnie wprowadzać do niej dane. W tym artykule chciałbym pokazać jak przenieść do telefonu istniejącą bazę danych. Zakładam, że plik z bazą danych może być większy niż 1MB, dlatego użyjemy kompresji. Android posiada ograniczenie, które powoduje, że nie można skorzystać z pliku zapisanego w katalogu assets większego niż 1 MB.

Zanim utworzymy archiwum .zip z bazą danych należy lekko zmodyfikować już istniejącą bazę. Plik z bazą należy otworzyć za pomocą odpowiedniego narzędzia, które pozwoli na dodanie tabeli oraz zmianę nazwy kluczy głównych. Jednym z takich narzędzi jest wtyczka dla przeglądarki internetowej Firefox o nazwie: SQLite Manager. Do istniejącej bazy należy wprowadzić nową tabelę za pomocą polecenia SQL:

CREATE TABLE "android_metadata" ("locale" TEXT DEFAULT 'en_US')

oraz dodać jeden wpis:

INSERT INTO "android_metadata" VALUES ('en_US')


Teraz dla wszystkich tabel należy zmienić klucz główny na _id;



Następnym krokiem jest utworzenie archiwum zip zawierającym jedynie plik z bazą danych. Kopiujemy go do folderu assets, który znajduje się w projekcie naszej aplikacji.



Teraz dodajemy do projektu nową klasę o nazwie: DataHelper, która rozszerza klasę: SQLiteOpenHelper. Do podanej klasy kopiujemy poniższy kod.



package org.axlinux.android;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class DataHelper extends SQLiteOpenHelper
{

private static String DATABASE_DIR = "/data/data/org.axlinux.android/databases/"; //Ścieżka do katalogu gdzie przechowywane są bazy danych
private static String DATABASE_NAME = "my_books.sqlite"; //Nazwa pliku z bazą danych
private static String DATABASE_ZIP = "my_books.sqlite.zip"; //Nazwa pliku zip w którym znajduje się skompresowany plik z bazą danych
private SQLiteDatabase db;
private final Context context;

public DataHelper(Context context)
{
super(context, DATABASE_NAME, null, 1);
this.context = context;
}

@Override
public void onCreate(SQLiteDatabase arg0)
{
//Implementację tej metody można pominąć

}

@Override
public void onUpgrade(SQLiteDatabase arg0, int arg1, int arg2)
{
//Implementację tej metody można pominąć

}

/**
* Zadaniem metody jest sprawdzenie czy istnieje plik z bazą danych
* @return true jeżeli plik w katalogu istnieje,
* false gdy nie istnieje - nie został jeszcze skopiowany z assets do katalogu w którym android przechowuje pliki baz danych
*/
public boolean dataBaseExists()
{
File f = new File(DATABASE_DIR);
if(!f.exists())
f.mkdir();

boolean result = true;
SQLiteDatabase db_test = null;

try{
db_test = SQLiteDatabase.openDatabase(DATABASE_DIR+DATABASE_NAME, null, SQLiteDatabase.OPEN_READONLY);
} catch (Exception ex){
result = false;
}

if(db_test != null)
db_test.close();


return result;
}

/**
* Metoda rozpakowuje plik z assets do katalogu gdzie Android trzyma bazy danych pod warunkiem, że już nie zostało to zrobione
* @throws IOException
*/
public void copyDataBase() throws IOException
{
if(!this.dataBaseExists())
{
File f = new File(DATABASE_DIR);
if(!f.exists())
f.mkdir();

ZipInputStream zis = new ZipInputStream(context.getAssets().open(DATABASE_ZIP));
ZipEntry entry;

entry = zis.getNextEntry();

int BUFFER = 2048;
FileOutputStream fos = new FileOutputStream(DATABASE_DIR + DATABASE_NAME);
BufferedOutputStream dest = new BufferedOutputStream(fos, BUFFER);
int count;
byte data[] = new byte[BUFFER];

while ((count = zis.read(data, 0, BUFFER)) != -1)
dest.write(data, 0, count);


dest.flush();
dest.close();
zis.close();
}
}

/**
* Metoda otwiera bazę danych, po wywołaniu tej metody można wykonywać polecenie SQL
* @return - true gdy otwarcie się powiodło
*/
public boolean open()
{
boolean result = false;

try{
String myPath = DATABASE_DIR + DATABASE_NAME;
db = SQLiteDatabase.openDatabase(myPath, null, SQLiteDatabase.OPEN_READWRITE);
result = true;
} catch (Exception ex) {
//Otwarcie bazy danych nie powiodło się}
}

return result;
}

public synchronized void close() {
if(db != null)
db.close();
super.close();

}

/**
* Wykonuje zapytanie SQL
* @param query - zapytanie SQL
* @return zwraca Stringa z rezultatem
*/
public String executeQuery(String query)
{
String result = "";
Cursor cursor = db.rawQuery(query, null);

if(cursor.moveToFirst())
{
do
{
result += cursor.getString(0) +". "+ cursor.getString(1) +" "+ cursor.getString(2) +"\n";
}while(cursor.moveToNext());
}

return result;
}
}



Klasa z metodą onCreate rozszerzająca klasę Activity



package org.axlinux.android;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class Main extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

String out ="";

TextView tv = (TextView) this.findViewById(R.id.TextView01);
DataHelper dh = new DataHelper(this);

out += "Czy istnieje baza danych? " + dh.dataBaseExists()+"\n";
try
{
dh.copyDataBase();
out += "Czy istnieje baza danych po skopiowaniu? " + dh.dataBaseExists()+"\n";
out +="Rezultat otworzenia bazy danych: " + dh.open()+"\n";
out += "Wynik zapytania \n" + dh.executeQuery("SELECT * FROM books limit 3;");
} catch (Exception ex)
{
out += ex.toString() +"\n";
}

tv.setText(out);

}
}



Pierwszym krokiem po utworzeniu nowej klasy jest modyfikacja pól klasy. Należy odpowiednio zmodyfikować wartości: DATABASE_DIR, DATABASE_NAME, DATABASE_ZIP.

DATABASE_DIR to miejsce gdzie Android przechowuje pliki z bazami danych. Należy poprawnie wprowadzić nazwę głównego pakietu. DATABASE_NAME to nazwa naszej bazy danych, która jest umieszczona wewnątrz archiwum zip, a DATABASE_ZIP to nazwa archiwum z bazą danych w katalogu assets.


W metodzie onCreate możemy teraz stworzyć instancję klasy DataHelper i wywołać metodę copyDataBase. Po zakończeniu warto wywołać metodę dataBaseExists() w celu sprawdzenia rezultatu.