Wychwytywanie błędnych zachowań programu - asercje , wyjątki.
Pojęcia :
serwer - klasa udostępniająca jakąś usługę
klient - korzystający z usług serwera ( klasa, aplikacja, człowiek).
bład - fizyczny błąd występujący w oprogramowaniu ( error)
błędne wykonanie ( zachowanie) - objaw występowania błędu ( fault ).
Rodzaje błędów :
syntaktyczne - składniowe
logiczne
błędne zachowania klienta, sytuacje awaryjne w otoczeniu serwera.
Sposoby sygnalizacji błędnych zachowań :
wydruki kontrolne
kody powrotu z metod
zgłaszanie wyjątków
asercje
Wyjątki :
Wyjątki - obiekty służące do przekazywania informacji diagnostycznej .
Zgłaszanie wyjątku :
void f ( )
{ .....
if ( o = = null) throw new NullPointerException( " Nie przekazano obiektu ");
....
}
Postać instrukcji throw :
throw new TypWyjatku(" opcjonalny komunikat diagnostyczny")
lub :
TypWyjatku e = new TypWyjatku("komunikat");
throw e;
Hierarchia klas ziązanych z obsługą wyjątków :
Throwable
Error
VirtualMachineError
AssertionError
Exception
RuntimeException
--- NulPointerException
--- IndexOutOfBoundsException
--- inne niekontrolowane wyjątki
IOException
inne kontrolowane wyjątki
Wyjątki klasy error sygnalizują poważne błędy systemowe, zazwyczaj nie są przechwytywane i nie powinno się wymuszać ich obsługi przez klienta.
Wyjątki klasy RuntimeException sygnalizują błędy programisty, czasami ich obsługa ma sens i wtedy je przechwytujemy, ale wymuszanie ich obsługi nie jest wskazane.
Wyjątki dziedziczące po klasie Exception sygnalizują błędne zachowanie otoczenia serwera na które serwer powinien być przygotowany, zazwyczaj ich obsługa jest możliwa i najczęściej jest wymuszana przez serwer.
Do wymuszania obsługi wyjątku służy klauzula throws.
void f ( ) throws NullPointerException
{ .....
if ( o = = null) throw new NullPointerException( " Nie przekazano obiektu ");
....
}
Jeśli mamy sensowną możliwość reakcji (np. poprawa wartości argumentu, doprecyzowanie informacji diagnostycznej itp. ) na wystąpienie wyjątku powinniśmy go przechwycić i obsłużyć - służą do tego instrukcje try ... catch.
void f ( )
{ ...
try {
// instrukcje mogące wygenerować wyjątek
} catch(Exception e) {
// obsługa wyjątku
}
...
// pozostałe instrukcje - pomijane w przypadku wystąpienia wyjątku
}
Jeśli instrukcje bloku try mogą wygenerować więcej rodzajów wyjątków to możemy użyć kilku instrukcji catch. Ponieważ sprawdzane są kolejno więc ustawiamy je od od najbardziej szczegółowego wyjątku do najbardziej ogólnego.
void f ( )
{ ...
try {
// utwórz strumień plikowy , odczytaj i zamknij go
} catch(FileNotFoundException e) {
// nie odnaleziono pliku o podanej nazwie
}
catch(IOException e ) {
// obsługa pozostałych błędów wejścia wyjścia
}
...
// pozostałe instrukcje - pomijane w przypadku wystąpienia wyjątku
}
Jeśli pewne instrukcje muszą być wykonane niezależnie od tego czy wystąpił wyjątek czy nie do bloków try catch dodajemy klauzulę finally.
void f ( )
{ ...
try {
// utwórz strumień plikowy i odczytaj go
} catch(FileNotFoundException e) {
// nie odnaleziono pliku o podanej nazwie
}
catch(IOException e ) {
// obsługa pozostałych błędów wejścia wyjścia
}
finally {
// instrukcje wykonywane zawsze np. zamknięcie pliku ( jeśli został otwarty )
}
...
// pozostałe instrukcje - pomijane w przypadku wystąpienia wyjątku
}
Jeśli nie wiemy co zrobić w przypadku wystąpienia wyjątku kontrolowanego nie powinniśmy przechwytywać go ( i dawać pustą obsługę, lub powielać systemową informację diagnostyczną - jak to się często robi ), ale przekazać go "wyżej" - do klienta ( może on będzie wiedział co z tym zrobić).
void f ( ) throws IOException
{
// instrukcje mogące wygenerować wyjątek IOException
}
Uwaga: możemy również zgłaszać nowe wyjątki w bloku catch.
Asercje :
Asercja jest to warunek logiczny sprawdzający czy stan programu ( wartości zmiennych ) jest zgodny z przyjętymi założeniami (ułatwia wykrywanie błędów logicznych programu). Stosowana przede wszystkim na etapie uruchamiania programu. Domyślnie java wyłącza sprawdzanie asercji. Włączenie wymaga ustawienia odpowiednich opcji kompilatora i loadera :
javac -source 1.4 MyClass.java
java -ea MyClass
W środowisku BlueJ trzeba ustawić opcję -ea ( i -eas jeśli chcemy objąć asercjami klasy systemowe ) dla maszyny wirtualnej :
czyli w pliku bluej.defs należy do wiersza bluej.vm.args dopisać -ea ( i ewentualnie usunąć znak komentarza '#' ), wiersz po zmianie :
bluej.vm.args=-server -Xincgc -ea -esa
Postać instrukcji assert :
assert warunek [ : wyrażenie]
jeśli warunek nie jest spełniony generowany jest wyjątek AssertionError z dodatkową informacją ( jeśli jest) przekazaną przez opcjonalne wyrażenie.
Asercje stosyjemy do sprawdzania :
inwariantów wewnętrznych
inwariantów przepływu sterowania
warunków wstępnych (pre) i końcowych ( post) dla metod
inwariantów klasowych
Nie powinno się stosować asercji :
do sprawdzania poprawności argumentów metod publicznych ( należy zgłosić wyjątek podklasy RuntimeException)
powodujących efekty uboczne ( są jednak możliwe wyjątkowe sytuacje )
np. // Broken! - action is contained in assertion
assert names.remove(null);
należy zrealizować tak :
// Fixed - action precedes assertion
boolean nullsRemoved = names.remove(null);
assert nullsRemoved; // Runs whether or not asserts are enabled
Ad. a
if (i % 3 == 0) {
...
} else if (i % 3 == 1) {
...
} else {
assert i % 3 == 2 : i;
...
}
Pozornie niemożliwa sytuacja.
Podobnie :
switch(suit) {
case Suit.CLUBS:
...
break;
case Suit.DIAMONDS:
...
break;
case Suit.HEARTS:
...
break;
case Suit.SPADES:
...
}
Przyjęliśmy założenie, że kolor karty może mieć tylko jedną z czterech wartości. Dla sprawdzenia tego możemy dodać :
default:
assert false : suit;
Inne ( nawet lepsze ) rozwiązanie to :
default:
throw new AssertionError(suit);
Ad. b
Zasada : wstawiaj asercje wszędzie tam gdzie nigdy nie powinno się dojść ( assert false).
Ad. c
Warunki wstępne ( poprawność argumentów wywołania metod ) sprawdzamy tylko dla metod prywatnych.
Warunki końcowe - sprawdzają czy efekt działania metody ( wynik i zmiana pól ) spełnia założenia.
Ad. d
Warunki jakie muszą być spełnione przez wartości pól aby obiekt był uznany za poprawny ( patrz np. Data). Powinniśmy sprawdzać w każdej metodzie modyfikującej pola.
Rekurencja.
Tym razem tylko przykłady, wprowadzenie mam w pamięci operacyjnej.
Wersja trochę niedydaktyczna - tak na prawdę jest to przykład programowania strukturalnego , a nie obiektowego.
import java.io.*;
public class Kalkulator
{StreamTokenizer wej=new StreamTokenizer(new BufferedReader(new InputStreamReader
(System.in)));
void oblicz() throws IOException
{ // ustawiamy analizator tak aby nowa linia (EOL) '/' i '-' były traktowane jako tokeny
// pod pojęciem liczby rozumiemy liczbę bez znaku
wej.eolIsSignificant(true);
wej.ordinaryChar('/');
wej.ordinaryChar('-');
System.out.println(" Wprowadz wyrażenie ");
wej.nextToken();
// każda metoda startuje z wczytanym piewrszym tokenem
System.out.println(" = " + wyrazenie());
}
double wyrazenie()throws IOException
{ double suma=0;
int oper=wej.ttype;
if(oper!='+' && oper!='-') // liczba lub wyrazenie w ( ) - czyli skladnik
{suma=skladnik(); oper=wej.ttype;}
while(oper=='+' || oper=='-')
{ wej.nextToken();
switch(oper)
{ case '+': suma+=skladnik(); break;
case '-': suma-=skladnik();
}
oper=wej.ttype;
}
return suma;
}
double skladnik() throws IOException
{double iloczyn=czynnik();
int oper=wej.ttype;
while(oper=='*' ||oper=='/')
{ wej.nextToken();
switch(oper)
{ case '*': iloczyn*=czynnik(); break;
case '/': iloczyn/=czynnik();
}
oper=wej.ttype;
}
return iloczyn;
}
double czynnik()throws IOException
{ double wart;
if(wej.ttype==wej.TT_NUMBER) wart=wej.nval;
// jeśli nie liczba to musi być wyrażenie w nawiasach
else {wej.nextToken(); wart=wyrazenie();}
wej.nextToken();
return wart;
}
}
Tym razem wersja wykorzystująca klasy wewnętrzne. Moim zdaniemwprowadzone nieco na siłę. Dodana jest również częściowa kontrola poprawności zapisu wyrażenia.
import java.io.*;
public class Kalkulator
{StreamTokenizer wej= new StreamTokenizer(new BufferedReader(new InputStreamReader
(System.in)));
void oblicz() throws IOException
{ // ustawiamy analizator tak aby nowa linia (EOL) '/' i '-' były traktowane jako tokeny
// pod pojęciem liczby rozumiemy liczbę bez znaku
wej.eolIsSignificant(true);
wej.ordinaryChar('/');
wej.ordinaryChar('-');
System.out.println(" Wprowadz wyrażenie ");
wej.nextToken();
// każda metoda startuje z wczytanym piewrszym tokenem
System.out.println(" = " + (new Wyrazenie()).dajWart());
}
class Wyrazenie
{ double lewy=0.0, // pierwszy argument i jednocześnie wynik
prawy=0.0; // drugi argument operatora dodawania
Wyrazenie() throws IOException
{ wyrazenie();}
double dajWart()
{ return lewy;}
void wyrazenie()throws IOException,RuntimeException
{ int oper=wej.ttype;
if(oper!='+' && oper!='-') // liczba lub wyrazenie w () - czyli skladnik
if(oper==wej.TT_NUMBER ||oper=='(') {lewy=(new Skladnik()).dajWart(); oper=wej.ttype;}
else throw new RuntimeException("Oczekiwany ( lub liczba ");
while(oper=='+' || oper=='-')
{ wej.nextToken();
prawy=(new Skladnik()).dajWart();
switch(oper)
{ case '+': lewy+=prawy ; break;
case '-': lewy-=prawy; break;
default : assert false : "nieoczekiwany znak "+(char) oper;
}
oper=wej.ttype;
}
}
}
class Skladnik
{ double lewy=0.0,
prawy=0.0;
Skladnik() throws IOException
{skladnik();}
double dajWart()
{return lewy;}
void skladnik() throws IOException
{ lewy=(new Czynnik()).dajWart();
int oper=wej.ttype;
while(oper=='*' ||oper=='/')
{ wej.nextToken();
prawy=(new Czynnik()).dajWart();
switch(oper)
{ case '*': lewy*=prawy; break;
case '/': lewy/=prawy; break;
default : assert false : "Nieoczekiwany znak "+ (char)oper;
}
oper=wej.ttype;
}
}
}
class Czynnik
{ double wart;
Czynnik() throws IOException
{czynnik();}
double dajWart()
{return wart;}
void czynnik()throws IOException, RuntimeException
{ if(wej.ttype==wej.TT_NUMBER) wart=wej.nval;
// jeśli nie liczba to musi być wyrażenie w nawiasach
else if(wej.ttype=='(')
{wej.nextToken(); wart=(new Wyrazenie()).dajWart();
if(wej.ttype!=')')
throw new RuntimeException("Oczekiwany ) "); }
else throw new RuntimeException("Oczekiwany ( ");
wej.nextToken();
}
}
}