Java po C++


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 Stringniemodyfikowalne, a więc, jeśli rezultat pewnej operacji jest klasy String, to nie musi być odrębnym obiektem.

Obiekty klasy StringBuffermodyfikowalne. 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 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/IPTelnet 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/IPgniazda (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 Ż


Wyszukiwarka

Podobne podstrony:
Po prostu Java 2 ppjav2
Po prostu Java 2
Po prostu Java 2
Po prostu Java 2 ppjav2
Wprowadzenie do jezyka Java Przewodnik krok po kroku
Po prostu Java 2 2
PO wyk07 v1
Rehabilitacja po endoprotezoplastyce stawu biodrowego
Systemy walutowe po II wojnie światowej
HTZ po 65 roku życia
Zaburzenia wodno elektrolitowe po przedawkowaniu alkoholu
Organy po TL 2
Metoda z wyboru usprawniania pacjentów po udarach mózgu
03Operacje bankowe po rednicz ce 1
Piramida zdrowia po niemiecku

więcej podobnych podstron