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

Podobne dokumenty