Jan Bielecki
Java
po
C++
Profesorowi
Jankowi Zabrodzkiemu
z wyrazami przyjaźni
Spis treści
Część I Język Java
Program w Javie
Proste programy
Java i C++
Programy źródłowe
Kompilacja i wykonanie
Biblioteki
Oblicze graficzne
Środowiska zintegrowane
Część II Aromat Javy
Java a język C++
Mój pierwszy program
Mój drugi program
Mój trzeci program
Uruchamianie programów
Część III Środowisko Cafe
Wywołanie środowiska
Edycja dokumentów
Utworzenie projektu
Otwarcie projektu
Modyfikowanie projektu
Konfigurowanie pulpitu
Kompilowanie modułu
Budowanie programu
Dostarczenie argumentów
Wykonanie programu
Uruchomienie programu
Część IV Programy
Struktura programu
Komentarze
Słowa kluczowe
Identyfikatory
Moduły
Pakiety
Deklaracje importu
Typy podstawowe
Typy całkowite
Typy rzeczywiste
Typ znakowy
Typ orzecznikowy
Typy obiektowe
Deklarowanie klas
Deklarowanie składników
Deklarowanie konstruktorów
Inicjowanie pól i zmiennych
Klasy abstrakcyjne
Metody abstrakcyjne
Tworzenie obiektów
Ładowanie klas
Inicjowanie klas
Typy łańcuchowe
Klasa String
Klasa StringBuffer
Typy interfejsowe
Implementowanie interfejsu
Interfejsy równorzędne
Typy odnośnikowe
Tworzenie odniesień
Przetwarzanie odniesień
Operator instanceof
Porównywanie obiektów
Klonowanie obiektów
Deklarowanie odnośników
Typy tablicowe
Deklarowanie tablic
Przetwarzanie elementów
Kopiowanie tablic
Klonowanie tablic
Przetwarzanie tablic
Procedury
Konstruktory
Funkcje
Metody
Podprogramy
Rekurencja
Wyrażenia
Priorytety i wiązania
Nowe operatory
l-wyrażenia
Konwersje
Przypisania
Instrukcje
Instrukcja for
Instrukcje break i continue
Instrukcja synchronized
Instrukcja try
Wyjątki
Wysyłanie wyjątków
Wyjątki predefiniowane
Weryfikowanie wyjątków
Definiowanie klas wyjątków
Wątki
Stany wątków
Priorytety
Wątek główny
Tworzenie wątków
Synchronizowanie wątków
Procedury synchronizowane
Monitor
Potok
Impas
Zniszczenie wątku
Przesyłanie
Klasa plikowa
Klasy wejściowe
Klasy wyjściowe
Przesyłanie przenośne
Przesyłanie leksemowe
Przesyłanie buforowane
Przesyłanie filtrowane
Przesyłanie wyrywkowe
Wykonywanie
Ładowanie klas
Tworzenie obiektów
Niszczenie obiektów
Uzyskiwanie dostępu
Wywoływanie metod
Definiowanie klas
Projektowanie kolekcji
Wykonywanie obcych programów
Połączenia
Właściwości
Implementacje
Część V Aplety
Opis apletu
Otoczenie apletu
Zdarzenia
Rozkłady
FlowLayout
BorderLayout
GridLayout
GridBagLayout
CardLayout
Komponenty
Label
Button
Checkbox
Choice
List
TextField
TextArea
ScrollBar
Canvas
MenuItem
Frame
Window
Dialog plikowy
Grafika
Kontekst
Wykresy
Współrzędne
Czcionki
Kolory
Kursory
Obrazy
Animacje
Generowanie
Odtwarzanie
Obcinanie
Buforowanie
Dźwięki
Komunikacja
Przełączenia
Platforma
Przeglądarka
Literatura
Dodatki
Priorytety operatorów
Słownik terminów
Styl programowania
Klasa uruchomieniowa
Hierarchia klas
Definicje klas
Od Autora
Od ponad 30 lat wykładam języki programowania: Fortran, Basic, Algol, COBOL, PL/I, Snobol, Lisp, Simulę, Pascal, Modulę, Adę, C i C++, ale nigdy dotąd nie zdarzyło mi się spotkać ze zjawiskiem tak spontanicznej akceptacji nowego języka programowania jakim jest Java.
Ponieważ wiele osób uważa mnie za promotora C++ i to nie tylko dlatego, że jestem czynnie zaangażowany w jego procedurę standaryzacyjną (oficjalny standard ANSI C++ jest planowany na sierpień 1997), śpieszę wyjaśnić, że Javę polubiłem natychmiast, a to nie tylko dzięki zapożyczeniom składni z C++ oraz wbudowaniu w nią mechanizmów programowania strukturalnego, zdarzeniowego, współbieżnego i rozproszonego, ale również pod wpływem doskonałej specyfikacji języka, opracowanej przez Jamesa Goslinga, w jednej osobie twórcę Javy i jednego z najbardziej cenionych współczesnych programistów.
Biorąc pod uwagę doświadczenia z posługiwania się wcześniejszymi językami programowania, zawarto w Javie wszystko to, co jest potrzebne aby programować łatwo, skutecznie i przyjemnie, a ponadto aby móc wypróbować całościową wiedzę o najnowszych technikach pisania programów.
Po kilku miesiącach obcowania z Javą nie mam żadnej wątpliwości, że stanie się ona jedynym językiem programowania nauczanym na kierunkach informatycznych. W praktyce zawodowej będą oczywiście używane także inne języki, ale po rzetelnym poznaniu Javy, nabranie biegłości w posługiwaniu się nimi nie sprawi nikomu żadnej trudności.
Niniejszą książkę napisałem przede wszystkim dla słuchaczy moich wykładów w Politechnice Warszawskiej i w Polsko-Japońskiej Wyższej Szkole Technik Komputerowych. Ponieważ wiem czego ich nauczyłem, dlatego zrezygnowałem z napisania tekstu dla początkujących, zakładając że Czytelnik zna już zasady programowania obiektowego w C++. Tym, których znajomość C++ jest umiarkowana, polecam zapoznanie się z moimi książkami Sekrety C++ lub ANSI C++, albo sięgnięcie do pozycji wymienionych w zawartych tam wykazach literatury.
Ponieważ w wielu wiodących uczelniach wyższych zadecydowano już o tym, że nowa generacja informatyków zacznie swoją edukację od Javy, w najbliższym czasie planuję wydanie książki Java od podstaw. Nie będzie ona zapewne tak naładowana treścią jak niniejsza, ale z pewnością okaże się łatwiejsza dla tych, którzy stawiają pierwsze kroki w programowaniu.
Życząc Czytelnikom przyjemnej lektury, pragnę w tym miejscu serdecznie podziękować tym wszystkim, którzy pomogli mi w realizacji niniejszego przedsięwzięcia.
Wśród nich na pierwszych miejscach znajdują się Stach Łazęcki i Krystyna Sosnowska. Ich życzliwość oraz pomoc w zdobywaniu potrzebnych mi lektur, a także udzielająca się wiara iż wiadomość o mojej śmierci jest znacznie przesadzona, dodały mi energii do wznowienia działalności pisarskiej po niemal 3 latach przerwy, kiedy to z blisko 100 książek wydanych w kilku krajach i w ponad 800,000 egz. ostały się na półkach zaledwie niedobitki, a wielu młodszych informatyków nawet nie wie, co i kiedy napisałem.
Jan Bielecki
Część I Język Java
Java to nie tylko najnowszy język programowania, ale również początek kolejnej zmiany paradygmatu. Po paradygmacie strukturalnym i obiektowym, widnieje na horyzoncie paradygmat sieciowy, w którym programy będą nie tylko strukturalne i obiektowe, ale co ważniejsze, będą zorganizowane w taki sposób, że umożliwią zaprzęgnięcie do pracy niezliczonych komputerów rozproszonych w niejednorodnej sieci globalnej.
Początki Javy były skromne. Język ten, powstały na początku lat 90, miał być narzędziem do oprogramowania konsumenckich urządzeń elektronicznych, wyposażonych w podstawową inteligencję; takich jak mówiące pralki i lodówki, interakcyjne telewizory, komunikujące się ze sobą urządzenia kuchenne oraz zintegrowane systemy domowe.
Chociaż głośno tego nie mówiono, wszystko co planowano, miało urzeczywistnić futurystyczne wizje domu przyszłości, zarysowane wcześniej przez Steve'a Jobsa, sławnego CEO firmy Apple, współtwórcy komputerów Lisa i Macintosh.
Podczas prac nad Javą eksplodował jednak Internet i wówczas James Gosling uznał, że to co wymyślił dla pralek i lodówek, doskonale pasuje do sieci łączącej w jeden organizm miliony komputerów, nadzorowanych przez tak odmienne systemy operacyjne jak Windows (60%), MacOS (22%) i UNIX (18%).
Po pojawieniu się Javy w Internecie, co nastąpiło w 1995 roku, niewielu dawało jej szansę przetrwania. Sceptycy uważali Javę za kolejny efemeryczny język programowania, a zwolennicy tak okrzepłych języków jak C++, Ada i COBOL nie czuli się zagrożeni koniecznością przekwalifikowania.
Stało się jednak coś, co przeszło najśmielsze oczekiwania twórców Javy. Z dnia na dzień społeczność Internetowa zaakceptowała ten język i uznała go za własny. Jak grzyby po deszczu zaczęły powstawać kluby dyskusyjne, organizowano konferencje i konkursy na najlepsze programy, a w okresie od wiosny do jesieni 1996 roku pojawiło się na temat Javy ponad 300 książek; wśród nich wielotomowe specyfikacje języka, potwierdzające przydatność Javy do pisania programów niezależnych od użytej platformy sprzętowej i systemowej.
W tym miejscu należy się zastanowić skąd wzięła się owa niezależność od platformy, leżąca u podstaw sukcesu Javy. Otóż Javę zaprojektowano w taki sposób, że w jej specyfikacji nie ma określeń zależne od implementacji, zwrotów stanowiących przekleństwo wszystkich tych, którzy kiedykolwiek próbowali pisać programy przenośne (oryg. portable).
Jednoznaczność specyfikacji stanowi prawdziwie rewolucyjny przełom, ponieważ od określeń "zależne" roi się w standardach wszystkich innych języków programowania, z powszechnie stosowanym C++ włącznie, w którym na przykład nie wiadomo, jakiego typu jest liczba 100000, jaki sens ma wyrażenie 1.0/0.0 oraz jaki jest rezultat dzielenia 5/-2.
W Javie na każde z takich pytań pada jednoznaczna odpowiedź. W szczególności 100000 jest typu "long", wyrażenie 1.0/0.0 ma wartość Double.POSITIVE_INFINITY, a rezultatem dzielenia 5/-2 jest 1.
Ale jednoznaczna specyfikacja, to zazwyczaj tylko gwarancja przenośności na poziomie kodu źródłowego, a więc konieczność ponownej kompilacji na innej platformie. To już bardzo dużo, o wiele więcej niż dotychczas, ale nie dla tych którzy patrzą w przyszłość.
Dlatego w Javie zapewniono dodatkowo przenośność na poziomie kodu binarnego, uwarunkowaną, wprowadzeniem pojęcia JavaPlatform jako połączenia JavaAPI (oryg. Java Application Programming Interface), standardowego interfejsu do apletów i aplikacji maszyny wirtualnej oraz JavaVM (oryg. Java Virtual Machine), abstrakcyjnej maszyny wirtualnej, stanowiącej docelowe "urządzenie", w którym będzie wykonywany program binarny.
Ponieważ produktem kompilacji staje się przy takich założeniach kod maszyny wirtualnej, nazywany dalej B-kodem (oryg. bytecode), konieczne jest wyposażenie każdej odmiennej platformy (ich liczba nie jest w istocie zbyt wielka) w interpreter JavaVM, obecnie także napisany w Javie, umożliwiający wykonanie dowolnego programu dostarczonego w B-kodzie.
Liczba interpreterów JavaVM stale wzrasta. W chwili obecnej istnieją już interpretery na platformach Windows (Windows 95, Windows NT), Macintosh (MacOS), UNIX (AIX, Solaris, Linux) i innych (OS/2, MVS, NetWare), co umożliwia wykonywanie programów skompilowanych do B-kodu na praktycznie dowolnym komputerze podłączonym do Internetu.
Uwaga: Jeśli ktoś krzywi się, słysząc, że kod wynikowy Javy jest interpretowany (co początkowo istotnie powodowało kilkakrotne spowolnienie wykonywania B-kodu w stosunku do C++), to należy wyjaśnić, że postęp w technikach interpretowania, a w szczególności wyposażenie najnowszych interpreterów JavaVM w mechanizm Just-In-Time (tworzenie kodu rodzimego w locie) praktycznie wyrównuje różnicę między efektywnością wykonania kodu kompilowanego i interpretowanego.
Bardzo interesująco przedstawia się także opublikowanie przez Sun specyfikacji systemu operacyjnego JavaOS wykonywanego na JavaVM, który być może już wkrótce zastąpi popularne systemy operacyjne, jak również przystąpienie do realizacji JavaBeans, platformowo niezależnego zestawu komponentów, które będą mogły być włączane do dowolnych apletów i aplikacji.
Ta przyszłościowa inicjatywa nie jest czystą abstrakcją, ponieważ powinna być rozpatrywana w kontekście planowanych na koniec 1996 roku dostaw Komputerów Sieciowych (oryg. Network Computer) oraz istnienia zaawansowanych projektów JavaChip, układów scalonych implementujących JavaVM.
Gdyby ambitne plany firmy Sun oraz powołanej przez nią firmy JavaSoft, powiodły się, to w ramach JavaPlatform mógłby powstać komputer sieciowy JavaNC, na układzie scalonym JavaChip, implementujący JavaVM, z systemem operacyjnym JavaOS, adaptowalną przeglądarką HotJava, językiem programowania skryptów JavaScript, gotowym zestawem komponentów JavaBeans oraz językiem programowania Java.
Ta całkiem realna perspektywa spędza zapewne sen z oczu osławionego tandemu
Andy Grove (Intel) - Bill Gates (Microsoft).
Jeśli jednak mowa o Billu Gatesie, to w kontekście zbiorowej fascynacji Javą warto prześledzić jaki był do niej stosunek Microsoftu. Otóż przez długie miesiące, podczas których praktycznie wszyscy Wielcy (w tym IBM, Oracle, Corel, Netscape, Lotus, Symantec, Borland, Toshiba, Hewlett Packard) dołączali do obozu zwycięzcy (czytaj firmy Sun), Microsoft zachowywał kompletne milczenie, znacząco ignorując wszystko co działo się wokół Javy.
Kiedy jednak było już niemal pewne, że gigant informatyczny po raz pierwszy się na czymś porządnie potknie, Microsoft uczynił podobną woltę jak niegdyś IBM (ignorujący niemal do ostatniej chwili mikrokomputery): dostrzegł Javę i uznał iż pasuje ona doskonale do jego własnych koncepcji.
Stało się coś niezwykłego: po raz pierwszy w swojej historii Microsoft nabył licencję na produkt, nie kupując firmy, która go oferowała.
W ślad za tą decyzją, potwierdzającą znaną zasadę, że jeśli czegoś nie można zniszczyć, to należy się do tego przyłączyć, zaczęło się traktowanie Javy jako czegoś cudownie pasującego do pomysłów Microsoftu.
Jednym z tego przejawów stało się wprowadzenie na rynek zintegrowanego środowiska programistycznego Visual J++ oraz ogłoszenie ActiveX, techniki integrującej wyspecjalizowane obiekty tworzone za pomocą różnych narzędzi programowania, jako naturalnego otoczenia Javy, umożliwiającego wyposażanie stron WWW w wysoce efektywne i atrakcyjne właściwości multimedialne (obraz, dźwięk, animację, trójwymiarowość, komunikację, pozorną rzeczywistość).
Co z tego wszystkiego wyjdzie? Trudno jeszcze zawyrokować. Nie ulega jednak wątpliwości, że Java została poważnie potraktowana przez niemal wszystkich producentów i programistów (w znacznej mierze dzięki jej związkom z językami C++, Simula, SmallTalk, Eiffel i Objective C) oraz że po raz pierwszy od zarania informatyki akceptacja języka wynikła z woli użytkowników, a nie została im narzucona przez producentów.
Program w Javie
Z tego co napisano powyżej wiadomo, że program napisany w Javie jest podobny do programu napisanego w C++, że kompilator Javy produkuje B-kod oraz że w każdym środowisku, w którym kod ten ma być wykonany musi istnieć JavaVM, która umożliwi zinterpretowanie B-kodu.
Co się tyczy programu źródłowego, to jest on aplikacją albo apletem. Aplikacja jest programem samodzielnym (oryg. selfcontained), a aplet jest programem wbudowanym (oryg. embedded) w skrypt przeglądarki (np. Netscape) albo w aplikację.
W celu utworzenia B-kodu programu należy posłużyć się kompilatorem. Firma Sun dostarcza za darmo zestaw JavaSDK (oryg. Java Software Development Kit) składający się z kompilatora języka Java, interpretera JavaVM oraz bibliotek i użytków.
Inne firmy dostarczają zintegrowane platformy uruchomieniowe (Symantec: platformę Visual Cafe, Borland: platformę Latte, Microsoft: platformę Visual J++), każdą z wbudowanym kompilatorem, interpreterem i uruchamiaczem (oryg. debugger). Użycie ich zapewnia znacznie większy komfort programowania niż użycie JavaSDK.
Proste programy
Nie bez rozbawienia (czytelnicy moich książek domyślą się natychmiast dlaczego) przeczytałem w jednym z periodyków komputerowych następujące zdanie
Poniżej przedstawiono najprostszy programik w Javie, realizujący typowe zagadnienie z pierwszego rozdziału każdego podręcznika programowania: wypisywanie tradycyjnego
Hello, I am Jan B.
na ekranie.
Program taki, napisany w C++ ma postać
#include <iostream.h>
int main(int argc, char *argv[])
{
cout << "Hello, I am Jan B." << endl;
return 0;
}
natomiast napisany w Javie przybiera postać
public
class Greet {
public static void main(String args[])
{
System.out.println("Hello, I am Jan B.");
}
}
Uwaga: Z zacytowanego w oryginale programu usunąłem błędy oraz dokonałem w nim drobnych zmian kosmetycznych. Dlatego w podanej tu wersji różni się nieco od przedstawionego przez Michała Gomulińskiego (PC Kurier 23 maja 1996).
Jakie zatem można dostrzec różnice? Otóż, przede wszystkim, nie ma w Javie dyrektyw (jak #include), nie ma zmiennych i funkcji globalnych (jak main), nie ma wskaźników (tu elementów tablicy argv), nie ma operatorów definiowanych (<<) oraz nie ma kwalifikatorów zakresu (::).
Funkcja main musi być zdefiniowana w ciele klasy oraz musi być publiczna. Dzięki temu jej definicja staje się widoczna także poza klasą (słowo kluczowe public występujące w C++ w roli nazwy sekcji jest w Javie używane jako specyfikator, a więc jest włączane do nagłówka składowej).
Ponadto, ponieważ definicje klas są zawarte w pakietach (oryg. package), te spośród klas programu, które mają być widoczne poza pakietami także muszą być jawnie deklarowane jako publiczne. Dlatego przed definicją klasy Greet występuje specyfikator public.
Odmienną interpretację mają także parametry funkcji main. W Javie parametr args jest tablicą odnośników (oryg. reference) do obiektów typu String, zainicjowanych argumentami wywołania. Ponieważ jednak args jest nie tylko tablicą, ale jest także obiektem klasy z polem length (określającym liczbę elementów tablicy), zbędny jest, występujący w C++, dodatkowy parametr argc.
Uwaga: W odróżnieniu od C++ przyjęto, że wartością parametru args[0] nie jest nazwa programu, ale pierwszy występujący po niej spójny ciąg znaków.
Dlatego inny, klasyczny program w C++
#include <iostream.h>
int main(int argc, char *argv[])
{
cout << "The arguments are: ";
for(int i = 1; i < argc ; i++)
cout << argv[i] << ' ';
cout << endl;
return 0;
}
przybiera w Javie postać
public
class ShowArgs {
public static void main(String args[])
{
System.out.print("The arguments are: ");
for(int i = 0; i < args.length ; i++)
System.out.print(args[i] + " ");
System.out.println();
}
}
Java i C++
Jedną z przyczyn szybkiej akceptacji Javy jest z pewnością jej podobieństwo do C++. Ma to niebagatelne znaczenie, ponieważ większość współczesnych programistów zna C++ dość dobrze, a potwierdzeniem tego może być mało znana wiadomość, że opracowane w USA Informatyczne Testy Kwalifikacyjne dla uczniów szkół średnich (oryg. Advanced Placement Tests) zakładają znajomość C++ (chociaż nikogo zapewne nie zdziwi, jeśli w najbliższej przyszłości zostaną zastąpione testami z Javy).
Z podstawowego C++ zapożyczono do Javy większość składni. Zrezygnowano jedynie z dyrektyw, struktur, unii, wskaźników, operatorów definiowanych i wielodziedziczenia (oryg. multiple inheritance).
Jako rekompensatę zapewniono przenośność programów między dowolnymi platformami sprzętowymi i systemowymi, zdalne wywoływanie metod (funkcji składowych klas), wbudowano w język mechanizmy współbieżności i dynamicznego zarządzania pamięcią oraz uniemożliwiono wyrządzanie szkód przez aplety pochodzące z niepewnych źródeł.
Zwłaszcza ta ostatnia cecha (której poświęcono ulubione przez większość "prawdziwych" programistów wskaźniki) zadecyduje zapewne o przyszłości Javy jako języka Internetu. Istnieje bowiem gwarancja, że nikt nie poniesie uszczerbku, jeśli z ciekawości lub z potrzeby skorzysta z dowolnego apletu, który znajdzie w Internecie, w tym z takiego, który został napisany przez złośliwego programistę.
Dzięki wprowadzonym uproszczeniom, nie występują w Javie takie znane z C++ deklaracje jak na przykład
void (*signal(int, void (*)(int)))(int);
które nawet wprawnym programistom sprawiają od czasu do czasu trudności.
Na skutek dołączenia do języka obszernych bibliotek, programy napisane w Javie są dość krótkie i dużo bardziej przejrzyste niż analogiczne programy napisane w C++. Dzięki temu uruchamianie i testowanie programów staje się łatwiejsze, a kod źródłowy ma cechy samodokumentujące (oryg. self-documenting).
Wszystkie to razem powoduje, że popularność Javy stale rośnie i coraz to nowe rzesze użytkowników przekonują się do tego języka. Szczególnie widoczne jest to w znanych środowiskach akademickich, gdzie z planów dydaktycznych są na korzyść Javy eliminowane takie popularne języki programowania jak Visual Basic, Delphi i Turbo Pascal.
Programy źródłowe
Programami są aplikacje i aplety. Zarówno aplikacja jak i aplet jest zestawem definicji klas. Każda definicja klasy jest umieszczona w module źródłowym zapamiętanym w pliku. Zestaw modułów źródłowych, z których każdy zaczyna się od takiej samej, albo od równoważnej jej deklaracji pakietu, tworzy pakiet.
Do każdej aplikacji musi należeć dokładnie jeden moduł źródłowy, którego klasa publiczna zawiera publiczną i statyczną funkcję main. Taki moduł jest modułem głównym aplikacji.
Następujący moduł zawiera dokładnie jedną publiczną klasę z publiczną i statyczną funkcją main typu "void (String [])", a więc jest modułem głównym.
public
class Master { // klasa publiczna
public static void main(String args[])
{
Slave.main(args); // wywołanie funkcji Slave.main
}
}
class Slave { // klasa niepubliczna
public static void main(String args[])
{
System.out.println("Down with the slavery!");
}
}
Jak można zauważyć, nic nie stoi na przeszkodzie istnienia więcej niż jednej funkcji main. Wykonywanie programu i tak zacznie się od tej, która jest zawarta w klasie publicznej.
Definicje klas mogą być poprzedzone deklaracjami importu. Deklaracja importu jest niekiedy porównywana do dyrektywy #include, ale w istocie umożliwia jedynie skrótowe odwoływanie się do nazw pakietów i klas.
W szczególności, jeśli w pakiecie java jest zawarty pakiet awt, a w nim klasa Button, to w zasięgu deklaracji
import java.awt.Button;
albo w zasięgu ogólniejszej od niej deklaracji
import java.awt.*;
deklaracja zmiennej myButton typu Button
java.awt.Button myButton;
może być skrócona do
Button myButton;
W każdym pliku źródłowym tylko jedna z klas może być publiczna. Jeśli plik zawiera definicję klasy publicznej Name, to nazwą zawierającego ją pliku musi być Name.java.
Jeśli plik nie zawiera deklaracji importu, to domniemywa się deklarację
import java.lang.*;
a jeśli nie zawiera deklaracji pakietu, to przyjmuje się, że wszystkie klasy zdefiniowane w tym pliku należą do pakietu domyślnego.
Ponadto wymaga się zachowania następującej kolejności elementów pliku
deklaracja pakietu
deklaracje importu
definicje klas
Uwaga: Przed, po i między rozpatrzonymi składnikami modułu źródłowego mogą występować komentarze. Mają one taką samą postać jak w C++.
Kompilacja i wykonanie
Podobnie jak w C++, każdy moduł źródłowy jest kompilowany niezależnie od pozostałych. W odróżnieniu od C++ zezwala się jednak, aby odwołanie do klasy wystąpiło leksykalnie wcześniej niż definicja klasy i to nawet wówczas, gdy zarówno odwołanie jak i definicja występują w tym samym module źródłowym.
Uwaga: Zarówno kompilator, jak i interpreter Javy musi mieć dostęp do B-kodu klas predefiniowanych. Katalog, w którym znajdują się pliki zawierające ten kod specyfikuje się zazwyczaj za pomocą parametru środowiska CLASSPATH, na przykład
path CLASSPATH=.;d:\cafe\java\lib\classes.zip
W wypadku użycia zestawu JavaSDK, wywołanie kompilatora w celu skompilowania modułu zawartego w pliku Name.java przybiera postać
javac Name.java
W następstwie udanej kompilacji powstaje plik Name.class zawierający B-kod klasy.
Jeśli przyjąć, że plik Name.java zawiera moduł główny aplikacji, to w celu jej wykonania, a po uprzednim utworzeniu pliku Name.class, należy użyć polecenia
java Name
(na uwagę zasługuje brak rozszerzenia .class).
Jeśli natomiast dokument zawarty w pliku Name.html zawiera frazę opisującą aplet, na przykład
<applet code=Greetings.class width=150 height=150> </applet>
to w celu zinterpretowania dokumentu i wykonania apletu należy użyć polecenia
appletviewer Name.html
W wypadku sięgnięcia do apletów znajdujących się w sieci Internet, można użyć dokumentu HTML zawierającego bardziej złożony opis apletu, na przykład podobny do
<applet codebase="http://java.sun.com/NervousText"
code=NervousText.class
width=400 height=75 allign=center>
<param name="text" value="This is the Applet Viewer.">
<blockquote>
<hr>
If you want to see a nervous dancing text
use Java-enabled browser like Netscape
<hr>
</blockquote>
</applet>
W opisie tym, za pomocą parametru codebase, podaje się nazwę katalogu zawierającego B-kod apletu.
Jeśli użyta przeglądarka nie potrafi wyświetlić apletu, to na jej ekranie pojawi się tekst podany między poleceniami <hr>.
Biblioteki
Rolę bibliotek pełnią w Javie pakiety. Do języka podstawowego dołączono trzy z nich: java.lang, java.io i java.util.
W pakiecie java.lang zawarto m.in. definicje klas
String do wykonywania operacji na ciągach znaków,
Math do wykonywania obliczeń numerycznych,
Thread do realizowania współbieżności,
System do wykonywania czynności systemowych.
W pakiecie java.io zawarto m.in. definicje klas
File do wykonywania operacji na plikach i katalogach,
InputStream do wprowadzania danych,
OutputStream do wyprowadzania danych.
W pakiecie java.util zawarto m.in. definicje klas
Vector do posługiwania się kolekcjami,
Enumeration do konstruowania iteratorów,
Date do operowania czasem i datą,
Hashtable do przetwarzania danych opatrzonych kluczami.
Do najważniejszych pakietów dodatkowych, nie włączonych do definicji języka, należą java.applet i java.awt. Umożliwiają one tworzenie aplikacji i apletów wykonywanych w środowisku graficznym (np. Windows, MacOS, Solaris).
Oblicze graficzne
Aplikacje i aplety o obliczu graficznym (oryg. graphical interface) z reguły posługują się pakietem java.awt, za pomocą którego tworzą okna o wyglądzie związanym z użytym systemem operacyjnym, ale o identycznej funkcjonalności.
Aplikacje
Prosta aplikacja do wykreślenia znanego już pozdrowienia, pokazana podczas wykonania na Ekranie Aplikacja Greet, ma następującą postać
Ekran Aplikacja Greet
### greet.gif
import java.awt.*;
public
class Greet {
public static void main(String args[])
{
new GreetFrame("Greetings");
}
}
class GreetFrame extends Frame {
GreetFrame(String caption)
{
super(caption);
resize(200, 200);
show();
}
public void paint(Graphics gDC)
{
gDC.drawString("Hello, I am Jan B.", 20, 20);
}
public boolean handleEvent(Event evt)
{
if(evt.id == Event.WINDOW_DESTROY)
System.exit(0);
return false;
}
}
Podana aplikacja składa się z klasy Greet oraz z klasy GreetFrame zdefiniowanej jako podklasa (klasa pochodna) klasy Frame.
W celu wykonania przytoczonej aplikacji (po uprzednim skompilowaniu jej do pliku Greet.class) należy wydać polecenie
java Greet
Spowoduje to kolejno
utworzenie obiektu klasy GreetFrame
new GreetFrame("Greetings");
wywołanie konstruktora klasy Frame w celu utworzenia okna aplikacji
super(caption);
ustalenie rozmiarów okna na 200 x 200 pikseli
resize(200, 200);
wyświetlenie okna
show();
wykreślenie pozdrowienia w miejscu o podanych współrzędnych
gDC.drawString("Hello, I am Jan B.", 20, 20);
Metoda paint jest wywoływana przez System. Odbywa się to automatycznie, jeśli tylko zajdzie potrzeba ponownego wykreślenia okna.
Zdarzenia zewnętrzne są obsługiwane przez metodę handleEvent. Jej reakcją na zamknięcie okna jest w podanym programie zakończenie wykonywania aplikacji
System.exit(0);
Aplety
Analogiczny aplet, przeznaczony do wyświetlenia za pomocą przeglądarki AppletViewer (wchodzącej w skład JavaSDK), albo za pomocą przeglądarki WWW, takiej jak na przykład Netscape albo Internet Explorer, pokazany podczas wykonania na Ekranie Aplet Greet, ma następującą postać
Ekran Aplet Greet
### applet.gif
import java.applet.*;
import java.awt.*;
public
class Greet extends Applet {
public void paint(Graphics gDC)
{
gDC.drawString("Hello, I am Jan B.", 20, 20);
}
}
W celu wykonania apletu (po uprzednim skompilowaniu go do pliku Greet.class) należy przeglądarce podać nazwę pliku HTML zawierającego opis apletu.
Plik taki, na przykład SayHello.html, może mieć następującą postać
<html>
<head>
<title> Ta strona zawiera aplet </title>
</head>
<body>
<p> Ten aplet oznajmia:
<br>
<applet codebase="d:\applets"
code=Greet.class
width=150 height=150>
</applet>
</body>
</html>
Przeglądarka AppletViewer
Wywołanie przeglądarki AppletViewer odbywa się za pomocą polecenia
appletviewer SayHello.html
Przeglądarka jest w stanie rozpoznać tylko frazy <applet. Dla każdej z nich (tu jest tylko jedna!) zostanie wyświetlone odrębne okno, a w jego górnej części pojawi się napis Applet.
Jeśli program d:\applets\Greet.class istnieje, to wykona się w oknie zatytułowanym
Applet Viewer: Greet.class
Wówczas w obszarze roboczym okna pojawi się napis
Hello, I am Jan B.
a w wierszu stanu napis
start: applet started
W przeciwnym razie pulpit apletu (oryg. client area) pozostanie pusty, a w wierszu stanu pojawi się napis
start: applet not initialized
Przeglądarka Netscape
Zgodnie z zasadami interpretowania dokumentów HTML, bezpośrednio po podaniu w klatce Location napisu
d:\applets\SayHello.html
na pasku tytułowym okna przeglądarki wyświetli się napis
Ta strona zawiera aplet
Natomiast w górnej części obszaru roboczego pojawi się napis
Ten aplet oznajmia:
a poniżej napis
Hello, I am Jan B.
Środowiska zintegrowane
Znacznie wygodniejszym sposobem programowania w Javie jest użycie Zintegrowanego Środowiska Rozwojowego (oryg. Integrated Development Environment), takiego jak na przykład Symantec Cafe, pokazanego na Ekranie Symantec Cafe. Każde z nich oferuje wygody podobne do tych jakie dostarczają środowiska Visual Basic, Delphi i Visual C++.
Ekran Symantec Cafe
### cafe.gif
Rada praktyczna
W środowisku Cafe, w celu uniknięcia zbyt szybkiego zniknięcia okna aplikacji niegraficznej, można zastosować oczywisty pomysł zawarty w następującym programie
public
class Greet {
public static void main(String args[])
throws Exception
{
System.out.println("Hello, I am Jan B.");
pause(); // wstrzymanie zakończenia
}
static void pause()
throws Exception
{
System.in.read();
}
}
Dzięki wywołaniu funkcji pause zakończenie wykonywania programu nastąpi dopiero po naciśnięciu klawisza Enter.
Wnioski na przyszłość
Zjawiska znanego jako Java nie wolno ignorować. Postępująca rewolucja Internetowa może Javę tylko wzmocnić. Wiele małych aplikacji oraz większość apletów będzie z pewnością pisana w Javie.
A co z wielkimi aplikacjami? Do niedawna odpowiadałem na to pytanie następująco:
Java nie nadaje się jeszcze do pisania wielkich aplikacji. Na tym polu wciąż króluje C++. Ale już za kilka lat, kiedy postęp w technice kompilacyjnej i zwiększenie szybkości komputerów zatrze różnice między językami kompilowanymi i interpretowanymi, następcą C++ stanie się zapewne Java.
Obecnie, pod wpływem Scotta McNeally, szefa firmy Sun, odpowiadam inaczej:
Pytanie o wielkie aplikacje jest źle postawione. Oby takich aplikacji powstawało jak najmniej. A to dlatego, że istota nadchodzącego paradygmatu sieciowego jest inna:
To nie jedno i wielkie, ale małe wśród mnogości małych, jest przyszłością rozproszonego systemu informacyjnego.
Część II Aromat Javy
Programiści dzielą się na tych, którzy najpierw poznają nowy język, a dopiero potem zaczynają się nim posługiwać oraz na tych, którzy zabierają się do programowania nie znając podstaw języka.
Chociaż należę do tych pierwszych, spotykam wielu, którzy uczą się języka tylko na przykładach programów. Dlatego z myślą o nich, przedstawiam trzy programy, napisane przeze mnie wkrótce po przeczytaniu Specyfikacji Języka Java. Zawierają one wiele nietrywialnych elementów, które powinny przybliżyć aromat języka.
Nie mam wątpliwości, że ci, którzy znają C++ i mają przynajmniej mgliste pojęcie o programowaniu obiektowym polubią Javę natychmiast, oraz że nie będą mieli większych trudności w zrozumieniu moich pierwszych programów.
Natomiast pozostali niech starają się zrozumieć tyle ile zdołają, a w wypadku napotkania przeszkód nie do przebrnięcia niech przejdą do rozdziału Programowanie, to jest do miejsca od którego wszystko co niezbędne do poznania Javy zostanie wyłożone systematycznie i bez niedomówień.
Uwaga: Podrozdział Java a język C++ zawiera opis podstawowych różnic między Javą i C++. Powinien być przestudiowany bardzo uważnie i w całości. Jeśli w czasie tej lektury wystąpią jakiekolwiek trudności, to oznacza to, że czytelnik nie zna C++ i brak ten musi uzupełnić we własnym zakresie (polecam moje książki: Sekrety C++ i ANSI C++).
Java a język C++
U podstaw zrozumienia Javy leży pojęcie odnośnika (zmiennej do identyfikowania innych zmiennych) oraz odniesienia (danej identyfikującej zmienną, przypisywanej odnośnikowi).
Ponieważ odnośniki istnieją w C++, można je przypomnieć odwołując się do następującego programu (którego zresztą, jako jednego z nielicznych, nie da się zapisać w Javie).
#include <iostream.h>
int main(void)
{
int one = 10, two = 20;
int &max(int &, int &);
cout << max(one, two) << endl; // 20
return 0;
}
int &max(int &refOne, int &refTwo)
{
return refOne > refTwo ? refOne : refTwo;
}
W chwili wywołania funkcji max odnośnik refOne jest inicjowany odniesieniem do zmiennej one, a odnośnik refTwo jest inicjowany odniesieniem do zmiennej two.
Rezultatem funkcji max jest odnośnik typu "int &" zainicjowany odniesieniem do większego z argumentów.
Klasy
Podobnie jak w C++, klasa jest opisem rodziny obiektów. Struktura obiektów jest określona przez pola klasy, a operacje na obiektach są określone przez jej konstruktory i metody.
Poza polami, konstruktorami i metodami klasa może zawierać także zmienne i funkcje statyczne. Takie składniki klasy (oryg. members) nie są związane z poszczególnymi obiektami, ale należą do całej klasy.
Istotna z punktu widzenia ochrony informacji dostępność (oryg. acessibility) pól, metod, zmiennych i funkcji jest w Javie określana indywidualnie, a nie tak jak w C++, w ramach sekcji. Jeśli dostępności pewnych składników klasy nie określi się jawnie, to będą one dostępne w pakiecie klas. Stanowi to odmianę deklaracji zaprzyjaźnienia (oryg. friend) znanej z C++.
public
class Complex { // publiczna klasa
protected double re; // chronione pole
private double im; // prywatne pole
public Complex
(double re, double im) // publiczny konstruktor
{
this.re = re;
this.im = im;
}
double abs() // przyjazna metoda
{
return Math.sqrt(re * re + im * im);
}
}
Ponieważ w Javie nie ma list inicjacyjnych, więc inicjowanie pól obiektu odbywa się w ciele konstruktora.
Zmienne
Deklaracje zmiennych typów predefiniowanych (w tym "char", "int", "long", "boolean") mają w Javie taką samą interpretację jak w ANSI C++.
Zmienne typu "char" są 16-bitowe, dzięki czemu umożliwiają reprezentowanie wszystkich znaków Unikodu (oryg. Unicode).
W szczególności
int var = 12
jest deklaracją zmiennej var typu "int" zainicjowanej daną o wartości 12, a
char chr = 'ś'
jest deklaracją zmiennej chr zainicjowaną kodem litery ś.
Natomiast deklaracje, w których występuje typ definiowany są interpretowane inaczej niż w C++.
Na przykład
class Point {
// ...
}
Point point;
Zadeklarowano odnośnik point do zmiennych klasy Point, a nie obiekt point klasy Point.
Obiekty
Ponieważ w Javie nie ma struktur ani unii, więc każdy jej obiekt jest egzemplarzem pewnej klasy (oryg. class instance). Z klasą są związane jej zmienne i funkcje, a w obiektach są zawarte zmienne, konstruktory i metody.
Właściwości zmiennych klasy, zarówno tych które są wspólne jej wszystkim obiektom, jak i tych, które wchodzą w skład poszczególnych obiektów, są określone przez deklaracje pól klasy (oryg. class field).
Uwaga: Ze względu na efektywność implementacji, kod metod klasy nie jest powielany i znajduje się fizycznie (ale nie logicznie!) poza obiektami klasy.
W następującej definicji klasy sklasyfikowano jej składniki.
class Fixed {
static int count = 0; // zmienna
static int getCount() // funkcja
{
return count;
}
int value; // pole
Fixed(int val) // konstruktor
{
value = val;
count++;
}
int getValue() // metoda
{
return value;
}
}
Fabrykowanie obiektów
W celu utworzenia obiektu należy użyć wyrażenia fabrykującego (oryg. factory expression) o postaci
new TypObiektowy(Arg, Arg, ... , Arg)
Jego rezultatem jest odnośnik zawierający odniesienie do właśnie sfabrykowanej zmiennej (w Javie słowo kluczowe new nie jest operatorem!).
public void paint(Graphics gDC)
{
Point point;
point = new Point(10, 20);
gDC.drawLine(0, 0, point.x, point.y);
}
Odniesienie do obiektu sfabrykowanego podczas opracowania wyrażenia
new Point(10, 10)
przypisano odnośnikowi point.
Uwaga: W odróżnieniu od C++, w Javie nie można za pomocą operacji new, tworzyć zmiennych skalarnych nie-obiektowych. A zatem nie istnieją wyrażenia fabrykujące takie jak na przykład
new int
albo
new Point
Odnośniki a wskaźniki
Reakcją każdego kto dowiaduje się, że w Javie nie ma wskaźników jest pytanie:
A jak programuje się listowe struktury danych?
Bo przecież w C++ bez wskaźników nie ma na to sposobu.
Okazuje się jednak, że odnośniki można w Javie nie tylko inicjować, ale że można im także przypisywać odniesienia. To już rozwiązuje problem.
W szczególności następujący program w C++
include <iostream.h>
struct Item {
Item *next;
int value;
Item(int i) : value(i)
{
}
};
int main(void)
{
Item *head = 0;
for(int i = 0; i < 10 ; i++) {
Item *newItem = new Item(i);
newItem->next = head;
head = newItem;
}
// ...
return 0;
}
przybiera w Javie postać
class Item {
Item next;
int value;
Item(int i)
{
value = i;
}
}
public
class Main {
public static void main(String args[])
{
Item head = null;
for(int i = 0; i < 10 ; i++) {
Item newItem = new Item(i);
newItem.next = head;
head = newItem;
}
// ...
}
}
A zatem brak wskaźników w Javie nie stanowi żadnego ograniczenia w możliwościach programowania dynamicznych struktur danych.
Między bajki można także włożyć opowieści o tym dla jakich to wzniosłych celów poświęcono wskaźniki. W istocie bezpieczeństwo Javy i wykluczenie możliwości programowania w niej wirusów, nie wynika z pozbycia się wskaźników, ale z wyeliminowania konwersji wskaźnikowych oraz z rygorystycznej kontroli ładowania i interpretowania B-kodu.
Procedury
Procedurami są konstruktory, funkcje i metody klasy (w Javie nie ma funkcji i zmiennych globalnych!). Identycznie jak w C++, każda procedura znajduje się w zakresie (oryg. scope) jej klas macierzystych, a zatem z ciała procedury są dostępne wszystkie składniki klasy, w tym również te, których deklaracje występują poniżej definicji procedury.
Na przykład
class Master {
int dx()
{
return dx;
}
int dx = 0;
// ...
}
Zasługuje na uwagę, że w Javie wolno definiować pole i metodę o takim samym identyfikatorze.
Zmienne lokalne
Zakresem i jednocześnie zasięgiem deklaracji zmiennej lokalnej (w tym parametru) procedury jest cały blok w którym wystąpiła deklaracja (począwszy od punktu tuż za deklaratorem).
Zasięgiem deklaracji zmiennych sterujących instrukcji for jest tylko ciało tej instrukcji.
void Sub(int x, int y)
{
for(int i = 0; false ; );
for(int i = 1; false ; ); // dobrze (w ANSI C++ błąd!)
int v = i; // błąd (nieznany inicjator)
int j = 2;
for(int j = 2; false ; ); // błąd (ponowna deklaracja)
int y; // błąd (ponowna deklaracja)
int z = 10;
int v = 20;
{
int v = 30; // błąd (ponowna deklaracja)
int u = 40;
}
{
int u = 50; // dobrze!
int z = 60; // błąd (ponowna deklaracja)
}
}
Odnośnik this
W ciele konstruktora i metody jest dostępny odnośnik this identyfikujący obiekt na rzecz którego wywołano konstruktor albo metodę.
Pierwszą (i tylko pierwszą!) instrukcją konstruktora może być instrukcja
this(Arg, Arg, ... , Arg);
albo
super(Arg, Arg, ... , Arg);
W pierwszej z nich jest wywoływany konstruktor danej klasy, a w drugiej konstruktor jej nadklasy (klasy bazowej). Jeśli w ciele konstruktora nie wystąpi żadna z tych instrukcji, to domniema się, że jego pierwszą instrukcją jest
super();
Dziedziczenie
Dziedziczenie wyraża się za pomocą słowa kluczowego extends.
Ponieważ istnieje tylko jedna klasa pierwotna (jest nią Object), więc w Javie hierarchia klas jest drzewem, a nie lasem (grafem acyklicznym) jak w C++.
class MyObject extends Object {
// ...
}
Polimorfizm
Każda metoda Javy jest domyślnie wirtualna (oryg. virtual). Każde wywołanie metody, która nie jest prywatna jest polimorficzne, tj.
Do wykonania jest wybierana metoda identyfikowana przez odniesienie przypisane odnośnikowi na rzecz którego odbywa się wywołanie.
Uwaga: Podczas kompilowania programu typ odnośnika służy tylko do upewnienia się, że w jego klasie (albo w interfejsie) występuje definicja wywoływanej metody. Podczas wykonywania programu typ odnośnika nie jest już brany pod uwagę.
class Horse {
// ...
void draw(Graphics gDC)
{
// ... wykreśl konia
}
static void fun(Graphics gDC, Horse horse)
{
horse.draw(gDC);
}
}
class Zebra extends Horse {
// ...
void draw(Graphics gDC)
{
// ... wykreśl zebrę
}
}
Wywołanie
horse.draw(gDC)
jest polimorficzne.
Jeśli parametr horse identyfikuje obiekt klasy Zebra, na przykład po wywołaniu funkcji fun z procedury paint
public void paint(Graphics gDC)
{
fun(gDC, new Zebra("Stripes", 8));
}
to w funkcji fun zostanie wywołana metoda draw klasy Zebra, mimo iż horse jest odnośnikiem do obiektów klasy Horse.
Implementowanie
Mimo iż każda klasa może mieć co najwyżej jedną nadklasę (w Javie nie ma wielodziedziczenia!), to jednak może implementować dowolnie wiele interfejsów.
Uwaga: Interfejs jest odmianą klasy abstrakcyjnej, która zawiera tylko deklaracje metod i definicje zmiennych.
Jeśli klasa implementuje interfejs, a nie ma być klasą abstrakcyjną, to musi dostarczyć definicje wszystkich metod zadeklarowanych w interfejsie.
Uwaga: Implementowanie interfejsu stosuje się najczęściej wówczas, gdy zestaw klas ma bardzo odległego, albo niedostępnego przodka, ale gdy musi być wyposażony we wspólną cechę, wyrażaną takimi słowami jak: skalowalna, przemieszczalna, przeliczalna, wykonywalna, itp.
class Shape {
// ...
}
interface Drawable {
// ...
void draw(Graphics gDC);
}
class DrawableShape extends Shape implements Drawable {
// ...
DrawableShape()
{
}
public void draw(Graphics gDC)
{
// ...
}
}
Wywoływanie
Każdemu odnośnikowi typu interfejsowego można przypisać odniesienie do obiektu klasy implementującej ten interfejs. Na rzecz takiego odnośnika można wówczas wywołać dowolną metodę tej klasy. Wywołanie takie jest wówczas polimorficzne.
Na przykład
public void paint(Graphics gDC)
{
Drawable item = new DrawableShape();
item.draw(gDC);
}
Wywołanie
item.draw(gDC);
jest polimorficzne.
Mimo iż odnośnik item jest klasy Drawable, następuje wywołanie metody draw klasy DrawableShape.
Tablice
Deklaracja tablicy w Javie jest deklaracją odnośnika do tablicy. Sama tablica musi być utworzona za pomocą wyrażenia fabrykującego.
Uwaga: Każda tablica jest obiektem klasy pochodnej klasy Object i implementuje interfejs Cloneable. Obiekt tablicowy jest wyposażony w publiczne pole length określające liczbę elementów tablicy.
Elementy predefiniowane
W celu utworzenia tablicy o elementach typu predefiniowanego należy użyć wyrażenia fabrykującego
new Typ [Rozmiar]
Jego rezultatem jest anonimowy odnośnik zainicjowany odniesieniem do właśnie sfabrykowanej tablicy.
Na przykład, wykonanie instrukcji
int arr[]; // deklaracja odnośnika
arr = new int [3]; // utworzenie tablicy
for(int i = 0; i < arr.length ; i++)
arr[i] = 0;
powoduje utworzenie i zainicjowanie (liczbą 0) wszystkich elementów tablicy identyfikowanej przez odnośnik arr.
Elementy obiektowe
W celu utworzenia tablicy o elementach typu obiektowego należy użyć wyrażenia fabrykującego
new Typ [Rozmiar]
Jego rezultatem jest odnośnik zainicjowany odniesieniem do wektora odnośników do elementów właśnie sfabrykowanej tablicy.
Na przykład, wykonanie instrukcji
String arr[]; // deklaracja odnośnika
arr = new String [3]; // utworzenie wektora odnośników
for(int i = 0; i < arr.length ; i++)
arr[i] = new String(); // utworzenie elementu podstawowego
powoduje utworzenie i zainicjowanie (pustym łańcuchem) wszystkich elementów podstawowych tablicy identyfikowanej przez odnośnik arr.
Wyjątki
Jeśli wykonanie instrukcji może spowodować powstanie sytuacji wyjątkowej nie wywodzącej się od RuntimeException i Error, to taka instrukcja musi być ujęta w blok instrukcji try, albo nagłówek procedury obejmującej tę instrukcję musi zawierać frazę throws wyszczególniającą klasę wyjątku.
W pierwszym przypadku we frazie catch określa się co należy uczynić w razie powstania sytuacji wyjątkowej, a w drugim pozostawia się taką decyzję procedurze wywołującej.
Na przykład, podczas wykonywania procedury
void setChar(char arr[], int pos, FileInputStream src)
{
int chr = src.read(); // IOException
arr[pos] = chr; // IndexOutOfBoundsException
}
mogą powstać dwie sytuacje wyjątkowe: IOException związana z nieudaną operacją wejścia oraz IndexOutOfBoundsException (klasy pochodnej od RuntimeException) związana z niewłaściwie dobranym indeksem tablicy.
Pierwsza z nich wymaga ujęcia instrukcji
int chr = src.read(); // IOException
w blok instrukcji try, na przykład
void setChar(char arr[], int pos, FileInputStream src)
{
try {
int chr = src.read(); // IOException
}
catch(IOException e) {
// ... // reakcja na sytuację wyjątkowa
}
arr[pos] = chr; // IndexOutOfBoundsException
}
albo użycia frazy throws wyszczególniającej klasę IOException
void setChar(char arr[], int pos, FileInputStream src)
throws IOException
{
int chr = src.read(); // IOException
arr[pos] = chr; // IndexOutOfBoundsException
}
natomiast druga nie wymaga takich zabiegów.
Uwaga: Jeśli użyto frazy throws, to instrukcja wywołująca procedurę setChar musi być ujęta w blok instrukcji try, albo procedura zawierająca taką instrukcję musi zawierać frazę throws wyszczególniającą klasę IOException.
Mój pierwszy program
Przedstawiony tu program, składa się z sekwencji budowanych następny-po-poprzednim apletów, z których ostatni rozwiązuje następujące zadanie
Posługując się techniką inkrementalnego programowania obiektowego napisać aplet umożliwiający tworzenie odrębnych okien wyposażonych w menu File i Help, w których metodą przeciągania można wykreślać okręgi i elipsy, a następnie animować je w odrębnych wątkach.
Uwaga: Zgodnie z techniką wieloużytkowego (oryg. reuseable) programowania obiektowego, jako metodę rozwoju programu przyjęto zastosowanie dziedziczenia i polimorfizmu.
ClickMeApplet
Przytoczony tu aplet, pokazany podczas wykonania na Ekranie Aplet ClickMeApplet, wyświetla napis Clickme!. W wierszu stanu przeglądarki apletów pojawia się napis
Jan B. Applet Tutorial
Ekran Aplet ClickMeApplet
### clickme.gif
Klasa ClickMeApplet jest klasą pochodną klasy Applet. Łatwo zauważyć, że program nie zawiera funkcji main, ani dyrektyw #include.
Deklaracje importu umożliwiają odwoływanie się do procedur przez ich skrócone nazwy, np. po prostu
drawString
zamiast
java.awt.Graphics.drawString
// ========================== ClickMeApplet
import java.applet.*;
import java.awt.*;
public
class ClickMeApplet extends Applet {
String clickMsg = "Click me!";
public void paint(Graphics gDC)
{
drawClickMe(gDC);
showStatus("Jan B. Applet Tutorial");
}
void drawClickMe(Graphics gDC)
{
gDC.drawString(clickMsg, 20, 20);
}
}
//=========================== (ClickMeApplet)
CircleApplet
Przytoczony tu aplet, pokazany podczas wykonania na Ekranie Aplet CircleApplet, wykreśla okrąg o środku w punkcie kliknięcia. Okrąg jest umieszczony w niewidocznym prostokącie o wymiarach 8 x 8 pikseli.
Ekran Aplet CircleApplet
### circle.gif
Ponieważ klasa CircleApplet jest podklasą klasy ClickMeApplet, więc procedura paint przedefiniowuje (oryg. override) odpowiadającą jej procedurę nadklasy.
Wywołanie metody klasy bazowej odbywa się za pomocą słowa kluczowe super. Dlatego tuż przed pierwszym kliknięciem jest wyświetlany napis
Click Me!
W chwili zwolnienia przycisku myszki System wywołuje metodę mouseUp. Rejestruje ona współrzędne kliknięcia, a wywołując metodę repaint zwraca się do Systemu o wywołanie metody paint.
Ponieważ metoda paint może być wywołana jeszcze przed kliknięciem, więc stosownie do sytuacji wywołuje metodę paint klasy bazowej (w celu wykreślenia napisu Click me!) albo wykreśla okrąg.
// ========================== CircleApplet
import java.applet.*;
import java.awt.*;
public
class CircleApplet extends ClickMeApplet {
int left, top, width = 32, height = 32;
boolean mouseClicked = false; // typ boolean
public boolean mouseUp(Event evt, int x, int y)
{
left = x - (width >> 2);
top = y - (height >> 2);
repaint();
return mouseClicked = true;
}
public void paint(Graphics gDC)
{
if(mouseClicked)
gDC.drawOval(left, top,
width >> 1, height >> 1);
else
super.paint(gDC);
}
}
//=========================== (CircleApplet)
OvalApplet
Przytoczony tu aplet, pokazany podczas wykonania na Ekranie Aplet OvalApplet, wykreśla elipsę o osiach równych 1/4 rozmiarów ramki apletu.
Metoda init, wywołana przez System tuż po zainicjowaniu apletu, rozpoznaje jego parametry, podane w dokumencie HTML
<applet code=MyApplet.class width=200 height=100>
</applet>
===================================================
dzięki czemu dostosowuje rozmiar elipsy do rozmiarów apletu.
Metoda paint wykreśla dodatkowo (gdyż wywołuje metodę paint nadklasy) prostokątną ramkę wytyczającą pulpit apletu.
Metodę drawClickMe przedefiniowano metodą wykreślającą napis ClickMe! umieszczony dokładnie w połowie odległości między górną i dolną krawędzią ramki. Do wykonania tego zadania wykorzystano metrykę czcionki.
Ekran Aplet OvalApplet
### oval.gif
// ========================== OvalApplet
import java.applet.*;
import java.awt.*;
public
class OvalApplet extends CircleApplet {
public void init()
{
String parWidth = getParameter("width"),
parHeight = getParameter("height");
width = Integer.parseInt(parWidth);
height = Integer.parseInt(parHeight);
}
public void paint(Graphics gDC)
{
Color color = gDC.getColor();
gDC.setColor(Color.black);
gDC.drawRect(0, 0, width-1, height-1); // prostokąt
gDC.setColor(color);
super.paint(gDC);
}
void drawClickMe(Graphics gDC)
{
FontMetrics metrics = gDC.getFontMetrics();
int ascent = metrics.getAscent(),
descent = metrics.getDescent(),
height = metrics.getHeight(),
leading = height - (ascent + descent),
top = (this.height - (ascent + descent)) / 2,
bottom = top + height - leading;
gDC.drawString(clickMsg, 20, bottom);
}
}
//=========================== (OvalApplet)
EllipseApplet
Przytoczony tu aplet, pokazany podczas wykonania na Ekranie Aplet EllipseApplet, wykreśla to co poprzednio, ale dzięki wyposażeniu ramki w przyciski Red (czerwony), Green (zielony) i Blue (niebieski) umożliwia wykreślanie elips w jednym z tych kolorów.
Ekran Aplet EllipseApplet
### ellipse.gif
Przyciski są tworzone w metodzie init i są obsługiwane przez metodę action. Metoda action jest wywoływana przez System w chwili kliknięcia przycisku.
// ========================== EllipseApplet
import java.applet.*;
import java.awt.*;
public
class EllipseApplet extends OvalApplet {
Color color;
public void init() // metoda init
{
super.init();
add(new Button("Red")); // przycisk Red
add(new Button("Green")); // przycisk Green
add(new Button("Blue")); // przycisk Blue
}
public boolean action(Event evt, Object arg)
{
color = null;
if(arg == "Red")
color = Color.red;
else if(arg == "Green")
color = Color.green;
else if(arg == "Blue")
color = Color.blue;
return color != null;
}
public void paint(Graphics gDC)
{
gDC.setColor(color);
super.paint(gDC);
}
}
//=========================== (EllipseApplet)
FrameApplet
Przytoczony tu aplet, pokazany podczas wykonania na Ekranie Aplet FrameApplet, wyświetla niezależne okno, w którym można wykreślać elipsy i przeciągać je myszką (oryg. drag) w dowolne miejsce.
Ekran Aplet FrameApplet
### frame.gif
Utworzenie okna, opisanego przez klasę FrameWindow, następuje po wykonaniu Ctrl-kliknięcia. Każda taka operacja tworzy dodatkowe i niezależne okno, wstępnie przesunięte o 10 pikseli względem poprzedniego.
Za pomocą uprzednio zdefiniowanych przycisków można wybrać kolor obrzeża elipsy.
Przeciąganie elips obsługuje metoda mouseDrag. Wykreślanie elips w docelowym kolorze realizuje metoda mouseUp, a zamknięcie okna metoda handleEvent.
// ========================== FrameApplet
import java.applet.*;
import java.awt.*;
public
class FrameApplet extends EllipseApplet {
static int pos = 0;
public boolean mouseUp(Event evt,int x, int y)
{
if((evt.modifiers & Event.CTRL_MASK) != 0) {
Frame frame = new FrameWindow(color);
frame.reshape(pos += 10, pos += 10, 200, 100);
frame.show(); // pokaż okno
return true;
} else
return super.mouseUp(evt, x, y);
}
}
// ====== FrameWindow
class FrameWindow extends Frame {
Color color;
int xOld, yOld;
FrameWindow(Color color)
{
super("Press and Drag");
this.color = color;
}
void drawEllipse(int x, int y,
Color color,
boolean xorMode)
{
Color thisColor = this.color;
Graphics gDC = getGraphics();
gDC.setColor(color);
if(xorMode)
gDC.setXORMode(Color.white);
int w = size().width >> 2,
h = size().height >> 2;
gDC.drawOval(x - w/2, y - h/2, w, h);
gDC.setColor(thisColor);
}
public boolean handleEvent(Event evt)
{
if(evt.id == Event.WINDOW_DESTROY) {
hide(); // ukryj okno
dispose(); // zniszcz okno
return true;
} else
return super.handleEvent(evt);
}
public boolean mouseDown(Event evt, int x, int y)
{
drawEllipse(x, y, Color.gray, true);
xOld = x;
yOld = y;
return true;
}
public boolean mouseDrag(Event evt, int x, int y)
{
drawEllipse(xOld, yOld, Color.gray, true);
mouseDown(evt, x, y);
return true;
}
public boolean mouseUp(Event evt, int x, int y)
{
drawEllipse(x, y, color, false);
return true;
}
}
//=========================== (FrameApplet)
MenuAplet
Przytoczony tu aplet, pokazany podczas wykonania na Ekranie Aplet MenuApplet, wyposaża okno w menu File i Help. Menu File zawiera polecenia New i Exit, a menu Help polecenie About. Menu są wyświetlane, ale jeszcze nie funkcjonują.
Ekran Aplet MenuApplet
### menu.gif
// ========================== MenuApplet
import java.applet.*;
import java.awt.*;
public
class MenuApplet extends FrameApplet {
Frame createFrame(Color color)
{
return new MenuWindow(color);
}
}
// ====== Menu Window
class MenuWindow extends FrameWindow {
MenuWindow(Color color)
{
super(color);
MenuBar menuBar = new MenuBar();
setMenuBar(menuBar);
Menu fileMenu = new Menu("File");
fileMenu.add("New");
fileMenu.add("-");
fileMenu.add("Exit");
menuBar.add(fileMenu);
Menu helpMenu = new Menu("Help");
helpMenu.add("About");
menuBar.add(helpMenu);
menuBar.setHelpMenu(helpMenu);
}
}
//=========================== (MenuApplet)
ActiveMenuAplet
Przytoczony tu aplet określa czynności opisane przez uprzednio zdefiniowane menu. Polecenie File/New wyczyszcza ramkę okna. Polecenie Help/About wyświetla informację o prawach autorskich, a polecenie File/Exit kończy wykonywanie programu.
Na ekranie Dialog About pokazano okno dialogowe About, a na Ekranie Dialog Quit okno dialogowe Quit.
Ekran Dialog About
### about.gif
Ekran Dialog Quit
### quit.gif
W programie posłużono się pojęciem panelu jako pojemnika komponentów graficznych, realizującego ich zadany rozkład.
Rozkład FlowLayout rozmieszcza komponenty jeden-za-drugim, ale tak, aby każdy całkowicie mieścił się w wierszu.
Rozkład BorderLayout dzieli pojemnik na 5 części: East (wschodnią), West (zachodnią), North (północną), South (południową) i Center (środkową). Nazwy te są używane podczas wstawiania komponentów do pojemnika.
Uwaga: Każdy aplet jest panelem o domyślnym rozkładzie FlowLayout. Natomiast okno jest pojemnikiem o domyślnym rozkładzie BorderLayout.
// ========================== ActiveMenuApplet
import java.applet.*;
import java.awt.*;
public
class ActiveMenuApplet extends MenuApplet {
Frame createFrame(Color color)
{
return new ActiveMenuWindow(color);
}
}
// ====== ActiveMenuWindow
class ActiveMenuWindow extends MenuWindow {
ActiveMenuWindow(Color color)
{
super(color);
}
public boolean action(Event evt, Object arg)
{
if(arg.equals("New"))
repaint();
else if(arg.equals("Exit")) {
Dialog quitDialog =
new QuitDialog(this);
quitDialog.pack();
quitDialog.show();
} else if(arg.equals("About")) {
Dialog aboutDialog =
new AboutDialog(this);
aboutDialog.pack();
aboutDialog.show();
} else
return super.action(evt, arg);
return true;
}
}
// ====== AboutDialog
class AboutDialog extends Dialog {
Button okButton;
public AboutDialog(Frame parent)
{
super(parent, "About dialog", false);
BorderLayout appletLayout = new BorderLayout(15, 15);
setLayout(appletLayout); // ustaw rozkład apletu
Label label = new Label("Copyright(c) 1996 Jan B.");
add("North", label);
Panel panel = new Panel(); // utwórz panel
okButton = new Button("OK"); // utwórz przycisk
panel.add(okButton); // wstaw przycisk do panelu
add("South", panel); // wstaw panel do apletu
pack();
}
public boolean action(Event evt, Object arg)
{
if(evt.target == okButton) {
hide();
dispose();
return true;
}
return false;
}
}
// ====== QuitDialog
class QuitDialog extends Dialog {
Button yesButton;
QuitDialog(Frame parent)
{
super(parent, "Quit dialog", false);
setLayout(new BorderLayout(15, 15));
Label label =
new Label("Do you really want to exit?");
add("North", label);
Panel panel = new Panel();
yesButton = new Button("Yes");
panel.add(yesButton);
Button noButton = new Button("No");
panel.add(noButton);
add("South", panel);
pack();
}
public boolean action(Event evt, Object arg)
{
if(evt.target instanceof Button) {
hide();
dispose();
if(evt.target == yesButton)
System.exit(0);
return true;
}
return false;
}
}
//=========================== (ActiveMenuApplet)
AnimatedMenuApplet
Przytoczony tu aplet, pokazany podczas wykonania na Ekranie Aplet AnimatedMenuApplet, uzupełnia możliwości poprzedniego apletu przez dodanie animacji.
Ekran Aplet AnimatedMenuApplet
### animate.gif
Tuż po zakończeniu przeciągania elipsy jest tworzony odrębny wątek (oryg. thread). Jego wykonanie, określone przez metodę run, polega na 100-krotnym (w odstępach co 100 ms), wypełnianiu elipsy przypadkowo wybranymi kolorami ustalonego zestawu.
// ========================== AnimatedMenuApplet
import java.applet.*;
import java.awt.*;
import java.util.*;
class AnimatedMenuApplet extends ActiveMenuApplet {
Frame createFrame(Color color)
{
return new AnimatedMenuWindow(color);
}
}
// ====== AnimatedMenuWindow
class AnimatedMenuWindow
extends ActiveMenuWindow implements Runnable {
int xCen, yCen;
AnimatedMenuWindow(Color color)
{
super(color);
}
public boolean mouseUp(Event evt, int x, int y)
{
Thread thread = new Thread(this);
xCen = x;
yCen = y;
thread.start();
return true;
}
public boolean lostFocus(Event evt, Object arg)
{
return false;
}
static final Color colors[] =
{
Color.red, Color.green,
Color.blue,Color.yellow,
Color.pink, Color.orange,
Color.magenta, Color.cyan,
};
public void run()
{
long seed = new Date().getTime();
Random rand = new Random(seed);
for(int i = 0; i < 100 ; i++) {
int random = rand.nextInt();
random %= colors.length;
random = random < 0 ? -random : random;
Color color = colors[random];
fillEllipse(xCen, yCen, color);
try {
Thread.currentThread().sleep(300);
}
catch(InterruptedException e) {
}
}
}
void fillEllipse(int x, int y, Color color)
{
Color thisColor = this.color;
Graphics gDC = getGraphics();
gDC.setColor(color);
int w = size().width >> 2,
h = size().height >> 2;
gDC.fillOval(x - w/2, y - h/2, w, h);
gDC.setColor(thisColor);
}
}
//=========================== (AnimatedMenuApplet)
Mój drugi program
Przedstawiony tu program rozwiązuje następujące zadanie:
Posługując się techniką programowania zdarzeniowego, obiektowego i współbieżnego napisać aplet animujący zestaw okręgów, elips i wielokątów o kształtach zdefiniowanych w dokumencie HTML.
W celu wykonania programu umieszczonego w pliku Master.java należy umieścić opis apletu w dokumencie Master.html, a skompilowany kod programu w pliku Master.class. Kod tła animacji można umieścić w pliku określonym przez parametr Back.
Na Ekranie Figury pokazano aplet podczas wykonywania.
Ekran Figury
### figures.gif
Opis apletu
Ramka apletu ma rozmiary 200 x 100 pikseli. B-kod apletu znajduje się w pliku Master.class. Zdefiniowano kolejno: okrąg, elipsę, trójkąt, kwadrat, elipsę. Obraz tła znajduje się w pliku Turbo.gif.
Uwaga: Współrzędne wierzchołków wielokątów należy podać w taki sposób, aby najmniejsza rzędna i odcięta miała wartość 0 (animacje figur zaczynają się od lewego-górnego narożnika).
<applet code=Master.class width=160 height=160>
<param name=Shape value=Fig>
<param name=Fig1 value="30">
<param name=Fig2 value="20 40">
<param name=Fig3 value="0 0 30 17 17 30">
<param name=Fig4 value="25 0 50 25 25 50 0 25">
<param name=Fig5 value="40 20">
<param name=Back value=Turbo.gif>
</applet>
Deklaracje importu
import java.applet.*;
import java.awt.*;
import java.awt.image.*;
import java.net.*;
import java.util.*;
Klasa Shape
W klasie zdefiniowano pola określające położenie (x, y), rozmiary (w, h) i kolor obiektu. Metoda animateXY służy do określenia nowej pozycji obiektu. Szybkość przemieszczania obiektu po ekranie określają przyrosty (dx, dy).
abstract
class Shape {
int x = 0, y = 0, // lewy-górny narożnik figury
w = 0, h = 0, // szerokość i wysokość figury
dx, dy; // przyrosty współrzędnych
Color color; // kolor wypełnienia
int dx()
{
return dx;
}
int dy()
{
return dy;
}
void setXY(int x, int y)
{
this.x = x;
this.y = y;
}
void setView(int dx, int dy, Color color)
{
this.dx = dx;
this.dy = dy;
this.color = color;
}
void animateXY(int width, int height)
{
x += dx;
if(x < 0)
x += (dx = -dx);
if(x + w > width)
x += (dx = -dx);
y += dy;
if(y < 0)
y += (dy = -dy);
if(y + h > height)
y += (dy = -dy);
}
}
Interfejs Drawable
Interfejs Drawable musi być implementowany przez klasę każdego obiektu, który ma być umieszczony w kolekcji realizowanej przez klasę Picture.
interface Drawable {
void draw(Graphics gDC);
}
Klasa Oval
Klasa Oval opisuje elipsy i okręgi. Wykreślanie odbywa się za pomocą metody draw.
class Oval extends Shape implements Drawable {
Oval(int w, int h)
{
super.w = w;
super.h = h;
}
public void draw(Graphics gDC)
{
gDC.setColor(color);
gDC.fillOval(x, y, w, h);
}
}
Klasa Figure
Klasa Figure opisuje wielokąty. Wykreślanie odbywa się za pomocą metody draw.
Klonowanie polega na utworzeniu kopii obiektu. Ponieważ rezultatem klonowania jest odnośnik do obiektów klasy Object, więc w celu przetworzenia otrzymanego odnośnika na odnośnik do wektora o elementach typu "int", poddano go konwersji do typu "int []".
class Figure extends Shape implements Drawable {
int xVec[], yVec[]; // współrzędne wierzchołków
Figure(int xVec[], int yVec[])
{
try { // klonowanie tablic
this.xVec = (int [])xVec.clone();
this.yVec = (int [])yVec.clone();
}
catch(CloneNotSupportedException e)
{
}
for(int i = 0; i < xVec.length ; i++) {
w = max(w, xVec[i]);
h = max(h, yVec[i]);
}
}
public void draw(Graphics gDC)
{
int len = xVec.length;
int xVec[] = null, yVec[] = null;
try {
xVec = (int [])this.xVec.clone();
yVec = (int [])this.yVec.clone();
}
catch(CloneNotSupportedException e) {
}
for(int i = 0; i < len ; i++) {
xVec[i] += x;
yVec[i] += y;
}
gDC.setColor(color);
gDC.fillPolygon(xVec, yVec, len);
}
static int max(int one, int two)
{
return one > two ? one : two;
}
}
Klasa BackGround
Klasa BackGround opisuje tło. Wykreślanie tła odbywa się za pomocą metody draw, pod nadzorem obserwatora dostarczonego w wywołaniu konstruktora.
class BackGround implements Drawable {
Image backImage;
ImageObserver observer;
BackGround(Image backImage, ImageObserver observer)
{
this.backImage = backImage;
this.observer = observer;
}
public void draw(Graphics gDC)
{
gDC.drawImage(backImage, 0, 0, observer);
}
}
Klasa Link
Klasa Link opisuje element listy obrazów wstawionych do kolekcji Picture. Obiekt klasy Link składa się z odnośnika do następnego elementu listy oraz z odnośnika do obiektu znajdującego się w kolekcji.
class Link {
Link nextItem; // następny element kolekcji
Drawable refItem; // obiekt w pojemniku
}
Klasa Picture
Klasa Picture opisuje kolekcję obiektów. Jej metoda add umożliwia dodanie obiektu do kolekcji.
Metoda draw wykreśla wszystkie obiekty znajdujące się w kolekcji. Metoda animateXY określa nowe współrzędne lewego-górnego narożnika obiektu.
class Picture { // klasa kolekcyjna
Link head = null; // początek listy
void add(Drawable item) // dodanie figury do kolekcji
{
Link newLink = new Link();
newLink.nextItem = head;
newLink.refItem = item;
head = newLink;
}
void draw(Graphics gDC) // wykreślenie zawartości
{
Link scan = head;
while(scan != null) {
scan.refItem.draw(gDC);
scan = scan.nextItem;
}
}
void animate(int width, int height)
{
Link scan = head;
if(scan != null &&
scan.refItem instanceof BackGround)
scan = scan.nextItem;
while(scan != null) {
Drawable refItem = scan.refItem;
((Shape)refItem).animateXY(width, height);
scan = scan.nextItem;
}
}
}
Klasa Master
Klasa Master realizuje postawione zadanie. Jej metody init, start, run, stop są (w tej kolejności) wywoływane przez System.
Metoda init tworzy kolekcję obiektów.
Metoda start tworzy wątek animujący.
Metoda run ładuje kolekcję obiektów (na podstawie parametrów podanych w dokumencie HTML), a następnie realizuje animację.
Uwaga: Ponieważ metoda getImage jedynie inicjuje przesłanie obrazu, a ściągnięcie obrazu z sieci nastąpi dopiero wówczas gdy obraz zostanie użyty, w celu upewnienia się, że obraz jest już dostępny, użyto zarządcy mediów MediaTracker.
public
class Master extends Applet implements Runnable {
Thread motion;
boolean stopped = false;
Picture picture;
static
long oldTime = System.currentTimeMillis();
Image backBuffer = null;
int picW = 0, picH = 0;
public void init()
{
createPicture(picture = new Picture());
}
public void start()
{
if(motion == null) {
motion = new Thread(this);
motion.start();
}
}
public void stop()
{
if(motion != null) {
motion.stop();
motion = null;
}
}
public boolean mouseUp(Event evt, int x, int y)
{
return stopped = true;
}
public void paint(Graphics gDC)
{
picture.draw(gDC);
}
public void run()
{
while(!stopped) {
Dimension dim = size();
int width = dim.width,
height = dim.height;
boolean sizeChange =
picW != width || picH != height;
if(backBuffer == null || sizeChange) {
backBuffer = createImage(width, height);
picW = width;
picH = height;
}
picture.animate(width, height);
Graphics gDC = backBuffer.getGraphics();
paint(gDC); // wykreślaj do bufora
gDC = getGraphics();
long thisTime = System.currentTimeMillis(),
timeSpan = thisTime - oldTime;
if(timeSpan < 100)
try {
Thread.sleep(100 - timeSpan);
}
catch(InterruptedException e) {
}
oldTime = System.currentTimeMillis();
gDC.drawImage(backBuffer, 0, 0, this);
}
motion.stop();
}
void createPicture(Picture picture)
{
String base = getParameter("Shape");
for(int i = 1; true; i++) {
String data = getParameter(base + i);
if(data == null)
break; // koniec wykazu figur
StringTokenizer tokens =
new StringTokenizer(data, " ");
Drawable item;
int tokenCount = tokens.countTokens();
int xAxis, yAxis;
switch(tokenCount) {
case 0:
throw new IllegalArgumentException();
case 1: // okrąg
xAxis = getInt(tokens);
item = new Oval(xAxis, xAxis);
break;
case 2: // elipsa
xAxis = getInt(tokens);
yAxis = getInt(tokens);
item = new Oval(xAxis, yAxis);
break;
default: // wielokąt
int xVec[], yVec[], j;
int noOfPoints = tokenCount/2;
xVec = new int [noOfPoints+1];
yVec = new int [noOfPoints+1];
for(j = 0; j < noOfPoints ; j++) {
xVec[j] = getInt(tokens);
yVec[j] = getInt(tokens);
}
xVec[j] = xVec[0]; // domknięcie
yVec[j] = yVec[0]; // wielokąta
item = new Figure(xVec, yVec);
}
int dx = getSpeed(),
dy = getSpeed();
Color color = getColor();
((Shape)item).setView(dx, dy, color);
picture.add(item); // figura do pojemnika
}
URL codeBase = getCodeBase();
String backName = getParameter("Back");
Image backImage = backName != null ?
getImage(codeBase, backName) :
getImage(codeBase, "Default.gif");
// sprowadzenie obrazu
MediaTracker tracker = new MediaTracker(this);
tracker.addImage(backImage, 0);
try {
tracker.waitForID(0); // czekanie na obraz
}
catch(InterruptedException e) {
}
BackGround backGround =
new BackGround(backImage, this);
picture.add(backGround); // dodanie tła
}
static int speed = 0;
static int getSpeed()
{
return ++speed;
}
static int hue = -1;
static Color getColor()
{
Color colors[] = { Color.red, Color.green,
Color.blue, Color.magenta,
Color.orange, Color.pink };
hue = ++hue % colors.length;
return colors[hue];
}
int getInt(StringTokenizer tokens)
{
String token = tokens.nextToken();
return Integer.parseInt(token);
}
}
Mój trzeci program
Przedstawiony tu program rozwiązuje następujące zadanie:
Napisać program wykorzystujący własny sterownik z trójwymiarowymi przyciskami umożliwiającymi wprowadzanie liczb całkowitych. Przycisk S oprogramować w taki sposób, aby umożliwiał przeniesienie liczby ze sterownika do klatki umieszczonej w górnej części apletu.
Program opisuje klasa Master. Klasa Display definiuje sterownik-wyświetlacz, a klasa Key sterownik-przycisk. Każdy z tych sterowników jest wieloużytkowym (oryg. reusable) komponentem, który może być wykorzystany w innych programach.
Na Ekranie Sterownik pokazano program-aplet podczas wykonywania.
Ekran Sterownik
### display.gif
Opis apletu
<applet code=Master.class width=130 height=180>
</applet>
Klasa Master
Klasa Master tworzy klatkę tekstową i wyświetlacz z przyciskami. Procedura mouseUp rozpoznaje naciśnięcie przycisku S wyświetlacza i obsługuje je w taki sposób, że liczbę znajdującą się w wyświetlaczu wstawia do klatki tekstowej.
import java.applet.*;
import java.awt.*;
public
class Master extends Applet {
private Display theDisplay;
private TextField theCell;
public void init()
{
setLayout(new BorderLayout());
theCell = new TextField("Ready");
theDisplay = new Display();
add("North", theCell);
add("East", theDisplay);
}
public boolean mouseUp(Event evt, int x, int y)
{
int value = theDisplay.getValue();
theCell.setText(String.valueOf(value));
return true;
}
}
Klasa Display
Klasa Display definiuje wyświetlacz z przyciskami. Zwolnienie przycisku E nie powoduje obsłużenia tego zdarzenia (po naciśnięciu przycisku E metoda mouseUp zwraca false). Stwarza to możliwość obsłużenia go w pojemniku Master.
// ========================== klasa Display
class Display extends Panel {
private int value = 0;
private Panel display = new Panel();
private TextField number = new TextField("0");
private Panel buttons = new Panel();
private static String labels = "7894561230 S";
private char lastKey = '0';
Display()
{
display.setLayout(new BorderLayout());
buttons.setLayout(new GridLayout(4, 3));
for(int i = 0; i < 12 ; i++) {
char chr = labels.charAt(i);
Key key = new Key(this, chr);
buttons.add(key);
}
display.add("North", number);
display.add("Center", buttons);
add(display);
}
public void setLastKey(char key)
{
lastKey = key;
}
int getValue()
{
return value;
}
void setValue(int value)
{
this.value = value;
number.setText(new String("" + value));
}
public boolean handleEvent(Event evt)
{
if(evt.id == Event.MOUSE_UP)
return mouseUp(evt, evt.x, evt.y);
return true;
}
public boolean mouseUp(Event evt, int x, int y)
{
switch(lastKey) {
case ' ':
value = 0;
number.setText("0");
return true;
case 'S':
return false; // nie obsłużono, posłano dalej
default:
int digit = lastKey - '0';
value = value * 10 + digit;
number.setText(String.valueOf(value));
return true;
}
}
}
// ========================== (Display)
Klasa Key
Klasa Key definiuje trójwymiarowy przycisk wyświetlacza. Efekt trójwymiarowości uzyskano przez wykreślenie łuków w kolorze czarnym i białym. Wybrany rozmiar przycisków określa metoda preferredSize.
Naciśnięcie przycisku wyświetlacza powoduje wykreślenie go w postaci wciśniętej. Zwolnienie przycisku powoduje zapamiętanie wciśniętego przycisku, ale bez obsłużenia tego zdarzenia (metoda mouseUp zwraca false). Umożliwia to obsłużenie tego zdarzenia w pojemniku-wyświetlaczu Display.
// ========================== klasa Key
class Key extends Panel {
Display display;
private char theKey;
private Color color;
private boolean mouseIsDown = false;
Key(Display display, char theKey)
{
this.display = display;
this.theKey = theKey;
}
public boolean mouseDown(Event evt, int x, int y)
{
mouseIsDown = true;
repaint();
return true;
}
public boolean mouseUp(Event evt, int x, int y)
{
mouseIsDown = false;
repaint();
display.setLastKey(theKey);
return false;
}
public void paint(Graphics gDC)
{
Rectangle bounds = bounds();
int w = bounds.width,
h = bounds.height;
Color oldColor = gDC.getColor();
drawOval(gDC, Color.red, 0, 0, w, h);
int x = (w /= 2) / 2,
y = (h /= 2) / 2;
drawOval(gDC, Color.green, x, y, w, h);
gDC.setColor(oldColor);
drawLabel(gDC, theKey, x, y, w, h);
}
void drawOval(Graphics gDC, Color color,
int x, int y, int w, int h)
{
gDC.setColor(color);
gDC.fillOval(x, y, w, h);
color = mouseIsDown ? Color.white : Color.black;
gDC.setColor(color);
gDC.drawArc(x, y, w, h, 45, -180);
color = mouseIsDown ? Color.black : Color.white;
gDC.setColor(color);
gDC.drawArc(x, y, w, h, 45, +180);
}
public void drawLabel(Graphics gDC, char label,
int x, int y, int w, int h)
{
Font font = new Font("Arial", Font.BOLD, 12);
gDC.setFont(font);
FontMetrics metrics = gDC.getFontMetrics();
int as = metrics.getAscent(),
ww = metrics.charWidth(label);
x += (w - ww) / 2;
y += (h + as) / 2;
gDC.drawString("" + label, x, y);
}
public Dimension preferredSize()
{
return new Dimension(30, 30);
}
}
// ========================== (Key)
Dla koneserów
Wykorzystując bez zmian zdefiniowane powyżej klasy Display i Key, można podać uogólnione rozwiązanie postawionego problemu, stanowiące ilustrację metody komunikowania się sterowników znanej pod nazwą Pośrednik-Obserwator-Kontroler (oryg. Model-View-Controller).
Istotą tej metody jest skonstruowanie klasy obiektów pośredniczących, które mogą być jednocześnie obserwowane i kontrolowane.
Jeśli obiekt-kontroler dokona modyfikacji obiektu-pośrednika, to za pomocą metody update zostanie o tym powiadomiony obiekt-obserwator. Kontroler i obserwator nic o sobie nie wiedzą i komunikują się tylko poprzez pośrednika. Dzięki temu modyfikacja jednego z tych sterowników nie ma żadnego wpływu na drugi. Czyni to program prawdziwie modularnym i zmniejsza niebezpieczeństwo niekontrolowanego propagowania zmian kodu.
Wykonanie następującego programu, w którym rolę pośrednika, obserwatora i kontrolera pełnią odpowiednio obiekty klas ObservableInt, ObserverTextField i ControllerDisplay ma taki sam skutek jak uprzednio.
Ponadto, wprowadzenie do klatki tekstowej pewnej liczby powoduje przeniesienie jej do klatki wyświetlacza.
Uwaga: Bardziej dociekliwi stwierdzą bez trudu, że zarówno klatka jak i wyświetlacz jest jednocześnie obserwatorem i kontrolerem.
W szczególności, gdyby metodę
public void update(Observable obs, Object arg)
{
setValue(theInt.getInt());
}
zastąpiono metodą
public void update(Observable obs, Object arg)
{
return;
}
to naciśnięcie klawisza Enter w obrębie klatki tekstowej anulowałoby przeniesienie liczby do wyświetlacza.
import java.applet.*;
import java.awt.*;
import java.util.*;
public
class Master extends Applet {
private Display theDisplay;
private TextField theCell;
ObservableInt theInt = new ObservableInt(0);
public void init()
{
setLayout(new BorderLayout());
theCell = new ObserverTextField(theInt, "Ready");
theDisplay = new ControllerDisplay(theInt);
add("North", theCell);
add("East", theDisplay);
}
}
// ========================== Klasa ObservableInt
class ObservableInt extends Observable {
int theInt;
ObservableInt(int theInt)
{
this.theInt = theInt;
}
public synchronized int getInt()
{
return theInt;
}
public synchronized void setInt(int newInt)
{
if(newInt != theInt) {
theInt = newInt;
setChanged();
notifyObservers();
}
}
}
// ========================== (ObservableInt)
// ========================== Klasa ObserverTextField
class ObserverTextField
extends TextField
implements Observer {
ObservableInt theInt;
ObserverTextField(ObservableInt theInt, String string)
{
this.theInt = theInt;
theInt.addObserver(this);
setText(string);
}
public boolean action(Event evt, Object arg)
{
String string = (String)arg;
int value;
try {
value = Integer.parseInt(string);
}
catch(NumberFormatException e) {
setText("Not a Number");
return true;
}
theInt.setInt(value);
return true;
}
public void update(Observable obs, Object arg)
{
setText("" + theInt.getInt());
}
}
// ========================== (ObserverTextField)
// ========================== Klasa ControllerDisplay
class ControllerDisplay extends Display implements Observer {
ObservableInt theInt;
ControllerDisplay(ObservableInt theInt)
{
this.theInt = theInt;
theInt.addObserver(this);
}
public boolean handleEvent(Event evt)
{
if(!super.handleEvent(evt))
theInt.setInt(getValue());
return true;
}
public void update(Observable obs, Object arg)
{
setValue(theInt.getInt());
}
}
// ========================== (ControllerDisplay)
Uruchamianie programów
O tym jaki zapewniono sobie komfort uruchamiania programów decyduje rodzaj użytego Środowiska Rozwojowego. Jednak nawet wówczas gdy nie zawiera ono uruchamiacza można odwołać się do starej i skutecznej metody wstawiania do programu wywołań funkcji podających wartości wyrażeń.
Klasa Debug
W przypadku zastosowania takiej metody polecam własną klasę Debug wyposażoną w przeciążoną funkcję toFrame.
Klasa ta, przytoczona w Dodatku Klasa uruchomieniowa, znakomicie ułatwia wyszukiwanie błędów dynamicznych w środowiskach bez uruchamiacza. Po wykonaniu kompilacji można ją włączać do programów poleceniami importu.
Uwaga: Klasa Debug może być wykorzystywana zarówno do uruchamiania aplikacji jak i apletów. Jako przydatne ćwiczenie można polecić taką jej przeróbkę, aby nadawała się do uruchamiania zestawu dwóch albo więcej apletów opisanych w tym samym dokumencie HTML (podana klasa Debug generuje w takim wypadku sytuację wyjątkową InstantiationError).
Użycie klasy Debug
Jeśli w pewnym miejscu programu (np. w metodzie init apletu) wykona się instrukcję
new Debug();
to każde wywołanie metody toFrame, na przykład
Debug.toFrame("Any Text");
albo
Debug.toFrame(Thread.currentThread());
wyświetli jej argument w odrębnym wierszu okna uruchomieniowego.
Następujący przykład ilustruje zasadę użycia klasy Debug, po dołączeniu jej w wersji źródłowej do kodu apletu.
public
class Any extends Applet {
public void init()
{
new Debug(); // utworzenie okna
// ...
new AnyClass();
// ...
anyProc(20);
}
void anyProc(int par)
{
// ...
Debug.toFrame("Hello from anyProc");
Debug.toFrame("par = " + par);
Debug.toFrame(Thread.currentThread());
}
// ...
}
class AnyClass {
AnyClass()
{
Debug.toFrame("Hello from AnyClass");
}
}
// ============================= klasy uruchomieniowe
// wg Dodatku Klasa Debug
class Debug {
// ...
}
class DebugFrame extends Frame {
// ...
}
//============================================================
Podczas wykonywania podanego programu w oknie uruchomieniowym zatytułowanym Debug wyświetli się napis
Debug
=====
Hello from AnyClass
Hello from anyProc
par = 20
Thread[thread applet-Any.class,6,group applet-Any.class]
Część III Środowisko Cafe
Wszystkie zamieszczone w książce programy uruchomiono w Środowisku Rozwojowym Cafe, składającym się z wbudowanego edytora, kompilatora i uruchamiacza. Posługiwanie się Cafe okazało się bardzo wygodne, a szybkość kompilatora i interpretera B-kodu można ocenić jako bardzo dobrą. Dlatego środowisko to (w odróżnieniu od mnogości innych, o których nie da się powiedzieć niczego dobrego) można polecić z pełnym przekonaniem.
Ci, którzy nie dysponują Środowiskiem Cafe, mogą pominąć ten rozdział. W chwili gdy niniejsza książka znajdzie się na półkach księgarskich, pojawi się na rynku wiele nowych środowisk, w tym Visual Cafe (Symantec) i Visual J++ (Microsoft). Ich wersje przed-inauguracyjne przedstawiają się nader obiecująco.
Uwaga: Od września 1996 jest już dostępne w Internecie Środowisko ADK for Win 3.1 dla komputerów z Windows 3.* (IBM). Może ono zainteresować tych, którzy jeszcze nie przeszli na Windows 95, ale już zainstalowali WinG v. 1.0 i Win32s v. 1.30, ściągnięte spod adresu
ftp://ftp.microsoft.com/softlib/mslfiles/pw1118.exe
Wywołanie środowiska
Po wywołaniu środowiska Cafe pojawia się informacja o jego wersji. Jest ona wyświetlana także po wydaniu polecenia Help/About. Na Ekranie Winieta Cafe pokazano ekran identyfikujący środowisko.
Ekran Winieta Cafe
### welcome.gif
Edycja dokumentów
Wydanie polecenia File/New powoduje utworzenie okna edycyjnego, w którym można przygotować moduł źródłowy albo dokument HTML. Po zakończeniu edycji moduł albo dokument należy zapamiętać w pliku o odpowiednio dobranej nazwie.
Jeśli tekst jest modułem źródłowym, który zawiera klasę publiczną Klasa, to nazwą pliku musi być Klasa.java. Jeśli jest dokumentem HTML, to nazwa pliku powinna mieć rozszerzenie .html.
Uwaga: Jeśli program zawiera tylko jedną klasę publiczna Klasa, to zaleca się umieszczenie dokumentu HTML w pliku Klasa.html.
Utworzenie projektu
W celu utworzenia projektu należy wydać polecenie Project/New. Spowoduje to wyświetlenie okna dialogowego Project Express pokazanego na Ekranie Nazwanie projektu.
Ekran Nazwanie projektu
### name.gif
W klatce Project Name należy wówczas podać Nazwę projektu, a przez zapoznanie się z listą Directories upewnić, że plik projektu zostanie zapamiętany we właściwym (i uprzednio utworzonym) katalogu.
Po naciśnięciu przycisku Next wyświetli się okno pokazane na Ekranie Określenie typu projektu.
Ekran Określenie typu projektu
### type.gif
W oknie tym należy podać czy tworzony program jest aplikacją czy apletem (lista Target Type), a przez wybranie nastawy Project Settings zdecydować się na to, czy program wynikowy będzie uruchamiany (Debug) czy dystrybuowany (Release).
Po naciśnięciu przycisku Next wyświetli się okno pokazane na Ekranie Dodaj pliki. W oknie tym należy podać jakie pliki wchodzą w skład projektu. Odbywa się to przez dwukliknięcie elementu listy File Name, albo przez zaznaczenie jej elementu i naciśnięcie przycisku Add.
Uwaga: Jeśli projekt dotyczy apletu, to do projektu należy również włączyć plik zawierający dokument HTML.
Ekran Dodaj pliki
### addfile.gif
Po naciśnięciu przycisku Next wyświetli się okno pokazane na Ekranie Ustawienia początkowe. W jego klatce Class Path można podać nazwy katalogów, w których mają być poszukiwane pliki włączone do projektu.
Uwaga: Jeśli klasy programu należą do pakietu domyślnego (w pliku źródłowym nie ma polecenia package), to klatka Class Path może pozostać pusta).
Ekran Ustawienia początkowe
### settings.gif
Otwarcie projektu
Uprzednio utworzony projekt może być następnie otwarty, a przetwarzanie programu może być wówczas kontynuowane.
W celu otwarcia projektu należy wydać polecenie Project/Open. Spowoduje to wyświetlenie okna Open Project, pokazanego na Ekranie Otwarcie projektu. W jego klatce File Name, z uwzględnieniem katalogu określonego przez listę Folders, należy podać nazwę uprzednio utworzonego projektu.
Ekran Otwarcie projektu
### openprj.gif
Modyfikowanie projektu
W celu zmodyfikowania otwartego (a więc już istniejącego) projektu, należy wydać polecenie Project/Edit albo Project/Settings. Spowoduje to wyświetlenie znanych już okienek dialogowych, w których można dokonać modyfikacji projektu (np. przekształcić aplikację w aplet, lub odwrotnie).
Konfigurowanie pulpitu
Przed, albo po utworzeniu projektu można skonfigurować pulpit środowiska. Można to wykonać za pomocą poleceń menu Window. W szczególności wydanie poleceń Views, Build i Debug spowoduje wyświetlenie okienek pokazanych na Ekranie Okienka Środowiska.
Uwaga: Operacje na okienku Views odbywają się przez przeciągnięcie ikony, a operacje na okienku Builds i Debug odbywają się przez kliknięcie.
Ekran Okienka Środowiska
### desktop.gif
Kompilowanie modułu
W celu skompilowania modułu źródłowego należy w okienku Build kliknąć ikonę Compile. Jeśli moduł zawiera błędy, to kompilator wyświetli je w odrębnym oknie. Dwukliknięcie wiersza wyszczególniającego błąd spowoduje wyświetlenie tego fragmentu programu, w którym błąd ten wykryto.
Uwaga: Skompilowanie pojedynczego modułu jest wykonywane rzadko. Zazwyczaj od razu przystępuje się do zbudowania programu. W takim wypadku Cafe kompiluje tylko te moduły, które zostały zmienione w okresie od poprzedniej ich kompilacji.
Na Ekranie Niepomyślna kompilacja pokazano skutek skompilowania programu zawierającego błędy składniowe.
Ekran Niepomyślna kompilacja
### badcomp.gif
Budowanie programu
W celu zbudowania programu należy w okienku Build kliknąć ikonę Build albo Rebuild All. Nastąpi wówczas skompilowanie wszystkich modułów wchodzących w skład projektu i utworzenie B-kodu programu. Sygnalizowanie ewentualnych błędów składniowych odbywa tak jak podczas kompilacji modułu.
Budowanie może dotyczyć tylko takiego programu, w którym wyróżniono moduł główny (plik zawierający opis apletu albo program z funkcją main). Ma to istotne znaczenie zwłaszcza wówczas, gdy w skład projektu wchodzi więcej niż jeden moduł źródłowy albo dokument HTML. W takim wypadku należy w oknie projektu kliknąć lewym przyciskiem myszki nazwę modułu, a następnie kliknąć ją prawym i wybrać opcję Mark as Main. Wykonanie tej operacji pokazano na Ekranie Zdefiniowanie modułu głównego.
Ekran Zdefiniowanie modułu głównego
### main.gif
Uwaga: Podczas kompilowania lub budowania programu pojawia się niekiedy niepokojący komunikat
Access violation
Niepokój przeradza się wkrótce w przerażenie, ponieważ wobec domniemanego zagrożenia bezpieczeństwa Systemu, kompilator nie udziela żadnej informacji o przyczynie błędu, a więc praktycznie nie wiadomo co zrobić, aby program naprawić i kontynuować wyszukiwanie zawartych w nim błędów.
Z moich doświadczeń wynika, że wymieniony komunikat wywołuje najczęściej literówka w nazwie klasy, na przykład napisanie
new DEbug();
zamiast
new Debug();
albo doprowadzenie do wystąpienia niezrównoważonych (oryg. unbalanced) nawiasów klamrowych. Czy błąd taki łatwo znaleźć, to już zupełnie inna sprawa. Dlatego należy dołożyć starań aby nie wystąpił.
Na ekranie Zagrożenie bezpieczeństwa pokazano sytuację, która doprowadziła do powstania rozpatrywanego komunikatu.
Ekran Zagrożenie bezpieczeństwa
### security.gif
Dostarczenie argumentów
Jeśli program jest aplikacją, to zazwyczaj oczekuje argumentów skojarzonych z parametrami funkcji main. W celu ich dostarczenia, należy wydać polecenie Project/Arguments. Spowoduje to wyświetlenie dialogu Run arguments, pokazanego na Ekranie Dostarczenie argumentów. W okienku dialogu można podać wymagane argumenty aplikacji.
Ekran Dostarczenie argumentów
### giveargs.gif
Wykonanie programu
W celu wykonania programu należy w okienku Build kliknąć ikonę Excecute Program. Jeśli program jest aplikacją, to nastąpi wówczas podjęcie wykonania publicznej i statycznej funkcji main. Jeśli jest apletem, to nastąpi zinterpretowanie dokumentu HTML wchodzącego w skład projektu i wyświetlenie opisanych w nim apletów.
Jeśli aktywowaniu programu nastąpi wyładowanie i załadowanie Cafe, bez możliwości zaobserwowania czegokolwiek, to zapewne popełniono jeden z następujących błędów
1) Program jest aplikacją, ale nie zawiera unikalnej statycznej i publicznej funkcji main.
2) Program jest aplikacją, która wyprowadza wyniki na konsolę, ale nie zapewniono możliwości obejrzenia konsoli (zazwyczaj pomaga System.in.read()).
3) Program jest apletem, ale nie zawiera publicznej nadklasy klasy Applet.
4) Program jest apletem, ale jego nazwa podana w opisie apletu (np. Master.class) nie wynika z nazwy klasy apletu (np. Master).
Jeśli po aktywowaniu apletu zniknie środowisko Cafe, ale nic się nie wyświetli, to najprawdopodobniejszą tego przyczyną jest błąd w opisie apletu, na przykład brak zamykającego nawiasu kątowego w opisie apletu.
Uwaga: W systemie Windows 95 należy w takim wypadku nacisnąć klawisze Ctrl-Alt-Del, wyszukać program Appletviewer, a następnie wydać polecenie End Task. Spowoduje to powrót do środowiska Cafe.
Uruchomienie programu
Podczas uruchamiania programów posługiwałem się własną klasa uruchomieniową, przytoczoną w Dodatku Klasa uruchomieniowa. Okazała się ona bardzo przydatna, dlatego szczerze zachęcam do jej użycia.
Ci, który oczekują większego komfortu mogą użyć uruchamiacza wbudowanego w Cafe. W celu jego uaktywnienia należy dokonać instalacji protokołu TCP/IP, zgodnie z wytycznymi podanymi w Systemie Pomocy (Help/Search/Index/Debugging/Debugging with Symantec Cafe).
Na Ekranie Uruchamianie pokazano wykonanie programu posługującego się klasą Debug. Wyświetlenie jego modułu źródłowego zapewniono dzięki aktywowaniu dodatkowej kopii środowiska Cafe, w którym otwarto plik zawierający ten moduł.
Ekran Uruchamianie
### debugger.gif
Część IV Programy
Jaka jest Java
W ideowym dokumencie, znanym jako Biała Księga, przedstawiono Javę jako język programowania, który jest
prosty
odporny
przenośny
wydajny
obiektowy
adaptowalny
bezpieczny
Java jest prosta, ponieważ uczy się jej łatwo, a dla programujących w C++ większość konstrukcji składniowych jest zrozumiała już na wstępie. Natomiast ci którzy nie znali C++ nauczą się Javy bardzo szybko, ponieważ będą im oszczędzone kłopoty związane ze złożonymi definicjami, konwersjami, wskaźnikami i zarządzaniem pamięcią.
Java jest odporna, ponieważ wbudowana w nią rygorystyczna kontrola typów, zarówno statyczna jak i dynamiczna umożliwia łatwe wykrywanie błędów, które w innych językach ujawniają się dopiero podczas żmudnego testowania programów.
Java jest przenośna, ponieważ jako język interpretowany, umożliwia pisanie programów niezależnych od architektury komputera oraz od jego systemu operacyjnego, takich które dają się wykonać wszędzie.
Java jest wydajna, ponieważ dzięki wbudowaniu w nią (zarówno na poziomie języka, jak i bibliotek) mechanizmów wielowątkowości i synchronizacji umożliwia efektywne wykonywanie programów w rozproszonych systemach wieloprocesorowych.
Java jest obiektowa, ponieważ włączono do niej mechanizmy hermetyzowania, dziedziczenia i polimorfizmu, umożliwiając tym samym programowanie w paradygmacie obiektowym.
Java jest adaptowalna, ponieważ z sieci są ładowane tylko te moduły, które są potrzebne do aktualnego wykonania programu.
Java jest bezpieczna, ponieważ ładowane moduły są dynamicznie weryfikowane, a żaden aplet nie jest w stanie naruszyć integralności systemu na rzecz którego został przywołany.
_________________________________________________________________________________________
Struktura programu
Program składa się z zestawu modułów źródłowych umieszczonych w plikach. Moduły źródłowe zawierają komentarze, deklaracje pakietu, deklaracje importu i definicje klas. Jeśli moduł zawiera definicję więcej niż jednej klasy, to tylko jedna z nich może być publiczna.
Uwaga: Klasa publiczna o nazwie Klasa musi być umieszczona w pliku o nazwie Klasa.java.
W odróżnieniu od C++ nie ma w Javie dyrektyw preprocesora, globalnych zmiennych i funkcji, unii, struktur, pól bitowych, wyliczników oraz deklaracji typedef. Brak tych konstrukcji nie stanowi jednak istotnego ograniczenia, ponieważ każdą z nich można zastąpić inną.
W szczególności, instrukcja o postaci
if(false) Blok
doskonale zastępuje dyrektywę
#if 0
Blok
#endif
Uwaga: Ponieważ w Javie nie ma globalnych zmiennych i funkcji, więc odpowiednik każdego z takich obiektów musi znajdować się w ciele klasy.
public
class Simple {
public static void main(String args[])
{
System.out.println("Hello");
}
}
Wykonanie programu powoduje wyprowadzenie napisu
Hello
Komentarze
W modułach mogą wystąpić komentarze dokumentacyjne, blokowe i wierszowe
Komentarz dokumentacyjny zaczyna się od /** i kończy się na najbliższym */.
Komentarz blokowy zaczyna się od /* i kończy się na najbliższym */.
Komentarz wierszowy zaczyna się od // i kończy w miejscu zakończenia wiersza.
package janb.book; // deklaracja pakietu
import java.lang.*; // deklaracja importu
// komentarz dokumentacyjny (poniżej)
/**
* This class implements an application.
* @author Jan B.
* @version 1.0
*/
/* klasa Greet */ // komentarz blokowy
public // definicja klasy publicznej
class Greet {
public static void main(String args[])
{
System.out.println("Hello");
}
}
Słowa kluczowe
Słowa kluczowe Javy wymieniono w Tabeli Słowa kluczowe. Niektóre z nich (m.in. const i goto nie mają interpretacji).
Tabela Słowa kluczowe
###
cast catch char class const
abstract boolean break byte case
continue default do double else
extends final finally float for
goto if implements import instanceof
int interface long native new
package private protected public return
short static super switch synchronized
this throw throws transient try
void volatile while
###
Identyfikatory
Desygnatorami obiektów programu są identyfikatory (oryg. identifier). Analogicznie do C++ identyfikatorem jest ciąg literowo-cyfrowy nie zaczynający się od cyfry. Nie-cyframi są m.in. znak dolara, znak podkreślenia oraz litery narodowe (np. ś) należące do Unikodu.
Zwyczajowo nazwy klas i stałych zaczyna się od litery dużej, a nazwy zmiennych, pakietów i procedur od małej.
Na przykład
Color nazwa klasy
PI nazwa stałej
myLongVar nazwa zmiennej
java.lang nazwa pakietu
getColor nazwa procedury
Moduły
Każdy moduł programu jest kompilowany niezależnie.
Jeśli moduł zawiera definicję klasy albo interfejsu Nazwa, to po skompilowaniu modułu powstaje plik wynikowy Nazwa.class.
Plik Nazwa.class musi być umieszczony w podkatalogu wynikającym z deklaracji pakietu.
W szczególności, jeśli deklaracja pakietu ma postać
package object.janb.applets;
to plik wynikowy musi być umieszczony w podkatalogu
object\janb\applets
Uwaga: Jeśli moduł nie zawiera określenia pakietu, to należy do pakietu domyślnego (oryg. default).
public
class Simplest {
public static void main(String args[])
{
}
}
Podano najprostszą aplikację, której klasa należy do pakietu domyślnego.
Pakiety
Pakiety mają strukturę hierarchiczną i są (zazwyczaj!) odwzorowywane na katalogi. B-kod klasy należącej do pakietu domyślnego jest umieszczany w katalogu bieżącym (oryg. current). Zaleca się, aby nazwa pakietu, który ma być dostępny globalnie (np. w Internecie), zaczynała się od odwróconej nazwy domeny (oryg. domain).
Uwaga: Nazwa domeny zaczyna się od dwuliterowego kodu kraju (np. Polska PL) albo od jednej z następujących nazw: COM (oryg. commercial), EDU (oryg. educational), GOV (oryg. government), MIL (oryg. military), NET (oryg. network), ORG (oryg. organization).
Na przykład, jeśli klasa Greet należy do pakietu janb.applets, to plik Greet.class musi być umieszczony w podkatalogu
janb\applets
dowolnego z tych katalogów, które są wymienione parametrze środowiska CLASSPATH.
Oznacza to w szczególności, że jeśli w Windows 95 parametr CLASSPATH ma na przykład wartość
.;c:\staff;c:\java\classes.zip
a plik Greet.class znajduje się w katalogu
c:\staff\janb\applets
to w Internecie klasa Greet ma nazwę
PL.edu.pw.ii.staff.janb.applets.Greet
domena podpakiet
.............pakiet.............klasa
Uwaga: Jeśli pakiet zawiera podpakiet o pewnej nazwie, to zabrania się, aby zawierał klasę albo interfejs o takiej samej nazwie.
Na przykład, jeśli pakiet
java.janb
zawiera podpakiet image, to nie może zawierać deklaracji klasy o nazwie image.
Deklaracje importu
Importowanie nazw pakietów, klas i interfejsów odbywa się za pomocą deklaracji importu.
import Pakiet;
Podana deklaracja umożliwia zastąpienie pełnej nazwy pakietu nazwą ostatniego jej identyfikatora.
Na przykład, po deklaracji
import pl.edu.pw.ii.janb.gui;
zamiast pełnej nazwy pl.edu.pw.ii.janb.awt można posługiwać się nazwą gui.
import Pakiet.Klasa;
import Pakiet.Interfejs;
Podane deklaracje umożliwiają zastąpienie pełnej nazwy klasy albo interfejsu po prostu jej albo jego identyfikatorem.
Na przykład, po deklaracji
import java.awt.Color;
zamiast pełnej nazwy java.awt.Color, można posługiwać się nazwą Color.
import Pakiet.*;
Podana deklaracja umożliwia zastąpienie każdej nazwy klasy oraz interfejsu podanego pakietu, po prostu jej albo jego identyfikatorem.
Na przykład, po deklaracji
import java.awt.*;
zamiast pełnej nazwy java.awt.Color można posługiwać się nazwą Color, a zamiast nazwy java.awt.Graphics można posługiwać się nazwą Graphics.
Deklaracja taka nie umożliwia jednak posługiwania się identyfikatorem image pakietu java.awt.image, ponieważ dotyczy tylko klas, a nie pakietów.
W celu ułatwienia posługiwania się identyfikatorem image należy użyć osobnej deklaracji
import java.awt.image;
Uwaga: Jeśli w module zawierającym deklarację wystąpiła klasa albo interfejs o nazwie identycznej z nazwą klasy albo z nazwą interfejsu podanego pakietu, to odwołanie do nazwy pochodzącej z pakietu nie może być uproszczone do identyfikatora.
Użycie deklaracji importu nie jest konieczne, ale znacznie zwiększa czytelność programu. Na przykład, następujący program
public
class Master extends java.awt.Applet {
public void paint(java.awt.Graphics gDC)
{
gDC.drawString("Hello", 20, 20);
}
}
można dzięki deklaracji importu zapisać w postaci
import java.awt.*;
public
class Master extends Applet {
public void paint(Graphics gDC)
{
gDC.drawString("Hello", 20, 20);
}
}
_________________________________________________________________________________________
Typy podstawowe
Podstawowymi typami danych w Javie są typy arytmetyczne i orzecznikowe (boolean). Typy arytmetyczne dzielą się na całkowite (byte, short, int, long), rzeczywiste (float, double) i znakowe (char).
Uwaga: Ponieważ typ znakowy jest typem arytmetycznym, więc na jego danych można wykonywać takie same operacje jak na danych całkowitych i rzeczywistych.
Typy całkowite
W odróżnieniu od C++, w którym nie precyzuje się sposobu reprezentowania danych, a jedyną gwarancją jest iż
sizeof(short) <= sizeof(int) <= sizeof(long)
w Javie nie dopuszczono żadnej dowolności.
Dzięki temu wiadomo, że dane typu
byte są 8-bitowe (od -128 do 127)
short są 16-bitowe (od -32768 do 32767)
int są 32-bitowe (od -2,147,483,648 do 2,147,483,647)
long są 64-bitowe (od -9,223,372,036,854,775,808 do
+9,223,372,036,854,775,807)
oraz że każda z nich jest daną-ze-znakiem, w zapisie uzupełnień do 2.
Wynika stąd m.in., że jeśli zaneguje się wszystkie bity danej, a następnie doda do niej 1, to skutek będzie taki, jakby znak danej zmieniono na przeciwny. Wyciągnięcie takiego wniosku w odniesieniu do C++ jest niemożliwe.
Typ liczby wynika jednoznacznie z jej wartości (jako kryterium wyboru przyjmuje się oszczędność reprezentacji).
Liczba zakończona literą L albo l jest typu "long".
Na przykład 32767 jest typu "short", a 32768 jest typu "int", ale 0L jest typu "long".
Typy rzeczywiste
W odróżnieniu od C++, z jedyną gwarancją iż
sizeof(float) <= sizeof(double) <= sizeof(long double)
w Javie przyjęto iż dane typu
float są 32-bitowe (w przybliżeniu od -3.4e38 do 3.4e38)
double są 64-bitowe (w przybliżeniu od -1.8e308 do 1.8e308)
oraz że reprezentacja oraz wyniki operacji na tych danych spełniają wymagania specyfikacji IEEE 754.
Wynika stąd m.in., że liczby rzeczywiste są reprezentowane w zapisie modułu-i-znaku, a wyniki nietypowych operacji, jak na przykład
1.0 / 0 (Double.POSITIVE_INFINITY)
1.0 /-0 (Double.NEGATIVE_INFINITY)
1.0 / 0 * 0 (Double.NaN tj. NotANumber)
mają ściśle określone wartości, które można reprezentować przez symbole podane w nawiasach.
Każda liczba z ustalonego w specyfikacji IEEE 754, wąskiego przedziału wokół 0 jest reprezentowana przez zero-ze-znakiem (tj. ujemne-zero albo dodatnie-zero). Z punktu widzenia relacji równe i nie-równe oba te zera uznaje się za równe.
Typ liczby rzeczywistej wynika jednoznacznie z jej wartości (jako kryterium wyboru przyjmuje się oszczędność reprezentacji).
Każda prosta liczba rzeczywista (np. -1.2, 1., -.2, 1e-2) oraz każda prosta liczba rzeczywista zakończona literą D albo d jest typu "double".
Natomiast każda prosta liczba zakończona literą F albo f jest typu "float".
Na przykład -2.4 oraz 3e2 jest typu "double", a -2e-3f jest typu "float".
Typ znakowy
Na drodze od C++ do Javy typy znakowe przeszły ewolucję. Typ "signed char" przekształcono w typ "byte", a w miejsce 1-bajtowych typów "char" i "unsigned char", wprowadzono w Javie typ całkowity-bez-znaku "char".
Dane typu "char" są 16-bitowymi znakami Unikodu. Unikod umożliwia reprezentowanie znaków wszystkich języków europejskich oraz większości znaków pozostałych języków, w tym ideograficznych znaków Han używanych w krajach azjatyckich.
Zaprojektowano go w taki sposób, że jego pierwszych 128 znaków jest identycznych z kodem ASCII ISO-7, a jego 256 znaków jest identycznych z kodem ASCII Latin-1 zawierającym m.in. większość znaków języków zachodnio-europejskich (bliższych informacji można zasięgnąć pod http://unicode.org).
Literały typu "char" mają postać 'c' gdzie c jest: znakiem widocznym (np. 'a'), symbolem znaku (np. '\n'), ósemkowym kodem znaku (np. '\141') albo czterocyfrowym szesnastkowym kodem znaku (np. '\u0061').
Uwaga: Szesnastkowy kod znaku zawiera literę u (a nie literę x, jak w C++).
Typ orzecznikowy
Typ orzecznikowy "boolean" ma taką samą interpretację jak w ANSI C++. Jest to typ logiczny z literałami false (fałsz) i true (prawda).
Zasługuje na podkreślenie, że typ orzecznikowy nie jest typem arytmetycznym oraz że nie istnieją konwersje między typami arytmetycznymi, a typem orzecznikowym.
Brak takich konwersji nie jest jednak istotnym utrudnieniem, ponieważ każdemu wyrażeniu arytmetycznemu e odpowiada orzeczenie (e)!=0, a każdemu orzeczeniu b wyrażenie (b)?1:0.
Biorąc to pod uwagę, następujące instrukcje C++
if(a) a = 1 / a;
a = a>1;
należy zapisać w Javie w postaci
if(a != 0) a = 1 /a;
a = a>1 ? 1 : 0;
_________________________________________________________________________________________
Typy obiektowe
Typy obiektowe są typami definiowanymi. Opisem typu obiektowego jest definicja klasy.
Każdy obiekt klasy składa się z niestatycznych pól zadeklarowanych w tej klasie oraz z pól zadeklarowanych w jej bezpośrednich i pośrednich podklasach.
Klasa bazowa jest w Javie nazywana nadklasą (oryg. superclass), a klasa pochodna podklasą (oryg. subclass).
Statyczne składniki klasy istnieją niezależnie od istnienia obiektów klasy. Dla odróżnienia ich od konstruktorów klasy oraz od jej składników niestatycznych: pól i metod (w C++ funkcji składowych), statyczne składniki klasy są nazywane zmiennymi i funkcjami klasy.
W odróżnieniu od C++, w Javie nie ma specyfikatora const, a zmienne ustalone (nazywane w Javie finalnymi) deklaruje się ze specyfikatorem final. Specyfikator final użyty w odniesieniu do metody oznacza, że w podklasie metoda ta nie może być przedefiniowana. Natomiast użyty w odniesieniu do pola i zmiennej oznacza jego albo jej niemodyfikowalność.
import java.io.*;
public
class Woman {
String name; // pole
byte age; // pole
final String LADY = "Lady"; // pole ustalone
static final int LIMIT = 100; // zmienna ustalona
Woman( // konstruktor
String namePar,
byte agePar)
{
name = namePar;
age = agePar;
}
public int getAge() // metoda
{
return age;
}
final String getName() // metoda finalna
{
return name;
}
static void printOld( // funkcja
PrintStream out,
Woman woman
)
{
if(woman.age > LIMIT)
out.println(woman.age);
}
}
Deklarowanie klas
W odróżnieniu od C++, definicja klasy w Javie może zawierać określenie czy klasa jest publiczna. Natomiast dziedziczenie klasy jest zawsze publiczne i wyraża się za pomocą frazy extends.
W odróżnieniu od C++, nie ma w Javie wielodziedziczenia, a jego odpowiednikiem jest implementowanie interfejsu.
Każda klasa zdefiniowana w Javie (z wyjątkiem predefiniowanej klasy Object, od której wywodzą się wszystkie inne klasy) dziedziczy dokładnie jedną nadklasę oraz może implementować dowolnie wiele interfejsów.
Uwaga: Jeśli w definicji klasy (różnej od Object) nie występuje fraza extends, to domniemywa się ją w postaci
extends Object
Następujące zestawienie pokazuje kilka typowych nagłówków definicji klas
public class Simple
class Simple
class Simple extends Object
class Complex extends Simple
class Complex extends Simple implements Runnable
class Complex extends Simple implements Runnable, Scalable
Pierwsza z wymienionych klas jest publiczna, a każda z pozostałych jest pakietowa (dostępna tylko z zawierającego ją pakietu).
Uwaga: W Javie nie ma innych klas niż publiczne i pakietowe.
Deklarowanie składników
Składnikami klasy są pola, funkcje, metody i konstruktory. Zasady ich deklarowania (z wyjątkiem konstruktorów!) nie odbiegają istotnie od zasad znanych z C++.
Należy jedynie pamiętać o tym, że nazwy sekcji języka C++ stały się w Javie specyfikatorami, a zatem, że następująca definicja zapisana w C++
class Point {
private:
int x, y;
public:
Point(int xVal, int yVal)
{
x = xVal;
y = yVal;
}
// ...
};
przybiera w Javie postać
class Point {
private int x, y;
public Point(int xVal, int yVal)
{
x = xVal;
y = yVal;
}
// ...
}
Odwoływanie się do składników
W odróżnieniu od C++, w Javie nie ma wskaźników do obiektów, a są tylko odnośniki do obiektów. Odnośnikiem, a nie wskaźnikiem, jest także zmienna this. W chwili wywołania metody klasy odnośnikowi this jest przypisywane odniesienie do obiektu, na rzecz którego odbywa się wywołanie.
W celu dokładniejszego wyjaśnienia pojęć wskaźnik i wskazanie oraz odnośnik i odniesienie, należy zacząć od spostrzeżenia, że w C++ istnieją dwa sposoby identyfikowania zmiennych: przez wskazanie i przez odniesienie.
W zasięgu następujących definicji
int Fix = 13;
int *pFix = &Fix;
int &rFix = Fix;
zmienna pFix identyfikuje Fix przez wskazanie (zmienną pFix zainicjowano wskazaniem), a zmienna rFix identyfikuje Fix przez odniesienie (zmienną rFix zainicjowano odniesieniem).
Uwaga: W C++ wskaźniki mogą być inicjowane oraz można im przypisywać dane, natomiast odnośniki mogą być tylko inicjowane.
Dokładna analiza wykazuje, że różnica między wskaźnikami/wskazaniami a odnośnikami/odniesieniami jest niewielka. W szczególności, dowolny program napisany w C++ nie ulegnie zmianie, jeśli
Każdy odnośnik zostanie zadeklarowany jako wskaźnik.
Każde wyrażenie inicjujące odnośnik poprzedzone operatorem &.
Każde odwołanie do odnośnika zostanie poprzedzone operatorem *.
Uwaga: Podany tu algorytm należałoby nieco rozbudować w przypadku użycia odnośników do zmiennych ustalonych.
Biorąc to pod uwagę, można więc (całkowicie mechanicznie!) zastąpić funkcję
int Sqrt(int Par)
{
int &Ref = Par;
return Ref * Ref;
}
funkcją
int Sqrt(int Par)
{
int *Ref = &Par;
return *Ref * *Ref;
}
A zatem, skoro odnośniki są tak podobne do wskaźników, nie powinno budzić zdziwienia, że Javie wystarczają tylko odnośniki.
Ponieważ jednak w Javie nie istnieje kwalifikator zakresu (::), a zamiast wskaźnika this występuje odnośnik, więc niektóre odwołania do składników klasy wyglądają w Javie nieco inaczej niż w C++.
W szczególności, następująca definicja klasy zapisana w ANSI C++
class Fixed {
private:
static int count = 0;
int fix;
public:
Fixed(int fix)
{
this->fix = fix;
Fixed::count++;
}
int getVal()
{
return fix;
}
};
przybiera w Javie postać
public
class Fixed {
static int count = 0;
private int fix;
public Fixed(int fix)
{
this.fix = fix; // this.fix, a nie this->fix
Fixed.count++; // Fixed.count, a nie Fixed::count
}
public int getVal()
{
return fix;
}
}
Odwoływanie się do nadklasy
W wypadku dziedziczenia klas i przesłonięcia (oryg. shadow) identyfikatorów nadklasy można użyć słowa kluczowego super.
W ciele podklasy, słowo super reprezentuje wówczas nazwę jej bezpośredniej nadklasy.
Dzięki temu, jeśli identyfikatorem składnika nadklasy jest Id, to super.Id jest kwalifikowaną nazwą tego składnika.
Uwaga: Ponieważ napis super.super.Id nie jest poprawny, więc jedynym sposobem odwołania się do pola i metody nie-bezpośredniej nadklasy jest zastosowanie konwersji odnośnika this.
W szczególności, następujący fragment programu w ANSI C++
class Alpha {
public:
static int count = 0;
int fix;
static void fun()
{
// ...
}
void met(void)
{
// ...
}
};
class Beta : public Alpha {
public:
int count, fix, fun, met; // przesłonięcia
void access(void)
{
Alpha::count++;
Alpha::fix++;
Alpha::fun();
Alpha::met();
}
// ...
};
class Gamma : public Beta {
public:
void access(void)
{
Alpha::count++;
Alpha::fix++;
Alpha::fun();
Alpha::met();
// ...
}
// ...
};
przybiera w Javie postać
public
class Alpha {
public static int count = 0;
public int fix;
public static void fun()
{
// ...
}
public void met()
{
// ...
}
}
class Beta extends Alpha {
public int count, fix, fun, met; // przesłonięcia
public void access(void)
{
super.count++;
super.fix++;
super.fun();
super.met();
}
// ...
}
class Gamma extends Beta {
public void access()
{
Alpha.count++;
((Alpha)this).fix++; // to samo co Alpha.fix++;
Alpha.fun();
((Alpha)this).met(); // to samo co this.met();
// ...
}
public void met()
{
// ...
}
// ...
}
Komentarz stwierdzający, że wywołanie
((Alpha)this).met()
jest równoważne wywołaniu
this.met()
wynika stąd, że w Javie każda procedura, która nie jest statyczna oraz nie jest prywatna, jest domyślnie wirtualna, a więc pozorne wywołanie metody met klasy Alpha jest niejawnie przekształcane w wywołanie przedefiniowującej ją metody met klasy Gamma.
Deklarowanie konstruktorów
Deklaracja konstruktora w Javie nie może zawierać listy inicjacyjnej, co powoduje, że nadanie wartości zmiennym obiektu musi odbyć się za pomocą przypisań.
Pierwszą instrukcją ciała konstruktora może być wywołanie innego konstruktora danej klasy. Wywołanie takie ma wówczas postać
this(Arg, Arg, ... , Arg);
w której każde Arg jest argumentem wywołania.
Jeśli instrukcji takiej nie użyto, to w jej miejscu może wystąpić wywołanie konstruktora nadklasy danej klasy
super(Arg, Arg, ... , Arg);
W pozostałych przypadkach, w miejscu każdej z takich instrukcji zostanie domniemana instrukcja
super();
stanowiąca wywołanie bezparametrowego konstruktora nadklasy.
Uwaga: Jeśli w klasie Klasa nie zadeklarowano ani jednego konstruktora, to definicja klasy zostanie niejawnie uzupełniona definicją
Klasa()
{
super();
}
Biorąc to wszystko pod uwagę, następujący fragment programu w C++
class Point {
private:
short x, y;
public:
Point(short x, short y) : x(x), y(y)
{
}
};
class Point3D : public Point {
private:
short z;
public:
Point3D(short x, short y, short z)
: Point(x,y), z(z)
{
}
};
przybiera w Javie postać
public
class Point {
private short x, y;
public Point(short x, short y)
{
this.x = x;
this.y = y;
}
}
public
class Point3D extends Point {
private short z;
public Point3D(short x, short y, short z)
{
super(x, y);
this.z = z;
}
}
Ponieważ klasa Point, podobnie jak każda klasa różna od Object, jest podklasą klasy Object, więc w miejscu tuż przed instrukcją
this.x = x;
zostanie niejawnie wstawiona instrukcja
super();
stanowiąca wywołanie bezparametrowego konstruktora klasy Object.
Inicjowanie pól i zmiennych
W Javie można deklaracje pól klasy uzupełniać inicjatorami (w ANSI C++ można w taki sposób inicjować tylko statyczne pola klasy).
Inicjator pola określa wartość początkową zmiennej, która w każdym obiekcie jest opisana przez rozpatrywane pole klasy. Podany inicjator jest opracowywany tuż po utworzeniu każdego obiektu klasy, jeszcze przed podjęciem wykonywania ciała konstruktora.
Inicjator pola statycznego określa wartość początkową zmiennej klasy. Jest on opracowywany jednokrotnie, tuż po załadowaniu klasy.
Uwaga: Zabrania się, aby w wyrażeniu inicjującym pole (albo zmienną) klasy wystąpiły odwołania do tych pól (albo zmiennych) własnej klasy, które są zadeklarowane leksykalnie później niż to pole (zmienna).
public
class Master {
public static void main(String args[])
{
Complex one = new Complex(3),
two = new Complex(3, 4);
System.out.println(Complex.count);
}
}
class Complex {
public static int count = 0;
private double re = 0, im = 0;
Complex()
{
count++;
}
Complex(double re)
{
this();
this.re = re;
}
Complex(double re, double im)
{
this(re);
this.im = im;
}
// ...
}
Podczas wykonywania instrukcji
System.out.println(Complex.count);
zmienna count ma wartość 2, a pole im obiektu one ma wartość 0.
Klasy abstrakcyjne
Klasą abstrakcyjną jest klasa zadeklarowana ze specyfikatorem abstract. Jeśli pewna klasa zawiera deklarację metody abstrakcyjnej, to jest metody zadeklarowanej ze specyfikatorem abstract, to musi być jawnie zadeklarowana jako abstrakcyjna.
Uwaga: Klasa abstrakcyjna różni się tym od klasy nieabstrakcyjnej tym, że nie można tworzyć jej obiektów bezpośrednio, to jest za pomocą za konstruktora albo metody fabrykującej (np. newInstance). Z tego powodu klasy abstrakcyjne są z reguły dziedziczone przez klasy nieabstrakcyjne.
public abstract
class Shape { // klasa abstrakcyjna
float x, y;
Shape(float x, float y)
{
this.x = x;
this.y = y;
}
public abstract double getArea(); // metoda abstrakcyjna
float getX() // metoda nieabstrakcyjna
{
return x;
}
float getY()
{
return y;
}
}
Klasa Shape zawiera deklarację metody abstrakcyjnej getArea oraz definicje dwóch metod nieabstrakcyjnych. Nie wolno pominąć specyfikatora abstract występującego w nagłówku definicji klasy.
Metody abstrakcyjne
W miejscu ciała metody abstrakcyjnej występuje średnik. W jednej z podklas klasy abstrakcyjnej musi dla każdej metody abstrakcyjnej być dostarczona metoda nieabstrakcyjna, o identycznej sygnaturze (oryg. signature) taka, której ciałem jest blok.
Uwaga: Dwie procedury mają identyczne sygnatury, jeśli mają takie same identyfikatory, a ich listy deklaracji parametrów (po usunięciu z nich identyfikatorów) składają się z takich samych jednostek leksykalnych.
W szczególności, dwie następujące procedury, mimo iż są istotnie różne (pierwsza jest funkcją, a druga metodą) mają identyczne sygnatury
static String proc(int one, int[] two[])
String proc(int uno, int[][] due)
Uwaga: Każda klasa, która odziedziczy metodę abstrakcyjną, ale nie dostarczy jej deklaracji nieabstrakcyjnej, sama staje się klasą abstrakcyjną.
abstract
class Shape {
float x, y;
Shape(float x, float y)
{
this.x = x;
this.y = y;
}
public abstract double getArea(); // metoda abstrakcyjna
float getX()
{
return x;
}
float getY()
{
return y;
}
}
public
class Circle extends Shape {
static final double Pi = Math.PI;
private float radius;
Circle(float x, float y, float radius)
{
super(x, y);
this.radius = radius;
}
public float getArea() // metoda nieabstrakcyjna
{
return Pi * radius * radius;
}
}
W klasie Circle przedefiniowano metodę abstrakcyjną getArea klasy Shape. Gdyby klasa Circle nie zawierała definicji metody getArea, to byłaby klasą abstrakcyjną.
Tworzenie obiektów
W odróżnieniu od C++, w Javie nie ma operatora new. Do utworzenia obiektu służy wyrażenie fabrykujące (oryg. instance creation expression) o postaci
new WywołanieKonstruktora
na przykład
new Circle(10, 10, 40)
Operacja wyrażona przez wyrażenie fabrykujące jest w uproszczeniu nazywana operacją new.
Ładowanie klas
W odróżnieniu od C++, w Javie stosuje się dynamiczne ładowanie klas (oryg. class loading). Polega ono na tym, że B-kod klasy jest dołączany do wykonywanego programu tylko wówczas, gdy kod klasy musi być aktywnie użyty (oryg. actively used).
W szczególności, jeśli w programie występuje deklaracja
FileInputStream src;
to klasa FileInputStream zostanie załadowana na przykład w instrukcji
src = new FileInputStream("Source");
gdyż dopiero tutaj ma miejsce aktywne użycie jej składników.
Dynamiczne ładowanie klas ma szczególnie istotne znaczenie w odniesieniu do programów, których klasy pochodzą z sieci globalnej, kiedy to załadowanie klasy, która nie jest aktywnie użyta, byłoby nie tylko zbyteczne, ale również czasochłonne.
Decyzja o dynamicznym ładowaniu klas wiąże się jednak z zagrożeniem, że w okresie między skompilowaniem i wykonaniem programu pewne klasy zostały zmienione i do tego programu już nie pasują.
W klasycznych językach programowania problem aktualizacji programu jest rozwiązywany przez ponowną kompilację. W Javie ponowna kompilacja nie jest w zasadzie wymagana, jednak aby uniknąć oczywistych sprzeczności, precyzyjnie określono zakres dopuszczalnych zmian definicji klas, które nie wpływają szkodliwie na wykonanie korzystającego z nich programu.
Uwaga: Ponieważ przedyskutowanie wszystkich dopuszczalnych zmian mogłoby być dość nużące, wystarczy podać, że na przykład dodanie do klasy nowych pól nie wymaga ponownego skompilowania programu (co jest niezbędne w C++).
class Primary {
static {
System.out.print("Primary ");
}
}
class Other {
static {
System.out.print("Other ");
}
}
class Derived extends Primary {
static {
System.out.print("Derived ");
}
}
public
class Master {
public static void main(String args[])
{
Other other = null;
new Derived();
System.out.println((Object)other == null);
}
}
Ponieważ klasa Other nie jest aktywnie użyta (nie utworzono ani jednego jej obiektu), więc nie będzie załadowana.
Wykonanie programu spowoduje wyprowadzenie napisu
Primary Derived true
a nie (jak w analogicznym programie napisanym w C++), napisu
Other Primary Derived true
Inicjowanie klas
Niekiedy zachodzi potrzeba wykonania czynności inicjujących dotyczących klasy jako całości (w odróżnieniu od inicjowania obiektów klasy).
W takim wypadku można umieścić w jej ciele dowolnie wiele inicjatorów klasy (oryg. class initializer) o postaci
static Blok
których każdy Blok zostanie wykonany (w kolejności jego wystąpienia w klasie) podczas ładowania klasy.
import java.util.*;
class SomeClass {
static long loadTime;
SomeClass()
{
}
static {
loadTime = System.currentTimeMillis();
}
// ...
}
Podczas ładowania klasy SomeClass, zmiennej loadTime zostanie przypisana liczba wyrażająca aktualny czas w milisekundach względem północy 1 stycznia 1970, czasu Greenwich (GMT).
_________________________________________________________________________________________
Typy łańcuchowe
Predefiniowanymi typami łańcuchowymi Javy są String i StringBuffer. W istocie nie są to odrębne typy, gdyż zostały implementowane jako klasy obiektowe. Jednak ze względu na szczególną rolę jaką pełnią w języku, zasługują na specjalne potraktowanie.
Zarówno String, jak i StringBuffer, są klasami finalnymi (oryg. final). Obiekty klasy String są niemodyfikowalne, a więc, jeśli rezultat pewnej operacji jest klasy String, to nie musi być odrębnym obiektem.
Obiekty klasy StringBuffer są modyfikowalne. Klasa ta jest używana przede wszystkim do tworzenia nowych obiektów klasy String.
W szczególności, wyrażenie
4 + "sale"
jest niejawnie przekształcane w wyrażenie
new StringBuffer().append(4).append("sale")
Klasa String
Metody klasy String są używane do porównywania, porządkowania, rozpoznawania, wycinania i przekształcania znaków.
Uwaga: Znaki łańcuchów są indeksowane od 0. Jeśli metoda ma zwrócić indeks znaku, ale takiego znaku nie ma, to zwraca wartość -1.
boolean equals(Object obj)
Metoda służy do porównania łańcucha z obiektem przekształconym w łańcuch (najczęściej z innym łańcuchem).
String hello = "Hello";
String world = "World";
boolean result = hello.equals(world); // false
int compareTo(String string)
Metoda służy do porównania dwóch łańcuchów. Jeśli są równe, to rezultatem jest 0. Jeśli pierwszy jest mniejszy, to rezultatem jest liczba ujemna, a jeśli drugi, to dodatnia.
String hello = "Hello";
String world = "World";
int value = hello.compareTo(world);
if(value < 0)
text = "less";
else if(value == 0)
text = "equal";
else
text = "greater";
boolean result = text.equals("less"); // true
char charAt(int pos)
Metoda zwraca znak występujący na podanej pozycji łańcucha.
String string = "Hello";
char chr = string.charAt(4); // 'o'
int indexOf(char chr)
Metoda zwraca indeks pierwszego wystąpienia podanego znaku w łańcuchu.
String string = "buffer";
int result = string.indexOf('u'); //1
int indexOf(String string)
Metoda zwraca indeks pierwszego znaku stanowiącego podany podciąg łańcucha.
String string = "remaining";
int pos = string.indexOf("main"); // 2
String substring(int from, int to)
Metoda zwraca podciąg łańcucha składający się ze znaków występujących "między" znakami o podanych indeksach.
String string = "0123456789";
String result = string.substring(2, 6); // 2345 (sic!)
String toUpperCase(String string)
Metoda zwraca łańcuch powstały z oryginału przez zamianę każdej małej litery na dużą.
String string = "Hello";
String result = string.toUpperCase(); // HELLO
String toLowerCase(String string)
Metoda zwraca łańcuch powstały z oryginału przez zamianę każdej dużej litery na małą.
String string = "Hello World";
String result = string.toLowerCase(); // hello world
Klasa StringBuffer
Metody klasy StringBuffer są używane do przekształcania ciągów znaków. W odróżnieniu od metod klasy String, metody te na ogół nie tworzą nowych obiektów, ale modyfikują je.
void setCharAt(int index, char chr)
Metoda zmienia znak na podanej pozycji łańcucha.
StringBuffer string = new StringBuffer("Hello World");
string.setCharAt(5, '*');
String result = new String(string); // Hello*World
StringBuffer append(char chr)
Metoda wydłuża łańcuch o podany znak.
StringBuffer string = new StringBuffer("Hello");
string.append('*').append("World");
String result = new String(string); // Hello*World
StringBuffer append(String string)
Metoda wydłuża łańcuch o podany łańcuch.
StringBuffer string = new StringBuffer("Hello");
string.append("World");
String result = new String(string); // HelloWorld
StringBuffer append(char arr[], int offset, int len)
Metoda wydłuża łańcuch o znaki podanego wycinka tablicy.
char arr[] = { ' ', 'W', 'o', 'r', 'l', 'd' };
StringBuffer string = new StringBuffer("Hello");
string.append(arr, 1, 5);
String result = new String(string); // HelloWorld
StringBuffer append(Object obj)
Metoda wydłuża łańcuch o łańcuch utworzony z podanego obiektu, po wywołaniu na jego rzecz metody toString jego klasy.
Double value = new Double(2 - 3.4);
StringBuffer string = new StringBuffer("x");
string.append(value);
String result = new String(string); // x-1.4
StringBuffer insert(int offset, char chr)
Metoda wstawia podany znak w podanym miejscu łańcucha.
StringBuffer string = new StringBuffer("HelloWorld");
string.insert(5, '*');
String result = new String(string); // Hello*World"
StringBuffer insert(int offset, String string)
Metoda wstawia podany łańcuch w podanym miejscu łańcucha.
StringBuffer string = new StringBuffer("HelloWorld");
string.insert(5, "***");
String result = new String(string); // Hello***World"
StringBuffer insert(int offset, char arr[])
Metoda wstawia znaki tablicy w podanym miejscu łańcucha.
char arr[] = { '*', ' ', '*' };
StringBuffer string = new StringBuffer("HelloWorld");
string.insert(5, arr);
String result = new String(string); // Hello* *World"
StringBuffer insert(int offset, Object obj)
Metoda wstawia znaki łańcucha utworzonego z podanego obiektu, po wywołaniu na jego rzecz metody toString jego klasy.
Integer value = new Integer(2);
StringBuffer string = new StringBuffer("x");
string.insert(0, value);
String result = new String(string); // 2x
_________________________________________________________________________________________
Typy interfejsowe
Typy interfejsowe są typami definiowanymi. Opisem typu interfejsowego jest definicja interfejsu. Definicja interfejsu nie zawiera deklaracji funkcji i konstruktorów, a wszystkie zadeklarowane w niej metody są publiczne i abstrakcyjne.
Typ interfejsowy deklaruje się podobnie jak typ obiektowy, ale za pomocą słowa kluczowego interface. Słowo to występuje w miejscu słowa kluczowego class.
Zabrania się użycia specyfikatorów private i protected. Użycie specyfikatorów abstract i public jest zbyteczne.
interface Drawable {
static Color myBlue = Color.blue; // zmienna
void setColor(Color color); // metoda
void draw(Graphics gDC);
}
Mimo braku specyfikatorów abstract i public, metody setColor i draw są publiczne i abstrakcyjne. Abstrakcyjny jest także interfejs Drawable.
Interfejs nie może dziedziczyć klas, ale może dziedziczyć dowolnie wiele interfejsów. Jeśli interfejs nie zawiera deklaracji metod, to pełni podobną rolę jak typ wylicznikowy C++, gdyż zadeklarowane w nim zmienne są dostępne w każdej klasie i w każdym interfejsie, które implementują ten interfejs.
public
interface Status {
static final byte
Single = 0, Married = 1, Divorced = 2;
}
interface Modifiable extends Growable, Moveable, Stackable {
// ...
}
Interfejs Modifiable dziedziczy interfejsy Growable, Moveable i Stackable.
W ciele każdej klasy implementującej interfejs Status można używać symboli Single, Married i Divorced.
Implementowanie interfejsu
Interfejs jest dodatkiem do hierarchii klas. O ile bowiem każda para klas ma tylko jednego wspólnego przodka, o tyle może mieć dodatkowo dowolnie wiele wspólnych interfejsów.
Dzięki temu, nawet wówczas gdy nie ma możliwości dodania metod do wspólnego przodka dwóch odrębnych klas, można je wyposażyć w ten sam interfejs, a następnie, w każdej z tych klas, przedefiniować jego metody abstrakcyjne. Taki sposób postępowania zadowalająco zastępuje wielodziedziczenie.
interface Sortable {
public boolean less(Sortable item);
}
class Person implements Sortable {
private String name;
private int age;
Person(String name, int age)
{
this.name = name;
this.age = age;
}
// ...
public boolean less(Sortable item) // "mniejsze"
{
Person person = (Person)item;
int value = name.compareTo(person.name);
if(value < 0)
return true;
else if(value > 0)
return false;
return age < person.age;
}
}
class Complex implements Sortable {
private double re, im;
Complex(double re, double im)
{
this.re = re;
this.im = im;
}
// ...
public boolean less(Sortable item) // "mniejsze"
{
Complex complex = (Complex)item;
if(re < complex.re)
return true;
if(re == complex.re && im < complex.im)
return true;
return false;
}
}
class Sorter {
public static Sortable min(Sortable one, Sortable two)
{
if(one.less(two))
return one;
return two;
}
// ...
public static void main(String args[])
{
Person john = new Person("John", 40),
mary = new Person("Mary", 30);
Person name = (Person)Sorter.min(john, mary);
// ...
Complex one = new Complex(2, 3),
two = new Complex(2, 4);
Complex value = (Complex)Sorter.min(one, two);
// ...
}
}
Funkcja min może być użyta do wyznaczenia minimum z każdej pary obiektów dowolnej klasy implementującej interfejs Sortable.
Interfejsy równorzędne
W implementacji pakietu java.awt poczesne miejsce zajmują interfejsy równorzędne (oryg. peer). Każdy z nich odpowiada pewnej klasie pakietu AWT, definiując metody, które muszą być utrzymywane (oryg. supported) na konkretnej platformie graficznej (np. Windows 95).
Przeniesienie komponentów graficznych na nową platformę sprowadza się więc od implementowania wszystkich metod każdego z interfejsów równorzędnych. Ponieważ nie jest to zadanie zwykłego użytkownika, wystarczy powiedzieć, że ogół tych wymaganych implementacji jest zebrany w obiekcie klasy abstrakcyjnej Toolkit, tworzonym niejawnie przez System.
_________________________________________________________________________________________
Typy odnośnikowe
Typ odnośnikowy jest typem danych identyfikujących obiekty. W zależności od tego czy w deklaracji odnośnika użyto nazwy klasy czy interfejsu, typ odnośnikowy jest obiektowy, albo interfejsowy.
Odnośnikom można przypisywać odniesienia. Predefiniowanym odniesieniem jest odniesienie puste (oryg. null), reprezentowane przez słowo kluczowe null. Odniesienie puste można przypisać odnośnikowi dowolnego typu.
Uwaga: Każdy literał łańcuchowy, na przykład "Hello", jest nazwą odnośnika do obiektu klasy String. Dla każdej pary identycznych literałowych wyrażeń stałych (w tym pary identycznych literałów), na przykład "Hello" i "He"+"llo" odnośnik ten ma taką samą wartość.
Tworzenie odniesień
W Javie do utworzenia odniesienia służy wyrażenie fabrykujące, które ma jedną z następujących postaci
new WywołanieKonstruktora
new TypPodstawowy Wymiary
new TypObiektowy Wymiary
w której fraza Wymiary jest zestawem fraz o postaci [Wyrażenie]. Wyrażenia występujące w końcowych wymiarach mogą być pominięte.
Na przykład
new String("Hello") // utworzenie obiektu
new int [3] // utworzenie wektora
new int [3][] // utworzenie wektora
new int [3][2] // utworzenie tablicy
new String [3] // utworzenie wektora
Rezultatem każdej operacji new jest odnośnik zainicjowany odniesieniem do właśnie utworzonego obiektu. W szczególności, jeśli klasa Circle ma konstruktor, który może być wywołany z trzema argumentami typu "int", to wykonanie operacji
new Circle(10, 10, 40)
powoduje utworzenie obiektu klasy Circle, zainicjowanego przez wymieniony konstruktor oraz dostarczenie, jako rezultatu tej operacji, odnośnika zainicjowanego odniesieniem do właśnie utworzonego obiektu.
Uwaga: W C++ odnośniki mogą być tylko inicjowane. W Javie można je zarówno inicjować, jak i przypisywać im odniesienia do obiektów. Z tego powodu w Javie na przykład instrukcję
Circle circle = new Circle(10, 10, 40); // zainicjowanie
można zastąpić parą instrukcji
Circle circle;
circle = new Circle(10, 10, 40); // przypisanie
Biorąc pod uwagę różnice między Javą i C++ w zakresie interpretowania deklaracji zmiennych typów definiowanych oraz semantyki operacji new, przydatne może być spostrzeżenie, że odpowiednikiem zapisanej w C++ instrukcji
Circle &circle = *new Circle(10, 10, 40);
jest w Javie
Circle circle = new Circle(10, 10, 40);
Przetwarzanie odniesień
Programując w Javie należy zwrócić szczególną uwagę na to, że w odróżnieniu od C++, wykonanie operacji przypisania nie powoduje skopiowania obiektu, ale jedynie skopiowanie odniesienia do obiektu.
Dlatego, po wykonaniu instrukcji
String Mary = new String("Mary");
String Girl;
Girl = Mary;
odnośnik Girl identyfikuje ten sam obiekt, który jest identyfikowany przez odnośnik Mary.
Ponadto, po wykonaniu powyższych instrukcji, relacja
Girl == Mary
jest w Javie poprawna i prawdziwa, ponieważ porównanie dotyczy odniesień, a nie obiektów.
Uwaga: Korzystając z bibliotek należy w Javie zwrócić szczególną uwagę na to, czy procedura o rezultacie odnośnikowym, taka jak na przykład getColor (podaj kolor), dostarcza odniesienia do obiektu opisującego kolor, czy do kopii takiego obiektu. Jeśli opis nie jest dostatecznie precyzyjny, to warto jest przeprowadzić eksperyment.
Operator instanceof
Javę wyposażono w operator instanceof, nie znany w C++. Operator ten użyty w operacji
Odnośnik instanceof Klasa
wyraża prawdziwość orzeczenia
Odnośnik nie jest pusty, a przypisane mu odniesienie identyfikuje obiekt klasy Klasa (albo dowolnej jej podklasy).
Natomiast użyty w operacji
Odnośnik instanceof Interfejs
wyraża prawdziwość orzeczenia
Odnośnik nie jest pusty, a przypisane mu odniesienie identyfikuje obiekt klasy implementującej Interfejs.
W następującej metodzie do obsługi zdarzeń, bada się czy zdarzenie dotyczy przycisku (oryg. Button), a w przypadku twierdzącym podaje jego nazwę.
public boolean handleEvent(Event evt)
{
if(evt.target instanceof Button) {
Graphics gDC = getGraphics();
gDC.drawString("You clicked on Button " + evt.arg, 20, 20);
return true;
}
return super.handleEvent(evt);
}
Jeśli przycisk opatrzono opisem Greet, to nastąpi wyprowadzenie napisu
You clicked on Button Greet
Porównywanie obiektów
W celu zbadania równości pary obiektów należy posłużyć się wyspecjalizowaną metodą. Dla predefiniowanej klasy String jest nią metoda compareTo, a dla wielu innych klas metoda equals.
static void printSorted(String one, String two)
{
int value = one.compareTo(two)
if(value <= 0)
System.out.println(one + " " + two);
else
System.out.println(two + " " + one);
}
Podana funkcja służy do wyprowadzenia jej argumentów w kolejności alfabetycznej.
Klonowanie obiektów
Jak już wyjaśniono, jeśli Girl i Mary są odnośnikami do obiektów, to wykonanie operacji
Girl = Mary
powoduje jedynie skopiowanie odnośnika, a nie utworzenie klonu (oryg. clone) obiektu identyfikowanego przez odnośnik Mary.
A zatem w celu utworzenia klonu obiektu identyfikowanego przez odnośnik Mary, należy użyć konstruktora kopiującego (oryg. copy constructor), na przykład
Girl = new Person(Mary)
albo metody clone, na przykład
Girl = Mary.clone()
Uwaga: Operacja klonowania może być wykonana tylko na obiektach klasy implementującej interfejs Cloneable. Wykonana za pomocą metody clone klasy Object polega na płytkim (oryg. shallow), to jest bitowym skopiowaniu pól obiektu (a więc bez kopiowania obiektów identyfikowanych przez pola, na czym polegałoby kopiowanie głębokie).
Następujący przykład ilustruje sposób definiowania klasy, której obiekty mogą być klonowane przez konstruktor kopiujący oraz przez metodę myClone.
public
class Master {
public static void main(String args[])
throws CloneNotSupportedException
{
Person john = new Person("John Smith", 40, "007");
Person johnClone = (Person)john.myClone();
john = null; // zniszczenie oryginału
System.gc(); // odzyskanie pamięci
johnClone.show();
// ...
}
}
class Citizen {
String passNo;
Citizen(String passNo)
{
this.passNo = passNo;
}
Object myClone() // głębokie klonowanie
throws CloneNotSupportedException
{
Citizen citizen =
(Citizen)clone(); // płytkie klonowanie
passNo = new String(passNo); // pogłębienie
return citizen;
}
}
class Person extends Citizen implements Cloneable {
private String name;
private int age;
// głębokie kopiowanie
public Person(String name, int age, String pass)
{
super(pass);
this.name = new String(name);
this.age = age;
}
// głębokie klonowanie
Object myClone()
throws CloneNotSupportedException
{
Person person = (Person)super.myClone();
person.name = new String(name);
person.age = age; // zbyteczne (sic!)
return person;
}
void show()
{
System.out.println("PassNo: " + passNo +
", Name: " + name +
", Age: " + age);
}
// ...
}
W instrukcji
Person person = (Person)super.myClone();
jest wywoływana metoda myClone klasy Citizen, a w niej metoda clone klasy Object.
Wykonanie metody clone klasy Object powoduje utworzenie płytkiej kopii obiektu klasy Person (sic!). Dlatego zarówno w metodzie myClone klasy Citizen, jak i w metodzie myClone klasy Person zawarto instrukcje "pogłębiające" klonowanie.
Deklarowanie odnośników
Ogólne zasady posługiwania się odnośnikami i odniesieniami są następujące
Jeśli Klasa jest nazwą typu obiektowego, to deklaracja
Klasa Nazwa
oznajmia, że Nazwa jest odnośnikiem, któremu można przypisać odniesienie do obiektu klasy Klasa, a także odniesienie do dowolnego obiektu jej podklasy.
A zatem w zasięgu następujących definicji
class Primary {
// ...
}
class Derived extends Primary {
// ...
}
instrukcja
Primary ref;
jest deklaracją odnośnika, któremu można przypisać zarówno odniesienie do obiektu klasy Primary
ref = new Primary()
jak i odniesienie do obiektu klasy Derived
ref = new Derived()
Jeśli Interfejs jest nazwą typu interfejsowego, to deklaracja
Interfejs Nazwa
oznajmia, że Nazwa jest odnośnikiem, któremu można przypisać odniesienie do obiektu każdej klasy, która odziedziczyła ten interfejs.
A zatem w zasięgu następujących definicji
class Primary implements Printable {
// ...
}
class Derived extends Primary {
// ...
}
instrukcja
Printable ref;
jest deklaracją odnośnika, któremu można przypisać zarówno odniesienie do obiektu klasy Primary
ref = new Primary();
jak i odniesienie do obiektu klasy Derived
ref = new Derived();
Odnośniki nieobiektowe
W odróżnieniu od C++, nie ma w Javie odnośników do zmiennych typów nieobiektowych oraz nie można użyć operacji new do utworzenia na stercie (oryg. heap) zmiennej typu nieobiektowego.
W konsekwencji, nie istnieje możliwość wykonania nawet takiej prostej operacji C++ jak na przykład
new boolean(true)
Biorąc pod uwagę to ograniczenie, predeklarowano w Javie rodzinę klas kopertowych (oryg. envelope), w tym: Integer, Long, Float, Double, Character i Boolean, umożliwiających wykonywanie podobnych operacji.
W szczególności, dzięki istnieniu klasy kopertowej Boolean, stworzono możliwość wykonania operacji
new Boolean(true)
którą dobry kompilator Javy przekształci w równie efektywny kod wynikowy jak kod utworzony z
new boolean(true)
przez dobry kompilator C++.
Klasy kopertowe
Zasadę tworzenia klas kopertowych, wyjaśnia następująca definicja predefiniowanej klasy kopertowej Boolean
public final
class Boolean {
// ...
private boolean value;
public Boolean(boolean value)
{
this.value = value;
}
public static Boolean valueOf(String s)
{
return new Boolean((s != null) &&
s.toLowerCase().equals("true"));
}
public String toString()
{
return value ? "true" : "false";
}
public boolean booleanValue()
{
return value;
}
public boolean equals(Object obj)
{
if((obj != null) && (obj instanceof Boolean))
return value == ((Boolean)obj).booleanValue();
return false;
}
// ...
}
Własne klasy kopertowe
Definiowanie własnych klas kopertowych może okazać się konieczne do wykonywania także innych prostych czynności, takich jak na przykład zamiana wartości dwóch zmiennych.
W szczególności, następujący program w C++
#include <iostream.h>
int main(void)
{
int neg = -100, pos = +100;
void swap(int &, int &);
cout << neg << " " << pos << endl; // -100 100
swap(neg, pos);
cout << neg << " " << pos << endl; // 100 -100
return 0;
}
void swap(int &one, int &two)
{
int tmp = one;
one = two;
two = tmp;
}
po przetworzeniu w program napisany w Javie, nie daje (sic!) oczekiwanego efektu, nawet jeśli użyje się predefiniowanej klasy kopertowej Integer
public
class Swap {
public static void main(String args[])
{
Integer neg = new Integer(-100),
pos = new Integer(+100);
System.out.println(neg.intValue() + " " +
pos.intValue()); // -100 100
swap(neg, pos);
System.out.println(neg.intValue() + " " +
pos.intValue()); // -100 100
}
static void swap(Integer one, Integer two)
{
Integer tmp = new Integer(one.intValue());
one = new Integer(two.intValue());
two = new Integer(tmp.intValue());
}
}
ale daje taki efekt jeśli użyje się własnej klasy kopertowej Integer2
public
class Swap {
public static void main(String args[])
{
Integer2 neg = new Integer2(-100),
pos = new Integer2(+100);
System.out.println(neg.intValue() + " " +
pos.intValue()); // -100 100
swap(neg, pos);
System.out.println(neg.intValue() + " " +
pos.intValue()); // 100 -100
}
static void swap(Integer2 one, Integer2 two)
{
int tmp = one.intValue();
one.setValue(two.intValue());
two.setValue(tmp);
}
}
class Integer2 {
private int value;
Integer2(int value)
{
this.value = value;
}
intValue()
{
return value;
}
void setValue(int value)
{
this.value = value;
}
}
Odnośniki i wskaźniki
Różnica między odnośnikami i wskaźnikami jest marginalna. Dlatego nie należy ubolewać nad tym, że w Javie nie ma wskaźników.
Dla poparcia tej tezy, następujący program w C++, posługujący się wskaźnikami w celu utworzenia listy obiektów
#include <iostream.h>
class Fixed {
private:
Fixed *pNext;
int val;
public:
Fixed(Fixed *pNext, int val)
: pNext(pNext), val(val)
{
}
Fixed *getNext(void)
{
return pNext;
}
int getVal(void)
{
return val;
}
};
int main(void)
{
int arr[] = { 10, 20, 30, 40, 50 };
Fixed *pHead = 0, *pTemp;
int length = sizeof(arr) / sizeof(int);
for(int i = 0; i < length ; i++)
pHead = new Fixed(pHead, arr[i]);
while(pHead) {
cout << pHead->getVal() << ' ';
pHead = (pTemp = pHead)->getNext();
delete pTemp;
}
cout << endl;
return 0;
}
przybiera w Javie postać
public
class List {
public static void main(String args[])
{
int arr[] = { 10, 20, 30, 40, 50 };
Fixed pHead = null;
int length = arr.length;
for(int i = 0; i < length ; i++)
pHead = new Fixed(pHead, arr[i]);
while(pHead != null) {
System.out.print(pHead.getVal() + " ");
pHead = pHead.getNext();
}
System.out.println();
}
}
class Fixed {
private Fixed pNext;
private int val;
public Fixed(Fixed pNext, int val)
{
this.pNext = pNext;
this.val = val;
}
public Fixed getNext()
{
return pNext;
}
public int getVal()
{
return val;
}
}
Na uwagę zasługuje brak operacji delete.
_________________________________________________________________________________________
Typy tablicowe
W Javie, podobnie jak w C++, istnieją tylko tablice-wektory. Ale ponieważ elementami tablicy mogą być tablice, więc tak jak w C++, można mówić o istnieniu tablic wielowymiarowych.
Uwaga: W odróżnieniu od C++, tablica w Javie jest obiektem. Każda typ tablicowy jest podklasą klasy Object i implementuje interfejs Cloneable.
Nazwa typu tablicowego w Javie ma postać podobną jak w C++, ale bez określenia rozmiaru tablicy. Ponadto każda tablica ma sygnaturę, która zaczyna się od tylu znaków [ (nawias kwadratowy otwierający) ile tablica ma wymiarów.
W Tabeli Sygnatury pokazano zestaw sygnatur kilku reprezentatywnych typów danych.
Tabela Sygnatury
###
Nazwa typu Sygnatura
int [] [I
long [] [J
String [][] [[Ljava.lang.String;
###
Uwaga: Jeśli elementy podstawowe tablicy są typu definiowanego, to sygnatura zawiera pełną nazwę klasy elementu. Typy predefiniowane są kodowane jednoliterowo: byte B, char C, float F, double D, int I, long J, short S, boolean Z.
public
class Master {
public static void main(String args[])
{
int arr[][][] = new int [3][][];
Class classObj = arr.getClass();
String name = classObj.getName();
System.out.println(name); // [[[I
String vec[] = new String [3];
classObj = vec.getClass();
name = classObj.getName();
System.out.println(name); // [Ljava.lang.String;
}
}
Pole length
Z każdą tablicą i jej elementem tablicowym jest zdefiniowane pole length, o wartości równej liczbie elementów tablicy. Tego rozmiaru tablicy nie należy mylić z liczbą elementów podstawowych tablicy.
Pole length nie należy do określenia typu tablicowego. Dlatego temu samemu odnośnikowi do tablicy można przypisywać odniesienia do tablic o innym rozmiarze oraz odniesienia do tablic innego, byle zgodnego z nią typu (oryg. compatible).
Uwaga: Tablica docelowa jest zgodna ze źródłową jeśli ma taką samą liczbę wymiarów, a elementy podstawowe obu tablic są zgodne w sensie przypisania (ma to miejsce na przykład wówczas, gdy klasa elementu podstawowego tablicy docelowej jest podklasą elementu podstawowego tablicy źródłowej).
Deklarowanie tablic
Jeśli elementy tablicy są typu podstawowego Typ ("byte", "short", "int", "long", "float", "double", "char", "boolean"), to deklaracja
Typ Nazwa[]
oznajmia, że Nazwa jest odnośnikiem (typu "Typ []"), do wektora elementów, z których każdy jest typu Typ.
Uwaga: W C++ analogiczna deklaracja miałaby postać
Typ (&Nazwa)[]
Podczas opracowania (oryg. elaboration) rozpatrywanej deklaracji, odnośnikowi Name jest przypisywane odniesienie puste.
Podczas deklarowania, albo w odrębnej instrukcji, odnośnikowi do tablicy jest zazwyczaj przypisywane odniesienie do tablicy-obiektu utworzonej na stercie.
A zatem, wykonanie następującej instrukcji
int arr[] = { 10, 20, 30 };
ma taki sam skutek jak wykonanie instrukcji
int arr[];
arr = new int [3];
for(int i = 0; i < arr.length ; i++)
arr[i] = 10 * (i + 1);
Uwaga: Ponieważ inicjator zmiennej musi być dokładnie takiego samego typu jak inicjowana zmienna, więc na przykład deklaracja
char arr[] = "Hello";
jest w Javie błędna, gdyż arr jest typu "char []", a "Hello" jest typu "String".
Jeśli elementy tablicy są typu obiektowego Typ, to deklaracja
Typ Nazwa[]
oznajmia, że Nazwa jest odnośnikiem (typu "Typ []"), do tablicy odnośników do obiektów typu Typ.
Uwaga: W C++ analogiczna deklaracja (gdyby była dopuszczalna!) miałaby postać
Typ &(&Name)[]
Podczas opracowywania rozpatrywanej deklaracji, odnośnikowi Name jest przypisywane odniesienie puste. Podczas deklarowania tablicy, albo w odrębnej instrukcji, odnośnikowi jest zazwyczaj przypisywane odniesienie do wektora odnośników utworzonego na stercie.
A zatem, wykonanie następującej instrukcji
String arr[] = { "Hello", "World" };
ma taki sam skutek jak wykonanie instrukcji
String arr[] = new String [2];
arr[0] = new String("Hello");
arr[1] = new String("World");
Liczba wymiarów tablicy
Liczbę wymiarów tablicy określa liczba pustych nawiasów kwadratowych. Nawiasy te mogą być w dowolny sposób rozdzielone między specyfikator i deklarator.
W szczególności, deklaracja
int arr[][][]
jest m.in. równoważna deklaracji
int[][] arr[]
a następujący nagłówek funkcji
static String[] fun()
jest m.in. równoważny nagłówkowi
static String fun()[]
Uwaga: Programujących w Javie zniechęca się (oryg. deprecates) do używania deklaracji funkcji w ostatniej z tych postaci.
Inicjatory klamrowe
Każdy inicjator klamrowy jest nazwą odnośnika do tablicy zainicjowanej wyrażeniami zawartymi w inicjatorze.
Na przykład, instrukcja
int arr[][] = { { 1, 2 }, null };
deklaruje tablicę dwuwymiarową i jest równoważna instrukcjom
int arr[][] = new int [2][];
arr[0] = new int [2];
arr[0][0] = 1;
arr[0][1] = 2;
arr[1] = null;
Tablice wielowymiarowe
Do tablic wielowymiarowych mają zastosowanie analogiczne zasady zarządzania pamięcią jak do tablic jednowymiarowych.
Ponieważ identyfikator tablicy wielowymiarowej jest odnośnikiem do wektora odnośników, a każdy z nich może zawierać odniesienie do podtablicy o innym rozmiarze, więc istnieje możliwość tworzenia tablic nieprostokątnych.
Na przykład, trójkątna tablica dwuwymiarowa
int arr[][] = { { 10 }, { 20, 20 }, { 30, 30, 30 } };
mogłaby w równoważny sposób zostać utworzona i zainicjowana za pomocą instrukcji
int arr[][];
arr = new int [3][];
for(int i = 0; i < arr.length ; i++) {
arr[i] = new int [i+1];
for(int j = 0; j < arr[i].length ; j++)
arr[i][j] = 10 * (i+1);
}
Uwaga: Z podanego przykładu wynika, że w Javie nie można stosować znanych z C++ zasad pomijania inicjatorów. Wynika to stąd, że na przykład instrukcja
int arr[][] = { // trójkąt
{ 10 },
{ 20, 20 },
{ 30, 30, 30 }
};
nie jest równoważna instrukcji
int arr[][] = { // prostokąt
{ 10, 0, 0 },
{ 20, 20, 0 },
{ 30, 30, 30 }
};
Przetwarzanie elementów
W odróżnieniu od C++, tablice w Javie są obiektami, a omyłkowe odwołanie się do nie istniejącego elementu jest niemożliwe, gdyż powoduje wysłanie wyjątku klasy IndexOutOfBoundsException.
Wysłanie takiego wyjątku powoduje zazwyczaj zakończenie wykonywania programu (chyba że wyjątek IndexOutOfBoundsException zostanie przechwycony i obsłużony).
public
class Greet {
public static void main(String args[])
{
System.out.println("Hello " +
args[0] + " " + args[1]);
}
}
Jeśli B-kod klasy znajduje się w pliku Greet.class, to po wydaniu polecenia
java Greet Jan B.
wyprowadzi się napis
Hello Jan B.
Jeśli jednak program zostanie wywołany z jednym argumentem, na przykład za pomocą polecenia
java Greet JanB.
to argument identyfikowany przez args[1] nie będzie istniał i wykonywanie programu zostanie zaniechane (oryg. aborted), na skutek nie obsłużenia wyjątku IndexOutOfBoundsException.
Aby tego uniknąć, program można przekształcić do postaci
public
class Greet {
public static void main(String args[])
{
try {
System.out.println("Hello " +
args[0] + " " + args[1]);
}
catch (IndexOutOfBoundsException Any) {
System.err.println("This program requires 2 arguments");
System.err.println("You supplied only " + args.length);
}
}
}
Obecnie próba odwołania się do nieistniejącego elementu tablicy zakończy się wyprowadzeniem szczegółowego komunikatu.
Kopiowanie tablic
Kopiowanie tablic można realizować w zwykły sposób, to jest przez napisanie odpowiedniej pętli. Jednak znacznie wygodniej jest użyć funkcji arraycopy zadeklarowanej w klasie System.
public static
void arraycopy(Object srcArr, srcPos,
Object trgArr, trgPos, int length)
Wykonanie funkcji arraycopy może dotyczyć tablic o elementach dowolnego typu. Jej argumentami są kolejno: odnośnik do tablicy źródłowej, indeks tablicy źródłowej, odnośnik do tablicy docelowej, indeks tablicy docelowej, liczba kopiowanych elementów (najwyższego poziomu!) tablicy.
Uwaga: W przypadku tablic wielowymiarowych kopiuje się tylko elementy najwyższego poziomu tablicy. Po wykonaniu kopiowania elementy niższych poziomów będą wspólne dla oryginału i kopii.
public
class Master {
public static void main(String args[])
{
int arr[][] = { { 1, 2 }, { 3, 4 }, { 5, 6 } },
vec[][] = { { 7, 7 }, { 7, 7 }, { 8, 9 } };
System.arraycopy(arr, 0, vec, 0, 2);
for(int i = 0; i < arr.length ; i++)
for(int j = 0; j < arr[i].length ; j++)
System.out.print(vec[i][j] + " ");
System.out.println("\n" + (vec[0] == arr[0]);
}
}
Wykonanie programu powoduje wyprowadzenie napisu
1 2 3 4 8 9
true
Stanowi to potwierdzenie, że kopiowanie dotyczy tylko najwyższego poziomu tablicy (w przeciwnym razie vec[0] nie byłoby równe arr[0]);
Uwaga: W wypadku niewłaściwie dobranych argumentów, podczas wykonania funkcji arraycopy może być wysłany wyjątek klasy IndexOutOfBoundsException albo ArrayStoreException.
Klonowanie tablic
Klonowanie tablic odbywa się za pomocą metody clone. Klonowanie tablicy dwu- i więcej wymiarowej jest płytkie, co oznacza, że jest kopiowany tylko wektor najwyższego poziomu. Natomiast po wykonaniu klonowania podtablice są wspólne dla oryginału i klonu.
public
class Master {
public static void main(String args[])
throws CloneNotSupportedException
{
{
int src[] = { 1, 2 };
int trg[] = (int [])src.clone();
System.out.println(
(src == trg) +
" " +
trg[0]
);
}
{
int src[][] = { { 1, 2 }, { 3, 4} };
int trg[][] = (int [][])src.clone();
System.out.println(
(src == trg) +
" " +
(src[0] == trg[0]) +
" " +
trg[0][0]
);
}
}
}
Wykonanie programu powoduje wyprowadzenie napisu
false 1
false true 1
Przetwarzanie tablic
Za przykład przetwarzania tablic niech posłuży sortowanie elementów w kolejności rosnącej.
public
class Sort {
public static void main(String args[])
{
String names[] = { "Jan", "Ewa", "Iza" };
show(names);
sort(names);
show(names);
}
static void show(String arr[])
{
int length = arr.length;
for(int i = 0; i < length ; i++)
System.out.print(arr[i] + " ");
System.out.println();
}
static void sort(String arr[])
{
int length = arr.length;
boolean sorted = false;
while(!sorted) {
sorted = true;
for(int i = 0; i < length-1 ; i++)
if(arr[i].compareTo(arr[i+1]) > 0) {
String tmp = arr[i];
arr[i] = arr[i+1];
arr[i+1] = tmp;
sorted = false;
}
}
}
}
Wykonanie programu powoduje wyprowadzenie napisu
Jan Ewa Iza
Ewa Iza Jan
_________________________________________________________________________________________
Procedury
Procedurami w Javie są konstruktory, funkcje i metody. W obrębie każdej klasy procedury mogą być przeciążone (oryg. overloaded), ale w takim wypadku muszą się różnić sygnaturami.
Zasady definiowania i wywoływania procedur w Javie są zbliżone do C++. Jeśli procedura jest przeciążona, to wywołanie rozstrzyga się na rzecz tego jej aspektu, do którego najlepiej pasuje dane wywołanie.
Wymaga się aby taki aspekt istniał oraz aby był tylko jeden (bo może się zdarzyć, że wywołanie pasuje do kilku aspektów, ale do żadnego z nich nie pasuje lepiej niż do pozostałych).
Uwaga: Uznaje się m.in., że argument klasy String lepiej pasuje do parametru typu String niż do parametru klasy Object, która jest nadklasą klasy String.
Na przykład
class Primary {
void subProg(String par)
{
// ...
}
}
class Derived extends Primary {
void subProg(Panel par)
{
// ...
}
void subProg(Object par)
{
// ...
}
void call(Applet applet)
{
subProg(applet);
}
}
W klasie Derived występują 2 (sic!) przeciążone metody subProg. Do wywołania
subProg(applet)
pasuje zarówno metoda z parametrem klasy Object, jak i metoda z parametrem klasy Panel, ale lepiej pasuje druga z nich, więc właśnie ta zostanie wywołana.
Gdyby do klasy Primary dodano definicję
void subProg(Applet par)
{
// ...
}
to program stałby się błędny, ponieważ wywołanie procedury subProg byłoby wówczas dwuznaczne (oryg. ambiguous) .
Uwaga: W C++ procedura subProg klasy Primary zostałaby wówczas uznana za przesłoniętą przez procedury klasy Derived i powstałej sytuacji nie uznano by za błędną.
Konstruktory
W konstruktorach Javy nie ma list inicjacyjnych, zrezygnowano z argumentów domyślnych, parametru wielokropek (oryg. ellipsis), statycznych zmiennych lokalnych oraz z funkcji globalnych.
Uwaga: W odróżnieniu od metody, w definicji konstruktora nie może wystąpić żaden z następujących specyfikatorów
abstract static final native synchronized
Istotna różnica między konstruktorami Javy i C++ polega jedynie na tym, że jeśli z konstruktora podklasy zostanie wywołany konstruktor nadklasy, a nim pewna metoda przedefiniowana w podklasie, to w Javie zostanie wywołana metoda klasy obiektu kompletnego, a nie metoda podklasy.
class Person {
private String name;
Person(String name)
{
this.name = name;
Who();
}
void Who()
{
System.out.println("I am a Person");
}
}
public
class Woman extends Person {
Woman(String name)
{
super(name);
}
void Who()
{
System.out.println("I am a Woman");
}
public static void main(String args[])
{
new Woman("Mary Smith");
}
}
Wykonanie podanej aplikacji powoduje wyprowadzenie napisu
I am a Woman
natomiast w analogicznym programie w C++ nastąpiłoby wyprowadzenie napisu
I am a Person
Wywołanie konstruktora
Wywołanie konstruktora w Javie może być tylko jawne, podczas wykonywania operacji new. W Javie nie istnieją inicjatory konstruktorowe, ani konwersje konstruktorowe.
Circle circleOne(10, 10, 40); // błąd (użyj new)
Circle circleTwo = Circle(10, 10, 40); // błąd (użyj new)
Funkcje
Funkcjami są statyczne składowe klasy. W ciele funkcji nie istnieje odnośnik this, a zatem w ciele funkcji nie można odwoływać się do niestatycznych pól klasy wprost przez identyfikator, tak jak można to czynić w ciele metody.
public static String[] List(Vector Vec)
{
// ...
}
Procedura List jest funkcją. Jej rezultatem jest odnośnik do wektora odnośników do obiektów klasy String.
Wywołanie funkcji
Wywołanie funkcji nie wymaga użycia odnośnika do obiektu i najczęściej odbywa się poprzez nazwę klasy. Jeśli odbywa się przez wyrażenie odnośnikowe, to w odróżnieniu od C++, są podczas jego opracowywania realizowane wszystkie skutki uboczne (oryg. side effects).
class Point {
// ...
Point tricky()
{
System.out.println("Hello from tricky!");
return new Point(0, 0);
}
static void fun()
{
// ...
}
void usage()
{
Point point = new Point(10, 10);
Point.fun(); // typowe wywołanie funkcji
point.fun(); // wywołanie poprzez odnośnik
tricky().fun(); // wywołanie poprzez wyrażenie
}
}
Wywołanie funkcji fun poprzez wyrażenie odnośnikowe tricky() powoduje zrealizowanie skutku ubocznego, którym jest wyprowadzenie napisu
Hello from tricky!
Uwaga: Wiele przydatnych funkcji zadeklarowano w klasach Math i System. W szczególności, w celu wyznaczenia pierwiastka kwadratowego należy użyć wywołania Math.sqrt(Arg). W języku C++ analogiczne wywołanie ma postać Math::sqrt(Arg).
Metody
Metodami są niestatyczne składowe klasy. W ciele metody istnieje odnośnik this, któremu w chwili wywołania metody jest przypisywane odniesienie do obiektu, na rzecz którego odbywa się wywołanie. Za pomocą tego odnośnika można z ciała metody odwoływać się do tych pól obiektu, które są w niej dostępne.
Identycznie jak w C++, jeśli w pewnym punkcie ciała metody jest widoczny (a więc nie-przesłonięty), identyfikator Id pola albo procedury, to nazwa this.Id może być uproszczona do Id.
public
class Complex {
Complex(float parRe, float im)
{
re = parRe; // this.re = parRe;
this.im = im;
}
private float re, im;
// ...
}
Ponieważ w punkcie przed pierwszym operatorem przypisania jest widoczna (sic!) deklaracja pola re, więc odwołanie do niego nie wymaga użycia odnośnika this.
Ponieważ w punkcie przed drugim operatorem przypisania nie jest widoczna deklaracja pola im (gdyż jest przesłonięta przez deklarację parametru), więc odwołanie do pola im wymaga użycia odnośnika this.
Wirtualnośc i polimorfizm
Każda metoda Javy jest domyślnie wirtualna, a każde wywołanie metody jest domyślnie polimorficzne.
Wywołanie polimorficzne polega na tym, że w miejscu wystąpienia wyrażenia
Odnośnik.Metoda(Arg, Arg, Arg)
nie jest wywoływana Metoda widoczna w klasie Odnośnika, ale jest wywoływana przedefiniowująca ją Metoda widoczna w klasie obiektu identyfikowanego przez odniesienie przypisane Odnośnikowi.
Jeśli Metody widocznej w klasie Odnośnika nie przedefiniowano, albo gdy metoda ta jest prywatna, to jest wywoływana właśnie ona. Wywołanie takie nie jest wówczas polimorficzne.
class Primary {
Point makePoint(int x, int y)
{
// ...
}
static void Fun(Primary ref)
{
ref.makePoint(10, 10);
// ...
}
}
class Derived extends Primary {
Point makePoint(int x, int y)
{
// ...
}
}
W instrukcji
ref.makePoint(10, 10);
zostanie wywołana metoda makePoint klasy Derived (a nie metoda makePoint klasy Primary).
Gdyby metoda makePoint klasy Primary była prywatna, to wywołanie nie byłoby polimorficzne i zostałaby wywołana metoda klasy Primary (a nie metoda klasy Derived).
Podprogramy
Podprogramem jest procedura rodzima (oryg. native) wyrażona w dowolnym języku programowania, na przykład w C++.
Deklaracja procedury rodzimej zawiera specyfikator native, a sam kod procedury jest dostarczany w postaci biblioteki dzielonej (oryg. shared).
Uwaga: W środowisku Windows bibliotekami dzielonymi są biblioteki DLL (oryg. Dynamic Link Library).
class SomeClass {
// ...
public native int printString(String string);
}
Metoda printString jest procedurą rodzimą. Jest zapewne napisana w innym języku niż Java.
Przygotowanie do wykonania programu zawierającego procedury rodzimej wymaga wykonania następujących czynności
zadeklarowania procedury
skompilowania klasy
wygenerowania nagłówka
utworzenia pnia
zakodowania procedury
utworzenia biblioteki
Następujący przykład dokładniej wyjaśni zestaw omówionych czynności.
Zadeklarowanie procedury
Zadeklarowanie procedury rodzimej polega na użyciu specyfikatora native.
class Greet {
public native void sayHello(); // deklaracja
static {
System.loadLibrary("Hello.dll"); // załadowanie
}
}
public class Master {
public static void main(String args[])
{
new Greet().sayHello(); // wywołanie
}
}
Skompilowanie klasy
Kompilacja klasy odbywa się za pomocą kompilatora javac
javac Greet.java
Po zakończeniu kompilacji powstanie plik Greet.class.
Wygenerowanie nagłówka
Wygenerowanie nagłówka odbywa się za pomocą generatora javah
javah Greet.java
Po zakończeniu generacji powstanie plik Greet.h.
Utworzenie pnia
Utworzenie pnia (oryg. stub) odbywa się za pomocą generatora wywołanego z argumentem -stubs
javah - stubs
Po zakończeniu generacji powstanie plik Greet.c.
Zakodowanie procedury
Zakodowanie procedury rodzimej odbywa się w dowolnie wybranym języku programowania (na przykład w C++).
#include <StubPreamble.h>
#include "Greet.h>
#include <iostream.h>
void Greet_sayHello(struct struct Hgreet *this)
{
cout << "Hello" << endl;
}
Utworzenie biblioteki
Utworzenie biblioteki odbywa się za pomocą kompilatora użytego języka, na przykład Visual C++.
Po zakończeniu kompilacji, na przykład w trybie MFCAppWizard (dll) powstanie plik Greet.dll.
Rekurencja
Wykonanie funkcji i metod może być w Javie rekurencyjne (oryg. recursive) i wielobieżne (oryg. reentrant).
Wielobieżność zawdzięcza Java przede wszystkim temu, iż usunięto z niej statyczne zmienne lokalne procedur. Ale nawet po tej zmianie, w warunkach wielobieżnego wykonania tej samej procedury przez dwa lub więcej wątków, musi być zapewniona odpowiednia ich synchronizacja.
Niech za przykład procedury, która może być wykonywana wielobieżnie, bez naruszenia integralności jej argumentu (oryg. integrity), posłuży następujący program, którego funkcja showAll wyprowadza ciąg znaków ujawniający strukturę dostarczonej mu tablicy o elementach podstawowych klasy String.
class Array {
synchronized static void showAll(Object obj)
throws IllegalArgumentException
{
String className = obj.getClass().getName();
if(className.charAt(0) != '[') // czy obj jest tablicą
throw new IllegalArgumentException();
int length = ((Object [])obj).length;
Object arr[] = new Object[length];
System.arraycopy(obj, 0, arr, 0, length);
boolean lastCall = arr[0] instanceof String;
System.out.print("{ ");
for(int i = 0; i < length ; i++)
if(arr[i] == null)
System.out.print("null");
else {
if(lastCall)
System.out.print("\"" + arr[i] + "\"");
else
showAll(arr[i]); // rekurencja
if(i < length-1)
System.out.print(", ");
}
System.out.print(" }");
}
}
public
class Master {
public static void main(String args[])
{
String arr[][][] = {
{ { "Hello" } , { " " } } ,
{ { "World" } }
};
try {
Array.showAll(arr);
System.out.println();
}
catch(IllegalArgumentException e) {
System.exit(1);
}
}
}
Funkcję showAll napisano w taki sposób, że na przykład w zasięgu deklaracji
String arr[][][] = {
{ { "Hello" }, { "*" } },
{ { "World" }, }
};
wykonanie instrukcji
Array.showAll(arr);
powoduje wyprowadzenie napisu
{ { { "Hello" }, { "*" } }, { { "World" } } }
_________________________________________________________________________________________
Wyrażenia
Zasady opracowywania wyrażeń są w Javie określone precyzyjnie i jednoznacznie.
W odróżnieniu od C++, gdzie kolejność opracowania argumentów operacji dwuargumentowej oraz kolejność opracowania argumentów wywołania funkcji zależy od implementacji, w Javie przyjęto zasadę opracowywania ściśle od-lewej-do-prawej.
W szczególności, wykonanie instrukcji
int fix = 1;
fix += fix + (fix = 2);
String str = 1 + 2 + "x";
powoduje przypisanie zmiennej fix danej o wartości 4, a zmiennej str danej o wartości "3x".
Natomiast wykonanie instrukcji
int fix = 1;
fix += (fix = 2) + fix;
String str = 1 + (2 + "x");
powoduje przypisanie zmiennej fix danej o wartości 5, a zmiennej str danej o wartości "12x".
Priorytety i wiązania
Identycznie jak w C++, są w Javie respektowane nawiasy oraz priorytety i wiązania operatorów. W Javie nie występuje natomiast operator zakresu (::) i połączenia (,), a konsekwencji pozbycia się wskaźników, także operator wskazania (->) i wskazania pola (->*).
Kropka i nawiasy są odrębnymi konstrukcjami składniowymi, które (wbrew temu co czyta się tu i ówdzie) nie są operatorami.
Dzięki temu, takie na przykład wyrażenie jak
new String("Hello").length()
jest poprawne, mimo iż rozpatrywane przy założeniu, że kropka i nawiasy są operatorami byłoby niepoprawnym wyrażeniem
new ((String("Hello")).(length()))
Uwaga: Pełen wykaz operatorów Javy, z wyszczególnieniem ich wiązań i priorytetów zamieszczono w Dodatku Priorytety operatorów.
Nowe operatory
W celu uproszczenia zapisu przesunięć bez-znaku-w-prawo, realizowanych w C++ na przykład za pomocą takich wyrażeń jak
(unsigned int) var >> 2
wprowadzono w Javie operator >>>, dzięki czemu przytoczone wyrażenie upraszcza się do
var >>> 2
(oczywiście istnieje także operator >>>=).
l-wyrażenia
W porównaniu z C++ obowiązują w Javie nieco zmodyfikowane zasady określające co jest, a co nie jest l-wyrażeniem (oryg. l-value).
Nie ma to większego praktycznego znaczenia, ale warto na przykład wiedzieć, że w Javie nie jest l-wyrażeniem przypisanie (np. =), zwiększenie (np. ++) i zmniejszenie (np. --).
Z tego powodu, nie jest więc w Javie poprawne wyrażenie
++(fix = 1) += 2 // w C++ równoważne fix = 4
(z czego można się tylko cieszyć!).
Konwersje
Podobnie jak w C++, są w Javie dozwolone promocje, konwersje arytmetyczne oraz konwersje obiektowe.
Promocje
Promocja polega na niejawnym przekształceniu argumentu operacji arytmetycznej
z typu "byte", "short", "char" do typu "int",
z typu "float" do typu "double"
z typu "long" do typu "float" oraz "double"
Na przykład, następujący fragment programu w C++
char chr = 2;
int fix = 4.8 + ~chr; // 1
(w którym ~chr ma wartość -3 typu "int"), równoważny
char chr = (char)2;
int fix = int(4.8 + double(~int(chr)));
przybiera w Javie postać
char chr = 2;
int fix = (int)(4.8 + ~chr);
równoważną
char chr = (char)2;
int fix = (int)(4.8 + (double)~(int)chr);
Uwaga: Dzięki promocji występującej w operacjach dwuargumentowych, wykonanie na przykład instrukcji
byte a = 128, b = 2, c = a * b / 4;
równoważnej
byte a = 128, b = 2, c = (int)a * (int)b / 4;
powoduje przypisanie zmiennej c wartości 64, a nie 0.
Konwersje arytmetyczne
Jawne i niejawne konwersje arytmetyczne są wykonywane zgodnie z zasadą zachowania wartości. Jeśli nie zawsze jest to możliwe, stosuje się obcinanie bitów bardziej znaczących (dla typów całkowitych) albo zaokrąglanie (oryg. rounding) (dla typów rzeczywistych).
Konwersje zawężające (oryg. narrowing) wymagają jawnego użycia operatora konwersji (oryg. cast), chyba że operacja dotyczy wyrażenia stałego typu "int" przypisywanego zmiennej typu "byte", "short", albo "char", ale i to tylko wówczas, gdy przypisanie zapewnia zachowanie wartości.
Uwaga: Wyjątek ten nie dotyczy skojarzenia parametru i argumentu procedury.
Na przykład
int ten = 10;
short small = ten; // błąd (brak konwersji)
short small = (short)ten; // dobrze (jawna konwersja)
short small = 10; // dobrze (wyjątek!)
void sub(short par)
{
//...
sub(10); // błąd (brak konwersji)
}
Konwersje obiektowe
Zawężające konwersje obiektowe (z klasy do podklasy), muszą być wyrażane jawnie. W odróżnieniu od C++, jest przeprowadzana dynamiczna kontrola poprawności takiej operacji.
Uwaga: Jeśli odnośnik do nadklasy, który nie identyfikuje obiektu podklasy jest poddawany konwersji na odnośnik do podklasy, to podczas wykonywania programu w Javie jest wysyłany wyjątek klasy ClassCastException (w C++ taki błąd nie jest wykrywany).
Na przykład
class Person {
String name;
byte age;
// ...
boolean isMarried(Person person)
{
return ((Woman)person).married;
}
}
final
class Woman extends Person {
boolean married;
// ...
}
Jeśli w zasięgu podanych definicji zostanie wywołana funkcja isMarried, to wykona się poprawnie tylko wówczas, gdy jej argument identyfikuje obiekt klasy Woman.
Taka sytuacja zaistnieje na przykład w wywołaniu
isMarried(new Woman())
ale nie zaistnieje w wywołaniu
isMarried(new Person())
Dlatego, w celu zachowania kontroli nad przebiegiem wykonania funkcji isMarried, należałoby obsłużyć sytuację wyjątkową klasy ClassCastException
boolean isMarried(Person person)
{
try {
return ((Woman)person).married;
}
catch(ClassCastException e) {
System.err.println(name + " Conversion error");
return false;
}
}
Konwersje łańcuchowe
Konwersje łańcuchowe są traktowane w sposób specjalny. W odróżnieniu od pozostałych konwersji, które muszą być wykonywane jawnie, konwersje łańcuchowe są wykonywane niejawnie, w każdym miejscu użycia operatora + (plus), którego jeden z argumentów jest typu String albo StringBuffer.
W takim wypadku niełańcuchowy argument operacji jest przetwarzany na łańcuch za pomocą metody valueOf (dla typów podstawowych), albo za pomocą metod toString (dla typów obiektowych).
W szczególności, podczas wykonywania instrukcji
System.out.println("" + 12.4 + new Thread());
wyrażenie ujęte w nawiasy jest niejawnie przekształcane w wyrażenie
new StringBuffer().
append("").
append(12.4).
append(new Thread())
Konwersje tablicowe
Ponieważ każda tablica jest podobiektem klasy Object oraz implementuje interfejs Cloneable, więc w zasięgu definicji klasy kopertowej StringEnvelope
class StringEnvelope implements Cloneable {
private String string;
StringEnvelope(String string)
{
this.string = string;
}
public String toString()
{
return string;
}
}
wykonanie procedury
void okSub()
{
StringEnvelope languages[] = new StringEnvelope [3];
languages[0] = new StringEnvelope("Fortran");
languages[1] = new StringEnvelope("C++");
languages[2] = new StringEnvelope("Java");
Object objects[] = languages;
Cloneable clones[] = (Cloneable [])objects;
StringEnvelope theBest;
theBest = ((StringEnvelope [])(Object [])clones)[2];
System.out.println(theBest); // Java
}
powoduje wyprowadzenie napisu
Java
Natomiast na skutek tego iż klasa String nie implementuje interfejsu Cloneable, podczas wykonywania następującej (poprawnej statycznie!) metody zostanie wysłany wyjątek klasy ClassCastException.
void errSub()
{
String languages[] = { "Fortran", "C++", "Java" };
Object objects[] = languages;
Cloneable clones[] = (Cloneable [])objects; // błąd
// ...
}
Przypisania
Przed pierwszym odwołaniem się do wartości zmiennej lokalnej należy w sposób w sposób definitywny (oryg. definite) przypisać jej daną.
Definitywność przypisania polega na tym, że na dowolnej drodze wiodącej od początku procedury (konstruktora, funkcji, metody) do miejsca, w którym wystąpiło odwołanie do zmiennej należy ponad wszelką wątpliwość przypisać jej daną.
Na przykład, w metodzie
void Fun()
{
int fix;
int val = 1;
if(val > 0)
fix = 10;
System.out.println(fix+1); // błąd (brak definitywnego
// przypisania)
}
występuje błąd, ponieważ, bez wdawania się w analizę wyrażenia val > 0, może zaistnieć przypuszczenie, że w miejscu opracowania wyrażenia fix+10 występuje odwołanie do zmiennej, której na skutek pominięcia instrukcji fix = 10, nie przypisano danej.
_________________________________________________________________________________________
Instrukcje
Niemal wszystkie poprawne instrukcje C++ są poprawnymi instrukcjami Javy. Istotna zmiana dotyczy jedynie wyrażeń warunkowych zawartych w instrukcjach if, for, do i while oraz wyrażeń warunkowych zawartych w operacjach &&, || i ?:. W Javie wszystkie takie wyrażenia takie muszą być typu "boolean".
Z tego powodu, następujący fragment programu w C++
int fix = 3;
while(fix--)
cout << fix * fix;
przybiera w Javie postać
int fix = 3;
while(fix-- != 0)
System.out.println(fix * fix);
Instrukcja for
Mimo iż w Javie nie ma operatora połączenia (,), fraza inicjująca pętlę może zawierać deklarację zmiennych lokalnych albo listę przypisań, zwiększeń, zmniejszeń, wywołań procedur i wykonań operacji new. Ponadto, ostatnia fraza instrukcji for może zawierać listę wyrażeń.
W szczególności, są więc poprawne takie instrukcje for jak
for(int i = 1, j = 2 ; i < 10 ; i++, j++)
System.out.println(i + j);
oraz
int i, j;
for(i = 1, j = 2 ; i < 10 ; ++i)
System.out.println(i+ j);
Uwaga: Zmienne zadeklarowane we frazie inicjującej instrukcji for są w Javie lokalne względem zawierającej jej pętli. A więc, w odróżnieniu od C++, mogą być deklarowane w rozłącznych pętlach, na przykład
void Fun()
{
for(int i = 0 ; i < 1 ; i++);
for(int i = 1 ; i > 0 ; i--); // dobrze (nie ma kolizji!)
}
Instrukcje break i continue
W Javie nie ma instrukcji goto, ale składnia instrukcji break i continue została w rozszerzona o możliwość użycia etykiety.
Etykieta wymieniona w instrukcji break oraz continue musi poprzedzać dowolną zawierającą ją instrukcję strukturalną.
W takim wypadku
Wykonanie instrukcji break powoduje zakończenie wykonywania zawierającej ją instrukcji strukturalnej opatrzonej podaną etykietą.
Wykonanie instrukcji continue powoduje kontynuowanie wykonywania zawierającej ją instrukcji strukturalnej opatrzonej podaną etykietą.
String answerYesOrNo()
{
String YesNo[] = { "No", "Yes" };
String theAnswer;
int reply = -1;
repeat:
while(true) {
try {
reply = System.in.read() - '0';
}
catch(IOException e) {
break;
}
try {
theAnswer = YesNo[response];
break repeat;
}
catch(IndexOutOfBoundsException e) {
System.out.println("Press 1 or 0");
continue repeat; // tutaj zbędne
}
}
return reply < 0 ? null : theAnswer;
}
Procedura wprowadza kolejne znaki, aż do napotkania znaku 1 albo 0, co odpowiednio oznacza odpowiedź Yes albo No.
Instrukcja synchronized
W Javie występuje instrukcja synchronized, nie znana w C++.
Ma ona postać ogólną
synchronized ( Wyrażenie ) Blok
w której Wyrażenie jest nazwą odnośnika, a Blok jest blokiem (instrukcją grupującą).
Wykonanie instrukcji synchronized powoduje przydzielenie wątkowi podanego Bloku jako sekcji krytycznej (oryg. critical section), a po wykonaniu go na rzecz obiektu identyfikowanego przez Wyrażenie, zwolnienie sekcji.
Uwaga: Jeśli podczas wykonywania sekcji krytycznej na rzecz pewnego obiektu, jakikolwiek inny wątek podejmie próbę wykonania tej samej sekcji krytycznej na rzecz tego samego obiektu, to jego wykonanie zostanie zablokowane do chwili zwolnienia sekcji krytycznej przez wątek, któremu ją przydzielono.
Wyrażając to inaczej: jeśli monitor (C. Hoare: Communications of the ACM Vol. 21, No. 8, 1978) związany z obiektem identyfikowanym przez Wyrażenie nie jest zajęty przez inny wątek, to dany wątek zajmuje monitor i zwalnia go po zakończeniu wykonywania instrukcji Blok. W przeciwnym razie wątek jest blokowany aż do zwolnienia monitora.
W następującej funkcji, wykonanie sekcji krytycznej wyznaczonej przez instrukcję synchronized, może być w danej chwili, na rzecz tego samego obiektu point, realizowane tylko przez co najwyżej jeden wątek. Dzięki temu, wywołanie metody incPoint powoduje zwiększenie obu pól (x, y) o tę samą wartość.
class Master {
// ...
public Point incPoint(Point point)
{
synchronized(point) {
point.incXY();
return point;
}
}
}
class Point {
int x, int y;
incXY()
{
this.x = x + 1;
this.y = y + 1;
}
// ...
}
Gdyby zrezygnowano z użycia instrukcji synchronized, a metodę incPoint wywołano z identycznym argumentem z dwóch różnych wątków, to mogłoby się zdarzyć, że po takich operacjach współrzędne x i y punktu nie byłyby równe.
Uwaga: Ponieważ przydzielenie sekcji krytycznej jest zawsze związane z konkretnym obiektem, więc gdyby w rozpatrywanym przykładzie frazę
synchronized(point)
zastąpiono frazą
synchronized(this)
to nic nie stałoby na przeszkodzie, aby sterowanie dwóch różnych wątków jednocześnie znalazło się w tym samym bloku. Tak mogłoby się zdarzyć, gdyby metoda incPoint zostałaby w każdym z wątków wywołana na rzecz innego obiektu (identyfikowanego wówczas przez odrębny odnośnik this).
Instrukcja try
W ramach wprowadzonej w Javie rozbudowy instrukcji try, bezpośrednio po frazach catch, może wystąpić fraza
finally Blok
w której Blok jest blokiem.
Blok frazy finally jest wykonywany po każdym zakończeniu wykonywania instrukcji try i to nawet wówczas, gdy zaprzestanie jej wykonywania jest gwałtowne (oryg. abrubt).
void testFinally()
{
label:
while(true)
try {
break label;
}
catch(Exception exc) {
}
finally {
System.out.println("Here I was");
}
System.out.println("Here I am");
}
Wykonanie instrukcji break, zostanie poprzedzone zakończenie wykonywania instrukcji try. Tuż przed zakończeniem wykonywania instrukcji try zostanie wykonany blok frazy finally.
Wywołanie procedury testFinally spowoduje to wyprowadzenie napisu
Here I was
Here I am
_________________________________________________________________________________________
Wyjątki
Zasady posługiwania się wyjątkami są w Javie niemal takie same jak w C++.
Istnieje tylko jedna istotna różnica: występującą w C++ frazę
throw(Wyjątek, Wyjątek, ... , Wyjątek)
zastąpiono frazą
throws Wyjątek, Wyjątek, ... , Wyjątek
Uwaga: W Javie nie występuje instrukcja
throw;
która w C++ oznacza wysłanie-dalej właśnie odebranego wyjątku.
Wysyłanie wyjątków
Wyjątek jest obiektem wysyłanym niejawnie przez System, albo jawnie za pomocą instrukcji throw. Jeśli wyjątek zostanie wysłany w pewnym punkcie wykonywania programu, to zostanie przechwycony przez pierwszą frazę catch, takiej najwęższej dynamicznie instrukcji try, której parametrowi można przypisać odniesienie do wyjątku.
W wypadku gdy fraza przechwytująca nie istnieje, wywołuje się metodę uncaughtException tej grupy wątków (oryg. thread group) do której należy właśnie wykonywany wątek.
class TestException {
public static void main(String args[])
{
try {
int num = Integer.parseInt(arg[0]),
den = Integer.parseInt(arg[1]);
divide(num, den);
}
catch(IndexOutOfBoundsException e) {
System.out.println("Wrong index");
}
catch(Exception e) {
System.out.println("Sorry" + e.getMessage());
}
}
static void divide(int Up, int Dn)
{
try {
Integer Result = new Integer(Up / Dn);
}
catch(OutOfMemoryError e) {
System.out.println("Out of memory");
}
}
}
Jeśli podany program zostanie wywołany z argumentami 1 i 0, to w punkcie wykonania operacji dzielenia powstanie sytuacja wyjątkowa ArithmeticException (spowodowana dzieleniem przez 0).
Podany punkt frazy jest dynamicznie zawarty w instrukcji try funkcji divide, a ta instrukcja jest dynamicznie zawarta w instrukcji try funkcji main.
Ponieważ w funkcji divide nie przewidziano obsługi wyjątku ArithmeticException (klasa ArithmeticException nie jest nadklasą klasy OutOfMemoryException), więc obsłużenie zaistniałej sytuacji wyjątkowej zostanie przeniesione do instrukcji try funkcji main.
Ponieważ klasa ArithmeticException nie jest podklasą klasy IndexOutOfBounds, ale jest podklasą klasy Exception, więc wyjątek zostanie przechwycony i obsłużony przez drugą z fraz catch.
Ponieważ z wygenerowanym przez system wyjątkiem klasy ArithmeticException jest związany komunikat
/ by zero
więc wykonanie programu spowoduje wyprowadzenie napisu
Sorry: / by zero
Wyjątki predefiniowane
Każdy wyjątek musi być obiektem klasy Throwable albo jej podklasy. Wymaganie to spełniają wyjątki predefiniowanych klas Error i Exception oraz wyjątki ich predefiniowanych podklas.
Klasa Error
Wyjątki klasy Error są związane z sytuacjami poważnymi, których obsłużenie niewiele daje, a nawet może być niemożliwe.
Object -> Throwable -> Error
VirtualMachineError (błąd maszyny wirtualnej), w tym
InternalError (błąd wewnętrzny)
OutOfMemoryError (brak pamięci operacyjnej)
StackOverflowError (przepełnienie stosu)
UnknownError (nieznany błąd)
LinkageError (błąd dołączenia klasy), w tym
ClassCircularityError (odwołanie okrężne)
ClassFormatError (błąd reprezentacji)
IncompatibleClassChangeError (błąd niezgodności), w tym
AbstractMethodError (błąd metody abstrakcyjnej)
IllegalAccessError (błąd dostępu)
InstantiationError (błąd utworzenia egzemplarza)
NoSuchFieldError (brak pola)
NoSuchMethodError (brak metody)
NoClassDefFoundError (brak klasy)
UnsatisfiedLinkError (brak definicji)
VerifyError (błąd weryfikacji)
ThreadDeath (uśmiercenie wątku)
AWTError (błąd pakietu AWT)
Klasa Exception
Wśród wyjątków klasy Exception na uwagę zasługują wyjątki podklas klasy RuntimeException związane na ogół z sytuacjami błędnymi, których wykrycie podczas kompilowania programu nie jest możliwe.
Object -> Throwable -> Exception
RuntimeException (błąd statycznie niewykrywalny)
ClassNotFoundException (nie znaleziono klasy)
ClassNotSupportedException (nie ma takiej klasy)
IllegalAccessException (błąd dostępu)
InstantiationException (błąd utworzenia egzemplarza)
InterruptedException (przerwanie nieaktywnego wątku)
NoSuchMethodException (brak metody)
AWTException (wyjątek pakietu AWT)
IOException (wyjątek wejścia-wyjścia), w tym
EOFException (koniec pliku)
FileNotFoundException (brak pliku)
InterruptedIOException (przerwanie operacji przesłania)
UTFDataFormatException (błąd kodu UTF-8)
MalformedURLException (błąd lokalizatora)
ProtocolException (błąd protokołu komunikacyjnego)
SocketException (błąd gniazda)
UnknownHostException (nieznany komputer)
UnknownServiceException (nieznana usługa)
Klasa RuntimeError
Klasa RuntimeError i jej podklasy są związane z sytuacjami, które najwygodniej jest obsługiwać dynamicznie.
W szczególności, następującą metodę, zaprogramowaną w sposób klasyczny, to jest bez obsługiwania wyjątków
int getChar(String array[], int index, int pos)
{
int length = array.length;
if(index >= 0 && index < length) {
String string = array[index];
length = string.length();
if(pos >= 0 && pos < length)
return string.charAt(pos);
}
System.err.println("IndexOutOfBounds error");
return -1;
}
zaleca się zapisać w postaci
char getChar(String array[], int index, int pos)
{
try {
return array[index].charAt(pos);
}
catch(IndexOutOfBoundsException e) {
System.err.println("IndexOutOfBounds error");
return -1;
}
}
RuntimeException
Empty StackException (pusty stos)
NoSuchElementException (nie ma takiego elementu)
ArithmeticException (dzielenie całkowite przez 0)
ArrayStoreException (przypisanie niezgodnego odniesienia)
ClassCastException (niedopuszczalny typ docelowy)
IllegalArgumentException (błędny argument), w tym
IllegalThreadStateException (błąd stanu wątku)
NumberFormatException (błąd zapisu liczby)
IllegalMonitorStateException (monitor w złym stanie)
IndexOutOfBoundsException (indeks poza zakresem)
NegativeArraySizeException (ujemny rozmiar tablicy)
NullPointerException (odwołanie przez puste odniesienie)
SecurityException (błąd zabezpieczenia)
Weryfikowanie wyjątków
Wyjątki dzielą się na nieweryfikowane i weryfikowane. Wyjątkami nieweryfikowalnymi są wyjątki klas RuntimeException i Error. Wszystkie pozostałe wyjątki są weryfikowane.
Weryfikacja polega na sprawdzeniu czy procedury programu (konstruktory, funkcje i metody) wyposażono w odpowiednio dobrane frazy throws.
Zasady wyposażania procedur we frazy throws są następujące:
Jeśli z ciała procedury może być wysłany wyjątek, to w jej nagłówku musi wystąpić fraza throws wyszczególniająca klasę tego wyjątku albo dowolną jej nadklasę.
Jeśli w pewnym punkcie programu jest wywoływana procedura z wyszczególnioną w jej nagłówku klasą wyjątku, to procedura wywołująca musi również wyszczególnić tę klasę (albo jej nadklasę!), chyba że rozpatrywane wywołanie zamknięto w bloku instrukcji try obsługującej ten wyjątek.
Ponieważ trudno jest zapamiętać nazwy klas wyjątków jakie mogą być wysłane przez poszczególne procedury, wystarczy program poddać kompilacji, aby dowiedzieć się, o jakich wyszczególnieniach zapomniano, a następnie program odpowiednio uzupełnić.
Uwaga: Zabrania się, aby metoda przedefiniowująca metodę nadklasy deklarowała wysłanie wyjątku, który nie jest wysyłany przez metodę przedefiniowywaną, chyba że jest on wyjątkiem nadklasy wyjątku wysyłanego przez tę metodę.
public
class ThrowsTest {
public static void main(String args[])
throws OutOfMemoryError
{
throwIt(false);
System.out.println("Hello ");
byte[] world = { 'W', 'o', 'r', 'l', 'd' };
System.out.write(world, 0, 5); // błąd (IOException)
}
static void throwIt(boolean cheat)
throws OutOfMemoryError
{
if(cheat)
throw new OutOfMemoryError();
}
}
Ponieważ wywołanie funkcji throwIt może spowodować wysłanie wyjątku klasy OutOfMemoryError, więc w funkcji main wyszczególniono ten wyjątek.
Gdyby tego nie uczyniono, to wywołanie funkcji throwIt należałoby ująć w blok instrukcji try obsługującej wyjątki klasy OutOfMemoryError albo jej nadklasy (tj. klasy Throwable, Error, VirtualMachineError).
try {
throwIt(false);
}
catch(OutOfMemoryError e) {
}
Ponieważ z nagłówka metody System.out.println
public synchronized void println(String s)
wynika, że nie wysyła ona wyjątków (brak frazy throws), więc jej użycie nie ma wpływu na obsługę wyjątków.
Natomiast, ponieważ z nagłówka metody System.out.write
public void write(byte[] b, int off, int len)
throws IOException
wynika, że wysyła ona wyjątek IOException, więc w miejscu jej wywołania występuje błąd, bo we frazie throws funkcji main nie użyto nazwy klasy IOException, ani nazwy żadnej jej nadklasy, a wywołanie
System.out.write(world, 0, 5);
nie jest objęte blokiem instrukcji try obsługującej wyjątki klasy IOException.
Definiowanie klas wyjątków
Własne klasy do tworzenia wyjątków mogą być bezpośrednimi podklasami klasy Throwable, albo mogą być włączone do hierarchii klas predefiniowanych, najczęściej jako podklasy klasy Exception.
W celu zilustrowania zasad definiowania klas wyjątków, pokazano istotne fragmenty definicji klas Throwable i Exception (zaczerpnięte z pakietu java.lang) oraz przykład własnej klasy WrongNumber.
public
class Throwable {
private String detailMessage;
public Throwable()
{
}
public Throwable(String message)
{
detailMessage = message;
}
public String getMessage()
{
return detailMessage;
}
public String toString()
{
String s = getClass().getName();
String message = getMessage();
return (message != null) ? (s + ": " + message) : s;
}
}
public
class Exception extends Throwable {
public Exception()
{
}
public Exception(String s)
{
super(s);
}
}
class WrongNumberException extends Exception {
private int number;
public WrongNumberException(int number)
{
super("WrongNumber");
this.number = number;
}
public String toString()
{
return "WrongNumber(" + number + ")";
}
}
Tak zdefiniowana klasa wyjątku mogłaby być użyta na przykład w następujący sposób
static void printNumber(int number)
throws WrongNumberException
{
if(number < 0)
throw new WrongNumber(number);
System.out.println(number);
}
void testWrongNumber()
{
try {
printNumber(-13);
}
catch(WrongNumberException e) {
System.out.println("" + e); // WrongNumber(-13)
}
catch(Exception e) {
System.out.println(e.getMessage());
}
}
Na skutek wywołania funkcji printNumber z ujemnym argumentem, podczas wykonywania frazy catch zostanie wyprowadzony napis
WrongNumber(-13)
_________________________________________________________________________________________
Wątki
Wątkiem jest sekwencyjny przepływ sterowania (oryg. flow of control) w ramach zadania (oryg. task) realizującego program.
Program uznaje się za napisany wielowątkowo (oryg. multithreaded), jeśli podczas wykonania go na wieloprocesorowej Maszynie Wirtualnej można stwierdzić, że są takie przedziały czasu, kiedy w co najmniej dwóch procesorach występuje współbieżny (oryg. concurrent) przepływ sterowania przez wątki programu.
Na komputerach jednoprocesorowych, współbieżność wątków jest tylko emulowana, a w każdej chwili przepływ sterowania dotyczy tylko jednego wątku. Ale nawet wówczas, program należy napisać w taki sposób, jakby miał być wykonany na maszynie wieloprocesorowej.
Tylko przy takim podejściu do programowania wielowątkowego można poprawnie rozwiązać problemy związane z komunikowaniem się i synchronizowaniem wątków.
Uwaga: W chwili obecnej, spośród trzech najpopularniejszych systemów operacyjnych Internetu: Windows 95, Solaris i MacOS, tylko pierwszy z nich poprawnie emuluje wielowątkowość wywłaszczeniową (oryg. preemptive), nie dopuszczając do zagłodzenia (oryg. starving) wątków o niskich priorytetach.
Stany wątków
Każdy wątek wymaga istnienia kontrolującego go obiektu klasy Thread.
Wykonanie na rzecz obiektu kontrolującego metody start powoduje utworzenie wątku, a wykonanie metody stop jego zniszczenie. W okresie między utworzeniem i zniszczeniem wątek istnieje (oryg. is alive).
O przebiegu wykonania wątku decyduje metoda run wywołana niejawnie przez System tuż po utworzeniu wątku. Metoda run należy do klasy implementującej interfejs Throwable, określonej przez argument konstruktora użytego do utworzenia obiektu kontrolującego wątek. Zakończenie wykonywania metody run powoduje niejawne wywołanie metody stop, a więc zniszczenie wątku.
Istniejący wątek jest wykonywany (oryg. runs), uśpiony (oryg. sleeps), zawieszony (oryg. suspended) albo wstrzymany (oryg. waits).
public synchronized void start()
Wywołanie metody start powoduje utworzenie wątku.
public static void sleep(long millis)
Wywołanie metody sleep powoduje uśpienie wątku na podany okres czasu.
public final void suspend()
Wywołanie metody suspend powoduje zawieszenie wątku.
public final void resume()
Wywołanie metody resume powoduje odwieszenie wątku.
public final void wait()
public final void wait(long timeout)
Wywołanie metody wait powoduje wstrzymanie wykonywania wątku.
public final void notify()
Wywołanie metody notify powoduje uwolnienie jednego wstrzymanego wątku.
public static void yield()
Wywołanie metody yield powoduje dobrowolne podzielenie się przez wątek dostępem do procesora. Po wywołaniu metody yield podejmie się wykonywanie innego wątku (o ile taki istnieje). Nie wyklucza to jednak zagłodzenia pewnego wątku jeśli jest ich ogółem więcej niż dwa.
public final synchronized void join()
public final synchronized void join(long millis)
Wywołanie metody join powoduje powstrzymanie wykonywania wątku aż do zniszczenia go przez inny wątek.
public static final void stop()
Wywołanie metody stop powoduje zniszczenie wątku.
public final boolean isAlive()
Wywołanie metody isAlive umożliwia stwierdzenie czy wątek istnieje, to jest czy na rzecz obiektu kontrolującego wątek wywołano już metodę start, ale nie wywołano jeszcze metody stop.
Priorytety
Z każdym wątkiem jest związany priorytet (oryg. priority), określający jego rangę (oryg. rank) wśród innych wątków.
Priorytet jest liczbą z przedziału 1..10. Jeśli nie poda się go jawnie, to nowy wątek przejmie priorytet wątku który go utworzył.
Jako zasadę przyjmuje się, że wątek o wyższym priorytecie uzyskuje większy przydział procesora niż wątek o niższym priorytecie. Nie oznacza to jednak, że jeśli są wykonywane wątki o różnych priorytetach, to ten o najwyższym priorytecie zmonopolizuje procesor (chociaż może się tak stać!).
Uwaga: Poleganie na priorytetach wątków nie daje gwarancji co do kolejności ich wykonania i dlatego musi być uznane za przejaw nieprzenośnego (oryg. non-portable) stylu sterowania wykonaniem wątków.
Wątek główny
W chwili rozpoczęcia wykonywania programu istnieje tylko wątek główny (oryg. main thread), należący do grupy wątków main. Każdy wątek może być uczyniony demonem (oryg. daemon).
Wykonywanie programu kończy się bezpośrednio po tym, gdy zakończy się wykonywanie ostatniego wątku, który nie jest demonem.
Uwaga: Jeśli utworzono okna graficzne, to zakończenie wykonywania programu jest wstrzymywane do chwili ich zamknięcia.
public
class Master {
public static void main(String args[])
{
Thread thread = Thread.currentThread();
ThreadGroup threadGroup = thread.getThreadGroup();
System.out.println("Thread: " + thread + ", " +
"Group: " + threadGroup + "," +
" IsAlive: " + thread.isAlive());
}
}
Podany program napisano jednowątkowo. Jego wykonanie powoduje wyprowadzenie napisu
Thread[main,5,main],
Group: java.lang.ThreadGroup[name=main,maxpri=10],
IsAlive: true
Tworzenie wątków
W celu utworzenia wątku należy utworzyć kontrolujący go obiekt klasy Thread. Obiektowi temu należy przekazać odnośnik do obiektu klasy implementującej metodę run interfejsu Runnable.
public
class Master {
public static void main(String args[])
{
System.out.println("Hello from MainThread");
Other other = new Other();
Thread thread = new Thread(other, "OtherThread");
thread.start();
}
}
class Other implements Runnable {
public void run()
{
System.out.println("Hello from OtherThread");
}
}
Podany program napisano wielowątkowo. Jest on wykonywany dwuwątkowo od chwili wywołania metody start
thread.start();
do chwili zakończenia wykonywania funkcji main albo metody run (co zajdzie wcześniej).
Wykonanie programu powoduje wyprowadzenie napisu
Hello from MainThread
Hello from OtherThread
Interfejs Runnable
Niekiedy metodę run deklaruje się w tej samej klasie, która tworzy nowy wątek. Taka klasa musi implementować interfejs Runnable.
public
class Master implements Runnable {
public static void main(String args[])
{
System.out.println("Hello from MainThread");
Master myThis = new Master();
new Thread(myThis, "OtherThread").start();
}
public void run()
{
System.out.println("Hello from OtherThread");
}
}
Podany program napisano wielowątkowo. Jest on wykonywany dwuwątkowo od chwili wywołania metody start
new Thread(this, "OtherThread").start();
do chwili zakończenia wykonywania funkcji main albo metody run (co zajdzie wcześniej).
Wykonanie programu powoduje wyprowadzenie napisu
Hello from MainThread
Hello from OtherThread
Synchronizowanie wątków
Jeśli dwa, lub więcej, wątków dzieli wspólny zasób (na przykład pamięć), to szczególną troską należy otoczyć dostęp do zasobu.
W szczególności dotyczy to nie-ulotnych (oryg. non-volatile) zmiennych typu "long" i "double", dostęp do których przez odrębne wątki musi być zawsze synchronizowany.
Uwaga: Wymaganie synchronizacji dotyczące zmiennych "long" i "double" jest niechętnym ukłonem twórców Javy pod adresem tych archaicznych komputerów, w których dostęp do zmiennych wymienionych typów składa się z odrębnego dostępu do ich części bardziej i mniej znaczącej.
Aby się przekonać o konieczności synchronizowania dostępu, wystarczy rozpatrzyć sytuację, gdy rolę wątków pełnią dwaj kasjerzy (oryg. teller), którzy mają nie-synchronizowany dostęp do wspólnej bazy danych.
Jeśli na koncie współmałżonków jest na przykład 100 USD, a każde z nich wpłaca w osobnym okienku 20 USD, to może zaistnieć następująca sytuacja
Pierwszy kasjer sprawdza konto i odnotowuje jego stan ($100), ale coś odrywa go do telefonu.
Drugi kasjer sprawdza konto, odnotowuje jego stan ($100), dodaje $20 i aktualizuje konto (do $120).
Pierwszy kasjer kończy rozmowę, dodaje $20 do odnotowanej sumy i aktualizuje bazę (do $120).
W następstwie nie-synchronizowanego dostępu do bazy danych, następuje zwiększenie konta nie o $40, ale $20.
Następujący program pokazuje jak uniknąć takiego niekorzystnego przebiegu wydarzeń.
import java.io.*;
class Savings {
private float savings;
Savings(float savings)
{
this.savings = savings;
}
void add(float amount)
{
savings += amount;
}
public String toString()
{
return String.valueOf(savings);
}
}
class T/*Transaction*/ {
int account;
float amount;
T/*Transaction*/(int account, float amount)
{
this.account = account;
this.amount = amount;
}
}
public
class Master {
static float savingsData[] =
{ 100, 500, 300, 400, 200 };
static Savings dataBase[] =
new Savings[savingsData.length];
static int noOfTellers = 2;
public static void main(String args[])
throws IOException
{
loadDataBase(dataBase, savingsData);
T/*Transaction*/
setOne[] = { new T(2, 10), new T(4, 20) },
setTwo[] = { new T(0, 30), new T(2, 10) };
// ===================
// 100 500 300 400 200 DataBase
// 10 20 John
// 30 10 Bill
// ===================
// 130 500 320 400 220 results
Teller john = new Teller("John", setOne, dataBase),
bill = new Teller("Bill", setTwo, dataBase);
Thread tellerOne = new Thread(john),
tellerTwo = new Thread(bill);
showDataBase(dataBase);
tellerOne.start();
tellerTwo.start();
while(counter < noOfTellers)
try {
Thread.currentThread().sleep(1000);
}
catch(InterruptedException e) {
}
showDataBase(dataBase);
}
static void loadDataBase(Savings dataBase[],
float savingsData[])
{
for(int i = 0; i < savingsData.length ; i++)
dataBase[i] = new Savings(savingsData[i]);
}
static void showDataBase(Savings dataBase[])
{
for(int i = 0; i < dataBase.length ; i++)
System.out.println("Account #" + i + ":" +
" $" + dataBase[i]);
}
static int counter = 0;
static synchronized void setDone()
{
counter++;
}
}
class Teller implements Runnable {
private String name;
private T/*Transation*/ set[];
private Savings dataBase[];
Teller(String name, T/*Transaction*/ set[],
Savings dataBase[])
{
this.name = name;
this.set = set;
this.dataBase = dataBase;
}
private void updateSavings(int i)
{
int account = set[i].account;
float amount = set[i].amount;
System.out.println(name + " " +
account + " " + amount);
dataBase[account].add(amount);
}
public void run()
{
for(int i = 0; i < set.length ; i++)
synchronized(dataBase)
updateSavings(i);
Master.setDone();
}
}
Dzięki synchronizacji dostępu do bazy dataBase, funkcja updateSavings może być w danej chwili wykonywana tylko przez co najwyżej jeden wątek.
Wykonanie programu powoduje wyprowadzenie napisu pokazanego w Tabeli Aktualizacja kont. Kolejność wierszy środkowej części podanego napisu może się zmieniać od wykonania do wykonania.
Tabela Aktyalizacja kont
###
Account #1 $100
Account #1 $500
Account #2 $300
Account #3 $400
Account #4 $200
John 2 10
Bill 0 30
John 4 20
Bill 2 10
Account #1 $130
Account #1 $500
Account #2 $320
Account #3 $400
Account #4 $220
###
Procedury synchronizowane
Procedurą synchronizowaną jest procedura zadeklarowana ze specyfikatorem synchronized.
class Counter {
private int counter = 0;
public synchronized void Counter()
{
counter += 1;
}
}
Metody synchronizowane
Wywołanie metody synchronizowanej Metoda, wywołanej na rzecz obiektu Obiekt w instrukcji
Metoda.Obiekt(Arg, Arg, ... , Arg);
jest równoważne wywołaniu
synchronized(Obiekt) {
Metoda.Obiekt(Arg, Arg, ... , Arg);
}
identycznej z nią metody niesynchronizowanej.
Funkcje synchronizowane
Wywołanie funkcji synchronizowanej Funkcja należącej do klasy Klasa w instrukcji
Klasa.Funkcja(Arg, Arg, ... , Arg);
jest równoważne wywołaniu
try {
synchronized(Class.forName(Klasa)) {
Klasa.Funkcja(Arg, Arg, ... , Arg);
}
}
catch(ClassNotFoundException e) {
}
identycznej z nią funkcji niesynchronizowanej.
Niekiedy można spotkać się z nieprawdziwym twierdzeniem, że na przykład klasa
class Counter {
private int counter = 0;
public synchronized void Counter(Point point)
{
if(point.x == 0 || point.y == 0)
counter++;
}
}
jest równoważna klasie
class Counter {
private int counter = 0;
public void Counter(Point point)
{
synchronized(this) {
if(point.x == 0 || point.y == 0)
counter++;
}
}
}
Kto po przeczytaniu tego rozdziału potrafi skonstruować program wykazujący nierównoważność podanych klas, ten nie musi się obawiać czy dostatecznie dobrze zna pułapki programowania współbieżnego.
Monitor
Jeśli metoda synchronizowana klasy zostanie wywołana na rzecz pewnego obiektu, to wywołujący ją wątek zajmie monitor obiektu.
Do czasu zwolnienia monitora, zostanie zablokowane wykonanie każdego innego wątku, który podejmie próbę wywołania (na rzecz tego samego obiektu), dowolnej metody sychronizowanej danej klasy.
Uwaga: Jeśli zablokowanie dostępu ma dotyczyć zmiennej statycznej, to użycie metody synchronizowanej nie jest wystarczające, ponieważ metoda może być wywołana na rzecz innego obiektu. W takim wypadku obiektem synchronizującym powinien być unikatowy obiekt klasy Class metody.
class Counter {
private static int counter = 0;
synchronized public void incCounter()
{
Class classObject = getClass();
synchronized(classObject) {
counter += 1;
}
}
}
Gdyby nie użyto instrukcji synchronized, to mimo zadeklarowania metody incCounter jak synchronizowanej, istniałaby możliwość jednoczesnego wykonania operacji zwiększenia zmiennej counter przez dwa różne wątki.
Uwaga: Gdyby procedura incCounter była funkcją, a nie metodą, to użycie instrukcji synchronized byłoby zbędne, ponieważ funkcje statyczne są synchronizowane na unikalnym obiekcie ich klasy.
Wątek znajdujący się w obrębie monitora może posługiwać się metodami wait i notify:
Wywołanie metody wait na rzecz obiektu powoduje zwolnienie monitora przez wątek i wstrzymanie wykonywania wątku. Umożliwi to innemu wątkowi ubieganie się o przydzielenie monitora.
Wywołanie metody notify na rzecz obiektu powoduje uwolnienie jednego wstrzymanego wątku. Umożliwi to uwolnionemu wątkowi ubieganie się o zajęcie monitora, ale nie wcześniej niż monitor zostanie zwolniony przez wątek wywołujący metodę notify.
Następujący program ilustruje użycie monitora do oprogramowana klasycznego zadania typu Producent-Konsument (oryg. producer-consumer).
public class Master {
static Box box = new Box();
public static void main(String args[])
{
new Producer(box);
new Consumer(box);
}
}
class Producer implements Runnable {
Box box;
Producer(Box box)
{
this.box = box;
new Thread(this).start();
}
public void run()
{
for(int number = 0; number < 5 ; number++)
box.putInto(number+1);
box.setDone();
}
}
class Consumer implements Runnable {
Box box;
Consumer(Box box)
{
this.box = box;
new Thread(this).start();
}
public void run()
{
int gotFrom;
while(true)
gotFrom = box.getFrom();
}
}
class Box {
int contents;
boolean boxEmpty = true;
static boolean producerDone = false; // flaga końca
public Box()
{
}
synchronized void setDone()
{
producerDone = true;
}
synchronized int getFrom()
{
if(boxEmpty) {
if(producerDone) {
try {
System.in.read(); // pauza
}
catch(IOException e) {
}
System.exit(0);
}
try {
wait();
}
catch(InterruptedException e) {
}
}
int number = contents;
System.out.println("Number " + number +
" from Box");
boxEmpty = true;
notify();
return contents;
}
synchronized void putInto(int number)
{
if(!boxEmpty)
try {
wait();
}
catch(InterruptedException e) {
}
contents = number;
System.out.println("Number " + number +
" to Box");
boxEmpty = false;
notify();
}
}
Producent i konsument operują na wspólnym zasobie, jakim jest pudełko box klasy Box. Producent wkłada liczby do pudełka, a konsument je wyjmuje. Jeśli pudełko jest puste, to czeka konsument, a jeśli jest pełne, to czeka producent.
Po wstawieniu do programu dodatkowych wierszy informujących o przebiegu wydarzeń można otrzymać na przykład sekwencję wyjściową pokazaną w Tabeli Produkcja-Konsumpcja.
Tabela Produkcja-Konsumpcja
###
koniec wykonania funkcji main ale wykonanie trwa
startuje producent zaczyna działać
producent wywołuje putInto(1)
producent wstawia 1 w pudełka
producent wywołuje putInto(2)
startuje konsument dopiero teraz
konsument wywołuje getFrom
konsument dostaje 1 z pudełka
konsument wywołuje getFrom
producent wstawia 2 do pudełka
konsument dostaje 2 z pudełka
producent wywołuje putInto(3)
konsument wywołuje getFrom
producent wstawia 3 do pudełka
producent wywołuje putInto(4)
konsument dostaje 3 z pudełka
producent wstawia 4 do pudełka
konsument wywołuje getFrom
producent wywołuje putInto(5)
konsument dostaje 4 z pudełka
producent wstawia 5 do pudełka
konsument wywołuje getFrom
producent kończy wykonywanie ustawia flagę końca
konsument dostaje 5 z pudełka
konsument wywołuje getFrom
konsument kończy wykonywanie po zbadaniu flagi
###
Uwaga: W przypadku wysyłania znaków do tego samego strumienia (np. do System.out) z kilku współbieżnie wykonywanych wątków należy zatroszczyć się o właściwą synchronizację wysyłań. W przeciwnym razie łańcuchy znaków pochodzące z różnych źródeł mogą być bezsensownie przemieszane.
Potok
Alternatywnym sposobem oprogramowania problemu wymiany informacji między producentem-konsumentem jest użycie potoku (oryg. pipe).
Każdy potok jest w istocie kolejką typu FIFO (oryg. First In First Out), do której jeden wątek wysyła dane, a drugi je z niej odbiera. Jeśli kolejka nie zawiera danych, to konsument czeka na wysłanie ich przez producenta. Synchronizacja wysyłania i oczekiwania jest realizowana niejawnie.
Następujący program ilustruje użycie potoków do rozwiązania problemu typu producent-konsument.
public
class Master {
static DataInputStream inPipe;
static DataOutputStream outPipe;
public static void main(String args[])
throws IOException
{
Producer producer = new Producer();
Consumer consumer = new Consumer();
producer.getPipe().connect(consumer.getPipe());
consumer.getPipe().connect(producer.getPipe());
new Thread(consumer).start();
new Thread(producer).start();
}
}
class Producer implements Runnable {
PipedOutputStream out;
Producer()
{
Master.outPipe =
new DataOutputStream(
out = new PipedOutputStream()
);
}
PipedOutputStream getPipe()
{
return out;
}
public void run()
{
for(int number = 0; number < 5 ; number++)
try {
Master.outPipe.writeInt(number);
}
catch(IOException e) {
}
}
}
class Consumer implements Runnable {
PipedInputStream in;
Consumer()
{
Master.inPipe =
new DataInputStream(
in = new PipedInputStream()
);
}
PipedInputStream getPipe()
{
return in;
}
public void run()
{
int number;
while(true) {
try {
number = Master.inPipe.readInt();
System.out.println(number);
}
catch(IOException e) {
break;
}
}
}
}
Impas
O ile synchronizacja nie jest zaprojektowana prawidłowo, może powstać impas (oryg. deadlock). Sprowadza się on do tego, że w zestawie współdziałających wątków każdy jest zablokowany, zawieszony albo wstrzymany, ale nie istnieje możliwość odwieszenia ani uwolnienia przynajmniej jednego wątku.
Następujący program monitoruje pracę dwóch osób. Każda z nich informuje o postępach: swoich i konkurenta.
public
class Master {
static Worker tom, bob;
public static void main(String args[])
{
tom = new Worker("Tom");
bob = new Worker("Bob");
new Thread(tom.setOther(bob)).start();
new Thread(bob.setOther(tom)).start();
}
static synchronized void sendMessage(String string)
{
System.out.println(string);
}
}
class Worker implements Runnable {
private long counter = 0;
String name;
Worker other;
Worker(String name)
{
this.name = name;
}
Worker setOther(Worker other)
{
this.other = other;
return this;
}
synchronized void showStatus()
{
Master.sendMessage(name + " " + counter +
", other: " + other.peep());
}
synchronized long peep()
{
return counter;
}
public void run()
{
while(true) {
counter++; // wykonanie pracy
showStatus(); // komunikat
}
}
}
Podczas wykonywania programu może (ale nie musi!) dojść do impasu. Dojdzie do niego na przykład przy takim splocie wydarzeń:
1. Wątek Tom wywołuje metodę showStatus.
2. Ponieważ showStatus jest metodą synchronizowaną, więc wątkowi Tom przydziela się monitor związany ze zmienną Master.tom.
Niech w tym miejscu zostanie przerwane wykonywnie wątku Tom.
3. Zaczyna się wykonywać wątek Bob i wywołuje metodę showStatus.
4. Ponieważ showStatus jest metodą synchronizowaną, więc wątkowi Bob przydziela się monitor związany ze zmienną Master.bob.
5. Metoda showStatus wywołuje metodę synchronizowaną peep.
6. Ponieważ peep jest metodą synchronizowaną, więc wątek Bob spodziewa się przydzielenia monitora związanego ze zmienną Master.tom (zmienna other jest odnośnikiem do tej właśnie zmiennej). Monitor jest już jednak przydzielony, więc wątek Bob zostaje zablokowany.
Niech w tym miejscu nastąpi wznowienie wykonywania wątku Tom.
7. Wątek Tom wywołuje metodę peep.
8. Ponieważ peep jest metodą synchronizowaną, więc wątek Tom spodziewa się przydzielenia monitora związanego ze zmienną Master.bob (zmienna other jest odnośnikiem do tej właśnie zmiennej).
9. Monitor jest już jednak już przydzielony, więc wątek Tom zostaje zablokowany, w oczekiwaniu na zwolnienie monitora.
Wystąpił impas. Żaden z rozpatrywanych wątków nie wykonuje się.
Wykonanie programu w systemie Windows 95 dość szybko doprowadzało do oczekiwanego zawieszenia programu. Każde wykonanie dawało inną sekwencję wyjściową. Najkrótszą z uzyskanych (ale dłuższą od teoretycznie najkrótszej) przytoczono w Tabeli Wyniki przed impasem.
Tabela Wyniki przed impasem
###
Tom 1, other: 0
Bob 1, other: 2
Tom 2, other: 1
Tom 3, other: 1
Tom 4, other: 1
Bob 2, other: 4
Tom 5, other: 3
###
Zniszczenie wątku
Przedwczesne zakończenie metody run następuje po wysłaniu do wątku wyjątku klasy ThreadDeath. Ponieważ ThreadDeath jest podklasą klasy Error, a nie Exception, w typowych sytuacjach wyjątek ten nie jest przechwytywany.
Nadrzędna obsługa wyjątku klasy ThreadDeath polega na wywołaniu na rzecz wątku metody stop oraz wykonanie metody notifyAll, która powoduje aktywowanie wszystkich wstrzymanych wątków.
Uwaga: Wątek może przechwycić wyjątek ThreadDeath (w celu wykonania specjalnej obsługi), ale w takim wypadku powinien sam wysłać ten wyjątek.
Na przykład
public void run()
{
while(true) {
try {
// ... // przetwarzanie
}
catch(Exception e) { // typowe wyjątki
// ..
}
catch(ThreadDeath e) {
// ... // specjalna obsługa
throw e; // wysłanie "dalej"
}
}
}
_________________________________________________________________________________________
Przesyłanie
Wiele programów wykonywanych w środowisku graficznym nie wymaga wykonywania operacji na plikach. Jednak umiejętność przesyłania danych między pamięcią operacyjną a plikiem, jest ważna w każdym języku programowania.
Podobnie jak C++, występuje w Javie pojęcie strumienia (oryg. stream), jako abstrakcyjnego odpowiednika źródła (oryg. source) i celu (oryg. target) przesłania danych. Obiektowymi odpowiednikami tego pojęcia są klasy InputStream i OutputStream.
Klasa plikowa
Równie często jak przesyłanie danych, występuje potrzeba wykonania operacji na pliku (oryg. file) albo katalogu (oryg. directory, folder). Do tego celu doskonale nadaje się klasa plikowa File.
Metody klasy File umożliwiają realizowanie zapytań o właściwości plików i katalogów. Jedna z metod umożliwia usunięcie pliku, ale żadna nie pozwala na usunięcie katalogu.
Uwaga: Jeśli nazwa pliku zawiera separatory "\" albo "/", to niezależnie od użytego systemu operacyjnego, każdy z nich jest uznawany za poprawny.
public
class Streams {
public static void main(String args[])
{
String path, name;
if( args.length != 2) {
System.out.println("Please supply Path & Name");
return;
}
path = args[0];
name = args[1];
String pathName = path + "/" + name;
File file = new File(pathName);
if(!file.exists())
throw new FileNotFoundException(pathName);
if(file.isFile()) {
if(!file.canRead()) {
System.out.println(
"File " + pathName + " is not readible"
);
return;
} else {
FileInputStream source =
newFileInputStream(pathName);
byte buffer[] = new byte[1024];
while(true) {
int byteCount = source.read(buffer);
if(byteCount == -1)
break;
System.out.write(buffer, 0, byteCount);
}
}
} else {
System.out.println(
"Directory " + pathName + " contains\n");
String list[] = file.list();
for(int i = 0; i < list.length ; i++) {
String fileName = list[i];
System.out.print(fileName);
File fullName = new File(pathName + "/" + fileName);
if(fullName.isDirectory())
System.out.print("\t is a directory");
System.out.println();
}
}
}
}
Wykonanie programu powoduje wyprowadzenie zawartości podanego pliku albo katalogu.
Uwaga: Jeśli wyprowadzenie zawartości katalogu powinno być ograniczone do plików mających pewne cechy (np. wyłącznie do plików z rozszerzeniem .exe), to można zastosować filtrowanie nazw (oryg. filename filtering). Polega ono na wykorzystaniu interfejsu FilenameFilter.
W szczególności, następujący program wyprowadza wyłącznie nazwy tych plików, które mają rozszerzenia .exe, i .com.
import java.io.*;
class Master {
public static void main(String args[])
{
String dirName = args[0];
File file = new File(dirName);
if(file.isDirectory()) {
System.out.println("Directory of " + dirName);
System.out.println();
FilenameFilter progs = new ProgsOnly();
String fileNames[] = file.list(progs);
for(int i = 0; i < fileNames.length ; i++)
System.out.println(fileNames[i]);
} else
System.out.println(dirName + "is not a " +
"directory!");
}
}
class ProgsOnly implements FilenameFilter {
public boolean accept(File dirName, String fileName)
{
return fileName.endsWith(".exe") ||
fileName.endsWith(".com") ||
fileName.endsWith(".EXE") ||
fileName.endsWith(".COM");
}
}
Klasy wejściowe
Klasami wejściowymi są podklasy klasy InputStream. Umożliwiają one wprowadzanie danych z plików i urządzeń (klasa FileInputStream), a także wprowadzanie danych z tablic, traktowanych jak wówczas jak pliki (klasa ByteArrayInputStream).
public
class Streams {
public static void main(String args[])
throws IOException
{
String greet = "Hello, I am Jan B.";
int length = greet.length();
char charArray[] = greet.toCharArray();
byte byteArray = new byte [length];
for(int i = 0; i < length ; i++)
byteArray[i] = (byte)charArray[i];
InputStream source =
new ByteArrayInputStream(byteArray);
int Chr;
while((Chr = source.read()) != -1)
System.out.print(
Character.toLowerCase((char)Chr)
);
System.out.println();
}
}
Wykonanie programu powoduje wyprowadzenie napisu
hello, i am jan b.
Instrukcji
for(int i = 0; i < length ; i++)
byteArray[i] = (byte)charArray[i];
nie można zastąpić prostszą instrukcją
System.arraycopy(charArray, 0, byteArray, 0, length);
ponieważ podczas wykonywania programu powstałaby wówczas sytuacja wyjątkowa
ArrayStoreException
Klasy wyjściowe
Klasami wyjściowymi są podklasy klasy OutputStream. Umożliwiają one wyprowadzanie danych do plików i urządzeń (klasa FileOutputStream), a także wyprowadzanie danych do tablic, traktowanych wówczas jak pliki (klasa ByteArrayOutputStream).
class Streams {
public static void main(String args[])
{
if(args.length != 2)
System.err.println("Please supply " +
"Source & Target");
else
try {
Streams.copyFile(args[0], args[1]);
}
catch(IOException e) {
System.err.println(e.getMessage());
}
System.out.println("Done!");
}
static void copyFile(String sourceName, String targetName)
throws IOException
{
File srcFile = new File(sourceName),
trgFile = new File(targetName);
if(!Streams.checkOut(srcFile, trgFile))
return;
InputStream src = new FileInputStream(srcFile);
OutputStream trg = new FileOutputStream(trgFile);
int length = (int)srcFile.length();
byte buf[] = new byte[length];
int size = src.read(buf);
if(size != length)
throw new IOException("Something is Wrong");
trg.write(buf, 0, length);
}
static boolean checkOut(File src, File trg)
throws IOException
{
if(!src.exists() || !src.isFile())
throw new FileCopyException("Source file " +
"does not exist");
if(!src.canRead())
throw new FileCopyException("Source file " +
"is unreadible");
if(trg.exists()) {
if(!trg.isFile())
throw new FileCopyException("Target " +
"is not a file");
System. out.print("Target file already exists." +
"Overwrite (y/n)?: ");
System.out.flush();
DataInputStream console =
new DataInputStream(System.in);
String reply = new String("");
while(reply != "y" && reply != "n")
reply = console.readLine();
if(reply.equals("n"))
return false;
if(!trg.canWrite())
throw new FileCopyException("Target " +
"is not writeable");
}
return true;
}
}
class FileCopyException extends IOException {
FileCopyException(String msg)
{
super(msg);
}
}
Wykonanie podanego programu, wywołanego na przykład za pomocą polecenia
java Streams c:\autoexec.bat a:\autoexec.old
powoduje skopiowanie (krótkiego) pliku z dysku C na dysk A.
Przesyłanie przenośne
Do wykonywania przenośnego przesyłania danych służą metody klas DataInputStream (implementująca interfejs DataInput) i DataOutputStream (implementująca interfejs DataOutput).
Metody te umożliwiają m.in. przesyłanie danych typów podstawowych w niezależnej od platformy postaci binarnej. Jest to szczególnie istotne w kontekście różnic, jakie występują między adresowaniem pamięci w procesorach różnych wykonawców.
Na przykład w procesorach firmy Intel podczas przesyłania rejestru do pamięci następuje zwierciadlane odwrócenie jego bajtów. Jest to przejawem zastosowania konwencji reprezentowania w pamięci RAM najmniej znaczącego bajtu danych wielobajtowych według zasady ostatni-na-początku (oryg. big-endian) istotnie różnej od zastosowanej na przykład w Power PC zasady ostatni-na-końcu (oryg. little-endian).
Uwaga: Dodatkową zaletą klas do przenośnego przesyłania danych jest to, że "radzą sobie" z takimi zakończeniami wierszy jak \n, \r, \r\n, traktując każde z nich jak \n.
W celu użycia metod klasy DataInputStream albo DataOutputStream należy utworzyć obiekt ich klasy, posługując się w tym celu konstruktorem akceptującym odpowiednio argument typu InputStream albo OutputStream.
public
class Master {
public static void main(String args[])
throws IOException
{
File srcFile = new File("SOURCE"),
trgFile = new File("TARGET");
InputStream source = new FileInputStream(srcFile),
OutputStream target = new FileOutputStream(trgFile);
DataInputStream src = new DataInputStream(source);
DataOutputStream trg = new DataOutputStream(target);
try {
double oneDouble;
while(true) {
oneDouble = src.readDouble();
trg.writeDouble(oneDouble);
}
}
catch(EOFException e) {
}
finally {
src.close();
trg.close();
}
}
}
Wykonanie programu powoduje przenośne skopiowanie wszystkich danych typu "double" z pliku SOURCE do pliku TARGET.
Przesyłanie leksemowe
W celu wprowadzenia z pliku zestawu danych o postaci leksemów (głównie słów i liczb) najlepiej jest wczytywać pełne wiersze poprzez strumień klasy DataInputStream, a po przetworzeniu ich za pomocą obiektu klasy StringTokenizer wyprowadzać je poprzez strumień klasy PrintStream.
Klasa DataInputStream
Wykonanie następującego programu powoduje wczytanie z pliku SOURCE zestawu liczb typu "double", a następnie wyprowadzenie do pliku TARGET ich kwadratów, po jednym w wierszu.
public
class Master {
public static void main(String args[])
throws IOException
{
DataInputStream inp = new DataInputStream(
new FileInputStream("SOURCE")
);
PrintStream out =
new PrintStream(
new FileOutputStream("TARGET")
);
String oneLine;
while((oneLine = inp.readLine()) != null) {
StringTokenizer tokenizer =
new StringTokenizer(oneLine, "\t ,");
while(tokenizer.hasMoreElements()) {
String token = (String)tokenizer.nextElement();
double number =
Double.valueOf(token).doubleValue();
number *= number;
out.println("" + number);
}
}
}
}
W odróżnieniu od pozostałych metod klasy DataInputStream, metoda readLine nie wysyła wyjątku EOFException. Objawem napotkania końca pliku jest zwrócenie przez tę metodę odniesienia pustego (null).
Klasa StreamTokenizer
Leksemy można także wprowadzać wprost ze strumienia wejściowego. Ilustruje to następujący program, w którym po założeniu słownika angielsko-polskiego, wczytuje się słowa angielskie ze standardowego strumienia wejściowego i wyszukuje ich polskie odpowiedniki.
import java.io.*;
import java.util.*;
public
class Master {
static Hashtable hashTable = new Hashtable();
static String dictionary[][] =
{
{ "red", "czerwony" },
{ "blue", "niebieski" },
{ "green", "zielony" }
};
static int whatNext;
public static void main(String args[])
throws IOException
{
load(hashTable, dictionary);
StreamTokenizer src = new StreamTokenizer(
System.in
);
while((whatNext = src.nextToken()) !=
StreamTokenizer.TT_EOF) {
if(whatNext == StreamTokenizer.TT_WORD) {
String english = src.sval,
polish;
polish = (String)hashTable.get(src.sval);
if(polish != null)
System.out.println(english + "\t" + polish);
else
System.out.println("I do not " +
"understand word " +
"\"" + english +
"\"");
} else
System.out.println("Please enter a word!");
}
}
static void load(Hashtable hash, String dict[][])
{
for(int i = 0; i < dict.length ; i ++) {
String english = dict[i][0],
polish = dict[i][1];
hash.put(english, polish);
}
}
}
Do skonstruowania słownika użyto klasy Hashtable wchodzącej w skład pakietu java.util.
Przesyłanie buforowane
Przesyłanie buforowane (oryg. buffered) odbywa się za pomocą strumieni klas BufferedInputStream i BufferedOutputStream. Dzięki buforowaniu, wprowadzane i wyprowadzane są duże porcje danych, co znacznie przyspiesza sekwencyjne przetwarzanie plików o dużych rozmiarach.
Następujący program ilustruje zastosowanie przesyłania buforowanego do kopiowania dużych plików danych.
import java.io.*;
public class Master {
static int Size = 4096; // rozmiar bufora
public static void main(String args[])
throws IOException
{
String srcName = args[0],
trgName = args[1];
FileInputStream srcFile =
new FileInputStream(srcName);
FileOutputStream trgFile =
new FileOutputStream(trgName);
BufferedInputStream src =
new BufferedInputStream(srcFile, Size);
BufferedOutputStream trg =
new BufferedOutputStream(trgFile, Size);
byte buffer[] = new byte [Size];
while(true) {
int bytesRead = src.read(buffer, 0, Size);
if(bytesRead == -1)
break;
trg.write(buffer, 0, bytesRead);
}
trg.close(); // niezbędne!
System.out.println("Done!");
}
}
Przesyłanie filtrowane
Przesyłanie filtrowane odbywa się za pomocą strumieni klas FilterInputStream i FilterOutputStream. Polega ono na tym, że jeśli utworzy się klasę pochodna od jednej z tych klas, a w niej przedefiniuje wybrane procedury, to właśnie one zostaną użyte do przesyłania danych.
Następujący program ilustruje zastosowanie przesyłania filtrowanego do implementowania programu GREP, znanego głównie z systemu UNIX, ale obecnie dostępnego także w innych systemach.
import java.io.*;
public class Master {
public static void main(String args[])
throws IOException
{
String string = args[0], // ciąg znaków
srcName = args[1]; // nazwa pliku
FileInputStream srcFile =
new FileInputStream(srcName);
DataInputStream srcData =
new DataInputStream(srcFile);
GrepInputStream srcGrep =
new GrepInputStream(srcData, string);
String oneLine;
while((oneLine = srcGrep.readLine()) != null)
System.out.println(oneLine);
srcGrep.close();
}
}
class GrepInputStream extends FilterInputStream {
DataInputStream src;
String string;
GrepInputStream(DataInputStream src, String string)
{
super(src);
this.src = src;
this.string = string;
}
public String readLine()
throws IOException
{
String oneLine;
do { // filtrowanie wierszy
oneLine = src.readLine();
if(oneLine == null)
return null;
} while(oneLine.indexOf(string) == -1);
return oneLine;
}
}
Filtrowanie wierszy polega na pomijaniu tych, w których nie jest zawarty łańcuch string.
Jeśli podany program zostanie wywołany na przykład za pomocą polecenia
java Master.java oneLine
to nastąpi wyprowadzenie tych jego wierszy które zawierają łańcuch oneLine
String oneLine;
while((oneLine = srcGrep.readLine()) != null)
System.out.println(oneLine);
String oneLine;
do { // filtrowanie wierszy
oneLine = src.readLine();
if(oneLine == null)
return null;
while(oneLine.indexOf(string) == -1);
return oneLine;
Przesyłanie wyrywkowe
Przesyłanie wyrywkowe odbywa się za pomocą strumieni klasy RandomAccessFile.
Z każdym strumieniem jest związana pozycja strumienia (oryg. file pointer) określona przez nieujemną liczbę całkowitą typu "long".
W chwili otwarcia strumień znajduje się w pozycji 0. Do zarządzania jego pozycją służą metody getFilePointer, seek i skipBytes.
long getFilePointer() throws IOException
Wywołanie metody getFilePointer powoduje dostarczenie bieżącej pozycji strumienia.
void seek(long position) throws IOException
Wywołanie metody seek zmienia bieżącą pozycję strumienia na podaną pozycję.
int skipBytes(int count)
Wywołanie metody skipBytes przemieszcza pozycję strumienia o podaną liczbę bajtów.
Wykonanie następującego programu, na przykład na podstawie danych zawartych w pliku SOURCE (dane muszą być oddzielone znakami tabulacji!), powoduje utworzenie bazy danych w pliku DATABASE a następnie wydrukowanie jej.
W Tabeli Dane i Rezultaty pokazano zawartość przykładowego pliku SOURCE oraz uzyskane rezultat przetworzenia bazy. Warto przemyśleć, dlaczego w wynikach pojawiły się dodatkowe spacje i co należałoby uczynić aby je usunąć.
Tabela Dane i rezultaty
###
Dane
3 12.50 Barbie
1 14.50 Ken
4 7.50 Top
Rezultaty
0 ---
1 $14.5 K e n
2 ---
3 $12.5 B a r b i e
4 $7.5 T o p
###
import java.awt.*;
import java.io.*;
import java.util.*;
public
class Master {
static int length = 5; // rozmiar bazy
public static void main(String args[])
throws IOException
{
RandomAccessFile dataBase =
new RandomAccessFile("c:\\DATABASE", "rw");
InputStream srcFile = new FileInputStream("SOURCE");
Master.clean(dataBase, length);
Master.createDataBase(dataBase, srcFile);
Master.showDataBase(dataBase);
}
static void clean(RandomAccessFile trgFile, int length)
throws IOException
{
int itemSize = Product.sizeOf();
for(int i = 0; i < length ; i++) {
Product product = new Product();
product.write(trgFile, i);
}
}
static void createDataBase(RandomAccessFile trgFile,
InputStream srcFile)
{
DataInputStream src = new DataInputStream(srcFile);
int itemSize = Product.sizeOf();
try {
String oneLine;
while((oneLine = src.readLine()) != null) {
StringTokenizer items =
new StringTokenizer(oneLine, "\t$\n");
int id = Integer.parseInt(Master.nextItem(items));
double price =
Double.valueOf(Master.nextItem(items))
.doubleValue();
String name = Master.nextItem(items);
trgFile.seek(id * itemSize);
Product product = new Product(name, price);
product.write(trgFile, id);
}
}
catch(IOException e) {
}
}
static String nextItem(StringTokenizer items)
{
String item = items.nextToken();
return item.substring(0, item.length()-1);
}
static void showDataBase(RandomAccessFile src)
throws IOException
{
for(int pos = 0; pos < Master.length; pos++) {
Product product = Product.read(src, pos);
System.out.print(pos + "\t");
if(product.print())
System.out.println("---");
}
}
}
class Product {
static String _12spaces = " ";
String name = _12spaces;
double price = 0;
public Product(String name, double price)
{
this.name = (name + _12spaces).substring(0, 12);
this.price = price;
}
public Product()
{
}
static int sizeOf()
{
return 24 + 8; // sizeOf(name) + sizeOf(price)
}
public void write(RandomAccessFile dataBase, int id)
throws IOException
{
dataBase.seek(id * Product.sizeOf());
dataBase.writeChars(name);
dataBase.writeDouble(price);
}
public static Product read(RandomAccessFile dataBase,
int id)
throws IOException
{
dataBase.seek(id * Product.sizeOf());
byte byteName[] = new byte [24];
dataBase.read(byteName);
StringBuffer name = new StringBuffer();
for(int i = 0; i < 24 ; i++, i++)
name.append((char)byteName[i+1]);
double price = dataBase.readDouble();
return new Product(new String(name, 0), price);
}
public boolean print()
{
boolean emptySlot = name.equals(_12spaces);
if(!emptySlot))
System.out.println("$" + price + "\t" + name);
return emptySlot;
}
}
_________________________________________________________________________________________
Wykonywanie
Wykonanie programu zaczyna się od wyodrębnienia klasy głównej zawierającej definicję funkcji main. Od tego momentu zaczyna się przetwarzanie klas i obiektów na które składa się ładowanie i łączenie klas, tworzenie, inicjowanie i niszczenie zmiennych, uzyskiwanie dostępu do zmiennych oraz wywoływanie procedur (w tym wywoływanie funkcji oraz wywoływanie metod na rzecz obiektów).
Ładowanie klas
Po zlokalizowaniu przez ładowacza klas (oryg. class loader) klasy głównej następuje załadowanie tej właśnie klasy oraz ewentualnie innych klas które mają być aktywnie użyte.
Każda załadowana klasa jest: weryfikowana (oryg. verified), przygotowywana (oryg. prepared), łączona (oryg. linked) i inicjowana (oryg. initialized).
Jeśli ładowana klasa jest podklasą innej klasy, to tuż przed jej zainicjowaniem jest ładowana jej nadklasa, która także podlega weryfikacji, przygotowaniu, łączeniu i inicjowaniu.
Weryfikacja
Weryfikacja polega na sprawdzeniu, czy B-kod klasy jest właściwie uformowany, czy zawiera odpowiednią tablicę symboli oraz czy spełnia określone w specyfikacji języka wymagania syntaktyczne, w tym czy zawiera wyłącznie dopuszczalne kody operacji, przeniesienia sterowania tylko do początku instrukcji oraz właściwe sygnatury procedur.
Przygotowanie
Przygotowanie polega na przydzieleniu pamięci dla zmiennych statycznych oraz na przypisaniu im wartości domyślnych (np. odnośnikom wartości null, a zmiennym arytmetycznym wartości 0).
Łączenie
Łączenie polega na realizowaniu odwołań do klas, interfejsów, zmiennych i pól w celu zastąpienia zweryfikowanych odwołań symbolicznych odwołaniami bezpośrednimi wykorzystującymi adresy Maszyny Wirtualnej.
Na tym etapie wychodzi na przykład na jaw, że od czasu skompilowania danej klasy nastąpiła nieakceptowalna zmiana używanej przez nią klasy (na przykład usunięcie wymaganego pola nadklasy).
Uwaga: Dodanie nowego pola do klasy bazowej, nie czyni jej nieakceptowalną z punktu widzenia klasy pochodnej i nie wymaga ponownej kompilacji klasy pochodnej.
Inicjowanie
Zainicjowanie polega na wykonaniu inicjatorów klasy i opracowaniu inicjatorów zmiennych w celu przypisania wartości zmiennym klasy.
Uwaga: Zanim to nastąpi jest wykonywane weryfikacja, przygotowanie i zainicjowanie nadklasy danej klasy.
Na przykład
class Point {
int x, y;
static int color = 0x00ff0000;
static {
System.out.println("Point initialized");
}
Point()
{
x = y = -1;
}
}
class Point3d extends Point {
static {
System.out.println("Point3d initialized");
}
int z;
}
public
class Master {
public static void main(String args[])
{
Point3d spacePoint;
spacePoint = new Point3d();
// ...
}
}
Przebieg wykonania podanego programu od momentu podjęcia wykonywania funkcji main jest następujący
Wykonanie instrukcji
Point3d spacePoint;
powoduje utworzenie zmiennej spacePoint, ale nie wymaga załadowania klasy Point3d, ponieważ klasa ta nie jest jeszcze aktywnie użyta.
Opracowanie operacji
new Point3d()
wymaga załadowania klasy Point3d.
W ramach ładowania klasy Point3d jest wykonywana jej weryfikacja, przygotowanie i łączenie, z czego wynika, że przed zainicjowaniem klasy Point3d należy załadować klasę Point.
W ramach przygotowania klasy Point, zmiennej color jest przypisywana wartość 0.
Ponieważ w ciele konstruktora klasy Point domniemywa się instrukcję
super()
więc następnie jest ładowana aktywnie użyta klasa Object (nadklasa klasy Point).
Po załadowaniu i zainicjowaniu klasy Object jest inicjowana zmienna color. Powoduje to przypisanie jej wartości 0x00ff0000.
Następnie jest wykonywany inicjator klasy Point, co powoduje wyprowadzenie napisu
Point initialized
Po zainicjowaniu klasy Point jest inicjowana klas Point3d. Powoduje to
wyprowadzenie napisu
Point3d initialized
Tworzenie obiektów
Obiekt klasy można utworzyć na trzy sposoby:
Za pomocą operacji new (określając w nim konstruktor, jaki ma być użyty do zainicjowania zmiennych obiektu), na przykład
new Vector()
Za pomocą metody metody fabrykującej (oryg. factory methods) clone, na przykład
(Vector)vector.clone()
Za pomocą metody fabrykującej newInstance, na przykład
(Vector)Class.forName("Vector").newInstance()
Uwaga: Wywołanie metody newInstance powoduje utworzenie takiego samego obiektu jaki tworzy bezparametrowy konstruktor klasy.
Ponieważ w odróżnieniu od C++, nie istnieją w Javie inicjatory konstruktorowe, takie jak w deklaracji
Point point(10, 10)
ani nie istnieje możliwość tworzenia obiektów za pomocą jawnych wywołań konstruktorów, na przykład
Point point = Point(20, 20);
więc następujący program w C++
#include <iostream.h>
#include <math.h>
class Point {
private:
int x, y;
public:
Point(int x, int y) : x(x), y(y)
{
}
double fromOrigin(void)
{
return sqrt(x * x + y * y);
}
};
int main(void)
{
Point pointOne(10, 10),
pointTwo = Point(20, 20);
cout << pointOne.fromOrigin() << ' '
<< pointTwo.fromOrigin() << endl;
return 0;
}
przybiera w Javie postać
public
class Point {
private int x, y;
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
double fromOrigin()
{
return Math.sqrt(x * x + y * y);
}
}
public
class Compute {
public static void main(String args[])
{
Point pointOne = new Point(10, 10),
pointTwo = new Point(20, 20);
System.out.println(pointOne.fromOrigin() + " " +
pointTwo.fromOrigin());
}
}
Niszczenie obiektów
W Javie zarządzanie pamięcią sterty jest całkowicie automatyczne, a obiekty do których nie ma odniesień są niejawnie niszczone i zwracane do sterty. Z tego powodu jest w Javie zbędny operator delete oraz destruktory.
Obiekt klasy jest niszczony automatycznie, ale nie wcześniej niż w chwili, gdy ani jednemu odnośnikowi nie jest już przypisane odniesienie do tego obiektu. Takie odzyskiwanie nieużytków (oryg. garbage collection) realizuje metoda gc klasy Runtime. Kiedy i przez jaki wątek zostanie wywołana - nie wiadomo. Jednak z pewnością będzie wywołana wówczas, gdy zaistnieje konieczność odzyskania nie używanej już pamięci.
Metoda finalize
Tuż przed zniszczeniem obiektu jest na jego rzecz niejawnie wywoływana metoda finalize jego klasy. Jeśli w ciele tej funkcji odnośnik this zostanie skopiowany do innego odnośnika, to zniszczenie obiektu nastąpi później, ale nie wcześniej niż w chwili, gdy żadnemu odnośnikowi nie będzie już przypisane odniesienie do tego obiektu (w takim wypadku nie będzie już ponownie wywołana metoda finalize).
class Point {
static Point reAnimate;
// ...
public void finalize()
{
reAnimate = this;
System.out.println("Point reanimated");
}
}
public
class Master {
public static void main(String args[])
{
Point point = new Point(10, 10);
point = null;
// ...
}
}
Po wykonaniu instrukcji
point = null;
nie istnieje ani jedno odniesienie do obiektu utworzonego za pomocą operacji new. Dlatego w dowolnej chwili może być podjęte zniszczenie tego obiektu.
Tuż przed zniszczeniem obiektu zostanie wywołana metoda finalize, która go wskrzesi (oryg. reanimates). Metoda ta nie będzie już wywołana ponownie na rzecz tego samego obiektu.
Należy zwrócić uwagę, że metoda finalize nie jest destruktorem. Dlatego jej rolę należy ograniczyć do zwrócenia zasobów systemowych innych niż pamięć (np. ikon, kursorów, pędzli, itp.) przydzielonych na rzecz obiektu jej klasy.
Uwaga: Dobrym zwyczajem jest umieszczenie w ciele metody finalize wywołania
super.finalize()
stanowiącego wywołanie metody finalize jej nadklasy.
Biorąc pod uwagę brak destruktorów, następującą klasę C++
class Number : public BaseClass {
private:
long *pNumber;
public:
Number(long num) : pNumber(new long(num))
{
}
long getNumber(void)
{
return *pNumber;
}
~Number(void)
{
BaseClass::~BaseClass();
delete pNumber;
}
};
należy w Javie zapisać w postaci
public
class Number extends BaseClass {
private Long number;
public Number(long num)
{
number = new Long(num);
}
long getNumber()
{
return number.longValue();
}
public void finalize()
{
super.finalize();
}
}
Uzyskiwanie dostępu
Klasa może być publiczna albo pakietowa. Składniki klasy (pola, zmienne, konstruktory, funkcje i metody) mogą być publiczne, chronione, prywatne i pakietowe.
Naruszenie dostępności (oryg. accessibility) jest wykrywane statycznie i dynamicznie. Jego objawem podczas wykonywania programu jest wysłanie wyjątku klasy IllegalAccessException.
Uwaga: W odróżnieniu od C++, w Javie nie ma specyfikatora zaprzyjaźnienia friend. Klasy i składniki zaprzyjaźnione z daną klasą umieszcza się zazwyczaj w obrębie tego samego pakietu.
Klasa publiczna jest deklarowana ze specyfikatorem public. Klasa publiczna jest dostępna wszędzie, to jest z każdego pakietu.
Klasa pakietowa jest deklarowana bez podania specyfikatora dostępu. Klasa pakietowa jest dostępna tylko w obrębie pakietu, w którym ją zadeklarowano.
public
class OpenToAll { // klasa publiczna
// ..
}
class OpenToSome { // klasa pakietowa
// ...
}
Składnik publiczny deklaruje się ze specyfikatorem public. Jako publiczny jest on dostępny wszędzie.
Składnik chroniony deklaruje się ze specyfikatorem protected. Jest on dostępny tylko w obrębie jego pakietu oraz tylko w jego podklasie (nawet jeśli podklasa należy do innego pakietu).
Składnik prywatny deklaruje się ze specyfikatorem private. Jest on dostępny tylko w obrębie jego klasy.
Składnik pakietowy deklaruje się bez podania specyfikatora dostępu. Jest on dostępny tylko w obrębie jego pakietu.
Uwaga: Jeśli składnik jest chroniony, to z innego pakietu można uzyskać do niego dostęp tylko przez obiekt klasy podklasy zdefiniowanej w tym pakiecie.
abstract
class SomeClass {
abstract public void one(); // metoda publiczna
protected SomeClass() // konstruktor chroniony
{
}
private static final
double Pi = 3.14; // zmienna prywatna
static double getPi() // funkcja pakietowa
{
return Pi;
}
// ...
}
Wywoływanie metod
Metody mogą być wywoływane na rzecz obiektów identyfikowanych przez odnośniki obiektowe i interfejsowe. Podczas kompilowania programu sprawdza się, czy w klasie odnośnika istnieje metoda o odpowiedniej sygnaturze, a następnie generuje się B-kod wywołania metody. Podczas wykonania programu bierze się pod uwagę jedynie odniesienie do obiektu przypisane odnośnikowi, a następnie wywołuje metodę zdefiniowaną w klasie tego obiektu. Ponieważ wszystkie wywołania metod w Javie są domyślnie polimorficzne, metoda taka jest zazwyczaj różna od metody należącej do klasy odnośnika.
Wywołanie obiektowe
Jeśli Klasa jest identyfikatorem klasy, to
Klasa Nazwa
jest deklaracją odnośnika Nazwa, któremu można przypisać odniesienie do obiektu tej klasy albo jej podklasy.
W zakresie takiej deklaracji, wywołanie
Nazwa.Metoda(Arg, Arg, ... , Arg)
w którym Metoda jest dowolną metodą widoczną w klasie Klasa powoduje wywołanie metody pochodzącej z klasy tego obiektu, odniesienie do którego przypisano odnośnikowi Nazwa (a więc niekoniecznie wywołanie metody widocznej w klasie Klasa).
public
class Master {
public static void main(String args[])
{
Shape myObject = new Circle(10, 10, 30);
double area = myObject.getArea();
// ...
}
}
abstract
class Shape {
private int x, y;
// ...
double getArea()
{
return 0;
}
}
class Circle extends Shape {
static final double Pi = Math.PI;
private int radius;
// ...
double getArea()
{
return Pi * radius * radius;
}
}
Ponieważ odnośnikowi myObject przypisano odniesienie do obiektu klasy Circle, więc wywołanie
myObject.getArea()
dotyczy metody klasy Circle, a nie metody klasy Shape.
Wywołanie interfejsowe
Jeśli Interfejs jest identyfikatorem interfejsu, to
Interfejs Nazwa
jest deklaracją odnośnika Nazwa, któremu można przypisać odniesienie do obiektu dowolnej klasy implementującej ten interfejs.
W zakresie takiej deklaracji, wywołanie
Nazwa.Metoda(Arg, Arg, ... , Arg)
w którym Metoda jest metodą klasy implementującej Interfejs, powoduje wywołanie metody Metoda, widocznej w klasie obiektu identyfikowanego przez odniesienie przypisane odnośnikowi Nazwa.
public
class Master {
public static void main(String args[])
{
Colorable myObject = new Circle(10, 10, 30);
myObject.setColor(Colorable.myBlue);
// ...
}
}
interface Colorable {
static Color myBlue = Color.blue; // zmienna
void setColor(Color color); // metoda
// ...
}
class Circle extends Shape implements Colorable {
// ...
Color color;
public void setColor(Color color) // przedefiniowanie
{
this.Color = color;
}
}
Ponieważ odnośnikowi myObject przypisano odniesienie do obiektu klasy Circle, więc wywołanie
myObject.setColor(Colorable.myBlue)
dotyczy metody setColor klasy Circle.
Definiowanie klas
Zdefiniowano własną klasę StringC (oraz klasę iteracyjną StringEnumerator) implementowaną na wzór predefiniowanej klasy String, w której na wzór C++, łańcuch jest reprezentowany przez ciąg zakończony znakiem '\u0000' ( o kodzie 0).
public
class StringC {
private char arr[];
private int len;
public StringC(String str) // utwórz StringC ze String
{
len = str.length();
arr = new char [len+1];
for(int i = 0; i < len ; i++)
arr[i] = str.charAt(i);
arr[len] = '\u0000';
}
public StringC(StringC str) // utwórz StringC ze StringC
{
len = str.length();
arr = new char [len+1];
for(int i = 0; i < len+1 ; i++)
arr[i] = str.arr[i];
}
int length()
{
return len;
}
char charAt(int idx)
{
return arr[idx];
}
public String toString() // utwórz String ze StringC
{
StringBuffer buf = new StringBuffer(len);
buf.insert(0, arr);
return buf.toString();
}
int compareTo(StringC str) // uszereguj
{
return toString().compareTo(str.toString());
}
public boolean equals(Object obj) // porównaj
{
if(obj != null && obj instanceof StringC) {
StringC str = (StringC)obj;
if(len == str.len) {
for(int i = 0; i < len ; i++)
if(arr[i] != str.arr[i])
return false;
return true;
}
}
return false;
}
public final synchronized Enumeration elements()
{
return new StringEnumerator(this);
}
}
final
class StringEnumerator implements Enumeration {
private StringC string;
private int pos = 0;
StringEnumerator(StringC str)
{
string = str;
}
public boolean hasMoreElements()
{
if(pos < string.length())
return true;
pos = 0;
return false;
}
public Object nextElement()
{
synchronized(string) {
if(hasMoreElements())
return new Character(string.charAt(pos++));
}
throw new NoSuchElementException(
"StringEnumerator"
);
}
}
Podane klasy mogą być użyte na przykład w następującym programie
import java.util.*;
public
class Master {
public static void main(String args[])
{
StringC hello = new StringC("Hello");
StringEnumerator strEnum =
new StringEnumerator(hello);
while(strEnum.hasMoreElements()) {
Object next = strEnum.nextElement();
Character chr = (Character)next;
System.out.print(chr.charValue());
}
System.out.println();
}
}
Wykonanie podanego programu powoduje wyprowadzenie napisu
Hello
Projektowanie kolekcji
Zdefiniowano kolekcję Shapes (oraz klasę iteracyjną ShapesEnumerator), umożliwiającą przechowywanie dowolnej liczby obiektów klas implementujących interfejs Measurable.
public
interface Measurable {
double getArea(); // wyznaczenie powierzchni
String kindOf(); // dostarczenie nazwy
}
public
class Shapes implements Cloneable {
private int noOfSlots = 1; // liczba miejsc
private int freeSlot = 0; // pierwsze wolne miejsce
Measurable vec[]; // odnośniki do obiektów
public Shapes()
{
vec = new Measurable [noOfSlots];
}
public int getFreeSlot()
{
return freeSlot;
}
int getNoOfSlots()
{
return noOfSlots;
}
Measurable getAt(int pos)
{
return vec[pos];
}
int addShape(Measurable shape) // dodaj kształt
{
if(freeSlot >= noOfSlots) {
Measurable vecOld[] = vec;
int noOfSlotsOld = noOfSlots;
noOfSlots <<= 1; // podwój pojemność
vec = new Measurable [noOfSlots];
System.arraycopy(vecOld, 0,
vec, 0, vecOld.length);
}
vec[freeSlot] = shape;
return freeSlot++;
}
Measurable remShape(int pos) // usuń kształt
throws IndexOutOfBoundsException
{
Measurable shape = vec[pos];
vec[pos] = null;
return shape;
}
double getArea() // zsumuj pola
{
double total = 0;
for(int i = 0; i < freeSlot ; i++)
if(vec[i] != null)
total += vec[i].getArea();
return total;
}
void showAll() // pokaż zawartość
{
for(int i = 0; i < freeSlot ; i++) {
Shape shape = (Shape)vec[i];
if(shape != null)
System.out.println(
"\t" + shape.kindOf() +
" At(" +
shape.getX() +
"," +
shape.getY() +
")"
);
}
}
public final synchronized Enumeration elements()
{
return new ShapesEnumerator(this);
}
}
public
class ShapesEnumerator implements Enumeration {
private Shapes shapes;
private int pos = 0;
public ShapesEnumerator(Shapes shapes)
{
this.shapes = shapes;
}
public boolean hasMoreElements()
{
if(pos < shapes.getFreeSlot())
return true;
pos = 0;
return false;
}
public Object nextElement()
{
if(hasMoreElements())
return shapes.getAt(pos++);
throw new NoSuchElementException(
"ShapesEnumerator"
);
}
}
Podane klasy mogą być użyte na przykład w następującym programie
public abstract
class Shape implements Measurable {
private int x = 0, y = 0; // współrzędne środka
Shape()
{
}
Shape(int x, int y)
{
this.x = x;
this.y = y;
}
int getX()
{
return x;
}
int getY()
{
return y;
}
public abstract double getArea(); // powierzchnia
public String kindOf() // nazwa obiektu
{
return getClass().toString();
}
}
public
class Circle extends Shape {
public static final double Pi = Math.PI;
private int radius;
public Circle(int x, int y, int radius)
{
super(x, y);
this.radius = radius;
}
public String kindOf()
{
return "Circle";
}
public double getArea()
{
return Pi * radius * radius;
}
}
public
class Square extends Shape {
private int side;
public Square(int x, int y, int side)
{
super(x, y);
this.side = side;
}
public String kindOf()
{
return "Square";
}
public double getArea()
{
return (double)side * side;
}
}
public
class Master {
public static void main(String args[])
{
Circle aCircle = new Circle(10, 10, 1), // aC
bCircle = new Circle(20, 20, 2), // bC
cCircle = new Circle(30, 30, 3); // cC
Square aSquare = new Square(40, 40, 5), // aS
bSquare = new Square(50, 50, 5); // bS
Shapes shapes = new Shapes();
int pos;
// obiekty w kolekcji
// ==========
shapes.addShape(aCircle); // aC
pos = shapes.addShape(bCircle); // aC, bC
shapes.remShape(pos); // aC
pos = shapes.addShape(cCircle); // aC, cC
shapes.addShape(aSquare); // aC, cC, aS
shapes.remShape(pos); // aC, aS
pos = shapes.addShape(bSquare); // aC, aS, bS
shapes.remShape(pos); // aC, aS
System.out.println("Shapes in collection:\n");
shapes.showAll();
System.out.println("\nTotal area: " + shapes.getArea());
System.out.println("\nCleaning the collection:\n");
ShapesEnumerator shapesEnum =
new ShapesEnumerator(shapes);
pos = 0;
while(shapesEnum.hasMoreElements()) {
Shape shape = (Shape)shapesEnum.nextElement();
if(shape != null)
System.out.println(
"\tDeleting " +
shape.kindOf() +
", Area: " +
shape.getArea()
);
shapes.remShape(pos++);
}
System.out.println("\nDone!");
}
}
Wykonanie programu powoduje wyprowadzenie napisu
Shapes in collection:
Circle At(10,10)
Square At(40,40)
Total area: 28.1416
Cleaning the collection:
Deleting Circle, Area: 3.14159
Deleting Square, Area: 25
Done!
Wykonywanie obcych programów
Na wykonanie programu może składać się także wykonywanie programów obcych. Czynność tę można zrealizować za pomocą metody exec należącej do klasy Runtime.
Klasa Runtime zawiera szereg procedur związanych z platformą wykonania programu. Ich wywoływanie odbywa się na rzecz obiektu dostarczonego przez funkcję getRuntime.
public static Runtime getRunTime()
Metoda getRunTime dostarcza odnośnik do obiektu reprezentującego platformę wykonania programu.
public long freeMemory()
Metoda freeMemory dostarcza informacji o rozmiarze wolnej do wykorzystania pamięci RAM.
public long totalMemory()
Metoda totalMemory dostarcza informacji o całkowitym rozmiarze pamięci RAM.
public void runFinalization()
Wywołanie metody runFinalization powoduje wykonanie metod finalize wszystkich klas programu.
public Process exec(String cmd)
Wywołanie metody exec powoduje utworzenie i wykonanie odrębnego procesu, w którym wykona się polecenie systemowe wyrażone przez argument.
Wykonanie następującego programu powoduje wydanie zewnętrznego polecenia dir. O jego wykonalności i rezultatach decyduje Administrator Systemu.
public
class Master {
public static void main(String args[])
{
Runtime runTime = Runtime.getRuntime();
String cmd = "dir c:\\*.exe"; // dir c:\*.exe
Process task = null;
try {
task = runTime.exec(cmd); // IOException
task.waitFor(); // InterruptedException
}
catch(Exception e) {
System.out.println(
"Error while executing " + cmd
);
System.exit(0);
}
int exitCode = task.exitValue();
System.out.println("Exit code = " + exitCode);
}
}
_________________________________________________________________________________________
Połączenia
Celem połączenia (oryg. connection) jest zapoznanie się z właściwościami albo z treścią zasobu opisanego przez lokalizator URL (oryg. Uniform Resource Locator).
W ogólnym wypadku, parametrami lokalizatora są: nazwa protokołu komunikacyjnego, najczęściej HTTP (oryg. Hypertext Transfer Protocol), nazwa komputera-gospodarza (oryg. host), numer portu (dla HTTP zwyczajowo 80) oraz nazwa pliku, na przykład
http://www.microsoft.com:80/html
Otwarcie połączenia odbywa się na rzecz obiektu lokalizatora (oryg. URL object) zainicjowanego takimi właśnie parametrami.
public final
class URL {
public URL(String protocol, String host,
int port, String file)
public URL(String protocol, String host,
String file);
// ...
URLConnection openConnection() throws IOException;
public final InputStream openStream()
throws IOEXception;
// ...
}
Następujący program wyświetla zawartość pliku znajdującego się w miejscu określonym przez lokalizator.
import java.io.*;
import java.net.*;
public
class Master {
public static void main(String args[])
throws MalformedURLException, IOException
{
String protocol = args[0],
host = args[1],
file = args[2];
URL fileURL = new URL(protocol, host, file);
URLConnection link = fileURL.openConnection();
StringBuffer fileData = new StringBuffer();
if(link.getContentLength() > 0) {
InputStream inp = fileURL.openStream();
DataInputStream text=
new DataInputStream(
new BufferedInputStream(inp)
);
String oneLine;
while((oneLine = text.readLine()) != null)
System.out.println(oneLine);
inp.close();
}
}
}
Jeśli podany program zostanie wywołany na przykład z argumentami
htpp www.yahoo.com index.html
to w wypadku zezwolenia na dostęp do podanego pliku, nastąpi ujawnienie jego zawartości.
Zasoby lokalne
W celu zapoznania się z zasobem lokalnym, na przykład obrazem zawartym w pliku, należy użyć protokołu file, na przykład
file://c:/Cafe/Projects/Tests/Bubbles.gif
W szczególności następujący fragment kodu umożliwia wyświetlenie zawartości pliku w formacie GIF znajdującego się katalogu c:\Cafe\Project systemu Windows 95.
void drawGifFile(Graphics gDC, String name)
{
URL fileURL;
try {
fileURL = new URL("file://c:/Cafe/Project/" + name);
}
catch(MalformedURLException e) {
// ...
}
Image gifImage = getImage(fileURL);
gDC.drawImage(gifImage);
}
Protokoły
Przesłanie danych w sieci wymaga użycia protokołu (oryg. protocol). Jednym z wielu jest protokół internetowy IP (oryg. Internet Protocol). Przesłanie za pomocą protokołu IP polega na ujęciu danych w paczki (oryg. packet) i wysłaniu ich w postaci datagramów (oryg. datagram) pod podany adres IP (jednoznacznie identyfikujący komputer w Internecie) oraz port.
Następujący program wyprowadza adres IP podanego komputera.
import java.io.*;
import java.net.*;
public
class Master {
public static void main(String args[])
throws UnknownHostException
{
InetAddress addr = InetAddress.getByName(args[0]);
System.out.println("The address is: " + addr);
}
}
Datagram
Analogicznie do wysłania zwykłego listu, który wcale nie musi dotrzeć do odbiorcy, oparty na protokole IP protokół datagramowy UDP (oryg. User Data Protocol) nie gwarantuje ani doręczenia, ani żadnej kolejności dostarczenia paczek. Mimo tych wad, ale ze względu na jego prostotę, protokół UDP ma jednak wiele zastosowań.
TCP/IP
Gwarancję przesłania paczek bez ich utraty oraz w oczekiwanej kolejności zapewnia protokół kontrolujący TCP (oryg. Transmission Control Protocol). Typowymi aplikacjami posługującymi się protokołem TCP/IP są Telnet i FTP (oryg. File Transfer Protocol).
Gniazdo
Ważnym składnikiem sesji (oryg. session) między parą komputerów-gospodarzy komunikujących się za sobą za pomocą protokołu TCP/IP są gniazda (oryg. socket); jedno w komputerze dostawcy (oryg. server) i jedno w komputerze odbiorcy (oryg. client).
Uwaga: Określenie gniazdo jest użyteczną abstrakcją reprezentującą punkt połączeniowy w sieci TCP/IP (podobnie jak gniazdo elektryczne jest punktem podłączenia domowych urządzeń elektrycznych).
Jeśli ma wystąpić komunikacja między parą komputerów, to odbywa się poprzez gniazda. Pomiędzy gniazdami może występować niepewna, ale szybka komunikacja datagramowa, albo wolniejsza, ale gwarantowana komunikacja kontrolowana.
Dostawca
Komputer-dostawca czeka na zgłoszenie się klienta. Po nawiązaniu połączenia, między dostawcą a odbiorcą odbywa się wymiana danych.
Następujący program ilustruje zasadę działania komputera-dostawcy.
import java.io.*;
import java.net.*;
public
class Server {
public static void main(String args[])
throws IOException
{
int stackDepth = 5;
int serverPortNo = 80;
Socket sock;
ServerSocket srvSock =
new ServerSocket(serverPortNo, stackDepth);
while(true) {
sock = srvSock.accept();
PrintStream out =
new PrintStream(sock.getOutputStream());
InputStream clt = sock.getInputStream();
DataInputStream inp = new DataInputStream(clt);
out.println("You are connected!\n" +
"Your e-mail address please.");
String reply = inp.readLine();
out.println("Thank you.\n" +
"That is all for today.");
sock.close();
}
}
}
Odbiorca
Komputer-odbiorca zgłasza się do ujawnionego mu portu dostawcy. Po nawiązaniu połączenia, między dostawcą a odbiorcą odbywa się wymiana danych.
Następujący program ilustruje zasadę działania komputera-odbiorcy komunikującego się uprzednio podanym dostawcą.
import java.io.*;
import java.net.*;
public
class Client {
public static void main(String args[])
throws IOException
{
String workStationName = "CANIBAL";
int serverPortNo = 80;
Socket sock = new Socket(workStationName, serverPortNo);
DataInputStream inp =
new DataInputStream(sock.getInputStream());
PrintStream out =
new PrintStream(sock.getOutputStream());
String oneLine = inp.readLine();
System.out.println(oneLine);
out.println("jbl@ii.pw.edu.pl");
System.out.println(oneLine);
out.flush();
sock.close();
}
}
_________________________________________________________________________________________
Właściwości
Informacje o właściwościach otoczenia (oryg. environment properties) można uzyskać za pomocą funkcji getProperties klasy System. Zmianę właściwości na czas bieżącego wykonania programu można uzyskać za pomocą funkcji setProperties.
Uwaga: Właściwości mogą być przechowywane w pliku i ładowane do obiektu klasy Properties za pomocą metody load tej klasy.
W chwili aktywowania Maszyny Wirtualnej ustawia się 15 standardowych właściwości wymienionych w Tabeli Właściwości.
Tabela Właściwości
###
java.version wersja
java.vendor sprzedawca
java.vendor.url adres sprzedawcy
java.home (*) katalog domowy Javy
java.class.version wersja API
java.class.path (*) wartość parametru CLASSPATH
os.name nazwa systemu operacyjnego
os.arch architektura komputera
file.separator separator nazw katalogów (np. \ albo /)
path.separator separator ścieżek (np. ; albo :)
line.separator koniec wiersza (np. \n albo \r\n)
user.name (*) nazwa użytkownika
user.home (*) katalog domowy użytkownika
user.dir (*) katalog bieżący użytkownika
Uwaga: Napis (*) wyróżnia te właściwości, które nie są dostępne dla apletu.
###
Poza właściwościami standardowymi istnieje szereg właściwości pomocniczych, a wśród nich
awt.appletWarning ostrzeżenie wyświetlane w oknach utworzonych przez applet
(domyślnie: Warning: Applet Window).
awt.font.Nazwa nazwa czcionki
W szczególności, jeśli program tworzy czcionkę, to jej nazwa Name jest przekształcana w nazwę zapisaną małymi literami, a następnie uzupełniana z przodu napisem awt.font.
Następnie System sprawdza czy zdefiniowano właściwość Nazwa.awt.font i przyjmuje, że chodzi o utworzenie czcionki o nazwie określonej przez tę właściwość.
import java.awt.*;
import java.util.*;
public
class Master {
static {
addFontProperties("JanB_Font", "TimesRoman");
}
public static void main(String args[])
{
Font myFont = new Font("JanB_Font", Font.BOLD, 24);
Frame frame = new Frame("Greeting");
frame.show();
Graphics gDC = frame.getGraphics();
gDC.setFont(myFont);
gDC.drawString("Hello in JanB_Font", 20, 20);
}
static void addFontProperties(String myName, String theName)
{
Properties props = new Properties();
String fontName = "awt.font." + myName;
props.put(fontName, theName); // klucz, wartość
System.setProperties(props);
System.getProperties().list(System.out);
}
}
Program wykreśla napis
Hello in JanB_Font
24-punktową, pogrubioną czcionką TimesRoman.
_________________________________________________________________________________________
Implementacje
Dobrzy programiści stają się bardzo dobrymi programistami przez studiowanie kodu doskonałych programistów. A jeśli poświęcą kodowaniu dostatecznie wiele czasu, to i oni staną się doskonałymi programistami.
Mając to na względzie przytoczono uproszczone fragmenty definicji kilku klas opracowanych przez doskonałych programistów firmy Sun. Zapoznanie się z ich oryginałami stanowi wypróbowany sposób na lepsze poznanie Javy.
Uwaga: Pakiety JavaBasePlatform (java.lang, java.awt, java.io, java.net, java.util) są dostępne w postaci źródłowej. Można do nich dotrzeć sięgając do
http://sunsite.icm.edu.pl/java-corner
Klasa String
public
class String {
private char value[]; // tablica znaków
private int offset; // pierwszy użyty
private int count; // licznik
public String()
{
value = new char[0];
}
public String(char array[])
{
count = array.length;
value = new char[count];
System.arraycopy(array, 0, value, 0, count);
}
public String(StringBuffer buffer)
{
value = buffer.getValue();
offset = 0;
count = buffer.length();
}
public int length()
{
return count;
}
public char charAt(int index)
{
if((index < 0) || (index >= count))
throw new IndexOutOfBoundsException(index);
return value[index + offset];
}
public boolean equals(Object anObject)
{
if((anObject != null) && (anObject instanceof String)) {
String string = (String)anObject;
int n = count;
if(n == string.count) {
char v1[] = value;
char v2[] = string.value;;
int i = offset;
int j = string.offset;
while (n-- != 0)
if(v1[i++] != v2[j++])
return false;
return true;
}
}
return false;
}
public int compareTo(String string) {
int len1 = count;
int len2 = string.count;
int n = Math.min(len1, len2);
char v1[] = value;
char v2[] = string.value;
int i = offset;
int j = string.offset;
while (n-- != 0) {
char c1 = v1[i++];
char c2 = v2[j++];
if(c1 != c2)
return c1 - c2;
}
return len1 - len2;
}
public static String valueOf(Object obj)
{
return (obj == null) ? "null" : obj.toString();
}
public static String valueOf(boolean b)
{
return b ? "true" : "false";
}
public static String valueOf(char c)
{
char data[] = { c };
return new String(data);
}
public void getChars(int srcBegin, int srcEnd,
char dst[], int dstBegin)
{
System.arraycopy(value, offset + srcBegin,
dst, dstBegin, srcEnd - srcBegin);
// ...
}
Klasa StringBuffer
public
class StringBuffer {
private char value[]; // tablica znaków
private int count; // licznik
public StringBuffer()
{
this(16);
}
public StringBuffer(int length)
{
value = new char[length];
}
public StringBuffer(String str)
{
this(str.length() + 16);
append(str);
}
public int length()
{
return count;
}
public int capacity()
{
return value.length;
}
public void ensureCapacity(int minCapacity)
{
int maxCapacity = value.length;
if(minCapacity > maxCapacity) {
int newCapacity = (maxCapacity + 1) * 2;
if(minCapacity > newCapacity)
newCapacity = minCapacity;
char newValue[] = new char[newCapacity];
System.arraycopy(value, 0, newValue, 0, count);
value = newValue;
}
}
public void setLength(int newLength)
{
if(newLength < 0)
throw new IndexOutOfBoundsException();
ensureCapacity(newLength);
for( ; count < newLength ; count++)
value[count] = '\0';
count = newLength;
}
public char charAt(int index)
{
if((index < 0) || (index >= count))
throw new IndexOutOfBoundsException();
return value[index];
}
public void getChars(int srcBegin, int srcEnd,
char dst[], int dstBegin)
{
if((srcBegin < 0) || (srcBegin >= count))
throw new IndexOutOfBoundsException();
if((srcEnd < 0) || (srcEnd > count))
throw new IndexOutOfBoundsException();
if(srcBegin < srcEnd)
System.arraycopy(value, srcBegin, dst,
dstBegin, srcEnd - srcBegin);
}
public void setCharAt(int index, char ch)
{
if((index < 0) || (index >= count))
throw new IndexOutOfBoundsException();
value[index] = ch;
}
public StringBuffer append(Object obj)
{
return append(String.valueOf(obj));
}
public StringBuffer append(String str)
{
if(str == null)
str = String.valueOf(str);
int len = str.length();
ensureCapacity(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
public StringBuffer append(char c)
{
ensureCapacity(count + 1);
value[count++] = c;
return this;
}
public StringBuffer insert(int offset, Object obj)
{
return insert(offset, String.valueOf(obj));
}
public StringBuffer insert(int offset, String str)
{
if((offset < 0) || (offset > count))
throw new IndexOutOfBoundsException();
int len = str.length();
ensureCapacity(count + len);
System.arraycopy(value, offset, value,
offset + len, count - offset);
str.getChars(0, len, value, offset);
count += len;
return this;
}
public StringBuffer insert(int offset, char c)
{
ensureCapacity(count + 1);
System.arraycopy(value, offset,
value, offset + 1, count - offset);
value[offset] = c;
count += 1;
return this;
}
public String toString()
{
return new String(this);
}
char[] getValue() // wyłącznie dla klasy String!
{
return value;
}
}
Klasa Vector
class Vector implements Cloneable {
protected Object elementData[];
protected int elementCount;
protected int capacityIncrement;
public Vector(int initialCapacity,
int capacityIncrement)
{
super();
elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
public Vector(int initialCapacity Ż