Programowanie Obiektowe (Java) Wyk ad siódmy ł 1. Wyliczenia
Transkrypt
Programowanie Obiektowe (Java) Wyk ad siódmy ł 1. Wyliczenia
Programowanie Obiektowe (Java) Wykład siódmy 1. Wyliczenia (tylko java 5 !!!) Język Java do wersji piątej nie posiadał konstrukcji wyliczenia znanej z języków C i C++. Programiści radzili sobie z tą niedogodnością definiując stałe w interfejsach. To rozwiązanie nie zastępuje jednak w pełni wyliczeń znanych z wcześniej wymienionych języków programowania i posiadały wiele wad (niemożność sprawdzenia typu, niemożność bezpośredniego wypisania na ekran nazwy elementu, możliwość kolizji nazw, itd.). Firma Sun postanowiła więc dodać w wersji piątej języka konstrukcję, która w pełni odpowiada wyliczeniom z innych języków oprogramowania i dodatkowo posiada pewne cechy obiektowości. W swej najprostszej postaci typy wyliczeniowe definiuje się podobnie jak w językach C/C++: public class Wyliczenia { private enum Zodiac {CAPRICORN, AQUARIUS, PISCES, ARIES, TAURUS, GEMINI, CANCER, LEO, VIRGO, LIBRA, SCORPIO, SAGITTARIUS} public static void main(String[] args) { for(Zodiac z: Zodiac.values()) System.out.println(z); } } Definicja typu może być poprzedzona modyfikatorem dostępu. Nazwy elementów zazwyczaj są pisane dużymi literami. W funkcji main() przykładowego programu wypisano na ekran nazwy wszystkich elementów. Warto zwrócić uwagę, że aby wypisać na ekran element wyliczenia wystarczy napisać nazwę zmiennej, która go przechowuje. W przykładzie zastosowano również nową postać pętli for, która odpowiada pętli foreach znanej z wielu języków skryptowych. Pętli tej można używać do 1 obsługi wyliczeń i kontenerów . Pętlę for w wyżej zamieszczonym programie można przeczytać w następujący sposób: „Dla każdego elementu należącego do wyliczenia wypisz jego nazwę na ekran”. Ogólna postać tej pętli dla typów wyliczeniowych jest następująca: for(TypWyliczeniowy zmienna: TypWyliczeniowy.values()) intrukcja; public class Wyliczenia2 { Jak już wspomniano wcześniej wyliczenie w Javie ma cechy obiektowości, tzn. każdy element wyliczenia może posiadać własny stan i zachowanie. Stan takiego elementu jest przechowywany w polach, natomiast za zachowanie odpowiedzialne są metody. Ilustruje to program, którego kod został umieszczony obok. W przykładzie zdefiniowano wyliczenie, którego elementami są samochody. Każdy samochód oprócz marki posiada także swój typ i ten typ jest zapamiętywany w polu type. Atrybut ten jest public enum Samochody { VOLKSWAGES("Polo"), PORSCHE("Carrera"), MERCEDES("SLR"); private final String type; Samochody(String type) { this.type = type; } public String getType() { inicjalizowany za pomocą konstruktora z parametrem. Konstruktor jest definiowany w ten sam sposób jak w przypadku klas, tzn. jest to metoda, która nazywa się tak samo jak typ wyliczeniowy i nie zwraca żadnej wartości. W typie wyliczeniowym została zdefiniowana również metoda getType(), która zwraca łańcuch określający typ return type; } } public static void main(String[] args) { for(Samochody s: Samochody.values()) danego samochodu (czyli zawartość pola type). System.out.println("Marka: "+s+" typ: "+s.getType()); Warto zwrócić uwagę na wykorzystanie słowa kluczowego this w konstruktorze do rozróżnienia } } pomiędzy nazwą pola i parametrem. 1 Kontenery są obiektami przechowującymi inne obiekty i będą tematem następnych wykładów. 1 Programowanie Obiektowe (Java) Możemy również określić różne zachowanie dla każdego z elementów wyliczenia przeciążając metodę abstrakcyjną wspólną dla całego wyliczenia. Tę technikę ilustruje poniższy przykład: Metodą abstrakcyjną jest metoda print(), która zostaje public class Wyliczenia3 { przeciążona dla każdego elementu wyliczenia osobno. W ten sposób dla każdego z tych elementów działa ona inaczej. public enum Wyliczanka { ENE {void print() {System.out.println("Ene");}}, DUE {void print() {System.out.println("Due");}}, RABE {void print() {System.out.println("Rabe");}}; abstract void print(); } public static void main(String[] args) { for(Wyliczanka w: Wyliczanka.values()) w.print(); } } 2. Wyjątki Mechanizm wyjątków pozwala na obsługę błędów czasu wykonania. Takie błędy nie są wykrywane na etapie kompilacji, gdyż nie są błędami składniowymi, a wynikającymi z dostarczenia aplikacji błędnych danych. Mechanizm wyjątków języka Java pochodzi z języka Ada, na którym wzorowana jest również obsługa wyjątków w C++ i Object Pascal'u (Delphi). W Javie ten mechanizm został rozbudowany. Wyjątki pozwalają na określenie zachowania aplikacji w momencie napotkania problemów z przetworzeniem danych lub błędów logicznych w programie. Mechanizm ten jest wyjątkowo elastyczny. Jeśli nie można prawidłowo zareagować na sytuację wyjątkową (obsłużyć wyjątek) w miejscu gdzie ona wystąpiła, to można ten wyjątek przesłać do wyższego kontekstu (np.: poza metodę w której wystąpił), gdzie prawdopodobnie będzie można sobie z nim poradzić. Wyjątki w Javie są niczym innym jak obiektami klas wyjątków. Klasą bazową dla wszystkich tych klas jest klasa o nazwie 2 Exception . Jest ona klasą najbardziej ogólną. Klasy które po niej dziedziczą są klasami specjalizowanymi. Mechanizm dziedziczenia pozwala na tworzenie własnych wyjątków. Załóżmy, że podczas wykonywania metody powstała sytuacja prowadząca do błędu. Kod, który wykryje taką sytuację może stworzyć i wyrzucić odpowiedni wyjątek informujący o tym błędzie, przerywając równocześnie wykonanie metody. Oto schemat takiego kodu: if(warunek) throw new Exception(); W powyższym schemacie zamiast wyjątku klasy Exception można wyrzucić obiekt dowolnej innej klasy dziedziczącej po Exception (a nawet obiekt klasy Throwable), na przykład NullPointerException. Każda klasa wyjątku ma co najmniej dwa konstruktory: konstruktor domyślny i konstruktor, który jako parametr pobiera łańcuch opisujący sytuację, jaka spowodowała wyjątek. Jeśli nie chcemy, aby wyrzucenie wyjątku powodowało zakończenie wykonywania metody możemy zamknąć kod, w którym może powstać wyjątek w obszarze chronionym (bloku prób): try { //Kod mogący spowodować powstanie wyjątku} Za tym blokiem umiejscawiamy procedury obsługi wyjątków. Te procedury mają postać bloków catch: catch(KlasaWyjątku w) { //Kod obsługi wyjątku} i może ich występować kilka, w zależności od tego jakie wyjątki mogą powstawać w kodzie zamkniętym w bloku try. Nie mogą się jednak one znajdować w dowolnym porządku. Jako pierwsze powinny być umieszczone w kodzie procedury obsługi najbardziej specjalizowanych wyjątków, a jako ostatnie procedury obsługujące najbardziej ogólne wyjątki. Przy odwrotnym uszeregowaniu każdy wyjątek będzie przechwytywany przez procedurę obsługi wyjątku najbardziej ogólnego, ze względu na dziedziczenie klas wyjątków (co zresztą zostanie wykryte przez kompilator i zgłoszone jako błąd). Są dwa schematy według 2 Klasa Exception dziedziczy po klasie Throwable, ale przyjmuje się ją za klasę podstawową dla wszystkich innych klas wyjątków. 2 Programowanie Obiektowe (Java) których działają takie procedury. Pierwszy polega na zakończeniu wykonania fragmentu kodu gdzie powstał wyjątek, a drugi na wznowieniu tego fragmentu po wcześniejszej próbie usunięcia domniemanej przyczyny błędu. Obiekt wyjątku przechowuje informacje na temat miejsca, gdzie powstał wyjątek i jego przyczyny. Można je wypisać na ekranie używając metod 3 printStackTrace() lub zapisać do strumienia używając jej wersji przeciążonej. Do uzyskania informacji o wyjątku możemy użyć także metody toString() jak również metod getMessage() i getLocalizedMessage(). Wszystkie one zwracają obiekt klasy String zawierający opis wyjątku. Zalecane jest by opis ten był wyświetlany nie przy pomocy strumienia out skojarzonego ze standardowym wyjściem, ale przy pomocy strumienia err, skojarzonego z wyjściem diagnostycznym. W procedurach obsługi wyjątków można po przechwyceniu wyjątku ponownie go wyrzucić, przy pomocy słowa kluczowego throw. Jednak w takiej sytuacji obiekt tego wyjątku będzie zawierał informacje z miejsca gdzie po raz pierwszy został wyrzucony, a nie z miejsca ponownego wyrzucenia. Nowy opis miejsca wyrzucenia wyjątku możemy umieścić w jego obiekcie wywołując metodę fillInStackTrace(). Za ciągiem procedur obsługi wyjątków możemy umieścić blok finally, który jest tworzony według następującego wzorca: finally { //Kod dla sekcji finally} Kod w sekcji finally wykonywany jest zawsze, niezależnie od tego, czy wyjątek wystąpił, czy też nie. Najczęściej umieszczane są w nich instrukcje, które muszą być wykonane w sposób niezawodny, jak np.: zamknięcie pliku. Jeśli te instrukcje mogą również spowodować wyjątki, to te wyjątki muszą być obsłużone wewnątrz bloku finally. Jeśli w danej metodzie nie możemy obsłużyć powstającego w niej wyjątku, bo nie mamy do tego wystarczającej ilości niezbędnych informacji, to możemy ten wyjątek wyrzucić poza metodę. Aby to uczynić musimy poinformować kompilator jakie wyjątki dana metoda wyrzuca, co robimy dodając je do listy wyrzucanych przez metodę wyjątków, która tworzona jest w nagłówku metody, według następującego schematu: TypWartościZwracanej nazwaMetody(lista argumetów) throws KlasaWyjątku1, KlasaWyjątku2 { //Kod metody} Jeśli metoda powoduje wyjątki, które nie są umieszczone na liście, to kompilator zgłosi błąd, lecz jeśli ta lista zawiera wyjątki, które nie są przez nią wyrzucane to taka sytuacja jest akceptowalna i służy przygotowaniu metody do wyrzucania wyjątków, które mogą pojawić się w jej przyszłych wersjach. Lista wyjątków nazywana prawidłowo specyfikacją wyjątków nie jest częścią metody i nie może służyć do jej przeciążania. Specyfikacja może być również pominięta w metodach przykrytych w klasach potomnych, lub może być zawężona. W klasach pochodnych nie można jednak rozszerzać specyfikacji wyjątków 4 dziedziczonych metod. Ostatnie stwierdzenie nie dotyczy konstruktorów , które mogą zgłaszać dowolne wyjątki. Jedynym wymogiem nakładanym na nie jest to, aby uwzględniały wyjątki konstruktorów klas bazowych. Ponieważ konstruktory odpowiedzialne są za tworzenie obiektów obsługa wyjątków w nich musi być starannie przemyślana. Należy również pamiętać, że zawsze pierwszą instrukcją wykonywaną przez konstruktor jest wywołanie konstruktora klasy bazowej i nie jest możliwe obsłużenie jego wyjątków - trzeba je zadeklarować jako wyrzucane przez konstruktor klasy pochodnej. Warunki wystąpienia wyjątków klasy RunTimeExcpetion i pochodnych są w języku Java domyślnie sprawdzane. Są to wyjątki najczęściej powodowane błędami programisty lub błędami których nie możemy uniknąć. Należą do nich wyjątki spowodowane przez niezainicjalizowane referencje (o wartości null) lub wyjątki spowodowane przekroczeniem zakresu tablicy. Tych wyjątków nie trzeba podawać w specyfikacjach metod. Jeśli nie będziemy ich przechwytywać, to dotrą one do metody main() i przed wyjściem z programu zostanie dla nich wywołana metoda printStackTrace(). Wystąpienie tych wyjątków sugeruje programiście, że w jego programie są błędy logiczne, które powinien usunąć. Oto przykład, który ilustruje użycie wyjątków: 3 4 Strumienie bd przedmiotem innych wykładów. Ponieważ one nie podlegają polimorfizmowi. 3 Programowanie Obiektowe (Java) W programie została zdefiniowana klasa nowego wyjątku o nazwie MyException. Konstuktory class MyException extends Exception { MyException() { System.err.println("Powstanie wyjątku."); tej klasy zostały tak napisane, aby w momencie tworzenia obiektu wyjątku informowały o tym użytkownika programu. Metoda test z klasy Tester zgłasza dwa } MyException(String s) { super(s); System.err.println("Powstanie wyjątku."); wyjątki, które następnie są obsługiwane w metodzie main(). Działanie obsługi } } wyjątków możemy przetestować uruchamiając program i podając jako jego argumenty wejściowe liczby całkowite, takie np. 1 i 3 lub 5 i 5. Podanie dwóch takich samych liczb, różnych od 5 nie spowoduje wyrzucenia przez metodę test wyjątków. class Tester { void test(int x, int y) throws Exception, MyException { if(x!=y) throw new MyException("Nierówne wartości argumentów!"); if(x==5) throw new Exception("X równe 5!"); } } Wyjątki zostaną również wyrzucone kiedy podamy na wejście programu liczby rzeczywiste lub kiedy nie podamy żadnych wartości. W ostatnim wypadku powstanie wyjątek klasy RunTimeException. public class Errors { public static void main(String[] args) { Tester t = new Tester(); try { t.test(Integer.parseInt(args[0]), Integer.parseInt(args[1])); } catch(MyException e) { Na zakończenie należy zaznaczyć, że opisy wyjątków, jakie wyrzucają standardowe metody, jak również opisy standardowych klas wyjątków znajdują się w dokumentacji języka Java. Omawiany na wcześniejszych wykładach program javadoc pozwala tworzyć do- System.err.println(e.getLocalizedMessage()); e.printStackTrace(); } catch(Exception e) { System.err.println(e.getMessage()); e.printStackTrace(); } finally { System.out.println("To jest zawsze wykonywane."); } kumentację do wyjątków definiowanych przez programistę oraz umieszczać odwołania w do- } } kumentacji HTML, do opisów wyjątków wyrzucanych przez poszczególne metody. 4 Programowanie Obiektowe (Java) 3. Asercje Asercje zostały dodane do języka Java w wersji 1.4. Pozwalają one programiście przeprowadzić testy mające na celu sprawdzenie, czy w trakcie pisania programu nie powstały w kodzie błędy logiczne, które powodują niezgodność jego działania z przyjętymi algorytmami. Innymi słowy asercje pozwalają stwierdzić, czy program działa wedle przyjętych założeń. Po zakończeniu testów mechanizm asercji można wyłączyć i program nie będzie ich uwzględniał. Asercje (nazywane też niezmien nikami) tworzymy według następujących wzorców. assert warunek; lub assert warunek : "Łańcuch opisujący błąd”; Łańcuch w drugim wyrażeniu może mieć bardziej skomplikowaną postać. Wyrażenie powoduje powstanie wyjątku, jeśli warunek nie jest spełniony (jest fałszywy). Aby asercje były uwzględniane należy uruchomić maszynę wirtualną z opcją 5 -enableassertions lub -ea, czyli np.: java -ea Program . Oto przykład zastosowania asercji: public class Asercje { Pierwsza asercja powoduje wyrzucenia wyjątku, jeśli dwie liczby stanowiące argumenty wywołania programu są sobie równe, druga, jeśli pierwsza z tych liczb jest równa pięć. Pominięcie w wywołaniu programu flagi -ea spowo- public static void main(String[] args) { if(args.length==2) { int a = Integer.parseInt(args[0]); duje również pominięcie w działaniu asercji. int x = Integer.parseInt(args[1]); System.out.println("a: "+a+" x: "+x); assert a==x; assert x==5:"Asercja nie jest spełniona! x = "+x; } } } Mechanizmem asercji można również sterować z poziomu programu korzystając z metod statycznych klasy ClassLoader. class Operation { public void compare(int b, int z) { assert b==z; Poniżej przedstawiony jest przykład pozwalający włączyć asercje dla ładowanej klasy. Inne sposoby programowego sterowania asercjami są opisane w dokumentacji klasy ClassLoader: assert b==5:"Asercja nie jest spełniona! x = "+z; } } public class Asercje2 { public static void main(String[] args) { if(args.length==2) { int a = Integer.parseInt(args[0]); int x = Integer.parseInt(args[1]); System.out.println("a: "+a+" x: "+x); ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true); new Operation().compare(a,x); } } } 5 W przypadku środowiska w wersji 1.4 należy skompilować program z opcją -source 1.4 w wersji 1.5 nie jest to wymagane. 5