Podstawy otwartych języków programowania Wyjątki i strumienie I/O Wiktor Wandachowicz
Wyjątki Podstawą filozofii Javy jest założenie, że: źle sformułowany kod nie zostanie wykonany Wyjątki są mechanizmem zabezpieczającym program podczas jego działania dynamicznie (w odróżnieniu od błędów pojawiających się podczas kompilacji przy statycznym sprawdzaniu poprawności programu). Stanowią element sięgający podstaw języka i szeroko stosowany we wszelkich bibliotekach Java. Lepiej jest przerwać wykonywanie programu, niż dopuścić do nieoczekiwanych efektów wynikających z błędu.
Typowa obsługa wyjątków Sytuacja błędna powoduje utworzenie nowego obiektu wyjątku i rzucenie go. Kod, który wyrzuca wyjątki można spróbować wykonać, a pojawiające się wyjątki można łapać. Złapany wyjątek można obsłużyć albo rzucić ponownie. Jeśli kod w metodzie rzuca wyjątek (nie jest on obsłużony), musi być to wyspecyfikowane w deklaracji metody. Można także wskazać finalne operacje wykonywane zawsze, niezależnie od tego czy wyjątek wystąpił, czy nie. Wyjątki pochodzą od klasy java.lang.throwable, jej główne podklasy to: java.lang.exception (najczęściej używana), java.lang.runtimeexception oraz java.lang.error.
Kategorie wyjątków Exception do typowej obsługi wyjątków. Tworząc własne klasy wyjątków najczęściej dziedziczy się je właśnie z tej klasy. RuntimeException oraz pochodne określają wyjątki, które mogą pojawić się podczas normalnego działania maszyny wirtualnej. Nie trzeba deklarować ich w definicjach metod, mogą pojawić się w każdej metodzie. Przykłady: NullPointerException, IndexOutOfBoundsException. Error oraz pochodne określają poważne problemy, sytuacje, które nie powinny zdarzyć się przy normalnym wykonaniu programu. Nie trzeba deklarować ich w definicjach metod. Przykłady: VirtualMachineError, AssertionError.
Łapanie wyjątków Odbywa się z użyciem bloków try i catch. Pierwszy blok (try spróbuj) obejmuje kod, który może spowodować wyjątek. Drugi blok (catch złap) określa jakie wyjątki chce obsłużyć i zawiera kod z obsługą błędów. Jeśli wyjątek został złapany, można m.in. wyświetlić informację o pochodzeniu błędu (jest to najczęściej używane): ex.printstacktrace() odczytać komunikat błędu: ex.getmessage() odczytać źródło błędu: ex.getcause()
Łapanie wyjątków przykład (1) import java.io.*; public class Imie { static void odczytajimie() { BufferedReader in = new BufferedReader( new InputStreamReader(System.in)); try { System.out.print("Podaj imie: "); blok try String imie = in.readline(); // może być wyjątek System.out.print("Witaj " + imie + "!"); catch (IOException e) { e.printstacktrace(); lokalna obsługa wyjątku public static void main(string[] args) { odczytajimie();
Łapanie wyjątków przykład (2) import java.io.*; specyfikacja wyjątku public class Imie { static void odczytajimie() throws IOException { BufferedReader in = new BufferedReader( new InputStreamReader(System.in)); System.out.print("Podaj imie: "); String imie = in.readline(); // tu może być wyjątek System.out.print("Witaj " + imie + "!"); public static void main(string[] args) { try { blok try odczytajimie(); catch (IOException e) { e.printstacktrace(); obsługa wyjątku
Rzucanie wyjątków Aby zasygnalizować błąd, należy utworzyć obiekt wyjątku (podając przyczynę błędu). Następnie należy rzucić wyjątek, z użyciem słowa kluczowego throw: long suma = 0; int iloscnieujemnych = 0; void dodajnieujemna(int liczba) throws IllegalArgumentException { if (liczba > 0) { suma += liczba; iloscnieujemnych++; else { throw new IllegalArgumentException("liczba <= 0");
Tworzenie własnych klas wyjątków Nowa klasa wyjątku powinna dziedziczyć z Exception. Może zawierać dodatkowe pola i metody. Powinna deklarować przynajmniej dwa konstruktory: bezparametrowy z jednym argumentem typu String public class ProgramException extends Exception { public ProgramException() { super(); public ProgramException(String message) { super(message);
Własne klasy wyjątków oraz wiązanie wyjątków Żeby móc używać mechanizmu wiązania wyjątków, wskazane jest dodanie jeszcze dwóch konstruktorów: z jednym argumentem typu Throwable z argumentami typu String oraz typu Throwable public class ProgramException extends Exception { public ProgramException() { super(); public ProgramException(String message) { super(message); public ProgramException(Throwable cause) { super(cause); public ProgramException(String message, Throwable cause) { super(message, cause);
Wiązanie wyjątków Wiązanie wyjątków (exception chaining) pozwala dowiedzieć się jaka była oryginalna przyczyna powstania wyjątku w przypadku, gdy wyjątek jest rzucany ponownie: void openfile() throws ProgramException { FileReader file = null; try { String filename = selectfile(); file = new FileReader(fileName); parsecontents(file); catch (IOException ex) { throw new ProgramException("Błąd odczytu pliku: " + ex.getmessage(), ex); catch (NumberFormatException ex) { throw new ProgramException("Błędny format pliku: " + ex.getmessage(), ex); finally { if (file!= null) try { file.close(); /* mamy wyjątek - zrób porządki i zamknij plik */ catch (IOException ex) { /* pominięcie ew. błędów */
Efekt wiązania wyjątków public class Program { public static void main(string[] args) { try { openfile(); catch (ProgramException ex) { ex.printstacktrace(); > java Program ProgramException: Błąd odczytu pliku: test.txt (No such file or directory) at Program.openFile(Program.java:34) at Program.main(Program.java:15) Caused by: java.io.filenotfoundexception: test.txt (No such file or directory) at java.io.fileinputstream.open(native Method) at java.io.fileinputstream.<init>(fileinputstream.java:106) at java.io.fileinputstream.<init>(fileinputstream.java:66) at java.io.filereader.<init>(filereader.java:41) at Program.openFile(Program.java:30)... 1 more
Dziedziczenie klas wyjątków Kolejność zapisu sekcji catch ma znaczenie, ponieważ przy wystąpieniu wyjątku są one przeglądane w kolejności występowania. Pierwsze dopasowanie do klasy wyjątku pomija pozostałe sekcje catch. Pojawiają się błędy kompilacji, gdy obsługa wyjątków klas bardziej ogólnych poprzedza obsługę wyjątków klas pochodnych. Kod zawarty w sekcji finally wykonywany jest zawsze, niezależnie od tego czy wystąpił wyjątek, czy nie. Służy to do czynności porządkowych, zwalniania zasobów, itp.
Podsumowanie Aby stworzyć niezawodny system, każdy jego komponent musi być niezawodny Obsługa wyjątków jest kluczowym sposobem zwiększenia niezawodności programów. Drugim sposobem jest używanie testów jednostkowych i testów modułowych (unit testing, module testing). Java wymusza wiele aspektów związanych z wyjątkami, co gwarantuje, że będą one spójnie używane zarówno przez twórców jak i użytkowników bibliotek.
Podstawy otwartych języków programowania Strumienie wejścia/wyjścia Wiktor Wandachowicz
Strumienie danych Strumień jest abstrakcyjną reprezentacją dowolnego źródła lub ujścia danych Na podstawie dwóch logicznych operacji wyróżniamy dwa typy strumieni: strumienie wejściowe (odczyt danych) strumienie wyjściowe (zapis danych) Strumień jest zawsze stowarzyszony z jakimś źródłem danych (odczyt) lub ujściem (zapis). Wyróżniamy także strumienie bajtowe i znakowe. W Javie obsługę strumieni zapewnia pakiet java.io
Strumienie wejściowe i wyjściowe strumień wejściowy źródło danych informacja odczyt Program strumień wyjściowy Program zapis informacja ujście danych
Algorytmy odczytu i zapisu Odczyt otwórz strumień jeśli jest jeszcze informacja [określane przez źródło] odczytaj informację zamknij strumień Zapis otwórz strumień jeśli jest jeszcze informacja [określane przez program] zapisz informację zamknij strumień
Hierarchia strumieni Strumienie bajtowe Strumienie znakowe Klasy strumieni są w pakiecie java.io Przy nieudanych operacjach pojawiają się wyjątki klasy java.io.ioexception Strumienie bajtowe i znakowe zapewniają praktycznie taką samą funkcjonalność (API), różnią się głównie obsługiwanymi typami danych.
Strumienie bajtowe Dwa najbardziej ogólne strumienie bajtowe to: InputStream do odczytu, oraz OutputStream do zapisu ciągów bajtów. void close() InputStream abstract int read() int read(byte[] b) int read( byte[] b, int off, int len) long skip(long n) abstract void write(int b) void write(byte[] b) void write( byte[] b, int off, int len) void flush() void close() OutputStream
Strumienie znakowe Podstawowymi strumieniami znakowymi są: Reader do odczytu, oraz Writer do zapisu ciągów znaków. int read() void close() Reader int read(char[] cbuf) abstract int read(char[] cbuf, int off, int len) long skip(long n) void write(int c) void write(char[] cbuf) abstract void write(char[] cbuf, int off, int len) void flush() void close() Writer
Rozszerzanie funkcjonalności Zwiększenie funkcjonalności odbywa się przez użycie wyspecjalizowanych strumieni. Korzystają one z innych strumieni, aby wykonywać swoją pracę. Robi się to przez opakowanie jednego typu strumienia w inny (aby to zadziałało musi istnieć w wybranej klasie strumienia odpowiedni konstruktor). InputStream int read(byte[] b) InputStreamReader int read(char[] cbuf) BufferedReader String readline()
Odczyt standardowego wejścia (Java 1.0 1.4) import java.io.*; public class PowielajLinie { public static void main(string[] args) throws IOException { System.out.println("Wpisuj linie tekstu. " + "Koniec gdy pusta linia."); BufferedReader in = new BufferedReader( new InputStreamReader(System.in)); String linia; do { linia = in.readline(); System.out.println(linia); while (linia.length() > 0); InputStream
Odczyt standardowego wejścia (Java 5, 6) import java.io.*; import java.util.scanner; public class PowielajLinie2 { public static void main(string[] args) { System.out.println("Wpisuj linie tekstu. " + "Koniec gdy pusta linia."); Scanner sc = new Scanner(System.in)); String linia; do { linia = sc.nextline(); System.out.println(linia); while (linia.length() > 0); InputStream
Odczyt i zapis plików Żeby otworzyć plik, wystarczy stworzyć obiekt wybranej klasy, podając nazwę pliku w postaci łańcucha znaków lub obiektu klasy File. FileInputStream odczyt pliku binarnego FileOutputStream zapis pliku binarnego (*) FileReader odczyt pliku znakowego FileWriter zapis pliku znakowego (*) (*) możliwe otwarcie pliku do dopisywania, bez usuwania poprzedniej zawartości Otwarty strumień można opakować w wybrany strumień wyspecjalizowany Po zakończeniu strumień należy zamknąć close()
Klasa File NIE JEST to plik służy raczej jako opis ścieżek do plików lub katalogów (klasa ta równie dobrze mogłaby nazywać się FilePath): File plik = new File("plik.txt"); FileReader in = new FileReader(plik); Przy użyciu tej klasy można także m.in. sprawdzać informacje o pliku: isdirectory(), isfile(), canread(), canwrite(), exists(), length(), lastmodified() wykonywać operacje na plikach i katalogach: delete(), mkdir(), mkdirs(), renameto() odczytywać zawartość katalogu: list(), listfiles()
Strumienie specjalizowane Zamiana strumieni bajtowych na znakowe: InputStreamReader / OutputStreamWriter Odczyt i zapis z uwzględnieniem znaków końca linii: BufferedReader readline() BufferedWriter newline() Strumień z buforem (większa wydajność): BufferedInputStream / BufferedOutputStream Przeciążone metody println(): PrintStream / PrintWriter
Strumienie specjalizowane Odczyt i zapis tablic bajtów lub znaków (w pamięci): ByteArrayInputStream / ByteArrayOutputStream CharArrayReader / CharArrayWriter Odczyt i zapis łańcuchów napisów (w pamięci): StringReader / StringWriter Odczyt i zapis plików (bajtowo i znakowo): FileInputStream / FileOutputStream FileReader / FileWriter Serializacja obiektów: ObjectInputStream / ObjectOutputStream
Klasa Osoba (JavaBean) public class Osoba { private String imie; private String nazwisko; public Osoba() { public String getimie() { return imie; public void setimie(string noweimie) { this.imie = noweimie; public String getnazwisko() { return nazwisko; public void setnazwisko(string nowenazwisko) { this.nazwisko = nowenazwisko; // opcjonalny dodatkowy konstruktor // (łatwiejsze tworzenie obiektów) public Osoba(String imie, String nazwisko) { this.imie = imie; this. nazwisko = nazwisko;
Zapis i odczyt danych w pliku import java.util.*; import java.io.*; public class ZapisOdczyt { private static List<Osoba> osoby = new ArrayList<Osoba>(); public static void main(string[] args) throws IOException { osoby.add(new Osoba("Jan", "Kowalski")); osoby.add(new Osoba("Adam", "Nowak")); zapisz(); osoby.clear(); odczytaj(); wyswietl(); private static void wyswietl() { int nr = 1; for (Osoba o : osoby) { System.out.println(" " + (nr++) + ". " + o.getimie() + " " + o.getnazwisko());
Zapis i odczyt danych w pliku private static void zapisz() throws IOException { PrintWriter plik = new PrintWriter( new BufferedWriter(new FileWriter("dane.txt"))); for (Osoba o : osoby) { plik.println(o.getimie() + ":" + o.getnazwisko()); plik.close(); private static void odczytaj() throws IOException { BufferedReader plik = new BufferedReader( new FileReader("dane.txt")); while (plik.ready()) { String s = plik.readline(); String[] dane = s.split(":"); osoby.add(new Osoba(dane[0], dane[1])); plik.close();
Serializacja Serializacja pozwala zamienić obiekt na sekwencję bajtów,, w sposób umożliwiający później wierne odtworzenie jego zawartości Inna nazwa to trwałość (ang. persistence) cecha oznaczająca, że życie obiektu nie jest ograniczone tylko do czasu działania programu. Format binarny, skuteczny również w sieci pomiędzy komputerami działającymi pod różnymi JVM. Dużo klas z bibliotek Javy jest zmodyfikowanych tak, że wspierają serializację (np. kolekcje).
Zastosowania serializacji Trwałość zapisanie obiektów na dysk oraz odtworzenie przy kolejnym uruchomieniu programu. Zdalne wywoływanie metod między komputerami poprzez RMI (ang. Remote Method Invocation). Pozwala to obiektowi istniejącemu na jednym komputerze zachowywać się tak, jakby istniał na drugim komputerze. Serializacja jest używana do przekazywania argumentów przy wywołaniu metod odległego obiektu oraz do zwracania wartości. Komponenty JavaBeans elementy z których buduje się interfejs użytkownika. W trakcie projektowania stan komponentu jest serializowany, a przy starcie programu jest on odtwarzany.
Sposoby realizacji Serializację typowo realizuje się przez: 1. Implementację interfejsu Serializable w klasie, która ma być serializowana (nie ma żadnych metod, jest znacznikiem ). Można także kontrolować przebieg serializacji przez: 2. Implementację w klasie interfejsu Externalizable oraz jego dwóch metod: writeexternal() zapis obiektu i readexternal() odczyt obiektu, musi być też dostępny publiczny konstruktor domyślny. 3. Implementację w klasie interfejsu Serializable oraz dodatkowo (!) dwóch prywatnych metod writeobject() i readobject().
Działanie Żeby zserializować obiekt, trzeba utworzyć strumień ObjectOutputStream i wywołać jego metodę writeobject() podając obiekt do serializacji. Może się tu pojawić wyjątek IOException. Żeby odtworzyć obiekt, trzeba utworzyć strumień ObjectInputStream, wywołać jego metodę readobject() i dokonać rzutowania (readobject() zwraca bowiem obiekt klasy Object). Mogą się tu pojawić wyjątki IOException oraz ClassNotFoundException. Słowo kluczowe transient pozwala wskazać, które składowe obiektu mają nie być serializowane.
Przykład serializowania obiektów import java.io.*; class Wezel implements Serializable { String nazwa; /*... */ public class Serializacja { Wezel korzen = new Wezel(); /*... */ void zapiszwezly() throws IOException { /*... utworzenie strumienia wyjściowego... */ ObjectOutputStream out = new ObjectOutputStream( /*strumień wyj.*/ ); out.writeobject(korzen); /*... zamknięcie strumienia wyjściowego... */
Przykład odtwarzania obiektów /*... */ void przywrocwezly() throws IOException, ClassNotFoundException { /*... utworzenie strumienia wejściowego... */ ObjectInputStream in = new ObjectInputStream( /*strumień wej.*/ ); // odczytanie obiektu i rzutowanie do // odpowiedniego typu korzen = (Wezel) in.readobject(); /*... zamknięcie strumienia wejściowego... */