7. Wyjątki, strumienie i wątki
7.1 Wyjątki
Dobry program = program odporny na błędy.
Konflikt odporność/czytelność
Wyjątki (exceptions) - elegancki sposób sprawdzania poprawności.
Element Javy, w którym pojawi się błąd, zgłasza wyjątek (throws exception).
Zgłoszony wyjątek musi być wychwycony (catched) na poziomie zgłoszenia lub wyższym. Obsługa wychwyconego wyjątku może obejmować komunikat o błędzie i ewentualne kroki zaradcze.
Wyjątki są obiektami klasy Throwable, zawartej w podstawowym pakiecie java.lang, lub obiektami jej klas potomnych (patrz schemat).
Klasa Error odpowiada wewnętrznym błędom w Wirtualnej Maszynie Javy. Błędy tego typu są rzadkie i mają fatalne skutki.
Wyjątki z klasy Exception są kontrolowane przez kompilator, który sprawdza, czy metoda zgłasza tylko te wyjątki, które zostały wymienione w jej deklaracji (nagłówku).
Wyjątki z klasy RunTimeException wskazują na błędy, które nie zostały wykryte przez kompilator. Np. NullPointerException sygnalizuje odwołanie do nieistniejącego obiektu.
Duża klasa IOException obejmuje sytuacje szczególne występujące podczas operacji wejścia-wyjścia. Np. NumberFormatException sygnalizuje niewłaściwą postać liczby, a EOFException - nieoczekiwane natknięcie się na koniec pliku.
AWTException obejmuje wyjątki związane z graficzną powłoką programu (okienka).
ClassNotFoundException pojawia się wtedy, gdy użyjemy błędnej nazwy klasy, lub mamy kłopoty z dostępem do niej.
W razie potrzeby programista może definiować własne wyjątki jako podklasy klasy Exception.
Fragment hierarchii wyjątków:
Wyjątkami z klas Error i RuntimeException zajmuje się system.
Pozostałe wyjątki muszą być uwzględnione w programie w następujący sposób:
a) zgłaszanie wyjątku -
thrownew < nazwa wyjątku>;
b) wychwytywanie i obsługa wyjątku -
try { < instrukcja > }
catch ( < typ wyjątku > < nazwa wyjątku > ) {
… // kod obsługujący wyjątek
}
[ catch ( < typ wyjątku > < nazwa wyjątku > ) {
… // kod obsługujący wyjątek
}]
…
[ finally {
… // kod wykonywany zawsze
}]
Każda metoda może zadeklarować chęć zgłaszania wyjątku:
void mojaMetoda ( ) throws mójWyjątek {
… // kod metody
throw new mójWyjatek ( );
…
}
Metoda nadrzędna może przechwytywać wyjątek zgłaszany przez metodę podrzędną -
void mojaSuperMetoda ( ) {
…
try { mojaMetoda ( ); }
catch ( mójWyjatek mw ) {
… // kod obsługujący wyjątek
}
…
}
lub przekazywać go wyżej -
void mojaSuperMetoda ( ) throws mójWyjatek {
…
mojaMetoda ( );
…
}
Aplikacja Dzielenie
class Dzielenie {
static int x, y;
… // nagłówek
static int dziel_1 ( int x, int y ) {
return x / y;
}
static int dziel_2 ( int x, int y ) throws przezZero {
if ( y == 0)
throw new przezZero ( );
else
return x / y;
}
public static void main ( String [ ] args )
{
nagłówek ( );
System.out.println ( dziel_1 ( 8, 4 ) );
System.out.println ( "-------------------");
try {
System.out.println ( dziel_2 ( 8, 0 ) ); }
catch ( przezZero pz ) { }
}
}
class przezZero extends Exception {
przezZero ( ) {
System.out.println ( "Dzielenie przez zero" );
}
}
7.2 Strumienie wejścia-wyjścia
Ze względu na to, że Java ma być niezależna od sprzętu, zadbano w niej o staranne oddzielenie opisu procedur wejścia/wyjścia od ich realizacji sprzętowej. Służą temu abstrakcyjne pojęcia strumieni wejścia/wyjścia:
W tym schemacie wczytanie danych z dowolnego sekwencyjnego źródła polega na:
otwarciu strumienia wejściowego;
wywołaniu metody read ( ) dla tego strumienia.
Analogicznie, zapisanie danych na dowolny sekwencyjny nośnik polega na:
otwarciu strumienia wyjściowego;
wywołaniu metody typu write ( ) dla tego strumienia.
Strumienie we/wy zawarte są w pakiecie java.io . Ograniczymy się do omówienia elementów tego pakietu operujących znakami, ponieważ elementy operujące na bajtach są analogiczne.
7.2.1 Strumienie wejścia
Klasą bazową do definiowania strumienia wejściowego dla znaków jest Reader. Klasa ta dostarcza metodę read ( ) pozwalającą wczytać znak. Metoda read ( ) zwraca wartość typu int, więc trzeba dokonać jawnej konwersji na typ znakowy.
Zwykle zamiast ogólnej klasy Reader stosowane są jej klasy potomne przystosowane do źródeł danych i zwiększające efektywność wczytywania:
CharArrayReader |
− pobiera dane z tablicy znaków |
FileReader |
− pobiera dane z pliku dyskowego |
BufferedReader |
− zwiększa szybkość wczytywania |
StringReader |
− pobiera dane z łańcucha znaków |
itp. |
|
Definicje strumieni można zagnieżdżać podobnie jak filtry w Unix'ie:
Reader r = new Strumień3 ( new Strumień2
( new Strumień1 ( źródło )));
Często stosowanym filtrem jest DataInputStream, który udostępnia metody bezpośredniego wczytywania podstawowych typów danych:
readInt ( ) |
− wczytanie liczby całkowitej |
readDouble ( ) |
− wczytanie liczby rzeczywistej |
readChar ( ) |
− wczytanie znaku |
readLine ( ) |
− wczytanie wiersza |
itp. |
|
Dobrą zasadą jest jawne zamykanie strumienia po zakończeniu operacji we/wy. Do tego celu służy metoda close ( ).
Większość strumieni wejściowych udostępnia metody:
skip ( long n ) |
pominięcie n znaków ( zwraca liczbę faktycznie pominiętych znaków typu |
mark ( ) |
zapamiętanie aktualnej pozycji |
reset ( ) |
powrót do zapamiętanej pozycji (lub do pozycji początkowej) |
available ( ) |
sprawdzenie, czy są znaki do czytania |
ready ( ) |
sprawdzenie, czy strumień jest dostępny |
Najczęściej strumienie kojarzone są z plikami dyskowymi. Pakiet java.io zawiera klasę File, która pozwala wygodnie manipulować kartotekami i plikami. Obiekty tej klasy można tworzyć na 3 sposoby:
File f = new File ( File kartoteka, String nazwa );
File f = new File ( String ścieżka, String nazwa );
File f = new File (String ścieżka );
Jak widać, obiektem typu File może być zarówno plik jak i kartoteka. Pewien kłopot stanowi brak jednolitego separatora składowych ścieżki dostępu w różnych systemach (`/' w Unix'ie, `\' w Windows). Aby temu zaradzić, obiekt klasy File korzysta ze zmiennej file.separator klasy System.
W miarę możliwości należy unikać podawania stałych ścieżek dostępu do plików, bo narażamy się w ten sposób na błędy spowodowane zmianami w systemie plików.
Obiekty klasy File udostępniają cały szereg pożytecznych metod:
exists ( ) |
sprawdza, czy ten obiekt istnieje |
canRead ( ) |
sprawdza, czy wolno czytać ten plik |
canWrite ( ) |
sprawdza, czy wolno pisać na ten plik |
length ( ) |
zwraca długość pliku (typu long) |
renameTo ( File nazwa) |
zmienia nazwę |
delete ( ) |
kasuje |
getName ( ) |
zwraca nazwę (typu String) |
getPath ( ) |
zwraca ścieżkę (typu String) |
list ( ) |
zwraca listę plików (typu List ) |
mkdir ( ) |
tworzy kartotekę |
Właściwym strumieniem wejścia do czytania znaków z pliku dyskowego jest FileReader ( ). Zatem operację czytania z dysku przygotowujemy w taki sposób:
File f = new File ( ”dysk\ścieżka\plik” );
Reader r = new FileReader ( f );
lub w skrócie
Reader r = new FileReader ( new File ( ”dysk\ścieżka\plik”));
Standardowym strumieniem wejścia jest System.in. Odpowiada mu klawiatura komputera.
7.2.2. Strumienie wyjścia
Klasą bazową do definiowania strumienia wyjściowego dla znaków jest Writer. Klasa ta dostarcza metodę write ( ) pozwalającą zapisać znak.
Zwykle zamiast ogólnej klasy Writer stosowane są jej klasy potomne przystosowane do źródeł danych i zwiększające efektywność wczytywania:
CharArrayWriter |
− zapisuje dane do tablicy znaków |
FileWriter |
− zapisuje dane do pliku dyskowego |
BufferedWriter |
− zwiększa szybkość zapisywania |
StringWriter |
− zapisuje dane do łańcucha znaków |
itp. |
|
Definicje strumieni można zagnieżdżać podobnie jak filtry w Unix'ie:
Writer r = new Strumień3 ( new Strumień2
( new Strumień1 ( źródło )));
Często stosowanym filtrem jest DataOutputStream, który udostępnia metody bezpośredniego wczytywania podstawowych typów danych:
writeInt ( ) |
− zapisanie liczby całkowitej |
writeDouble ( ) |
− zapisanie liczby rzeczywistej |
writeChar ( ) |
− zapisanie znaku |
writeLine ( ) |
− zapisanie wiersza |
itp. |
|
Właściwym strumieniem wyjścia do zapisywania znaków do pliku dyskowego jest FileWriter ( ). Zatem operację zapisywania na dysk przygotowujemy w taki sposób:
File f = new File ( ”dysk\ścieżka\plik” );
Writer r = new FileWriter ( f );
lub w skrócie
Writer r = new FileWriter ( new File ( ”dysk\ścieżka\plik”));
Standardowym strumieniem wyjścia jest System.out. Odpowiada mu okno MS-DOS w systemie Windows, lub okno terminala w systemie Unix czy Linux. Za pomocą metody print ( ) możemy wyświetlić w tym oknie znak lub napis, a metoda println ( ) kończy wyświetlanie przesunięciem do nowego wiersza.
Aplikacja We_wy
import java.io.*;
class We_wy {
static final int MAX = 10;
static final char [ ] znakiStale =
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
static void drukujZnaki ( char [ ] znaki ) {
System.out.println ( );
for ( int i = 0; i < MAX; i++ ) {
System.out.print ( " " + znaki [ i ] );
}
System.out.println ( );
}
static void kopiujZnaki ( char [ ] oryginal, char [ ] kopia ) {
CharArrayReader r = new CharArrayReader ( oryginal );
try { for ( int i = 0; i < MAX; i++ )
kopia [ i ] = ( char ) r.read ( );
}
catch ( IOException e ) {
System.out.println ( " Blad w czytaniu znakow" );
}
}
static void kopiujCzescZnakow ( char [ ] oryginal, char [ ] kopia,
long n ) {
CharArrayReader r = new CharArrayReader ( oryginal );
long sk;
try { if ( r.skip ( n ) != n ) {
System.out.println ( " Zabraklo znakow" );
return; }
}
catch ( IOException e ) {
System.out.println ( " Blad w pomijaniu znakow" );
}
try { for ( int i = ( int ) n ; i < MAX; i++ )
kopia [ i ] = ( char ) r.read ( );
}
catch ( IOException e ) {
System.out.println ( " Blad w czytaniu znakow" );
}
}
static void powielCzescZnakow ( char [ ] oryginal, char [ ] kopia,
long n, int razy ) {
CharArrayReader r = new CharArrayReader ( oryginal );
long sk;
try { if ( r.skip ( n ) != n ) {
System.out.println ( " Zabraklo znakow " );
return;
}
r.mark ( MAX ); // argument pomijany
for ( int k = 0; k < razy; k++ ) {
for ( int i = 0; i < MAX- ( int ) n ; i++ )
kopia [ (MAX - ( int ) n ) * k + i ] =
( char ) r.read ( );
r.reset ( );
}
}
catch ( IOException e ) {
System.out.println ( " Blad w czytaniu znakow" );
}
}
public static void main ( String args [ ] ) {
char [ ] znaki = new char [MAX];
drukujZnaki ( znakiStale );
kopiujZnaki ( znakiStale, znaki );
drukujZnaki ( znaki );
char [ ] znaki1 = new char [MAX];
long n = 4;
kopiujCzescZnakow ( znakiStale, znaki1 , n );
drukujZnaki ( znaki1 );
char [ ] znaki2 = new char [MAX];
powielCzescZnakow ( znakiStale, znaki2 , 6l , 2 );
drukujZnaki ( znaki2 );
}
}
Aplikacja Plik
import java.io.*;
import java.awt.*;
class Plik {
public static void main ( String args [ ] ) {
BufferedReader we = null;
BufferedWriter wy = null;
PrintWriter wy1 = null;
File kartoteka = new File ( "." ); // bieżąca kartoteka
String [ ] pliki = new String [ 100]; // lista plików
pliki = kartoteka.list ( ); // wczytujemy zawartość
for ( int i = 0; i < 10; i++ ) // bieżącej kartoteki
System.out.println ( pliki [ i ] ); // i wyświetlamy ją
try { we = new BufferedReader // otwieramy pierwszy
( new FileReader ( pliki [ 0 ] ) ); // plik do
} // czytania
catch ( IOException e ) {
System.out.println (" Brak pliku " + pliki [ 0 ] ); }
String s = "pusty";
try { s = we.readLine ( ); // wczytujemy wiersz
System.out.println ("s = " + s ); // i wyswietlamy go
}
catch ( IOException e ) {
System.out.println (" Brak zawartosci" );
}
try { we.close ( ); } // zamykamy strumień
catch ( IOException e ) {
System.out.println (" Nie moge zamknąć " );
}
// otwieramy plik testowy
File testowy = new File ( ".", "testowy.txt" );
try { wy1 = new PrintWriter
( new FileWriter ( testowy ) );
}
catch ( IOException e ) {
System.out.println (" Nie mogę otworzyć pliku " + testowy );
}
for ( int i = 0; i < 10; i++ )
wy1.println ( "to jest test" ); // zapisujemy 10 razy
wy1.close ( ); // i zamykamy strumien
}
}
Aplikacja Wiek
import java.io.*;
class Wiek {
String imie;
int wiek;
Wiek ( String imie, int wiek ) {
this.imie = imie;
this.wiek = wiek;
}
public static String czytajLancuch ( ) {
char c;
StringBuffer b = new StringBuffer ( );
System.out.print(" > "); // znak zachęty
try {
while (( c = (char) System.in.read ( )) != '\n' )
b.append ( c );
}
catch (java.io.IOException e) { }
return b.toString ( );
}
public static int czytajLiczbeCalkowita ( ) {
String s = czytajLancuch ( );
int i;
s = s.substring ( 0,( s.length ( ) - 1 ));
try { i = Integer.parseInt ( s ); }
catch ( NumberFormatException e ) {
System.out.println ( "To nie jest liczba calkowita" );
i = 0;
}
return i;
}
static boolean sprawdzImie ( String imie ) throws
BladImienia {
char c = '0';
Character ch = new Character ( c );
for ( int i = 0; i < imie.length()-1; i++ ) {
c = imie.charAt ( i );
if ( ! ch.isLetter ( c )) throw new BladImienia ( c );
}
return true;
}
static String czytajImie ( ) {
String imie = "puste";
boolean ok = false;
while ( ! ok ) {
System.out.print( '\n' + " Imie " );
imie = czytajLancuch ( );
try { ok = sprawdzImie ( imie ); }
catch ( BladImienia e ) { }
}
return imie;
}
static boolean sprawdzWiek ( int wiek ) throws BladWieku {
if ( wiek < 0 ) throw new BladWieku ( );
else if ( wiek == 0 ) return false;
else return true;
}
int czytajWiek ( ) {
int wiek = 0;
boolean ok = false;
while ( ! ok ) {
System.out.print( '\n' + " Wiek " );
wiek = czytajLiczbeCalkowita ( );
try { ok = sprawdzWiek ( wiek ); }
catch ( BladWieku e ) { }
}
return wiek; }
void pisz ( ) {
System.out.println ( '\n' + " Dane osobowe:");
System.out.println ( " Imie - " + imie );
System.out.println ( " Wiek - " + wiek + '\n');
}
static boolean akceptuj( ) {
System.out.print ( '\n' + " Akceptujesz? ( t/n ) " );
String tn = czytajLancuch ( );
if ( tn.charAt ( 0 ) == 't' ) return true;
else return false;
}
public static void main ( String args [ ] ) {
boolean ok = false;
Wiek osoba = new Wiek ( "Jan", 25 );
while ( !ok ) {
osoba.imie = osoba.czytajImie ( );
osoba.wiek = osoba.czytajWiek ( );
osoba.pisz ( );
ok = akceptuj ( );
}
}
}
//------------------------------------------------------------
class BladImienia extends Exception {
BladImienia ( char c ) {
System.out.println("Znak " + c + " nie jest litera");
}
}
//------------------------------------------------------------
class BladWieku extends Exception {
BladWieku ( ) {
System.out.println("Wiek nie moze byc ujemny");
}
}
7.3. Tworzenie równoległych wątków
Współczesne systemy operacyjne pozwalają uruchamiać równolegle kilka zadań (multitasking), np. edytor tekstu i odtwarzacz płyty kompaktowej. Podobne działanie wewnątrz zadania nazywa się przetwarzaniem wielowątkowym (multithreading). Często stosuje się je w graficznych formach komunikacji z użytkownikiem.
Tworzenie programów wielowątkowych w Javie jest łatwe. Można to robić na dwa sposoby:
rozszerzając klasę Thread;
korzystając z interfejsu Runnable.
Nowy wątek tworzymy jako obiekt klasy Thread:
Thread wątek = new Thread ( );
Uruchamiamy go za pomocą metody start ( ), a zatrzymujemy - za pomocą metody stop ( ).
Uruchomiony wątek wykonuje metodę run ( ). Implementacja tej metody w klasie Thread jest pusta, należy ją zatem przesłonić własną metodą.
Wątek można usypiać za pomocą metody sleep ( long time ). Argumentem tej metody jest czas uśpienia w milisekundach.
Alternatywnym sposobem uruchamiania wątków jest wykorzytanie interfejsu Runnable. Interfejs ten zawiera nagłówek jedynej metody
public void run ( );
Obiekty typu Runnable można przekazywać jako argumenty konstruktora klasy Thread:
new Thread ( Runnable object );
Utworzony w ten sposób obiekt posiada własny wątek i wykonuje metode run ( ) obiektu object.
Zastosowanie obu wersji uruchamiania wątków pokażemy na przykładzie aplikacji, która uruchamia 2 wątki: jeden z nich wyświetla co pewien czas napis "ping", drugi - napis "PONG".
Aplikacja Ping1
class Ping1 extends Thread {
String word; // jakie słowo wypisać
int delay; // jak długo czekać
static boolean stop = false; // kiedy skończyc
Ping1 ( String whatToSay, int delayTime ) {
word = whatToSay;
delay = delayTime;
}
public void run ( ) {
try {
while ( !stop ) {
System.out.print ( word + " " );
sleep ( delay ); // czekaj
}
}
catch ( InterruptedException e ) {
return; // zakończ ten wątek
}
}
public static void main ( String [ ] args ) {
new Ping1 ( "ping", 33 ).start ( );
new Ping1 ( "PONG", 100 ).start ( );
Stop st = new Stop ( );
st.start ( );
stop = st.czytajX ( );
}
}
//------------------------------------------------
import java.io.*;
class Stop extends Thread {
boolean stop = false;
public void run ( ) {
if ( stop ) return;
}
boolean czytajX ( ) {
char zn = '0';
try {
zn = (char)System.in.read ( );
}
catch ( IOException e ) { }
if ( zn == 'x' ) return true;
else return false;
}
public static void main ( String [ ] args ) {
Stop st = new Stop ( );
st.start ( );
st.stop = st.czytajX ( );
}
}
Aplikacja Ping2
class Ping2 implements Runnable {
String word; // jakie słowo wypisać
int delay; // jak długo czekać
static boolean stop = false; // kiedy skończyc
Ping2 ( String whatToSay, int delayTime ) {
word = whatToSay;
delay = delayTime;
}
public void run ( ) {
try {
…
Thread.sleep ( delay ); // czekaj
…
}
public static void main ( String [ ] args ) {
Runnable ping = new Ping2 ( "ping", 33 );
Runnable pong = new Ping2 ( "PONG", 100 );
new Thread ( ping ).start ( );
new Thread ( pong ).start ( );
Stop st = new Stop ( );
st.start ( );
stop = st.czytajX ( );
}
}
Adam Borkowski Język programowania „Java” 7-1
Adam Borkowski Język programowania „Java” 7-24
Adam Borkowski Język programowania „Java” 7-3
EOFException
NumberFormatException
RuntimeException
ClassNotFound
Exception
AWTException
IOException
Exception
Error
Throwable
• • •
• • •
• • •
• • •
strumień
wejścia
przetwa-rzanie
strumień
wyjścia
wyświetlanie
formularza
odbiór
danych
wyświetla-nie formularza
odbiór danych