Wprowadzenie
Język JAVA jest niewątpliwie najbardziej rozwijającym się obecnie
środowiskiem tworzenia aplikacji. Czerpie on to co najlepsze z takich języków jak
C++ czy Smaltalk przy zdecydowanie prostszej i bardziej czytelnej składni
(konstrukcji) programów. Zawiera elementy programowania zarówno strukturalnego
jak i obiektowego, zdarzeniowego jak i współbieżnego. Poprzez standardowe jak i
rozszerzone biblioteki wkracza w różnorodne rejony zastosowań takie jak np. karty
inteligentne i elektronika, systemy zarządzania bazami danych, obsługa
multimediów, Internet, grafika 3D, kryptografia, itd. Co więcej JAVA jest
niespotykanie bezpiecznym środowiskiem i umożliwia w znaczny sposób kontrolę i
sterowanie bezpieczeństwem. Zdecydowanie różni się od innych języków trzeciej
generacji tym, że jest językiem interpretowanym a nie kompilowanym. Oznacza to, że
powstały w wyniku kompilacji kod wynikowy nie jest programem jaki można
niezależnie uruchomić lecz stanowi tzw. Beta-kod, który jest interpretowany przez
Maszynę Wirtualną (JavaVM) pracującą w określonym środowisku. Ze względu na
kod nie istotne jest na jakim sprzęcie będzie uruchamiana aplikacja. Ważna jest tylko
Maszyna Wirtualna. Jest to niezwykle ciekawy pomysł umożliwiający odcięcie się od
wszystkich poziomów sprzętowo-programowych będących poniżej Maszyny
Wirtualnej. Koncepcja ta jest powszechna również w samym języku JAVA, dzięki
czemu poprzez stworzenie abstrakcyjnych klas i metod podstawowe biblioteki Javy
nie muszą być nieustannie rozbudowywane.
JAVA jest niewątpliwie językiem najbliższej przyszłości, warto więc poświęcić mu
trochę czasu.
Java platformą tworzenia i wykonywania aplikacji
Platformą nazywa się przeważnie pewną kombinację sprzętu i
oprogramowania umożliwiającą tworzenie i wykonywanie programów. Przyjęło się
powszechnie mówić o tzw. platformie sprzętowo-programowej. Platformę stanowi
więc komputer o danej konfiguracji oraz system operacyjny w środowisku którego
uruchamiana jest dowolna aplikacja. Przykładowe platformy to Intel PC + Windows
NT; Sun Ultra + Solaris; SGI O2 + Irix 6.4, itp. Konstrukcja platformy Javy jest
podobna, niemniej nie odnosi się bezpośrednio do sprzętu i systemu operacyjnego,
które stanowią dla niej pewną abstrakcję. Istotą platformy Javy jest zbiór dwóch
elementów: Java API (Application Programming Interfaces) - interfejsy tworzenia
aplikcaji oraz JavaVM (Virtual Machine) - maszyna wirtualna. Maszyna wirtualna
Javy jest rozwinięciem dotychczas używanego pojęcia platformy, stanowiąc pewną
nadbudowę. Maszyna wirtualna interpretuje kod wynikowy (Beta-kod) Javy do kodu
wykonywalnego danego systemu operacyjnego i komputera, którego jest
nadbudową. Oznacza to, że Maszyna Wirtualna jest interfejsem pomiędzy
uniwersalnym kodem Javy, a różnymi konfiguracjami komputerów. Ta różnorodność
systemów komputerowych wymaga różnorodności Maszyn Wirtualnych. Firma Sun
dostarcza obecnie Maszynę Wirtualną wersji Java 2 dla systemów operacyjnych
Windows95/98/NT oraz Solaris. Oczywiście interpretacja kodu właściwa dla danego
systemu operacyjnego (konwersja w locie Beta-kodu do kodu wykonywalnego)
wymaga odpowiednich bibliotek. Biblioteki klas, metod, pól, itp., zarówno te zależne
sprzętowo jak i te niezależne sprzętowo stworzone już w Javie znajdują się w postaci
skompilowanej w Java API. Podsumowując Java VM oraz Java API tworzą platformę
Javy zwane często środowiskiem uruchomieniowym aplikacji - Java Runtime Engine
(JRE).
Java - środowisko tworzenia aplikacji
Aby można było uruchomić aplikację na platformie Javy trzeba ją najpierw
stworzyć, po czym skompilować do Beta-kodu. Posługując się regułami języka Java
oraz zapleczem klas i metod powstaje kod źródłowy programu. W celu generacji
Beta-kodu program ten podaje się następnie kompilacji (np. kompilatorem java
firmy Sun w pełni stworzonego za pomocą języka Java). Dla potrzeb tworzenia
aplikacji SUN oferuje pakiet Java Development Kit (JDK lub Java SDK - Software
Development Kit), który składa się z JRE oraz narzędzi kompilacji i bibliotek.
Java - język programowania
Stworzenie programu w Javie polega na umiejętnym wykorzystaniu
znajomości reguł języka oraz bibliotek. Konstrukcja programu i reguły języka
przypominają znacznie język C. Programiści znający ten język z łatwością i
przyjemnością rozpoczną pracę z Javą. Dla znawców języka C miła będzie
informacja, że w Javie nie używa się w ogóle wskaźników, statycznego rezerwowania
i zwalniania pamięci itp. Program w Javie nie zawiesi się więc z uwagi na Null
Pointer Assigment. Bardzo istotne w konstrukcji programu jest znajomość
obsługiwanych typów. Ponieważ kod Javy jest niezależny od sprzętu, to również typy
danych są niezależne od sprzętu (platformy). Java jest językiem obiektowym, dzięki
czemu kod jest uniwersalny i bardzo czytelny. Nowością jaką niesie ze sobą Java
jest również tworzenie tzw. apletów. Aplet jest programem wykonywanym w
określonych ramach (nadbudowie Maszyny Wirtualnej). Aplet ma więc takie
możliwości jakie nadaje mu program uruchomieniowy. Przykładowe programy
uruchomieniowe to przeglądarki WWW np. Netscape, Internet Explorer. Kolejnym
nowym elementem konstrukcyjnym Javy jest to, że można kod grupować w liczne
wątki, które w wyniku interpretacji tworzą niezależnie wykonywane procesy
współdzielące czas procesora.
Opis środowiska Java 2 SDK
Proces tworzenia aplikacji Javy z pomocą dostarczanego przez Suna
środowiska Java 2 SDK można przedstawić następująco:
1. Napisanie z pomocą dowolnego edytora tekstu kodu źródłowego programu
zawierającego klasę publiczną o nazwie takiej samej (dokładnie takiej samej z
uwzględnieniem wielkości znaków) jak docelowa nazwa programu np. RycerzJedi.
2. Nagranie kodu źródłowego jako pliku o danej nazwie z rozszerzeniem .java, np.
RycerzJedi.java
3. Kompilacja kodu źródłowego zawartego w pliku z rozszerzeniem .java do pliku
docelowego o rozszerzeniu .class zawierającego Beta-kod np.
c:\ javac RycerzJedi.java
gdzie:
javac - nazwa komilatora programów Javy stworzonego przez Suna
(kompilator napisany w Javie),
RycerzJedi.java - kod źródłowy programu do kompilacji (WAŻNE: podana
nazwa pliku musi zawierać rozszerzenie .java).
W Wyniku kompilacji powstanie plik lub zestaw plików z tym samym trzonem nazwy o
rozszerzeniu .class, np. RycerzJedi.class.
4. Uruchomienie w środowisku interpretatora Beta-kodu, np.
c:\ java RycerzJedi
gdzie:
java - nazwa interpretatora Javy stworzonego przez Suna, inaczej
uruchomienie Maszyny Wirtualnej,
RycerzJedi - nazwa pliku z Beta-kodem programu w Javie kompilacji
(WAŻNE: podana nazwa pliku nie może zawierać rozszerzenia .class).
W celu kompilacji i uruchomienia programu napisanego w języku Java użyto w
powyższym przykładzie dwóch podstawowych narzędzi pakietu Java 2 SDK: javac
oraz java. Kompilator javac (często nazywany Jawak) jest nieodzowną częścią
pakietu SDK, podczas gdy interpretator java stanowi specyficzną dl adanej
platformy część pakietu środowiska uruchomieniowego Java Runtime Engine.
Wynika stąd, że po instalacji pakietu Java SDK interpretator java będzie znajdował
się zarówno w części JRE (niezależnej od tworzenia aplikacji) jak i w zbiorze
narzędzi tworzenia aplikacji. Przykładowo katalog zawierający Java 2 SDK wygląda
następująco:
< DIR > bin
< DIR > demo
< DIR > include
< DIR > include-old
< DIR > jre
< DIR > lib
935 COPYRIGHT
8˙762 LICENSE
6˙010 README
9˙431 README.html
16˙715˙279 src.jar
313˙746 Uninst.isu
W katalogu bin znajdują się liczne narzędzia obsługi aplikacji np:
javac - kompilator,
java - interpretator z konsolą ,
javaw - interpretator bez konsoli,
javadoc - generator dokumentacji API,
appletviewer - interpretator apletów,
jar - zarządzanie plikami archiwów (JAR),
jdb - debager,
Ze względu na to, że pisząc programy w Javie często korzysta się z narzędzi
znajdujących się w katalogu bin, warto ustawić w środowisku ścieżkę dostępu do
tego katalogu. Narzędzia dostępne w tym katalogu można wywoływać z licznymi
opcjami. Praktycznie jednak najbardziej przydatne opcje to:
*javac:
-g ->wyświetl pełną informację debagera,
- verbose ->wyświetl wszystkie komunikaty w czasie kompilacji,
np. javac -g -verbose RycerzJedi.java
*java:
-cp -classpath -> gdzie -classpath katalog zawierający wykorzystywane klasy
użytkownika (lepiej ustawić zmienną środowiska CLASSPATH),
-version ->wyświetl wersję platformy Javy.
Drugim ważnym katalogiem jest katalog jre. Jak łatwo się domyślić w katalogu tym
znajduje się Java Runtime Environment JRE - platforma Javy. Zgodnie z tym co
powiedziano na początku platforma Javy składa się z Maszyny Wirtualnej oraz
bibliotek API. Dlatego katalog jre podzielony jest na dwa podkatalogi: bin - w
którym znajduje się interpretator java (ten sam co wcześniej) oraz : lib gdzie
znajdują się spakowane biblioteki API oraz pliki konfiguracyjne i środowiskowe
platformy (np. określające poziom bezpieczeństwa, czcionki, fonty, itp.).
Uwaga praktyczna:
W czasie nauki języka Java lepiej unikać wszelkiego rodzaju programów typu
szybkiego tworzenia aplikacji, gdyż traci się często kontrolę nad zrozumieniem treści
tworzonego programu.
Integralną częścią środowiska Javy są biblioteki. Korzystanie z bibliotek jest znacznie
prostsze jeśli rozumie się jak z nich korzystać. Nieodzownym jest więc korzystanie z
dokumentacji bibliotek API. Opis bibliotek jest dostępny oddzielnie względem
środowiska JDK i stworzony jest jako serwis WWW, który można przeglądać on-line
lub off-line. Dokumentacja zawiera spis wszystkich pakietów, klas, ich pól i metod
oraz wyjątków wraz z odpowiednimi opisami.
Tworzenie i obsługa programów
Klasy obiektów
Ponieważ Java jest językiem obiektowym operuje na pojęciach ogólnych.
Znane z historii filozofii pojęcie uniwersalii, a więc pewnych klas ogólnych bytów np.
człowiek, małpa, rycerz Jedi, jest stosowane w Javie (jak i w innych językach
obiektowych). Klasa bytów (class) określa opis bytu (stałe, zmienne, pola) oraz jego
zachowanie (metody). Przykładowo klasę ludzi można opisać poprzez parametry:
kolor oczu, rasę, kolor włosów, itp., (pola klasy) ; oraz poprzez zachowanie: chodzi,
oddycha, itp, (metody klasy). Obiektem jest konkretna realizacja klasy, a więc np.
konkretny człowiek - Kowalski, a nie sama klasa. Można upraszczając podać
generalny podział na rodzaj kodu strukturalny i obiektowy. W ujęciu strukturalnym w
celu określenia zbioru konkretnych osób należy zdefiniować zbiór (wektor, macierz)
klas (struktur), gdzie każda klasa opisuje jedną osobę. W podejściu obiektowym w
celu określenia zbioru konkretnych osób należy zainicjować zbiór obiektów tej samej
klasy. Odwołanie się do parametru klasy w podejściu strukturalnym odbędzie się
poprzez odniesie przez nazwę odpowiedniej klasy, natomiast w podejściu
obiektowym poprzez nazwę obiektu. Oznacza to, że w aplikacjach obiektowych bez
inicjacji obiektu nie można odwołać się do parametrów (pól) i metod klasy. Wyjątkiem
są te pola i metody, które oznaczone są jako statyczne (static). Pole statyczne, bez
względu na liczbę obiektów danej klasy, będzie miało tylko jedną wartość. Pola i
metody statyczne mogą być wywoływane przez referencję do klasy. Po tym
podstawowym wprowadzeniu zapoznajmy się z zasadniczą konstrukcją programu w
Javie (rozważania dotyczące Javy jako Języka obiektowego znajdują się w dalszej
części tego dokumentu).
Klasę w Javie konstruuje się zaczynając od stworzenia ramy klasy:
class NazwaKlasy {
// pola
typ_zmiennej zmienna :
.
.
.
typ_zmiennej zmienna:
// konstruktor - metoda o tej samej nazwie co klasa - wywoływana automatycznie
// przy tworzeniu obiektu danej klasy
NazwaKlasy(typ_argumentu nazwa_argumentu){ treść konstruktora :
}
// metofdy
typ_wartości_zwracanej mazwa_metody (typ_argumentu nazwa_argumentu){
treść_metody:}
// koniec class NazwaKlasy
Przykład
//RycerzJedi.java
//klasa
class RycerzJedi{
//pola
String nazwa;
String kolor_miecza;
//konstruktor
RycerzJedi(String nazwa, String kolor_miecza){
this.nazwa=nazwa;
this.kolor_miecza=kolor_miecza;
}
//metody
void opis(){
System.out.println(Rycerz +nazwa+ ma +kolor_miecza+ miecz.);
}
}// koniec class RycerzJedi
Warto zwrócić uwagę na zastosowaną już metodę notacji. Otóż przyjęło się w Javie
(specyfikacja języka Java) oznaczanie nazwy klasy poprzez stosowanie wielkich liter
dla pierwszej litery i tych, które rozpoczynają nowe znaczenie w nazwie klasy.
Metody i obiekty oznacza się z małych liter. Warto również stosować prosty
komentarz rozpoczynający się od znaków // i mający zasięg jednego wiersza. Dalej
w tekście zostaną omówione inne, ważne zagadnienia związane z konstrukcją i
opisem kodu.
Przedstawiona powyżej rama i przykład klasy nie mogą być wprost uruchomione.
Konieczne jest utworzenie elementu wywołującego określone działanie. W Javie jest
to możliwe na dwa sposoby. Po pierwsze za pomocą aplikacji, po drugie appletu.
Aplikacje
Aplikacja w języku Java to zdefiniowana klasa publiczna wraz z jedną ściśle
określoną metodą statyczną o formie:
public staticvoin main (String atgd[]){
} // koniec public static void main(String argd[])
Tablica args[] będąca argumentem metody main() jest zbiorem argumentów
wywołania aplikacji w ciele której znajduje się metoda. Kolejność argumentów jest
następująca: argument 1 wywołania umieszczany jest w args[0], argument 2
wywołania umieszczany jest w args[1], itd. Występująca nazwa args oznacza
dynamicznie tworzony obiekt, który zawiera args.length elementów typu łańcuch
znaków. Pole length oznacz więc liczbę elementów tablicy. Łatwo jest więc określić
argumenty oraz ich liczbę korzystając z obiektu args. Warto zwrócić uwagę na to,
że oznaczenie length jest własnością tablicy (ukrytym polem każdej tablicy) a nie
metodą obiektu args. Istnieje metoda klasy String o nazwie length() (metody
zawsze są zakończone nawiasami pustymi lub przechowującymi argumenty), łatwo
więc się pomylić. Dla zapamiętania różnicy posłużmy się prostym przykładem
wywołania:
args.length - oznacz ilość elementów tablicy args;
args[0].length() -oznacza rozmiar zmiennej tekstowej o indeksie 0 w tablicy args.
Prosta aplikacja działająca w Javie będzie miała następującą formę:
// Jedi.java :
public class Jedi{
public static void main(String args[]){
System.out.println(Rycerz Luke ma niebieski miecz.);
}// koniec public static void main(String args[])
}// koniec public class Jedi
Nagrywając powyższy kod do plik Jedi.java, kompilując poprzez podanie polecenia:
javac -g -verbose Jedi.java (pamiętać należy że kompilację należy wykonać z
scieżki występowania pliku źródłowego, lub należy mieć odpowiednio ustawioną
zmienną środowiska CLASSPATH) można wykonać powstały Beta-kod Jedi.class
używając interpretatora: java Jedi. Uzyskamy następujący efekt:
Rycerz Luke ma niebieski miecz.
Jak działa aplikacja z przykładu ? Otóż w ustalonej ramie klasy, w metodzie
programu uruchomiono polecenie wysłania do strumienia wyjścia (na standardowe
urządzenie wyjścia - monitor) łańcuch znaków Rycerz Luke ma niebieski miecz..
Konstrukcja ta wymaga krótkiego komentarza. Słowo System występujące w
poleceniu wydruku oznacza statyczne odwołanie się do elementu klasy System. Tym
elementem jest pole o nazwie out, które stanowi zainicjowany obiekt typu
PrintStream (strumień wydruku). Jedną z metod klasy PrintStream jest metoda o
nazwie println, która wyświetla w formie tekstu podaną wartość argumentu oraz
powoduje przejście do nowej linii (wysyłany znak końca linii). W podanym przykładzie
nie ukazano jawnie tworzenia obiektu. Można więc powyższy przykład nieco
rozwinąć.
// Jedi1.java :
//klasa
class RycerzJedi{
//pola
String nazwa;
String kolor_miecza;
//konstruktor
RycerzJedi(String nazwa, String kolor_miecza){
this.nazwa=nazwa;
this.kolor_miecza=kolor_miecza;
}
//metody
void opis(){
System.out.println(Rycerz +nazwa+ ma +kolor_miecza+ miecz.);
}
}// koniec class RycerzJedi
public class Jedi1{
public static void main(String args[]){
RycerzJedi luke = new RycerzJedi(Luke, niebieski);
RycerzJedi ben = new RycerzJedi(Obi-wan,biały);
luke.opis();
ben.opis();
}// koniec public static void main(String args[])
}// koniec public class Jedi1
Zapiszmy kod aplikacji pod nazwą Jedi1.java, a następnie wykonajmy kompilację z
użyciem polecenia: javac -g -verbose Jedi1.java. Zwróćmy uwagę na wyświetlane w
czasie kompilacji komunikaty. Po pierwsze kompilator ładuje wykorzystywane przez
nas klasy. Klasy te znajdują się w podkatalogu lib katalogu jre (biblioteki Java
Runtime Engine). Klasy są pogrupowane w pakiety tematyczne np. lang czy io.
Wszystkie grupy klas znajdują się w jednym zbiorze o nazwie java. Całość jest
spakowana metodą JAR (opracowaną dla Javy) do pliku rt.jar. Wszystkie
prekomilowane klasy znajdujące się w pliku rt.jar stanowią podstawowe biblioteki
języka Java. Biblioteki te ujęte w pakiety wywoływane są następująco:
np.:
java.lang.*; - oznacza wszstkie klasy (interfejsy i wyjątki - o czym później) grupy lang,
głównej biblioteki języka Java,
java.io.*; - oznacza wszstkie klasy (interfejsy i wyjątki - o czym później) grupy io
(wejście/wyjście), głównej biblioteki języka Java,
java.net.Socket; - oznacza klasę Socket grupy net (sieć), głównej biblioteki języka
Java.
Wykorzystując różne klasy biblioteki języka Java należy pamiętać w jakim pakiecie
się znajdują. Wszystkie klasy poza tymi zawartymi w pakiecie java.lang.*
wymagają zastosowania jawnego importu pakietu (klasy) w kodzie programu.
Odbywa się to poprzez dodanie na początku kodu źródłowego linii:
np.:
import java.net.*;
Po dodaniu takiej linii kompilator wie gdzie szukać używanych w kodzie klas. Pakiety
zaczynające się od słowa java oznaczają zasadnicze pakiety języka Java. Niemniej
możliwe są różne inne pakiety, których klasy można stosować np.:
org.omg.CORBA. Nazewnictwo pakietów jest określone w specyfikacji języka i
opiera się o domeny Internetowe. Oczywiście można tworzyć własne pakiety.
Występujące w nazwie pakietu kropki oznaczają zmianę poziomu w drzewie
katalogów przechowywania klas. Przykładowo org.omg.CORBA.Context oznacza,
że w katalogu org, w podkatalogu omg, w podkatalogu CORBA znajduje się
klasa Context.class; czyli zapis pakietu jest równoważny w sensie systemu plików:
/org/omg/CORBA/Context.class.
Obserwując dalej komunikaty wyprodukowane przez kompilator Javy łatwo
zauważyć, że zapisane zostały dwie klasy: RycerzJedi.class oraz Jedi1.class.
Oddzielnie definiowane klasy zawsze będą oddzielnie zapisywane w Beta-kodzie.
Po kompilacji należy uruchomić przykład W tym celu wywołajmy interpretator:
java Jedi1. Zdarza się czasem, że wykorzystywany Beta-kod klas pomocniczych
(np. klasa RycerzJedi.class) znajduje się w innym miejscu dysku. Wówczas
otrzymamy w czasie wywołania aplikacji błąd o niezdefiniowanej klasie. Należy wtedy
ustawić odpowiednio zmienną środowiska CLASSPATH lub wywołać odpowiednio
interpretator:
java -cp <ścieżka dostępu do klas> NAZWA_KLASY_GŁÓWNEJ.
Prawidłowe działanie aplikacji z przykładu da następujący rezultat:
Rycerz Luke ma niebieski miecz.
Rycerz Obi-wan ma biały miecz.
Prześledźmy konstrukcję kodu źródłowego w przykładzie 1.3. Kod składa się z
dwóch klas: RycerzJedi i Jedi1. Klasa RyczerzJedi nie posiada metody main, a więc
nie jest główną klasą aplikacji. Klasa ta jest wykorzystywana dla celów danej aplikacji
i jest skonstruowana z pól, konstruktora i metody. Pola oznaczają pewną własność
obiektów jakie będą tworzone: nazwa - łańcuch znaków oznaczający nazwę rycerza
Jedi; kolor_miecza - łańcuch znaków oznaczający kolor miecza świetlnego rycerza
Jedi. Konstruktor jest metodą wywoływaną automatycznie przy tworzeniu obiektu.
Stosuje się go do podawania argumentów obiektowi, oraz do potrzebnej z punktu
widzenia danej klasy grupy operacji startowych. Wywołanie konstruktora powoduje
zwrócenie referencji do obiektu danej klasy. Nie można więc deklarować konstruktora
z typem void. W rozpatrywanym przykładzie konstruktor posiada dwa argumenty o
nazwach takich samych jak nazwy pól. Oczywiście nie jest konieczne stosowanie
takich samych oznaczeń. Jednak w celach edukacyjnych zastosowano tu te same
oznaczenia, żeby pokazać rolę słowa kluczowego this. Słowo this oznacza obiekt
klasy w ciele której pojawia się this. Stworzenie więc przypisania this.nazwa=nazwa
powoduje przypisanie zmiennej lokalnej nazwa do pola danej klasy nazwa. Klasa
RycerzJedi ma również zdefiniowaną metodę opis, która wyświetla własności obiektu
czyli treść pól. Druga klasa Jedi1 zawiera metodę main(), a więc jest główną klasą
aplikacji. W metodzie main() zawarto linie inicjowania obiektów np.:
Inicjowanie to polega na stworzeniu obiektu danej klasy, poprzez użycie słowa
kluczowego new i wywołanie konstruktora klasy z właściwymi argumentami. W
podanym przykładzie tworzony jest obiekt klasy RycerzJedi o nazwie luke i
przypisuje się mu dane właściwości. Po stworzeniu dwóch obiektów w klasie Jedi1
następuje wywołanie metod opis() dla danych obiektów. Tak stworzona konstrukcja
aplikacji umożliwia w sposób prosty skorzystanie z klasy RycerzJedi dowolnej innej
klasie, aplikacji czy w aplecie.
Aplety
Oprócz aplikacji możliwe jest wywołanie określonego działania poprzez aplet.
Aplet jest formą aplikacji wywoływanej w ściśle określonym środowisku. Aplet nie jest
wywoływany wprost przez kod klasy *.class lecz poprzez plik HTML w kodzie którego
zawarto odniesienie do kodu apletu *.class, np.:
< /aplet >
Zapis ten oznacza, że w oknie o szerokości 200 i wysokości 100 będzie uruchomiony
aplet o kodzie Jedi2.class. Zasadniczo aplet jest programem graficznym, stąd też
wyświetlany tekst musi być rysowany graficznie. Polecenie:
System.out.println("Rycerz Luke ma niebieski miescz...");;;
nie spowoduje wyświetlenia tekstu w oknie apletu, lecz wyświetli tekst w konsoli
Javy jeśli do takiej mamy dostęp. Uruchamiając aplet należy mieć Beta-kod Javy
oraz plik odwoławczy w HTML.
Tworząc aplet tworzymy klasę dziedziczącą z klasy Applet, wykorzystując
podstawowe metody takie jak init(), start(), paint(), stop() i destroy(). Wywołując
aplikację wywołujemy metodę main(), wywołując natomiast aplet wywołujemy
przedstawione wyżej metody w podanej kolejności. Metody init() i destroy() są
wykonywane jednorazowo (można uznać metodę init() za konstruktor apletu). Metody
start(), paint(), stop() mogą być wykonywane wielokrotnie. Ponieważ aplet korzysta z
klasy Applet oraz metod graficznych konieczne jest importowanie określonych
pakietów. Rozpatrzmy następujący przykładOprócz aplikacji możliwe jest wywołanie określonego działania poprzez aplet.
Aplet jest formą aplikacji wywoływanej w ściśle określonym środowisku. Aplet nie jest
wywoływany wprost przez kod klasy *.class lecz poprzez plik HTML w kodzie którego
zawarto odniesienie do kodu apletu *.class, np.:
< aplet code = Jedi2.clas width = 200 height =100>
< /aplet >
Zapis ten oznacza, że w oknie o szerokości 200 i wysokości 100 będzie uruchomiony
aplet o kodzie Jedi2.class. Zasadniczo aplet jest programem graficznym, stąd też
wyświetlany tekst musi być rysowany graficznie. Polecenie:
System.out.println("Rycerz Luke ma niebieski miecz.");
nie spowoduje wyświetlenia tekstu w oknie apletu, lecz wyświetli tekst w konsoli
Javy jeśli do takiej mamy dostęp. Uruchamiając aplet należy mieć Beta-kod Javy
oraz plik odwoławczy w HTML.
Tworząc aplet tworzymy klasę dziedziczącą z klasy Applet, wykorzystując
podstawowe metody takie jak init(), start(), paint(), stop() i destroy(). Wywołując
aplikację wywołujemy metodę main(), wywołując natomiast aplet wywołujemy
przedstawione wyżej metody w podanej kolejności. Metody init() i destroy() są
wykonywane jednorazowo (można uznać metodę init() za konstruktor apletu). Metody
start(), paint(), stop() mogą być wykonywane wielokrotnie. Ponieważ aplet korzysta z
klasy Applet oraz metod graficznych konieczne jest importowanie określonych
pakietów. Rozpatrzmy następujący przykład
// Jedi2.java :
import java.applet.Applet;
import java.awt.*;
public class Jedi2 extends Applet{
public void paint(Graphics g){
g.drawString(Rycerz Luke ma niebieski miecz., 15,15);
}
} // koniec public class Jedi2.class extends Applet
------------------------------------------------------------------
Kompilacja kodu Jedi2.java odbywa się tak jak dla kodu aplikacji. Uruchomienie
natomiast apletu wymaga otwarcia kodu HTML w przeglądarce WWW zgodnej z
Javą lub za pomocą programu appletviewer dostarczanego w Java 2 SDK.
Uruchomienie apletu odbywa się wówczas przez podanie:
apletviewer Jedi2.html
Pojawia się wówczas okno apletu o podanych w pliku Jedi2.html rozmiarach z
napisem generowanym przez metodę drawString(). Kod apletu z przykładu zawiera jedynie implementację metody paint(), wywołanie
której generuje kontekst graficzny urządzenia Graphics g, dzięki któremu możliwe
jest użycie metody graficznej drawString().
Ponieważ aplety są tylko specyficzną formą użycia klas dlatego apletami zajmiemy
się w dalszej części tego opracowania.
Aplikacja i aplet w jednym kodzie
Poniższy obrazuje możliwość stworzenia w jednym kodzie źródłowym
zarówno aplikacji jak i apletu. W zależności od metody interpretacji Beta-kodu (np.
wykorzystanie java lub appletviewer) otrzymamy zaprogramowany efekt.
// JediW.java:
import java.applet.Applet;
import java.awt.*;
class RycerzJedi{
//pola
String nazwa;
String kolor_miecza;
//konstruktor
RycerzJedi(String nazwa, String kolor_miecza){
this.nazwa=nazwa;
this.kolor_miecza=kolor_miecza;
}
//metody
void opis(){
System.out.println(Rycerz +nazwa+ ma +kolor_miecza+ miecz.);
}
}// koniec class RycerzJedi
public class JediW extends Applet {
public void paint(Graphics g){
g.drawString(Rycerz Luke ma niebieski miecz., 15,15);
}
public void init(){
RycerzJedi luke = new RycerzJedi(Luke, niebieski);
RycerzJedi ben = new RycerzJedi(Obi-wan,biały);
luke.opis();
ben.opis();
}
public static void main(String args[]){
JediW j = new JediW();
j.init();
}// koniec public static void main(String args[])
}// koniec public class JediW
------------------------------------------------------------------
W celu stworzenia uniwersalnego kodu należy zadeklarować główną klasę publiczną
jako klasę dziedziczącą z klasy Applet, a następnie zdefiniować podstawowe metody
apletu (init(), paint()) i aplikacji (main()).
Interpretacja, kompilacja i obsługa klas w Javie
Wywołanie programu stworzonego w Javie i obserwacja efektów jego
działania jest dziedziną użytkownika programu. Programista powinien również
wiedzieć jaka jest metoda powstawania kodu maszynowego danej platformy czyli jak
jest droga pomiędzy kodem źródłowym a kodem wykonywanym. W sposób celowy
użyto to sformułowania kod wykonywany zamiast kod wykonywalny aby ukazać,
że nie zawsze użytkownik programu ma do dyspozycji kod wykonywalny na daną
platformę, lecz czasami kod taki jest generowany w trakcie uruchamiania programu
(np. wykonywanie programów stworzonych w Javie). W celu wyjaśnienia
mechanizmu generacji kodu maszynowego danej platformy dla programów
stworzonych w Javie przedstawić trzeba najpierw kilka podstawowych zagadnień
związanych z kodem maszynowym.
Jak wiadomo kod maszynowy jest serią liczb interpretowaną przez komputer
(procesor) w celu wywołania pożądanego efektu. Posługiwanie się ciągiem liczb w
celu wywołania określonego działania nie jest jednak zbyt efektywne, a na pewno nie
jest przyjazne użytkownikowi. Opracowano więc prosty język, który zawiera proste
instrukcje mnemoniczne np. MOV, LDA, wywoływane z odpowiednimi wartościami
lub parametrami. Przygotowany w ten sposób kod jest tłumaczony przez komputer
na kod maszynowy. Język, o którym tu mowa to oczywiście asembler (od
angielskiego assembly - gromadzić, składać ). Wyrażenia napisane w asemblerze są
tłumaczone na odpowiadające im ciągi liczb kodu maszynowego. Oznacza to, że
jedna linia kodu asemblera (wyrażenie) generuje jedną linie ciągu liczb np. LDA A, J
jest tłumaczone na 1378 00 1000, o ile kod LDA to 1378, akumulator to 00 a zmienna
J jest pod adresem J. Kolejnym krokiem w stronę programisty było stworzenie
języków wysokiego rzędu jak np. C, dla których pojedyncza linia kodu źródłowego
tego języka może być zamieniona na klika linia kodu maszynowego danej platformy.
Proces konwersji kodu źródłowego języka wysokiego poziomu do kodu
maszynowego (wykonywalnego) danej platformy nazwano kompilacją (statyczną).
Kompilacja składa się z trzech podstawowych procesów: tłumaczenia kodu
źródłowego, generacji kodu maszynowego i optymalizacji kodu maszynowego.
Tłumaczenie kodu źródłowego polega na wydobyciu z tekstu źródłowego programu
elementów języka np. if, while, (, class; a następnie ich łączeniu w wyrażenia
języka. Jeżeli napotkane zostaną niezrozumiałe elementy języka lub wyrażenia, nie
będą zgodne z wzorcami danego języka, to kompilator zgłasza błąd kompilacji. Po
poprawnym tłumaczeniu kodu źródłowego wyrażenia podlegają konwersji na kod
maszynowy (czasem na kod asemblera, w sumie na to samo wychodzi). Następnie
następuje proces optymalizacji całego powstałego kodu maszynowego.
Optymalizacja ma za zadanie zmniejszenie wielkości kodu, poprawę szybkości jego
działania, itp. Wyrażenia języka są często kompilowane, tworząc biblioteki, a więc
gotowe zbiory kodów możliwe do wykorzystania przy konstrukcji własnego programu.
Kompilując program (łącząc kody - linker) korzystamy z gotowych, prekompilowanych
kodów. Biblioteki stanowią zarówno konieczną część zasobów języka
programowania jak i mogą być wytwarzane przez użytkowników środowiska tworzenia programów. Podsumowując kompilacja statyczna jest procesem konwersji
kodu źródłowego na kod wykonywalny dla danej platformy sprzętowej.
Kolejną metodą konwersji kodu źródłowego na kod maszynowy jest interpretowanie
kodu. Interpretowanie polega na cyklicznym (pętla) pobieraniu instrukcji języka,
tłumaczeniu instrukcji, generacji i wykonywaniu kodu. Przykładowe interpretatory to
wszelkie powłoki systemów operacyjnych tzw. shelle (DOS, sh, csh, itp.).
Interpretowanie kodu ma więc tą wadę, że nie można wykonac optymalizacji kodu,
gdyż nie jest on dostępny.
Nieco inaczej wygląda interpretowanie kodu źródłowego w Javie. Ponieważ zakłada
się, że tworzone programy w Javie mogą być uruchamiane na dowolnej platformie
sprzętowej, dlatego konieczne jest stworzenie takich interpretatorów, które umożliwią
konwersję tego samego kodu źródłowego na określone i takie samo działanie
niezależnie od platformy. Interpretowanie kodu jest jednak procesem
czasochłonnym, tak więc konieczne są pewne modyfikacje w procesie interpretacji,
aby uruchamialny program był efektywny czasowo. W przetwarzaniu kodu
źródłowego Javy wprowadzono więc pewne zabiegi zwiększające efektywność pracy
z programem stworzonym w Javie. Obsługa kodu źródłowego Javy przebiega
dwuetapowo. Po pierwsze wykonywana jest kompilacja kodu źródłowego do kodu
pośredniego zwanego kodem maszyny wirtualnej. Kod pośredni jest efektem
tłumaczenia kodu źródłowego zgodnie z strukturą języka Java. Powstaje więc pewien
zestaw bajtów, który aby mógł być uruchomiony musi być przekonwertowany na kod
wykonywalny danej platformy. Zestaw bajtów wygenerowany poprzez użycie
kompilatora JAVAC nosi nazwę kodu bajtów, lub B-kodu albo Beta-kodu.
Wygenerowany B-kod jest interpretowany przez interpreter maszyny wirtualnej na
kod wynikowy danej platformy. W celu dalszej poprawy szybkości działania
programów opracowano zamiast interpretatora B-kod różne kompilatory dynamiczne.
Kompilatory dynamiczne kompilują w locie Beta-kod do kodu wykonywalnego danej
platformy. Stworzony w ten sposób kod wykonywalny jest umieszczany w pamięci
komputera nie jest więc zapisywany w formie pliku (programu) na dysku. Oznacza to,
że po skończeniu działania programu kod wykonywalny jest niedostępny. Ta
specyficzna kompilacja dynamiczna jest realizowana przez tzw. kompilatory Just-In-
Time (JIT). Najbardziej popularne kompilatory tej klasy, a zarazem umieszczane
standardowo w dystrybucji JDK i JRE przez SUNa to kompilatory firmy Symantec.
Używając programu maszyny wirtualnej java firmy SUN standardowo korzystamy z
kompilatora dynamicznego JIT firmy Symantec zapisanego w pliku /jre/bin/symcjit.dll
(dla Win95/98/NT). Można wywołać interpretator bez kompilacji dynamicznej poprzez
ustawienie zmiennej środowiska wywołując przykładowo:
java -Djava.compiler = NONE JediW
gdzie JAVA.COMPILER jest zmienną środowiska ustawianą albo na brak
kompilatora (czyli korzystamy z interpretatora) lub na dany kompilator.
Dalszą poprawę efektywności czasowej wykonywalnych programów Javy niesie ze
sobą łączona interpretacja i kompilacja dynamiczna B-kodu. Jest to wykonywane
przez najnowszy produkt SUN-a: Java HotSpot. Rozwiązanie to umożliwia
kontrolowanie efektywności czasowej interpretowanych metod i w chwili gdy
określona metoda jest wykryta jako czasochłonna wówczas generowany jest dla niej
kod poprzez kompilator dynamiczny. Odejście od całkowitej kompilacji dynamicznej kodu jest powodowane tym, że kod wykonywalny zajmuje dużo miejsca w pamięci i
jest mało efektywny w zarządzaniu.
Standardowo dla potrzeb tego kursu używany będzie jednak kompilator JIT firmy
Symantec wbudowany w dystrybucję JDK w wersji 1.2.
W celu zrozumienia metod tłumaczenia i interpretacji (kompilacji) kodu w Javie warto
prześledzić proces ładowania klas do pamięci. Okazuje się, że dla każdej klasy
stworzonej przez użytkownika generowany jest automatycznie obiekt klasy Class. W
czasie wykonywania kodu, kiedy powstać ma obiekt stworzonej klasy maszyna
wirtualna sprawdza, czy został już wcześniej załadowany do pamięci obiekt klasy
Class związany z klasą dla której ma powstać nowy obiekt. Jeśli nie został
załadowany do pamięci odpowiedni obiekt klasy Class wówczas maszyna wirtualna
lokalizuje i ładuje B-kod klasy źródłowej *.class obiektu, który ma powstać w danym
momencie wykonywania programu. Oznacza to, że tylko potrzebny B-kod jest
ładowany przez maszynę wirtualną (ClassLoader). Jeśli w danym wykorzystaniu
programu nie ma potrzeby wykorzystania określonych klas, to odpowiadającym im
kod nie jest ładowany.
W celu zobrazowania zagadnienia rozpatrzmy następujący program przykładowy:
// Rycerze.java:
class Luke {
Luke (){
System.out.println("Moc jest z toba Luke!");
}
static {
System.out.println("Jest Luke!");
}
}// koniec class Luke
class Anakin{
Anakin (){
System.out.println("Nie lekcewaz ciemnej strony mocy Anakin!");
}
static {
System.out.println("Jest Anakin!");
}
}// koniec class Anakin
class Vader {
Vader (){
System.out.println("Ciemna strona mocy jest potezna!!!");
}
static {
System.out.println("Jest Vader!");
}
}//// koniec class Vader
public class Rycerze {
public static void main(String args[]) {
System.out.println("Zaczynamy. Kto jest gotow pojsc z nami?");
System.out.println("Luke ?");
new Luke();
System.out.println("Witaj Luke!");
System.out.println("Anakin ?");
try {
Class c = Class.forName("Anakin");
/* usunięcie komentarza w kolejnej linii spowoduje utworzenie obiektu
klasy Anakin, a więc wywołany zostanie również konstruktor */
// c.newInstance();
} catch(Exception e) {
e.printStackTrace();
}
System.out.println("Witaj Anakin!");
System.out.println("Ktos jeszcze ?");
new Vader();
System.out.println("Gin Vader!");
}
} // public class Rycerze
W przykładzie tym zdefiniowano trzy klasy Luke, Anakin oraz Vader, w których
ciałach umieszczono kod statyczny (wykonywany wraz w ładowaniem klasy)
generujący napis na konsoli. Dodatkowo, klasy te posiadają konstruktory
wykonywane gdy wywoływany jest obiekt danej klasy. Klasa głowna aplikacji Rycerze
wywołuje szereg komunikatów powiązanych z wykorzystaniem klas Luke, Anakin
oraz Vader. Początkowo wywoływany jest komunikat rozpoczęcia wykonywania
głównej metody aplikacji ("Zaczynamy. Kto jest gotow pojsc z nami?"). Po stworzeniu
obiektu klasy Luke zostanie załadowana ta klasa, uruchomiona część statyczna klasy
oraz wywołany konstruktor tej klasy. Oznacza to, że pojawią się da komunikaty: Jest
Luke! oraz Moc jest z toba Luke!. Odwołanie się do kolejnej klasy Anakin jest zgoła
odmienne. Nie tworzony jest obiekt tej klasy, lecz tworzony jest obiekt klasy Class
skojarzony z klasą o podanej nazwie czyli Anakin. Klasa Anaikn jest więc ładowana
do systemu co objawia się wyświetleniem komunikatu: "Jest Anakin!". Nie zostanie
natomiast wywołany konstruktor tej klasy, gdyż nie tworzymy obiektu tej klasy. Można
to zrobić usuwając komentarz przy instrukcji c.newInstance(); powodującej tworzenie
nowego wystąpienia klasy Anakin (konwersja klas Class - > Anakin) czyli tworzenie
obiektu tej klasy. Obsługa klasy Vader jest podobna do obsługi klasy Luke.
Kompilując powyższy kod oraz wywołując go z interpretatorem lub kompilatorem
dynamicznym Symantec JIT dla wersji JDK 1.2 (Symantec Java! Just-In-Time
compiler, version 3.00.078(x) for JDK 1.2) otrzymamy:
Zaczynamy. Kto jest gotow pojsc z nami?
Luke ?
Jest Luke!
Moc jest z toba Luke!
Witaj Luke!
Anakin ?
Jest Anakin!
Witaj Anakin!
Ktos jeszcze ?
Jest Vader!
Ciemna strona mocy jest potezna!!!
Gin Vader!
lub gdy tworzymy obiekt przez c.newInstance():
Zaczynamy. Kto jest gotow pojsc z nami?
Luke ?
Jest Luke!
Moc jest z toba Luke!
Witaj Luke!
Anakin ?
Jest Anakin!
Nie lekcewaz ciemnej strony mocy Anakin!
Witaj Anakin!
Ktos jeszcze ?
Jest Vader!
Ciemna strona mocy jest potezna!!!
Gin Vader!
Ciekawe jest to, że w niektórych wersjach interpretacji czy kompilacji wynik będzie
zupełnie inny na przykład dla kompilatora Symantec Java! Just-In-Time compiler,
version 210-054 for JDK 1.1.2:
Jest Luke!
Jest Vader!
Zaczynamy. Kto jest gotow pojsc z nami?
Luke ?
Moc jest z toba Luke!
Witaj Luke!
Anakin ?
Jest Anakin!
Nie lekcewaz ciemnej strony mocy Anakin!
Witaj Anakin!
Ktos jeszcze ?
Ciemna strona mocy jest potezna!!!
Gin Vader!
Na tym przykładzie widać jasno, że najpierw analizowany był cały kod i załadowane
zostały te klasy, których obiekty są wywoływane w metodzie main(), czyli klasy Luke i
Vader.
Wyjątki
W kodzie programu z przykładu zostały wykorzystane instrukcje try i catch:
try{
Class c = Class.forName("Annakin");
/* usunięcie komentarza w kolejnej linii spowoduje utworzenie obiektu
klasy Anakin, a więc wywołany zostanie również konstruktr */
//c.newInstance();
} catch(ClasNotFoundException e) {
e.printStackTrace();
}
Otóż w Javie oprócz błędów mogą również występować wyjątki. Wyjątki są to
określone sytuacje niewłaściwe ze względu na funkcjonowanie klas lub metod.
Przykładowe wyjątki to np. dzielenie przez zero, brak hosta o podanym adresie, brak
pliku o podanej ścieżce, czy brak klasy. Każdy wyjątek związany jest z określoną
klasą i jej metodami. Jeżeli dana metoda może spowodować wystąpienie wyjątku (co
jest opisane w dokumentacji Javy) to należy wykonać pewne działanie związane z
obsługa wyjątku. Metodę taką zamyka się wówczas w bloku kodu oznaczonego
instrukcją warunkową try (spróbuj). Blok należy zakończyć działaniem przewidzianym
dla sytuacji wyjątkowej w zależności od powstałego wyjątku. Detekcja rodzaju
wyjątku odbywa się poprzez umieszczenie instrukcji catch (nazwa wyjątku i jego
zmienna), w bloku której definiuje się działanie (obsługę wyjątku). Przykładowe
działanie to wyświetlenie komunikatu na konsoli platformy. Wystąpienie wyjątku i jego
właściwa obsługa nie powoduje przerwania pracy programu. Nazwy i typy wyjątków
są zdefiniowane wraz z klasami i interfejsami w odpowiednich pakietach w
dokumentacji Javy. Wyjątek jest definiowany przez właściwą mu klasę o specyficznej
nazwie zawierającej fraze Exception. Przykłądowe klasy wyjątków to:
w pakiecie java.io.*:
EOFException - koniec pliku
FileNotFoundException - brak pliku
InterruptedIOException - przerwanie operacji we/wy
IOException - klasa nadrzędna wyjątków we/wy
w pakiecie java.lang.*:
ArithmeticException - wyjątek operacji arytmetycznych np. dzielenie przez zero,
ArrayIndexOutOfBoundsException - przekroczenie zakresu tablicy,
ClassNotFoundException - brak klasy,
Exception - klasa nadrzędna wyjątków.
Przykładowe błędy:
NoSuchFieldError - błąd braku danego pola w klasie,
NoSuchMethodError - błąd braku danej metody w klasie,
OutOfMemoryError- błąd braku pamięci.
Niektóre wyjątki dziedziczą z klasy RuntimeException, czyli są to wyjątki, które
powstają dopiero w czasie działania programu i nie można ich przewidzieć, np.
związać z określoną metodą. Przykładowym wyjątkiem tego typu jest
ArithmeticException - wyjątek operacji arytmetycznych np. dzielenie przez zero. Dla
wyjątków klasy RuntimeException nie jest wymagane stosowanie obsługi wyjątków
(tzn. kompilator nie zwróci błędu). Programista może jednak przewidzieć (powinien
przeiwdzieć) wystąpienie tego typu wyjątku i obsłużyć go w kodzie programu.
Istnieje jeszcze jedna możliwość deklaracji, że dana procedura może zwrócić
wyjątek. Zamiast stosować przechwytywanie wyjątku deklaruje się kod poprzez
użycie słowa kluczowego throws po nazwie metody, w której ciele występują metody
powodujące możliwość powstania sytuacji wyjątkowej np.:
public class Rycerze {
public static void main(String args[]) throws Exception{
(...)
Class c = Class.forName("Anakin");
(..)
}
}// public class Rycerze
W powyższym przykładzie oznaczenie metody main() jako metody zwracającej jakiś
wyjątek (ponieważ Exception jest nadklasą klas wyjątków) zwalnia nas ( w sensie nie
będzie błędu kompilacji) od używania instrukcji obsługi wyjątków. Zamiast klasy
Exception można użyć konkretnego wyjątku jaki jest związany z obsługą kodu
wewnątrz metody np. ClassNotFoundException.
Klasy wewnętrzne
W powyższych przykładach zdefiniowano klasy pomocnicze poza ciałem klasy
głównej programu W powyższych przykładach zdefiniowano klasy pomocnicze poza ciałem klasy
głównej programu (poza klasą publiczną). Klasy tak zdefiniowane nazywa się klasami
zewnętrznymi (outer classes). W Javie można również definiować klasy w ciele klasy
publicznej czyli tworzyć klasy wewnętrzne (inner classes).
Przykładowy kod zawierający klasę wewnętrzną może wyglądać następująco:
////// MasterJedi.java;
public class MasterJedi {
public long pesel;
public MasterJedi {
pesel = i:
}
class StudentJedi {
public long pesel ;
public SrudentJedi(String s ling j) {
System.out.printl(s+j);
};
// koniec class StudentJedi
public static void main (String args[]) {
MasterJedi mj = new MasterJedi(9010090);
System.out.printl("PESEL mistrza to : " +mj.pesel);
MasteJedi.StudentJedi sj = mj.new StudentJedi("PESEL studenta to: ", 8008120459L);
}
} //koniec public class MasterJedi
W metodzie main() tworzony jest obiekt klasy wewnętrznej
StudentJedi. Stworzenie obiektu tej klasy jest możliwe tylko wtedy, gdy istnieje obiekt
klasy zewnętrznej, w tym przypadku klasy MasterJedi. Konieczne jest więc najpierw
wywołanie obiektu klasy zewnętrznej, a później poprzez odwołanie się do tego
obiektu stworzenie obiektu klasy wewnętrznej.
Stosowanie klasy wewnętrznych oprócz możliwości grupowania kodu i sterowania
dostępem daje inne możliwości, do których powrócimy w dalszych rozważaniach na
temat programowania obiektowego, a w szczególności na temat interfejsów.
Konstrukcja kodu
Metody konstrukcji kodu
Zapoznanie się z ogólną metodologią tworzenia programów w Javie pozwala
na wejście w szczegółowy opis języka i tworzenie przykładowych aplikacji. Warto
jednak najpierw określić szczegółowe zasady pisania kodu oraz stosowania
komentarzy.
Czytelne pisanie kodu programu ułatwia w znaczny sposób unikania błędów oraz
upraszcza przyswajalność kodu. Podstawą tworzenia kodu w sposób czytelny jest
grupowanie kodu w bloki, wyróżnialne wcięciami. Blokiem nazwiemy zestaw kodu
ujęty w nawiasy klamrowe:
np.:
instrukcja _grupująca{
kod...
kod...
}// koniec instrukcja_grupująca
Instrukcja grupująca rozpoczyna blok kodu. Instrukcją grupującą może być klasa,
metoda, instrukcja warunkowa, instrukcja pętli, itp. Klamra otwierająca blok znajduje
się bezpośrednio po instrukcji grupującej. Pierwsza fragment kodu po klamrze
otwierającej umieszcza się w nowej linii. Każda linia bloku jest przesunięta względem
pierwszego znaku instrukcji_grupującej o stałą liczbę znaków. Blok kończy się
umieszczeniem klamry zamykającej w nowej linii na wysokości pierwszego znaku
instrukcji grupującej. Blok warto kończyć komentarzem umożliwiającym identyfikację
bloku zamykanego przez daną klamrę.
W konstrukcji programu należy pamiętać, że język Java rozróżnia małe i wielkie
litery, Oznacza to, że przykładowe nazwy zmiennych Jedi oraz jedi nie są
równoważne i określają dwie różne zmienne. Dodatkowe elementy stylu
programowania, o których należy pamiętać to:
- stosowanie nadmiarowości celem poprawienia czytelności kodu np.:
int a=(c * 2) + (b / 3); zamiast int a=c* 2 + b/ 3;
- stosowanie separatorów np.:
int a=(c * 2) + (b / 3); zamiast int a=(c*2)+(b/3);
- konsekwencja w oznaczaniu, np.:
int[] n;
int n[];
definicja jednoznaczna lecz różna forma.
Ważnym elementem czytelnej konstrukcji kodu jest używanie dokumentacji kodu za
pomocą komentarzy. W Javie stosuje się trzy podstawowe typy komentarza:
- komentarz liniowy:
// miejsce na komentarz do końca linii
- komentarz blokowy:
/*
miejsce na komentarz w dowolnym miejscu bloku
*/
- komentarz dokumentacyjny
/**
miejsce na komentarz w dowolnym miejscu bloku
*/
Komentarz liniowy wykorzystuje się do krótkiego oznaczania kodu np. interpretacji
zmiennych, oznaczania końca bloku, itp. Komentarz blokowy stosuje się do
chwilowego wyłączania kodu z kompilacji oraz do wprowadzania szerszych opisów
kodów. Komentarz dokumentacyjny używa się do tworzenia dokumentacji kodu
polegającej na opisie klas i metod, ich argumentów, dziedziczenia, twórców kodu, itp.
Komentarz dokumentacyjny w Javie może zawierać dodatkowe elementy takie jak:
@author - umożliwia podanie autora kodu,
@version - umożliwia podanie wersji kodu,
@see - umożliwia stworzenie referencji,
itp.
Pełny zestaw elementów formatujących znajduje się w opisie narzędzi javadoc,
tworzących dokumentację na podstawie kodu i komentarza dokumentacyjnego.
Należy pamiętać, że standardowo zostaną opisane tylko elementy kodu oznaczane
jako public i protected.
O czym należy pamiętać używając komentarzy w Javie? Otóż o tym, że komentarz
jest zastępowany spacją, co oznacza że kod:
double pi = 3.14/*kto by to wszystko zapamiętał*/56;
jest interpretowany jako double pi = 3.14 56; co jest błędem.
// Jedi3.java :
/**
RycerzJedi określa klasę rycerzy Jedi
@author
@version 1.0
*/
class RycerzJedi{
/** nazwa - określa nazwę rycerza Jedi */
String nazwa;
/** kolor_miecza - określa kolor miecza rycerza Jedi */
String kolor_miecza;
/** konstruktor umożliwia podanie właściwości obiektu */
RycerzJedi(String nazwa, String kolor_miecza){
this.nazwa=nazwa;
this.kolor_miecza=kolor_miecza;
}
/** metoda opis wyświetla zestaw właściwości rycerza Jedi */
void opis(){
System.out.println("Rycerz "+nazwa+ " ma "+kolor_miecza+" miecz.");
}
}// koniec class RycerzJedi
/**
Jedi3 określa klasę główna aplikacji
@
author
@version 1.0
*/
public class Jedi3{
public static void main(String args[]){
RycerzJedi luke = new RycerzJedi("Luke", "niebieski");
RycerzJedi ben = new RycerzJedi("Obi-wan","biały");
luke.opis();
ben.opis();
}// koniec public static void main(String args[])
}// koniec public class Jedi3
Tworząc dokumentację HTML dla kodu z przykładu 2.1 należy w następujący sposób
wykorzystać narzędzie javadoc:
javadoc -private -author -version Jedi3.java
gdzie :
-private powoduje ,że wygenerowana zostanie dokumentacja dla wszystkich typów elementów private, public i protected
-author powoduje ,że zostanie wygenerowana informacja o autorze
-version powoduje ,że wygenerowana zostanie informacja o wersji.
W rezultacie otrzymuje się szereg stron w formacie HTML, wraz ze stroną startową
index.html.
Literały
Literały oznaczają nazwy zmiennych, których typ oraz wartości wynikają z
zapisu. Przykładowo Niech moc będzie z Wami! jest literałem o typie łańcuch
znaków (String) i wartości Niech moc będzie z Wami!. Inne przykłady to: true, false,
12, 0.1, e, 234L, 0.34f, itp. Tłumaczenie kodu z literałami jest pobieraniem
konkretnych wartości typu łańcuch znaków, znak, liczba rzeczywista, liczba
całkowita, wartość logiczna (true lub false). Uwaga, wartości logiczne nie są
jednoznaczne w Javie z wartościami liczbowymi np. typu 0 i 1.
Identyfikatory i słowa kluczowe
Stworzenie programu wymaga znajomości zasad tworzenia identyfikatorów
oraz podstawowych słów kluczowych. Zasady tworzenia identyfikatorów określają
reguły konstrukcji nazw pakietów, klas, interfejsów, metod i zmiennych. Identyfikatory
są więc elementami odwołującymi się do podstawowych elementów języka.
Identyfikator tworzy się korzystając z liter, liczb, znaku podkreślenia _ oraz znaku
$. Tworzona nazwa nie może się jednak zaczynać liczbą. Pierwszym znakiem
może być litera bądź symbol podkreślenia _ lub $. W Javie rozróżnialne są wielkie
i małe litery w nazwach, tak więc nazwy: Jedi i jedi oznaczać będą dwa różne
identyfikatory!
Słowa kluczowe to identyfikatory o specjalnym znaczeniu dla języka Java.
Identyfikatory te są już zdefiniowane dla języka i posiadają określone znaczenie w
kodzie programu. Wszystkie więc nazwy w kodzie źródłowym będące poza
komentarzem i i nie są stworzonymi przez programistę identyfikatorami są
traktowane jako słowa kluczowe i ich znaczenie jest poszukiwane przez kompilator w
czasie kompilacji. Słowa kluczowe (nazwy) są więc zarezerwowane dla języka i
programista nie może używać tych nazw dla konstrukcji własnych identyfikatorów.
Następujące słowa kluczowe są zdefiniowane w Javie:
abstract - deklaruje klase lub metodę jako abstrakcyjną,
boolean - deklaruje zmienną lub wartość zwracaną jako wartość logiczną,
break - przerwanie,
byte - deklaruje zmienną lub wartość zwracaną jako wartość byte,
case - określa opcję w instrukcji wyboru switch,
catch - obsługuje wyjątek,
char - deklaruje zmienną lub wartość zwracaną jako wartość znaku,
class - oznacza początek definicji klasy,
const,
continue - przerywa pętle rozpoczynając jej kolejny cykl,
default - określa domyślne działąnie dla instrukcji wyboru switch,
do - rozpoczyna blok instrukcji w pętli while,
double - deklaruje zmienną lub wartość zwracaną jako wartość rzeczywistą o
podwójnej precyzji,
else - określa kod warunkowy do wykonania jeśli nie jest spełniony warunek w
instrukcji if,
extends - określa, że definiowana klasa dziedziczy z innej,
final - określa pole, metodę lub klasę jako stałą,
finally - gwarantuje wykonywalność blok kodu,
float - deklaruje zmienną lub wartość zwracaną jako wartość rzeczywistą,
for - określa początek pętli for,
goto,
if - określa początek instrukcji warunkowej if,
implements - deklaruje, że klasa korzysta z danego interfejsu,
import - określa dostęp do pakietów i klas,
instanceof - sprawdza czy dany obiekt jest wystąpieniem określonej klasy,
int - deklaruje zmienną lub wartość zwracaną jako wartość całkowitą 4 bajtową,
interface - określa początek definicji interfejsu,
long - deklaruje zmienną lub wartość zwracaną jako wartość całkowitą 8 bajtową,
native - określa, że metoda jest tworzona w kodzie danej platformy (np. C),
new - wywołuje nowy obiekt,
package - określa nazwę pakietu do którego należy dana klasa,
private - deklaruje metodę lub pole jako prywatne,
protected - deklaruje metodę lub pole jako chronione,
public - deklaruje metodę lub pole jako dostępne,
return - określa zwracanie wartości metody,
short - deklaruje zmienną lub wartość zwracaną jako wartość całkowitą 2 bajtową,
static - deklaruje pole, metodę lub kod jako statyczny,
super - odniesienie do rodzica danego obiektu,
switch - początek instrukcji wyboru,
synchronized - oznacza fragment kodu jako synchronizowany,
this - odniesienie do aktualnego obiektu,
throw - zgłasza wyjątek,
throws - deklaruje wyjątek zgłaszany przez metodę,
transient - oznacza blokadę pola przed szeregowaniem,
try - początek bloku kodu, który może wygenerować wyjątek,
void - deklaruje, że metoda nie zwraca żadnej wartości,
volatile - ostrzega kompilator, że zmienna może się zmieniać asynchronicznie,
while - początek pętli.
przy czym słowa kluczowe const (stała) oraz goto (skok do) są zarezerwowane lecz
nie używane.
Zmienne
Przetwarzanie informacji w kodzie programu wymaga zdefiniowania i pracy ze
zmiennymi. Zmienne są to elementy programu wynikowego reprezentowane w
kodzie źródłowym przez identyfikatory, literały i wyrażenia. Zmienne są zawsze
określonego typu. Typ zmiennych (typ danych) definiowany jest w Javie przez typy
podstawowe i odnośnikowe (referencyjne). W celu pracy ze zmiennymi należy
zapoznać się z możliwymi typami danych oferowanymi w środowisku Java.
Typy danych Podstawowe typy danych definiowane w specyfikacji Javy charakteryzują się
następującymi właściwościami:
1. Typy danych są niezależne od platformy sprzętowej (w C i C++ często inaczej
interpretowane były zmienne zadeklarowane jako dany typ, np. dla zmiennej typu int
możliwe były reprezentacje w zależności od platformy 2 lub 4 bajtowe. Konieczne
było stosowanie operatora sizeof w celu uzyskania informacji o rozmiarze zmiennej) i
ich rozmiar jest stały określony w specyfikacji.
2. Konwersja (casting - rzutowanie) typów danych jest ograniczona tylko dla liczb.
Nie można dokonać więc konwersji wprost pomiędzy typem znakowym a liczbą, czy
pomiędzy typem logicznym a liczbą.
3. Wszystkie typy liczbowe są przechowywane za znakiem, np. byte: -128..0..127.
Nie ma typów oznaczanych w innych językach jako unsigned, czyli bez znaku.
4. Wszystkie podstawowe typy danych są oznaczane z małych liter.
5. Nie istnieje w gronie podstawowych typów danych typ łańcucha znaków.
6. Istnieją klasy typów danych oznaczanych z wielkich liter, umożliwiające konwersję
i inne operacje na obiektach tych klas, a przez to na podstawowych typach danych.
Wśród licznych klas typów danych znajduje się klasa String, umożliwiająca tworzenie
obiektów reprezentujących łańcuch znaków. Typ łańcucha znaków jest więc tylko
typem referencyjnym, gdyż odnosi się do klasy, a nie do podstawowego typu danych.
Następujące podstawowe typy danych są zdefiniowane w specyfikacji Javy:
boolean : (1 bit) typ jednobitowy oznaczający albo true albo false. Nie może
podlegać konwersji do postaci liczbowej, czyli nie możliwe jest znaczenie typu jako 1
lub 0. Oznaczanie wartości typów (jak wszystkie w Javie) jest ściśle związane z
wielkością liter. Przykładowe oznaczenia TRUE czy False nie mają nic wspólnego z
wartościami typu boolean.
byte : (1 bajt) typ liczbowy, jednobajtowy za znakiem. Wartości tego typu mieszczą
się w przedziale: -128 do 127.
short : (2 bajty) typ liczbowy, dwubajtowy ze znakiem. Wartości tego typu
mieszczą się w przedziale: -32,768 do 32,767
int : (4 bajty) typ liczbowy, czterobajtowy ze znakiem. Wartości tego typu mieszczą
się w przedziale: -2,147,483,648 do 2,147,483,647.
long : (8 bajtów) typ liczbowy, ośmiobajtowy ze znakiem. Wartości tego typu
mieszczą się w przedziale: -9,223,372,036,854,775,808 do
+9,223,372,036,854,775,807.
float : ( 4 bajty, spec. IEEE 754) typ liczb rzeczywistych, czterobajtowy ze znakiem.
Wartości tego typu mieszczą się w przedziale: 1.40129846432481707e-45 to
3.40282346638528860e+38 (dodatnie lub ujemne).
double : (8 bajtów spec. IEEE 754) typ liczb rzeczywistych, ośmiobajtowy ze
znakiem. Wartości tego typu mieszczą się w przedziale:
4.94065645841246544e-324d to 1.79769313486231570e+308d (dodatnie lub
ujemne).
char : (2 bajty), typ znakowy dwubajtowy, dodatni. Kod dwubajtowy umożliwia zapis
wszelkich kodów w systemie Unicode, który to jest standardem w Javie. Zakres
wartości kodu to: 0 to 65,535. Tablica znaków nie jest łańcuchem znaków, tak jak to
jest interpretowane w C czy C++.
void: typ nie jest reprezentowany przez żadną wartość, wskazuje, że dana metoda
nic nie zwraca
.
W Javie w bibliotece podstawowej języka: java.lang.* znajdują się następujące klasy
typów danych:
Boolean: klasa umożliwiająca stworzenie obiektu przechowującego pole o wartości
typu podstawowego boolean. Klasa ta daje liczne możliwości przetwarzania wartości
typu boolean na inne. Możliwe jest przykładowo uzyskanie reprezentacji znakowej
(łańcuch znaków -> obiekt klasy String). Jest to możliwe poprzez wywołanie metody
toString(). Metoda ta jest stosowana dla wszystkich klas typów danych, tak więc
również statyczna metoda klasy Boolean (nie trzeb tworzyć obiektu aby się do
metody statycznej odwoływać) zwracająca wartość typu podstawowego boolean na
podstawie argumentu metody będącego łańcuchem znaków, np.
Boolean.valueof(yes); zwraca wartość true;
Byte : klasa umożliwiająca stworzenie obiektu przechowującego pole o wartości typu
podstawowego byte. Klasa ta umożliwia liczne konwersje typu liczbowego byte do
innych podstawowych typów liczbowych. Stosowane są przykładowo następujące
metody tej klasy: intValue() - konwersja wartości typu byte na typ int; floatValue()-
konwersja wartości typu byte na typ float; longValue()- konwersja wartości typu byte
na typ long; i inne. Statyczne pola tej klasy: MIN_VALUE oraz MAX_VALUE,
umożliwiają pozyskanie rozmiaru danego typu w Javie. Podobne właściwości
posiadają inne klasy liczbowych typów danych:
Short ,
Integer,
Long ,
Float ,
Double .
Character : klasa umożliwiająca stworzenie obiektu przechowującego pole o wartości
typu podstawowego char. Zdefiniowano obszerny zbiór pól statycznych oraz metod
tej klasy. Większość pól i metod dotyczy obsługi standardowej strony kodowej
platformy Javy czyli Unicodu (wrersja 2.0). Programy w Javie są zapisywane w
Unicodzie. Unicode jest systemem reprezentacji znaków graficznych poprzez liczby z
zakresu 0-65,535. Pierwsze 128 znaków kodu Unicode to znaki kodu ASCII (ANSI
X3.4) czyli American Standard Code for Information Interchange. Kolejne 128
znaków w Unicodzie to odpowiednio kolejne 128 znaków w rozszerzonym kodzie
ASCII (ISO Latin 1). Z wyjątkiem komentarzy, identyfikatorów i literałów znakowych i
łańcuchów znaków wszelki kod w Javie musi być sformowany przez znaki ASCII
(bezpośrednio lub jako kody Unicodu zaczynające się od sekwencji \u, np. \u00F8).
Oznacza to, że np. komentarz może być zapisany w Unicodzie.
Wiele jednak programów stworzonych w Javie wymaga przetwarzania danych
tekstowych z wykorzystaniem innych systemów kodowania znaków, np. ISO-8859-2.
Konieczne są więc mechanizmy konwersji standardów kodowania. Mechanizmy takie
są dostępne w Javie.
Metody dostępne w klasie Character umożliwiają ponadto przetwarzanie znaków i ich
identyfikację. Przykładowo za pomocą metody isWhitespace (char ch) można
uzyskać informację czy dany znak jest znakiem sterującym zwanym jako :Java
whitespace, do których zalicza się:
- Unicode: spacja (category "Zs"), lecz nie spacja oznaczana przez (\u00A0 lub
\uFEFF).
- Unicode: separator linii (category "Zl").
- Unicode: separator paragrafu (category "Zp").
- \u0009, HORIZONTAL TABULATION.
- \u000A, LINE FEED.
- \u000B, VERTICAL TABULATION.
- \u000C, FORM FEED.
- \u000D, CARRIAGE RETURN.
- \u001C, FILE SEPARATOR.
- \u001D, GROUP SEPARATOR.
- \u001E, RECORD SEPARATOR.
- \u001F, UNIT SEPARATOR.
String - klasa umożliwiająca stworzenie nowego obiektu typu łańcuch znaków. Nie
istnieje typ podstawowy łańcucha znaków, tak więc klasa ta jest podstawą tworzenia
wszystkich zmiennych przechowujących tekst. Obiekt klasy String może być
inicjowany następująco:
String str = Rycerz Luke;
lub
String str = new String(Rycerz Luke); //i inne konstruktory klasy String.
Pierwszy sposób inicjowania przez referencje jest podobne do inicjowania zmiennych
typów podstawowych. Drugi sposób jest inicjowania jest jawnym wywołaniem
konstruktora klasy String. Klasa String umożliwia szereg operacji na łańcuchach
znaków (np. zmiana wielkości, itp.). Należy pamiętać, że tablica znaków char nie jest
rozumiana jako obiekt String. Obiekt klasy String może być stworzony za pomocą
tablicy znaków, np.: String str = new String(c); gdzie c jest tablicą znaków np.: char
c[] = new char[10]. Wśród konstruktorów klasy String znajduje się również
konstruktor umożliwiający stworzenie łańcucha znaków na podstawie tablicy bajtów,
według podanej strony kodowej, np.: String str = new String (b, Cp1250), gdzie b to
tablica bajtów, np.: byte b[] = new byte[10];. Warto pamiętać również o tym, że obiekt
klasy String nie reprezentuje sobą wartości typu podstawowego, stąd nie można
porównywać dwóch łańcuchów znaków bezpośrednio, lecz poprzez metodę klasy
String: public boolean equals(Object anObject). Poniższy przykład obrazuję
porównywanie łańcuchów znaków.
//Moc.java
public class Moc{
public static void main(String args[]){
String dobro = new String("Dobro - jasna strona mocy");
System.out.println("Ciemna strona mocy twierdzi:");
String zlo = new String("Dobro - jasna strona mocy");
if (zlo == dobro){
System.out.println("Moc to jedno");
} else
System.out.println("Dwie moce?");
if (zlo.equals(dobro)){
System.out.println("Moc to jedno");
} else
System.out.println("Dwie moce?");
}
}// koniec public class Moc
Object - klasa ta jest klasą nadrzędną wszystkich klas w Javie, tak więc tworzenie
własnych typów danych będących klasami jest odwołaniem się do obiektu klasy
Object.
Void - przechowuje referencje do obiektu klasy Class reprezentującej typ
podstawowy void.
Poniżej zaprezentowano przykładową aplikację ukazującą różne typy danych
podstawowych i klasy typów danych.
//Typy.java
public class Typy{
public static void main(String args[]){
boolean prawda[] = new boolean[2];
prawda[0]=true;
byte bajt = (byte)0;
int liczba = 135;
long gluga = 123456789L;
char znak1 = '\u0104'; \\ w Unicodzie kod litery Ą
char znak2 = 'ą';
char znaki[] = { 'M', 'O', 'C'};
System.out.println("Oto zmienne: ");
System.out.println("boolean= "+prawda[1]);
System.out.println("byte= "+bajt);
System.out.println("int= "+liczba);
System.out.println("char= "+znak1);
System.out.println("char= "+znak2);
Integer liczba1 = new Integer(liczba);
bajt=liczba1.byteValue();
System.out.println("byte= "+bajt);
String str = new String(znaki);
System.out.println(str);
}
}// koniec public class Typy
Warto zauważyć, ze chociaż wszystkie zmienne muszą być zainicjowane jeżeli mają
być użyte, to zmienna typu boolean prawda jest zadeklarowana jako tablica dwóch
elementów, z czego drugi element nie jest inicjowany jawnie. Drugi element jest
inicjowany w tablicy automatycznie na wartość domyślną.
Następujące wartości domyślne są przyjmowane dla zmiennych poszczególnych
typów podstawowych:
boolean: false
char: \u0000′ (null)
byte: (byte)0
short: (short)0
int: 0
long: 0L
float: 0.0f
double: 0.0 (lub 0.0d)
Zmienna podstawowego typu jest konkretną wartością przechowywaną na stosie, co
umożliwia efektywny do niej dostęp. Zmienna typu Class jest interpretowana jako
uchwyt do obiektu typu Class, np.: String l; oznacza deklaracje uchwytu l do obiektu
klasy String. Zadeklarowany uchwyt jest również przechowywane na stosie, nie mniej
konieczne jest zainicjowanie uchwytu, lub inaczej wskazanie obiektu, który będzie
związany z uchwytem. Przykładowo przypisanie obiektu do uchwytu może wyglądać
następująco: l = new String (Jedi);. W wyniku wykonania instrukcji new powstaje
obiekt danej klasy (np. String), który jest przechowywany na stercie. Przechowywanie
wszystkich obiektów na stercie ma tę zaletę, że w czasie kompilacji nie jest
potrzebna wiedza na temat wielkości pamięci jaka musi być zarezerwowana. Wadą
przechowywania obiektów na stercie jest dłuższy czas dostępu do obszarów pamięci
sterty, co powoduje spowolnienie pracy programu.
Oprócz istniejących typów podstawowych oraz klas typów zdefiniowanych w pakiecie
java.lang.* (czyli np. Boolean, Integer, Byte, itp.) można skorzystać z tysięcy innych
typów danych. Definicją typu jest definicja każdej klasy. Stąd też istnieje możliwość
skorzystania z typów danych (klas) wbudowanych w biblioteki standardowe i
rozszerzone języka Java lub można stworzyć własne typy danych (klasy).
Operatory
Operatory to elementy języka służące do generacji nowych wartości na
podstawie podanych argumentów (jeden lub więcej). Operator wiąże się więc
najczęściej z określonym działaniem na zmiennych. Prawie wszystkie operatory (z
wyjątkiem: =, ==, !=, +, +=) działają na podstawowych typach danych, a nie na
obiektach.
Wyróżnia się następujące klasy operatorów podane wedle ich kolejności
wykonywania:
• operatory negacji;
• operatory matematyczne,
• operatory przesunięcia,
• operatory relacji,
• operatory logiczne i bitowe,
• operatory warunkowe,
• operatory przypisania.
Operator negacji powoduje zmianę wartości zmiennej na przeciwną pod względem
znaku, np.: int a =4; x = -a; (to x jest równe -4), itd.
Operatory matematyczne to takie operatory, które służą do wykonywania operacji
matematycznych na argumentach. Do operacji matematycznych zalicza się:
mnożenie *;
dzielenie /;
modulo - reszta z dzielenia %,
dodawanie +,
odejmowanie -.
W wyniku dzielenia liczba całkowitych Java nie zaokrągla wyników do najbliższej
wartości całkowitej, lecz obcina powstałą liczbę do liczby całkowitej. Dodatkowym
elementem wykonywania operacji matematycznych w Javie (podobnie jak i w C) jest
skrócony zapis operacji matematycznych jeśli jest wykonywana operacja na
zmiennej, która przechowuje zarazem wynik tej operacji. Wówczas możliwe są
następujące skrócone zapisy operacji:
• zwiększanie / zmniejszanie o 1 wartości zmiennej:
zapis klasyczny, np.: x = x+1; x= x-1;
zapis skrócony, np.: x++, x--.
• operacja na zmiennej:
zapis klasyczny, np.: x = x+4; x= x*6; x= x/9;
zapis skrócony, np.: x+=4; x*=6; x/=9;
Zwiększanie lub zmniejszanie wartości zmiennej o 1 możliwe jest na dwa sposoby:
a) zwiększanie/zmniejszanie przed operacją (najpierw zmniejsz/zwiększ wartość
zmiennej, później wykonaj operację na tej zmiennej), wówczas notacja operacji jest
następująca np.: --x; ++x;
b) zwiększanie/zmniejszanie po operacji (najpierw wykonaj operację na tej zmiennej
a później zmniejsz/zwiększ wartość zmiennej), wówczas notacja operacji jest
następująca np.: x--; x++;
//Senat.java
public class Senat{
public static void main(String args[]){
int x = 35;
int s = x++; //warto zmienić kod na ++x i zobaczyć jakie będą wydruki
System.out.println("Senat Republiki bez planety Naboo składa się z "+ s +" światów");
System.out.println("Senat Republiki wraz z planetą Naboo składa się z "+ x +" światów");
x/=6;
System.out.println("Wszystkie planety senatu mieszczą się w " + x + " galaktykach");
}
}// koniec public class Senat
W Javie nie istnieje możliwość przeciążania operatorów (tzn. dodawania nowego
zakresu ich działania). Określono jedynie rozszerzenie operacji dodawania na
obiekty typu String. Wówczas możliwe jest wykonanie operacji:
//Relacje.java
public class Relacje{
public static void main(String args[]){
String luke = "Luke'a";
String anakin = "Anakin";
String relacja = " jest ojcem ";
String str = anakin+relacja+luke;
System.out.println(str);
}
}// koniec public class Relacje
generującej następujący komunikat:
Anakin jest ojcem Luke′a.
Operatory przesunięcia działają na bitach w ich reprezentacji poprzez całkowite typy
podstawowe danych. Operator << powoduje przesunięcie w lewo o zadaną liczbę
bitów, natomiast operator >> powoduje przesunięcie w prawo o zadaną liczbę
bitów, np.:
int liczba = 20;
int liczbaL = liczba << 2;
int liczbaR = liczba>>2;
Operatory relacji generują określony rezultat reprezentowany przez typ logiczny
boolean w wyniku przeprowadzenia porównania:
a > b - a większe od b,
a < b - a mniejsze od b,
a >= b - a większe równe jak b,
a < =b - a mniejsze równe jak b,
a == b - a identyczne z b,
a != b - a różne od b.
Operatory logiczne również generują rezultat reprezentowany przez typ logiczny
boolean. Rezultat ten jest tworzony w wyniku działania operacji:
a && b - a i b (rezultatem jest true jeśli a i b są true);
a || b - a lub b (rezultatem jest true jeśli a lub b są true).
Operatory bitowe działają podobnie lecz operują na bitach. Ich zapis jest
następujący:
& - bitowy AND,
| - bitowy OR,
^ - bitowy XOR.
Operator warunkowy w Javie jest skróconą wersją instrukcji warunkowej if. Operator
ten ma postać:
wyrażenie_boolean ? wartość1 : wartość2;
gdzie:
wyrażenie_boolean oznacza operację generującą jako wynik true lub false;
wartość1 oznacza działanie podjęte wówczas, gdy wynik wyrażenia jest true;
wartość2 oznacza działanie podjęte wówczas, gdy wynik wyrażenia jest false.
//Relacje1.java
public class Relacje1{
public static void main(String args[]){
System.out.println("Kto to Vader?")
String vader = "Vader";
String anakin = "Anakin";
boolean test = anakin.equals(vader) ;
String s = (vader.equals(anakin)) ? vader : anakin;
System.out.println(s);
}
}// koniec public class Relacje1
Operacje przypisania polegają na podstawieniu zmiennej (lewa strona - left value -
Lvalue) danej wartości, lub wartości innej zmiennej albo wartości wynikowej operacji
(prawa strona - right value - Rvalue), np.: a = 12, a = b; a= a+b;.
Dodatkowo należy podkreślić zjawisko tzw. aliasingu, polegające na przypisaniu tego
samego uchwytu do dwóch zmiennych, czyli obie wskazują na ten sam obiekt, np.:
//Liczby.java
class Liczba{
int i;
}
public class Liczby{
public static void main(String args[]){
Liczba k = new Liczba();
Liczba l = new Liczba();
k.i=4;
l.i=10;
System.out.println(" Oto liczby: "+ k.i+", "+l.i);
k=l;
System.out.println(" Oto liczby: "+ k.i+", "+l.i);
l.i=20;
System.out.println(" Oto liczby: "+ k.i+", "+l.i);
}
}// koniec public class Liczby
Instrukcje sterujące
Instrukcje sterujące służą do sterowanie przepływem wykonywania instrukcji. Do
instrukcji sterujących można zaliczyć:
- pętle umożliwiające iteracyjne wykonywanie kodu tak długo aż spełniony jest
warunek,
- instrukcje wyboru umożliwiające wybór kodu w zależności od podanego
argumentu,
- instrukcje warunkowe umożliwiające wykonanie kodu w zależności od spełnienia
podanych warunków,
- instrukcje powrotu umożliwiające przedwczesne zakończenie pętli lub bloku kodu.
Pętla while:
while(wyrażenie_logicze)
wyrażenie
Pętla ta powoduje wykonywanie wyrażenia tak długo dopóki rezultat wyrażenia
logicznego jest true. Wyrażenie stanowi zazwyczaj blok programu wyróżnialny
klamrami.
Pętla do-while:
do
wyrażenie
while(wyrażenie_logicze);
Podobnie jak pętla while sprawdzany jest tu rezultat wyrażenia logicznego. Jeżeli
rezultat ten jest false wówczas przerywana jest pętla. Różnica pomiędzy while i dowhile
polega na tym, że w tej pierwszej warunek pętli sprawdzany jest przed
wykonaniem wyrażenia po raz pierwszy.
Petla for:
for(inicjowanie; wyrażenie_logicze ; krok)
wyrażenie
Pętla ta powoduje wykonanie wyrażenia tyle razy ile to wynika z warunku
przedstawionego w wywołaniu pętli. Warunek ten polega na określeniu wartości
startowej iteracji, określeniu końca iteracji oraz kroku. Przykładowo:
for (int i =0; i<10; i++){
System.out.println(Niech moc będzie z Wami);
}
Dla wszystkich pętli stosować można polecenia break i continue umieszczane w ciele
pętli. Polecenie break przerywa pętlę i ją kończy (następuje przejście do kolejnej
instrukcji w kodzie). Polecenie continue przerywa pętle dla danej iteracji i rozpoczyna
następną iterację.
Instrukcja wyboru switch:
switch(wyrażenie_wyboru) {
case wartość1 : wyrażenie; break;
case wartość2 : wyrażenie; break;
case wartość3 : wyrażenie; break;
case wartość4 : wyrażenie; break;
// …
default: wyrażenie;
}
Instrukcja wyboru switch powoduje sprawdzenie stanu wyrażenia_wyboru (zmiennej
liczbowej) i w zależności od jej stanu (wartości) wykonywane jest wyrażenie. Słowo
break oznacza przerwanie działania w instrukcji wyboru (nie jest wykonywane kolejne
wyrażenie). Domyślne wyrażenie jest wykonywane dla wszystkich innych stanów niż
te wymienione w ciele instrukcji.
Instrukcja warunkowa if:
if (wyrażenie_logiczne)) { wyrażenie } else { wyrażenie};
Instrukcja warunkowa if sprawdza stan logiczny wyrażenia logicznego i jeżeli jest true
to wykonywane jest pierwsze wyrażenie, jeśli false to wykonywane jest wyrażenie po
słowie kluczowym else.
Instrukcja powrotu return:
return (wyrażenie);
Instrukcja powrotu return kończy metodę, w ciele której się znajduje i powoduje
przeniesienie wartości wyrażenia do kodu wywołującego daną metodę. Oznacza to,
że typ wartości wyrażenia instrukcji return musi być zgodny z typem zadeklarowanym
w czasie definicji metody, w ciele której znajduje się instrukcja return.
Prześledźmy dwa przykłady obrazujące działanie pętli i instrukcji warunkowych.
//PetleCzasu.java
public class PetleCzasu{
public static void main(String args[]){
int i=0;
do{
i++;
if (i==10) break;
System.out.println("Moc jest z Wami");
}while(true);
for (int j=0; j<10; j++){
if ((j==2) || (j==4)) continue;
System.out.println("Moc jest ze MNA ");
}
}
}// koniec public class PetleCzasu
Powyższy program demonstruje działanie pętli do-while, pętli for oraz instrukcji
warunkowej if. W czasie obsługi pierwszej pętli ustawiono warunek logiczny na true.
Oznacza to, że pętla będzie wykonywała się w nieskończoność o ile w jej ciele nie
nastąpi przerwanie pętli. Przerwanie pętli w przykładzie 2.8 jest wykonane poprzez
wywołanie instrukcji break, po osiągnięciu przez wskaźnik iteracji wartości 10.
Przerwanie następuje przed poleceniem wydruku komunikat, tak więc zaledwie 9
razy zostanie wyświetlona wiadomość: Moc jest z Wami. Druga pętla przykładu
przebiega przez 10 iteracji, niemniej dla iteracji nr 2 i 4 generowane jest polecenie
continue, które powoduje przerwanie pętli dla danej iteracji i rozpoczęcie nowej.
Dlatego komunikat obsługiwany w tej pętli: Moc jest ze MNA, zostanie wyświetlony
zaledwie 8 razy. Kolejny przykład ukazuje zasadę działania instrukcji wyboru switch.
//Wybor.java
public class Wybor{
public static void main(String args[]) throws Exception{
System.out.println("Wybierz liczbę mieczy: "+
"1, 2, 3 lub 4 i naciśnij Enter");
int test = System.in.read();
switch(test){
case 49:
System.out.println("Wybrano 1");
break;
case 50:
System.out.println("Wybrano 2");
break;
case 51:
System.out.println("Wybrano 3");
break;
case 52:
System.out.println("Wybrano 4");
/* Brak break; program przejdzie do
pola default i wykona odpowienie
instrukcje */
default:
System.out.println("Wybrano cos");
}//koniec switch
}
}// koniec public class Wybor
Powyższy przykład ukazuje działanie instrukcji wyboru switch. Na początku
przykładowego programu pobierana jest wartość liczby całkowitej będącej
reprezentacją znaku przycisku klawiatury wciśniętego przez użytkownika programu.
Pobranie wartości znaku jest wykonane poprzez otwarcie standardowego strumienia
wejściowego (System.in - > otwarty InputStrem) i wywołanie metody read(),
zwracającej kod znaku jako liczbę int w przedziale 0-255. Następnie w zależności od
wartości pobranej liczby generowana jest odpowiednia wiadomość na ekranie
monitora. Po obsłudze każdego wyrażenia wyboru z wyjątkiem liczby 52 (kod znaku
1) umieszczana jest instrukcja przerwania break. Brak tej instrukcji powoduje
uruchomienie wyrażeń znajdujących się w obsłudze kolejnego wyrażenia wyboru
(np.: obsługa wartości 52). W przypadku, kiedy wciśnięty klawisz klawiatury różni się
od 1, 2, 3 czy 4 obsłużony zostanie standardowy warunek wyboru default.
Informacja praktyczna wyświetlanie polskich znaków w
konsoli platformy Javy
Jak można było zauważyć w pracy z poprzednimi programami przykładowymi
wszelkie komunikaty zawierające polskie litery wyświetlane były z krzakami zamiast
polskich liter. Warto rozważyć ten problem, ponieważ szereg aplikacji może
wymagać używania polskich liter.
Po pierwsze istotne jest uświadomienie sobie w jakim systemie kodowania znaków
stworzony został kod źródłowy programu. Jest to istotne, ponieważ często stosuje się
w kodzie źródłowym specyficzne znaki języka polskiego. Wiele programów zawiera w
swym kodzie źródłowym zainicjowane zmienne typu char lub String zawierające takie
znaki, np.:
(...)
String str = W mroku jawił się tylko żółty odcień świetlistego miecza;
(...)
Tworząc kod źródłowy zawierający specyficzne znaki języka należy zwrócić uwagę
na to, jaki edytor tekstu albo w jakim systemie kodowania znaków zapisany został
kod źródłowy. Dla MS Windows PL standardową metodą kodowania znaków jest
system Cp1250, często reprezentowany w edytorze jako kod ANSI. Natomiast dla
środowiska MS DOS PL wykorzystywaną stroną kodową jest Cp852. Jeszcze inaczej
może być w środowisku UNIX gdzie wykorzystuje się polską stronę kodową ISO-
8859-2. Zapisując kod źródłowy dokonujemy konwersji znaków na bajty zgodnie z
wybraną (lub standardową dla danej platformy) stroną kodową. Zestaw bajtów jest
zapisywany w pliku i może być następnie wykorzystywany do wyświetlenia jako tekst
po konwersji bajtów na znaki (wedle wybranej strony kodowej). Przykładowo tworząc
tekst w środowisku DOS PL np. korzystając z programu edit, zawierający polskie
znaki, następnie nagrywając plik z tym tekstem otrzymamy zbiór bajtów
odpowiadający znakom według strony kodowej Cp852. Otwarcie tego pliku w
środowisku Windows PL wybierając standardową stronę kodową (Cp1250)
spowoduje wyświetlenie tekstu bez polskich liter. Jest to oczywiście spowodowane
tym, że zapisane w pliku bajty kodowane przy wyświetlaniu według innego kodu
znaków niż przy tworzeniu będą reprezentowane przez inne znaki. Podobna sytuacja
wystąpi przy każdej innej zamianie stron kodowych. Ponieważ Java używa systemu
kodowania znaków Unicode do przechowywania wszelkich zmiennych typu char lub
String, dlatego w czasie kompilacji kodu wszystko jest zamieniane na Unicode.
Oznacza to, że również zmienne typu char i String są zamieniane na Unicode.
Konwersja ta przebiega następująco:
a) kompilator czyta bajty zapisane w pliku kodu źródłowego,
b) kompilator sprawdza zmienną środowiska (file.encoding) oznaczającą stronę
kodową dla danej platformy (System.getProperty(file.encoding);)
c) kompilator dokonuje konwersji bajtów na znaki według strony kodowej danje
platformy,
d) kompilator dokonuje konwersji powstałych znaków na odpowiadające im kody
znaków w Unicodzie (16-bitowe).
Ponieważ kompilator dokonuje konwersji bajtów z pliku poprzez stronę kodową
platformy na kody w Unicodzie istotna jest zgodność kodowania znaków przy
tworzeniu pliku (z kodem źródłowym) z kodowaniem przy konwersji znaków na
Unicode. Standardową stronę kodową platformy można uzyskać jako własność
systemu (zmienna Maszyny Wirtualnej) poprzez wywołanie metody
System.getProperty(file.encoding). Dla platformy Windows PL jest to oczywiście
Cp1250. Programista, pisząc kod źródłowy zawierający specyficzne znaki języka
polskiego po czym nagrywając go do pliku zgodnie ze stroną kodową Cp1250 (ANSI)
otrzymuje gotowy do kompilacji w standardowym środowisku Javy (Windows PL)
zbiór bajtów. Jeżeli plik źródłowy został zapisany zgodnie z inną stroną kodową niż
standardowa strona kodowa platformy (np. plik nagrano w środowisku DOS PL -
CP852) to wywołanie kompilatora musi jawnie wskazywać na sposób kodowania
użyty do stworzenia zbioru bajtów kodu źródłowego, np.:
Javac -g encoding "Cp852" NazwaPrrogramu.java
Ponieważ specyficzne znaki języka polskiego są jeszcze inaczej kodowane na
maszynach UNIX (ISO 8859-2), to właściwe wydaje się używanie Unicodu do zapisu
polskich liter w źródle programu. Można to robić bezpośrednio wpisując znaki
ucieczki, np. /u0105 zamiast znaku ą, lub poprzez wprowadzenie automatycznej
konwersji plików wykorzystując jednolity system kodowania. Kody specyficznych
znaków języka polskiego w Unicodzie są następujące:
Znak : ą Kod heksadecymalny : 0105 Kod dziesiętny : 261
Znak :ć Kod heksadecymalny :0107 Kod dziesiętny :263
Znak :ę Kod heksadecymalny :0119 Kod dziesiętny :281
Znak :ł Kod heksadecymalny :0142 Kod dziesiętny :322
Znak :ń Kod heksadecymalny :0144 Kod dziesiętny :324
Znak :ó Kod heksadecymalny :00F3 Kod dziesiętny :243
Znak :ś Kod heksadecymalny :015B Kod dziesiętny :339
Znak :ź Kod heksadecymalny :017A Kod dziesiętny :378
Znak :ż Kod heksadecymalny :017C Kod dziesiętny :380
Znak :Ą Kod heksadecymalny :0104 Kod dziesiętny :260
Znak :Ć Kod heksadecymalny :0106 Kod dziesiętny :262
Znak :Ę Kod heksadecymalny :0118 Kod dziesiętny :280
Znak :Ł Kod heksadecymalny :0141 Kod dziesiętny :321
Znak :Ń Kod heksadecymalny :0143 Kod dziesiętny :323
Znak :Ó Kod heksadecymalny :00D3 Kod dziesiętny :211
Znak :Ś Kod heksadecymalny :015A Kod dziesiętny :346
Znak :Ź Kod heksadecymalny :0179 Kod dziesiętny :377
Znak :Ż Kod heksadecymalny :017B Kod dziesiętny :379
Stworzenie odpowiednich kodów Unicodu w skompilowanych plikach B-kodu nie
stanowi jeszcze rozwiązania problemu wyświetlania polskich znaków w konsoli Javy.
Otóż Maszyna Wirtualna wykonując kod zapisany w pliku dokonuje konwersji
zmiennych znakowych z Unicodu na kod właściwy dla danej platformy. Dla Maszyny
Wirtualnej pracującej w środowisku Windows PL standardową stroną kodową jest
Cp1250. Oznacza to, że znaki zapisane kodowo w Unicodzie podlegają konwersji
na bajty dla strony kodowej Cp1250. W przypadku wyświetlania wiadomości na
ekranie bajty te są wysyłane do standardowego strumienia wyjścia (do konsoli) gdzie
są interpretowane i wyświetlane jako znaki. W tym momencie występuje pewien
problem. Otóż dla danych ustawień lokalnych (Strefa Czasowa i inne ustawienia
lokalne) dla Polski stroną kodową platformy jest Cp1250 lecz stroną kodową konsoli
(konsola DOS-u) jest Cp852. Dla tak zdefiniowanej konsoli bajty znaków powstałe
poprzez kodowanie w Cp1250 nie spowodują wyświetlenia polskich znaków lecz
otrzymamy inne znaki językowe. Jakie jest więc rozwiązanie tego problemu? Otóż
trzeba stworzyć własny strumień wyjścia obsługujący żądaną stronę kodową. W
rozważanym przypadku będzie to oczywiście Cp852. Do konstrukcji nowego
strumienia wyjścia można użyć klasy OutputStreamWriter, której konstruktor
umożliwia podanie metody kodowania znaków np.: PrintWriter o = new PrintWriter( new OutputStreamWriter(System.out,"Cp852"), true);. Następujący przykład ukazuje
sposób wykorzystania tej klasy.
//ZnakiPL.java
import java.io.*;
public class ZnakiPL{
public static void main(String args[]){
String st = "śłąęóżźćń";
char zn = 'ą';
byte c[] = new byte[10];
byte d[] = new byte[10];
String st1="nic";
String st2="nic";
String st3="nic";
String st4="nic";
try{
PrintWriter o = new PrintWriter( new OutputStreamWriter(System.out,"Cp852"), true);
System.out.println("Kodowanie to: "+System.getProperty("file.encoding"));
c=st.getBytes("Cp852");
d=st.getBytes("Cp1250");
o.println("W Unicodzie \u0105 to: "+(int)zn);
o.println("Je\u015Bli prezentowana liczba jest r\u00F3\u017Cna od 261 to oznacza,");
o.println("\u017Ce kod programu napisano z inn\u0105 stron\u0105 kodow\u0105 ni\u017C ta,");
o.println("kt\u00F3r\u0105 u\u017Cyto w kompilacji (standardowo dla Windows PL->Cp1250");
st2 = new String(c, "Cp852");
st1 = new String(d,"Cp1250");
st3 = new String(c,"Cp1250");
st4 = new String(d,"Cp852");
o.println("Przejście: Cp852(bajty)->Cp852(String)->Unicode(Java)->Cp852(strumien)daje: " + st2);
o.println("Przejście: Cp1250(bajty)->Cp1250(String)->Unicode(Java)->Cp852(strumien)daje: " +st1);
o.println("Przejście: Cp852(bajty)->Cp1250(String)->Unicode(Java)->Cp852(strumien)daje: " +st3);
o.println("Przejście: Cp1250(bajty)->Cp852(String)->Unicode(Java)->Cp852(strumien)daje: " +st4);
o.println("Przejście: Cp1250(String)->Unicode(Java)->Cp852(strumien)daje: " +st);
//wyświetla polskie znaki w konsoli DOS
System.out.println(" ");
System.out.println("Przejście: Cp852(bajty)->Cp852(String)->Unicode(Java)->Cp1250(strumien)daje: " + st2);
System.out.println("Przejście: Cp1250(bajty)->Cp1250(String)->Unicode(Java)->Cp1250(strumien)daje: " +st1);
System.out.println("Przejście: Cp852(bajty)->Cp1250(String)->Unicode(Java)->Cp1250(strumien)daje: " +st3);
System.out.println("Przejście: Cp1250(bajty)->Cp852(String)->Unicode(Java)->Cp1250(strumien)daje: " +st4);
System.out.println("Przejście: Cp1250(String)->Unicode(Java)->Cp1250(strumien)daje: " +st);
System.out.println(" ");
String st5= "Oto Unicode: "+ "\u0104\u0105\u0142\u0144\u00F3\u015B ";
System.out.println("Przejście: Unicode(String)->Unicode(Java)->Cp1250(strumien)daje: " + st5);
o.println("Przejście: Unicode(String)->Unicode(Java)->Cp852(strumien)daje: " + st5);
} catch (Exception e){
e.printStackTrace();
}
}// koniec main()
}// koniec public class ZnakiPL
Podobne problemy i sposób rozwiązania można wykorzystać przy innych przejściach
pomiędzy stronami kodowymi, oraz dla obsługi innych strumieni np.: plików.
Programowanie obiektowe
Obiekty
Człowiek różnymi metodami modeluje otaczający do świat, jego właściwości i
procesy w nim zachodzące. W opisie rzeczywistości wprowadzono pojęcia
kategoryzujące elementy świata. Do najbardziej znanych pojęć tego typu należy
zaliczyć byt jako coś co istnieje. Starożytni filozofowie dokonywali różnych
podziałów bytów. Wprowadzono podział na bytu ogólne i jednostkowe. Arystoteles
wprowadził ciekawy opis bytu używając terminów forma i materia. Tak rozpoczęto
klasyfikować byty pod względem ich form, czy też gatunku lub inaczej typu albo
wreszcie klasy. Można więc przedstawić klasę takich bytów, które posiadają wspólne
właściwości (formę). Klasa (forma) opisuje byt, jej połączenie z materią tworzy byt.
Można więc powiedzieć, że materia połączona z określoną formą tworzy jednostkowy
byt - czyli obiekt. W ten sposób uzyskano podstawę założeń programowania
obiektowego: Obiekt jest jednostkowym wystąpieniem Klasy go opisującej. Oznacza
to, że za pomocą programowania obiektowego tworzony jest model bytu, a nie jego
opis ilościowy, tak jak to jest wykonywane w programowaniu proceduralnym. Opis
obiektu w klasie odbywa się poprzez modelowanie jego zachowania (metody) i stanu
(pola). Jak wspomniano już na początku tego opracowania może istnieć wiele
obiektów danej klasy. Każdy z nich jest jednak jednostkowy i istnieje w pamięci
komputera. Dostęp do obiektu jest możliwy za pomocą uchwytu (odwołanie do
pamięci - stery - gdzie przechowywany jest obiekt). Nowy obiekt danej klasy
tworzony jest za pomocą instrukcji new z podaniem nazwy klasy, np.:
Rycerz luke = new Rycerz ();
oznacza, że tworzony jest nowy obiekt typu Rycerz, do którego przywiązany jest
uchwyt luke. Tworzenie obiektu danej klasy bardzo dobrze ilustruje stosowany już w
tej pracy kod obsługujący ładowanie klas:
Clas c = Class.forName("Rycerz");
Rycerz luke = c.newInstance();
Kod ten wskazuje, że dla potrzeb tworzenia obiektów klasy Rycerz pobierany jest
kod tej klasy a następnie tworzone jest nowe wystąpienie tej klasy. Oczywiście
znacznie prostsze jest stosowanie instrukcji new.
Jeżeli temu samemu uchwytowi przypisany zostanie inny obiekt, wówczas obiekt,
na który pierwotnie wskazywał uchwyt ginie. Nie ma więc potrzeby w Javie
usuwania nieużywanych obiektów, gdyż jest to wykonywane automatycznie. Każdy
obiekt jest więc jednostkowym wystąpieniem klasy i ma charakter dynamiczny.
Można jednak wyróżnić elementy niezmienne dla obiektów tej samej klasy.
Przykładowo liczba Rycerzy nie jest własnością bytu (obiektu) lecz klasy, która
opisuje byty tego typu. Oznacza to, konieczność definicji nie dynamicznych lecz
statycznych (niezmiennych) pól i metod klasy, do których odwołanie odbywa się nie
przez obiekty lecz przez nazwę klasy obiektów. Wszystkie pola i metody statyczne
muszą być wyróżnialne poprzez oznacznik static. Określenie pola klasy jako static
oznacza, że jego stan jest jednostkowy dla wszystkich obiektów danej klasy - jest
własnością klasy a nie obiektów. Obiekt zmieniając stan pola statycznego zmienia go
dla wszystkich innych obiektów. Przykładowo:
//Republika.java
class Rycerz{
static int liczbaRycerzy=0;
int numerRycerza =0;
Rycerz (String imie){
System.out.println(Rada Jedi głosi: + imie+ jest nowym Rycerzem Jedi);
}
}
public class Republika{
public static void main (String args[]){
Rycerz yoda = new Rycerz(Yoda);
Rycerz anakin = new Rycerz(Anakin);
Rycerz luke = new Rycerz(Luke);
yoda.numerRycerza=1;
yoda.liczbaRycerzy++;
System.out.println(Liczba rycerzy wg. Yody: +yoda.liczbaRycerzy);
System.out.println(Liczba rycerzy wg. Anakina: +anakin.liczbaRycerzy);
System.out.println(Liczba rycerzy wg. Lukea: +luke.liczbaRycerzy);
anakin.numerRycerza=2;
anakin.liczbaRycerzy++;
System.out.println(Liczba rycerzy wg. Yody: +yoda.liczbaRycerzy);
System.out.println(Liczba rycerzy wg. Anakina: +anakin.liczbaRycerzy);
System.out.println(Liczba rycerzy wg. Lukea: +luke.liczbaRycerzy);
luke.numerRycerza=3;
luke.liczbaRycerzy++;
System.out.println(Liczba rycerzy wg. Yody: +yoda.liczbaRycerzy);
System.out.println(Liczba rycerzy wg. Anakina: +anakin.liczbaRycerzy);
System.out.println(Liczba rycerzy wg. Lukea: +luke.liczbaRycerzy);
}
}// koniec public class Republika
Powyższy przykład ukazuje brak zależności zmiennej liczbaRycerzy od
poszczególnych obiektów. Statyczne pola klas są więc doskonałymi elementami
przechowującymi informację o stanie klasy (np. ilość otwartych okien, plików, itp.).
Możliwe jest również stworzenie statycznej metody, której działanie jest wywoływane
bez konieczności tworzenia obiektu klasy, w ciele której zdefiniowano statyczną
metodę. Przykładowa metoda statyczna może zwracać liczbę rycerzy przechowywaną jako pole statyczne poprzez:
public static int liczbR(){
iint lR = Rycerz.liczbaRyyucerzy;
System.out.println("Liczba Ryyczerzy Jedi w Republice wynosi: "+lR);
return lR;
}
Należy pamiętać, co zostało już przedstawione wcześniej, tworzenie obiektu
powoduje wywołanie procedury jego inicjowania zwanej konstruktorem. Konstruktor
jest metodą o tej samej nazwie co nazwa klasy, dla której tworzony jest obiekt.
Konstruktor jest wywoływany automatycznie przy tworzeniu obiektu. Stosuje się go
do podawania argumentów obiektowi, oraz do potrzebnej z punktu widzenia danej
klasy grupy operacji startowych. Wywołanie konstruktora powoduje zwrócenie
referencji do obiektu danej klasy. Nie można więc deklarować konstruktora z typem
void. Kod konstruktora może zawierać wywołanie innego konstruktora tej samej klasy
lub wywołanie konstruktora nadklasy. Kolejność wołania konstruktorów w kodzie
danego konstruktora jest następująca:
NazwaKlasy (argumenty){
this(argumenty1; // wywołanie innego konstruktora tej samej klasy
super(argumenty1); //wywołanie kkonstruktra nadklasy
kod;
}
Jeżeli programista jawnie nie zdefiniował konstruktora to jest on tworzony domyślnie,
jako kod pusty bez argumentów , np.:
NzawaKlasy(){
}
Rozważania dotyczące klas zamieszczono na początku tego opracowania. W Javie
istnieje możliwość sprawdzenia czy dany obiekt jest wystąpieniem danej klasy. Do
tego celu służy operator instanceof, np. luke instanceof Rycerz. Efektem działania
operatora jest wartość logiczna true - jeśli obiekt jest danej klasy, lub false - w
przeciwnym przypadku.
Klasy abstrakcyjne
Kolejnym bardzo ważnym elementem programowania obiektowego jest
odwołanie się do rozważań w filozofii nad możliwością istnienia bytów ogólnych
(pojęć ogólnych, uniwersalii, itp.). Przykładowe pytanie tych rozważań może być
następujące: Czy może istnieć obiekt ogólny Rycerz? Teoretycznie w Javie nie może
istnieć taki obiekt, lecz istnieje obiekt klasy Class związany i reprezentujący daną
klasę (np.: Class c = Class.forName(Rycerz);). Obiekt taki jest wykorzystywany
przez Javę do obsługi klas (szczególnie w zarządzaniu ładowaniem klas do pamięci).
W Javie istnieje jeszcze jeden typ klas, dla których nie można inicjować obiektów
metodą new. Ponieważ nie można stworzyć obiektów takiej klasy, klasa taka nie ma
rzeczywistego wykorzystania w programowaniu obiektowym i jest oznaczana jako
abstrakcyjna - abstract. Klasa abstrakcyjna zawiera w opisie obiektu również metody
abstrakcyjne. W celu stworzenia obiektu, dla którego opis znajduje się w klasie
abstrakcyjnej należy stworzyć klasę pochodną klasy abstrakcyjnej i podać
rzeczywisty opis (realizację) wszystkim metodom abstrakcyjnym zdefiniowanym w
klasie abstrakcyjnej. Klasy abstrakcyjne stosuje się wówczas, gdy na danym stopniu
opisu ogólnego możliwych obiektów nie można podać rzeczywistej realizacji jednej
lub wielu metod. Przykładowo tworząc klasę Statek nie można podać metody
obliczającej pole powierzchni statku, gdyż ta zależy od danego typu statku
kosmicznego, dla którego dany jest wzór na pole powierzchni. Tak więc klasa
pochodna typu statku np.: GwiezdnyNiszczyciel, może zdefiniować w swym ciele
metodę o tej samej nazwie co w klasie Statek, lecz określonej implementacji
(realizacji). Dzięki temu wszystkie typy statków opisywane własnymi klasami będą
miały podobny opis ogólny, ułatwiający wymianę informacji.
//Flota.java
abstract class Statek{
int numerStatku;
int liczbaDzial;
long predkoscMax;
public abstract int polePowierzchni();
public void informacje(){
System.out.println("Liczba dział = "+liczbaDzial);
System.out.println("Prędkość maksymalna = "+predkoscMax);
System.out.println("Numer identyfikacyjny = "+numerStatku);
}
} // koniec abstract class Statek{
class GwiezdnyNiszczyciel extends Statek{
int wysTrojkata;
int dlgPodstawy;
GwiezdnyNiszczyciel(int numer){
numerStatku=numer;
}
public int polePowierzchni(){
return (wysTrojkata*dlgPodstawy/2);
}
}// koniec class GwiezdnyNiszczyciel
class GwiezdnySokol extends Statek{
int szer;
int dlg;
GwiezdnySokol(int numer){
numerStatku=numer;
}
public int polePowierzchni(){
return (dlg*szer);
}
} // koniec class GwiezdnySokol
public class Flota{
public static void main(String args[]){
GwiezdnyNiszczyciel gw1 = new GwiezdnyNiszczyciel(1);
gw1.wysTrojkata=200;
gw1.dlgPodstawy=500;
gw1.liczbaDzial=6;
gw1.predkoscMax=100;
GwiezdnySokol gs1 = new GwiezdnySokol(1);
gs1.dlg=40;
gs1.szer=15;
gs1.liczbaDzial=3;
gs1.predkoscMax=120;
gw1.informacje();
System.out.println("Pole powierzchni Niszczyciela to: " + gw1.polePowierzchni() +" m(2)");
gs1.informacje();
System.out.println("Pole powierzchni Sokola to: " + gs1.polePowierzchni() +" m(2)");
}
}// koniec public class Flota
W powyższym przykładzie określono 4 klasy wśród których jedna jest abstrakcyjną
(Statek) w ciele której zawarto określone pola, metodę abstrakcyjną
polePowierzchni() oraz metodę zdefiniowaną informacje(). Dwie klasy
GwiezdnyNiszczyciel oraz GwiezdnySokol są klasami pochodnymi (dziedziczą o
czy później w tym rozdziale) klasy abstrakcyjnej i dokonują definicji metody
abstrakcyjnej dziedziczonej klasy. Ostatnia klasa Flota zawiera wywołanie obiektów
zdefiniowanych klas GwiezdnyNiszczyciel oraz GwiezdnySokol. Warto zauważyć, że
następuje odniesienie do metody informacje() klasy abstrakcyjnej tak, jakby była to
metoda obiektu klasy implementującej klasę abstrakcyjną (czyli GwiezdnyNiszczyciel
albo GwiezdnySokol).
Interfejsy, specyfikatory dostępu
Klasa abstrakcyjna oprócz metod abstrakcyjnych ma również metody
zdefiniowane. Abstrakcja wprowadzana przez taką klasę jest więc tylko częściowa.
Można sobie oczywiście wyobrazić sytuację gdzie wszystkie metody będą
abstrakcyjne. Wprowadzenie samych metod abstrakcyjnych, a więc tylko ich nazw,
argumentów wywołania i zwracanych typów umożliwia stworzenie uniwersalnego
zbioru funkcji możliwych do wykorzystania w różnych programach bez znajomości
realizacji danej metody. Realizacja danej metody może być wykonywana w różny
sposób w zależności od potrzeb bądź od otoczenia sprzętowo-programowego (np.
odczyt z portu, kodowanie wiadomości, itp.). Zbiór metod abstrakcyjnych jest więc swoistym interfejsem pomiędzy kodem użytkownika a zrealizowanymi metodami.
Java określa nowy typ zbiorczy nazywając go intereface, który składa się ze zbioru
metod abstrakcyjnych oraz ze zbioru statycznych (static) i stałych (final) pól. Tworząc
nową klasę wykorzystującą opis metod podany w interfejsie implementuje
(implements) się dany interfejs, tworząc definicję (realizację) każdej metody
abstrakcyjnej interfejsu. Przykładowy interfejs oraz jego implementacja może
przebiegać następująco:
// BronJedi.java
interface MieczJedi {
static final String typ="świetlny";
abstract void dzwiek();
abstract float moc(int oslabienie);
} // koniec interface MieczJedi
class BronLukea implements MieczJedi{
int dlugosc;
int szerokosc;
int mocGeneracji;
BronLukea(int d, int s, int m){
this.dlugosc=d;
this.szerokosc=s;
this.mocGeneracji=m;
}
public void dzwiek(){
System.out.println("Dźwięk:");
System.out.println("zzzzzzZZZZZZzzzzzzz");
}
public float moc(int oslabienie){
float r = szerokosc / 2;
float moc_miecza= (mocGeneracji / (dlugosc * r * r * 3.1456f) ) / oslabienie;
System.out.println("Moc miecza wynosi: "+moc_miecza);
return moc_miecza;
}
}// koniec class BronLukea
class BronVadera implements MieczJedi{
int dlugosc;
int szerokosc;
int mocGeneracji;
BronVadera(int d, int s, int m){
this.dlugosc=d;
this.szerokosc=s;
this.mocGeneracji=m;
}
public void dzwiek(){
System.out.println("Dźwięk:");
System.out.println("uuuuuuuuUUUUUUuuuuu");
}
public float moc(int oslabienie){
float r = szerokosc / 2;
float moc_miecza= (mocGeneracji / (dlugosc * r * r * 3.1456f) ) / oslabienie;
System.out.println("Moc miecza wynosi: "+moc_miecza);
return moc_miecza;
}
public void info(){
System.out.println("Miecz to broń słabych Jedi. ");
}
}// koniec class BronVadera
public class BronJedi {
public static void main(String args[]){
BronLukea bl =new BronLukea(70,5,100);
BronVadera bv =new BronVadera(60,5,80);
System.out.println("Miecz "+bl.typ+" Lukea:");
bl.dzwiek();
bl.moc(2);
System.out.println("Miecz "+bv.typ+" Vadera:");
bv.dzwiek();
bv.moc(2);
bv.info();
System.out.println("Miecz Luke'a ma większą moc! Giń Vader !");
}
}// koniec public class BronJedi
W powyższym przykładzie stworzono interfejs opisujący miecz. Do opisu tego
elementu wykorzystano pole typu String określające rodzaj miecza oraz dwie metody
abstrakcyjne dotyczące opisu dźwięku i mocy mieczy. Definicja pola zawiera
elementy deklaracji static oraz final. Otóż każde pole interfejsu, nawet jeśli nie jest to
jawnie przedstawione jest typu stałego (final) i statycznego (static). Wszystkie
elementy interfejsu, a więc zarówno pola jaki i metody są domyślnie zadeklarowane
jako publiczne (public). Można zadeklarować je również jako protected lecz nie
można jako private. Czym są specyfikatory dostępu public, protected oraz private?
Otóż oznaczają one sposób dostępu do elementów klasy w procesie komunikacji
pomiędzy różnymi obiektami i klasami (hermetyzacja odseparowanie składników).
Specyfikator public (publiczny) określa dany element jako ogólnodostępny dla
każdego kodu wykorzystującego dany element (np. dla programu klienta
korzystającego z biblioteki, w której zdefiniowano dany element). W celu zabronienia
innym klasom korzystania z określonych elementów należy je zadeklarować jako
private (prywatne). Tak zadeklarowane elementy będą własnością prywatną klasy i
jej metod. Można określić również dostęp do elementów tylko w pakiecie klas. Należy
wówczas zadeklarować takie elementy jako protected. Wówczas tylko klasy
pochodne (obiekty klas pochodnych) mogą korzystać z elementów oznaczonych jako
protected, inne klasy nie mają dostępu. Natura interfejsów jest taka, że muszą być
implementowane jeżeli celowym ma być ich stworzenie. Dlatego elementy interfejsów
nie mogą być specyfikowane jako private. Ważne jest również to, że każdy element
nie zadeklarowany jawnie specyfikatorem dostępu ma dostęp oznaczany jako
friendly. Specyfikator taki oznacza dostęp do elementów jako publiczny dla
wszystkich elementów tego samego pakietu, lecz jako prywatny poza nim.
Wykorzystując interfejsy należy pamiętać, że stworzenie definicji (realizacji) metody
abstrakcyjnej interfejsu wymaga podania takiego samego specyfikatora dostępu jak
dla deklaracji metody abstrakcyjnej w interfejsie. W ogromnej większości przypadków
jest to dostęp typu public. W przykładzie 3.3 w deklaracji metod abstrakcyjnych
interfejsu pominięto specyfikator. Oznacza to, że zostanie użyty domyślny
specyfikator public. Definicja metod abstrakcyjnych w klasach implementujących
interfejs musi zawierać jawnie specyfikator public przed podaniem zwracanego typu
metody. Następujący kod:
(...)
abstract float moc (int oslabienie);
(...)
float moc (int oslabiebienie){
//realizacja
}
(...)
wygeneruje w czasie kompilacji bład, ponieważ implemnetacha metody abstrakcyjnej jest postrzegana na zewnątrz jak private
Należy ją więc jawnie zadeklarować jako public:
public float moc (int oslabienie){
//realizacja
}
Wykorzystywany w interfejsach specyfikator final również określa dostęp. Ogólnie
element oznaczony jako final jest elementem o wartości niezmiennej (np. stała, stąd
nie ma konieczności stosowania słowa kluczowego const) i musi być zainicjowany w
czasie deklarowania. Specyfikator final może jednak służyć do innych celów niż tylko
oznaczanie stałych. Możliwe jest oznaczenie uchwytu do obiektu jako final.
Wówczas konieczne jest zainicjowanie obiektu wraz z deklaracją oraz konieczna jest
świadomość, że dany uchwyt nie może wskazywać innego obiektu w danym kodzie
programu. Przykładowy kod:
(...)
final Ryycerz r = new Ryycerz ("Luke");
(...)
r = new Rycerz ("Vader");
(...)
spowoduje błąd ponieważ wystąpiła próba zmiany referencji uchwytu obiektu r.
Uchwyt raz oznaczony jako final nie może wskazywać innego obiektu, jednakże nie
oznacza to, że obiekt nie może się zmieniać. Słowem kluczowym final można też
oznaczać pola bez ich inicjowania (blank final) , niemniej należy takie inicjowanie
przeprowadzić zanim dane pole zostanie wykorzystane (np. w konstruktorze klasy).
Specyfikator final może być również stosowany w metodach klasy. Oznaczenie
argumentu metody jako final określi, że nie można dokonywać zmian na tym
argumencie w ciele metody. Oznaczenie metody jako final powoduje dwie
konsekwencje: po pierwsze sprawia, że metoda nie może podlegać zmianom w
procesie dziedziczenia klas, po drugie kompilator pracując z taką metodą wgrywa jej
kod zamiast tworzyć referencje (call) do kodu tej metody (podobnie jak inline w C).
Oczywiście można również określić klasę jako final. Wówczas niemożliwe jest
dziedziczenie (tworzenie klas pochodnych) z tak określonej klasy.
Z wykorzystywaniem interfejsów w Javie związane są jeszcze dwa zagadnienia.
Pierwsze z nich dotyczy prezentowanych już klas wewnętrznych (klas anonimowe),
drugie adapterów.
Statyczne klasy wewnętrzne
Omawiając klasy wewnętrzne warto rozważyć przypadek kiedy nie trzeba
tworzyć obiektu klasy zewnętrznej aby stworzyć obiekt klasy wewnętrznej. Jest to
możliwe wówczas, gdy klasa wewnętrzna będzie zadeklarowana jako statyczna.
//MieczL70Info.java
interface Dlugosc{
public String wyswietl();
}// koniec interface Dlugosc
public class MieczL70Info {
public static int d=70;
static class Dlg implements Dlugosc{
private int y;
public Dlg(int j) {
y=j;
System.out.println(wyswietl());
}
public String wyswietl(){
return (new String("Długość miecza L70 = "+y));
}
}//koniec static class Dlg
public static Dlugosc info(int i){
return (new Dlg(i));
}
public static void main(String args[]) {
MieczL70Info.info(d);
}
}//koniec public class MieczL70Info
W powyższym przykładzie stworzenie obiektu klasy wewnętrznej odbyło się bez
potrzeby tworzenia obiektu klasy wewnętrznej. Zdefiniowana klasa wewnętrzna Dlg
jest określona jako static, implementuje interfejs Dlugosc definiując jego metodę
wyswietl(). Wywołanie stworzenia obiektu odbywa się poprzez uruchomienie metody
klasy zewnętrznej MieczL70Info typu Dlugosc (interfejs), a mianowicie metody info().
Klasy anonimowe
Z punktu widzenia zastosowań interfejsów o wiele ciekawsze są innego
rodzaju definicje klas wewnętrznych. Rozpatrzmy kolejny przykład:
//Bateria.java
interface MocGeneracji{
abstract public int moc();
}// koniec interface MocGeneracji
public class Bateria{
int val;
Bateria(int poziomEnergii){
this.val=poziomEnergii;
System.out.println("Stan baterii= "+this.val);
}
public MocGeneracji m_gen(){
return new MocGeneracji() {
public int moc(){
return (val / 10);
}
}; //średnik kończy linię instrukcji w ciele metody m_gen()
}
public static void main(String args[]){
Bateria b = new Bateria(40);
MocGeneracji mg = b.m_gen();
System.out.println("Maksymalna moc generacji= "+mg.moc());
}
}//koniec public class Bateria
Przykład powyższy wprowadza ciekawą konstrukcję. W ciele klasy głównej aplikacji
zdefiniowano metodę m_gen() typu MocGeneracji (interfejs). Metoda ta musi
zwracać obiekt typu MocGeneracji czyli obiekt interfejsu! Obiekt zwracany jest
poprzez klasyczne wyrażenie w instrukcji return: new MocGeneracji(). Ciekawe
jednak jest to, co dzieje się bezpośrednio po instrukcji tworzenia obiektu. Otóż
definiowana jest tam klasa, nie posiadająca swojej nazwy, stąd nazywana klasą
anonimową. W ciele tej klasy wyróżnianej blokiem kodu (klamry) zawarto definicję
metody dla zadeklarowanej metody abstrakcyjnej interfejsu, którego obiekt jest
tworzony. Stworzony obiekt klasy anonimowej jest automatycznie rzutowany (new
MocGeneracji()) na typ interfejsu. Konstruktor klasy anonimowej jest oczywiście
domyślny (metoda pusta). W ciele metody main() aplikacji wykorzystano stworzoną
klasę anonimową w ten sposób, że zainicjowany obiekt typu interfejs: MocGeneracji
jest właściwie obiektem klasy anonimowej, stąd tylko uchwyt obiektu jest
deklarowany jako uchwyt typu MocGeneracji i wskazuje on na obiekt klasy
anonimowej. Dlatego możliwe jest wywołanie właściwości obiektu poprzez
zastosowanie uchwytu interfejsu. Dzięki temu możliwe jest wyświetlenie, w ostatniej
linii kodu tej przykładowej aplikacji, komunikatu zawierającego wartość zwracaną
przez metodę obiektu klasy anonimowej. Należy pamiętać, że definiowanie klasy
anonimowej ma te same właściwości co definiowanie klasy jawnej. Dlatego istotne
jest rozpatrzenie przy konstrukcji takich klas zastosowanych praw dostępu do pól i
metod. Przykładowo umieszczenie zmiennej (bez specyfikatora final) w ciele metody,
w której definiuje się klasę anonimową korzystającą z tej zmiennej spowoduje błąd
kompilacji.
Adaptery
Korzystanie z interfejsów ma jednak czasem swoje złe strony. Otóż stosując
interfejs zdefiniowany w jakimś pakiecie w opracowywanym kodzie programista musi
zawsze dokonać definicji wszystkich metod zadeklarowanych w interfejsie. Jeśli
metod tych jest dużo a programista kilkakrotnie korzysta z danego interfejsu
wykorzystując zaledwie dwie metody, wówczas wiele linijek powstałego kodu to kod
martwy (nieużyteczny). W takich przypadkach warto stworzyć klasę implementującą
dany interfejs, tworząc w jej ciele puste metody odpowiadające metodą
zadeklarowanym w interfejsie. Korzystając w programie z interfejsu, korzysta się
wówczas z tak stworzonej klasy, stanowiącej niejako element adaptujący interfejs do
potrzeb programisty. Klasa adaptująca nosi nazwę w Javie adaptera. Wykorzystanie
adaptera w kodzie tworzonego programu możliwe jest dzięki właściwościom
dziedziczenia (tworzona klasa dziedziczy z klasy adaptera, czyli korzysta z jej metod)
oraz przesłaniania (realizacja metody jest wykonywana w kodzie programu jako
przepisanie metody z klasy adaptera). Dziedziczenie i przesłanianie są omówione
dalej. Poniższy przykład ukazuje tworzenie i wykorzystanie klasy adaptera.
//Robot.java
interface R2D2{
String wyswietl();
String pokaz();
int policz(int a, int b);
float generuj();
double srednia();
}
abstract class R2D2Adapter implements R2D2{
public String wyswietl(){
return "";
}
public String pokaz(){
return"";
}
public int policz(int a, int b){
return 0;
}
public float generuj(){
return 0.0f;
}
public double srednia(){
return 0.0d;
}
}
public class Robot extends R2D2Adapter{
public String wyswietl(){
String s = "Tekst ten jest tworem adaptacji R2D2";
System.out.println(s);
return s;
}
public static void main(String args[]) {
Robot r = new Robot();
r.wyswietl();
}
}//koniec public class Robot
Dziedziczenie
Nazwa dziedziczenie związana jest z procesem ewolucyjnym, w którym
potomkowie posiadają pewne cechy rodziców. Podobne znaczenie tej nazwy jest
używane w programowaniu obiektowym. Otóż zakładając, że każdy typ lub gatunek
może mieć swój podgatunek, a więc zawężony i uszczegółowiony opis obiektów, to
właściwości tych obiektów będą wynikały zarówno ze specyfikacji gatunku jak i
podgatunku. Inaczej mówiąc, elementy klas nadrzędnych będą również elementami
ich pochodnych (klas dziedziczących z klasy nadrzędnej). Przykładowo klasą
nadrzędną może być klasa Rycerz, klasami z niej dziedziczącymi mogą być klasy
Człowiek, Gungan, Kobieta, itp. W Javie określenie dziedziczenia odbywa się
poprzez użycie słowa kluczowego extends (rozszerza). Przykładowa deklaracja:
class Kobieta extends Rycerz {
(...)
}
określa, że tworzona klasa Kobieta dziedziczy z klasy Rycerz. W Javie możliwe jest
tylko dziedziczenie typu jeden-do-jednego, co oznacza, że klasa może dziedziczyć
tylko z jednej klasy nadrzędnej. Ponieważ klasa nadrzędna może również
dziedziczyć z jednej klasy dla niej nadrzędnej otrzymuje się specyficzne drzewo
dziedziczenia w Javie. Nadrzędną klasą dla wszystkich klas w Javie jest klasa
Object. Wszystkie klasy bezpośrednio lub pośrednio z niej dziedziczą, czyli klasa ta
stanowi korzeń drzewa dziedziczenia. Ponieważ znajduje się ona w pakiecie
java.lang.*, można powiedzieć, że wszystkie klasy będą korzystały z tego pakietu. W
Javie możliwe jest więc dziedziczenie wielopoziomowe polegające na tym, że wiele
klas może dziedziczyć z jednej (lecz nie odwrotnie).
Oczywiście istnieje możliwość sterowania sposobu wykorzystania elementów klasy
nadrzędnej przez klasy pochodne. Do podstawowych metod tego typu sterowania
należą: przeciążenie metod oraz przepisanie metod. Przeciążenie polega w
programowaniu obiektowym na rozszerzeniu działania danej operacji (o tej samej
nazwie) na innego typu argumenty. Przykładowo przeciążenie operatorów oznacza
stworzenie możliwości wykorzystywania np. znaku + do dodawania dwóch obiektów.
Jak wspomniano wcześniej przeciążenie operatorów nie jest dostępne w Javie poza
jedynym wyjątkiem dotyczącym dodawania obiektów typu String. Przeciążenie metod
polega na definicji zbioru metod o tej samej nazwie lecz operujących na innych
typach danych (argumentach). Przykładowo :
//StanRepubliki.java
class Rep{
int x = 36;
String wiad="Stan Republiki: ";
String typ=" światów.";
void stan(String s){
System.out.println(wiad+s+typ);
}
void stan(int i){
System.out.println(wiad+i+typ);
}
}// koniec class Rep
public class StanRepubliki extends Rep{
public static void main(String args[]){
StanRepubliki st = new StanRepubliki();
st.stan(36);
st.stan("trzydzieści sześć");
}
}//koniec public class StanRepubliki
W powyższym przykładzie zastosowano przeciążenie metod stan() klasy nadrzędnej
Rep. Pierwotna metoda stan() zdefiniowana jest dla argumentu typu String. Druga
metoda stan() dokonuje przeciążenia metody pierwotnej o obsługę argumentu typu
int. W ten sam sposób możliwe jest wyświetlenie różnych typów danych przez bardzo
często używaną w tej pracy instrukcję System.out.println(). Otóż w klasie PrintStream
(obiekt out ) zdefiniowano szereg metod println() dla różnych typów argumentów.
Dzięki temu bardzo wygodne jest stosowanie tej samej nazwy metody bez zbędnego
rozważania nad typem argumentu.
Innym sposobem sterowania relacjami pomiędzy elementami klasy nadrzędnej i
pochodnej jest przepisywanie lub inaczej przesłanianie metod. Przesłanianie polega
na stworzeniu w klasie pochodnej nowej definicji dla metody istniejącej w klasie
nadrzędnej. Stworzony obiekt w czasie odwołania się do danej metody podczas
wykonywania programu wywołuje odpowiedni kod. Przywiązanie odpowiedniego
kodu metody, a więc kodu na nowo zdefiniowanej metody, jest zadaniem obiektu w
czasie wykonywania programu. Proces ten jest często nazywany przywiązaniem
dynamicznym (dynamic binding), a konstrukcja kodu zawierająca różne definicje tak
samo zadeklarowanej metody określana jest polimorfizmem. Poniższy przykład
ukazuje sposób przesłaniania (polimorfizmu) metod i jest odniesiony do
prezentowanego wyżej kodu programu.
//StanRepubliki1.java
class Rep{
int x = 36;
String wiad="Stan Republiki: ";
String typ=" światów.";
void stan(String s){
System.out.println(wiad+s+typ);
}
void stan(int i){
System.out.println(wiad+i+typ);
}
}// koniec class Rep
public class StanRepubliki1 extends Rep{
void stan(int i){
i--;
System.out.println("Bez planety Naboo Republika skłąda się z "+i +
" światów");
}
public static void main(String args[]){
StanRepubliki1 st = new StanRepubliki1();
st.stan(36);
st.stan("trzydzieści sześć");
}
}//koniec public class StanRepubliki1
Przykład ten jest rozwinięciem kodu StanRepubliki i zawiera dodatkową definicję
metody stan() w ciele klasy pochodnej StanRepubliki1. Nowa definicja powoduje
przesłanianie starej, stąd wywołanie polecenia st.stan(36) wywoła pojawienie się
napisu:
Bez planety Naboo Republika składa się z 35 światów
zamiast:
Stan Republiki: 36 światów
Przeciążanie i przesłanianie metod są zatem niezwykle efektywnymi metodami w
konstruowaniu funkcjonalności obiektów.
Niszczenie obiektów zwalnianie pamięci
Bardzo istotnym zagadnieniem w rozważaniach nad pracą z obiektami jest
problem niszczenia obiektów. O ile tworzenie obiektów i ich inicjowanie za pomocą
konstruktora jest jawnie określone (instrukcja new), o tyle niszczenie obiektów i
zwalnianie przydzielonych im zasobów jest niejawne. W Javie nie istnieje operator
delete (C++) umożliwiający usunięcie obiekty, lub operator free (C ) zwalniającyprzydzieloną pamięć. Dlaczego? Dlatego, że wszystko w Javie dzieje się
dynamicznie. Dynamicznie przydzielana jest pamięć obiektowi, dynamicznie jest więc
też zwalniana. Owa dynamiczność określa nic innego jak to, że programista nie
kontroluje dostępu do pamięci, tak więc nie wie gdzie Java umieszcza poszczególne
obiekty. Obiekt będący w pamięci komputera jest dostępny dla użytkownika poprzez
uchwyt. Uchwyt jest więc odniesieniem do obiektu i sprawia, że obiekt jest
określony. Brak odniesienia do obiektu sprawia, że obiekt jest niewykorzystywany,
tak więc Java może go zniszczyć, a co za tym idzie zwolnić pamięć. Niszczeniem
obiektów i zwalanianiem pamięci zajmuje się w Javie proces działający w tle noszący
nazwę GarbageCollection (czyli kolekcjoner śmieci). Obiekty bez referencji są
umieszczane w śmietniku a pamięć im przydzielona jest zwalniana najszybciej jak
to jest możliwe (proces kolekcjonera śmieci posiada niski priorytet stąd zwrócenie
przydzielonej pamięci do systemu nie koniecznie odbędzie się natychmiastowo). Nie
wiadomo kiedy proces GarbageCollection dokonuje zwalniania pamięci i programista
nie ma żadnej, bezpośredniej możliwości kontrolowania tego procesu.
//Rozmiar.java
class Rep{
int x = 36;
String wiad="Stan Republiki: ";
String typ=" światów.";
void stan(String s){
System.out.println(wiad+s+typ);
}
void stan(int i){
System.out.println(wiad+i+typ);
}
}// koniec class Rep
public class Rozmiar extends Rep{
void stan(int i){
i--;
System.out.println("Republika skłąda się z "+i +" światów");
}
public static void main(String args[]){
System.gc();
Thread.currentThread().yield();
long s1 = Runtime.getRuntime().freeMemory();
System.out.println("Test rozmiaru1: "+s1+ " lat św.(3)");
long s2 = Runtime.getRuntime().freeMemory();
System.out.println("Test rozmiaru2: "+s2+ " lat św.(3)");
long stan1 = Runtime.getRuntime().freeMemory();
System.out.println("Test rozmiaru3: "+stan1+ " lat św.(3) \n");
Rozmiar[] r = new Rozmiar[10000];
long stan2 = Runtime.getRuntime().freeMemory();
System.out.println("Zadeklarowano 10000 obiektów, rozmiar: "+stan2+" lat św.(3)");
for ( int i =0; i < r.length; i++ )
r[i] = new Rozmiar();
long stan3 = Runtime.getRuntime().freeMemory();
System.out.println("Zainicjowano 10000 obiektów, rozmiar: "+stan3+ " lat św.(3) \n");
r=null;
long stan4 = Runtime.getRuntime().freeMemory();
System.out.println("Brak odwołania, rozmiar: "+stan4+ " lat św.(3)");
/*
System.gc();
long stan5 = Runtime.getRuntime().freeMemory();
System.out.println("Wywołano likwidację, rozmiar: "+stan5+ " lat św.(3)\n");
*/ //można usunąć ten komentarz i wywołać GC
int[] liczbaSatelitówPlanet = new int[100000];
long stan6 = Runtime.getRuntime().freeMemory();
System.out.println("Daklaracja 100000 int, rozmiar: "+stan6+ " lat św.(3)");
for ( int i =0; i < liczbaSatelitówPlanet.length; i++ )
liczbaSatelitówPlanet[i] = 12;
long stan7 = Runtime.getRuntime().freeMemory();
System.out.println("Inicjacja 100000 int, rozmiar: "+stan7+ " lat św.(3) \n");
liczbaSatelitówPlanet=null;
long stan8 = Runtime.getRuntime().freeMemory();
System.out.println("Brak odwołania, rozmiar: "+stan8+ " lat św.(3)");
System.gc();
long stan9 = Runtime.getRuntime().freeMemory();
System.out.println("Wywołano likwidację, rozmiar: "+stan9+" lat św.(3)");
}
}//koniec public class Rozmiar
Warto prześledzić działanie powyższego programu. Otóż na początku programu
wywoływany jest jawnie proces GarbageCollection. Wywołanie to jest wezwaniem do
systemu aby wykonał on operację niszczenia nie wykorzystywanych obiektów. W
kolejnym kroku zostaje trzykrotnie pobrana i wyświetlona informacja dotycząca
wielkości stery. Wyświetlone wartości mogą się nieznacznie wahać (pamięć jest
obszarem dynamicznym platformy) i dla standardowej wielkości początkowej sterty
równej 1MB wyniosą około 800kB. Kolejną operacją w programie jest deklaracja
tablicy 10 000 obiektów typu Rep. W wyniku tej deklaracji zostaną umieszczone w
pamięci uchwyty w liczbie 10 000, przy czym każdy uchwyt obiektu jest
reprezentowany przez 4 bajty (platforma Windows NT) co oznacza, że wielkość
pamięci zmaleje o 40 000 bajtów. Następnie wykonano w programie inicjacje
zadeklarowanych obiektów klasy Rep, co również zmniejszyło pamięć o tyle ile jest
potrzebne przez 10 000 obiektów klasy Rep. Kolejnym krokiem jest zmiana
odniesienia uchwytu. Brak odniesienia nie wpłynął jednak bezpośrednio na zmianę
wielkości wolnej pamięci. Jest to związane z tym, że GarbageCollection nie zwalnia
pamięci bezpośrednio po zmianie odniesienia (generacji martwego obiektu), lecz
dopiero po pewnym czasie (nie wiadomo dokładnie ile ponieważ jest to proces
dynamiczny). W przykładowym programie wykonano następnie operację stworzenia
tablicy zmiennych typu podstawowego int o rozmiarze 100 000. Ponieważ definicja
tablicy zmiennych typu podstawowego powoduje automatyczne wypełnienie tej
tablicy wartościami domyślnymi danego typu, dlatego pamięć zmniejszyła się o
wielkość 100 000 * 4 B (int), czyli o 400 000B. Podstawienie własnych wartości do
pól tablicy nie zmienia wielkości wolnej pamięci. W kolejnym kroku zmieniono
odniesienie uchwytu obiektu tablicy na null, tworząc w ten sposób obiekt tablicowy
obiektem martwym. Jak wspomniano wcześniej zmiana odniesienia nie musi
wywołać natychmiastowego zwolnienia pamięci. W celu wymuszenia zwolnienia
pamięci wykonano jawne przywołanie procesu GarbageCollection. Przywołanie to
może dać efekt (lecz nie zawsze musi taki efekt wystąpić od razu, szczególnie dla
obiektów innych niż tablicowe -> patrz komentarz w kodzie rozważanego przykładu) i
zwolniona może zostać pamięć zajmowana poprzednio przez obiekt tablicowy oraz
zbiór obiektów klasy Rep. Praktycznie programista nie ma możliwości uzyskania
kontroli nad pracą procesu GarbageCollection (a więc brak jest synchronicznej
kontroli nad procesem zwalniania pamięci).
Praca z Maszyną Wirtualną Javy umożliwia ustawienie parametrów związanych z
procesem GarbageCollection oraz związanych z wielkością pamięci. I tak wywołanie
Maszyny Wirtualnej z opcją -Xnoclassgc wyłącza proces GarbageCollection (np.:
java -Xnoclassgc Rozmiar); wywołanie Maszyny Wirtualnej z opcją -XmsNNN, gdzie
NNN jest liczbą całkowitą, powoduje ustawienie początkowej wielkości sterty na NNN
bajtów (np.: java -Xms8000000 Rozmiar); wywołanie Maszyny Wirtualnej z opcją -
XmxNNN, gdzie NNN jest liczbą całkowitą, powoduje ustawienie maksymalnej
wielkości sterty na NNN bajtów (np.: java -Xmr8000000 Rozmiar).
Pamięć nie jest jednak jedynym zasobem jaki może wykorzystywać obiekt. Dlatego
ważne jest czasem jawne zwracanie zasobów w czasie niszczenia obiektu.
Przykładowo trzeba czasem zamknąć plik (strumień do pliku), który jest otwarty albo
zniszczyć gniazdo dla połączeń w oparciu o TCP/IP. Z tych względów stworzono w
Javie możliwość wykonania działania w czasie niszczenia obiektu. Jest to możliwe
dzięki przesłonięciu metody finalize() (metoda bez argumentów i nic nie zwracająca
void). Instrukcje umieszczone w ciele tej metody zostaną wykonane w czasie
niszczenia obiektu klasy, w ciele której znajduje się metoda finalize().
Tablice
Tworzenie tablicy w Javie jest jednoznaczne tworzeniu obiektu typu Array
zawierającego zbiór elementów zadeklarowanego typu. Przykładowo definicja int []
liczby = new int[10]; tworzy obiekt zawierający 10 pól, każde przechowujące wartość
domyślną typu int. Standardowo dla stworzonej tablicy istnieje jej obiekt, który
przechowuje w niejawnym polu długość tablicy (pole length). Przechowanie wartości
w tablicy polega na wykorzystaniu instrukcji przypisania z podaniem numeru pola w
tablicy: liczba[3] = 123. W Javie można oczywiści stworzyć tablicę dowolnych
obiektów, a nie tylko tablicę zmiennych podstawowych typów danych. Podobnie jak
inne języki programowania Java umożliwia tworzenie tablic wielopoziomowych.
Każdy dodatkowy poziom jest zaznaczany dodatkowym zbiorem nawiasów w
instrukcji definiującej obiekt lub przy dowolnym odwołaniu się do elementu
wielopoziomowej tablicy. Najprostszą tablica wielopoziomową jest oczywiście
macierz definiowana jako np.: int [][] liczby = new int[10][20]; czyli jest to macierz o
wymiarach 10 na 20 przechowująca elementy typu int. Rozmiar wielopoziomowej
tablicy można uzyskać posługując się kolejno polem length, np.: long rozmiar1 =
liczny[].length; long rozmiar2 = liczby.length.
//Dane.java
public class Dane extends Rep{
public static void main(String args[]){
int stan[][] = new int [2][2];
for (int i = 0; i
System.out.println("Liczba satelitów planety "+i+
" w galaktyce "+j+"wynosi: "+stan[i][j]);
}
}
long wiek[] = new long[3];
wiek[0]=600000000L;
wiek[1]=35000000L;
System.out.println("Wiek planety 0 = "+wiek[0]+ " lat");
System.out.println("Wiek planety 1 = "+wiek[1]+ " lat");
System.out.println("Wiek planety 2 = "+wiek[2]+ " lat");
}
}//koniec public class Dane
W powyższym przykładzie stworzono dwie tablice. Pierwsza z nich jest macierzą o
rozmiarach 2x2, której inicjalizacji dokonano w pętli for. Warto zwrócić uwagę na
sposób określania rozmiaru macierzy. Stosowana metoda jest bardzo efektywna i
szybka. Druga tablica jest zdefiniowania do przechowywania trzech elementów typu
long. Inicjowanie tablicy odbywa się tylko dla dwóch pól. Oznacza to, że wartość
trzeciego pola będzie domyślna dla typu long czyli 0. Efekty stworzenia i
zainicjowania tablic są wyświetlone na ekranie.
Programowanie współbieżne wątki
Rys historyczny
Na początku człowiek stworzył maszynę. Dalsza historia rozwoju technologii to
próby uzyskania jak największej efektywności działania maszyny. Stwierdzenie to
jest również słuszne dla komputera - maszyny matematycznej.
W pierwszym okresie użytkowania komputerów, celem zwiększenia ich efektywności
działania, stosowano programowanie wsadowe. W podejściu takim, programy
wykonywały się cyklicznie: jeden po drugim. Komputer stał się maszyną
wielozadaniową wykonującą różne operacje, np. liczył całkę, po czym rozwiązywał
model fizyczny zjawiska, następnie obliczał wymaganą grubość pancerza czołgu, itd.
Ponieważ typy zadań są często różne, wymagają odmiennych zasobów komputera,
dlatego opracowano rozwiązanie umożliwiające dzielenie zasobów komputera
pomiędzy poszczególnych użytkowników. W ten sposób każdy użytkownik
otrzymywał prawo wykonania swojego programu na komputerze. Rozwiązanie to
było niezwykle istotne w przypadku konieczności wykonania dwóch zadań, skrajnie
obciążającego czasowo komputer oraz mało obciążającego czasowo komputer, np.
policzenie parametrów skomplikowanego (wielowymiarowego) modelu i rozwiązanie
układu równań. Wielozadaniowość nie stanowiła jednak wystarczająco efektywnego
rozwiązania. Zaproponowano więc możliwość podziału zadań (programów) na
mniejsze funkcjonalnie spójne fragmenty kodu wykonywanego (odseparowane
strumienie wykonywania działań) zwane wątkami (threads). Wątek jest więc
swoistym podprogramem (zbiorem wykonywanych operacji) umożliwiającym jego
realizację w oderwaniu od innych wątków danego zadania. Relacja pomiędzy
wątkami określana głównie poprzez dwa mechanizmy: synchronizację i zasadę
pierwszeństwa. Synchronizacja wątków jest niezwykle istotnym zagadnieniem,
szczególnie wówczas gdy są one ze sobą powiązane, np. korzystają z wspólnego
zbioru danych. Określanie zasad pierwszeństwa - priorytetów jest pomocne
wówczas, gdy efekt działania danego wątku jest o wiele bardziej istotny dla
użytkownika niż efekt działania innego wątku. Oczywiście wątki mogą być
generowane nie tylko jawnie przez użytkownika komputera ale również niejawnie
poprzez wywołane programy komputerowe. Przykładowo dla Maszyny Wirtualnej
Javy działa co najmniej kilka wątków: jeden obsługujący kod z metodą main(), drugi
obsługujący zarządzanie pamięcią (GarbageCollection) jeszcze inny zajmujący się
odświeżaniem ekranu, itp. Tworzenie wielu wątków jest docelowo związane z
możliwością ich równoczesnego wykonywania (programowanie równoległe). Możliwe
jest to jednak jedynie w systemach wieloprocesorowych. W większości obecnych
komputerach osobistych i stacjach roboczych współbieżność wątków jest
emulowana. Stosowanie podziału kodu na liczne wątki (procesy danego programu
zadania) ma więc charakter zwiększenia efektywności działania (głównie w
systemach wieloprocesorowych) oraz ma charakter porządkowania kodu (głównie w
systemach jednoprocesorowych - podział zadań).
Programy Javy (zarówno aplety jak i aplikacje) są ze swej natury wielowątkowe.
Świadczy o tym choćby najprostszy podział na dwa wątki: wątek obsługi kodu z
metodą main() (dla aplikacji) oraz wątek zarządzania stertą GarbageCollection
(posiadający znacznie niższy priorytet). Oczywiście programista tworząc własną
aplikację może zapragnąć podzielić przepływ wykonywania działań w programie na
szereg własnych wątków. Java umożliwia dokonanie takiego podziału.
Tworzenie wątków w Javie
W celu napisania kodu wątku w Javie konieczne jest albo bezpośrednie
dziedziczenie z klasy Thread lub pośrednie poprzez implementację interfejsu
Runnable. Wykorzystanie interfejsu jest szczególnie istotne wówczas, gdy dana
klasa (klasa wątku) dziedziczy już z innej klasy, a ponieważ w Javie nie może
dziedziczyć z dwóch różnych klas, dlatego konieczne jest zaimplementowanie
interfejsu. Każdy wątek powinien być wywołany, mieć opisane zadania do wykonania
oraz posiadać zdolność do zakończenia jego działania. Funkcjonalność tą, otrzymuje
się poprzez stosowanie trzech podstawowych dla wątków metod klasy Thread:
start() - jawnie wywołuje rozpoczęcie wątku,
run() - zawiera zestaw zadań do wykonania,
interrupt() - umożliwia przerwanie działania wątku.
Metoda run() nie jest wywoływana jawnie lecz pośrednio poprzez metodę start().
Użycie metody start() powoduje wykonanie działań zawartych w ciele metody run().
Jeśli w międzyczasie nie zostanie przerwane zadanie, w ciele którego dany wątek
działa, to końcem życia wątku będzie koniec działania metody run(). Do innych
ważnych metod opisujących działanie wątków należy zaliczyć:
static int activeCount():
- wywołanie tej metody powoduje zwrócenie liczby wszystkich aktywnych
wątków danej grupy,
static int enumerate(Thread[] tarray):
- wywołanie tej metody powoduje skopiowanie wszystkich aktywnych wątków
danej grupy do tablicy oraz powoduje zwrócenie liczby wszystkich
skopiowanych wątków,
static void sleep (long ms); gdzie ms to liczba milisekund:
- wywołanie tej metody powoduje uśpienie danego wątku na czas wskazany
przez liczbę milisekund;
static yield():
- wywołanie tej metody powoduje przerwanie wykonywania aktualnego wątku
kosztem wykonywania innego wątku (jeśli taki istnieje) na Maszynie
Wirtualnej. Metodę tą stosowaliśmy omawiając proces GarbageCollection,
zawieszając wątek działania programu na rzecz wywołania wątku zwalniania
pamięci (System.gc()).
Ponadto istnieją jeszcze inne metody klasy Thread, np. setName(), setDaemon(),
setPriority(), getName(), getPriority(), isDaemon(), isAlive(), isInterrupt(), join(), itd.,
które mogą być pomocne w pracy z wątkami. Część z tych metod zostanie poniżej
zaprezentowana.
W celu zobrazowania możliwości generacji wątków w Javie posłużmy się
następującym przykładem:
//Los.java:
import java.util.*;
class Jasnosc extends Thread {
Thread j;
Jasnosc(Thread j){
this.j=j;
}
public void run(){
int n=0;
boolean b = false;
Random r = new Random();
do{
if( !(this.isInterrupted())){
n = r.nextInt();
System.out.println("Jasność");
} else {
n=200000000;
b=true;
}
}while(n<200000000);
if(b){
System.out.println(this+" jestem przerwany, kończę pracę");
} else {
Thread t = Thread.currentThread();
System.out.println("Tu wątek "+t+" Jasność");
System.out.println("Zatrzymuję wątek: "+j);
j.interrupt();
System.out.println("KONIEC: Jasność");
}
}
}// koniec class Jasnosc
class Ciemnosc extends Thread {
Thread c;
public void ustawC(Thread td){
this.c=td;
}
public void run(){
int n=0;
Random r = new Random(12345678L);
boolean b = false;
do{
if( !(this.isInterrupted())){
n = r.nextInt();
System.out.println("Ciemność ");
} else {
n=200000000;
b=true;
}
}while(n<200000000);
if(b){
System.out.println(this+" jestem przerwany, kończę pracę");
} else {
if (c.isAlive()) {
Thread t = Thread.currentThread();
System.out.println("Tu wątek "+t+" Ciemność");
System.out.println("Zatrzymuję wątek: "+c);
c.interrupt();
} else {
Thread t = Thread.currentThread();
System.out.println("Tu wątek "+t+" jestem jedyny ");
}
System.out.println("KONIEC: Ciemność");
}
}
}// koniec class Ciemnosc
public class Los{
public static void main(String args[]){
Ciemnosc zlo = new Ciemnosc();
Jasnosc dobro = new Jasnosc(zlo);
zlo.ustawC(dobro);
zlo.start();
dobro.start();
}
}// koniec public class Los
Przykładowy rezultat działania powyższego programu może być następujący:
Ciemność
Jasność
Ciemność
Jasność
Ciemność
Tu wątek Thread[Thread-1,5,main] Jasność
Ciemność
Zatrzymuję wątek: Thread[Thread-0,5,main]
Ciemność
KONIEC: Jasność
Thread[Thread-0,5,main] jestem przerwany, kończę pracę
W prezentowanym przykładzie stworzono dwie klasy dziedziczące z klasy Thread.
Obie klasy zawierają definicję metody run() konieczną dla pracy danego wątku.
Metody run() zawierają generację liczb pseudolosowych w pętli, przerywanej w
momencie otrzymania wartości progowej (=> 200 000 000). Dodatkowo
wprowadzono w pętli do-while() obu metod warunek sprawdzający czy dany wątek
nie został przerwany poleceniem interrupt(). Jeżeli wygenerowana zostanie liczba
przekraczająca wartość progową to dany wątek zgłasza kilka komunikatów oraz
przerywa pracę drugiego. W programie umieszczono kontrolę działania wątków
(isAlive() - zwraca true, jeżeli dany wątek wykonuje jeszcze działanie zdefiniowane w
jego metodzie run()), gdyż koniec wykonywania metody run() danego wątku jest
równoważne z jego eliminacją. Nie można wówczas otrzymać informacji o
działającym wątku poprzez domyślną konwersję uchwytu obiektu metodą toString()
w ciele instrukcji System.out.println(). Jeśli wątek pracuje wówczas można uzyskać o
nim prostą informację zawierającą dane o jego nazwie (np. Thread-0), priorytecie
(np. 5) oraz o nazwie wątku naczelnego (np. main): np.: Thread[Thread-0,5,main].
Efekt działania powyższego programu może być różny ponieważ wartość zmiennej
sprawdzanej w warunku pętli jest generowana losowo.
Kolejny przykład ukazuje możliwość nadawania nazw poszczególnym wątkom oraz
ustawiania ich priorytetów:
//Widzenie.java:
//Kod klas Ciemnosc i Jasnosc musi byś dostępny dla klasy Widzenie,
//umieszczony np. w tym samym katalogu (ten sam pakiet).
import java.util.*;
public class Widzenie{
public static void main(String args[]){
System.out.println("Maksymalny priorytet wątku = "+Thread.MAX_PRIORITY);
System.out.println("Minimalny priorytet wątku = "+Thread.MIN_PRIORITY);
System.out.println("Normalny priorytet wątku = " + Thread.NORM_PRIORITY+"\n");
Ciemnosc zlo = new Ciemnosc();
Jasnosc dobro = new Jasnosc(zlo);
zlo.setName("zlo");
zlo.setPriority(4);
dobro.setName("dobro");
dobro.setPriority(6);
zlo.ustawC(dobro);
zlo.start();
dobro.start();
}
}// koniec public class Widzenie
Priorytety
W wyniku działania pierwszych trzech instrukcji powyższego programu
wyświetlone są informacje o wartościach priorytetów: maksymalnej (10), minimalnej
(1) i domyślnej (5). Następnie nadano nazwy poszczególnym wątkom, tak więc nie
będzie już występowała nazwa domyślna tj.: Thread-NUMER, lecz ta ustawiona
przez programistę. Wątek o nazwie zlo uzyskał priorytet 4, natomiast wątek o
nazwie dobro uzyskał priorytet 6. Oznacza to, że pierwszeństwo dostępu do
zasobów komputera uzyskał wątek dobro. W rezultacie działania programu
informacje generowane przez wątek zlo mogą się nie pojawić na ekranie monitora.
Domyślna wartość priorytetu, co było zaprezentowane w przykładzie 4.1, wynosi 5.
Priorytety wątków można zmieniać w czasie wykonywania działań.
Priorytety stanowią pewien problem z punktu widzenia uniwersalności kodu w
Javie. Otóż system priorytetów danej platformy (systemu operacyjnego) musi być
odwzorowany na zakres 10 stanów. Przykładowo dla systemu operacyjnego Solaris
liczba priorytetów wynosi 231, podczas gdy dla Windows NT aktualnych priorytetów
jest praktycznie 7. Trudno jest więc ocenić czy ustawiając priorytet w Javie na 9
uzyskamy dla Windows NT priorytet 5, 6 czy może 7. Co więcej poprzez mechanizm
zwany priority boosting Windows NT może zmienić priorytet wątku, który związany
jest z wykonywaniem operacji wejścia/wyjścia. Oznacza to, że nie można
wykorzystywać uniwersalnie priorytetów do sterowania lub inaczej do wyzwalania
wątków. Co może zrobić programista aby zastosować priorytety do sterowania
wątkami? Może ograniczyć się do użycia stałych Thread.MIN_PRIORITY,
Thread.NORM_PRIORITY oraz Thread.MAX_PRIORITY
Przetwarzanie współbieżne a równoległe
Generalnie możliwe są dwa sposoby przetwarzania wątków: współbieżnie lub
równolegle. Przetwarzanie współbieżne oznacza wykonywanie kilku zadań przez
procesor w tym samym czasie poprzez przeplatanie wątków (fragmentów zadań).
Przetwarzanie równoległe natomiast oznacza wykonywanie kilku zadań w tym
samym czasie przez odpowiednią liczbę procesorów równą ilości zadań.
Teoretycznie Java umożliwia jedynie przetwarzanie współbieżne ponieważ wszystkie
wątki są wykonywane w otoczeniu Maszyny Wirtualnej, będącej jednym zadaniem
dla danej platformy. Można oczywiście stworzyć klika kopii Maszyny Wirtualnej dla
każdego procesora, i dla nich uruchamiać wątki (które mogą się komunikować).
Innym rozwiązaniem przetwarzania równoległego w Javie jest odwzorowywanie
wątków Maszyny Wirtualnej na wątki danej platformy (systemu operacyjnego).
Oznacza to oczywiście odejście od uniwersalności kodu.
Sterowanie wątkami (przetwarzanie wątków) związane jest więc z dwoma
istniejącymi modelami: wielozadaniowość kooperacyjna, bez wywłaszczania
(cooperative multitasking) wielozadaniowość z wywłaszczaniem (szeregowania
zadań - preemptive multitasking). Pierwszy z nich polega na tym, że dany wątek tak
długo korzysta ze swoich zasobów (procesora) aż zdecyduje się zakończyć swoją
pracę. Jeżeli dany wątek kończy swoje wykonywanie to uruchamiany jest wątek
(przydzielane są mu zasoby) o najwyższym priorytecie wśród tych, które czekały na
uruchomienie. Taki model sterowania wątkami umożliwia jedynie współbieżność
wątków a wyklucza przetwarzanie równoległe. Równoległe przetwarzanie jest
możliwe jedynie wtedy, gdy obsługa wątków jest wykonana w modelu szeregowania
poszczególnych wątków (do przełączania pomiędzy wątkami). Przykładem systemu
operacyjnego wykorzystującego pierwszy model przełączania pomiędzy wątkami jest
Windows 3.1, natomiast przykładem implementacji drugiego modelu jest Windows
NT. Co ciekawe Solaris umożliwia wykorzystanie obu modeli.
Wątki są odwzorowywane na procesy danej platformy według metody zależnej od
systemu operacyjnego platformy. Dla NT jeden wątek jest odwzorowywany na jeden
proces (odwołanie do jądra systemu - około 600 cykli maszynowych). Dla innych
systemów operacyjnych może odwzorowywanie może wyglądać inaczej.
Przykładowo Solaris wprowadza pojęcie tzw. lightweight process, oznaczające prosty
proces systemu mogący zawierać jeden lub kilka wątków. Oczywiście dany proces
można przypisać do konkretnego procesora (w systemie operacyjnym). Problem
zarządzania wątkami i procesami to oddzielne zagadnienie. W rozdziale tym istotny
jest tylko jeden wniosek dla programisty tworzącego programy w Javie: Sposób
obsługi wątków (priorytety, przełączanie, odwzorowywanie na procesy, itp.) zależy
wyłącznie od Maszyny Wirtualnej, czyli pośrednio od platformy pierwotnej. Praca z
wątkami nie jest więc w pełni niezależna od platformy tak, jak to zakładała pradawna
idea Javy: write once run anywhere.
Przerywanie pracy wątkom
Przerwanie pracy danego wątku może być rozumiane dwojako: albo jako
chwilowe wstrzymanie pracy, lub jako likwidacja wątku. Likwidacja wątku może
teoretycznie odbywać się na trzy sposoby: morderstwo, samobójstwo oraz śmierć
naturalna. W początkowych wersjach JDK (do 1.2) w klasie Thread zdefiniowana
była metoda stop(), która w zależności od wywołania powodowała zabicie lub
samobójstwo wątku. Niestety ponieważ w wyniku wywołania metody stop()
powstawały częste błędy związane z wprowadzeniem bibliotek DLL w środowisku
Windows w stan niestabilny, dlatego metoda stop() uznana została za przestarzałą
(jest wycofywana - deprecated). Likwidacja wątku może więc odbyć się jedynie
poprzez jego naturalną śmierć. Naturalna śmierć wątku wyznaczana jest poprzez
zakończenie działania metody run(). Konieczne jest więc zastosowanie takiej
konstrukcji metody run(), aby można było sterować końcem pracy wątku. Najbardziej
popularne są dwie metody: pierwsza wykorzystuje wiadomość przerwania pracy
wątku interrupt(), druga bada stan przyjętej flagi. Pierwsza metoda została
wykorzystana w przykładzie 4.1. Można jednak uprościć kod programu poprzez
ustawienie jednego warunku, w którym będzie wykonywana treść wątku np.:
while (! Thread.interupted() {
(...)
}
Druga metoda związana jest z podobnym testem flagi. Wartość flagi generowana jest
w wyniku działania wyrażenia. Przykładowo można testować zgodność dwóch
wątków:
while(watekMoj = watekObecny){
(...)
}
wówczas można wywołać warunek końca używając metody:
public void stop () {
watekObecny= null;
}
lub można testować wartość zwracaną przez funkcję:
while(fun(arg1,arg2));
gdzie
public boolean fun(int arg1, int arg2){
{ (...)
return true;
}
{(...)
rretur false;
}
}
itp. Niestety nie zawsze można zakończyć pracę wątku poprzez odpowiednie ustawienie
flagi. Dlaczego, otóż dlatego, że dany wątek może być blokowany ze względu na
dostęp do danych, które są chwilowo niedostępne (zablokował je inny wątek).
Wówczas możliwość testowania flagi pojawia się dopiero po odblokowaniu wątku (co
może bardzo długo trwać). Dlatego pewną metodą zakończenia pracy wątku jest
wywołanie metody interrupt(), która wygeneruje wyjątek dla danego wątku.
Odpowiednia obsługa wyjątku może doprowadzić do zakończenia działania metody
run, a więc i wątku. Należy jednak pamiętać o tym, że generacja wyjątku może
spowodować taki stan pól obiektu lub klasy, który spowoduje niewłaściwe działanie
programu. Jeśli takie zjawisko może wystąpić dla tworzonej aplikacji wówczas należy
ustawić odpowiednie warunki kontrolne w obsłudze wyjątku, przed zakończeniem
pracy wątku. Przykładowa obsługa zakończenia pracy wątku może wyglądać
następująco:
public void run(){
try{
while(!(this.isInterupted)){
/*wyrażenia + insturkcje generujące wyjątek IE , np. sleep(1)*/
}
}catch (InterruptedException ie){
}// iinstrukcja pusta → przejście do końca metody
}
Przerywanie tymczasowe
Możliwe jest również tymczasowe zawieszenie pracy wątku czyli wprowadzenie
go w stan Not Runnable. Możliwe jest to na trzy sposoby:
- wywołanie metody sleep();
- wywołanie metody wait() w celu oczekiwania na spełnienie określonego warunku;
- blokowanie wątku przez operację wejścia/wyjścia (aż do jej zakończenia).
W pierwszym przypadku stosuje się jedną z metod sleep() zdefiniowaną w klasie
Thread. Metoda sleep() umożliwia wprowadzenie w stan wstrzymania pracy wątku na
określony czas podawany jako liczba milisekund stanowiąca argument wywołania
metody. W czasie zaśnięcia wątku może pojawić się wiadomość przerywająca pracę
wątku (interrupt()) dlatego konieczna jest obsługa wyjątku InterruptedException.
Korzystanie z metody wait() zdefiniowanej w klasie Object polega na wstrzymaniu
wykonywania danego wątku aż do pojawienia się wiadomości notify() lub notifyAll()
wskazującej na dokonanie zmiany, na którą czekał wątek. Te trzy metody są
zdefiniowane w klasie Object i są dziedziczone przez wszystkie obiekty w Javie.
Należy tutaj wskazać na istotną różnicę pomiędzy dwoma pierwszymi sposobami
tymczasowego wstrzymywania pracy wątku. Otóż wywołanie metody sleep() nie
powoduje utraty praw (blokady) do danych (wątek dalej blokuje monitor - o czym
dalej w tym rozdziale) związanych z danym wątkiem. Oznacza to, że inny wątek,
który z tych danych chce skorzystać (o ile były zablokowane) nie może tego uczynić i
będzie czekał na zwolnienie blokady. To, czy dany kod (zmienne) są blokowane czy
nie określa instrukcja sychronized, która jest opisana szerzej w dalszej części tego
rozdziału. Dla odróżnienia wywołanie metody wait() odblokowuje dane, i czeka na
kolejne przejęcie tych danych (zablokowanie) po wykonaniu pewnego działania przez
inne wątki.
Rozważmy następujący przykład obrazujący możliwość zatrzymywania oraz
tymczasowego wstrzymywania (sleep) pracy wątku.
//Walka.java:
import java.util.*;
class Imperium extends Thread {
private String glos;
Imperium(String s){
this.glos=s;
}
public void set(String s){
this.glos=s;
}
public boolean imperator(){
String mowca = Thread.currentThread().getName();
if (mowca.equals(glos)){
System.out.println("Mówi Imperator !");
return true;
}
false;
}// koniec public boolean imperator()
public void run(){
while(imperator());
System.out.println("Koniec pracy Imperium");
}
}// koniec class Imperium
class Republika extends Thread {
private String glos;
Republika(String s){
this.glos=s;
}
public void set(String s){
this.glos=s;
}
public boolean senat(){
String mowca = Thread.currentThread().getName();
System.out.println("Mówi Senat !");
return true;
}
return false;
}// koniec public boolean senat()
public void run(){
while(senat());
System.out.println("Koniec pracy Senatu");
}
}// koniec class Republika
class RadaJedi extends Thread {
private String glos;
private Imperium imp;
private Republika rp;
RadaJedi(String s, Imperium i, Republika r){
this.glos=s;
this.imp=i;
this.rp=r;
}
public boolean rada(){
String mowca = Thread.currentThread().getName();
if (mowca.equals(glos)){
System.out.println("Zamach stanu - Rada Jedi u władzy Senat !");
imp.set(glos);
rp.set(glos);
try{
sleep(500);
} catch (InterruptedException ie){
}
return false;
}
return true;
}// koniec public boolean imperator()
public void run(){
while(rada());
System.out.println("Koniec pracy Rady");
}
}// koniec class RadaJedi
public class Walka{
public static void main(String args[]){
Imperium im = new Imperium("Imperator");
Republika rep = new Republika("Senat");
RadaJedi rj = new RadaJedi("Rada",im,rep);
im.setName("Imperator");
rep.setName("Senat");
rj.setName("Rada");
im.start();
rep.start();
try{
Thread.currentThread().sleep(6000);
} catch (InterruptedException ie){
}
rj.start();
}
}// koniec public class Walka
Powyższy przykład ukazuje metodę zakończenia pracy wątku w oparciu o jego
śmierć naturalną spowodowaną końcem działania metody run(). Trzy klasy wątków
Imperium, Republika i RadaJedi wykorzystują ten sam mechanizm zakończenia
pracy metody run() poprzez ustawienie pola glos na wartość inną niż nazwa
aktualnego wątku. W metodzie głównej wprowadzono również tymczasowe
wstrzymanie wykonywania wątku głównego (Thread[main,5,main]) na okres 6
sekund, celem ukazania pracy pozostałych wątków. Po okresie 6 sekund
uruchomiony zostaje wątek o nazwie Rada powodujący zakończenie pracy
wszystkich stworzonych wątków przez wątek główny.
Istnieją również inne metody tymczasowego wstrzymania pracy wątku. Pierwsza z
nich powoduje chwilowe wstrzymanie aktualnie wykonywanego wątku umożliwiając
innym wątkom podjęcie pracy. Metoda ta opiera się o wykorzystanie poznanej już
statycznej metody yield(). Inne przypadki wstrzymywania pracy wątku mogą być
związane z blokowaniem wątku ze względu na brak dostępu do zablokowanych
danych. Wątek rozpocznie dalszą pracę dopiero wtedy, gdy potrzebne dane będą dla
niego dostępne. Ciekawą sytuacją może być również wstrzymanie pracy wątku priorytecie.
Ponieważ uruchamianie wątku jest czasochłonne dla Maszyny Wirtualnej, często
stosuje się metodę zwaną jako pull threads. Wyciąganie wątków polega na tym, że
w czasie rozpoczęcia pracy programu tworzone są liczne wątki, które następnie są
wprowadzane w stan czuwania i umieszczane są w pamięci (na stosie). Program w
czasie pracy jeżeli potrzebuje wątku nie tworzy go, lecz pobiera z pamięci i budzi do
pracy. Rozwiązanie takie jest szczególnie efektywne przy tworzeniu różnego rodzaju
serwerów, kiedy czas uruchamiania programu nie jest tak istotny co czas
wykonywania poleceń podczas realizacji programu. Przykładowym serwerem gdzie
takie rozwiązanie może być wykorzystanie jest serwer WWW, w którym każde
połączenie jest oddzielnym wątkiem.
Poniższy przykład ukazuje efekt działania metod wait() i notify() w celu
tymczasowego wstrzymywania wątku i wraz ze zwolnieniem blokowanych danych w
celu ich wczytania przez wątek konkurujący.
// Wojna.java:
class Strzal {
private String strzelec = "nikt";
private String tratata[]={ "PIF","PAF"};
private int i=0;
public synchronized boolean strzal(String wrog) {
String kto = Thread.currentThread().getName();
if (strzelec.compareTo("POKOJ") == 0)
return false;
if (wrog.compareTo("POKOJ") == 0) {
strzelec = wrog;
notifyAll();
return false;
}
if (strzelec.equals("nikt")) {
strzelec = kto;
return true;
}
if (kto.compareTo(strzelec) == 0) {
System.out.println(tratata[i]+"! ("+strzelec+")");
strzelec = wrog;
i=1-i;
notifyAll();
} else {
try {
long zwloka = System.currentTimeMillis();
wait(200);
if ((System.currentTimeMillis() - zwloka) > 200) {
System.out.println("Tu "+kto+", czekam na ruch osobnika:"+ strzelec);
}
} catch (InterruptedException ie) {
}
}
return true;
}
}//koniec class Strzal
class Strzelec implements Runnable {
Strzal s;
String wrogPubliczny;
public Strzelec(String wrog, Strzal st) {
s = st;
wrogPubliczny = wrog;
}
public void run() {
while (s.strzal(wrogPubliczny)) ;
}
}// koniec class Strzelec
public class Wojna {
public static void main(String args[]) {
Strzal st = new Strzal();
Thread luke = new Thread(new Strzelec("Vader", st));
Thread vader = new Thread(new Strzelec("Luke",st));
luke.setName("Luke");
vader.setName("Vader");
luke.start();
vader.start();
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException ie) {
}
st.strzal("POKOJ");
System.out.println("Nastał pokój!!!");
}
}// koniec public class Wojna
Przykładowy program skonstruowano z trzech klas. Pierwsza z nich jest
odpowiedzialna za wysyłanie komunikatów w zależności od podanej nazwy zmiennej.
Jeżeli nazwa strzelca jest inna niż bieżąca (czyli inna niż tego do kogo należy ruch)
to następuje oczekiwanie na nowego strzelca. Druga klasa tworzy definicję metody
run() konieczną dla opisania działania wątków. Ostatnia główna klasa programu
tworzy obiekt klasy opisującej proces strzelania (klasy pierwszej) a następnie
uruchamia dwa wątki podając nazwy wrogich strzelców względem danego wątku.
Strzelanie trwa jedną sekundę po czym następuje POKOJ, czyli przerwanie pracy
wątków poprzez zakończenie pracy metod run() (w pętli while() pojawia się wartość
logiczna false). Efektem działania programu jest następujący wydruk:
PIF! (Luke)
PAF! (Vader)
PIF! (Luke)
PAF! (Vader)
PIF! (Luke)
PAF! (Vader)
PIF! (Luke)
PAF! (Vader)
PIF! (Luke)
PAF! (Vader)
PIF! (Luke)
PAF! (Vader)
PIF! (Luke)
PAF! (Vader)
PIF! (Luke)
PAF! (Vader)
PIF! (Luke)
PAF! (Vader)
PIF! (Luke)
PAF! (Vader)
PIF! (Luke)
PAF! (Vader)
PIF! (Luke)
PAF! (Vader)
PIF! (Luke)
PAF! (Vader)
PIF! (Luke)
PAF! (Vader)
PIF! (Luke)
PAF! (Vader)
Nastał pokój!!!
Warto wspomnieć coś na temat stanów wątku. Do podstawowych należy zaliczyć te
opisywane przez metody isAlive() oraz isInterrupted(). Metoda isAlive() zwraca
wartość logiczną true jeżeli dany wątek został uruchomiony (start()) i nie został
jeszcze zakończony (nie umarł). Metoda isAlive() nie dostarcza informacji o tym, czy
wykonywanie danego wątku jest zawieszone tymczasowo czy nie (brak rozróżnienia
pomiędzy wątkiem Runnable i Not Runnable). Metoda isInterrupted() zwraca wartość
true jeżeli dla danego wątku została przesłana wiadomość (wykonano metodę)
interrupt().
Jeśli dwa wątki konkurują w celu uzyskania praw dostępu do danego obiektu
to zachowanie kodu zależeć może od tego, który wątek wygra. Zjawisko te (race
condition - wyścig) jest najczęściej bardzo niekorzystne ze względu na brak kontroli
(nieokreśloność) w zapewnianiu kolejności obsługi wiadomości generowanych przez
różne wątki do tego samego obiektu.
Załóżmy, że istnieje następująca klasa:
class Test {
int n = 0;
int m = 1;
void zamianaLP() {
n=m
}
vois zamiana PL () {
m=n;
}
}//koniec class Test
oraz stworzone są dwa wątki, jeden wykonuje metodę zamianaLP(), drugi wykonuje
metodę zamianaPL(). O ile oba wątki są równouprawnione i wykonywane są
równolegle, to problem polega na określeniu kolejności działania, czyli określeniu
jakie wartości końcowe przyjmą pola n i m. obiektu klasy Test. Możliwe są tu różne
sytuacje. Każda z dwóch metod wykonuje dla swoich potrzeb aktualne kopie robocze
zmiennych. Następnie po wykonaniu zadania wartości tych zmiennych są
przypisywane do zmiennych oryginalnych przechowywanych w pamięci głównej. Jak
łatwo się domyśleć możliwe są więc następujące stany końcowe zmiennych:
- n=0,m=0; wartość zmiennej n została przepisana do m;
- n=1, m=1; wartość zmiennej m została przepisana do n;
- n=1, m=0; wartości zmiennych zostały zamienione.
W większości przypadków (poza generatorem pseudolosowym) zjawisko wyścigu
jest niekorzystne. Konieczne jest więc zastosowanie takiej konstrukcji kodu aby
można było to zjawisko wyeliminować. Zanim jednak zaprezentowane zostanie
rozwiązanie problemu warto przeanalizować konkretny przykład.
//WyscigiNN.java:
public class WyscigiNN implements Runnable{
private int n;
WyscigiNN(int nr){
this.n=nr;
}
void wyswietl(int i){
System.out.println("Dobro = "+i);
System.out.println("Zlo = "+i);
}
void zmianaZla(){
System.out.print("ZLO: ");
for(int l=0; l<10;l++,++n){
System.out.print(Thread.currentThread().getName()+": "+n+" ");
}
System.out.println(" ");
}
void zmianaDobra(){
System.out.print("DOBRO: ");
for(int l=0; l<20;l++,--n){
System.out.print(Thread.currentThread().getName()+": "+n+" ");
}
System.out.println(" ");
}
public void run(){
while (!(Thread.interrupted())){
if ( (Thread.currentThread().getName()).equals("T1")){
zmianaZla();
} else
zmianaDobra();
}
}
public static void main(String args[]){
WyscigiNN w1= new WyscigiNN(100);
Thread t1,t2;
t1= new Thread(w1);
t2=new Thread(w1);
t1.setName("T1");
t2.setName("T2");
t2.start();
t1.start();
try{
Thread.currentThread().sleep(600);
} catch (InterruptedException ie){
}t1.interrupt();
t2.interrupt();
try{
Thread.currentThread().sleep(2000);
} catch (InterruptedException ie){
}
System.out.println("\n");
w1.wyswietl(w1.n);
}
}// koniec public class WyscigiNN
Powyższy program umożliwia obserwację zjawiska wyścigu. Klasa główna aplikacji
implementuje interfejs Runnable w celu definicji metody run() koniecznej dla pracy
wątków. W ciele klasy głównej zadeklarowano jedną zmienną prywatną typu int oraz
pięć metod. Pierwsza metod wyswietl() umożliwia wydruk wartości pola obiektu
(zmiennej n). Druga i trzecia metoda, a więc zmianaZla() i zmianaDobra() wykonują
wydruk modyfikowanej wartości pola obiektu (n). Każda drukowana wartość
poprzedzana jest nazwa wątku (T1 lub T2), który wywołał daną metodę. W
metodzie run() w pętli warunkowej określającej koniec działania wątku (przerwanie -
interrupt()) wywoływana jest albo metoda zmianaZla() albo zmianaDobra() w
zależności od nazwy aktualnego wątku. Metoda statyczna aplikacji zawiera inicjację
obiektu klasy głównej, inicjację obiektów wątków (inicjacja nie jest równoważna z
rozpoczęciem pracy wątku; wątek nie jest obiektem), nadanie im nazw i ich
uruchomienie. W celu obserwacji uruchomionych wątków wstrzymuje się
wykonywanie wątku głównego (main) na okres 600 milisekund (sleep(600)).
Następnie wysyłane są wiadomości przerwania pracy wątków, co powoduje zakończenie działania metod run(). Kolejne uśpienie wątku głównego ma na celu
wypracowanie czasu na zakończenie pracy przerywanych wątków zanim
wyświetlone zostaną ostateczne rezultaty, czyli wartość pola obiektu (n). W wyniku
działania programu można uzyskać następujący wydruk:
ZLO: DOBRO: T1: 100 T2: 100 T1: 101 T2: 100 T1: 101 T2: 100 T1: 100 T1: 101 T1: 102 T1: 103 T1: 104 T1: 105 T1: 106
T2: 107 ZLO: T1: 106 T1: 107 T1: 108 T1: 109 T1: 110 T1: 111 T2: 106 T1: 112 T2: 111 T1: 112 T2: 111 T1: 112 T2: 111
T1: 112 T2: 111
T2: 111 T2: 110 ZLO: T2: 109 T2: 108 T2: 107 T2: 106 T2: 105 T2: 104 T2: 103 T2: 102 T2: 101
DOBRO: T2: 100 T2: 99 T2: 98 T2: 97 T2: 96 T2: 95 T2: 94 T2: 93 T2: 92 T2: 91 T2: 90 T2: 89 T2: 88 T2: 87 T2: 86 T2: 85
T2: 84 T2: 83 T2: 82 T2: 81
DOBRO: T2: 80 T2: 79 T2: 78 T2: 77 T2: 76 T2: 75 T2: 74 T1: 73 T1: 74 T1: 75 T2: 75 T1: 76 T2: 75 T1: 76 T1: 76 T1: 77
T1: 78 T1: 79 T1: 80
ZLO: T1: 81 T1: 82 T1: 83 T1: 84 T1: 85 T1: 86 T1: 87 T1: 88 T1: 89 T1: 90
ZLO: T1: 91 T1: 92 T1: 93 T1: 94 T1: 95 T1: 96 T1: 97 T2: 75 T2: 96 T2: 95 T2: 94 T2: 93 T2: 92 T2: 91 T2: 90 T2: 89 T2:
88 T2: 87
DOBRO: T2: 86 T2: 85 T2: 84 T2: 83 T2: 82 T2: 81 T2: 80 T2: 79 T2: 78 T2: 77 T2: 76 T2: 75 T2: 74 T2: 73 T2: 72 T2: 71
T2: 70 T2: 69 T2: 68 T2: 67
DOBRO: T2: 66 T2: 65 T2: 64 T2: 63 T2: 62 T2: 61 T2: 60 T2: 59 T2: 58 T2: 57 T2: 56 T1: 57 T2: 56 T1: 57 T2: 56 T1: 57
T2: 56
T2: 56 T2: 55 T2: 54 T2: 53 T2: 52 T2: 51
Dobro = 50
Zlo = 50
Łatwo zauważyć, że otrzymano nieregularne przeplatanie wątków T1 i T2,
powodujące różne operację na polu obiektu. Ponieważ T1 i T2 są równomiernie
uprawnione do dostępu do pola, uzyskano różne wartości (nieregularne - co
uwypuklono na wydruku podkreśleniami tekstu) tego pola w czasie działania nawet
tej samej pętli for metody zmianaZla() lub zamianaDobra(). Oznacza to sytuację, gdy
jeden wątek korzysta z pola, które jest właśnie zmienione przez inny wątek. Ta
sytuacja jest właśnie określana mianem race condition.
Konieczny jest więc tutaj mechanizm zabezpieczenia danych, w ten sposób, że jeżeli
jeden wątek korzysta z nich to inne nie mogą z nich korzystać. Najprostszym
rozwiązaniem byłoby zablokowanie fragmentu kodu, z którego korzysta dany wątek
tak, że inne wątki muszą czekać tak długo, aż wątek odblokuje kod. Chroniony region
kodu jest często nazywany monitorem (obowiązujące pojęcie w Javie). Monitor jest
chroniony poprzez wzajemnie wykluczające się semafory. Oznacza to, że stworzenie
monitora oraz ustawienie jego semafora (zamknięcie monitora) powoduje sytuację
taką, że żaden wątek nie może z tego monitora korzystać. Jeżeli zmieni się stan
semafora (zwolnienie danych) wówczas inny wątek może ten monitor sobie
przywiązać (ustawić odpowiednio semafor tego wątku). W Javie blokada monitora
odbywa się poprzez uruchomienie metody lub kodu oznaczonej jako synchronized.
Jeżeli występuje dla danego wątku kod oznaczony jako synchronized, to kod ten
staje się kodem chronionym i jego uruchomienie jest równoważne z ustanowieniem
blokady na tym kodzie (o ile monitor ten nie jest już blokowany). Słowo kluczowe
synchronized stosuje się jako oznaczenie metody (specyfikator) lub jako instrukcja.
Przykładowo jeżeli metoda zmiana() przynależy do danego obiektu wówczas zapis:
synchronized void zmiana () {
/*wyrażenia*/
}
jest równoważny praktycznie zapisowi:
vvoid zmiana () {
synchronised (thhis){
/*wyrażenia*/
}
}
Pierwszy zapis oznacza metodę synchronizowaną a drugi instrukcję synchronizującą.
Blokada zakładana dla kodu oznaczonego poprzez synchronized, powoduje najpierw
obliczenie odwołania (uchwytu) do danego obiektu (this) i założenie blokady.
Wówczas żaden inny wątek nie będzie miał dostępu do monitora danego obiektu.
Inaczej ujmując, żaden inny wątek nie może wykonać metody oznaczonej jako
synchronized. Jeżeli zakończone zostanie działanie tak oznaczonej metody wówczas
blokada jest zwalniana. Niestety stosowanie specyfikatora synchronized powoduje
zwolnienie pracy metody nieraz i dziesięciokrotnie. Dlatego warto stosować instrukcję
synchronized obejmując blokiem tylko to, co jest niezbędne.
W Javie (podobnie jak w innych językach programowania współbieżnego) możliwe są
dwa typy obszarów działania wątku: dane dynamiczne - obiekt oraz dane statyczne.
Obiekt, a więc konkretne i jednostkowe wystąpienie danej klasy jest opisywany
poprzez pola i metody (w programowaniu obiektowym często nazywane z j.
angielskiego jako: instance variables {fields, methods}). Klasa może być również
opisywana poprzez pola i metody niezmienne (statyczne - static) dla dowolnego
obiektu tej klasy (w języku angielskim elementy te nazywane są czasem class
variables{fields, methods}). Pola i metody statyczne opisują więc stan wszystkich
obiektów danej klasy, podczas gdy pola i metody obiektu opisują stan danego
obiektu. Dualizm ten ma również swoje następstwa w sposobie korzystania z
omawianych elementów przez wątki. Omawiany do tej pory monitor jest związany z
obiektem danej klasy. Fragment kodu oznaczony przez słowo kluczowe synchronized
(np. metoda) może być wykonywana równocześnie przez różne wątki, lecz pod
warunkiem, że związane z nimi obiekty otrzymujące dane wiadomości są różne.
Oznacza to, że w czasie wykonywania metody oznaczonej jako synchronized
blokowany jest monitor danego obiektu, tak że inne wątki nie mają do niego dostępu.
Poza monitorem spotyka się w literaturze [1][2][3] pojęcie kodu krytycznego - critical
section. W [3] kod krytyczny jest definiowany jako ten, w którym wykonywany jest
dostęp do tego samego obiektu z kilku różnych wątków. Kod taki, bez względu na to
czy dotyczy klasy (static) czy obiektu oznaczany może być poprzez blok ze słowem
kluczowym synchronized. W [2] ukazano jednak ścisły rozdział pomiędzy pojęciem
critical section a monitorem: kod krytyczny to ten związany z klasą (a więc z kodem
statycznym), monitor natomiast jest związany z obiektem danej klasy. Oznacza to, że
kod krytyczny to taki kod, który może być wykonywany tylko poprzez jeden wątek w
czasie! Nie możliwe jest bowiem stworzenie wielu obiektów z jednostkowymi
metodami typu static (każdy obiekt widzi to samo pole lub metodę typu static).
Oczywiście druga interpretacja jest słuszna. Dlaczego? Otóż jak już wiadomo metody
i pola statyczne danej klasy należą do obiektu klasy Class związanego z daną
metodą. Obiekt ten posiada również swój monitor, gdzie blokowanie następuje
poprzez wywołanie instrukcji synchronized static. Maszyna Wirtualna Javy
implementuje blokowanie monitora poprzez swoją instrukcję monitorenter, natomiast
zwalnia blokadę poprzez wywołanie instrukcji monitorexit. Niestety monitor obiektu
klasy Class nie jest związany z monitorami obiektów jednostkowych danej klasy.
Oznacza to, że metoda obiektu oznaczona jako synchronized (blokowany jest więc
monitor - dostęp do obiektu ) ma dostęp do pól statycznych (pola te nie należą
bowiem do obiektu, czyli nie są blokowane). Rozpatrzmy następujący fragment kodu:
//KolorMiecza.java
class KolorMiecza{
private static Color k = Color.red
synchronized public ustawKolor(Color kolor){
this.k=kolor;
}
}// koniec class KolorMiecza
Jeżeli dwa wątki wywołają równocześnie metody ustawKolor() dla dwóch różnych
obiektów klasy KolorMiecza to nie wiadomo jaką wartość będzie przechowywało pole
klasy k. Występuje więc tutaj wyścig race condition czyli dwa wątki modyfikują
(rywalizują o) tę samą zmienną. Oznacza to, że zablokowanie obiektu klasy
KolorMiecza nie oznacza zablokowania odpowiadającemu mu obiektu klasy Class,
czyli pola statyczne nie są wówczas blokowane. Dlatego bardzo ważne jest
rozróżnienie pomiędzy monitorem (kodem obiektu) a kodem krytycznym (kodem
klasy).
Na zakończenie omawiania problemu wykorzystywania instrukcji synchronized warto
zmodyfikować prezentowany wcześniej przykład WyscigiNN.java dodając do metod
zmianaZla() oraz zmianaDobra() instrukcje lub specyfikator synchronized.
// WyscigiSN.java:
public class WyscigiSN implements Runnable{
private int n;
WyscigiSN(int nr){
this.n=nr;
}
void wyswietl(int i){
System.out.println("Dobro = "+i);
System.out.println("Zlo = "+i);
}
void zmianaZla(){
synchronized (this){
System.out.print("ZLO: ");
for(int l=0; l<10;l++,++n){
System.out.print(Thread.currentThread().getName()+": "+n+" ");
}
System.out.println(" ");
}
void zmianaDobra(){
synchronized (this){
System.out.print("DOBRO: ");
for(int l=0; l<20;l++,--n){
System.out.print(Thread.currentThread().getName()+": "+n+" ");
}
System.out.println(" ");
}
}
public void run(){
while (!(Thread.interrupted())){
if ( (Thread.currentThread().getName()).equals("T1")){
zmianaZla();
} else
zmianaDobra();
}
}
public static void main(String args[]){
WyscigiSN w1= new WyscigiSN(100);
Thread t1,t2;
t1= new Thread(w1);
t2=new Thread(w1);
t1.setName("T1");
t2.setName("T2");
t2.start();
t1.start();
try{
Thread.currentThread().sleep(600);
} catch (InterruptedException ie){
}t
1.interrupt();
t2.interrupt();
try{
Thread.currentThread().sleep(2000);
} catch (InterruptedException ie){
}
System.out.println("\n");
w1.wyswietl(w1.n);
}
}// koniec public class WyscigiSN
Rezultat działanie powyższego programu może być następujący:
ZLO: T1: 100 T1: 101 T1: 102 T1: 103 T1: 104 T1: 105 T1: 106 T1: 107 T1: 108 T1: 109
DOBRO: T2: 110 T2: 109 T2: 108 T2: 107 T2: 106 T2: 105 T2: 104 T2: 103 T2: 102 T2: 101 T2: 100 T2: 99 T2: 98 T2: 97
T2: 96 T2: 95 T2: 94 T2: 93 T2: 92 T2: 91
ZLO: T1: 90 T1: 91 T1: 92 T1: 93 T1: 94 T1: 95 T1: 96 T1: 97 T1: 98 T1: 99
DOBRO: T2: 100 T2: 99 T2: 98 T2: 97 T2: 96 T2: 95 T2: 94 T2: 93 T2: 92 T2: 91 T2: 90 T2: 89 T2: 88 T2: 87 T2: 86 T2: 85
T2: 84 T2: 83 T2: 82 T2: 81
ZLO: T1: 80 T1: 81 T1: 82 T1: 83 T1: 84 T1: 85 T1: 86 T1: 87 T1: 88 T1: 89
DOBRO: T2: 90 T2: 89 T2: 88 T2: 87 T2: 86 T2: 85 T2: 84 T2: 83 T2: 82 T2: 81 T2: 80 T2: 79 T2: 78 T2: 77 T2: 76 T2: 75
T2: 74 T2: 73 T2: 72 T2: 71
ZLO: T1: 70 T1: 71 T1: 72 T1: 73 T1: 74 T1: 75 T1: 76 T1: 77 T1: 78 T1: 79
DOBRO: T2: 80 T2: 79 T2: 78 T2: 77 T2: 76 T2: 75 T2: 74 T2: 73 T2: 72 T2: 71 T2: 70 T2: 69 T2: 68 T2: 67 T2: 66 T2: 65
T2: 64 T2: 63 T2: 62 T2: 61
ZLO: T1: 60 T1: 61 T1: 62 T1: 63 T1: 64 T1: 65 T1: 66 T1: 67 T1: 68 T1: 69
DOBRO: T2: 70 T2: 69 T2: 68 T2: 67 T2: 66 T2: 65 T2: 64 T2: 63 T2: 62 T2: 61 T2: 60 T2: 59 T2: 58 T2: 57 T2: 56 T2: 55
T2: 54 T2: 53 T2: 52 T2: 51
ZLO: T1: 50 T1: 51 T1: 52 T1: 53 T1: 54 T1: 55 T1: 56 T1: 57 T1: 58 T1: 59
Dobro = 60
Zlo = 60
Rezultat jasno przedstawia, że poszczególne metody są wykonywane sekwencyjnie,
co oznacza blokowanie dostępu do metody (pola) przez wątek, który z niej korzysta.
W wydruku wyraźnie można zaobserwować kolejne realizacje pętli for ujętych w
metodach zmianaZla() i zmianaDobra().
Należy na koniec dodać, że możliwa jest sytuacja taka, że wszystkie istniejące wątki
będą się wzajemnie blokować. Wówczas żaden kod nie jest wykonywany i powstaje
tzw. impas, zakleszczenie (deadlock). Java nie dostarcza specjalnych mechanizmów
detekcji impasu. Programista musi niestety przewidzieć ewentualną możliwość
wystąpienia totalnej blokady, i temu zaradzić modyfikując kod.
Grupy wątków ThreadGroup
Poszczególne wątki można z sobą powiązać poprzez tworzenie grup wątków.
Grupy wątków są przydatne ze względu na sposób organizacji pracy programu, a co
za tym idzie możliwością sterowania prawami wątków. Przykładowo inne powinny
być uprawnienia zawiązane z wątkami apletu niż te, związane z aplikacjami.
Grupowanie wątków możliwe jest dzięki zastosowaniu klasy ThreadGroup.
Stworzenie obiektu tej klasy umożliwia zgrupowanie nowo tworzonych wątków
poprzez odwołanie się do obiektu ThreadGroup w konstruktorze każdego z wątków,
np. Thread(ThreadGroup tg, String nazwa). Ponieważ w Javie wszystkie obiekty
mają jedno główne źródło, również i dla obiektów typu ThreadGroup można
przeprowadzić analizę hierarchii obiektów. Dodatkowo każdy obiekt klasy
ThreadGroup może być jawnie stworzony jako potomek określonej grupy wątków, np.
poprzez wywołanie konstruktora ThreadGroup(ThreadGroup rodzic, String nazwa).
Maszyna Wirtualna Javy pracuje więc ze zbiorem zorganizowanymi w grupy wątków.
Poniższa aplikacja ukazuje wszystkie pracujące wątki na danej platformie Javy:
//Duchy.java
public class Duchy {
public static void main(String args[]) {
ThreadGroup grupa = Thread.currentThread().getThreadGroup();
while(grupa.getParent() != null) {
grupa = grupa.getParent();
}
Thread[] watki = new Thread[grupa.activeCount()];
grupa.enumerate(watki);
for (int k = 0; k < watki.length; k++) {
System.out.println(watki[k]);
}
}
}// koniec public class Duchy
W powyższym przykładzie zastosowano pętlę while, w ciele której poszukiwany jest
obiekt klasy ThreadGroup będący na korzeniem w drzewie wszystkich grup wątków.
Następnie dla tak określonego obiektu tworzona jest tablica obiektów klasy Thread o
rozmiarze wynikającym z liczby aktywnych wątków zwracanych metodą
activeCount(). W kolejnym kroku za pomocą metody enumerate() inicjowana jest
tablica obiektami wątków podstawowej grupy wątków. Prezentacja otrzymanych
wyników wykonana jest poprzez znaną już konwersję wątków na tekst. W rezultacie
można uzyskać następujący wynik:
Thread[Signal dispatcher,5,system]
Thread[Reference Handler,10,system]
Thread[Finalizer,8,system]
Thread[main,5,main]
Metody klasy ThreadGroup, oprócz tych już poznanych (activeCount(), enumerate(),
getParent()) są podobne do tych zdefiniowanych dla klasy Thread, lecz dotyczą
obiektów klasy ThreadGroup, np. getName() zwraca nazwę grupy; interrupt()
przerywa wszystkie wątki w grupie, itp. Ciekawą metodą jest metoda list() wysyłająca
na standardowe urządzenie wyjścia (np. ekran) informacje o danej grupie.
Zastosowanie tej metody w powyższym przykładzie znacznie skraca kod źródłowy:
//Duchy1.java
public class Duchy1 {
public static void main(String args[]) {
ThreadGroup grupa = Thread.currentThread().getThreadGroup();
while(grupa.getParent() != null) {
grupa = grupa.getParent();
}
grupa.list();
}
}// koniec public class Duchy1
W rezultacie działania tej metody można uzyskać następujący wydruk na ekranie:
java.lang.ThreadGroup[name=system,maxpri=10]
Thread[Signal dispatcher,5,system]
Thread[Reference Handler,10,system]
Thread[Finalizer,8,system]
java.lang.ThreadGroup[name=main,maxpri=10]
Thread[main,5,main]
Z wydruku tego jasno widać, że na danej platformie Javy aktywne są dwie grupy
wątków: główna o nazwie system oraz potomna o nazwie main. W grupie głównej
znajduje się między innymi wątek o nazwie Finalizer, związany z wykonywaniem
zadań w czasie niszczenia obiektów. Jak wspomniano wcześniej w tym materiale
zwalnianiem pamięci w Javie zajmuje się wątek GarbageCollection. Programista
praktycznie nie ma na niego wpływu. Problem jednak polega na tym, że czasami
konieczne jest wykonanie pewnych działań w czasie niszczenia obiektu, innych niż
zwalnianie pamięci (np. zamknięcie strumienia do pliku). Obsługą takich zleceń
zajmuje się wątek Finalizer (wykonywanie kodu zawartego w metodach finalize()).
Ciekawostką może być inny sposób uzyskania raportu o działających wątkach w
systemie. Otóż w czasie działania programu w Javie należy w konsoli przywołać
instrukcję przerwania: dla Windows CTRL-BREAK, dla Solaris kill QUIT (dla
procesu Javy). Po instrukcji przerwania następuje wydruk stanu wątków i oczywiście
przerwanie programu Javy.
Demony
Wszystkie wątki stworzone przez użytkownika giną w momencie zakończenia
wątku naczelnego (zadania). Jeżeli programista pragnie stworzyć z danego wątku
niezależny proces działający w tle (demon) wówczas musi dla obiektu danego wątku
podać jawnie polecenie deklaracji demona: setDaemon(true). Metoda ta musi być
wywołana zanim rozpoczęte zostanie działanie wątku poprzez zastosowanie metody
start(). Poprzez odwołanie się do uchwytu wątku można sprawdzić czy jest on
demonem czy nie. Do tego celu służy metoda isDaemon() zwracająca wartość true
jeżeli dany wątek jest demonem. Rozważmy następujący przykład:
//Demony.java
public class Demony extends Thread{
public void run(){
while(!Thread.interrupted()){
}
}
public static void main(String args[]) {
Demony d = new Demony();
d.setName(DEMON);
d.setDaemon(true);
d.start();
ThreadGroup grupa = Thread.currentThread().getThreadGroup();
while(grupa.getParent() != null) {
grupa = grupa.getParent();
}
Thread[] watki = new Thread[grupa.activeCount()];
grupa.enumerate(watki);
for (int k = 0; k < watki.length; k++) {
if (watki[k].isDaemon()) System.out.println(Demon: +watki[k]);
}
d.interrupt();
}
}// koniec public class Demony
Powyższy program jest przerobioną wersją aplikacji Duchy.java tworzącą nowego
demona w systemie oraz drukującą tylko te wątki, które są demonami. Wynik
działania tego programu jest następujący:
Demon: Thread[Signal dispatcher,5,system]
Demon: Thread[Reference Handler,10,system]
Demon: Thread[Finalizer,8,system]
Demon: Thread[DEMON,5,main]
Okazuje się więc, że jedynym wątkiem w Javie po uruchomieniu programu (czyli bez
własnych wątków) nie będącym demonem jest wątek main. Demon jest więc wątkiem
działającym w tle. Jeżeli wszystkie istniejące wątki są demonami to Maszyna
Wirtualna kończy pracę. Podobnie można oznaczyć grupę wątków jako demon. Nie
oznacza to jednak, że automatycznie wszystkie wątki należące do tej grupy będą
demonami.
Poniższy przykład ukazuje sytuację, w której wszystkie aktualnie wykonywane wątki
przez Maszynę Wirtualną stanją się demonami. Wówczas maszyna kończy pracę.
//Duch.java
class Zly extends Thread{
Zly(){
super();
setName("Zly_demon");
//setDaemon(true);
}
public void run(){
while(!this.isInterrupted()){
}
}
}// koniec class Zly
class Cacper extends Thread{
Cacper(ThreadGroup g, String s){
super(g,s);
}
public void run(){
Zly z = new Zly();
z.start();
while(!this.isInterrupted()){
} }
}//koniec class Cacper
public class Duch {
public static void main(String args[]) {
ThreadGroup zle_duchy = new ThreadGroup("zle_duchy");
Cacper c = new Cacper(zle_duchy,"cacper");
c.start();
try{
Thread.currentThread().sleep(4000);
}catch (Exception e){}
ThreadGroup grupa = Thread.currentThread().getThreadGroup();
while(grupa.getParent() != null) {
grupa = grupa.getParent();
}
grupa.list();
c.interrupt();
try{
Thread.currentThread().sleep(4000);
}catch (Exception e){}
grupa = Thread.currentThread().getThreadGroup();
while(grupa.getParent() != null) {
grupa = grupa.getParent();
}
grupa.list();
}
}// koniec public class Duch
W powyższym kodzie stworzono trzy wątki: main-> cacper -> Zly_demon. Po
określonym czasie (4 sekundy) wątek cacper ginie śmiercią naturalną, zostawiając
pracujący wątek Zly_demon. Po uruchomieniu tego programu pokażą się dwa
wydruki o stanie wątków, po czym program zawiesi się (wieczna pętla wątku
Zly_demon). Co ciekawe, jeżeli wywoła się polecenie wydruku stanu wątków
(CTRL-Break dla Windows) okaże się, że brak jest wątku o nazwie main. Jeżeli
powyższy kod ulegnie zmianie w ten sposób, że wątek Zly_demon uzyska status
demona, wówczas Maszyna Wirtualna przerwie pracę nawet jeżeli wątek
Zly_demon wykonuje wieczne działania.
Aplety, grafika w Javie
Aplety
Aplet jest programem komputerowym, stworzonym w ten sposób, że możliwe
jest jego wykonywania tylko z poziomu innej aplikacji. Oznacza to, że aplet nie jest
samodzielnym programem. Jak wspomniano już na początku tego materiału,
definicja apletu odbywa się poprzez definicję klasy dziedziczącej z klasy Aplet.
Konieczne jest więc jawne importowanie pakietu java.applet.* zawierającego tylko
jedną klasę Applet.Nadklasą klasy Applet jest klasa Panel zdefiniowana w graficznym pakiecie AWT
Abstract Window Toolkit. Łatwo się więc domyśleć, że również i aplet ma formę
graficzną. Można w dużym uproszczeniu powiedzieć, że pole robocze apletu jest
oknem graficznym, w którym wszystkie operacje edycyjne wykonuje się przez
odwołanie do obiektu graficznego klasy Graphics. AWT zostanie omówiona poniżej i
tam również zaprezentowane zostaną liczne przykłady apletów związane z grafiką.
Jak już wspomniano aplet jest programem wykonywany pod kontrolę innej aplikacji.
Przykładową aplikacją może być przeglądarka stron HTML (np. Netscae Navigator,
MS Internet Explorer, itp.). Oznacza to, że konieczne jest umieszczenie wywołania
kodu apletu w ciele strony HTML obsługiwanej przez daną przeglądarkę. W tym celu
wprowadzono w HTML następujące tagi: < APPLET > oraz < /APPLET > oznaczające
odpowiednio początek i koniec pól definiujących aplet. Podstawowym polem
(atrybutem) jest pole CODE informujące aplikację czytającą HTML gdzie znajduje się
skompilowany kod apletu. Przykładowo w omawianym już przykładzie 1.4 atrybut
CODE zdefiniowano następująco: code=Jedi2.class. W przykładzie tym aplikacja ma
pobrać kod apletu umieszczony w pliku o nazwie Jedi2.class znajdujący się w
aktualnym katalogu. W celu wskazania ewentualnego katalogu, w którym
umieszczono kod apletu można posłużyć się polem CODEBASE, np.
code=Jedi2.class codebase=klasy. Aplet może posiadać swoją nazwę określoną w
kodzie HTML poprzez wykorzystanie pola NAME, np. code=Jedi2.class
name=aplecik. Inne bardzo popularne pola wykorzystywane do opisu apletu to
WIDTH oraz HEIGHT. Oba parametry oznaczają rozmiar (szerokość i wysokość)
pola pracy apletu zawartego na stronie WWW. Przykładowo code=Jedi2.class
width=200 height=100 oznacza ustalenie pola pracy apletu na stronie WWW o
szerokości 200 pikseli i wysokości 100 pikseli. Możliwe jest również sterowanie
położeniem okna względem tekstu poprzez przypisanie odpowiedniej wartości do
pola ALIGN, takich jak: LEFT, RIGHT, TOP, TEXTTOP, MIDDLE, ABSMIDDLE,
BASELINE, BOTTOM oraz ABSBOTTOM np. align=middle. Czasami przydatne jest
odseparowanie pola apletu od otaczającego tekstu. Wykonuje się to poprzez
właściwe ustawienie liczby pikseli stanowiących spację w poziomie i pionie, czyli
poprzez wykorzystanie pól: HSPACE i VSPACE. Ponieważ z różnych przyczyn nie
zawsze przeglądarka będzie w stanie uruchomić aplet dlatego warto wprowadzić
zastępczy tekst co wykonuje się poprzez przypisanie tekstu do pola ALT, np.
alt=Aplet rysujący znak firmowy
W pracy z apletami istotny jest również fakt, że często koniecznie trzeba wykorzystać
kilka różnych plików związanych z apletem (np. kilka klas, obrazki, dźwięki, itp.).
Efektywnie wygodne jest powiązanie wszystkich plików związanych z danym apletem
w jeden zbiór (np. eliminacja czasu nawiązania połączeń osobno dla każdego pliku
HTTP 1.0). Java daje taką możliwość poprzez stworzenie archiwum narzędziem
dostarczanym w dystrybucji JDK a mianowicie: JAR. Wywołanie polecenia jar z
odpowiednimi przełącznikami i atrybutami umożliwia liczne operacje na archiwach.
Przykładowe opcje:
-c stwórz nowe archiwum
-t pokaż zawartość archiwum
-x pobierz podane pliki z archiwum
-u aktualizuj archiwum
-f określenie nazwy archiwum
-O stwórz archiwum bez domyślnej kompresji plików metodą ZIP
W celu stworzenia archiwum z wszystkich klas zawartych w bieżącym katalogu i
nadać mu nazwę JediArchiwum można wywołać polecenie:
jar -cf JediArchiwum *.class
Tak stworzone archiwum można przejrzeć: jar tf JediArchiwum, lub odtworzyć: jar
xf JediArchiwum.W celu wykorzystania apletu, którego kod jest zawarty w pliku archiwum trzeba
wykorzystać pole ARCHIVES i nadać mu wartość nazwy pliku archiwum np.:
code=Jedi2 archives=JediArchiwum.jar. Należy pamiętać o tym, że w polu code
podaje się tylko nazwę klasy apletu bez rozszerzenia *.class.
W celu demonstracji omówionych zagadnień rozbudujmy aplet, a właściwie
wywołujący go kod HTML
// Jedi2.java :
import java.applet.Applet;
import java.awt.*;
public class Jedi2 extends Applet{
public void paint(Graphics g){
g.drawString(Rycerz Luke ma niebieski miecz., 15,15);
}
} // koniec public class Jedi2.class extends Applet
------------------------------------------------------------------
Jedi2n.html :
< html >
< head >
< title > Przykładowy aplet< /title>
< /head >
< body >
< applet code=Jedi2.class name= aplecik
width=200 height=100
alt=Aplet drukujący tekst
align=middle
hspace=5 vspace=5>
Tu pojawia się aplet
< /applet >
< /body >
< /html >
Łatwo zauważyć, że opis i znaczenie pól dla elementu APPLET w HTML jest
podobne jak dla elementu IMG. Spośród wymienionych pól konieczne do
zastosowania w celu opisu apletu są pola CODE oraz WIDTH i HEIGHT. Te ostatnie
dwa są wymagane przez program appletviewer, niemniej w niektórych
przeglądarkach istnieje możliwość wprowadzenia opisu apletu bez podania rozmiaru
okna apletu. Czy będzie wówczas wygenerowane domyślne okno? Tak, obszarem
okna będzie wolne pole edycyjne dostępne przeglądarce (zależne od rozdzielczości
ekranu). Oczywiście nie jest to właściwe działanie, i ważne jest ustawianie pól
WIDTH oraz HEIGHT. Poniższy program ukazuje możliwość wykorzystania nazwy apletu oraz pobrania
rozmiaru apletu:
//Jedi2nn.java
import java.applet.Applet;
import java.awt.*;
class Sith2n{
Applet app;
Sith2n(Applet a){
this.app=a;
}
public Dimension rozmiar(){
Dimension d = app.getSize();
return d;
}
}// koniec class Sith
public class Jedi2n extends Applet{
public void paint(Graphics g){
g.drawString("Rycerz Luke ma niebieski miecz.", 15,15);
Sith2n s = new Sith2n(this);
Dimension d = s.rozmiar();
String roz = new String("Szerokość="+d.width+
"; wysokość="+d.height);
g.drawString(roz,15,30);
g.drawString(info(),15,45);
}
public String info(){
Applet aplet=getAppletContext().getApplet("aplecik");
return (aplet.getCodeBase()).toString();
}
} // koniec public class Jedi2n extends Applet
//----------------------------------------------------------
//Jedi2nn.html:
< html >
< head >
< title > Przykładowy aplet< /title >
< /head >
< p > Oto aplet: < /p >
< body >
< applet code=Jedi2n.class name= aplecik
width=200 height=100
alt="Aplet drukujący tekst"
align=middle
hspace=5 vspace=5 >
Tu pojawia się aplet
< /applet >
< /body >
< /htm l>
W przykładzie tym zastosowano metodę getSize() zwracającą obiekt klasy
Dimension zawierający pola szerokości (width) i wysokość (height) opisujące rozmiar
okna apletu. Metoda info() przekazuje tekst opisujący adres źródłowy apletu, którego
obiekt jest uzyskiwany przez odwołanie się do nazwy apletu. Jest to typowo
edukacyjny przykład (metoda info() mogła by być` zrealizowana tylko poprzez
odwołanie się do this.getCodeBase()).
Powyższy kod został rozbity na dwie klasy celem dodatkowego pokazania
możliwości pracy z archiwami. W celu stworzenia archiwum składającego się z
dwóch klas Jedi2n.class oraz Sith.class należy wywołać program jar:
jar -cf JediArchiwum.jar Jedi2n.class Sith2nnn.class
Po stworzeniu archiwum warto sprwdzić jego zawartość poprzez:
jar -tf JediArchiwum.jar
Tak przygotowane archiwum możemy wykorzystać do konstrukcji kodu HTML
wywołującego aplet Jedi2n:
//Jedi2nnn.html
< html >
< head >
< title > Przykładowy aplet< /title >
< /head >
< p > Oto aplet: < /p >
< body >
< applet code=Jedi2n name= aplecik archives=JediArchiwum.jar
width=200 height=100
alt="Aplet drukujący tekst"
align=middle
hspace=5 vspace=5 >
Tu pojawia się aplet
< /applet >
< /body >
< /html >
Oczywiście efekt działanie jest taki sam jak poprzednio.
Ostatnie pole w opisie apletu, które warto omówić to pole przechowujące parametr
apletu. Parametr jest skonstruowany poprzez dwa argumenty: name oraz value. W
polu name parametr przechowuje swoją nazwę, natomiast w polu value swoją
wartość. Konstrukcja parametru jest następująca:
< param name=Nazwa value=Wartość >
np.:
< aplet code =Jedi2n.class width = 300 hight =100 >
< param name = Opis value = "To jeest aplet" >
< /aplet >
Dla obu pól parametru istotna jest wielkość znaków. Jeżeli pole wartość przechowuje
spację lub znaki specjalne musi być ujęte w cudzysłowie. Ilość parametrów jest
dowolna. W kodzie apletu można pobrać wartość parametru o danej nazwie
wykorzystując metodę getParameter() z argumentem stanowiącym nazwę
parametru, np. getParameter(Opis).
Przydatną metodą klasy Applet jest metoda showStatus(). Podawany jako argument
tej metody tekst jest wyświetlany w oknie statusu przeglądarki. Jest to często bardzo
wygodna metoda powiadamiania użytkownika o działaniu apletu. Omawiane
wcześniej (rozdział 1) metody obsługi apletu (init(), start(), stop(), paint(), destroy())
pozwalają się domyśleć jaką formę przybiera każdy aplet. Otóż każdy aplet jest
właściwie wątkiem (podobieństwo start(), stop()) należącym do domyślnej grupy o
nazwie applet-tu_nazwa_klasy_z_rozszerzeniem, z domyślnym priorytetem 5 o
domyślnej nazwie Thread-tu_kolejny_numer. Metody start() i stop() apletu nie
odpowiadają oczywiście metodą o tych samych nazwach zdefiniowanych w klasie
Thread. Należy więc pamiętać o tym, że zmiana strony WWW w przeglądarce wcale
nie oznacza zakończenia pracy apletu. Zmiana strony oznacza jedynie wywołanie
metody stop() apletu czyli żądanie zawieszenia aktywności apletu (np. zatrzymanie
animacji) - przejście od statusu start do init. Standardowo metoda stop() nic nie robi.
Jeżeli jest potrzebna jakaś akcja przy zmianie stron wówczas musi być ona
zdefiniowana w ciele metody stop(). Podobnie jest z metodą destroy(), z tym, że
wołana jest ona bezpośrednio przez zakończeniem pracy apletu (np. koniec pracy
przeglądarki). Omawiane metody obsługi apletu są więc wywoływane przez aplikację
kontrolującą aplet w wyniku wystąpienia określonych zdarzeń, i mogą być
przedefiniowane przez programistę celem wprowadzenia zamierzonego działania.
Należy tu podkreślić bardzo ważny element w korzystaniu z przeglądarek w pracy z
apletami. Aby uruchomić aplet dana przeglądarka musi być zgodna, tzn., dostarczać
Maszynę Wirtualną do obsługi kodu bajtów. Obsługiwana wersja Javy może być
łatwo uzyskana poprzez uruchomienie konsoli Javy, którą stanowi opcjonalne okno
przeglądarki. W celu uniezależnienia się od zaimplementowanej wersji Javy w danej
przeglądarce Sun opracował i udostępnia plug-in Javy. Po zainstalowaniu wtyczki i
odpowiednim przygotowaniu kodu HTML (HTMLconverter) można uruchomić
praktycznie każdy dobrze napisany aplet, nawet dla najnowszej wersji JDK
obsługiwanej zazwyczaj przez plug-in. Przygotowanie kodu HTML zawierającego
aplet do pracy z wtyczką wymaga znacznych zmian w opisie apletu. Jest to
spowodowane tym, że w części kodu HTML gdzie był uruchamiany aplet musi być
uruchamiana wtyczka, której parametrami wywołania są kod klasy apletu, nazwa
apletu oraz typ elementu. Dotychczasowe wywołanie apletu przez tag < APPLET >
musi być usunięte lub wzięte w komentarz. Pomocnym narzędziem wykonującym
automatyczną konwersję kodu HTML do wersji zgodnej z wtyczką jest aplikacja Javy:
HTMLConverter. Sun dostarcza tę aplikację w celu szybkiego dostosowania kodu
HTML, bez konieczności ręcznej modyfikacji źródła HTML. HTMLConverter może
dokonać konwersji pojedynczego pliku HTML lub całej serii plików wykonując przy
tym kopie zapasowe wersji oryginalnych w jednym z katalogów. Przykładowa treść
pliku HTML prezentowanego wcześniej (Jedi2n.html) po konwersji będzie wyglądała
następująco:
//Jedi2n.html po konwersji
< html >
< head >
< title > Przykładowy aplet< /title >
< /head >
< body >
< p > To jest aplet < /p >
< OBJECT classid="clsid:8AD9C840-044E-11D1-B3E9-00805F499D93"
WIDTH = 200 HEIGHT = 100 NAME = aplecik ALIGN = middle VSPACE = 5 HSPACE = 5 ALT = "Aplet
drukujący tekst" codebase="http://java.sun.com/products/plugin/1.2/jinstall-12-win32.cab#Version=1,2,0,0">
< PARAM NAME = CODE VALUE = Jedi2.class >
< PARAM NAME = NAME VALUE = aplecik >
< PARAM NAME="type" VALUE="application/x-java-applet;version=1.2">
< COMMENT >
< EMBED type="application/x-java-applet;version=1.2" java_CODE = Jedi2.class ALT = "Aplet drukujący tekst"
NAME = aplecik WIDTH = 200 HEIGHT = 100 ALIGN = middle VSPACE = 5 HSPACE = 5
pluginspage="http://java.sun.com/products/plugin/1.2/plugin-install.html">< NOEMBED >< /COMMENT >
< /NOEMBED >< /EMBED >
< /OBJECT >
< !--"END_CONVERTED_APPLET"-->
< p > Fajny no nie < /p >
< /body >
< /html >
Wyraźnie widać podział pliku na część związaną z tagiem OBJCET oraz na część
wziętą w komentarz związaną z tagiem APPLET. Oczywiście aby można była
skorzystać z tego kodu należy zainstalować wtyczkę do przeglądarki. Kod
instalacyjny można pobrać z serwera opisanego w polu pluginspage równym
" http://java.sun.com/products/plugin/1.2/plugin-install.html."
Powyżej omówione zostały podstawowe zagadnienia związane tylko z apletami.
Bardzo ważne są również inne tematy dotyczące apletów jak np. wykorzystanie
grafiki, bezpieczeństwo, itp., które będą systematycznie w tych materiałach
omawiane.
Grafika w Javie
Biblioteka Abstract Window Toolkit - AWT jako historycznie pierwsza w JDK
dostarcza szereg elementów wykorzystywanych do tworzenia interfejsów graficznych
dla potrzeb komunikacji z
użytkownikiem i obsługi jego danych. Do najbardziej istotnych elementów tego
pakietu należy zaliczyć:
kompnenty (Components {widgets}),
rozkałdy(Layout managers),
zdarzenia (Events),
rysunki i obrazy (Drawing and images)...
Od wersji JAVA JDK 2 wprowadzono istotne rozszerzenia AWT związane z
przygotowaniem GUI. Podstawowe elementy tej rozbudowy to pakiety javax.swing i
tak zwany zestaw Java2D rozszerzający klasy biblioteki java.awt. Do najbardziej
istotnych elementów javax.swing i Java2D należy zaliczyć:
- nowe, rozbudowane graficznie komponenty w SWING,
- nowe rozkłady i metody pracy z kontenerami,
- rozbudowa biblioteki java.awt.image,
- dodanie nowych klas do biblioteki java.awt (np. Graphics2D).
Platforma 2 Javy w sposób znaczny zmienia możliwości tworzenia aplikacji stąd
krótko zostaną omówione podstawowe elementy pakietu AWT i SWING wraz z
ich porównaniem.
Komponenty
Komponenty to podstawowe elementy graficzne aktywne lub bierne służące do
tworzenia interfejsu graficznego. Komponenty są reprezentowane przez klasy
głównie dziedziczące z klasy Component:
Box.Filler, Button, Canvas, Checkbox, Choice, Container, Label, List, Scrollbar,
TextComponent
Każdy komponent posiada odpowiednio skonstruowaną klasę i metody, które
opisane są w dokumentacji API.
Klasa Component stanowi klasę abstrakcyjną tak więc nie odwołuje się do niej
bezpośrednio (przez stworzenie obiektu za pomocą new) lecz przez klasy potomne.
Klasa Component dostarcza szereg ogólnie wykorzystywanych metod np.:
public Font getFont() - zwraca informacje o czcionce wybranej dla komponentu
public Graphics getGraphics() - zwraca kontekst graficzny komponentu potrzebny
przy wywoływaniu metod graficznych
public void paint(Graphics g) - rysuje komponent,
itp.
Jednym z komponentów jest kontener reprezentowany przez klasę Container.
Kontener jest komponentem, w którym można umieszczać inne komponenty
Przykładowym kontenerem wykorzystywanym dotąd w tym materiale w przykładach
jest Applet (dziedziczy z klasy Panel, a ta z klasy Container). Podstawowe klasy
(kontenery) dziedziczące z klasy Container to:
BasicSplitPaneDivider, Box, CellRendererPane,
DefaultTreeCellEditor.EditorContainer, JComponent, Panel, ScrollPane, Window
Najczęściej wykorzystywane kontenery w AWT to Panel oraz Window. Ten ostatni
nie jest wprost wykorzystywany lecz poprzez swoje podklasy:
BasicToolBarUI.DragWindow, Dialog, Frame, JWindow
Klasa Frame jest kontenerem bezpośrednio wykorzystywanym do tworzenia okien
graficznych popularnych w GUI. Dialog jest kontenerem dedykowanym komunikacji z
użytkownikiem i stanowi okno o zmienionej funkcjonalności względem Frame (brak
możliwości dodania paska Menu).
Podstawowa praca w konstrukcji interfejsu graficznego w Javie polega na:
- zadeklarowaniu i zdefiniowaniu kontenera,
- zadeklarowaniu komponentu,
- zdefiniowaniu (zainicjowanie) komponentu
- dodanie komponentu do kontenera.
Ostatni krok w konstrukcji interfejsu związany jest ze sposobem umieszczania
komponentów w kontenerze. Sposób te określany jest w Javie rozkładem (Layout). Z
każdym kontenerem jest więc związany rozkład, w ramach którego umieszczane są
komponenty.
Zanim omówione zostaną kontenery i rozkłady warto zapoznać się z typami
komponentów, które w tych kontenerach mogą być umieszczane.
Do podstawowych komponentów należy zaliczyć:
a) dziedziczące pośrednio i bezpośrednio z klasy Component:
- Label pole etykiety
- Button przycisk
Canvas pole graficzne
Checkbox element wyboru (logiczny)
Choice -element wyboru (z listy)
List lista elementów
Scrollbar suwak
TextComponent:
TestField: pole tekstowe
TextArea: obszar tekstowy
b) nie dziedziczące z klasy Component:
MenuBar pasek menu,
MenuItem - element menu:
Menu menu (zbiór elementów)
PopupMenu menu podręczne.
Komponent, którego klasa nie dziedziczy z klasy Component to MenuComponent.
Klasa ta jest nadklasą komponentów: MenuBar, MenuItem. Ta ostatnia dostarcza
również poprzez klasy dziedziczące z niej następujące komponenty: Menu oraz
PopupMenu. Łącznie cztery klasy tj. MenuBar, MenuItem, Menu oraz PopupMenu są
wykorzystywane do tworzenia menu programu graficznego.
Wszystkie komponenty biblioteki AWT są dobrze opisane w dokumentacji (Java API)
Javy, i dlatego nie będą szczegółowo omawiane w tym materiale. Rozpatrzmy
jedynie metodę tworzenia komponentu i dodawania go do kontenera. Jedynym
kontenerem poznanym do tej pory był Applet i dlatego będzie on wykorzystany w
poniższych przykładach.
Przykładowy komponent - Button - umożliwia stworzenie przycisku graficznego wraz
z odpowiednią etykietą tekstową. Stworzenie obiektu typu Button polega na
implementacji prostej instrukcji:
Button przyciskKoniec = new Button("Koniec");
Dodanie tak stworzonego elementu do tworzonego interfejsu wymaga jeszcze
wywołania metody:
add(przyciskKoniec);
Po uaktywnieniu interfejsu przycisk będzie widoczny dla użytkownika. W podobny
sposób tworzy się obiekty pozostałych klas komponentów, manipuluje się ich
metodami dla uzyskania odpowiedniego ich wyglądu, oraz dodaje się je do
tworzonego środowiska graficznego.
Przykładowy kod źródłowy dla komponentu Button może wyglądać w następujący
sposób:
//Ognia.java:
import java.awt.*;
import java.applet.*;
public class Ognia extends Applet {
public void init() {
Button przyciskTest = new Button("przycisnij mnie...");
add(przyciskTest);
}
}// koniec public class Ognia
//*************************************
Ognia.html :
...
< APPLET CODE="Ognia.class" WIDTH=70 HEIGHT=40 ALIGN=CENTER>< /APPLET >
Praca z pozostałymi komponentami wygląda podobnie. Poniższy przykład
demonstruje różne typy komponentów.
//Pulpit.java:
public class Pulpit extends Applet {
public void init() {
Button przyciskTest = new Button("ognia...");
add(przyciskTest);
Label opis = new Label("Strzelac");
add(opis);
Checkbox rak = new Checkbox("Rakieta");
Checkbox bomb = new Checkbox("Bomba");
add(rak);
add(bomb);
Choice kolor = new Choice();
kolor.add("zielona");
kolor.add("czerwona");
kolor.add("niebieska");
add(kolor);
List lista = new List(2, false);
lista.add("mała");
lista.add("malutka");
lista.add("wielka");
lista.add("duża");
lista.add("ogromna");
lista.add("gigant");
add(lista);
TextField param = new TextField("Podaj parametry");
add(param);
}
}// koniec public class Pulpit
//*******************************************************
< html >
< APPLET CODE="Pulpit.class" WIDTH=800 HEIGHT=80 ALIGN=CENTER>< /APPLET >
< /html >
Komponenty związane z menu są często wykorzystywane przy konstrukcji GUI,
niemniej wymagają kontenera typu Frame, dlatego zostaną omówione później (wraz
z tym kontenerem).
Pakiet javax.swing dostarcza szeregu zmodyfikowanych i nowych kontenerów i
komponentów. Między innymi zmodyfikowano system umieszczania komponentów
w kontenerach (co jest omówione dalej) oraz dodano możliwość wyświetlania ikon na
komponentach. Przykładowe komponenty z pakietu javax.swing można znaleźć w
dokumentacji SWING. Wszystkie komponenty tej biblioteki są reprezentowane przez
odpowiednie klasy, których nazwa zaczyna się od J, np. JButton. Komponenty
biblioteki SWING dziedziczą z AWT (np. z klasy Component). Podstawowe
komponenty tej biblioteki są podobne jak dla AWT z tym, że oznaczane z dodatkową
literą J. Wprowadzono również nowe i przeredagowane elementy jak np.:
JColorChooser pole wyboru koloru
JFileChooser pole wyboru pliku
JPasswordField pole hasła,
JProgressBar pasek stanu
JRadioButton element wyboru
JScrollBar - suwak
JTable - tabela
JTabbedPane - pole zakładek
JToggleButton przycisk dwu stanowy
JToolBar pasek narzędzi
JTree lista w postaci drzewa
Implementacja komponentów SWING jest podobna do implementacji komponentów
zdefiniowanych w AWT.
//Przycisk.java:
import java.awt.*;
import java.applet.*;
import javax.swing.*;
public class Przycisk extends JApplet {
public void init() {
String s. = getCodeBase().toString()+/+jwr2.gif;
/* dla apletu uruchamianego z serwera HTTP
w przeciwnym wypadku może nastąpić błąd bezpieczeństwa
próba odczytu z dysku */
JButton przyciskTest = new JButton(new ImageIcon(s));
getContentPane().add(przyciskTest);
}
}// koniec public class Przycisk
W powyższym przykładzie zastosowano kontener z biblioteki SWING - JApplet, i
dlatego inna jest forma dodania tworzonego komponentu (JButton) do tego
kontenera (getContentPane().add()). Różnica ta zostanie opisana w podrozdziale
dotyczącym rozkładów.
Kontenery
Do tej pory przedstawione zostały dwa kontenery (komponenty zawierające
inne komponenty): Applet oraz JApplet. Warto zapoznać się z pozostałymi.
Zasadniczym kontenerem jest w Javie jest okno reprezentowane przez klasę
Window. Kontener taki nie posiada ani granic okna (ramy) ani możliwości dodania
menu. Stanowi więc jakby pole robocze. Dlatego nie korzysta się z tego kontenera
wprost, lecz poprzez klasy potomne: Frame oraz Dialog. Obiekt klasy Frame jest
związany z kontenerem okna graficznego o sprecyzowanych cechach (ramie - ang.
Frame). Kontener taki stanowi więc to, co jest odbierane w środowisku interfejsu
graficznego przez okno graficzne. Stąd w Javie przyjęło się określać kontener klasy
Frame mianem okna. Okno może posiadać pasek menu. Drugim kontenerem
reprezentowanym przez klasę dziedziczącą z klasy Window - jest kontener typu
Dialog. Kontener ten podobnie jak okno graficzne (Frame) posiada określone ramy,
lecz różni się od tego okna tym, że nie można mu przypisać paska menu. Okno
dialogowe (kontener reprezentowany przez klasę Dialog) posiada zawsze
właściciela, jest zatem zależne. Inne, często wykorzystywane kontenery to Panel
oraz ScrollPane. Panel służy do wyodrębnienia w kontenerze głównym kilku
obszarów roboczych. Klasa Panel nie posiada więc prawie w ogóle żadnych metod
(1) własnych. ScrollPane jest kontenerem umożliwiającym wykorzystanie pasków
przesuwu poziomego i pionowego dla kontenera głównego. Oczywiście biblioteka
SWING definiuje własne kontenery, których nazwa zaczyna się od J, i tak np.
JFrame, JWindow, JDialog, JPanel, JApplet, JFileDialog, itp.
Znając charakterystykę kontenerów warto zapoznać się z możliwością ich
wykorzystania. Zasada jest bardzo prosta - kontenery tworzy się dla:
1. aplikacji - zasadniczy kontener to Frame lub JFrame,
2. apletu - zasadniczy kontener to Applet lub JApplet.
Nie oznacza to jednak, że nie można używać okien w apletach i apletów w oknach!
Tworząc okno graficzne należy kierować się następującą procedurą:
1. deklaracja i zainicjowanie okna, np.: Frame okno = new Frame(Program);
2. ustawienie parametrów okna, np. okno.setSize(300,300);
3. dodanie komponentów do okna, np.: okno.add(new Button(Ognia));
4. ustawienie okna jako aktywnego, np.: okno.setVisible(true);
Brak ostatniego kroku spowoduje, że zdefiniowane okno będzie niewidoczne dla
użytkownika. Rozpatrzmy następujący przykład:
//Dzialo.java:
import java.awt.*;
class Okno extends Frame{
Okno(String nazwa){
super(nazwa); //metoda super wywołuje konstruktor nadklasy
setResizable(false);
setSize(400,400);
}
}// koniec class Okno
public class Dzialo{
public static void main(String args[]){
Okno o = new Okno("Panel sterujący działem");
o.add(new Button("Ognia"));
o.setVisible(true);
}
}// koniec class Dzialo
Efektem działania kodu będzie stworzenie okna graficznego o niezmiennych
rozmiarach 400/400 pikseli. Okno zawiera jeden przycisk pokrywający cały obszar
roboczy. Ponieważ nie omówiono jeszcze metod obsługi zdarzeń nie możliwe jest
zakończenie pracy okna przez wykorzystanie kontrolek okna. Dlatego trzeba
przerwać pracę aplikacji w konsoli (CTRL-C). Okno (podobnie jak Aplet) może być
podzielone na obszary poprzez wykorzystanie paneli. Co więcej panele mogą
również być podzielone na inne panele. Powyższy kod można rozwinąć do postaci:
//DzialoS.java:
import java.awt.*;
class Okno extends Frame{
Okno(String nazwa){
super(nazwa);
setResizable(false);
setSize(400,400);
}
}// koniec class Okno
public class DzialoS{
public static void main(String args[]){
Okno o = new Okno("Panel sterujący działem");
o.setLayout(new FlowLayout()); //zmiana rozkładu
Panel p = new Panel();
p.add(new Button("Ognia"));
p.add(new Button("Stop"));
o.add(p);
Panel p1 = new Panel();
p.add(new Label("Kontrola działa"));
o.add(p1);
o.setVisible(true);
}
}// koniec class DzialoS
W kodzie powyższym wprowadzono dwa panele dzielące okno na dwie części. W
panelu pierwszy umieszczono dwa przyciski, natomiast w panelu drugim jedną
etykietę. Bezpośrednio po zainicjowaniu obiektu okna dokonano zmiany metody
rozkładu elementów aby umożliwić wyświetlenie wszystkich tworzonych
komponentów. Rozkłady są omówione dalej. Omawiany przykład wskazuje na
ciekawy sposób konstrukcji interfejsu graficznego w Javie. Praktycznie interfejs
składany jest z klocków (kontenery i komponenty) co znacznie upraszcza procedurę
tworzenia programów graficznych.
Z kontenerem typu Frame związany jest zbiór komponentów menu. W celu
stworzenia menu okna graficznego należy wykonać następujące kroki:
1. zainicjować obiekt klasy MenuBar reprezentujący pasek menu;
2. zainicjować obiekt klasy Menu reprezentujący jedną kolumnę wyborów,
3. zainicjować obiekt klasy MenuItem reprezentujący element menu w danej
kolumnie
4. dodać element menu do Menu;
5. powtórzyć kroki 2,3,4 tyle razy ile ma być pozycji w kolumnie i kolumn
6. dodać pasek menu do okna graficznego.
Realizację menu przedstawia następujący program:
//DzialoM.java:
import java.awt.*;
class Okno extends Frame{
Okno(String nazwa){
super(nazwa);
setResizable(false);
setSize(400,400);
}
}// koniec class Okno
public class DzialoM{
public static void main(String args[]){
Okno o = new Okno("Panel sterujący działem");
MenuBar pasek = new MenuBar();
Menu plik = new Menu("Plik");
plik.add(new MenuItem("-")); //separator
plik.add(new MenuItem("Ognia"));
plik.add(new MenuItem("Stop"));
pasek.add(plik);
Menu edycja = new Menu("Edycja");
edycja.add(new MenuItem("-"));
edycja.add(new MenuItem("Pokaż dane działa"));
pasek.add(edycja);
o.setMenuBar(pasek);
o.add(new Button());
o.setVisible(true);
Dialog d = new Dialog(o,"Dane działa", false);
d.setSize(200,200);
d.setVisible(true);
d.show();
}
}// koniec class DzialoM
Przykład ten demonstruje mechanizm tworzenia menu w oknie graficznym. Warto
zauważyć, że dodanie paska menu do okna odbywa się nie poprzez metodę add()
lecz przez setMenuBar(). Jest to istotne ze względu na ukazanie różnicy pomiędzy
komponentem typu menu a innymi komponentami, które dziedziczą z klasy
Component jak Button, Label, Panel. W omawianym przykładzie ukazano również
metodę tworzenia okien dialogowych. Wywołany konstruktor powoduje utworzenie
okna dialogowego właścicielem którego będzie okno graficzne o; nazwą okna
dialogowego będzie Dane działa oraz okno to nie będzie blokować operacji wejścia.
W pakiecie SWING również zdefiniowano klasy komponentów związane z menu. Jak
w większości przypadków, główna różnica pomiędzy analogicznymi komponentami
AWT i SWING polega na tym, że te ostatnie umożliwiają implementację elementów
graficznych (ikon). Możliwe jest więc na przykład skonstruowanie menu, w którym
każdy element jest reprezentowany tylko przez ikonę graficzną.
Rozkłady
Bardzo częstym dylematem programisty jest problem rozkładu komponentów
w ramach tworzonego interfejsu graficznego. Problem rozdzielczości ekranu, liczenia
pikseli to typowe zadania do rozwiązania przed przystąpieniem do projektu interfejsu.
W języku JAVA problemy te w znaczny sposób zredukowano wprowadzając tzw.
rozkłady. Rozkład oznacza nic innego jak sposób układania komponentów na danej
formie (np. aplet , panel). System zarządzający rozkładami umieszcza dany
komponent (jako rezultat działania metody add()) zgodnie z przyjętym rozkładem.
Definiowane w AWT rozkłady to:
BORDER LAYOUT - (domyślny dla kontenerów: Window, Frame, Dialog, JWindow,
JFrame, JDialog) komponenty są umieszczane i dopasowywane do pięciu regionów:
północ, południe, wschód, zachód oraz centrum. Każdy z pięciu
regionów jest identyfikowany przez stałą z zakresu: NORTH, SOUTH, EAST, WEST,
oraz CENTER.
CARD LAYOUT - każdy komponent w danej formie (kontenerze) jest rozumiany jako
karta. W danym czasie widoczna jest tylko jedna karta, a forma jest
rozumiana jako stos kart. Metody omawianej klasy umożliwiają zarządzanie
przekładaniem tych kart.
FLOW LAYOUT - (domyślny dla kontenerów: Panel, Applet, JPanel, JApplet)
komponenty są umieszczane w ciągu "przepływu" od lewej do prawej (podobnie do
kolejności pojawiania się liter przy pisaniu na klawiaturze).
GRID LAYOUT - komponenty są umieszczane w elementach regularnej siatki (gridu).
Forma (kontener) jest dzielona na równe pola, w których kolejno
umieszczane są komponenty.
GRIDBAG LAYOUT - komponenty umieszczane są w dynamicznie tworzonej siatce
regularnych pól, przy czym komponent może zajmować więcej niż jedno
pole.
Przykład zastosowania rozkładu BORDER LAYOUT ukazano poniżej:
//ButtonTest4.java
import java.awt.*;
import java.applet.Applet;
public class ButtonTest4 extends Applet {
public void init() {
setLayout(new BorderLayout());
add(new Button("North"), BorderLayout.NORTH);
add(new Button("South"), BorderLayout.SOUTH);
add(new Button("East"), BorderLayout.EAST);
add(new Button("West"), BorderLayout.WEST);
add(new Button("Center"), BorderLayout.CENTER);
}
} // koniec public class ButtonTest4
Pakiet Swing wprowadzający nowe kontenery i komponenty ustala również nowe
metody używania rozkładów oraz wprowadza nowe rozkłady. Kontener (forma)
zawiera podstawowe komponenty zwane PANE (płyty).
Podstawowym komponentem jest tu płyta główna (korzeń) - JRootPane. Płyta ta jest
fundamentalną częścią kontenerów w Swing takich jak JFrame, JDialog,
JWindow, JApplet. Oznacza to, że umieszczenie komponentów nie odbywa się
bezpośrednio przez odwołanie do tych kontenerów lecz poprzez JRootPane. Dla
potrzeb konstrukcji GUI z wykorzystaniem elementów pakietu Swing istotna jest
znajomość struktury JRootPane, pokazanej poniżej.
JRootPane jest tworzona przez glassPane (szyba, płyta szklana) i layeredPane
(warstwę płyt), na którą składają się: opcjonalna menuBar (pasek menu) oraz
contentPane (płyta robocza, zawartość). Płyta glassPane jest umieszczona zawsze
na wierzchu wszystkich elementów (stąd szyba) i jest związana z poruszaniem
myszy. Ponieważ glassPane może stanowić dowolny komponent możliwe jest więc
rysowanie w tym komponencie. Domyślnie glassPane jest niewidoczna. Płyta
contentPane stanowi główną roboczą część kontenera gdzie rozmieszczamy
komponenty i musi być zawsze nadrzędna względem każdego "dziecka" płyty
JRootPane. Oznacza to, że zamiast bezpośredniego dodania komponentu do
kontenera musimy dodać element do płyty contentPane:
-zamiast dodania : rootPane.add(child);
czy jak to było w AWT np:
Frame frame
frame.add(child)
-wykonujemy: rootPane.getContentPane().add(child);
np. JFrame frame;
frame.getContenPane()add(child);
Jest to niezwykle ważna różnica pomiędzy klasycznym dodawaniem elementów do
kontenerów w AWT a w SWING. Oznacza to, że również ustawienia rozkładów muszą odwoływać się do płyt zamiast bezpośrednio do kontenerów np.
frame.getContentPane().setLayout(new GridLayout(3,3)).
Dodatkowo w pakiecie Swing wprowadzono inne rozkłady takie jak:
-BOX LAYOUT
-OVERLAY LAYOUT
-SCROLLPANE LAYOUT
-VIEWPORT LAYOUT
Przykładowo można porównać działanie rozkładu BorderLayout dla implementacji
appletu w AWT i SWING:
// AWT:
// ButtonTest4.java
import java.awt.*;
import java.applet.Applet;
public class ButtonTest4 extends Applet {
public void init() {
setLayout(new BorderLayout());
add(new Button("North"), BorderLayout.NORTH);
add(new Button("South"), BorderLayout.SOUTH);
add(new Button("East"), BorderLayout.EAST);
add(new Button("West"), BorderLayout.WEST);
add(new Button("Center"), BorderLayout.CENTER);
}
} // koniec public class ButtonTest4
Lub
// SWING:
// ButtonTest5.java:
import java.awt.*;
import java.applet.Applet;
import javax.swing.*;
public class ButtonTest5 extends JApplet {
public void init() {
getContentPane().setLayout(new BorderLayout());
getContentPane().add(new Button("North"), BorderLayout.NORTH);
getContentPane().add(new Button("South"), BorderLayout.SOUTH);
getContentPane().add(new Button("East"), BorderLayout.EAST);
getContentPane().add(new Button("West"), BorderLayout.WEST);
getContentPane().add(new Button("Center"), BorderLayout.CENTER);
}
} // koniec public class ButtonTest5
W celu zrozumienia różnicy warto skompilować i uruchomić aplet buttonTest5 z
usunięciem części " getContentPane()" z linii getContentPane().setLayout(new
BorderLayout()); w kodzie programu.
Zdarzenia
Oprócz wyświetlenia komponentu konieczne jest stworzenie odpowiedniego
sterowania związanego z danym komponentem. W przykładowym kodzie apletu
ButtonTest.java ukazano implementację metody action():
//ButtonTest.java:
import java.awt.*;
import java.applet.*;
public class ButtonTest extends Applet {
public void init() {
Button przyciskTest = new Button("przycisnij mnie...");
add(przyciskTest);
}
public boolean action(Event e, Object test) {
if(test.equals("przycisnij mnie..."))
System.out.println("Przycisnieto " + test);
else
return false;
return true;
}
}// koniec public class ButtonTest
//---------------------------------------------------
ButtonTest.html :
...
< APPLET CODE="ButtonTest.class" WIDTH=70 HEIGHT=40 ALIGN=CENTER>< /APPLET >
Uwaga! Kompilacja tego kodu w środowisku JDK późniejszym niż 1.0 zgłosi
ostrzeżenie o użyciu metody (action()) o obniżonej wartości (deprecated). Nie
powoduje to jednak w tym przypadku przerwania procesu kompilacji.
Metoda ta obsługuje zdarzenia związane z obiektem typu Button, i w rozpatrywanym
przykładzie wyświetla tekst na standardowym urządzeniu wyjścia. Warto zwrócić
uwagę na instrukcję warunkową if(test.equals()). Otóż metoda action może
obsługiwać kilka komponentów i w zależności od obiektu test wykonywać to
działanie, które programista umieścił w odpowiedniej części warunkowej. W
przypadku obsługi wielu różnych komponentów bardzo często pojawiają się błędy
wynikające z nazewnictwa etykiety komponentu (przestawione litery, wielkie litery,
itp.). Ogólnie biorąc każdy komponent może wywołać określone zdarzenie (Event),
którego obsługa umożliwia interaktywną pracę z interfejsem programu. W
tradycyjnym modelu GUI program generuje interfejs graficzny, a następnie w
nieskończonej pętli czeka na pojawienie się zdarzeń, które należy w celowy sposób
obsłużyć. W Javie 1.0 obsługę zdarzeń wykonuje się poprzez odpowiednie instrukcje
warunkowe określające typ zdarzenia w ciele metod: action() oraz handleEvent() (od
JDK 1.1 metody te są wycofywane deprecated). Takie rozwiązanie jest mało
efektywne, a na pewno nie jest obiektowe. Od wersji Javy 1.1 wprowadza się nowy
system przekazywania i obsługi zdarzeń określający obiekty jako nasłuchujące
zdarzeń (listeners) i jako generujące zdarzenia (sources). W tym nowym modelu
obsługi zdarzeń komponent "odpala" ("fire") zdarzenie. Każdy typ zdarzenia jest
reprezentowany przez określoną klasę. W nowym systemie można nawet stworzyć
własne typy zdarzeń. Wygenerowane przez komponent zdarzenie jest odbierane
przez właściwy element nasłuchu związany z komponentem generującym zdarzenie.
Jeżeli w ciele klasy nasłuchu danych zdarzeń istnieje metoda obsługi tego zdarzenia,
to zostanie ona wykonana. Generalnie więc można rozdzielić źródło zdarzeń i
obsługę zdarzeń. Najczęstszym modelem programu wykorzystywanym do tworzenia
obsługi zdarzeń jest implementacja obsługi zdarzeń jako klasa wewnętrzna (inner
class) klasy komponentu, którego zdarzenia mają
być obsługiwane. Przykładowo:
//ButtonTest2.java:
import java.awt.*;
import java.awt.event.*; // Koniecznie należy pamiętać o importowaniu pakietu event
import java.applet.*;
public class ButtonTest2 extends Applet {
Button b1, b2;
public void init() {
b1 = new Button("Przycisk 1");
b2 = new Button("Przycisk 2");
b1.addActionListener(new B1()); //dodajemy obiekt nasłuchujący zdarzenia dla komponentu b1
b2.addActionListener(new B2()); //dodajemy obiekt nasłuchujący zdarzenia dla komponentu b1
add(b1);
add(b2);
} // koniec public void init()
class B1 implements ActionListener {
public void actionPerformed(ActionEvent e) {
getAppletContext().showStatus("Przycisk 1");
}
} // koniec class B1
class B2 implements ActionListener {
public void actionPerformed(ActionEvent e) {
getAppletContext().showStatus("Przycisk 2");
}
} // koniec class B2
/* Dla porównania stary model obsługi zdarzeń:
public boolean action(Event e, Object test) {
if(e.target.equals(b1)) //lub test.equals("Przycisk 1"); gorsze rozwiazanie
getAppletContext().showStatus("Przycisk 1");
else if(e.target.equals(b2))
getAppletContext().showStatus("Przycisk 2");
else
return super.action(e, test);
return true;
}
*/
} // koniec public class ButtonTest2 extends Applet
//---------------------------------------------------
ButtonTest2.html :
...
< APPLET CODE="ButtonTest2.class" WIDTH=70 HEIGHT=40 ALIGN=CENTER >< /APPLET >
Jak widać na prezentowanym przykładzie element nasłuchu zdarzeń jest obiektem
klasy implementującej interfejs nasłuchu. To, co musi zrobić programista to stworzyć
odpowiedni obiekt nasłuchu dla komponentu odpalającego zdarzenie. Przesłanie
obiektu nasłuchu polega na wykonaniu metody addXXXXXListener() dla danego
komponentu, gdzie XXXXX oznacza typ zdarzenia jakie ma być nasłuchiwane (np.
addMouseListener, addActionListener, addFocusListener, ...).
Sposób rejestracji i nazwa metody obsługującej tę rejestrację daje programiście
informację jaki typ zdarzeń jest obsługiwany w danym fragmencie kodu.
Często rejestracja obiektu obsługi zdarzeń zawiera definicję klasy i metody obsługi
zdarzenia. W przeciwieństwie do wyżej omawianej metody definiowane klasy są
anonimowe. To alternatywne rozwiązanie obsługi zdarzeń jest często
wykorzystywane szczególnie tam, gdzie kod obsługi zdarzenia jest krótki.
Najprostszym przykładem może być stworzenie ramki - FRAME (okna), która będzie
wyświetlana na ekranie i prostego kodu obsługującego zdarzenie zamknięcia ramki
(np. metodą System.exit(0);). Standardowo wykorzystuje się następujący kod:
...
addWindowListener(new WindowAdapter(){ /ddodajemy obsługę
zdarzeń okna
public void windowClosing((WindwEvent e){
//implementacja metody zamykania okna przy wystąpieniudanego zdarzenia
System.out.println("Dziekujemy za prace z programem ...");
// akcja podejmowana przy zamykaniu okna
System.exit (0)
// koniec programu
}
});
...
Możemy więc teraz stworzyć przykładowy program (może być wywołany zarówno
jako aplet jak i aplikacja) demonstrujący omówione zagadnienia:
//ButtonTest3.java:
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
public class ButtonTest3 extends Applet {
Button b1, b2;
TextField t = new TextField(20);
public void init() {
b1 = new Button("Przycisk 1");
b2 = new Button("Przycisk 2");
b1.addActionListener(new B1());
b2.addActionListener(new B2());
add(b1);
add(b2);
add(t);
}
class B1 implements ActionListener {
public void actionPerformed(ActionEvent e) {
t.setText("Przycisk 1");
}
}// koniec class B1
class B2 implements ActionListener {
public void actionPerformed(ActionEvent e) {
t.setText("Przycisk 2");
}
}// koniec class B2
// statyczna metoda main() wymagana dla aplikacji:
public static void main(String args[]) {
ButtonTest3 applet = new ButtonTest3();
Frame okno = new Frame("ButtonTest3");
okno.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e){
System.out.println("Dziekujemy za prace z programem...");
System.exit(0);
}
});
okno.add(applet, BorderLayout.CENTER);
okno.setSize(300,200);
applet.init();
applet.start();
okno.setVisible(true);
}
} // koniec public class ButtonTest3 extends Applet
W celu właściwej obsługi zdarzeń konieczna jest wiedza dotycząca typu zdarzeń .
Związane jest z tym poznanie podstawowych klas zdarzeń. W wersji JDK 1.0
podstawową klasą przechowującą informacje związaną z danym zdarzeniem była
klasa java.awt.Event. W celu kompatybilności z JDK1.0 klasa ta występuje również w
późniejszych wersjach JDK. Począwszy od JDK 1.1 podstawową klasą zdarzeń jest
klasa java.awt.AWTEvent będąca korzeniem wszystkich klas zdarzeń w AWT. Klasa
ta dziedziczy z klasy java.util.EventObject będącą korzeniem wszystkich klas
zdarzeń w Javie (nie tylko AWT ale np. dla SWING, itp.). Klasy zdarzeń dla AWT są
zdefiniowane w pakiecie java.awt.event.* i dlatego w celu obsługi zdarzeń konieczne
jest importowanie tego pakietu:
(java.awt.event.*)Dodatkowe zdarzenia zdefiniowane dla biblioteki SWING to:
(javax.swing.event.*).Każda klasa zdarzeń definiuje zbiór pól przechowujących stan zdarzenia. Stan
zdarzenia jest istotny ze względu na obsługę zdarzenia, a więc na wykonanie
odpowiedniego działania związanego z wartościami pól obiektu zdarzenia.
Referencja do obiektu odpowiedniego zdarzenia jest podawana jako argument przy
wykonywaniu metody obsługującej to zdarzenie. Metody związane ze zdarzeniami są
pogrupowane jako zbiory abstrakcyjnych metod w interfejsach lub jako zbiory
pustych metod w klasach (adaptery). Zadaniem programisty jest definicja metod
wykorzystywanych do obsługi zdarzeń.Wykonując metodę obsługującą zdarzenie można wykorzystać pola obiektu danego
zdarzenia. Możliwość ta pozwala określić okoliczności wystąpienia zdarzenia np.:
współrzędne kursora podczas generacji zdarzenia, typ klawisza klawiatury jaki był
wciśnięty, nazwę komponentu źródłowego dla danego zdarzenia, stan lewego
przycisku myszki podczas zdarzenia, itp. Analiza pól obiektu jest zatem niezwykle
istotna z punktu widzenia działania tworzonego interfejsu graficznego.
Poniższy program ukazuje obsługę zdarzeń związanych z klawiaturą i myszką:
//Komunikator.java:
import java.awt.*;
import java.awt.event.*;
class Ekran extends Canvas{
public String s="Witam";
private Font f;
Ekran (){
super();
f = new Font("Times New Roman",Font.BOLD,20);
setFont(f);
addKeyListener(new KeyAdapter(){
public void keyPressed(KeyEvent ke){
s=new String(ke.paramString());
repaint();
}
});
addMouseListener(new MouseAdapter(){
public void mousePressed(MouseEvent me){
s=new String(me.paramString());
repaint();
}
});
}
public void paint(Graphics g){
g.drawString(s,20,220);
}
}// koniec class Ekran
public class Komunikator extends Frame {
Komunikator (String nazwa){
super(nazwa);
}
public static void main(String args[]){
Komunikator okno = new Komunikator("Komunikator");
okno.setSize(600,500);
Ekran e = new Ekran();
okno.add(e);
okno.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e){
System.out.println("Dziekujemy za prace z programem...");
System.exit(0);
}
});
okno.setVisible(true);
}
}// koniec public class Komunikator
W przykładzie tym zastosowano komponent Canvas stanowiący pole graficzne, w
obrębie którego można rysować używając metod zdefiniowanych w klasie Graphics.
Z polem graficznym związano obsługę dwóch typów zdarzeń: wciśnięcie klawisza
klawiatury (keyPressed()) oraz wciśnięcie klawisza myszki (mousePressed()).
Zdarzenia te są opisane w obiektach klas KeyEvent oraz MouseEvent. Każdy obiekt
przechowuje informacje związane z powstałym zdarzeniem i dlatego można te pola
odczytać celem wyświetlenia ich wartości. W powyższym przykładzie zasotosowano
metodę paramString() zwracającą wartości pól obiektu w postaci łańcucha znaków
(String). W przypadku wystąpienia zdarzenia i jego obsługi za pomocą omawianych
metod generowana jest metoda repaint() powodująca odświeżenie pola graficznego.
Warto zauważyć, że w definiowaniu klas anonimowych obsługujących zdarzenia
zastosowano adaptery. Jak wspomniano wcześniej jest to na tyle wygodne w
przeciwieństwie od implementacji interfejsów, że nie trzeba definiować wszystkich
metod w nich zawartych (zadeklarowanych).
Grafika i multimedia w Javie
Grafika (rysunki)>
Pakiet AWT zarówno w wersjach wcześniejszych jak i w wersji 2 wyposażony
jest w klasę Graphics, a od wersji 2 dodatkowo w klasę Graphics2D. Klasy te
zawierają liczne metody umożliwiające tworzenie i zarządzanie grafiką w Javie.
Podstawą pracy z grafiką jest tzw. kontekst graficzny, który jako obiekt posiada
właściwości konkretnego systemu prezentacji np. panelu. W AWT kontekst graficzny
jest dostarczany do komponentu poprzez następujące metody:
- paint
- paintAll
- update
- print
- printAll
- getGraphics
Obiekt graficzny (kontekst) zawiera informacje o stanie grafiki potrzebne dla
podstawowych operacji wykonywanych przez metody Javy. Zaliczyć tu należy
następujące informacje:
- obiekt komponentu, który będzie obsługiwany,
- współrzędne obszaru rysowania oraz obcinania,
- aktualny kolor,
- aktualne czcionki,
- aktualna funkcja operacji na pikselach logicznych (XOR lub Paint),
- aktualny kolor dla operacji XOR.
Posiadając obiekt graficzny można wykonać szereg operacji rysowania np.: Graphics
g;
g.drawLine(int x1, int y1, int x2, int y2) - rysuje linię pomiędzy współrzędnymi (x1,y1)
a (x2,y2), używając aktualnego koloru,
g.drawRect(int x, int y, int width, int height) - rysuje prostokąt o wysokości height i
szerokości width począwszy od punktu (x,y), używając aktualnego koloru,
g.drawString(String str, int x, int y) - rysuje tekst str począwszy od punktu (x,y),
używając aktualnego koloru i czcionki,
g.drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) -
wyświetla aktualnie dostępny zestaw pikseli obrazu img na tle o kolorze bgcolor, począwszy od punktu (x,y)
g.setColor(Color c) - ustawia aktualny kolor c np. Color.red
g.setFont(Font font) - ustawia aktualny zestaw czcionek
W celu rysowania elementów grafiki konieczna jest znajomość układu współrzędnych
w ramach, którego wyznacza się współrzędne rysowania. Podstawowym układem
współrzędnych w Javie jest układ użytkownika, będący pewną abstrakcją układów
współrzędnych dla wszystkich możliwych urządzeń.
Układ współrzędnych konkretnego urządzenia może pokrywać się z układem
użytkownika lub nie. W razie potrzeby współrzędne z układu użytkownika są
automatycznie transformowane do właściwego układu współrzędnych danego
urządzenia.
Jako ciekawostkę można podać, że JAVA nie definiuje wprost metody umożliwiającej
rysowanie piksela, która w innych językach programowania służy często do
tworzenia obrazów. W Javie nie jest to potrzebne. Do tych celów służą liczne metody
drawImage(). Niemniej łatwo skonstruować metodę rysującą piksel np.
drawPixel(Colorc c, int x, int y){
setColor(c);
draawLine(x,y,x,y);
}
Pierwotne wersje AWT definiują kilka obiektów geometrii jak np. Point, Rectangle.
Elementy te są bardzo przydatne dlatego, że, co jest właściwe dla języków
obiektowych, nie definiujemy za każdym razem prostokąta za pomocą atrybutów
opisowych (współrzędnych) lecz przez gotowy obiekt - prostokąt, dla którego znane
są (różne metody) jego liczne właściwości.
Poniższa aplikacja umożliwia sprawdzenie działania prostych metod graficznych:
//Rysunki.java:
import java.awt.event.*;
import java.awt.*;
public class Rysunki extends Frame {
Rysunki () {
super ("Rysunki");
setSize(200, 220);
}
public void paint (Graphics g) {
Insets insets = getInsets();
g.translate (insets.left, insets.top);
g.drawLine (5, 5, 195, 5);
g.drawLine (5, 75, 5, 75);
g.drawRect (25, 10, 50, 75);
g.fillRect (25, 110, 50, 75);
g.drawRoundRect (100, 10, 50, 75, 60, 50);
g.fillRoundRect (100, 110, 50, 75, 60, 50);
g.setColor(Color.red);
g.drawString ("Test grafiki",50, 100);
g.setColor(Color.black);
}
public static void main (String [] args) {
Frame f = new Rysunki ();
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e){
System.out.println("Dziekujemy za prace z programem...");
System.exit(0);
}
});
f.setVisible(true);
}
}// koniec public class Rysunki extends Frame
Wykorzystana w powyższym kodzie metoda translate() zmienia początek układu
współrzędnych przesuwając go do aktywnego pola graficznego (bez ramek).
Java2D API w sposób znaczny rozszerza możliwości graficzne AWT. Po pierwsze
umożliwia zarządzanie i rysowanie elementów graficznych o współrzędnych
zmiennoprzecinkowych (float i double). Własność ta jest niezwykle przydatna dla
różnych aplikacji m.in. dla aplikacji w medycynie (wymiarowanie, planowanie terapii,
projektowanie implantów, itp.). Ta podstawowa zmiana podejścia do rysowania
obiektów graficznych i geometrycznych powoduje powstanie, nowych, licznych klas i
metod. W sposób szczególny należy wyróżnić tutaj sposób rysowania nowych
elementów. Odbywa się to poprzez zastosowanie jednej metody:
Graphics2D g2;
g2.draw(Shape s);
Metoda draw umożliwia narysowanie dowolnego obiektu implementującego interfejs
Shape (kształt). Przykładowo narysowanie linii o współrzędnych typu float można
wykonać w następujący sposób:
Line2D linia = new Line2D.Float(20.f, 10.0f, 100.0f, 10.0f);
g2.draw(linia);
Oczywiście klasa Line2D implementuje interfejs Shape. Java2D wprowadza liczne
klasy w ramach pakietu java.awt.geom, np:
Arc2D.Double
Arc2D.Float
CubicCurve2D.Double
CubicCurve2D.Float
Dimension2D
Ellipse2D.Double
Ellipse2D.Float
GeneralPath
Line2D
Line2D.Double
Line2D.Float
Point2D
Point2D.Double
Point2D.Float
QuadCurve2D.Double
QuadCurve2D.Float
Rectangle2D
Rectangle2D.Double
Rectangle2D.Float
RoundRectangle2D.Double
RoundRectangle2D.Float
W celu skorzystania z tych oraz innych dobrodziejstw jakie wprowadza Java2D
należy skonstruować obiekt graficzny typu Graphics2D. Ponieważ Graphics2D
rozszerza klasę Graphics, to konstrukcja obiektu typu Graphics2D polega na:
Graphics2D g2 = (Graphics2D) g;
gdzie g jest obiektem graficznym otrzymywanym jak omówiono wyżej.
Uwaga! Argumentem metody paint komponentów jest obiekt klasy Graphics a nie
Graphics2D.
Dodatkowe klasy w AWT wspomagające grafikę to BasicStroke oraz TexturePaint.
Pierwsza z nich umożliwia stworzenie właściwości rysowanego obiektu takich jak np.:
szerokość linii, typ linii. Przykładowo ustawienie szerokości linii na 12 punktów
odbywać się może poprzez zastosowanie następującego kodu:
grubaLinia = new BasicStroke(12.0f);
g2.setStroke(grubaLInia);
Klasa TexturePoint umożliwia wypełnienie danego kształtu (Shape) określoną
teksturą. Do dodatkowych zalet grafiki w Java2D należy zaliczyć:
- sterowanie jakością grafiki (np. antyaliasing, interpolacje)
- sterowanie przekształceniami geometrycznymi (przekształcenia sztywne - affiniczne
- klasa AffineTransform),
- sterowanie przeźroczystością elementów graficznych,
- bogate narzędzia do zarządzania czcionkami i rysowania tekstu,
- narzędzia do drukowania grafiki,
- inne.
Przykładowa aplikacja ukazująca proste elementy grafiki w Java2D
//Rysunki2.java:
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.*;
public class Rysunki2 extends Frame {
Rysunki2 () {
super ("Rysunki2");
setSize(200, 220);
}
public void paint (Graphics g) {
Graphics2D g2 = (Graphics2D) g;
Insets insets = getInsets();
g2.translate (insets.left, insets.top);
Line2D linia = new Line2D.Float(20.0f, 20.0f, 180.0f, 20.0f);
g2.draw(linia);
BasicStroke grubaLinia = new BasicStroke(6.0f);
g2.setStroke(grubaLinia);
g2.setColor(Color.red);
Line2D linia2 = new Line2D.Float(20.0f, 180.0f, 180.0f, 180.0f);
g2.draw(linia2);
g2.drawString ("Test grafiki",50, 100);
g2.setColor(Color.black);
}
public static void main (String [] args) {
Frame f = new Rysunki2 ();
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e){
System.out.println("Dziekujemy za prace z programem...");
System.exit(0);
}
});
f.setVisible(true);
}
} // koniec public class Rysunki2 extends Frame
Czcionki
W Javie można korzystać z różnych typów czcionek, które związane są z daną
platformą na jakiej pracuje Maszyna Wirtualna. Dostęp do czcionek odbywa się
poprzez trzy typy nazw: nazwy logiczne czcionek, nazwy czcionek, nazwy rodziny
czcionek. Nazwy logiczne czcionek to nazwy zdefiniowane dla Javy. Możliwe są
następujące nazwy logiczne czcionek w Javie: Dialog, DialogInput, Monospaced,
Serif, SansSerif, oraz Symbol. Nazwy logiczne są odwzorowywane na nazwy
czcionek powiązane z czcionkami dla danego systemu. Odwzorowanie to występuje
w pliku font.properties znajdującego się w katalogu lib podkatalogu jre (Java Runtime
Engine). W pliku tym zdefiniowano również sposób kodowania znaków. Z tego
powodu w zależności od sposobu kodowania i typów wykorzystywanych czcionek
definiowane są różne pliki font.properties z odpowiednim rozszerzeniem np.
font.properties.pl. Przykładowe fragmenty plików opisujący właściwości czcionek to
np.:
//Windows NT ffont.properites:
(...)
sserif.0= Times New Roman,ANSI_CHARSET
serif.1=WingDings,SYMBOL_CHARSET,NEED_CONVERTED
(...)
//Solaris font.properties.pl:
(...)
serif.plain.0 = -linotype-times-memedium-r-normal--*---%d-*-*-p-*-iso8859-1
sserif.1 = -monotype-timesnewroman-regular-r-normal--*-%d-*-*-p-*-iso8859-2
(...)
Różnica pomiędzy nazwą logiczną czcionek w Javie a ich nazwami jest niewielka,
np. nazwa logiczna Serif, nazwa serif. Różnice nazw ukazuje poniższy program:
//Nazwy.java
import java.awt.*;
public class Nazwy {
private Font f;
Nazwy (){
f = new Font("Serif",Font.PLAIN,12);
System.out.println("Oto nazwa logiczna czcionki Serif: "+f.getName());
System.out.println("Oto nazwa czcionki Serif: "+f.getFontName());
f = new Font("SansSerif",Font.PLAIN,12);
System.out.println("Oto nazwa logiczna czcionki SansSerif: " + f.getName());
System.out.println("Oto nazwa czcionki SansSerif: " +f.getFontName());
f = new Font("Dialog",Font.PLAIN,12);
System.out.println("Oto nazwa logiczna czcionki Dialog: "+f.getName());
System.out.println("Oto nazwa czcionki Dialog: "+f.getFontName());
f = new Font("DialogInput",Font.PLAIN,12);
System.out.println("Oto nazwa logiczna czcionki DialogInput: " + f.getName());
System.out.println("Oto nazwa czcionki DialogInput: "+ f.getFontName());
f = new Font("Monospaced",Font.PLAIN,12);
System.out.println("Oto nazwa logiczna czcionki Monospaced: "+ f.getName());
System.out.println("Oto nazwa czcionki Monospaced: "+ f.getFontName());
f = new Font("Symbol",Font.PLAIN,12);
System.out.println("Oto nazwa logiczna czcionki Symbol: "+ f.getName());
System.out.println("Oto nazwa czcionki Symbol: "+f.getFontName());
}
public static void main(String args[]){
Nazwy czcionki = new Nazwy();
}
}// koniec public class Nazwy
W pracy z Javą w celu stworzenia uniwersalnego kodu należy korzystać z nazw
logicznych. Dla danej platformy można oczywiście używać nazw fontów tam
zainstalowanych. W celu uzyskania informacji o zainstalowanych czcionkach na
danej platformie należy posłużyć się metodami klasy GraphicsEnvironment (w
poprzednich wersjach JDK należało korzystać z metody getFontList() klasy Toolkit).
Poniższy program ukazuje zainstalowane czcionki w systemie poprzez podanie nazw
rodzin czcionek oraz poszczególnych nazw.
//Czcionki.java:
import java.awt.*;
import java.awt.event.*;
public class Czcionki extends Frame {
public String s[];
private Font czcionki[];
private Font f;
Czcionki (String nazwa){
super(nazwa);
f = new Font("Verdana",Font.BOLD,12);
s=GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
czcionki=GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts();
}
public static void main(String args[]){
Czcionki okno = new Czcionki("Lista czcionek");
okno.setSize(600,500);
okno.setLayout(new GridLayout(2,1));
List lista1 = new List(1, false);
for (int i=0; i
}
lista1.setFont(okno.f);
okno.add(lista1);
List lista2 = new List(1, false);
for (int i=0; i
}
lista2.setFont(okno.f);
okno.add(lista2);
okno.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e){
System.out.println("Dziekujemy za prace z programem...");
System.exit(0);
}
});
okno.setVisible(true);
}
}// koniec public class Czcionki
Pełny opis czcionek można uzyskać posługując się metodami zdefiniowanymi w
klasie FontMetrics. Podstawowe metody umożliwiają między innymi uzyskanie
informacji o szerokości znaku lub znaków dla wybranej czcionki (charWithd(),
charsWidth()), o wysokości czcionki (getHight()), o odległości między linią bazową
czcionki a wierzchołkiem znaku (getAscent()), o odległości między linią bazową
czcionki a podstawą znaku (getDescent()), itd.
Ustawianie czcionek dla danego komponentu polega na stworzeniu obiektu klasy
Font a następnie na wywołaniu metody komponentu setFont(), np.:
(...)
Coomponent c;
(...)
Font f = new FFont ("Arial", Font.PLAIN, 12);
c.setFont(f);
(...)
Wywołanie konstruktora klasy Font w powyższym przykładzie wymaga określenia
nazwy lub nazwy logicznej czcionki, stylu, oraz rozmiaru czcionki. Styl czcionki może
być określony przez następujące pola klasy Font:
Font.PLAIN tekst zwykły,
Font.ITALIC tekst pochyły (kursywa),
Font.BOLD tekst pogrubiony,
Font.ITALIC + Font.BOLD tekst pogrubiony i pochyły.
Do tej pory omówione zostały podstawowe własności grafiki dostarczanej przez
Java2D z pominięciem obsługi obrazów i kolorów. Zarządzanie kolorami i metody
definiowania, wyświetlania i przetwarzania obrazów są niezwykle istotne z punktu
widzenia aplikacji medycznych. Dlatego teraz te zagadnienia zostaną osobno
omówione.
Kolor
Kolor jest własnością postrzegania promieniowania przez człowieka. Średni
obserwator (definiowany przez różne organizacje normalizacyjne) postrzega
konkretne kolory jako wynik rejestracji promieniowania elektromagnetycznego o
konkretnych długościach fal. Ponieważ system wzrokowy człowieka wykorzystuje trzy
typy receptorów do rejestracji różnych zakresów promieniowania (będziemy dalej
nazywać te zakresy zakresami postrzeganych kolorów) również systemy sztuczne
tworzą podobne modele reprezentacji kolorów. Najbliższym spektralnie systemem
wykorzystywanym dla celów reprezentacji kolorów jest system RGB (posiadający
różne wady np. brak reprezentacji wszystkich barw, różnice kolorów często inne od
tych postrzeganych przez człowieka (nieliniowa zależność postrzegania zmian
intensywności), itp.). System RGB jest zależny od urządzenia, stąd istnieją różne
jego wersje np. CIE RGB, PAL RGB, NTSC RGB, sRGB.
Standardowo przyjmuje się, że kolor na podstawie przestrzenni RGB
definiowany jest jako liniowa kombinacja barw podstawowych:
kolor = r*R+g*G+b*B,
gdzie RBG to kolory (długości fal) podstawowe, a rgb to wartości stymulujące
(tristimulus values) wprowadzające odpowiednie wagi z zakresu (0,1). Zakres wag w
systemach komputerowych jest przeważnie modyfikowany do postaci zakresu
dyskretnych wartości całkowitych o dynamice 8 bitowej czyli 0..255. Stąd też
programista definiuje kolor jako zbiór trzech wag z zakresu 0..255, np., w Javie
Color(0,0,255) daje barwę niebieską. W Javie istnieją predefiniowane stałe statyczne
w klasie Color reprezentujące najczęściej wykorzystywane kolory np. Color.blue.
Domyślną przestrzenią kolorów w Javie jest sRGB , niemniej można
korzystać z innych przestrzenni kolorów np. niezależnej sprzętowo CIE XYZ (CIE -
Commission Internationale de L′Eclairage). Wprowadzono specjalny pakiet
jawa.awt.color obsługujący przestrzenie kolorów oraz standardowe profile urządzeń
bazując na specyfikacji ICC Profile Format Specification, Version 3.4, August 15,
1997, International Color Consortium. Podsumowując należy wskazać najbardziej
istotne klasy w Javie związane z kolorem:
- java.awt.color.ColorSpace
- java.awt.Color
- java.awt.image.ColorModel
Warto zwrócić uwagę, że każda z wymienionych klas występuje w innym miejscu. Do
tej pory omówiona została krótko rola klasy ColorSpace oraz Color. Ostatnia z
prezentowanych klas ColorModel jest związana (jak nazwa pakietu sugeruje) z
tworzeniem i wyświetlaniem obrazów. Klasa ColorModel opisuje bowiem konkretną
metodę odwzorowania wartości piksela na dany kolor. W celu określenia koloru
piksela zgodnie z przyjętą przestrzenią kolorów definiuje się w wybranym modelu
komponenty kolorów i przeźroczystości (np. Red Green Blue Alpha). Metodę
definiowania tych komponentów określa właśnie ColorModel.
Najbardziej znane formy zapisu komponentów to tablice komponentów lub jedna
wartość 32 bitowa, gdzie każde 8 bitów wykorzystywane jest do przechowywania
informacji o danych komponencie. Wykorzystywane podklasy klasy abstrakcyjnej
ColorModel to: ComponentColorModel, IndexColorModel oraz PackedColorModel.
Pierwszy model - ComponentColorModel jest uniwersalną wersją przechowywania
współrzędnych w wybranej przestrzeni kolorów, stąd nadaje się do reprezentacji
dowolnej przestrzeni kolorów. Każda próbka (komponent) w tym modelu jest
przechowywana oddzielnie. Dla każdego więc piksela tworzony jest zbiór
odseparowanych próbek. Ilość próbek na piksel musi być taka sama jak ilość
współrzędnych w przyjętej przestrzeni kolorów. Kolejność występowania próbek jest
definiowana przez przestrzeń kolorów, przykładowo dla przestrzeni RGB -
TYPE_RGB, index 0 oznacza próbkę dla komponentu RED, index 1 dla GREEN,
index 2 dla BLUE. Typ danych dla przechowywania próbek może być 8-bitowy, 16-
bitowy lub 32-bitowy: DataBuffer.TYPE_BYTE, DataBuffer.TYPE_USHORT,
DataBuffer.TYPE_INT. Wywołując konstruktor tej klasy podaje się przede wszystkim
przestrzeń kolorów, oraz tablicę o długości odpowiadającej liczbie komponentów w
danej przestrzeni przechowującą dla każdego komponentu liczbę znaczących bitów.
Kolejny model - IndexColorModel - jest szczególnie wykorzystywany w aplikacjach
medycznych, ponieważ odwołuje się on do koloru nie poprzez zbiór próbek lecz
poprzez wskaźnik do tablicy kolorów. Definiując model kolorów zgodnie z tą klasą
tworzymy tablicę kolorów, do której później odwołujemy się podając wskaźnik. Model
taki jest wykorzystywany w różnych aplikacjach tam, gdzie tworzy się tak zwane
palety kolorów, tablice kolorów (pseudokolorów). Jest to szczególnie ważne tam,
gdzie posiadane wartości do tworzenia obrazu (wartości pikseli) nie reprezentują
promieniowania widzialnego. Mówi się wówczas o tak zwanych sztucznych obrazach
i tablicach pseudo-kolorów (np. obrazy w podczerwieni, obrazy ultradźwiękowe,
obrazy Rtg, itp.). W medycynie prawie wszystkie metody obrazowania wykorzystują
tablice pseudokolorów, a właściwie jedną tablicę - odcieni szarości (najczęściej
R=G=B). Wywołując jeden z konstruktorów klasy IndexColorModel podaje się liczbę
bitów na piksel, rozmiar tablicy oraz konkretne definicje kolorów poprzez zbiór trzech
podstawowych próbek dla RGB.
Ostatni z przedstawianych modeli - PackedColorModel - jest abstrakcyjną klasą w
ramach której kolor jest oznaczany dla danej przestrzeni kolorów poprzez definicję
wszystkich komponentów oddzielnie, spakowanych w jednej liczbie 8, 16 lub 32
bitowej. Podklasą tej abstrakcyjnej klasy jest DirectColorModel. Przy wywołaniu
konstruktora podaje się liczbę bitów na piksel oraz wartości masek dla każdego
komponentu celem wskazania wartości tych komponentów. Obecnie model ten
wykorzystuje się jedynie dla przestrzeni sRGB.
Obrazy
Możliwości pozyskiwania, tworzenia i przetwarzania obrazów w Javie są
bardzo duże. Począwszy od pierwszych wersji w ramach AWT poprzez Java2D a
skończywszy na tworzonej JAI (Java Advanced Imaging API - bardziej elastyczne
definicje obrazów, bogate biblioteki narzędzi np. DFT) język JAVA dostarcza wiele
narzędzi do tworzenia profesjonalnych systemów syntezy, przetwarzania, analizy i
prezentacji obrazów. W początkowych wersjach bibliotek graficznych Javy podstawą pracy z obrazami
była klasa java.awt.Image. Obiekty tej abstrakcyjnej klasy nadrzędnej uzyskiwane są
w sposób zależny od urządzeń. Podstawowa metoda zwracająca obiekt typu Image
(klasa Image jest abstrakcyjna), często wykorzystywana w programach tworzonych w
Javie to getImage(). Metoda ta dla aplikacji jest związana z klasą Toolkit, natomiast
dla appletów z klasą Applet. Wywołanie metody getImage polega albo na podaniu
ścieżki dostępu (jako String) lub lokalizatora URL do obrazu przechowywanego w
formacie GIF lub JPEG.Inną metodą uzyskania obiektu Image jest stworzenie obrazu poprzez wykorzystanie
metody createImage(). Aby uzyskać obiekt typu Image za pomocą tej metody jako
argument należy podać element implementujący interfejs ImageProducer. Obiekt
będący wystąpieniem klasy implementującej ten interfejs jest odpowiedzialny za
stworzenie (produkcję) obrazu jaki jest związany z obiektem typu Image.
Przykładowo w poprzednich metodach getImage() obiekt typu Image jest często
zwracany wcześniej niż stworzony zostanie (np. załadowany z sieci) obraz. Wówczas
metody odwołujące się do obiektu Image zwrócą błąd. Dlatego obiekt typu
ImageProducer informuje klientów (obserwatorów) o zakończonym procesie
tworzenia obrazu związanego z obiektem typu Image. Klientów stanowią obiekty klas
implementujących interfejs ImageObserver, których zachowanie jest ukazane
poprzez jedyną metodę imageUpdate(). Metoda ta zgodnie z informacją od obiektu
ImageProducer żąda odrysowania elementu (np. komponentu graficznego jak np.
panel, applet, itd. - java.awt.Component implementuje interfejs ImageObserver). W
czasie gdy ImageProducer wysyła informację o stanie do obiektu ImageObserver
wysyła również dane do konsumenta czyli obiektu będącego wystąpieniem klasy
implementującej interfejs ImageConsumer. Przykładowe klasy implementujące
interfejs ImageConsumer to java.awt.image.ImageFilter oraz
java.awt.image.PixelGrabber. Ponieważ do obiektu ImageConsumer dostarczane są
wszystkie informacje związane z tworzonym obrazem (wymiary, piksele, model
koloru) obiekt ten może za pomocą swoich metod wykonać wiele operacji na danych.
Przykładowo obiekty klasy ImageFilter umożliwiają wycinanie próbek, filtrację
kolorów, itp. natomiast obiekty klasy PixelGrabber umożliwiają pozyskać część lub
wszystkie próbki z obrazu. Powracając do metody createImage(), której argumentem
jest obiekt ImageProducer, umożliwia ona stworzenie (generację) obrazu na
podstawie posiadanych danych. Konieczne jest jednak stworzenie obiektu typu
ImageProducer, będącego argumentem tej metody. W tym celu wykorzystuje się
klasę implementującą interfejs ImageProducer - MemoryImageSource. Klasa ta
dostarcza szereg konstruktorów, którym podaje się: typ modelu kolorów (ColorModel)
lub wykorzystuje się domyślny RGB, rozmiar tworzonego obrazu, wartości pikseli.
Przykładowo w następujący sposób można wygenerować własny obiekt typu Image:
Obraz.java:
import java.awt.event.*;
import java.awt.image.*;
import java.awt.*;
public class Obraz extends Frame {
Image ob;
Obraz () {
super ("Obraz");
setSize(200, 220);
ob=stworzObraz();
}
public Image stworzObraz(){
int w = 100; //szerokość obrazu
int h = 100; //wysokość obrazu
int pix[] = new int[w * h]; //tablica wartości próbek
int index = 0;
//generacja przykładowego obrazu
for (int y = 0; y < h; y++) {
int red = (y * 255) / (h - 1);
for (int x = 0; x < w; x++) {
int blue = (x * 255) / (w - 1);
pix[index++] = (255 << 24) | (red << 16) | blue;
}
}
Image img = createImage(new MemoryImageSource(w, h, pix, 0, w));
//tworzony jest obraz w RGB o szerokości w, wysokości h,
//na podstawie tablicy próbek pix, bez przesunięcia w tej tablicy z w elementami w linii
return img;
}
public void paint (Graphics g) {
Insets insets = getInsets();
g.translate (insets.left, insets.top);
g.drawImage(ob,50,50,this);
}
public static void main (String [] args) {
Frame f = new Obraz ();
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e){
System.out.println("Dziekujemy za prace z programem...");
System.exit(0);
}
});
f.setVisible(true);
}
}//koniec public class Obraz extends Frame
Mając obiekt typu Image można obraz z nim związany wyświetlić (narysować). W tym
celu należy wykorzystać jedną z metod drawImage(). W najprostszej metodzie
drawImage() podając jako argument obiekt typu Image, współrzędne x,y oraz
obserwatora (ImageObserver, często w ciele klasy komponentu, w którym się rysuje
odwołanie do obserwatora jest wskazaniem aktualnego obiektu komponentu - this)
możemy wyświetlić obrazu w dozwolonym do tego elemencie. Inna wersja metody
drawImage() umożliwia skalowanie wyświetlanego obrazu poprzez podanie
dodatkowych argumentów: szerokość (width) i wysokość (height).
Oprócz poznanych do tej pory operacji związanych z obrazami (stworzenie obiektu
Image, filtracja danych, wyświetlenie i skalowanie) często wykorzystuje się dwie z
kilku istniejących metod klasy Image, a mianowicie Image.getWidth() oraz
Image.getHeight(). Metody te umożliwiają poznanie wymiarów obrazu, co jest często
niezwykle istotne z punktu widzenia ich wyświetlania i przetwarzania. Argumentem
obu metod jest ImageObserver (powiadomienie o dostępnych danych) po to aby
można było uzyskać dane o stworzonym obrazie dla istniejącego już obiektu Image.
Opisane dotąd metody pracy z obrazami są historycznie pierwsze, a ich znaczne
rozszerzenie daje Java2D. Java2D API rozszerza a zarazem zmienia koncepcję
pracy z obrazami w Javie. Podstawową klasą jest w tej koncepcji klasa
BufferedImage, będącą rozszerzeniem klasy Image z dostępnym buforem danych.
Obiekt BufferedImage może być stworzony bezpośrednio w pamięci i użyty do
przechowywania i przetwarzania danych obrazu uzyskanego z pliku lub poprzez
URL.
Obraz BufferedImage może być wyświetlony poprzez użycie obiektów klasy
Graphics2D. Obiekt BufferedImage zawiera dwa istotne obiekty: obiekt danych -
Raster oraz model kolorów ColorModel. Klasa Raster umożliwia zarządzanie danymi
obrazu. Na obiekt tej klasy składają się obiekty DataBuffer oraz SampleModel.
DataBuffer stanowi macierz wartości próbek obrazu, natomiast SampleModel określa
sposób interpretacji tych próbek. Przykładowo dla danego piksela próbki (RGB)
mogą być przechowywane w trzech różnych macierzach (banded interleved) lub w
jednej macierzy w formie przeplatanych próbek ( pixel interleved) dla różnych
komponentów (R1G1B1R2G2B2...). Rolą SampleModel jest określenie jakiej formy
użyto do zapisu danych w macierzach. Najczęściej nie tworzy się bezpośrednio
obiektu Raster lecz wykorzystuje się efekt działania obiektu BufferedImage, który
rozbija Image na Raster oraz ColorModel. Niemniej istnieje możliwość stworzenia
obiektu Raster poprzez stworzenie obiektu WritableRaster i podaniu go jako
argumentu w jednym z konstruktorów klasy BufferedImage. Najbardziej popularne
rozwiązanie tworzące obiekt klasy BufferedImage przedstawia następujący przykład:
URL url= ...
Image img = getToolkit().getImage(url);
try {
// pętla w której czekamy na skonczenie produkcji obrazu dla obiketu img
MediaTracker mt = new MediTracker (this);
mt.adImage(img, 0);
mt.waitForID(00);
} catch (Exceprion e) {}
int iw = img.getWidth(this);
int ih = img.gettHeight(this);
Graphics2D g2 = bi.createGraphics(); //określamy kontekst graficzny dla obiektu
g2.drawImage((img, 0, 0, this);
//wrysowujemy obiekt do bufora - wypełniamy DataBufer obiektu BuferedImage
Dla tak stworzonego obiektu BufferedImage można następnie przeprowadzić liczne
korekcje graficzne, dodać elementy graficzne, zastosować metody przetwarzania
obrazu oraz wyświetlić obraz. Wyświetlenie obrazu obiektu BufferedImage odbywa
się poprzez wykorzystanie metod drawImage() zdefiniowanych dla klasy Graphics2D
(np. g2.drawImage(bi,null,null);). Korekcje graficzne i dodawanie elementów
graficznych polega na rysowaniu w stworzonym kontekście graficznym dla obiektu
BufferedImage. Przetwarzanie obrazu odbywa się głównie poprzez wykorzystanie
klas implementujących interfejs BufferedImageOp a mianowicie:
AffineTransformOp, BandCombineOp, ColorConvertOp, ConvolveOp, LookupOp,
oraz RescaleOp.
Do najczęściej wykorzystywanych operacji należą przekształcenia geometryczne
sztywne (affiniczne) - aplikacja AffineTransformOp - oraz operacje splotu. Te ostatnie
wykorzystuje się do tworzenia filtrów cyfrowych, za pomocą których można zmieniać
jakość obrazu oraz wykrywać elementy obrazu jak np. linie, punkty, itp. Poniższy
przykład ukazuje możliwość implementacji filtru cyfrowego oraz przykładową
operację skalowania.
float[] SHARPEN3x3_3 = { 0.f, -1.f, 0.f, //maska filtru cyfrowego
-1.f, 5.f, -1.f,
0.f, -1.f, 0.f};
BufferedImage bi=...
AffineTransform at = new AffineTransform();
at.scale(2.0, 2.0);
//określenie operacji geometrycznej - skalowanie wsp. 2
BufferedImageOp biop = null;
BufferedImage bimg = new BufferedImage (w,h,BufferedImage. TYPE_INT_RGB);
// tworzymy nowy bufor
Kernel kernel = new Kernel(3,3,SHARPEN3x3_3); //tworzymy kernel - jądro splotu
ConvolveOp cop = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
//definiujemy operację splotu z przepisywaniem wartości krawędzi
cop.filter(bi,bimg); //wykonujemy operację splotu
biop = new AffineTransformOp(at, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
//definiujemy operację skalowania i interpolacji obrazu
g2.drawImage(bimg,biop,x,y); //rysujemy skalowany obraz
Poniższy przykład ukazuje możliwość implementacji filtru dolnoprzepustowego:
//Obraz2.java:
import java.awt.event.*;
import java.awt.image.*;
import java.awt.*;
public class Obraz2 extends Frame {
BufferedImage bi;
boolean stan=false;
Button b1;
Obraz2 () {
super ("Obraz2");
setSize(200, 220);
gUI();
bi=stworzObraz();
}
public void gUI(){
setLayout(new BorderLayout());
Panel kontrola = new Panel();
b1 = new Button("Filtracja");
b1.addActionListener(new B1());
kontrola.add(b1);
add(kontrola,BorderLayout.SOUTH);
}
public BufferedImage stworzObraz(){
int w = 100; //szerokość obrazu
int h = 100; //wysokość obrazu
int pix[] = new int[w * h]; //tablica wartości próbek
int index = 0;
for (int y = 0; y < h; y++) {
int red = (y * 255) / (h - 1);
for (int x = 0; x < w; x++) {
int blue = (x * 255) / (w - 1);
pix[index++] = (255 << 24) | (red << 16) | blue;
}
}
Image img = createImage(new MemoryImageSource(w, h, pix, 0, w));
BufferedImage bimg = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = bimg.createGraphics();
g2.drawImage(img, 0, 0, this);
return bimg;
}
public void paint (Graphics g) {
Insets insets = getInsets();
g.translate (insets.left, insets.top);
Graphics2D g2d = (Graphics2D) g;
g2d.drawImage(bi,null,50,50);
}
class B1 implements ActionListener {
public void actionPerformed(ActionEvent e) {
float lp1=1.0f/9.0f;
float l=4.0f*lp1;
float[] lp = { lp1, lp1, lp1,
lp1, l, lp1,
lp1, lp1, lp1};
if (!stan){
Kernel kernel = new Kernel(3,3,lp);
ConvolveOp cop = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
bi = cop.filter(bi,null);
stan=true;
b1.setLabel("Powrot");
}else
{
bi=stworzObraz();
stan=false;
b1.setLabel("Filtracja");
}
validate();
repaint();
}
}
public static void main (String [] args) {
Frame f = new Obraz2 ();
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e){
System.out.println("Dziekujemy za prace z programem...");
System.exit(0);
}
});
f.setVisible(true);
}
}// koniec public class Obraz2 extends Frame
Dźwięki
Do wersji JDK 1.2 można korzystać z obsługi plików dźwiękowych w
formacie au (8-bit, mono), tylko w obrębie apletu. W celu odtworzenia dźwięku
zapisanego w pliku o formacie au należy wykorzystać jedną z dwóch metod
zdefiniowaną w klasie Applet:
public void play (URL url); gdzie "url" to adrs pliku wrazz z nazwą pliku
lub
public void play (URL url , String nazwa); gdzie "url" to adres pliku, a "nazwa" to nazwa pliku
Przykładowo można zastosować następujący kod:
//Graj.java:
import java.applet.*;
public class Graj extends Applet{
public void init(){
String nazwa = getParameter(muzyka);
if(nazwa != null) play(getDocumentBase(),nazwa);
}
}// koniec public class Graj
//--------------------------------------------
//Graj.html:
(...)
(...)
Powyższy przykład rozpocznie odtwarzanie pliku dźwiękowego o nazwie pobranej z
pola parametru w pliku html, przechowywanego w tym samym katalogu na serwerze
co dokument html. Ponieważ kod zawarto w metodzie init() odtwarzanie dźwięku
będzie jednorazowe. W celu odtwarzania dźwięku w pętli można wykorzystać
interfejs AudioClip. Zdefiniowane w klasie Applet metody getAudioClip() wywoływane
tak samo jak metody play(), zwracają obiekt typu AudioClip (nie jawna definicja
metod interfejsu). Dostępne metody umożliwiają zarówno jednorazowe odtworzenie
dźwięku (play()) jak i odtwarzanie w pętli loop(()) oraz zatrzymanie odtwarzania
dźwięku (stop()).
Java Media API
Pracę z wieloma mediami w Javie w znaczny sposób rozszerzają dodatkowe
bibliteki Javy (Extensions) oznaczane przez javax. W ramach projektu Java Media
API stworzono różne biblioteki ułatwiające pracę z mediami. Do podstawowych
bibliotek należy zaliczyć m.in:
Java 2D dwu-wymiarowa grafika i obrazowanie
Java 3D trój-wymiarowa grafika i obrazowanie
Java Advanced Imaging rozszerzenie Java2D pod względem reprezentacji
i przetwarzania obrazów
Java Media Framework przechwytywanie i odtwarzanie mediów (audiowideo)
,
Java Speech synteza i rozpoznawanie dźwięku,
Java Sound przechwytywanie i odtwarzanie dźwięków.
W wersji JDK 1.3 dostępna jest standardowo biblioteka Java Sound, umożliwiająca
nagrywanie i odtwarzanie dźwięków w różnych formatach.
Strumienie, operacje wejścia-wyjścia
Strumienie
W celu stworzenia uniwersalnego modelu przepływu danych wykorzystuje się
w Javie model strumieni danych. Strumień (stream) jest sekwencją danych, zwykle
bajtów. Pochodzenie i typ sekwencji danych zależny jest od danego środowiska.
Podstawowe typu strumieni to te związane z operacjami wprowadzania danych do
programu (operacje wejścia) i z operacjami wyprowadzania danych poza program
(operacje wyjścia). W Javie do obsługi operacji wejścia stworzono klasę InputStream,
natomiast dla obsługi operacji wyjścia stworzono klasę OutputStream. Strumień
związany jest również z typem obszaru (urządzenia) z/do którego sekwencja danych
przepływa oraz typem danych. Podstawowe obszary (urządzenia) to np. pamięć
operacyjna, dyski (pliki), sieć, drukarka, ekran, itp. Typy danych jakie mogą być
wykorzystywane przy przesyłaniu danych to np. byte, String, Object, itp. Zgodnie z
dystrybucją JDK 1.0 klasy OutputStream oraz InputStream reprezentują strumienie
jako sekwencje bajtów, czyli elementów typu byte. Często jednak zachodzi potrzeba
formatowania danych strumienia. Można to wykonać w Javie korzystając z różnych
klas formatujących dziedziczących z OutputStream i InputStream. Dobrym
przykładem jest obsługa danych tekstowych wyświetlanych na standardowym
urządzeniu wyjścia. Zgodnie z tym co było omawiane wcześniej klasyczną metodą
wydruku tekstu w konsoli jest użycie polecenia System.out.println(); gdzie out jest
obiektem klasy PrintStream, stanowiącej klasę formatującą bajty sekwencji
pochodzących z OutputStream na tekst. Rozpatrując dalej różnorodność obsługi
strumieni w Javie należy wspomnieć o dodatkowych klasach wprowadzonych z
wersją JDK 1.1, a mianowicie klasach Reader oraz Writer. Klasy te stanowią
analogię do klas InputStream oraz OutputStream, niemniej przygotowane są do
obsługi danych tekstowych (String). W omawianych wcześniej rozdziałach użyto już
przykładowej klasy dziedziczącej z klasy Writer, a mianowicie klasy PrintWriter
(zamiast PrintStream). Wprowadzenie dodatkowych klas obsługujących sekwencje
łańcuchów znaków miało na celu ujednolicenie pracy w środowisku Javy z tekstem
zapisywanym kodowanym w Unicodzie (2 bajty na znak). Dodatkowe, oddzielne
klasy strumieni to klasa StreamTokenizer dzieląca strumień tekstowy na leksemy
oraz klasa RandomAccessFile obsługująca pliki zawierające rekordy o znanych
rozmiarach, tak że można dowolnie poruszać się w obrębie rekordów i je
modyfikować. Ważnym zagadnieniem związanym ze strumieniami jest możliwość
zapisu obiektu jako sekwencji bajtów i przesłanie go do programu (metody)
działającej zdalnie. Efekt ten jest uzyskiwany poprzez zastosowanie mechanizmu
serializacji i wykorzystania klas ObjectInputStream oraz ObjectOutputStream.Wszystkie omawiane klasy obsługujące różne typy strumieni są zdefiniowane w
pakiecie java.io.
Wykorzystanie strumieni jest powszechne w tworzeniu programów tak więc warto
bliżej zapoznać się z podstawowymi klasami je obsługującymi.
Standardowe obsługa wejścia-wyjścia - klasy InputStream oraz
OutputStream
Obsługa wejścia klasa InputStream
Klasa ta jest klasą abstrakcyjną i zawiera podstawowe metody umożliwiające
odczytywanie i kontrolę bajtów ze strumienia. Ponieważ jest to klasa abstrakcyjna nie
tworzy się dynamicznie obiektu tej klasy, lecz można go uzyskać poprzez odwołanie
się do standardowego wejścia zainicjowanego zawsze w polu in klasy System, czyli
System.in. Inne możliwości uzyskania obiektu klasy InputStream to wywołanie
metod zwracających referencję do obiektu tego typu np.: metoda getInputStream()
zdefiniowana w klasie Socket. Jedyną metodą abstrakcyjną (czyniącą z tej klasy
klasę abstrakcyjną ) jest metoda read() oznaczająca czytanie kolejnego bajtu ze
strumienia wejścia. Pozostałe metody umożliwiają:
- odczyt bajtów do zdefiniowanej tablicy:
int read(byte b[]);
int read(bute b[],int offset, int length);
- pominięcie określonej liczby bajtów w odczycie:
long skip (long n);
- kontrolę stanu strumienia (czy są dane):
int available ();
- tworzenie markerów:
boolean marSupported(); kontrola czy tworzenie markerów jest możliwe
synchrnized void mark(int readlimit);
synchronized void reste()
- zamknięcie strumienia:
void close ();
Prawie wszystkie metody ( poza markSupported() oraz mark()) mogą wygenerować
wyjątek, który musi być zadeklarowany lub obsługiwany w kodzie programu.
Podstawową klasą wyjątków jest tutaj klasa IOException.
Pokazane wyżej metody read() blokują dostęp tak długo, jak dane są dostępne lub
wystąpi koniec pliku albo wygenerowany zostanie wyjątek.
Klasy dziedziczące z klasy InputStream to:
• ByteArrayInputStream - definiuje pole bufora bajtów,
• FileInputStream - umożliwia odczyt pliku (strumień z pliku),
• FilterInputStream - praktycznie kopiuje funkcjonalność InputStream celem jej
wykorzystania w klasach dziedziczących z FilterInputStream wprowadzającą
dodatkowe narzędzia do obsługi sekwencji bajtów:
BufferedInputStream, CheckedInputStream, DataInputStream, DigestInputStream,
InflaterInputStream, LineNumberInputStream, ProgressMonitorInputStream,
PushbackInputStream
np. DataInputStream umożliwia programowi odczyt danych zgodnie z podstawowymi
formatami zdefiniowanymi w Javie (char, int, long, double, itp.).
•ObjectInputStream - dokonuje rekonstrukcji obiektu z sekwencji bajtów
(stworzonej w wyniku serializacji i zapisu metodą writeObject() klasy
ObjectOutputStream),
• PipedInputStream - dokonuje przepływu danych do odpowiadającemu obiektowi
klasy PipedOutputStream, który jest podawany jako argument konstruktora klasy
PipedInputStream,
• SequenceInputStream, dokonuje logicznej koncentracji strumieni w jeden,
StringBufferInputStream - tworzy strumień z podanego łańcucha znaków (obiektu
String) - klasa ta jest oznaczona w JDK 1.2 jako deprecated ponieważ nie
dokonuje właściwej konwersji znaków na bajty. Postuluje się jej zastąpienie klasą
StringReader.
Obsługa wejścia klasa OutputStream
W podobny sposób, niemniej dotyczący obsługi wyjścia, definiowane są klasy
dziedziczące z klasy OutputStream. Klasa ta jest również klasą abstrakcyjną z jedyną
abstrakcyjną metodą write() zapisująca kolejny bajt do strumienia. Podstawowe
metody tej klasy to:
void close() - zamknięcie strumienia,
void flush() - przesuwa buforowane dane do strumienia,
void write(byte[] b, int off, int len),
void write(byte[] b) - zapisują dane z tablicy b do strumienia wyjścia.
Klasy dziedziczące z klasy OutputStream to:
•ByteArrayOutputStream,
• FileOutputStream,
• FilterOutputStream,
• ObjectOutputStream,
• PipedOutputStream.
Klasy te obsługują strumień wyjściowy a ich funkcjonalność jest analogiczna do
omawianych wyżej wersji obsługujących wejście.
//Echo.java
import java.io.*;
public class Echo{
public static void main(String args[]){
byte b[] = new byte[100];
try{
System.in.read(b);
System.out.write(b);
System.out.flush();
} catch (IOException ioe){
System.out.println("Błąd wejścia-wyjścia");
}
}
}//koniec public class Echo
Powyższy program ukazuje proste zastosowanie strumieni. Początkowo
wykorzystywany jest istniejący strumień wejścia System.in w celu wczytania danych
ze standardowego urządzenia wejścia (klawiatura). Dane są wczytywane aż
wciśnięty zostanie klawisz Enter (koniec danych). Wczytane znaki są zapisywane do
bufora b, który to jest następnie wykorzystywany do pobrania danych celem wysłania
ich do strumienia wyjścia. Przyjętym w powyższym przykładzie strumień wyjścia to
System.out. Warto zauważyć, że wykorzystano metody write() oraz flush() do zapisu
danych. W efekcie działania programu otrzymujemy echo (powtórzenie) napisu
wprowadzonego z klawiatury.
Kolejny przykład ukazuje możliwość dostępu do pliku.
//ZapiszPlany.java:
import java.io.*;
class Plany implements Serializable{
private int liczbaLegionow;
private int liczbaDzial;
private String haslo;
public void ustaw(int lL, int lD, String h){
this.liczbaLegionow=lL;
this.liczbaDzial=lD;
this.haslo=h;
}
public void wyswietl(){
System.out.println("Liczba legionów = "+liczbaLegionow);
System.out.println("Liczba dział = "+liczbaDzial);
System.out.println("Hasło dostępu = "+haslo);
}
}// koniec class Plany
class Nadawca extends Thread{
private String plikDanych;
ZapiszPlany zp;
Nadawca(String s, ZapiszPlany zp){
this.plikDanych=s;
this.zp=zp;
setName("Nadawca");
}
public void run(){
byte b[] = new byte[100];
try{
System.out.println("Podaj hasło");
System.in.read(b);
String s= new String(b);
System.out.println("Zapisuję do pliku");
Plany p = new Plany();
p.ustaw(1,2,s);
ObjectOutputStream o = new ObjectOutputStream(new
FileOutputStream(plikDanych));
o.writeObject(p);
o.flush();
o.close();
} catch (IOException ioe){
System.out.println("Błąd wejścia-wyjścia");
}
zp.ustaw();
}
}// koniec class Nadawca
class Odbiorca extends Thread{
private String plikDanych;
ZapiszPlany zp;
Odbiorca(String s, ZapiszPlany zp){
this.plikDanych=s;
this.zp=zp;
setName("Odbiorca");
}
public void run(){
while( (!(this.isInterrupted())) && (zp.pobierz()) ){
}
try{
System.out.println("Odczyt");
ObjectInputStream i = new ObjectInputStream(new FileInputStream(plikDanych));
Plany p = (Plany) i.readObject();
p.wyswietl();
} catch (Exception e){
System.out.println("Błąd wejścia-wyjścia lub brak klasy Plany");
}
}
}// koniec class Odbiorca
public class ZapiszPlany{
static boolean czekaj=true;
synchronized void ustaw(){
czekaj=false;
}
synchronized boolean pobierz(){
return czekaj;
}
public static void main (String args[]){
ZapiszPlany z = new ZapiszPlany();
Nadawca n = new Nadawca("plany.txt",z);
Odbiorca o = new Odbiorca("plany.txt",z);
o.start();
n.start();
}
}//koniec public class ZapiszPlany
Powyższy program demonstruje zastosowanie obsługi strumieni dostępu do plików
oraz wykorzystanie serializacji obiektów. Stworzono w kodzie cztery klasy. Pierwsza
zawiera definicje zbioru pól (dane) oraz metod dostępu do nich. Klasa implementuje
interfejs Serializable w celu umożliwienia zapisu obiektu do strumienia. Kolejne dwie
klasy opisują zachowanie wątków generującego zapis danych i odczytującego zapis
danych. W klasie Nadawca zawarto kod wczytujący zestaw znaków w klawiatury,
który jest przypisywany do pola obiektu klasy Plany. Następnie tworzony jest
strumień dostępu do pliku o podanej nazwie sformatowany do przesyłania obiektów.
Zapis do bufora i przesłanie do strumienia odbywa się poprzez metody writeObject()
oraz flush(). Po zakończeniu procesu zapisu danych do pliku powiadamiany jest
drugi wątek, opisany w klasie Odbiorca. Zapisany obiekt jest odczytywany poprzez
metodę readObject() a następnie jest wykonywana jedna z metod obiektu
wyświetlająca stan danych. Jak widać przesłanie przez strumień obiektów jest
ciekawym rozwiązaniem i umożliwia tworzenie rozproszonych aplikacji.
Obsługa plików
Dostęp do plików zaprezentowany wcześniej wykorzystywał klasy
FileInputStream i FileOutputStream. Konstruktory tych klas umożliwiają inicjacje
strumienia poprzez podanie jako argumentu albo nazwy pliku poprzez obiekt typu
String lub poprzez podanie nazwy logicznej reprezentowanej przez obiekt klasy File.
Klasa File opisuje abstrakcyjną reprezentację ścieżek dostępu do plików i katalogów.
Ścieżka dostępu do pliku może być sklasyfikowana ze względu na jej zasięg lub ze
względu na środowisko dla którego jest zdefiniowana. W pierwszym przypadku dzieli
się ścieżki dostępu na absolutne i relatywne. Absolutne to te, które podają adres do
pliku względem głównego korzenia systemu plików danego środowiska. Relatywne to
te, które adresują plik względem katalogu bieżącego. Druga klasyfikacja rozróżnia
ścieżki dostępu pod względem środowiska dla którego jest ona zdefiniowana, co w
praktyce dzieli ścieżki dostępu na te zdefiniowane dla systemów opartych na UNIX i
na te zdefiniowane dla systemów opartych na MS Windows (własność systemu o
nazwie file.separator). Przykłady:
a) absolutna ścieżka dostępu:
UNIX: /utl/software/java/projekty
MS Windows: c:\utl\softare\java\projekty
b) relatywna ścieżka dostępu:
UNIX: java/projekty
MS Windows: java\projekty.
Tworząc obiekt klasy File dokonywana jest konwersja łańcucha znaków na
abstrakcyjną ścieżkę dostępu do pliku (abstrakcyjna ścieżka dostępu do pliku jest
tworzona według określonych reguł podanych w dokumentacji API). Metody klasy
File umożliwiają liczne kontrolę podanej ścieżki i plików (np. isFile(), isDirectory(),
isHidden, canRead(), itp.) oraz dokonywania konwersji (np. getPath(), getParent(),
getName(), toURL(), itp.) i wykonywania prostych operacji (list() mkdir(), itp.).
Uwaga! Należy pamiętać, że zapis tekstowy ścieżki dostępu dla środowiska MS
Windows musi zawierać podwójny separator, gdyż pojedynczy znak umieszczony w
inicjacji łańcucha znaków oznacz początek kodu ucieczki, np.
"c:\\java\\kurs\\wyklad\\np".
// PobierzDane.java:
import java.io.*;
public class PobierzDane{
public static void main(String args[]){
File f = new File("DANE1");
if (f.mkdir()) {
File g = new File (".");
String s[] = g.list();
for (int i =0; i
}
} else {
System.out.println("Błąd operacji I/O");
}
}
}//koniec public class PobierzDane
Program PobierzDane ukazuje ciekawą własność obiektu File. Otóż stworzenie
obiektu tej klasy nie oznacza otwarcia strumienia czy stworzenia uchwytu do pliku.
Obiekt klasy File może więc być stworzony praktycznie dla dowolnej nazwy ścieżki.
W prezentowanym przykładzie utworzono katalog o nazwie DANE1, a następnie
dokonano wydruku plików i katalogów zawartych pod aktualnym adresem ścieżki
(pod tym, z którego wywołano program java).
Pracę z plikami o swobodnym dostępie ułatwia zastosowanie innej klasy obsługującej
operacje wejścia-wyjścia, a mianowicie klasy RandomAccessFile. Zdefiniowana jest
w klasie obsługa plików zawierających rekordy o znanych rozmiarach, tak że można
dowolnie poruszać się w obrębie rekordów i je modyfikować. Dane w pliku są
interpretowane jako dane w macierzy do której dostęp jest możliwy poprzez
odpowiednie ustawienie głowicy czytającej czy zapisującej dane. Zdefiniowano w tej
klasie metody przesuwania głowicy (getFilePointer(), seek()) oraz szereg metod
czytania i zapisu różnych typów danych.
Obsługa strumieni tekstu
W związku z problemem wynikającym z konwersji znaków Javy (Unicode) na
bajty i odwrotnie występujących we wczesnych (JDK 1.0) realizacjach klas obsługi
strumieni począwszy od wersji JDK1.1 wprowadzono dodatkowe klasy Reader i
Writer. Obie abstrakcyjne klasy są analogicznie skonstruowane (dziedziczenie z
klasy Object i deklaracja metod) jak klasy InputStream oraz OutputStream.
Dziedziczące z nich klasy umożliwiają prostą i formatowaną obsługę sekwencji
tekstu:
Reader:
BufferedReader buforuje otrzymywany tekst,
LineNumberReader przechowuje dodatkowo informacje o numerze
linii
CharArrayReader wprowadza bufor znaków do odczytu,
FilterReader klasa abstrakcyjna formatowania danych tekstowych,
PushbackReader przygotowuje dane odesłania do strumienia,
InputStreamReader czyta bajty zamieniające je na tekst według podanego
systemu kodowania znaków,
FileReader odczyt danych tekstowych z pliku dla domyślnego
systemu kodowania znaków, poprzez podanie ścieżki zależnej
systemowo (String) lub abstrakcyjnej (File)
PipedReader obsługa potoku (związanie z klasą odczytującą),
StringReader obsługa strumienia pochodzącego od obiektu klasy String.
Konstrukcja klasy Writer jest analogiczna do Reader, z tym, że definiowany jest zapis
zamiast odczytu. Podobnie wygląda struktura klas dziedziczących z Writer:
Writer:
BufferedWriter,
CharArrayWriter,
FilterWriter,
OutputStreamWriter,
FileWriter
PipedWriter,
PrintWriter, - formatowanie danych do postaci tekstu (analogiczna do
PrintStream)
StringWriter
Odczyt danych odbywa się poprzez zastosowanie metod read() lub readLine()
natomiast zapis danych do strumienia poprzez wykorzystanie metod write().
Zastosowanie klas dziedziczących z Reader i Writer ma dwie zasadnicze zalety:
właściwa konwersja bajtów na znaki w Unicodzie i odwrotnie oraz możliwość
zdefiniowania systemu kodowania znaków. W celu zobrazowania sposobu
korzystania z tych klas warto zapoznać się z następującym przykładem:
//Czytaj.java:
import java.io.*;
/*Plik plik.txt powinien zawierać polskie znaki wprowadzone w kodzie Cp1250: "ąśęółżźćń"; */
public class Czytaj{
public static void main(String args[]){
String s;
char zn[] = new char[9];
try{
InputStreamReader r = new InputStreamReader((new FileInputStream("plik.txt")), "Cp1250");
r.read(zn,0,zn.length);
s = new String(zn);
System.out.println(s);
OutputStreamWriter o = new OutputStreamWriter((new FileOutputStream("plik1.txt")),"Cp852");
o.write(s,0,s.length());
o.flush();
} catch (Exception e){}
}
}// koniec public class Czytaj
Prezentowany program tworzy dwa strumienie: jeden wejścia czytający plik tekstowy
zapisany według strony tekstowej Cp1250 (Windows PL) i drugi wyjścia zapisujący
nowy plik wyjściowy tekstem według strony kodowej Cp852 (DOS PL). Oczywiście
nazwy plików jak i nazwy stron kodowych można zmieniać dla potrzeb ćwiczeń i w
ten sposób dokonywać konwersji plików z np. Cp1250 na ISO8859-2. W celu
weryfikacji działania powyższego programu należy otworzyć plik o nazwie plik.txt w
edytorze obsługującym Cp1250 a plik o nazwie plik1.txt w edytorze obsługującym
Cp852. W przypadku gdy nie ma konieczności zmiany systemu kodowania znaków
warto wykorzystywać klasy FileReader oraz FileWriter zamiast InputStreamReader i
OutputStreamWriter. Dla poprawienia efektywności pracy istotne jest również
buforowanie czytanych danych co w rezultacie prowadzi do następującego
wywołania obiektu:
BuferedReader br = new BuferedReader(new FileReader("plik.txt");
Dzielenie strumienia klasa StreamTokenizer
Na zakończenie omawiania klas związanych z obsługą strumieni warto
zapoznać się z klasą StreamTokenizer, dzieląca strumień tekstowy na leksemy.
Klasa ta daje więc swoistą funkcjonalność wykrywanie elementów strumienia i
umieszczania ich w tablicy. Wskazując znak oddzielający leksemy można dokonać
przeformatowania przesłanego tekstu (np. podzielić ścieżkę dostępu, dokonać
detekcji liczb w tekście, itp.). Pobranie leksemu z tablicy odbywa się poprzez
wywołanie metody nextToken(). Poniższy przykład ilustruje stosowanie klasy
StreamTokenizer:
//FormatujStrumien.java:
import java.io.*;
public class FormatujStrumien{
public static void main(String args[]){
System.out.println("Podaj tekst zawierający znak : .");
Reader r = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer st = new StreamTokenizer(r);
st.ordinaryChar('.');
st.ordinaryChar('-');
st.ordinaryChar('A');
try{
while (st.nextToken() != StreamTokenizer.TT_EOF){
if (st.ttype==StreamTokenizer.TT_WORD){
System.out.println(new String(st.sval));
}
}
}catch (IOException ioe){
System.out.println("Błąd operacji I/O");
}
}
}//koniec public class FormatujStrumien
W powyższym przykładzie stworzono obiekt klasy Reader na podstawie
standardowego strumienia wejścia, który jest następnie wykorzystany przy
inicjowaniu obiektu klasy StreamTokenizer. Ponieważ w zbiorze domyślnych znaków
dzielących strumień nie ma znaków . oraz - dodano te znaki za pomocą metody
ordinaryChar(). Dodatkowo ustawiono znak A jako znak dzielący strumień.
Następnie w bloku instrukcji warunkowej uruchomiona jest pętla działająca tak długo
aż nie wystąpi koniec pliku (aż nie wciśnięty zostanie kod CTRL-Z a następnie
Enter). W pętli pobierany jest kolejny wczytywany element, sprawdzany jest jego typ
i jeśli jest to słowo (tekst) to drukowany jest na ekranie tekst będący wartością pola
sval obiektu klasy StreamTokenizer. Obsługa programu polega na wprowadzaniu
tekstu ze znakami go dzielącymi i kończeniu linii wciskając Enter. W ten sposób linia
po linii można analizować wprowadzany tekst. Koniec pracy programu jest
wymuszany sekwencją końca pliku CTRL-Z i Enter.
Warto zauważyć, że istnieje klasa StringTokenizer o podobnym działaniu, której
argumentem nie jest jednak strumień a obiekt klasy String.
Strumienie poza java.io
W JDK istnieją jeszcze inne strumienie zdefiniowane poza pakietem java.io.
Przykładowo w pakiecie java.util.zip zdefiniowano szereg klas strumieni
obsługujących kompresję w formie ZIP i GZIP. Podstawowe klasy strumieni tam
przechowywane to:
CheckedInputStream
CheckedOutputStream
DeflaterOutputStream
GZIPInputStream
GZIPOutputStream
InflaterInputStream
ZipInputStream
ZipOutputStream
Przykładowo w celu dokonania kompresji pliku metodą GZIP można zastosować
następujący kod:
//Kompresja.java:
import java.io.*;
import java.util.zip.*;
public class Kompresja{
public static void main(String args[]){
String s;
byte b[] = new byte[100];
for (int i=0; i<100; i++){
b[i]=(byte) (i/10);
}
try{
FileOutputStream o = new FileOutputStream("plik2.txt");
o.write(b);
o.flush();
o.close();
FileOutputStream fos = new FileOutputStream("plik2.gz");
GZIPOutputStream z = new GZIPOutputStream(new BufferedOutputStream(fos));
z.write(b,0,b.length);
z.close();
} catch (Exception e){}
}
}// koniec public class Kompresja
W prezentowanym kodzie tworzona jest tablica bajtów wypełniana kolejnymi
wartościami od 1 do 10. Tablica ta jest następnie przesyłana do strumieni
wyjściowych raz bezpośrednio do pliku, drugi raz poprzez kompresję metodą GZIP.
W wyniku działania programu uzyskuje się dwa pliki: bez kompresji plik2.txt i z
kompresją plik2.gz.
W pakietach standardowych jak i w pakietach będących rozszerzeniem bibliotek Javy
można znaleźć jeszcze szereg innych strumieni związanych z przesyłaniem danych
(np. w kryptografii czy w obsłudze portów).
Ze względu na liczne potrzeby wykorzystywania portów szeregowych i równoległych
komputera Sun opracował pakiet rozszerzenia Javy o nazwie javax.comm. Pakiet ten
umożliwia obsługę portów poprzez strumienie. Poniższy przykład ukazuje próbę
zapisu do portu szeregowego.
//ZapiszPort.java:
import java.io.*;
import java.util.*;
import javax.comm.*;
public class ZapiszPort {
static SerialPort port;
static CommPortIdentifier id;
static Enumeration info;
static String dane = "Tu Czerwona Jarzębina - odbiór \n";
static OutputStream os;
public static void main(String args[]) {
info = CommPortIdentifier.getPortIdentifiers();
while (info.hasMoreElements()) {
id = (CommPortIdentifier) info.nextElement();
if (id.getPortType() == CommPortIdentifier.PORT_SERIAL) {
if (id.getName().equals("COM1")) {
try {
port = (SerialPort) id.open("ZapiszPort", 2000);
} catch (PortInUseException e) {}
try {
os = port.getOutputStream();
} catch (IOException ioe) {}
try {
port.setSerialPortParams(9600,
SerialPort.DATABITS_8,
SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE);
} catch (UnsupportedCommOperationException ue) {}
try {
os.write(dane.getBytes());
} catch (IOException e) {}
}
}
}
}
}// koniec public class ZapiszPort
Integracja Javy z innymi językami - JNI.
Programowanie sieciowe
Integracja Javy z innymi językami - Java Native Interface (JNI)
Tworząc programy w środowisku języka programowania Java napotyka się
czasami na ograniczenia związane z dostępem do specyficznych dla danej platformy
cech. Konieczne jest wówczas zastosowanie narzędzi obsługujących te cechy a
następnie zaimplementowanie tych narządzi w kodzie programu tworzonego w Javie.
Operacja taka jest możliwa poprzez wykorzystanie swoistego interfejsu pomiędzy
kodem w Javie a kodem programu stworzonego w innym środowisku, np. C lub C++.
Co więcej wykorzystanie istniejących już funkcji (napisanych wcześniej w innych
językach programowania niż Java) może znacznie uprościć tworzenie nowej aplikacji
w Javie, szczególnie wtedy gdy liczy się czas. Interfejs umożliwiający to specyficzne
połączenie kodów został nazwany Java Native Interface, lub w skrócie JNI. Każda
funkcja napisana w innym języku niż Java a implementowana bezpośrednio w kodzie
Javy nosi nazwę metody rodzimej (native method) i wymaga jawnej,
sformalizowanej deklaracji. Metody rodzime mogą wykorzystywać obiekty Javy tak,
jak to czynią metody tworzone w Javie, a więc tworzyć obiekty, używać je oraz
modyfikować. Aby funkcjonalność metod rodzimych była pełna metody te mogą
wywoływać metody tworzone w Javie, przekazywać im parametry i pobierać wartości
lub referencje zwracane przez te metody. Możliwa jest również obsługa wyjątków
metod rodzimych.
Czym jest więc JNI? JNI to interfejs zawierający:
• plik nagłówkowy środowiska rodzimego (np. plik jni.h dla środowiska C);
• generator pliku nagłówkowego metody rodzimej (np. javah -jni);
• formalizację deklaracji metody rodzimej,
• definicję i rzutowanie typów danych,
• zbiór metod (funkcji) umożliwiających wymianę danych i ustawianie stanów (np.
wyjątków, monitora, itp.).
Implementacja JNI polega najprościej na wykonaniu następujących działań:
1. stworzenie programu w Javie zawierającego deklarację metody rodzimej (native);
2. kompilacja programu;
3. generacja pliku nagłówkowego środowiska rodzimego dla klasy stworzonego
programu (javah -jni);
4. stworzenie implementacji metody rodzimej z wykorzystaniem plików
nagłówkowych interfejsu JNI i klasy stworzonego programu ;
5. kompilacja metody rodzimej i umieszczenie jej w bibliotece;
6. uruchomienie programu korzystającego z metody rodzimej poprzez ładowanie
biblioteki.
Obsługa metod rodzimych w kodzie Javy
Pierwszym krokiem w implementacji interfejsu JNI jest stworzenie kodu w
Javie obsługującego metody rodzime. Najprostsza struktura obsługi może wyglądać
następująco:
//Informacje.java:
class Informacje{
//deklaracja metody rodzimej
public native int infoSystemu(String parametr);
//ładowanie biblioteki zawierającej implementację metody rodzimej
static{
System.loadLibrary(sysinfo);
}
//wykorzystanie metody rodzimej
public static void main(String args[]){
Informacje i = new Informacje();
int status = i.infoSystemu(CZAS);
}
}// koniec class Informacje
Powyższy szkic stosowania metod rodzimych zawiera trzy bloki. Pierwszy z nich
deklaruje metodę rodzimą, która różni się od pozostałych metod tym, że używany jest
specyfikator native w deklaracji. Drugi blok to kod statyczny ładowany przy
interpretowaniu (kompilacji) kodu bajtów pobierający bibliotekę przechowujący
realizację metody rodzimej. Ostatni blok to zastosowanie metody rodzimej.
Zadeklarowanie metody jako native oznacza, że kompilator ma uznać daną metodę
jako rodzimą zdefiniowaną i zaimplementowaną poza Javą. Podana w deklaracji
metody rodzimej nazwa jest odwzorowywana później na nazwę funkcji w kodzie
rodzimym zgodnie z regułą:
nazwa -> Java_NazwaPakietu_NazwaKlasy_nazwa, czyli np.
infoSystemu -> Java_Informacje_infoSystemu, (brak nazwy pakietu, gdyż klasa
Informacje zawiera się w pakiecie domyślnym, który nie posiada nazwy).
Wykorzystanie bibliotek, w których znajduje się realizacja metod rodzimych wymaga,
aby biblioteki te były dostępne dla uruchamianego programu, tzn. musi być
odpowiednio ustalona ścieżka dostępu.
Kompilacja i generacja plików nagłówkowych
Kompilacja kodu Javy wykorzystującego metody rodzime odbywa się tak samo
jak dla czystego kodu Javy, np. javac -g Informacje.java.
Nowością jest natomiast wygenerowanie pliku nagłówkowego, jaki zawarty będzie w
kodzie metody rodzimej. Generacja taka wymaga zastosowania narzędzia javah,
które generuje plik nagłówkowy o nazwie takiej jak podana nazwa klasy z
rozszerzeniem .h (header - nagłówek) tworzony w tym samym katalogu gdzie
znajduje się plik klasy programu. Przykładowo wywołanie polecenia:
javah -jini Informacje
spowoduje wygenerowanie następującego pliku nagłówkowego:
//Informacje.h:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class Informacje */
#ifndef _Included_Informacje
#define _Included_Informacje
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Informacje
* Method: infoSystemu
* Signature: (Ljava/lang/String;)I
*/
JNIEXPORT jint JNICALL Java_Informacje_infoSystemu
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
Jak widać jest to nagłówek dla kodu metody rodzimej tworzonego w C/C++.
Zasadniczo można tworzyć implementacje metod rodzimych w innych językach ale to
wymaga indywidualnego podejścia (konwersji typów i nazw), stąd zaleca się
korzystanie jedynie z C/C++ do obsługi metod rodzimych. W zasadniczej części
nagłówka (poza analizą składni dla C czy C++) zawarto opis metody używając do
tego komentarza zawierającego nazwę klasy, w ciele której zadeklarowano metodę
rodzimą, nazwę metody rodzimej w kodzie Javy oraz podpis (sygnatura) metody.
Sygnatura metody ma format (typy-argumentów)typy-zwracane; gdzie poszczególne
typy są reprezentowane przez swoje sygnatury, i tak:
sygnatura | znaczenie typu w Javie
Z | boolean
B | byte
C | char
S | short
I | int
J | long
F | float
D | double
Lpełna-nazwa-klasy | pełna nazwa klasy
[typ | typ[]
W powyższym przykładzie pliku nagłówkowego widnieje informacja, że metoda
zawiera argument o sygnaturze Ljava/lang/String czyli obiekt klasy String oraz
zwraca wartość typu o sygnaturze I czyli typu int. Można określić sygnatury typów
argumentów i wartości zwracanych metod poprzez użycie narzędzia de-asemblacji
kodu a mianowicie javap:
javap -s -p Informacje
co wygeneruje:
Compiled from Informacje.java
class Informacje extends java.lang.Object {
static {};
/* ()V */
Informacje();
/* ()V */
public native int infoSystemu(java.lang.String);
/* (Ljava/lang/String;)I */
public static void main(java.lang.String[]);
/* ([Ljava/lang/String;)V */
}
W powyższym wydruku w opcji komentarza zawarto sygnatury typów.
Po opisie metody rodzimej następuje deklaracja metody dla środowiska rodzimego
czyli C/C++. Deklaracja ta oprócz omówionej już nazwy zawiera szereg nowych
elementów i tak:
JNIEXPORT i JNICALL - oznaczenie funkcji eksportowanych z bibliotek (jest to
odniesienie się do specyfikacji deklaracji funkcji eksportowanych, JNIEXPORT oraz
JNICALL są zdefiniowane w jni_md.h);
jint lub inny typ - oznaczenie typu elementu zwracanego przez funkcję, np.
typ w Javie : typ rodzimy : rozmiar, typ dla C/C++ (Win32)
boolean : jboolean : 8 : unsigned unsigned char
byte : jbyte : 8 : signed char
char : jchar : 16 : unsigned unsigned short
short : jshort : 16 : short
int : jint : 32 : long
long : jlong : 64 : __int64
float : jfloat : 32 : float
double : jdouble : 64 : double
void : void : void
JNIEnv* wskaźnik interfejsu JNI zorganizowany jako tablica funkcji JNI o konkretnej
lokalizacji. Metoda rodzima wykorzystuje funkcje poprzez odwołanie się do
wskaźnika JNIEnv*. Przykładowo można pobrać rozmiar macierzy wykorzystując
funkcję GetArrayLength() poprzez wskaźnik JNIEnv*:
(...) JNIEnv *env (...) jintArray macierz (...)
jsize rozmiar = (*env)->GetArrayLength(env, macierz);
jobject to kolejny element deklaracji funkcji w JNI. Argument tego typu stanowi
referencję do bieżącego obiektu; jest odpowiednikiem this w Javie.
Warto tu zauważyć, że odpowiednikiem metody Javy zadeklarowanej jako native
jest funkcja zawierająca co najmniej dwa argumenty, nawet wówczas gdy metoda nie
zawiera żadnego.
jstring lub inne typy argumentów - kolejne argumenty funkcji (argumenty metody
rodzimej).
Implementacja metody rodzimej - funkcja a biblioteka
Kolejny krok stosowania funkcji w kodzie Javy to stworzenie tych funkcji lub
ich adaptacja do formy wymaganej przez JNI. Deklaracja realizowanej funkcji musi
być taka sama jak założona (wygenerowana) w pliku nagłówkowym. Następujący
przykład ukazuję przykładową realizację metody rodzimej deklarowanej we
wcześniejszych przykładach:
//informacje.cpp
#include "jni.h"
#include
#include
#include "Informacje.h"
JNIEXPORT jint JNICALL Java_Informacje_infoSystemu(JNIEnv *env, jobject o, jstring str){
struct time t;
const char *s=(env)->GetStringUTFChars(str,0);
printf("Obsługiwana aktualnie opcja to: %s\n",s);
if ((((*s=='C')&&(*(s+1)=='Z'))&&(*(s+2)=='A'))&&(*(s+3)=='S')){
gettime(&t);
printf("Bieżący czas to: %2d:%02d:%02d.%02d\n",t.ti_hour, t.ti_min, t.ti_sec, t.ti_hund);
return 1;
} else {
printf("Nie obsługiwana opcja");
return 0;
}
}
W powyższym kodzie funkcji w C++ włączono szereg plików nagłówkowych. Plik
stdio.h oraz dos.h to standardowe pliki nagłówkowe biblioteki C. Pierwszy jest
wymagany ze względu na stosowanie funkcji printf, drugi ze względu na dostęp do
czasu systemowego poprzez funkcję gettime() oraz strukturę time. Ponadto zawarto
dwa pliki nagłówkowe wymagane ze względu na implementację interfejsu JNI, czyli
jni.h (definicja typów i metod) oraz Informacje.h (deklaracja funkcji odpowiadającej
metodzie rodzimej). Pierwsze linia kodu funkcji zawiera deklarację zmiennej typu
struktury time przechowującej informacje związane z czasem systemowym po
wykorzystaniu funkcji gettime() w dalszej części kodu. Kolejna linia kodu jest bardzo
ważna ze względu na zobrazowanie działania konwersji danych. Wykorzystano tam
funkcję GetStringUTFChars() do konwersji zmiennej (argumentu funkcji) typu jstring
do const char *. Konwersja ta jest wymagana gdyż nie występuje wprost
odwzorowanie typów String na const char *. Jest to również spowodowane tym, że
Java przechowuje znaki w Unicodzie i dlatego konieczna jest dodatkowa konwersja
znaków z typu String(Unicode -UTF) na char *. Warto zwrócić uwagę na wywołanie
funkcji konwertującej. Odbywa się ono poprzez wykorzystanie wskaźnika (env) do
struktury przechowującej szereg różnych funkcji (określonych w jni.h). Ze względu na
sposób zapisu funkcji w jni.h możliwe są dwa sposoby wywołania funkcji albo:
(env*)->GetStringUTFChars(env*, str ,0) dla C albo:
(env)->GetStringUTFChars(str,0) dla C++.
Obie funkcje są zapisane w jni.h, druga z nich jest realizowana poprzez
zastosowanie pierwszej z pierwszym argumentem typu this.
Dalsza część kodu przykładu to wyświetlenie komunikatu zawierającego wartość
argumentu tekstowego po konwersji, pobranie informacji o czasie systemowym i
wyświetlenie go.
Tak przygotowany kod funkcji należy następnie skompilować i wygenerować
bibliotekę (w prezentowanym przykładzie będzie to biblioteka typu Dynamic Link
Library - DLL).
Należy pamiętać o lokalizacji plików jni.h oraz innych plików nagłówkowych wołanych
przez jni.h. Pliki te są zapisane w podkatalogu include oraz include/win32 głównego
katalogu JDK.
Tak przygotowana funkcja i biblioteka umożliwiają uruchomienie programu w Javie
wykorzystującego metodę rodzimą. W wyniku wywołania prezentowanego na
początku kodu :
java Informacje
uzyskamy rezultat typu:
Obsługiwana aktualnie opcja to: CZAS
Bieżący czas to: 16:11:06.50
gdzie podawany czas zależy oczywiście od ustawień systemowych.
Dostęp do metod i pól zdefiniowanych w Javie
W celu wykorzystania w funkcji metod i metod statycznych zdefiniowanych w Javie
należy wykonać trzy podstawowe kroki:
1. pobrać obiekt klasy (Class) w ciele której znajdować ma się żądana metoda:
jclass c = (env)->GetObjectClass(o); gdzie o jest zmienną typu jobject
2. pobrać identyfikator metody dla danej klasy poprzez podanie nazwy metody oraz
sygnatur:
jmethodID id = (env)->GetMethodID(c, suma, (II)I); id przyjmuje wartość 0 jeżeli
nie ma szukanej metody w klasie reprezentowanej przez obiekt c,
3. wywołać metodę poprzez podanie identyfikatora id oraz obiektu o, np.:
(env)->CallIntMethod(o,id,a1,a2); gdzie a1 i a2 to argumenty. W zależności od
zwracanego typu można wykorzystać różne funkcje np. CallVoidMethod(),
CallBooleanMethod(), itp.
Wykorzystanie metod statycznych jest analogiczne do powyższego schematu z tą
różnicą, że w kroku 2 i 3 należy wywołać odpowiednie funkcje GetStaticMethodID() i
CallStaticIntMethod() (lub podobne innych typów), przy czym funkcje CallStatic* jako
argument wykorzystują zmienną typu jclass zamiast jobject (odwołanie się do obiektu
klasy Class reprezentującego klasę zamiast do obiektu będącego wystąpieniem
klasy).
Przykładowe fragmenty kodów w Javie i w C obrazujące stosowanie metod mogą być
następujące:
//Liczenie.java:
(...)
public native void oblicz(int a, int b);
public int suma(int a, int b){
return (a+b);
}
(...)
//policz.cpp:
(...) JNIEXPORT void JNICALL Java_Liczenie_oblicz(JNIEnv *env, jobject o, jint a, jint b){
jclass c = (env)->GetObjectClass(o);
jmethodID id = (env)->GetMethodID(c, suma, (II)I);
if (id==0)
return;
printf(Oto wartość sumy argumentów: %d, (env)->CallIntMethod(o,id,a,b));
}
Dostęp do pól obiektów i zmiennych statycznych klas Javy z poziomu funkcji jest
wykonywany w podobny sposób jak dla metod. Wyróżnia się trzy kroki postępowania:
1. pobrać obiekt klasy (Class) w ciele której znajdować ma się żądana zmienna:
jclass c = (env)->GetObjectClass(o); gdzie o jest zmienną typu jobject
2. pobrać identyfikator zmiennej dla danej klasy poprzez podanie nazwy zmiennej
oraz sygnatury:
jfielddID id = (env)->GetFieldID(c, a, I); id przyjmuje wartość 0 jeżeli nie ma
szukanej zmiennej w klasie reprezentowanej przez obiekt c,
3. pobrać lub ustawić wartość zmiennej poprzez podanie identyfikatora id oraz
obiektu o, np.:
jint a = (env)->GetIntField(o,id);
lub
(env)->SetIntField(o,id,a);
W zależności od typu zmiennej można wykorzystać różne funkcje np.
SetObjectField(), GetObjectField(), GetLongField(), SetDoubleField(), itp.
Wykorzystanie pól statycznych jest analogiczne do powyższego schematu z tą
różnicą, że w kroku 2 i 3 należy wywołać odpowiednie funkcje GetStaticFieldID() i
SetStaticIntField() (lub podobne innych typów), przy czym funkcje {Set,Get}Static*
jako argument wykorzystują zmienną typu jclass zamiast jobject (odwołanie się do
obiektu klasy Class reprezentującego klasę zamiast do obiektu będącego
wystąpieniem klasy).
Przykładowe fragmenty kodów w Javie i w C ukazujące dostęp do pól mogą być
następujące:
//Liczenie.java:
class Liczenie{
static String str=Operacja zakonczona;
int suma = 10;
(...)
public native void oblicz(int a, int b);
public int suma(int a, int b){
return (a+b);
}
(...)
}
//policz.cpp:
(...) JNIEXPORT void JNICALL Java_Liczenie_oblicz(JNIEnv *env, jobject o, jint a, jint b){
jclass c = (env)->GetObjectClass(o);
jfieldID id = (env)->GetFieldID(c, suma, I);
if (id==0)
return;
jint s = (env)->GetIntField(o,id);
printf(Oto wartość sumy argumentów: %d,s);
(env)->SetIntField(o,id, (env)->CallIntMethod(o,id,a,b));
id = (env)->GetStaticFieldID(c, str, Ljava/lang/String;);
if (id==0)
return;
jstring tekst = (env)->GetStaticObjectField(c,id);
printf(tekst);
}
Na zakończenie tej sekcji warto przedstawić w skrócie zagadnienia związane z
wyjątkami. Wyjątki mogą pojawić się w różnych okolicznościach w pracy z metodami
rodzimymi np. przy braku wzywanej metody, przy braku wzywanego pola, przy złym
argumencie, itp. JNI dostarcza klika funkcji do obsługi zdarzeń w kodzie funkcji.
Podstawowe funkcje to ExceptionOccurred(), ExceptionDescribe() i ExceptionClear().
Pierwsza funkcja bada czy wystąpił wyjątek; jeśli tak to zwraca wartość logiczną
odpowiadającą prawdzie. Wówczas można w bloku instrukcji warunkowej zawrzeć
pozostałe dwie funkcje (wysłanie informacji o wyjątku na ekran, oraz wyczyszczenie
wyjątku). Dodatkowo można wywołać wyjątek, który zostanie zwrócony do kody Javy,
gdzie należy go obsłużyć. Można to zrobić za pomocą funkcji Throw(jthrowable o) lub
ThrowNew(jclass, const char *).
Należy pamiętać również o tym, że JNI umożliwia korzystanie z Maszyny Wirtualnej
w ramach aplikacji rodzimej. W tworzeniu takiej aplikacji zanim wywoływane będą
metody i pola (tak jak to ukazano do tej pory) konieczne jest wywołanie Maszyny
Wirtualnej Javy oraz uzyskanie obiektu klasy, np.:
JDK1_1InitArgs vm;
JavaVM *jvm;
JNIEnv *env;
jclass c;
vm.version=0x00010001;
//określenie wersji Maszyny Wirtualnej jako 1.1.2 i wyższych
JNI_GetDefaultJavaVMInitArgs(&vm);
jint test = JNI_CreateJavaVM(&jvm,&env,&vm);
c=(env)->FindClass(NazwaKlasy);
// np. GetStatic{Method,Field}Int(c,...) i tak dalej
Programowanie sieciowe
Tworzenie programów działających w sieciach komputerowych było jednym z
celów stworzenia języka Java. Możliwości aplikacji sieciowych tworzonych za
pomocą Javy są różne. Podstawowe, proste rozwiązania wykorzystujące
mechanizmy wysokiego poziomu pracy w sieci dostarczane są przez podstawowe
klasy połączeń (adresowania) jak InetAddress i URL.
Adresowanie komputerów w sieci (InetAddress i URL)
Klasa InetAddress opisuje adres komputera w sieci poprzez nazwę/domenę,
np. www-med.eti.pg.gda.pl oraz poprzez numer IP, np. 153.19.51.66. Istnieje szereg
metod statycznych klasy InetAddress wykorzystywanych do tworzenia obiektu klasy,
brak jest bowiem konstruktorów. Podstawowe metody wykorzystywane do tworzenia
obiektów to:
InetAddress.getByName(String nazwa),
InetAddress.getAllByName(String nazwa),
InetAddress.getLocalHost();
Pierwsza metoda tworzy obiekt klasy bazując na podanej nazwie komputera lub
adresie. Druga metoda jest wykorzystywana wówczas kiedy komputer o danej
nazwie ma wiele adresów IP. Zwracana jest wówczas tablica obiektów typu
InetAddress. Ostania metoda jest wykorzystywana do uzyskania obiektu
reprezentującego adres komputera lokalnego. Wszystkie metody muszą zawierać
deklaracje lub obsługę wyjątku UnknownHostException powstającego w przypadku
braku identyfikacji komputera o podanej nazwie lub adresie. Poniżej zaprezentowano
przykładowy program wykorzystujący prezentowane metody klasy InetAddress.
//Adresy.java
//Adresy.java
import java.net.*;
public class Adresy{
public static void main(String args[]){
try{
InetAddress a0 = InetAddress.getLocalHost();
System.out.println("Adres komputera "+a0.getHostName()+" to: " +a0)
InetAddress a1 = InetAddress.getByName("biomed.eti.pg.gda.pl");
System.out.println("Adres komputera biomed to: "+a1);
InetAddress a2[] = InetAddress.getAllByName("www.eti.pg.gda.pl");
System.out.println("Adres komputera www.eti.pg.gda.pl to:");
for(int i=0; i
} catch (UnknownHostException he) {
he.printStackTrace();
}
}
}// koniec public class Adresy
W wyniku działania powyższego programu wyświetlone zostaną informacje na temat
adresów sieciowych wybranych komputerów. W programie zastosowano również
jedną z metod klasy InetAddress umożliwiającą operacje na adresie a mianowicie
getHostName(). Metoda ta zwraca nazwę komputera jako obiekt klasy String. Istnieją
również metody umożliwiające filtrację adresu komputera w celu uzyskania jedynie
numeru IP: byte[] getAddress(), String getHostAddress().
Inną klasą wykorzystywaną w Javie do adresowania komputerów jest klasa
URL oraz jej pochodne (URL, URLClassLoader, URLConnection, URLDecoder,
URLEncoder, URLStreamHandler). URL czyli Uniform Resource Locator jest
specjalną formą adresu zasobów w sieci. URL posiada dwa podstawowe elementy:
identyfikator protokołu oraz nazwę zasobów. Identyfikator protokołu to np. http, ftp,
gopher, rmi czy jdbc. Nazwę zasobów stanowią takie elementy jak: nazwa hosta,
nazwa pliku, numer portu, nazwa odwołania (w danym pliku). Tworząc więc obiekt
klasy URL otrzymujemy gotowy wskaźnik, który jest wykorzystywany przez liczne
metody Javy (np. otwieranie obrazka getImage(), tworzenie połączenia w JDBC -
Connection). W odróżnieniu od klasy InetAddress tworzenie obiektów klasy URL
odbywa się poprzez wykorzystanie jednego z licznych konstruktorów. Każdy z nich
związany jest z koniecznością obsługi wyjątku MalformedURLException powstającym
w wypadku problemów z identyfikacją wskazanego w wywołaniu konstruktora
protokołu. Liczne konstruktory umożliwiają podania parametrów adres URL albo
poprzez odpowiednik adresu będący tekstem (String) albo poprzez tworzenie tego
adresu z takich elementów jak nazwa protokołu, nazwa komputera, port, nazwa pliku,
itp. Przykładowe konstruktory mają postać:
URL(String address) ,
URL(String protokół, String host, int port, String plik)
Klasa URL zawiera szereg metod umożliwiających filtrację adresu, a więc pobranie
nazwy protokołu (getProtocol()), nazwy komputera (getHost()), pliku (getFile()) czy
numeru portu (getPort()). Dodatkowo klasa URL zawiera metody umożliwiające
wykonywanie połączenia z hostem (tworzone jest gniazdo o czym dalej) i
przesyłanie danych. Przykładowy program ukazuje możliwość wykorzystania
funkcjonalności klasy URL:
//Pobiez.java
import java.net.*;
import java.io.*;
public class Pobiez{
public static void main(String args[]){
URL url;
String tekst;
if (args.length !=1) {
System.out.println("Wywołanie: Pobiez URL; gdzie URL to adres zasobów");
System.exit(1);
}
try{
url = new URL(args[0]);
BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()));
while( (tekst=br.readLine()) !=null){
System.out.println(tekst);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}// koniec public class Pobiez
Powyższy program umożliwia pobranie źródła wskazanego pliku (html) i wyświetlenie
go na ekranie. Można oczywiście tak skonstruować strumień aby pobierany plik był
nagrywany do lokalnego pliku i w ten sposób stworzyć narzędzie do kopiowania stron
z Internetu. W przykładzie zastosowano konstrukcję obiektu klasy URL poprzez
podanie argumentu do konstruktora jako tekstu (String) będącego argumentem
wywołania aplikacji.
Komunikacja przez Internet (klient-serwer)
Do komunikacji poprzez Internet programy Javy wykorzystują protokoły TCP i
UDP. Klasy URL* oraz Socket i ServerSocket wykorzystują Transfer Control Protocol
klasy takie jak DatagramPacket, DatagramSocket, oraz MulticastSocket korzystają z
User Datagram Protocol. W pakiecie java.net zdefiniowane są jeszcze inne klasy, z
których warto przytoczyć te związane z autoryzacją połączeń i nadawaniem
uprawnień: Authenticator, NetPermission, PasswordAuthentication,
SocketPermission.
W aplikacjach klient-serwer, serwer dostarcza określonej usługi np. przetwarza
zapytanie skierowane do bazy danych, zapisuje serię obrazów diagnostycznych.
Klient wykorzystuje usługi świadczone przez serwer i jest odpowiedzialny za żądanie
usługi oraz obsługę wyników. Funkcje pełnione przez każdą ze stron można ująć
następująco:
- połączenie z urządzeniem zdalnym (przygotowanie wysyłania i odbioru danych),
- wysyłanie danych,
- odbiór danych,
- zamknięcie połączenia,
- przywiązanie portu (dla danej aplikacji na danym hoście),
- nasłuch,
- akceptacja połączeń na danych portach.
Pierwsze cztery funkcje są właściwe dla klienta, serwer rozszerza je o dodatkowe
trzy. W Javie obsługę klienta, a więc realizację pierwszych czterech funkcji dostarcza
klasa Socket; dla serwera przeznaczono klasę ServerSocket. Socket (gniazdo) to
dobrze znane pojęcie wynikające z planów stworzenia dostępu do sieci jako do
strumienia danych (strumień wejścia, strumień wyjścia). Koncepcja strumieni miała
być uniwersalna dla każdego możliwego przepływu danych (z urządzenia, z pliku, z
sieci). Socket jest więc pewną abstrakcją umożliwiającą przejście na wyższy poziom i
nie zajmowaniem się takimi zagadnieniami jak rozmiar pakietu, retransmisja pakietu,
typ mediów, itp. Rozważmy teraz zagadnienia związane ze stroną klienta procesu
komunikacji sieciowej, a więc zapoznajmy się z klasą Socket pakietu java.net.
Klasa Socket posiada szereg różnych konstruktorów, z których najbardziej popularne
są:
Socket(InetAddress address, int port) - gdzie address to obiekt klasy InetAddress będący adresem IP lub nazwą hosta, port - numer portu (0-65535)
Socket(String host ,int port) - gdzie host to tejst oznaczający nazwę hosta, port - numer portu (0-65535)
Ponieważ połączenie wykonywane przez obiekt klasy Socket może nie powieść się z
różnych przyczyn (np. nie znany host) konieczna jest obsługa wyjątków.
Klasyczny fragment kodu obsługi klasy Socket pokazano poniżej:
try {
Socket gniazdo = new Socket("www.on-liner.pl", 80);
}
catch( UnknownHostException e) {
System.err.println(e);
}
catch (IOException e) {
System.err.println(e);
}
W powyższym przykładzie podjęta jest próba (try) połączenia z serwerem
www.amg.gda.pl na porcie 80. Jeżeli nazwa hosta jest nieznana lub serwer nazw nie
działa zostanie zwrócony wyjątek UnknownHostException (wyjątek nieznanego
hosta). Jeśli z innych przyczyn nie uda się uzyskać połączenia zostanie zwrócony
wyjątek IOException (wyjątek wejścia-wyjścia). Najprostszym przykładem programu
implementującego klasę Socket może być prosty skaner portów serwera:
//SkanerPortow.java:
import java.net.*;
import java.io.*;
public class SkanerPortow {
public static void main(String[] args) {
Socket gniazdo;
String host = "localhost";
if (args.length > 0) {
host = args[0]; //jeśli nie podano argumentu programu hostem bedzie komputer lokalny
}
for (int n = 0; n < 1024; n++) { //skanuj wszystkie porty "roota"
try {
gniazdo = new Socket(host, n);
System.out.println("Znalazłem serwer na porcie " + n + " komputera: " + host);
}
catch (UnknownHostException e) {
System.err.println(e);
break; //koniec pętli for w razie nieznanego hosta
}
catch (IOException e) {
// System.err.println(e); - nie drukuj informacji o braku serwera
}
}
}
} // koniec public class SkanerPortow
Program powyższy jest skanerem portów na których działają aplikacje (serwery).
Skanowane porty 0-1023 należą do zakresu portów, które na maszynach UNIXowych
przynależą do administratora, to znaczy tylko root może uruchamiać serwery
na tych portach. Odwzorowanie portów na aplikację można znaleźć w pliku ustawień
/etc/services. Klasa Socket posiada wiele metod umożliwiających między innymi
uzyskanie informacji związanych z obiektem tej klasy takich jak:
- getLocalAddress() - zwraca lokalny adres, do którego dowiązane jest gniazdo,
- getLocalPort() - zwraca lokalny port, do którego dowiązane jest gniazdo,
- getInetAddress() - zwraca zdalny adres, do którego gniazdo jest podłączone,
- getPort() - zwraca zdalny numer portu, do którego gniazdo jest podłączone.
Kolejny przykład obrazuje wykorzystanie metody getLocalPort().
// SkanerPortow2.java:
import java.net.*;
import java.io.*;
public class SkanerPortow2 {
public static void main(String[] args) {
Socket gniazdo;
String host = "localhost";
if (args.length > 0) {
host = args[0];
}
for (int n = 0; n < 1024; n++) {
try {
gniazdo = new Socket(host, n);
int lokalPort = gniazdo.getLocalPort();
System.out.println("Numer lokalnego portu to:" + lokalPort);
System.out.println("Znalazłem serwer na porcie " + n + " komputera: " + host);
}
catch (UnknownHostException e) {
System.err.println(e);
break;
}
catch (IOException e) {
//System.err.println(e);
}
}
}
}// koniec public class SkanerPortow2
Powiedziano wcześniej, że idea gniazd związana była ze stworzeniem strumienia, do
którego można pisać, i z którego można czytać. Podstawową, uniwersalną a
zarazem abstrakcyjną klasą obsługującą strumień wejściowy jest klasa InputStream
dla strumienia wyjściowego jest to OutputStream. Obydwie klasy są rozszerzane i
stanowią podstawę przepływu danych (np. z i do pliku, z i do urządzenia, z i do sieci).
W celu obsługi sieci w klasie Socket zdefiniowano metody zwracające obiekty klas
InputStream oraz OutputStream, co umożliwia stworzenie strumieni do przesyłania
danych przez sieć. Najczęściej wykorzystywane klasy nie abstrakcyjne to
BufferedReader obsługujące czytanie danych przez bufor oraz PrintStream dla
obsługi zapisu nie zwracająca wyjątku IOException. W celu zobrazowania pracy ze
strumieniami posłużmy się przykładami. Pierwszy przykład prezentuje działanie
typowe dla klienta, a więc czytanie ze strumienia.
Przykładowy program łączy się z serwerem czasu a następnie wyświetla ten czas na
ekranie.
//Zegar.java:
import java.net.*;
import java.io.*;
public class Zegar {
public static void main(String[] args) {
Socket gniazdo;
String host = "localhost";
BufferedReader strumienCzasu;
if (args.length > 0) {
host = args[0];
}
try {
gniazdo = new Socket(host, 13);
strumienCzasu = new BufferedReader(new InputStreamReader(gniazdo.getInputStream()));
String czas = strumienCzasu.readLine(); //wprowadź linię znaków z bufora strumienia
System.out.println("Na "+host+" jest: "+czas);
}
catch (UnknownHostException e) {
System.err.println(e);
}
catch (IOException e) {
System.err.println(e);
}
}
}//koniec public class Zegar
Kolejny przykład umożliwi pokazanie zarówno stworzenie programu klienta jak i
programu serwera.
//KlientEcho.java:
import java.net.*;
import java.io.*;
public class KlientEcho {
public static void main(String[] args) {
Socket gniazdo;
String host = "localhost";
BufferedReader strumienEcha, strumienWe;
PrintStream strumienWy;
String echo;
if (args.length > 0) {
host = args[0];
}
try {
gniazdo = new Socket(host, 7); //port 7 jest standardowym portem obslugi echa
strumienWe = new BufferedReader(new InputStreamReader(gniazdo.getInputStream()));
//czytaj z serwera
strumienWy = new PrintStream(gniazdo.getOutputStream());
strumienEcha = new BufferedReader(new InputStreamReader(System.in));
//czytaj z klawiatury
while(true){
echo=strumienEcha.readLine();
if (echo.equals(".")) break; //znak . oznacza koniec pracy
strumienWy.println(echo); //wyslij do serwera
System.out.println(strumienWe.readLine()); //wyslij na monitor
}
}
catch (UnknownHostException e) {
System.err.println(e);
}
catch (IOException e) {
System.err.println(e);
}
}
}//koniec public class KlientEcho
Program serwera
//SerwerEcho.java:
import java.net.*;
import java.io.*;
public class SerwerEcho {
public static void main(String[] args) {
ServerSocket serwer;
Socket gniazdo;
String host = "localhost";
BufferedReader strumienEcha, strumienWe;
PrintStream strumienWy;
String echo;
if (args.length > 0) {
host = args[0];
}
try {
serwer = new ServerSocket(7); //stworz serwer pracujacy na porcie 7 biezacego komputera
while(true){ //główna pętla serwera
try{
while(true){ //główna pętla połączenia
gniazdo = serwer.accept(); //przyjmuj połączenia i stworz gniazdo
System.out.println("Jest polaczenie");
while(true){
strumienWe = new BufferedReader(new InputStreamReader(gniazdo.getInputStream()));
strumienWy = new PrintStream(gniazdo.getOutputStream());
echo=strumienWe.readLine();
strumienWy.println(echo); //wyslij to co przyszlo
}//od while
}//od while
}//od try
catch (SocketException e){
System.out.println("Zerwano polaczenie"); //klient zerwał połączenie
}
catch (IOException e) {
System.err.println(e);
}
}//od while
}// od try
catch (IOException e) {
System.err.println(e);
}
}
}//koniec public class SerwerEcho
Przykład SerwerEcho.java demonstruje zastosowanie klasy ServerSocket do
tworzenia serwerów w architekturze klient-serwer. Klasa ServerSocket dostarcza
kilka konstruktorów umożliwiających stworzenie serwera na danym porcie, z
określoną maksymalną liczbą połączeń (kolejka), na danym porcie danego
komputera o konkretnym IP (ważne w przypadku wielu interfejsów) z określoną
maksymalną liczbą połączeń (kolejka). Kolejnym istotnym elementem implementacji
klasy ServerSocket jest oczekiwanie i zamykanie połączeń. W tym celu wykorzystuje
się metody ServerSocket.accept() oraz Socket.close(). Metoda accept() blokuje
przepływ instrukcji programu i czeka na połączenie z klientem. Jeśli klient podłączy
się do serwera metoda accept() zwraca gniazdo, na którym jest połączenie.
Zamknięcie połączenia odbywa się poprzez wywołanie metody close() dla
stworzonego gniazda. Wówczas połączenie jest zakończone i aktywny jest nasłuch
accept() oczekujący na następne podłączenie. Jeżeli jest potrzeba zwolnienia portu
serwera to wówczas należy wywołać metodę ServerSocket.close(). Inne przydatne
metody klasy ServerSocket to getLocalPort() umożliwiająca uzyskanie numeru portu
na jakim pracuje serwer oraz metoda setSoTimeout() umożliwiająca danemu
serwerowi ustawienie czasu nasłuchu metodą accept(). Metoda setSoTimeout() musi
być wywołana przed accept(). Czas podaje się w milisekundach. Jeżeli w
zdefiniowanym okresie czasu accept() nie zwraca gniazda (brak połączenia)
zwracany jest wyjątek InterruptedException.
Serwlety
Serwlet jest definiowaną przez programistę klasą implementującą interfejs
javax.servlet.Servlet. Zadaniem serwletu jest wykonywanie działań w środowisku
serwera. Oznacza to, że serwlet nie stanowi oddzielnej aplikacji, lecz jego
wykonywanie jest zależne od serwera. Serwlet stanowi zatem odmianę apletu, z tym,
że działa nie po stronie klienta (w otoczeniu np. przeglądarki WWW) lecz po stronie
serwera. Taka forma działanie serwleta wymaga od serwera zdolności do
interpretacji kodu pośredniego Javy oraz możliwości wykorzystania produktu
wykonanego kodu Javy w obrębie funkcjonalności serwera. Interpretacja kodu
pośredniego odbywa się najczęściej za pośrednictwem zewnętrznej względem
serwera maszyny wirtualnej co wymaga zaledwie prostej konfiguracji serwera.
Zdolność do wykorzystania serwletów przez serwer deklarują producenci danego
serwera. Należy podkreślić, że biblioteki kodów konieczne do tworzenia servletów
(javax.servlet.*) nie stanowią części standardowej dystrybucji i muszą być pobrane
od producenta (SUN) oddzielnie jako standardowe rozszerzenie JDK. Istnieją różne
wersje bibliotek kodów servletów (JSDK - Java Servlet Development Kit), które są
obecnie wykorzystywane w różnych serwerach: 2.0 (np. jserv - dodatek do serwera
WWW Apache 1.3.x), 2.1, 2.2 (np. tomcat). Przed przystąpieniem do tworzenia
serwletów konieczne jest zatem ustalenie wersji biblioteki jakiej należy użyć, aby być
zgodnym z obsługiwanym serwerem.
Model obsługi wiadomości.
Serwlety wykorzystują model obsługi wiadomości typu żądanie odpowiedź
(request response). W modelu tym klient wysyła wiadomość zawierającą żądanie
usługi, natomiast serwer wykonuje usługę i przesyła do klienta żądane informacje.
Model taki jest związany z większością popularnych protokołów komunikacyjnych jak
FTP czy HTTP, stąd serwlety można teoretycznie stworzyć dla różnych typów usług.
Działający serwer komunikuje się z serwletem (stanowiącym część procesu serwera)
w oparciu o trzy podstawowe metody zadeklarowane w interfejsie
javax.servlet.Server:
- init(),
- service()
- destroy()
Rolę i moment wywołania tych metod ukazuje następująca sekwencja zadań
wykonywana w czasie uruchamiania serwleta:
a) serwer ładuje kod pośredni serwletu,
b) serwer wywołuje obiekt załadowanego kodu,
c) serwer woła metodę init() serwletu, która umożliwia wszelkie wymagane przez
serwlet ustawienia. Metoda ta stanowi odpowiednik metody init() apletu, oraz z
funkcjonalnego punktu widzenia odpowiada również konstruktorowi klasy obiektu.
d) serwer na podstawie danych dostarczonych przez klienta usługi tworzy obiekt
żądania (request object), implementujący interfejs ServletRequest,
e) serwer tworzy obiekt odpowiedzi (response object) implementujący interfejs
ServletResponse,
f) serwer woła metodę serwletu service(), która realizuje zadania (usługi) stworzone
przez programistę,
g) metoda service() przetwarza obiekt żądania i korzystając z metod obiektu
odpowiedzi wysyła określoną informację do klienta,
h) jeżeli serwer nie potrzebuje serwletu woła jego metodę destroy(), której obsługa
umożliwia zwolnienie zasobów (jak np. zamknięcie plików, zamknięcie dostępu do
bazy danych, itp.).
Model obsługi serwletu jest więc bardzo prosty. Interfejs javax.servlet.Servlet
wymaga oczywiście pełnej definicji wszystkich metod stąd tworząc kody klas
serwletów nie korzysta się bezpośrednio z implementacji tego interfejsu lecz
dziedziczy się po klasach interfejs ten implementujących. Do klas tych należy
zaliczyć klasy:
GenericServlet
HttpServlet
definiowane kolejno w dwóch dostępnych pakietach JSDK API:
javax.servlet
javax.servlet.http.
Korzystając z przedstawionego modelu można skonstruować pierwszy przykład
serwletu:
//WitajcieServlet.java:
import javax.servlet.*;
import java.io.*;
public class WitajcieServlet extends GenericServlet{
static String witajcie= "Witajcie ! Jestem serwletem!\n";
public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
response.setContentLength(witajcie.length());
response.setContentType("text/plain");
PrintWriter pw=response.getWriter());
pw.print(witajcie);
pw.close();
}
}// koniec public class WitajcieServlet
Powyższy przykład definiuje nową klasę WitajcieServlet, która dziedziczy po
GenericServlet, czyli automatycznie implementuje interfejs javax.servlet.Servlet. W
ciele klasy serwletu zdefiniowana jedną metodę (przepisano metodę service() klasy
GenericServlet) service(), która na podstawie pobranych argumentów będących
obiektami żądania i odpowiedzi, wykonuje określoną usługę i przesyła informacje do
klienta. W przypadku omawianej metody usługa polega na przesłaniu wiadomości o
określonym rozmiarze (setContentLength()), typie (setContentType()) i treści
(print(witajcie)) do klienta. Zdefiniowana klasa serwletu korzysta z domyślnych
definicji metod init() oraz destroy() występujących w klasie rodzica (GenericServlet).
Środowisko wykonywania serwletów: prowadzenia dialogu z serwletem. Możliwe tutaj są różne rozwiązania. Najprostszym
z nich jest zastosowaniem dostarczanego w ramach JSDK prostego programu
servletrunner (odpowiednik funkcjonalny programu appletviewer dla apletów). Ten
prosty serwer umożliwia określenie podstawowych parametrów przy jego wywołaniu
jak numer portu, katalog serwletów itp. Standardowa konfiguracja serwera wygląda
następująco:
./servletrunner -v
servletrunner starting with settings:
port = 8080
backlog = 50
max handlers = 100
timeout = 5000
servlet dir = ./examples
document dir = ./examples
servlet propfile = ./examples/servlet.properties
Bardzo ważnym elementem konfiguracji programu servletrunner jest pliku
właściwości serwletów: servlet.properties. Wymagane jest aby w pliku tym podana
została nazwa servletu reprezentując plik jego klasy, np.:
//servlet.properties:
# Servlets Properties
#
# servlet.
# servlet.
# that can be accessed by the servlet using the
# servlet API calls
#
# Witajcie servlet
servlet.witajcie.code=WitajcieServlet
Tak przygotowany plik ustawień własności serwletu oraz uruchomienie programu
servletrunner umożliwia wywołanie servletu, np.:
lynx medvis.eti.pg.gda.pl:8080/servlet/witajcie
albo
http://medvis.eti.pg.gda.pl:8080/servlet/witajcie
W konsoli serwera (servletrunner) pojawi się wówczas komunikat:
WitajcieServlet: init
oznaczający wywołanie metody init() serwletu przez serwer.
Warto zwrócić uwagę, że przy wywołaniu serwletu pojawił się w ścieżce dostępu
katalog o nazwie servlet. Jest to katalog logiczny, definiowany w ustawieniach
serwera (w powyższym przykładzie katalog logiczny servlet to katalog fizyczny
W celu uruchomienia serwletu należy go najpierw skompilować do czego
wymagana jest oczywiście platforma Javy oraz dodatkowo pakiety kodów serwletów
(JSDK). Biblioteki JSDK należy zainstalować, tak aby były widoczne dla kompilatora
Javy (np. zawrzeć pakiety kodów serwletów w ustawieniach CLASSPATH lub
przegrać archiwum *.jar servletów do katalogu ext stanowiącego część biblioteki
(lib) maszyny wirtualnej (jre). Udana kompilacja kodu umożliwia jego wywołanie.
Uruchomienie serwletu wymaga jednak odpowiedniego serwera zdolnego do
examples). W wyniku poprawnego działania serwletu klient uzyska wyświetloną
następującą informację:
Witajcie ! Jestem serwletem!
Wadą wykorzystania programu servletrunner jest konieczność jego powtórnego
wywoływania w przypadku zmiany kodu serwletu. Na rynku dostępne są obecnie
różne wersje serwerów, głównie WWW, obsługujących serwlety. Do najbardziej
jednak popularnych należy zaliczyć rozszerzenie serwerów WWW opracowanych w
ramach projektu Apache ApacheJServ oraz nowy serwer obsługujący servlety oraz
Java Server Pages (dynamiczna konstrukcja stron WWW). Podstawowa różnica
pomiędzy tymi produktami jest taka, że pierwszy z nich jest dodatkiem do serwera
WWW obsługującym kody serwletów zgodne ze specyfikacją 2.0, natomiast drugi
jest referencyjną implementacją specyfikacji serwletów 2.2 pracującą jako
samodzielny serwer WWW.
Odwołanie do środowiska wywołania serwletu może następować poprzez wykonanie
metod zdefiniowanych dla klasy ServletContext. W czasie inicjowania serwletu (init())
informacje środowiska podawane są w ramach obiektu ServletConfig. Odwołanie się
do tego obiektu poprzez metodę getServletContext() w czasie wykonywania serwletu
(service()) zwraca obiekt klasy ServletContext. W ten sposób można wywołać na nim
szereg metod uzyskując cenne często informacje o środowisku pracy. Poniższy
przykład demonstruje wykorzystanie metody getServerInfo() obiektu klasy
ServletContext w celu uzyskania informacji o nazwie serwera servletów i jego wersji.
//SerwerServlet.java:
import javax.servlet.*;
import java.io.*;
public class SerwerServlet extends GenericServlet{
static String witajcie= "Jestem serwletem! Dzia\u0142am na:\n";
private ServletConfig ustawienia;
public void init(ServletConfig cfg){
ustawienia=cfg;
}
public void service(ServletRequest request, ServletResponse response) throws ServletException,
IOException {
String wiad=witajcie+"";
ServletContext sc = ustawienia.getServletContext();
wiad+=sc.getServerInfo();
response.setContentLength(wiad.length());
response.setContentType("text/plain; charset=iso-8859-2");
PrintWriter pw=response.getWriter();
pw.print(wiad);
pw.close();
}
}// koniec public class SerwerServlet
Efektem działania powyższego serwletu może być komunikat:
Jestem serwletem! Działam na:
servletrunner/2.0
lub np.:
Jestem serwletem! Działam na:
Tomcat Web Server/3.1 (JSP 1.1; Servlet 2.2; Java 1.2; SunOS 5.5.1 sparc;
java.vendor=Sun Microsystems Inc.)
Kontrola środowiska wymiany wiadomości
W modelu wymiany wiadomości typu żądanie-odpowiedź ważną rolę pełni
dostęp do informacji konfiguracyjnych występujących w żądaniu oraz możliwość
ustawień podobnych informacji dla odpowiedzi. W przypadku serwletów funkcje te
możliwe są poprzez obsługę obiektów podawanych jako argumenty w głównych
metodach usług serwletu. W przypadku pakietu javax.servlet.* obiekty te posiadają
interfejs odpowiednio o nazwach: ServletRequest (dane żądania) i ServletResponse
(dane odpowiedzi). Dla pakietu javax.servlet.http.*, w którym zdefiniowano klasy i
metody obsługi protokołu HTTP przez servlety, występując interfejsy dziedziczące po
powyższych: HttpServletRequest i HttpServletResponse. Obiekty obu typów
posiadają do dyspozycji szereg metod umożliwiających dostęp do parametrów
wywołania, ustawień konfiguracyjnych i innych. Przykładowe metody obiektów typu
*Request to:
getCharacterEncoding() pobranie informacji o stronie kodowej
getContentLength() pobranie informacji o rozmiarze wiadomości
getContentType() pobranie informacji o typie (MIME) wiadomości
getParameter(java.lang.String name) pobranie wartości parametru o danej nazwie
getProtocol() pobranie informacji o protokole komunikacji
getRemoteAddr() pobranie informacji o adresie komputera zdalnego,
getReader() utworzenie obiektu strumienia do odczytu (znaków)
getInputStream() utworzenie obiektu strumienia do odczytu (bajtów)
Przykładowe metody obiektów typu *Response to:
getCharacterEncoding() pobranie informacji o aktualnej stronie kodowej,
getOutputStream()-utworzenie obiektu strumienia do zapisu (bajtów)
getWriter() utworzenie obiektu strumienia do zapisu (znaków)
setContentLength(int len) ustawienie rozmiaru wiadomości
setContentType(java.lang.String type) ustawienie typu (MIME) wiadomości.
Niektóre z powyższych metod stosowane były we wcześniejszych przykładach. Inną
ilustracją metod obiektów typu *Request i *Response może być następujący
program:
//KlientServlet.java:
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
public class KlientServlet extends HttpServlet {
static String wiad = "Oto nag\u0142\u00F3wki: \n";;
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException,
ServletException{
String nazwa, wartosc, nag="";
response.setContentType("text/plain; charset=iso-8859-2");
// dla wersji nowszej od JSDK 2.0 można ustalić lokalizacje:
//response.setLocale(new Locale("pl","PL"));
PrintWriter pw = response.getWriter();
Enumeration e = request.getHeaderNames();
while (e.hasMoreElements()) {
nazwa = (String)e.nextElement();
wartosc = request.getHeader(nazwa);
nag+=(nazwa + " = " + wartosc+"\n");
}
pw.print(wiad+nag);
pw.close();
}
}// koniec public class KlientServlet
Powyższy serwlet jest zdefiniowany dla klasy typu HttpServlet definiującej szereg
metod obsługujących protokół HTTP. Jedną z usług tego protokołu jest pobranie
nagłówka i danych GET. Obsługa tej usługi jest możliwa poprzez zastosowanie
metody doGet(), do której są przekazywane obiekty typu HttpServletRequest i
HttpServletResponse. Ponieważ wraz z przesyłaniem wiadomości w HTTP przesyła
się również nagłówek zawierający informacje sterujące można te informacje pobrać i
wyświetlić. Omawiany program pobiera zestaw informacji konfiguracyjnych
pochodzących z nagłówków wygenerowanych przez klienta usługi
(request.getHeaderNames()). Pobrane informacje są formatowane w postaci
tekstowej zgodnie ze stroną kodową iso-8859-2 i są przesyłane jako treść informacji
do klienta (respone). W wyniku działania tego serwletu klient może obserwować w
swojej przeglądarce następujący efekt:
Oto nagłówki:
Connection = Keep-Alive
User-Agent = Mozilla/4.6 [en-gb] (WinNT; I)
Host = med16:8080
Accept = image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */
Accept-Encoding = gzip
Accept-Language = en-GB,en,en-*
Accept-Charset = iso-8859-1,*,utf-8
Metody wywoływania serwletów.
Serwlety mogą być również (o ile umożliwia to serwer np. Java Web
ServerTM) wywoływane z kodu HTML poprzez zastosowanie mechanizmu SSI
(Serwer-Side Include). Technika opisu serwletu w kodzie HTML jest podobna do
opisu apletu. W najprostszej formie opis ten może wyglądać następująco:
...
W przedstawionym opisie wykorzystano znacznik
name nazwa servletu
code nazwa pliku kodu serwletu
NAZWA_1, WARTOSC_1 pary parametrów wywołania serwletu.
Parametry wywołania serwletu mogą być pobrane przez serwlet poprzez użycie
metod klas GenericServlet i HttpServlet jak np. getInitParameter(java.lang.String
name) dostarczającą wartość parametru o podanej nazwie. W obrębie znacznika
//LicznikServlet.java:
import javax.servlet.*;
import java.io.*;
import java.util.*;
public class LicznikServlet extends GenericServlet{
static Date pocz = new Date();
static int licznik=0;
public void service(ServletRequest request, ServletResponse response) throws ServletException,
IOException {
int licz_tmp;
response.setContentType("text/plain; charset=iso-8859-2");
PrintWriter pw=response.getWriter();
synchronized(this){
licz_tmp=licznik++;
}
pw.print("Liczba odwiedzin strony od "+pocz+" wynosi: "+licz_tmp);
pw.close();
}
}// koniec public class LicznikServlet
//serwlety.shtml:
< html >
< head >
< title > Serwlety: wywołanie serwletu za pomocą znaczników w kodzie HTML
< /head >
< body >
< H1 >Strona demonstracyjna< /H1 >
< H2 >Pokaz wywołania serwletu za pomocą znaczników z kodu HTML< /H2 >
< servlet name="LicznikServlet" code="LicznikServlet">
< /servlet >
< /body >
< /html >
Niestety zastosowanie tego typu jest ograniczone ponieważ tylko niektóre serwery je
umożliwiają (Java Web ServerTM). Stosowanie dynamicznej kompozycji stron w
obrębie kodu HTML jest obecnie realizowane poprzez technologię Java Server
Pages (JSP).
Obsługa protokołu HTTP pakiet javax.servlet.http.*
Protokół HTTP jest jednym z najbardziej popularnych ze względu na obsługę
WWW. Dlatego trudno się dziwić, że opracowano pakiet klas (javax.servlet.http.*)
umożliwiający obsługę usług tego protokołu w ramach serwletów. Wyróżnia się
następujące usługi (metody) protokołu HTTP:
- HEAD żądanie informacji od serwera w formie danych nagłówka
- GET- żądanie informacji od serwera w formie danych nagłówka i treści (np. pliku),
występuje ograniczenie rozmiaru danych do przesłania (kilkaset bajtów)
- POST żądanie umożliwiające przesłanie danych od klienta do serwera
- PUT żądanie umożliwiające przesłanie przez klienta pliku do serwera (analogia
do ftp)
- DELETE żądanie usunięcia dokumentu z serwera
- TRACE żądanie zwrotnego przesłania nagłówków wiadomości do klienta
(testowanie)
- OPTIONS żądanie od serwera identyfikacji obsługiwanych metod, informacja o
których jest przesyłana zwrotnie w nagłówku
- Inne.
W celu obsługi powyższych usług stworzono zestaw metod dostępnych poprzez
klasę javax.servlet.http.HttpServlet dziedziczącą po javax.servlet.Servlet, a
mianowicie:
doGet() dla żądań HEAD i GET
doPost() dla żądania POST
doPut() dla żądania PUT
doDelete() dla żądania DELETE
doTrace() dla żądania TRACE
doOptions() dla żądania OPTIONS.
Każda z wymienionych metoda jest zadeklarowana jako chroniona (protected).
Jedyną metodą dostępną (public) jest metoda service() poprzez którą przechodzą
wysyłane przez klienta żądania i są kierowane do obsługi w ramach wyżej
pokazanych metod doXXX(). Najprostszą realizacją usługi na żądanie ze strony
klienta jest dynamiczne stworzenie strony HTML, którą odczytuje klient.
//StronaServlet.java:
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
public class StronaServlet extends HttpServlet {
static String tytul = "Strona g\u0142\u00F3wna serwisu nauki JAVY \n";
String dane="";
public void init(ServletConfig config){
dane=config.getServletContext().getServerInfo();
}
public String naglowek(String tytul){
String nag="< !DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2//PL\" >"
+ "< HTML >"
+ "< HEAD > "
+ " < TITLE >" + tytul + "< /TITLE >"
+" < META NAME=\"Autor\" CONTENT=\"">"
+ " < META NAME=\"Serwer\" CONTENT=\"" + dane + "\">"
+ "< /HEAD >";
return nag;
}
public String stopka(){
String stop ="< /BODY >"+ "< /HTML >";
return stop;
}
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException,
ServletException{
Date data = new Date();
response.setContentType("text/html; charset=iso-8859-2");
PrintWriter pw = response.getWriter();
String wiad=naglowek(tytul);
wiad=wiad + ""
+ " < CENTER >"
+ " < H1 >" + tytul + "< /H1 >"
+ " < H2 >Witamy na stronie kursu Javy!< /H2 >"
+ " < H3 >[ Czas: " + data + " ]< /H3 >"
+ " < FONT SIZE=\"-2\">Zapraszam na:"
+ " J\u0119zyk JAVA
"
+ " < /FONT >"
+ " < /CENTER >";
wiad+=stopka();
pw.print(wiad);
pw.close();
}
}// koniec public class StronaServlet
W wyniku działania powyższego programu można uzyskać następujący
(sformatowany) tekst:
Strona główna serwisu nauki JAVY
Witamy na stronie kursu Javy!
[ Czas: Tue May 09 16:35:13 GMT+02:00 2000 ]
Zapraszam na: Język JAVA
Rozważając konstruowanie dynamicznych stron HTML w oparciu o serwlety
konieczne jest przedstawienie łączenia ich z formularzami.
//OgloszenieServlet.java
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
public class OgloszenieServlet extends HttpServlet {
static String tytul = "Strona testowa serwisu nauki JAVY \n";
String dane="";
public void init(ServletConfig config){
dane=config.getServletContext().getServerInfo();
}
public String naglowek(String tytul){
String nag=""
+ "< HTML >"
+ "< HEAD >"
+ " < TITLE >" + tytul + "< /TITLE > "
+ " < META NAME=\"Autor\" CONTENT=\"">"
+ " < META NAME=\"Serwer\" CONTENT=\"" + dane + "\">"
+ "< /HEAD >";
return nag;
}
public String stopka(){
String stop =""+ "