Klasa Files
Transkrypt
Klasa Files
Aplikacje w Javie – wykład 10 Strumienie (Klasa Files, Formatter, serializacja obiektów) Wątki (tworzenie i uruchamianie, zadania i wykonawcy) Treści prezentowane w wykładzie zostały oparte o: ● Barteczko, JAVA Programowanie praktyczne od podstaw, PWN, 2014 ● http://docs.oracle.com/javase/8/docs/ ● C. S. Horstmann, G. Cornell, Java. Podstawy, Helion, Gliwice 2008 1 Klasa Files ● ● Strumienie we/wy są użyteczne, ale wiele operacji na plikach łatwiej jest wykonywać przy pomocy statycznych metod klasy Files z pakietu java.nio.file (To jest inna klasa niż File). Zapewnia ona ulepszoną reprezentację nowoczesnych systemów i obiektów plikowych (m.in. większą liczbę arybutów obiektów plikowych, obsługę linków symbolicznych) i w przeciwieństwie do klasy File dostarcza metod wejścia-wyjścia dla plików. Większość metod klasy Files ma argumenty typu Path, reprezentują one ścieżki obiektów plikowych (plików, katalogów) w sposób niezależny od konkretnego systemu plikowego. Ścieżki te uzyskujemy przy pomocy metody get z klasy Paths. Przykłady działania dla systemu Windows: Paths.get("C:/Temp/plik1.txt"); //absolutna ścieżka do pliku Paths.get("in1.txt"); //plik in1.txt z bieżącego katalogu Paths.get("."); //katalog bieżący Paths.get("../p2.txt"); //p2.txt z nadkatalogu bieżcego katalogu Paths.get("/"); //główny katalog (root) bieżącego dysku Paths.get("/Temp"); //katalog Temp bieżącego dysku Paths.get("Temp"); //podkatalog Temp bieżącego katalogu Paths.get("C:", "Temp", "p.txt"); //plik C:\Temp\p.txt 2 Klasa Files - metody KOPIOWANIE PLIKÓW. Metoda Files.copy(Path source, Path target, CopyOption... options) umożliwia kopiowanie plików z uwzględnieniem podanych opcji: REPLACE_EXISTING - zastąpienie pliku w przypadku, gdy docelowy plik istnieje (domyślnie wyjatek FileAlreadyExistsException) COPY_ATTRIBUTES - dla kopii pliku mają być zachowane atrybuty oryginału Metoda Files.move() - pozwala na zmianę nazwy lub umiejscowienia pliku. import java.nio.file.*; import static java.nio.file.StandardCopyOption.*; public class FcopyDemo{ static void copyFile(String srcFn, String destFn, CopyOption... opt) throws IOException{ Files.copy(Paths.get(srcFn), Paths.get(destFn), opt); } 3 Klasa Files - metody public static void main(String[] args)throws IOException{ copyFile("in1", "out2"); //wyjatek jeśli out2 istnieje copyFile("in1", "out1", REPLACE_EXISTING); //jeśli out1 istnieje to będzie zastąpiony copyFile("in1", "/Temp/in1",COPY_ATTRIBUTES); //kopiuje in1 do katalogu Temp z zachowaniem atrybutów } } PRZETWARZANIE WIERSZY PLIKU TEKSTOWEGO. Metoda static List<String> readAllLines(Path path, Charset cs) zwraca listę wszystkich wierszy pliku, wymagane jest podanie strony kodowej jako obiektu klasy Charset (domyślna strona kodowa Charset.defaultCharset()) for (String line: Files.readAllLines(Paths.get("in1"), Charset.defaultCharset()) ) System.out.println(line); 4 Klasa Files - metody CZYTANIE I ZAPISYWANIE BAJTÓW. Metoda Files.getAllBytes(Path) zwraca zawartość pliku jako tablicę bajtów. Tablicę możemy zapisać do pliku static Path write(Path path, byte[] bytes, OpenOption... options) Z metod tych korzystamy, gdy działamy na plikach binarnych, ale można ich użyć również do plików tekstowych. Dokonajmy zamiany znaków tabulacji na spację w pliku. Operacje readAll...() jednokrotnie przetwarzają i od razu zamykaja pliki (tak samo Files.write(...)). void tabToSpace(String fname) throws IOException { Path fpath = Paths.get(fname); byte[] cont = Files.readAllBytes(fpath); for(int i=0; i<cont.lenght; i++){ if(cont[i]==0x09) cont[i] = (byte) ' '; //09 to hex kod znaku tabulacji } Files.write(fpath, cont); } 5 Klasa Files - metody CZYTANIE I ZAPIS WIERSZY. Druga wersja metody Files.write(): static Path write(Path path, Iterable<? extends CharSequence> lines, Charset cs, OpenOption... options) ma jako argument listę wierszy, które mają być zapisane do pliku. Łatwo więc można zmienić kodowanie pliku. Do ustalania stron kodowych użyjemy statycznej metody forName z klasy Charset: Path file = Paths.get("page.html"); Charset cpIn = Charset.forName("Cp1250"); cpOut = Charset.forName("ISO8859-2"); Files.write(file, Files.readAllLines(file, cpIn), cpOut); Przedstawione metody readAll... nadają się do operowania na stosunkowo niewielkich plikach, ponieważ wczytują do pamięci od razu całą zawartość pliku. Dla bardzo dużych plików powinniśmy użyć innych metod np. strumieni we/wy. Jeśli chcemy czytać plik sukcesywnie (i być może nie do końca), lepiej użyć Scannera. Dla skanera źródłem danych oprócz File, String, może być Path oraz dowolny Reader i InputStream. W konstruktorze Scannera możemy podać stronę kodową wczytywanego pliku (jego treść będzie dekodowana do Unikodu zgodnie z tą stroną). Scanner zamykamy za pomocą metody close() (nie zgłasza ona wyjątków kontrolowanych ) lub używamy try-with-resources. 6 Formatter Klasa java.util.Formatter zapewnia możliwości formatowania danych. Tworząc formator (za pomocą wywołania konstruktora) możemy określić: ● destynację formatowanych danych(dokąd mają być zapisane), którą może być: – ● ● ● File, String, OutputStream, obiekty klas implementujących interfejs Appendable, czyli: BufferedWriter, CharArrayWriter, CharBuffer, FileWriter, FilterWriter, LogStream, OutputStreamWriter, PipedWriter, PrintStream, PrintWriter, StringBuffer, StringBuilder (szybsza wersja StringBuffer, bo niesynchronizowana), StringWriter, Writer lokalizację (ustawienia regionalne, reprezentowane przez obiekt klasy Locale), wpływającą m.in. na reprezentację liczb i dat, stronę kodową (do kodowania napisów) - dla strumieni, plików i stringów Uwaga: formatory dla destynacji implementującyh interfejs Closeable (m.in. pliki, strumienie) powinny być po użyciu zamykane lub wymiatane (close(), flush()), co powoduje zamknięcie lub wymiecenie buforów tych destynacji. 7 Formatter ● Formatowanie polega na wywołaniu jednej z dwóch wersji metody format (na rzecz formatora): Formatter format(Locale l, String fmt, Object... args) Formatter format(String fmt, Object... args) ● ● Łańcuch formatu (parametr fmt) zawiera dowolne ciągi znaków oraz specjalne symbole formatujące. Dalej następują dane do wstawienia w łańcuch formatu w miejscu elementów formatu i do sformatowania według zasad określonych przez te elementy (zmienna liczba argumentów dowolnego typu – formalnie Object). Dzięki autoboxingowi nie ma problemu z formatowaniem danych typów prostych. Dla uproszczenia dostępne są: – statyczne metody format w klasie String, – metody format i printf (działające tak samo) w klasach PrintStream i PrintWriter, wyprowadzajace sformatowane napisy na wyjście. 8 Formatter Elementy formatu mają następującą ogólną postać %[arg_ind$][flags][width][.precision]conversion gdzie ● ● ● ● ● arg_ind$ – numer argumentu (z listy argumentów args) do sformatowania przez dany element; numeracja zaczyna się od 1; poczynając od 2-go elementu można zastosować w tym miejscu znak < , co oznacza, że dany element ma być zastosowany wobec argumentu użytego w poprzednim formatowaniu flags – znaki modyfikujące sposób formatowania (są różne dla różnych typów konwersji conversion) width – minimalna liczba znaków dla danego argumentu w wynikowym napisie .precision – liczba pokazywanych miejsc dziesiętnych (dla liczb rzeczywistych) lub maksymalna liczba wyprowadzonych znaków (dla np. napisów) conversion – konwersja określa jak ma być traktowany i formatowany odpowiadający danemu elementowi argument, np. jako liczba rzeczywista, jako data, jako napis Uwaga: nawiasy kwadratowe oznaczają opcjonalność. 9 Formatter - konwersje Wśród flag na szczególną uwagę zasługują: '-' – wynik wyrównany w polu do lewej (domyślnie jest wyrównany do prawej), '+' – wynik zawiera zawsze znak (dla typów liczbowych), ' ' – wynik zawiera wiodącą spację dla argumentów nieujemnych (tylko dla typów liczbowych). Konwersja Może być stosowana wobec Wynik s lub S dowolnych danych Jeżeli argument jest null - napis "null": w przeciwym razie jeżeli klasa arg na to zezwala - wynik wywołania arg.formatTo(...) w przeciwnym razie wynik wywołania arg.toString() Uwaga: użycie jako symbolu konwersji dużego S spowoduje zamianę liter napisu na duże. c lub C typów reprezentujących znaki Unicode znak Unicode d typów reprezentujących liczby całkowite liczba całkowita (dziesiętna) f float, double, Float, Double, BigDecimal liczba rzeczywista z separatorem miejsc dzisiętnych 10 Formatter - konwersje Konwersja Może być stosowana wobec tH Wynik tM godzina na zegarze 24-godzinnym-2 cyfry (0023) minuty - 2 cyfry (00 - 59) tS sekundy - 2 cyfry (00-60) tY tm td danych reprezentujących czas, czyli: long, Long, Calendar, Date rok - 4 cyfry (np. 2014) miesiąc - 2 cyfry (01-12) dzień miesiąca - 2 cyfry (01 -31) tR czas na zegarze 24 godzinnym sformatowany jako "%tH:%tM" tT czas na zegarze 24 godzinnym sformatowany jako "%tH:%tM:%tS" tF data sformatowana jako "%tY-%tm-%td" 11 Formatter - przykład Aby uzyskać sformatowane wyniki: liczbę z dwoma miejscami dziesiętnymi, datę w postaci rok-miesiąc-dzień możemy napisać: import java.util.*; public class Format1 { public static void main(String[] args) { double cena = 1.52; double ilosc = 3; double koszt = cena * ilosc; System.out.printf("Koszt wynosi %.2f zł", koszt); System.out.printf("\nData: %tF",Calendar.getInstance()); } } Wynik: Koszt wynosi 4,56 zł Data: 2014-12-08 Warto tu zwrócić uwagę na to, że dla lokalizacji polskiej liczba pokazywana jest z przecinkiem jako separatorem miejsc dziesiętnych. Aby uzyskać kropkę można napisać: System.out.printf(Locale.ROOT, "Koszt wynosi %.2f zł", koszt); W tym przypadku stała statyczna Locale.ROOT oznacza neutralną lokalizację (bez wybranego kraju i języka). 12 Formatter - przykład import java.util.Calendar; public class Format2 { public static void main(String[] args) { System.out.println("Wyrównany wydruk tablicy (po 2 elementy w wierszu)"); int[] arr = { 1, 100, 200, 4000 }; int k = 1; for (int i : arr) { System.out.printf("%5d", i); if (k++%2 == 0) System.out.println(); } // Zastosowanie znaku < //(element formatu stosowany wobec argumentu // z poprzedniego formatowania) System.out.println("Zaokrąglenia"); System.out.printf("%.3f %<.2f %<.1f", 1.256); // Znak < szczególnie przydatny w datach/czasie Calendar c = Calendar.getInstance(); c.set(Calendar.MONTH, 1); System.out.printf("\nW roku %tY i miesiącu %<tm mamy %d dni", c, c.getActualMaximum(Calendar.DATE) ); 13 Formatter - przykład // Oczywiście możemy formatować do stringów String dateNow = String.format("%td-%<tm-%<tY", System.currentTimeMillis()); System.out.printf("\n" + dateNow); } } WYJŚCIE: Wyrównany wydruk tablicy (po 2 elementy w wierszu) 1 100 200 4000 Zaokraglenia 1,256 1,26 1,3 W roku 2014 i miesiącu 02 mamy 28 dni 09-12-2014 Formatowanie dat można też uzyskać za pomocą klas SimpleDateFormat oraz od Javy 8 : DateTimeFormatter. Umożliwiają one nie tylko formatowanie ale i parsowanie dat z napisów. 14 Serializacja obiektów ● ● ● ● ● ● ● ● ● Obiekty tworzone przez program rezydują w pamięci operacyjnej, w przestrzeni adresowej procesu. Są zatem nietrwałe, bo kiedy program kończy działanie wszystko co znajduje się w jego przestrzeni adresowej ulega wyczyszczeniu i nie może być odtworzone. Serializacja (szeregowanie) pozwala na utrwalanie obiektów. W Javie polega ona na zapisywaniu obiektów do strumienia. Podstawowe zastosowania serializacji: – komunikacja pomiędzy obiektami/aplikacjami poprzez gniazdka (sockets), – zachowanie obiektu (jego stanu i właściwości) do późniejszego odtworzenia i wykorzystania przez tę samą lub inną aplikację. Do zapisywania/odczytywania obiektów służą klasy ObjectOutputStream oraz ObjectInputStream, które należą do strumieniowych klas przetwarzających. Metoda klasy ObjectOutputStream: void writeObject(Object o) zapisuje obiekt o do strumienia. Metoda klasy ObjectInputStream: Object readObject() odczytuje obiekt ze strumienia i zwraca referencję do niego Do strumieni mogą być zapisywane tylko serializowalne obiekty. Obiekt jest serializowalny jeśli jego klasa implementuje interfejs Serializable Prawie wszystkie klasy standardowych pakietów Javy implementują ten interfejs. Również tablice (które są obiektami specjalnych klas definiowanych w trakcie kompilacji) są serializowalne. 15 Serializacja obiektów - przykład Przykład: program zapisuje do strumienia obiekty - datę, tablicę opisów i odpowiadającą każdemu opisowi temperaturę. Następnie odczytuje te obiekty ze strumienia i odtwarza je. import java.io.*; import java.util.*; class Serial { public static void main(String args[]) { Date data = new Date(); int[] temperatura = { 25, 19 , 22}; String[] opis = { "dzień", "noc", "woda" }; // Zapis try { ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream("test.ser")); out.writeObject(data); out.writeObject(opis); out.writeObject(temperatura); out.close(); } catch(IOException exc) { exc.printStackTrace(); System.exit(1); } 16 Serializacja obiektów - przykład // Odtworzenie (zazwyczaj w innym programie) try { ObjectInputStream in = new ObjectInputStream( new FileInputStream("test.ser") ); Date odczytData = (Date) in.readObject(); String[] odczytOpis = (String[]) in.readObject(); int[] odczytTemp = (int[]) in.readObject(); in.close(); System.out.println(String.valueOf(odczytData)); for (int i=0; i<odczytOpis.length; i++) System.out.println(odczytOpis[i] + " " + odczytTemp[i]); } catch(IOException exc) { exc.printStackTrace(); System.exit(1); } catch(ClassNotFoundException exc) { System.out.println("Nie można odnaleźć klasy obiektu"); System.exit(1); } } } 17 Serializacja obiektów ● ● ● ● ● Metoda readObject() pobiera ze strumienia zapisane charakterystyki obiektu (w tym również oznaczenie klasy do której należy zapisany obiekt) - na ich podstawie tworzy nowy obiekt tej klasy i inicjuje go odczytanymi wartościami. Wynikiem jest referencja formalnego typu Object wskazująca na nowoutworzony obiekt, który jest identyczny z zapisanym. Ponieważ wynikiem jest Object, należy wykonać odpowiednią konwersję zawężającą do właściwego typu (referencji do konkretnej podklasy klasy Object, tej mianowicie, której egzemplarzem faktycznie jest odczytany obiekt). Może się też okazać, że w strumieniu zapisano obiekt klasy, która nie jest dostępna przy odczytywaniu (np. została usunięta). Wtedy przy tworzeniu obiektu z odczytanych danych powstanie wyjątek ClassNotFoundException, który musimy obsługiwać. Aby serializować obiekty własnych klas, klasa winna implementować interfejs Serializable. Interfejs ten jest pusty - nie musimy więc implementować żadnych jego metod, wystarczy tylko wskazać, że nasza klasa go implementuje. Takie interfejsy (bez metod) nazywane są interfejsami znacznikowymi. Ich jedyną funkcją jest umożliwienie sprawdzenia typu np. za pomocą operatora instanceof. Metoda writeObject to własnie robi, gdy podejmuje decyzje o zapisie: jeśli jej argument x jest typu Serializable (x instanceof Serializable ma wartośc true), to obiekt jest zapisywany do strumienia, w przeciwnym razie – nie. 18 Serializacja obiektów ● ● Przy serializacji nie są zapisywane pola statyczne oraz pola deklarowane ze specyfikatorem transient; specyfikatora transient używamy więc wobec elementów informacji o obiekcie, których nie chcemy poddawać utrwaleniu. Pełniejszą kontrolę nad sposobem serializacji możemy zyskać definiując odpowiednie metody w klasie obiektu serializowanego. Metody te winny mieć następujące sygnatury: private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException; private void writeObject(java.io.ObjectOutputStream stream) throws IOException; ● Całkowitą kontrolę nad formatem i sposobem serializacji zyskujemy poprzez implementację w klasie interfejsu Externalizable i dostarczenie metod writeExternal i readExternal 19 Programowanie współbieżne ● ● ● ● ● ● ● Uruchomienie dowolnego programu (aplikacji) powoduje stworzenie procesu w systemie operacyjnym. Dla każdego procesu alokowane są przez system wymagane przez niego zasoby np. pamięciowe i plikowe. Proces - to wykonujący się program wraz z dynamicznie przydzielanymi mu przez system zasobami (np. pamięcią operacyjną, zasobami plikowymi) oraz, ewentualnie, innymi kontekstami wykonania programu (np. obiektami tworzonymi przez program) . Systemy wielozadaniowe pozwalają na (teoretycznie) równoległe wykonywanie wielu procesów, z których każdy ma swój kontekst, w tym swoje zasoby. W systemach wielozadaniowych i wielowątkowych - jednostką wykonawczą procesu jest wątek. Każdy proces ma co najmniej jeden działający wątek, ale może też mieć ich wiele. Proces "posiada" zasoby i inne konteksty, wykonaniem "zadań" procesu zajmują się wątki (swego rodzaju podprocesy, wykonujące różne działania w kontekście jednego procesu). Wątki działają (teoretycznie) równolegle. Zatem równoległość działań w ramach procesu (jednego programu) osiągamy przez uruchamianie kilku różnych wątków. Wątek - to sekwencja działań, która może wykonywać się równolegle z innymi sekwencjami działań w kontekście danego procesu (programu). 20 Programowanie współbieżne W systemach jednoprocesorowych tak naprawdę w każdym momencie czasu wykonuje się tylko jeden wątek i nie ma tu "prawdziwej" równoległości. Wrażenie równoległości działania wątków osiągane jest przez mechanizm przydzielania czasu procesora poszczególnym wykonującym się wątkom. Każdy wątek uzyskuje dostęp do procesora na krótki czas (kwant czasu), po czym "oddaje procesor" innemu wątkowi. Zmiany są tak szybkie, że powstaje wrażenie równoległości działania. Zmiany wątków mogą dokonywać się według dwóch mechanizmów: ● ● współpracy (cooperative multitasking) - wątek sam decyduje, kiedy oddać czas procesora innym wątkom, wywłaszczania (pre-emptive multitasking) - o dostępie wątków do procesora decyduje systemowy zarządca wątków: przydziela on wątkowi kwant czasu procesora, po upłynięciu którego odsuwa wątek od procesora i przydziela kwant czasu procesora innemu wątkowi. Ponieważ Java jest językiem wieloplatformowym, a różne systemy operacyjne stosują różne mechanizmy udostępniania wątkom czasu procesora, pisząc programy wielowątkowe w Javie powinniśmy zakładać, że mogą one działać zarówno w środowisku "współpracy", jak i "konkurencji" (mechanizm wywłaszczania) 21 Programowanie współbieżne ● ● ● Proces wykonuje się poprzez wykonanie jego wątków. Zatem, zwolnienie procesora przez wątek jednego procesu i przydzielenie procesora wątkowi innego procesu wymaga "przeładowania" kontekstu procesu, bowiem każdy proces ma swój niezależny kontekst (np. przestrzeń adresową, odniesienia do otwartych plików). Podstawowa różnica pomiędzy procesami i wątkami polega na tym, że różne wątki w ramach jednego procesu mają dostęp do całego kontekstu tego procesu (m.in. przydzielonych mu zasobów). Wobec tego zamiana wątków jednego procesu "przy procesorze" jest wykonywana szybciej niż zamiana procesów (wątków różnych procesów). Z punktu widzenia programisty wspólny dostęp wszystkich wątków jednego procesu do kontekstu tego procesu ma zarówno zalety jak i wady. Zaletą jest możliwość łatwego dostępu do wspólnych danych programu. Wadą brak ochrony danych programu przed równoległymi zmianami, dokonywanymi przez różne wątki, co może prowadzić do niespójności danych, a czego unikanie wiąże się z koniecznością synchronizacji działania wątków. 22 Tworzenie i uruchamianie wątku (klasa Thread) Uruchamianiem i zarządzaniem wątkami w Javie zajmuje się klasa Thread. Aby uruchomić wątek należy stworzyć obiekt klasy Thread i użyć metody start() wobec tego obiektu. Kod, wykonujący się w wątku (sekwencja działań, wykonująca się równolegle z innymi działaniami programu) określany jest przez obiekt klasy implementującej interfejs Runnable. Interfejs ten zawiera deklarację metody run(), w której przy implementacji zapisujemy kod. Ten kod będzie wykonywany w wątku (równolegle z innymi fragmentami wykonującymi się w innych wątkach). Metoda run() określa co ma robić wątek. Klasa Thread implementuje interfejs Runnable (podając "pustą" metodę run). Pierwszy sposób tworzenia i uruchamiania wątku 1) Zdefiniować własną klasę dziedziczącą Thread (np. class Timer extends Thread) 2) Przedefiniować odziedziczoną metodą run(), podając w niej działania, które ma wykonywać wątek 3) Stworzyć obiekt naszej klasy (np. Timer timer = new Timer(...); 4) Wysłać mu komunikat start() (np. timer.start()) 23 Tworzenie i uruchamianie wątku (klasa Thread) Niech klasa Timer służy do zliczania czasu (nie mylić tej klasy z klasami Timer z pakietu javax.swing oraz Timer z pakietu java.util; tutaj pod tą nazwą opisujemy własną klasę - licznik czasu). public class Timer extends Thread { public void run() { int time = 0; //licznik sekund while (true) { try { this.sleep(1000); //usypiamy wątek na 1 sekundę }catch(InterruptedException exc) { System.out.println("Wątek zliczania czasu zoostał przerwany."); return; } time++; int minutes = time/60; int sec = time%60; System.out.println(minutes + ":" + sec); } } } 24 Tworzenie i uruchamianie wątku (klasa Thread) ● ● ● ● Kodu zliczającego upływ czasu dostarczyliśmy w metodzie run(). Zmienna time jest licznikiem sekund. W pętli while usypiamy wątek, wykonujący tę metodę run(), na 1000 milisekund, czyli 1 sekundę (statyczna metoda sleep z klasy Thread, która usypia bieżący wątek), po czym zwiększamy licznik sekund (zmienna time) i wyprowadzamy informację o upływie czasu na konsolę. W trakcie uśpienia wątek jest odsuwany od procesora (kod metody run nie wykonuje się wtedy). Dzięki temu uzyskujemy pożądany efekt. Metoda sleep może zgłaszać wyjątek InterruptedException, który powstaje na skutek przerwania działania wątku (metodą interrupt()). Dlatego musimy obsługiwać ten wyjątek. Przykład. Licznik czasu możemy zastosować np. w programie, który wymaga od użytkownika podania wszystkich stolic (lądowych) sąsiadów Polski. Wraz z podawaniem kolejnych stolic chcemy wypisywać na konsoli informację o upływającym czasie. 25 Tworzenie i uruchamianie wątku (klasa Thread) import java.util.*; import static javax.swing.JOptionPane.*; public class Quiz { public static void main(String args[]) { // Stolice do odgadnięcia Set<String> cap = new HashSet<>( Arrays.asList("praga", "bratysława","moskwa", "berlin", "kijów", "wilno", "mińsk")); Set<String> entered = new HashSet<>();//stolice już podane showMessageDialog(null, "Podaj stolice lądowych sąsiadów Polski"); String askMsg = "Wpisz kolejną stolicę:" ; int count = 0; // ile podano prawidłowych odpowiedzi // Uruchomienie wątku zliczającego i pokazującego upływający czas new Timer().start(); // dopóki nie podano wszystkich stolic for (int n=cap.size(); count<n;) { String in = JOptionPane.showInputDialog("Odpowiedzi: " + count + '/'+ n +'\n' + askMsg); if (in == null) break; in = in.toLowerCase(); // jeśli tej stolicy wcześniej nie podano i odp. jest prawidłowa if (!entered.contains(in) && cap.contains(in)){ count++; entered.add(in); } } } } 26 Tworzenie i uruchamianie wątku (klasa Thread) W pętli pobieramy dane od użytkownika, dopóki nie wpisze on prawidłowo wszystkich znajdujących się w zbiorze cap stolic lub nie przerwie wpisywania, wybierając w dialogu Cancel. Ponieważ wcześniej utworzyliśmy i uruchomiliśmy wątek zliczania czasu: Timer tm = new Timer(); tm.start(); lub zwięźlej: new Timer().start(); to nasz program równolegle wykonuje dwa zadania: interakcję z użytkownikiem (pytania o stolice) oraz wypisywanie na konsoli informacji o upływającym czasie. Wywołanie metody start() na rzecz obiektu klasy Thread powoduje uruchomienie metody run() z klasy Timer. To wywołanie nie blokuje głównego programu; wykonywane są dalsze jego instrukcje, a kod metody run() działa równolegle. 27 Tworzenie i uruchamianie wątku (interfejs Runnable) Jak już wiemy, kod wykonywany przez wątek podajemy w metodzie run(). A metoda run() może być zdefiniowana w dowolnej klasie implementującej interfejs Runnable. Klasa Thread dostarcza zaś konstruktora, którego argument jest typu Runnnable. Konstruktor ten tworzy wątek, który będzie wykonywał kod zapisany w metodzie run() w klasie obiektu, do którego referencję przekazano wspomnianemu wyżej konstruktorowi. Drugi sposób tworzenia i uruchamiania wątków. 1) Zdefiniować klasę implementującą interfejs Runnable (np. class X implements Runnable). 2) Dostarczyć w niej definicji metody run() (co ma robić wątek). 3) Utworzyć obiekt tej klasy (np. X x = new X(); ) 4) Utworzyć obiekt klasy Thread, przekazując w konstruktorze referencję do obiektu utworzonego w p.3 (np.Thread thread = new Thread(x);). 5) Wywołać na rzecz nowoutworzonego obiektu klasy Thread metodę start ( thread.start();) 28 Tworzenie i uruchamianie wątku (interfejs Runnable) Oprogramowanie uprzednio omawianego licznika czasu przy użyciu drugiego sposobu: class Timer implements Runnable { public void run() { int time = 0; while (true) { try { Thread.sleep(1000); } catch(InterruptedException exc) { System.out.println("Wątek zliczania czasu został przerwany."); return; } time++; int minutes = time/60; int sec = time%60; System.out.println(minutes + ":" + sec); } } } 29 Tworzenie i uruchamianie wątku (interfejs Runnable) Wówczas utworzenie i uruchomienie wątku zliczającego czas (w innej klasie): Timer tm = new Timer(); Thread thread = new Thread(tm); thread.start(); lub zwięźlej: new Thread(new Timer()).start(); Jak poprzednio, w metodzie run() dla uśpienia wątku na sekundę używamy statycznej metody sleep z klasy Thread, która usypia bieżący wątek (w tym przypadku ten, w którym wykonuje się ta metoda run()). Nie mogliśmy tym razem napisać this.sleep(), bo nowa klasa Timer nie dziedziczy już klasy Thread. Drugi sposób tworzenia i uruchamiania wątków ma pewne zalety w stosunku do korzystania wyłącznie z klasy Thread (czyli sposobu pierwszego): ● ● niekiedy daje lepsze możliwości separowania kodu (kod odpowiedzialny za pracę wątku może być wyraźnie wyodrębniony w klasie implementującej Runnable). w niektórych okolicznościach - mianowicie, gdy chcemy umieścić metodę run() w klasie, która dziedziczy jakąś inną klasę - jest jedynym możliwym sposobem. 30 Tworzenie i uruchamianie wątku – klasy anonimowe Przy tworzeniu wątków ad hoc (zwykle tylko raz i na jakąś konkretną potrzebę) bardzo często posługujemy się anonimowymi klasami wewnętrznymi i to zwykle lokalnymi. Na przykład - do zliczania i pokazywania upływu czasu w trakcie jakiejś interakcji użytkownika z programem, jak w poniższym programie: new Thread(new Runnable() { public void run() { int time = 0; while (true) { try { Thread.sleep(1000); } catch(InterruptedException exc) { return; } System.out.println( time++/60 + " min. " + time%60 + " sek."); } } }).start(); String s, out = ""; while ((s = JOptionPane.showInputDialog("Tekst:")) != null) out += " " + s; System.out.println(out); Zauważmy, że działanie kodu zliczającego czas nigdy się nie kończy. 31 Zadania i wykonawcy Zauważmy, że kod wątku zapisywany jest w metodzie run(), a klasa Thread tak naprawdę nic nie robi. To kod metody run() wykonuje się "w wątku" (czyli współbieżnie), choć często mówimy nieco mylnie (potocznie): "wątek się wykonuje". Tymczasem wcale nie jesteśmy zainteresowani wątkami (obiektami klasy Thread) tylko zadaniami (zapisanymi w metodzie run()), które "poprzez wątki" się wykonują. Chcielibyśmy rozumować raczej w kategoriach zadań do wykonania, a nie technicznych szczegółów sposobu ich wykonania. Od Javy 1.5 zadania "do wykonania" mogą być odseparowane od wątków. Wprowadzony został pakiet java.util.concurrent, dzięki któremu możemy efektywnie rozwiązać wiele problemów zwiazanych z programowaniem współbieżnym: ● ● ● jeśli chcesz łatwo tworzyć pule wątków i zarządzać nimi bez trudnego programowania, to użyj odpowiednich Serwisów Wykonawców (ExecutorService) jeśli chcesz myśleć w kategoriach zadań (Task), nie wątków, to daj Wykonawcom zadania do wykonania, oni zdecydują jak najlepiej podzielić je między wątki, ale ogólna strategia podziału i uruchamiania jest pod Twoją kontrolą (mamy wybór różnych strategii) jeśli chcesz mieć łatwo dostępne wyniki współbieżnych zadań, to użyj interfejsu Callable, zaufaj Wykonawcom i odbieraj wyniki w postaci FutureTask obiektu pozwalającego na asynchroniczne testy (wyniki już są? jeszcze nie ma?), reagowanie na wyjątki, odczytywanie wyników zadań i podłączanie callbacków. 32 Zadania i wykonawcy Tworzenie wątków jest kosztowne czasowo. Pule wątków pozwalają na ponowne użycie wolnych wątków, a także na ewentualne limitowanie maksymalnej liczby wątków w puli. My rozumujemy w kategoriach zadania do wykonania (określanego przez kod Runnable), tworzeniem i uruchamianiem wątków zajmują się Wykonawcy. Wykonawca jest obiektem klasy implementującej interfejs Executor, zawierający jedną metodę execute(Runnable). Interfejs ExecutorService rozszerza go, dostarczając dodatkowych metod, np. do zakończenia działania Wykonawcy. W Javie mamy do dyspozycji kilka rodzajów gotowych Wykonawców, fabrykowanych przez odpowiednie metody klasy Executors m.in.: ● ● ● ● Wykonawca uruchamiający podane mu zadania w jednym wątku (po kolei) (Executors.newSingleThreadExecutor()), Wykonawca, prowadzący pulę wątków o zadanych maksymalnych rozmiarach (Executors.newFixedThreadPool()), Wykonawca, prowadzący pulę wątków o dynamicznych rozmiarach (Executors.newCachedThreadPool()), Wykonawcy zarządzający tworzeniem i wykonaniem wątków w określonym czasie lub z określoną periodycznością (Executors.newScheduled....()) 33 Zadania i wykonawcy - przykład import java.util.concurrent.*; class Task implements Runnable { private String name; private final int N; public Task(String name, int n) { N=n; this.name = name; } @Override public void run() { int sum=0; for (int i=1, k=0; i <= N; i++) { if (i%1000 == 0) System.out.println(name + " … count " + (k+=1000)); sum+=i; } System.out.println(name + ", sum = " + sum); } } 34 Zadania i wykonawcy - przykład public class Wykonawca { public static void main(String[] args) { Executor exec = Executors.newFixedThreadPool(2); for (int i=1; i<=4; i++) { exec.execute(new Task("Task " + i, i*1000)); } } } run: Task 1 … count 1000 Task 1, sum = 500500 Task 2 … count 1000 Task 3 … count 1000 Task 2 … count 2000 Task 3 … count 2000 Task 2, sum = 2001000 Task 3 … count 3000 Task 4 … count 1000 Task 3, sum = 4501500 Task 4 … count 2000 Task 4 … count 3000 Task 4 … count 4000 Task 4, sum = 8002000 35 Zadania i wykonawcy Zastosowanie FixedThreadPool z liczbą wątków w puli równą 2, powoduje, że równolegle wykonują się po 2 zadania, przy zmianie wykonawcy na Executor exec = Executors.newCachedThreadPool(); wykonywałyby się równolegle 4 zadania. Gdy nasze zadania zakończą się, Wykonawca nadal "działa" i jest gotowy do przyjmowania nowych zadań. Zamknięcie Wykonawcy oznacza, iż nie będzie on już przyjmował nowych zadań do wykonania; jednak przekazane mu wcześniej i jeszcze nie zakończone będzie wykonywał. Usługę zamknięcia dostarcza interfejs ExecutorService, który jest rozszerzeniem interfejsu Executor. Metody fabryczne klasy Executors zwracają Wykonawców implementujących ExecutorService. 36 Zadania i wykonawcy public static void main(String[] args) { ExecutorService exec = Executors.newFixedThreadPool(2); for (int i=1; i<=4; i++) { exec.execute(new Task("Task " + i)); } Thread.yield();//sygnalizuje,że wątek może być //odsunięty od procesora exec.shutdown();//nie pozwala na zlecenie nowych zadań, //ale wykonywane zadania nadal działają try { exec.execute(new Task("Task after shutdown")); } catch (RejectedExecutionException exc) { exc.printStackTrace(); } try { exec.awaitTermination(5, TimeUnit.SECONDS); } catch(InterruptedException exc) { exc.printStackTrace(); } System.out.println("Terminated: " + exec.isTerminated()); } 37 Zadania i wykonawcy ● ● ● Ponieważ metoda shutdown() zamyka ExecutorService, zadanie "Task after shutdown" nie zostanie uruchomione (powstanie wyjątek RejectedExecutionException; ten wyjątek może powstawać również wtedy, gdy ExecutorService z innych powodów niż zamknięcie odmawia wykonania zadania). Na końcu programu - za pomocą metody awaitTermination(...) wstrzymujemy bieżący wątek dopóki Wykonawca nie zakończy wszystkich zadań (albo dopóki nie minie 5 sekund lub też nie wystąpi przerwanie bieżącego wątku za pomocą metody interrupt). Warto stosować metodę awaitTermination(), kiedy chcemy mieć pewność, że Wykonawaca naprawdę zakończył działanie i wyczyścił wszystkie swoje zajęte zasoby (np. bez tego nasz głowny wątek może sie skończyć wcześniej niż Wykonawcy i aplikacja nie zakończy działania). ExecutorService dostarcza także metody shutdownNow(), która ma za zadanie zakończyć działanie wszystkich aktualnie wykonujących się zadań (wątków) i zamknąć Wykonawcę, uniemożliwia wykonanie zadań jeszcze nie rozpoczętych. (Metoda shutdownNow() używa metody interrupt() wobec odpowiednich wątków, w reakcji na co powinny się zakończyć. ) Task Task Task Task Task 2 … count 100000000 1 … count 100000000 1, sum = 5000000050000000 2 … count 200000000 2, sum = 20000000100000000 38 Pliki o dostępie swobodnym ● ● ● ● ● Klasa RandomAccessFile definiuje pliki o dostępie swobodnym, które mogą być otwarte z trybie "czytania" lub "czytania i pisania". Swobodny dostęp oznacza dostęp do dowolnego bajtu danych bez potrzeby sekwencyjnego przetwarzania pliku od początku. Konstruktory klasy mają następującą postać: RandomAccessFile(String filename, String mode) RandomAccessFile(File file, String mode) gdzie mode oznacza jeden z następujących trybów otwarcia – "r" - plik tylko do odczytu, – "rw" - plik do odczytu i zapisu, – "rws", "rwd" - jak "rw", ale z wymuszeniem synchronicznego zapisu każdej zmiany na dysk. Pliki o dostępie swobodnym mogą być traktowane jako ciągi bajtów. Bieżący bajt do odczytu lub miejsce do zapisu określa specjalny wskaźnik pozycji w pliku (filePointer). Pozycję tę możemy zmieniać za pomocą metod seek() i skip(). Jest także zmieniana przy każdej operacji czytania lub pisania. Do czytania/pisania służy wiele metod read... i write... , które pozwalają operować na różnych rodzajach danych odczytywanych z i zapisywanych do pliku (np. readDouble, readLine, writeInt itp.), Pliki o dostępie swobodnym nie są strumieniami. Klasa RandomAccessFile nie należy więc do hierarchii klas strumieniowych 39 Archiwizacja, kompresja i dekompresja Pakiet java.util.zip dostarcza klas umożliwiających kompresję i dekompresję danych. Wybrane klasy pakietu java.util.zip: ● ● ● ● ● ● ● ● ● ● Deflater - kompresja dowolnych danych z użyciem biblioteki ZLIB (działanie na danych w pamięci np. Stringach) Inflater - kompresja dowolnych danych z użyciem biblioteki ZLIB (działanie na danych w pamięci np. Stringach) DeflaterOutputStream - strumieniowa klasa przetwarzająca: pozwala na kompresję danych (wg protokołu ZLIB) w trakcie zapisu dotsrumienia wyjściowego. InflaterInputStream - strumieniowa klasa przetwarzająca; pozwala na dekompresję danych w trakcie odczytu. GZIPInputStream - strumieniowa klasa przetwarzająca: dekompresja w trakcie odczytywania plików w formacie GZIP. GZIPOutputStream - strumieniowa klasa przetwarzająca: kompresja w trakcie zapisu do plików w formacie GZIP. ZipInputStream - jak poprzednie klasy, ale czytanie i rozpakowywanie plików ZIP ZipOutputStream - jak poprzednie klasy, ale kompresja danych i zapis do plików ZIP ZipEntry - reprezentuje element ("wejście") w pliku ZIP ZipFile - klasa pozwalająca czytać "wejścia" elementy z pliku ZIP w dowolnym porządku. 40 Archiwizacja, kompresja i dekompresja Zastosowanie narzędzi kompresji - dekompresji rozpatrzymy na przykładzie przetwarzania plików ZIP. Zauważmy, że wraz ze spakowaną zawartością plików, archiwum ZIP zawiera elementy opisujące każdy plik (tzw. "wejścia" - entry). Aby skompresować (spakować) dane i zapisać je do pliku ZIP trzeba wykonać następujące kroki. ● Nałożyć na strumień wyjściowy związany z nowotworzonym archiwum obiekt klasy przetwarzającej ZipOutputStream: ZipOutputStreame zip = new ZipOutputStream( new BufferedInputStream( new FileInputStream("nazwa.zip"))); ● Dla każdego pliku wejściowego (który ma podlegać kompresji) utworzyć "entry" w pliku ZIP: ZipEntry entry = new ZipEntry(nazwa); ● Zapisać "entry": zip.putNextEntry(entry); ● ● Zapisać do strumienia zip zawartość pliku wejściowego. Zamknąć bieżące "entry" (kolejne putNextEntry robi to automatycznie): zip.closeEntry(); 41 Archiwizacja, kompresja i dekompresja Przy rozpakowaniu archiwum możemy użyć klasy ZipFile lub ZipInputStream. W tym ostatnim przypadku kolejność działań jest następująca. ● Stworzenie rozpakowującego strumienia wejściowego: ZipInputStream zis = new ZipInputStream( new BufferedInputStream( new FileInputSTream("nazwa.zip"))); ● Przetwarzanie (rozpakownaie) elementów archiwum: ZipEntry entry; // element archiwum (spakowany plik lub katalog) //Dopóki są w w archiwum elementy, pobieramy je i przetwarzamy while((entry = zis.getNextEntry()) != null) { String ename = entry.getName(); // nazwa elementu archiwum ... } ● ● W powyższej pętli dla każdego elementu (pliku) archiwum tworzymy strumień wyjściowy (do zapisu)/ o nazwie takiej samej jak w archiwum (ename) I dalej w tej pętli czytamy dane ze strumeinia zis (metoda read() zwróci -1 gdy przeczytamy dany element - plik - z archiwum) i zapisujemy je do strumienia wyjściowego (reprezentującego rozpakowany plik). 42