jw9

Transkrypt

jw9
Aplikacje w Javie – wykład 9
Strumienie
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
Strumienie
Strumień danych oznacza ciąg danych, do którego dane mogą być dodawane i z
którego dane mogą być pobierane.
Przy czym:
●
●
strumień związany jest ze źródłem lub odbiornikiem danych
źródło lub odbiornik mogą być dowolne: plik, pamięć, zasoby sieciowe (poprzez
URL), gniazdo, potok ...
●
strumień służy do zapisywania-odczytywania informacji - dowolnych danych
●
program:
●
●
–
kojarzy strumień z zewnętrznym źródłem/odbiornikiem,
–
otwiera strumień,
–
dodaje lub pobiera dane ze strumienia,
–
zamyka strumień.
przy czytaniu lub zapisie danych z/do strumienia mogą być wykonywane
dodatkowe operacje (np. buforowanie, kodowanie-dekodowanie, kompresjadekompresja)
w Javie dostarczono klas reprezentujących strumienie. Hierarchia tych klas
pozwala na programowanie w sposób niezależny od konkretnych źródeł i
odbiorników.
2
java.io
●
●
●
●
java.nio
Java dostarcza dwóch podstawowych pakietów (z podpakietami), służących do
przeprowadzania operacji wejścia-wyjścia:
–
java.io
–
java.nio
Pakiet java.io zawiera przede wszystkim klasy, które pozwalają operować na
strumieniach danych.
W pakiecie java.nio ("Java new input-output", w skrócie NIO) wprowadzono
dodatkowe środki wejścia-wyjścia, takie jak kanały, bufory i selektory. Mimo
nazwy ("new input-output") środki te nie zastępują klas strumieniowych. Służą
przede wszystkim do zapewnienia wysokiej efektywności i elastyczności
programów, które w bardzo dużym stopniu obciążone są operacjami wejściawyjścia. W szczególności dotyczy to serwerów, które muszą równolegle
obsługiwać ogromną liczbę połączeń sieciowych.
Oprócz tego Java dostarcza klas reprezentujących inne od strumieni obiekty
operacji wejścia-wyjścia. Do klas tych należy np. klasa File z pakietu java.io opisująca pliki i katalogi, a także - w pakiecie java.net - klasy reprezentujące
obiekty "sieciowe", takie jak URL czy gniazdo (socket), mogące stanowić źródło
lub odbiornik danych w sieci (w szczególności w Internecie). Obiekty tych klas nie
stanowią strumieni. Do operowania na nich strumienie (lub kanały) są jednak
potrzebne i możemy je uzyskać przez użycie odpowiednich konstruktorów lub
metod.
3
Klasy strumieniowe
Klasy strumieniowe można podzielić na grupy wg następujących kryteriów:
●
●
●
klasy strumieni wejściowych – klasy strumieni wyjściowych
(Na strumieniach możemy wykonywać dwie podstawowe operacje:
odczytywanie danych i zapisywanie danych. Z tego punktu widzenia
możemy mówić o strumieniach wejściowych i wyjściowych)
klasy dla strumieni bajtowych – klasy dla strumieni znakowych
(strumienie znakowe realizują przesyłanie znaków, które w Javie są znakami
Unicodu, strumienie bajtowe przesyłają bajty danych)
UWAGA: Przy przetwarzaniu tekstów należy korzystać ze strumieni
znakowych ze względu na to, iż w trakcie czytania/pisania wykonywane są
odpowiednie operacje dekodowania/kodowania ze względu na stronę
kodową właściwą dla źródła/odbiornika
klasy przetwarzające – klasy przedmiotowe
(klasy przetwarzające implementują określone rodzaje przetwarzania
strumieni, niezależnie od źródła/odbiornika, klasy przedmiotowe są
związane z konkretnymi rodzajami źródła/odbiornika)
4
Klasy strumieniowe
Nadklasy, z których wywodzą się wszystkie inne klasy strumieni
Strumienie bajtowe
Wejście
InputStream
Wyjście
OutputStream
Strumienie znakowe
Reader
Writer
Wszystkie powyższe klasy są abstrakcyjne i zawierają deklaracje podstawowych metod
przetwarzania strumieni, które podklasy winny implementować.
●
●
●
●
●
Przy tworzeniu obiektu-strumienia strumień jest automatycznie otwierany,
czytanie read() (bajtów, znaków) - różne wersje tej (przeciążonej) metody
pozwalają na przeczytanie jednego bajtu ze strumienia bajtowego lub znaku ze
strumienia znakowego albo całej porcji bajtów/znaków,
zapisywanie write() (bajtów/znaków) - różne wersje tej (przeciążonej) metody
pozwalają zapisywać pojedyncze bajty/znaki lub tablice bajtów/znaków, a w
przypadku strumieni znakowych również napisy (obiekty klasy String),
pozycjonowanie strumieni (metody skip(..), mark(..), reset() ) - każdy
strumień może być traktowany jako sekwencja bajtów/znaków, czytanie i
zapisywanie zawsze dotyczy bieżącej pozycji tej sekwencji; po wykonaniu operacji
czytania lub zapisu bieżąca pozycja jest zwiększana; metody pozycjonowania
pozwalają zmieniać bieżącą pozycję.
zamykanie strumieni (metoda close()) - strumień zawsze należy zamknąć po
zakończeniu operacji na nim.
5
Klasy strumieniowe - przykład
Metody te są zazwyczaj odpowiednio przedefiniowane w klasach dziedziczących, a
polimorfizm zapewnia ich właściwe użycie
Przykład. Stwórzmy ogólną klasę udostępniającą kopiowanie strumieni.
import java.io.*;
class StreamCopier {
static void copy(InputStream in, OutputStream out)
throws IOException {
int c;
while ((c = in.read()) != -1) out.write(c);
}
static void copy(Reader in, Writer out)
throws IOException {
int c;
while ((c = in.read()) != -1) out.write(c);
}
}
Uwaga: metoda read() zwraca liczbę całkowitą, reprezentującą kolejny znak ze
strumienia znakowego (lub bajt ze strumienia bajtowego) albo wartość -1 gdy
czytanie sięga poza koniec pliku.
6
Klasy strumieniowe - przykład
●
Możemy teraz użyć metody copy wobec dowolnych strumieni z
odpowiednich konkretnych klas hierarchii klas strumieniowych, np.
StreamCopier.copy(input, output);
Po to by kopiowanie miało sens input musi oznaczać konkretne źródło
danych, a output – konkretny odbiornik danych.
●
●
●
●
Strumień abstrakcyjny (w którymś momencie) musi być związany z
konkretnym źródłem bądź odbiornikiem.
W Javie jest to możliwe głównie (ale nie tylko) dzięki wprowadzeniu na
kolejnych szczeblach dziedziczenia omawianych czterech hierarchii (we-wy,
bajty-znaki) konkretnych klas oznaczających różne rodzaje
źródła/odbiornika danych. Można by je nazwać klasami przedmiotowymi,
bowiem mają one ustalone „przedmioty” operacji – konkretne rodzaje źródła
bądź odbiornika.
Źródła bądź odbiorniki danych mogą być różnorodne. Strumień może być
związany np. z plikiem, z pamięcią operacyjną, z potokiem, z zasobem
sieciowym, z gniazdkiem (socket)....
Klasy przedmiotowe wprowadzono dla wygody operowania na konkretnych
rodzajach źródeł i odbiorników.
7
Klasy przedmiotowe
Źródło/odbiornik
Strumienie znakowe
Strumienie bajtowe
CharArrayReader,
CharArrayWriter
ByteArrayInputStream,
ByteArrayOutputStream
StringReader,
StringWriter
StringBufferInputStream
Potok
PipedReader,
PipedWriter
PipedInputStream,
PipedOutputStream
Plik
FileReader,
FileWriter
FileInputStream,
FileOutputStream
Pamięć
8
Klasy przedmiotowe - przykład
Teraz już możemy użyć przykładowej (pokazanej poprzednio) klasy
StreamCopier np. do kopiowania plików binarnych
public class StreamCopy1 {
public static void main(String[] args) {
try {
InputStream in1 = new FileInputStream("in.dat");
try {
OutputStream out1 = new FileOutputStream("out.dat");
try {
StreamCopier.copy(in1, out1); //kopiowanie
} finally {
out1.close();
}
} finally {
in1.close();
}
} catch (IOException exc) {//brak pliku lub błąd WE-WY
System.err.println("I/O error: " + exc);
System.exit(1);
}
}
}
9
Klasy przedmiotowe - przykład
●
●
Klauzulla finally jest wykonywana niezależnie od tego czy wystapi wyjątek czy
nie, dlatego umieszczamy tam metodę close().
Dla uproszczenia, w Javie 7 wprowadzono instrukcję try-with-resources:
try(otwarcie zasobu1; otwarcie zasobu2; ...){
//przetwarzanie zasobów
}
która powoduje automatyczne zamknięcie zasobów, zarówno przy normalnym
zakończeniu ich przetwarzania, jak i w przypadku wyrzucenia wyjątku.
try(FileReader in1 = new FileReader("plik0.txt");
FileWriter out1 = new FileWriter("plik1.txt");
FileInputStream in2 = new FileInputStream("in");
FileOutputStream out2 = new FileOutputStream("out")){
StreamCopier.copy(in1, out1);
StreamCopier.copy(in2, out2);
}catch(IOException exc) {//brak pliku lub bład WE-WY
}
System.err.println("I/O error: " + exc);
System.exit(1);
10
Klasy przedmiotowe
●
●
●
●
●
●
jedną z wersji konstruktorów klas strumieniowych związanych z plikami są
konstruktory, w których podajemy jako argument nazwę pliku (można też
podać referenecję do obiektu klasy File),
przy tworzeniu obiektów klas strumieniowych związanych z plikami,
odpowiednie pliki są otwierane; strumienie wejściowe są otwierane "tylko do
odczytu", strumienie wyjściowe "tylko do zapisu".
strumienie wyjściowe mogą być otwarte w trybie dopisywania (należy użyć
konstruktora z drugim argumentem append ustawionym na true); w takim
przypadku dane będą dopisywane do końca strumienia,
przy operacjach na strumieniach może powstać wyjątek klasy IOException
oznaczający błąd operacji (np. odczytu lub zapisu), a także wyjątki klas
pochodnych FileNotFoundException (brak pliku) oraz EOFException
(w trakcie operacji czytania lub pozycjonowania osiągnięto koniec pliku),
przy obsłudze wyjątków wejścia-wyjścia czasami warto zastosować metodę
printStackTrace(), która wyprowadza dokładne informacje o przyczynie
i miejscu wystąpienia wyjątku.
Użycie klas przedmiotowych nie jest jedynym sposobem związania
logicznego strumienia z fizycznym źródłem lub odbiornikiem. Inne klasy
(spoza pakietu java.io, np. klasy sieciowe) mogą dostarczać metod, które
zwracają jako wynik referencję do strumienia związanego z konkretnym
źródłem/odbiornikiem (np. plikiem w sieci).
11
Klasy przetwarzające
Rodzaj
przetwarzania
Buforowanie
Filtrowanie
Konwersja: bajtyznaki
Konkatenacja
Strumienie znakowe
Strumienie bajtowe
BufferedReader,
BufferedWriter
FilterReader,
FilterWriter
InputStreamReader,
OutputStreamWriter
BufferedInputStream,
BufferedOutputStream
FilterInputStream,
FilterOutputStream
SequenceInputStream
Zliczanie wierszy
LineNumberReader
ObjectInputStream,
ObjectOutputStream
DataInputStream,
DataOutputStream
LineNumberInputStream
Podglądanie
PushbackReader
PushbackInputStream
Drukowanie
PrintWriter
PrintStream
Serializacja obiektów
Konwersje danych
12
Klasy przetwarzające
●
●
●
●
●
●
Buforowanie ogranicza liczbę fizycznych odwołań do urządzeń zewnętrznych.
Klasy Filter... są klasami abstrakcyjnymi, definiującymi interfejs dla
rzeczywistych filtrów. Filtrami są np.:
–
DataInputStream i DataOutputStream,
–
BufferedInputStream i BufferedOutputStream,
–
–
LineNumberInputStream,
PushbackInputStream,
–
PrintStream,
Można tworzyć własne filtry.
Konwersje bajty-znaki: InputStreamReader czyta bajty ze strumienia
definiowanego przez InputStream (strumień bajtowy) i zamienia je na znaki
(16 bitowe), używając domyślnej lub podanej strony kodowej,
OutputStreamWriter wykonuje przy zapisie konwersję odwrotną.
Konkatenacja strumieni wejściowych pozwala połączyć strumienie i traktować
je jak jeden strumień.
Serializacja służy do "utrwalania" obiektów po to, by odtworzyć je w innym
kontekście (przy ponownym uruchomieniu programu lub w innym miejscu, np.
programie działającym w innym miejscu sieci po przekazaniu "utrwalonego"
obiektu przez socket),
13
Klasy przetwarzające
●
●
●
●
DataInputStream i DataOutputStream pozwalają czytać/pisać dane
typów pierwotnych (np. liczby rzeczywiste) w postaci binarnej. Strumienie są
tutaj strumieniami binarnymi, w związku z tym koniec strumienia rozpoznaje
się jako wyjątek EOFException.
LineNumber... zlicza wiersze strumienia przy czytaniu (i pozwala w każdym
momencie uzyskać informację o numerze wiersza).
PushBack.. pozwala podglądnąć następny znak/bajt w strumieniu bez
"wyciągania" tego znaku/bajtu.
Klasy Print... zawierają wygodne metody wyjścia (np. println).
Niekoniecznie oznacza to drukowanie fizyczne, często wykorzystywane jest w
powiązaniu z innymi strumieniami po to by łatwo wyprowadzać informacje.
Konstruktory klas przetwarzających mają jako argument referencję do obiektów
podstawowych klas abstrakcyjnych hierarchii dziedziczenia (InputStream,
OutputStream, Reader, Writer). Dlatego przetwarzanie (automatyczna
transformacja) danych jest logicznie oderwana od fizycznego strumienia, stanowi
swoistą na niego nakładkę.
Zatem zastosowanie klas przetwarzających wymaga:
●
●
stworzenia obiektu związanego z fizycznym źródłem/odbiornikiem
stworzenie obiektu odpowiedniej klasy przetwarzającej, "nałożonego" na
fizyczny strumień.
14
Buforowanie
Buforowanie ogranicza liczbę fizycznych odwołań do urządzeń zewnętrznych, dzięki
temu, że fizyczny odczyt lub zapis dotyczy całych porcji danych, gromadzonych w
buforze (wydzielonym obszarze pamięci). Jedno fizyczne odwołanie wczytuje dane ze
strumienia do bufora lub zapisuje zawartość bufora do strumienia. W naszym programie
operacje czytania lub pisania dotyczą w większości bufora (dopóki są w nim dane lub
dopóki jest miejsce na dane) i tylko niekiedy powodują fizyczny odczyt (gdy bufor jest
pusty) lub zapis (gdy bufor jest pełny).
●
Np. przy czytaniu dużych plików tekstowych należy unikać bezpośredniego czytania za
pomocą klasy FileReader. To samo dotyczy zapisu.
●
Zastosowanie klasy BufferedReader (czy BufferedWriter) powinno przynieść
poprawę efektywności działania programu. Ale klasa BufferedReader
(BufferedWriter) jest klasą przetwarzającą, a wobec tego w jej konstruktorze nie
możemy bezpośrednio podać fizycznego źródła danych.
●
Np. przy czytaniu plików źródło podajemy przy konstrukcji obiektu typu FileReader, a
po to, żeby uzyskać buforowanie, "opakowujemy" FileReadera BufferedReaderem.
FileReader fr = new FileReader("plik.txt");//żródło
BufferedReader br = new BufferedReader(fr);// dodajemy "opakowanie"
// umożliwiające
buforowanie
String line;// czytamy wiersz po wierszu
while ((line = br.readLine()) != null) { // kolejny wiersz pliku:
//metoda readLine zwraca wiersz lub null jeśli koniec pliku
// ... tu coś robimy z odczytanym wierszem
}
15
●
Buforowanie - przykład
Przykład: program, czytający plik tekstowy i zapisujący jego zawartość do innego pliku
wraz z numerami wierszy.
import java.io.*;
class Lines {
public static void main(String args[]) {
try (LineNumberReader lr =
new LineNumberReader(new FileReader(args[0]));
BufferedWriter bw =
new BufferedWriter(new FileWriter(args[1]))) {
String line;
while ((line = lr.readLine()) != null) {
bw.write(lr.getLineNumber() + " " + line);
bw.newLine();
}
} catch (IOException exc) {
System.err.println(exc.toString());
System.exit(1);
}
}
}
16
Buforowanie
●
●
●
●
●
Klasa LineNumberReader dziedziczy klasę BufferedReader, dając
możliwość prostego uzyskiwania informacji o numerze bieżącego wiersza
(metoda getLineNumber()),
do zapisu tekstu używana jest metoda write(String),
zastosowanie metody newLine() z klasy BufferedWriter pozwala w
niezależny od platformy systemowej sposób zapisywać znak końca wierszy,
przy zamknięciu (close) wyjściowego strumienia buforowanego zawartość
bufora jest zapisywana do strumienia;
istnieje też możliwość "ręcznego" opróżnianienia bufora przy pomocy
metody void flush(), zapisującej dane, które pozostały w buforze, a nie
zostały jeszcze zapisane w miejscu przeznaczenia. Działa ona dla
wszystkich strumieni wyjściowych (bajtowych i znakowych).
17
Strumienie binarne
●
●
●
●
●
Klasy przetwarzające DataInputStream i DataOutputStream służą do
odczytu/zapisu danych typów pierwotnych w postaci binarnej (oraz
łańcuchów znakowych).
Metody tych klas mają postać:
typ readTyp()
void writeTyp(typ arg)
gdzie typ odpowiada nazwie któregoś z typów pierwotnych. Mamy więc np.
metody int readInt(), double readDouble() itp.
Dane typu String mogą być zapisywane/czytane do/z strumieni binarnych
za pomocą metod writeUTF i readUTF.
Przykład. Stwórzmy klasę Obserwacje, której obiekty reprezentują
obserwacje. Każda obserwacaja ma: nazwę oraz odpowiadający jej ciąg
(tablicę) liczb rzeczywistych. Może to być np. maxTemp z 12 liczbami,
pokazującymi maksymalną temperaturę w 12 miesiącach roku. W klasie tej
zdefiniujemy dwie metody służące do zapisu obserwacji w postaci binarnej
do strumienia i odczytywania binarnych strumieni obserwacji.
Format zapisu obserwacji w pliku binarnym:
nazwa
liczba_elementów_tablicy
dane_tablicy
18
Strumienie binarne - przykład
import java.io.*;
class Obserwacje {
String name;
double[] data;
}
public Obserwacje() {}
public Obserwacje(String nam, double[] dat) {
name = nam;
data = dat;
}
public void writeTo(DataOutputStream dout)throws IOException {
dout.writeUTF(name);
dout.writeInt(data.length);
for (int i=0; i<data.length; i++) dout.writeDouble(data[i]);
}
public Obserwacje readFrom(DataInputStream din)throws IOException {
name = din.readUTF();
int n = din.readInt();
data = new double[n];
for (int i=0; i<n; i++) data[i] = din.readDouble();
return this;
}
public void show() {
System.out.println(name);
for (int i=0; i<data.length; i++) System.out.print(data[i] + " ");
System.out.println("");
}
19
Strumienie binarne - przykład
import java.io.*;
class BinDat {
public static void main(String args[]) throws IOException {
double[] a = {1, 2, 3, 4};
double[] b = {7, 8, 9, 10};
//tworzymy dwie obserwacje:
Obserwacje obsA = new Obserwacje("Dane A", a);
Obserwacje obsB = new Obserwacje("Dane B", b);
obsA.show();
obsB.show();
try (DataOutputStream out =
new DataOutputStream(new FileOutputStream("dane"))) {
obsA.writeTo(out); //zapis obserwacji do pliku
obsB.writeTo(out); //zapis obserwacji do pliku
}
try (DataInputStream in =
new DataInputStream(new FileInputStream("dane"))) {
// z tego samego pliku odczytujemy dane do innych obiektów-obserwacji
// i jednocześnie pokazujemy odczytane dane na konsoli
new Obserwacje().readFrom(in).show();
new Obserwacje().readFrom(in).show();
}
}
}
20
Kodowanie
●
●
●
●
●
●
●
Java posługuje się znakami w formacie Unicode. Są to - ogólnie - wielkości 16bitowe.
Środowiska natywne (np. Windows) najczęściej zapisują teksty jako sekwencje
bajtów w różnych systemach kodowania (sposób kodowania nazwamy stroną
kodową). W systemie Windows jest to najczęściej Cp1250 lub UTF-8.
Powstaje zatem problem pogodzenia najczęściej bajtowego charakteru plików
natywnych ze strumieniami znakowymi. Strumienie znakowe FileReader i
FileWriter konwertują - niewidocznie dla nas - bajtowe źródła w znaki
Unicodu i odwrotnie. Wykorzystywane są tu dwie klasy: InputStreamReader i
OutputStreamWriter, które dokonują właściwych konwersji w trakcie
czytania/pisania.
Klasy te możemy wykorzystać również samodzielnie. Jeśli w konstruktorach tych
klas nie podamy strony kodowej - przy konwersjach zostanie przyjęta domyślna
strona kodowa.
Aby się dowiedzieć, jakie jest domyślne kodowanie używamy metody
String p = System.getProperty("file.encoding");
System.out.println(p);
W zależności od ustawień na danej platformie otrzymamy różne wyniki.
Np. ibm-852, Cp852 (Latin 2), Cp1252 (Windows Western Europe /Latin-1) albo
UTF-8.
Inna wersja konstruktorów pozwala na podanie stron kodowych, które będą
używane do kodownia i dekodowania bajty-znaki.
21
Kodowanie - przykład
Przykład. Napiszmy funkcję wykonującą konwersję strumienia wejściowego is o stronie
kodowej inCp do strumienia os o stronie kodowej outCp
import java.io.*;
import java.net.URL;
public class URLToFile {
public static void convert(InputStream is, String inCp,
OutputStream os, String outCp) throws IOException {
try (BufferedReader in =
new BufferedReader(new InputStreamReader(is, inCp));
BufferedWriter out =
new BufferedWriter(new OutputStreamWriter(os, outCp))){
String line;
while ((line = in.readLine()) != null) {
out.write(line);
out.newLine();
}
}
}
public static void main(String[] args) throws IOException {
convert(new URL("http://www.kul.pl").openStream(), "UTF-8",
new FileOutputStream("kul.txt"), "Cp1250");
}
}
22
Obiekty plikowe - klasa File
Klasa File oznacza obiekty plikowe (pliki i katalogi). Jej metody umożliwiają m.in.
uzyskiwanie informacji o plikach i katalogach, jak również wykonywanie działań na
systemie plikowym.
Wybrane metody klasy File
●
●
●
●
●
●
●
●
●
●
●
●
boolean canRead() - czy plik może być czytany?
boolean canWrite()- czy plik może być zapisywany?
boolean createNewFile() - tworzy nowy pusty plik
static File createTempFile(String prefix, String suffix,
File directory)- tworzy nowy plik tymczasowy z nazwą wg wzorca w
podanym katalogu
boolean delete() - usuwa plik lub katalog
void deleteOnExit() - zaznacza plik do usunięcia po zakończeniu
programu
boolean exists() - czy plik/katalog istnieje?
String getName() - nazwa pliku lub katalogu
String getParent() - katalog nadrzędny
String getPath() - ścieżka
boolean isDirectory() - czy to katalog?
boolean isFile() - czy plik?
23
Obiekty plikowe - klasa File
boolean isHidden() - czy ukryty?
●
long lastModified() - czas ostatniej modyfikacji
●
long length() - rozmiar
●
String[] list() - lista nazw plików i katalogów w katalogu
●
String[] list(FilenameFilter filter) – filtrowana lista nazw
plików
●
File[] listFiles() - lista plików i katalogów
●
File[] listFiles(FileFilter filter) - filtrowana lista plików i
katalogów
●
File[] listFiles(FilenameFilter filter)- filtrowana lista plików i
katalogów
●
boolean mkdir() - tworzy katalog
●
boolean renameTo(File dest) – zmienia nazwę/przenosi plik lub
katalog.
●
boolean setReadOnly() - zaznacza jako "tylko od odczytu"
●
URI toURI()- tworzy obiekt klasy URI (Uniform Resource Identifier),
reprezentujący ten obiekt plikowy
FilenameFilter i FileFilter - interfejsy umożliwiające wybiórcze, wg
dowolnie konstruowanych kryteriów, listowanie plików.
●
24
Scanner
Klasa java.util.Scanner pozwala na łatwy rozbiór informacji tekstowej zawierającej
napisy i dane typów prostych.
Możliwości:
●
●
●
●
●
●
●
działa na klasie String, plikach (File), strumieniach, kanałach, np.
Scanner sc = new Scanner(System.in);
Scanner sc1 = new Scanner(new File("myNumbers"));
String input = "1 fish 2 fish red fish blue fish";
Scanner s = new Scanner(input).useDelimiter("\\s*fish\\s*");
do parsowania używa wyrażeń regularnych (w tym prostych separatorów, ale również
dowolnych złożonych wyrażeń),
łatwo rozbija teksty na wiersze (String nextLine(), boolean
hasNextLine()),
umie wyróżnić i skonwertować dane typów prostych (a także BigDecimal),
pozwala na rozbiór, polegający nie tylko na wyróżnianiu symboli rozdzielonych
separatorami, ale również na wyróżnianiu symboli pasujących do podanego wyrażenia
regularnego (metoda findInText(...), metoda skip(...)),
sposób rozbioru można zmieniać w trakcie skanowania tekstu, m.in. stosując rozliczne
metody next...(), w tym takie, które pozwalają podawać różne wyrażenia
regularne.
pozwala na zlokalizowany rozbiór danych.
25
Scanner
Wybrane metody:
●
String next() - pobieranie kolejnych elementów (ang. token) (napisów
rozdzielonych separatorem – domyślnie białe znaki)
●
boolean hasNext() - sprawdza czy jest dostępny kolejny element
●
String nextLine() - pobieranie kolejnych wierszy
●
boolean hasNextLine() - sprawdza czy jest kolejna linia
●
int nextInt() - pobieranie kolejnego elementu jako liczbę całkowitą
●
●
●
●
boolean hasNextInt() - sprawdzanie czy następny element jest liczbą
całkowitą
int nextDouble() - pobieranie kolejnego elementu jako liczbę
rzeczywistą
boolean hasNextDouble() - sprawdzanie czy następny element jest
liczbą rzeczywistą
Scanner useDelimiter(String regex) - ustawia separator skanera
na separator skonstruowany na podstawie parametru
26
Skaner - przykład
import java.util.*;
class Employee {
String name;
double salary;
Employee(String n, double s) {
name = n; salary = s;
}
public double getSalary() { return salary; }
public String toString() { return name + " " + salary; }
}
27
Skaner - przykład
public class Skaner1{
public static void main(String[] args) {
String s1 = "1 2 3";
String s2 = "Jan Kowalski\t1200\nA. Grabowski\t1500";
Scanner scan1 = new Scanner(s1);
int suma = 0;
while (scan1.hasNextInt()) suma += scan1.nextInt();
System.out.println("Suma = " + suma);
List<Employee> list = new ArrayList<>();
Scanner scan2 = new Scanner(s2);
while (scan2.hasNextLine()) {
Scanner scan3 = new Scanner(scan2.nextLine()).useDelimiter("\\t");
String name = scan3.next();
double salary = scan3.nextDouble();
list.add(new Employee(name, salary));
}
double value = 0;
for (Employee emp : list) {
value += emp.getSalary();
System.out.println(emp);
}
System.out.println("Suma zarobków: " + value);
}
}
28
Wyrażenia regularne- podstawy
Wyrażenie regularne stanowi opis wspólnych cech (składni) zbioru łańcuchów
znakowych.
Możemy sobie wyobrażać, że wyrażenie regularne jest pewnym wzorcem, który
opisuje jeden lub wiele napisów, pasujących do tego wzorca. Wzorzec taki
zapisujemy za pomocą specjalnej składni wyrażeń regularnych.
Najprostszym wzorcem jest po prostu sekwencja znaków, które nie mają
specjalnego znaczenia (sekwencja literałów).
Np. wyrażenie regularne abc stanowi wzorzec opisujący trzy występujące po
sobie znaki: a, b i c. Wzorzec ten opisuje jeden napis "abc".
We wzorcach możemy stosować znaki specjalne (tzw. metaznaki) oraz
tworzone za ich pomocą konstrukcje składniowe. Do znaków specjalnych
należą:
$ ^ . * +
? [ ] ( )
{ } \
Uwagi:
●
●
jesli chcemy traktować znaki specjalne jako literały - poprzedzamy je
odwrotnym ukośnikiem \.
w niektórych konstrukcjach składniowych metaznaki tracą specjalne
znaczenie i są traktowane literalnie.
29
Wyrażenia regularne- podstawy
Za pomocą znaków specjalnych i tworzonych za ich pomocą bardziej
rozbudowanych konstrukcji składniowych opisujemy m.in.
●
●
●
●
wystąpienie jednego z wielu znaków - odpowiednie konstrukcje składniowe
noszą nazwę klasy znaków (np. litery lub cyfry),
początek lub koniec ograniczonego ciągu znaków (np. wiersza lub słowa) granice,
powtórzenia - w składni wyrażeń regularnych opisywane przez tzw.
kwantyfikatory,
logiczne kombinacje wyrażeń regularnych.
Np. wyrażenie regularne [0-9] stanowi wzorzec opisujący jeden znak, który
może być dowolną cyfrą 0,1,2,... ,9. Wzorzec ten opisuje wszystkie napisy
składające się z jednej cyfry.
A wyrażenie regularne a.*z (a, kropka, gwiazdka, z) opisuje dowolną
sekwencję znaków, zaczynających się od litery a i kończących się literą z. Do
wzorca tego pasują np. następujące napisy: "az", "abz", "a x y z".
30
Wyrażenia regularne- podstawy
Wyrażeń regularnych możemy użyć m.in. do:
●
stwierdzenia czy dany napis pasuje do podanego przez wyrażenie wzorca,
●
stwierdzenia czy dany napis zawiera podłańcuch znakowy pasujący do
podanego wzorca i ew. uzyskania tego podnapisu i/lub jego pozycji w
napisie,
●
zamiany części napisu, pasujących do wzorca na inne napisy,
●
wyróżniania części napisu, które są rozdzielane ciagami znaków posującymi
do podanego wzorca.
W Javie służą do tego klasy pakietu java.util.regex: Pattern i Matcher.
Przed zastosowaniem wyrażenia regularnego do składniowej analizy jakiegoś
napisu musi ono być skompilowane. Obiekty klasy Pattern reprezentują
skompilowane wyrażenia regularne, a obiekty te uzyskujemy za pomocą
statycznych metod klasy Pattern - compile(...), mających za argument
wyrażenie regularne.
Obiekty klasy Matcher wykonują operacje wyszukiwania w tekście za pomocą
interpretacji skompilowanego wyrażenia regularnego i dopasowywania go do
tekstu lub jego częsci.
31
Wyrażenia regularne- podstawy
Obiekt-matcher jest zawsze związany z danym wzorcem. Zatem uzyskujemy go
od obiektu-wzorca za pomocą metody matcher(...) klasy Pattern,
podając jako jej argument przeszukiwany tekst.
Następnie możemy dokonywać różnych operacji przeszukiwania i zastępowania
tekstu poprzez użycie różnych metod klasy Matcher.
W szczególności:
●
●
metoda matches() stara się dopasować do wzorca cały podany łańcuch
znakowy,
metoda find() przeszukuje wejściowy łańcuch znakowy i wyszukuje
kolejne pasujące do wzorca jego podłańcuchy.
Wszystkie metody dopasowania/wyszukiwania zwracają wartości typu
boolean, stwierdzające dopasowanie (true) lub jego brak (false). Więcej
informacji o dopasowaniu (jaki konkretnie tekst pasuje do wzorca, gdzie jest
jego początek, a gdzie koniec itp.) można uzyskać odpytując matcher o aktualny
jego stan za pomocą odpowiednich metod.
32
Wyrażenia regularne- podstawy
Typową sekwencję operacji, potrzebnych do zastosowania wyrażeń regularnych
można opisać w następujący schematyczny sposób.
A) Tekst, podlegający dopasowaniu może być reprezentowany przez obiekt
dowolnej klasy implementującej interfejs CharSequence (np. String,
StringBuffer, CharBuffer z pakietu java.nio) np:
String text = "ala-127";
B) Tworzymy wyrażenie regularne jako napis np.
String regexp = "[0-9]";
C) Kompilujemy wyrażenie regularne i uzyskujemy skompilowany wzorzec.
Pattern pattern = Pattern.compile(regexp);
D) Tworzymy obiekt-matcher związany z danym wyrażeniem, podając przy tym
tekst do dopasowania:
Matcher matcher = pattern.matcher(text);
E) Szukamy dopasowania tekstu ( w tekście ) zgodnie ze wzorcem np.
boolean hasMatch = matcher.find();
albo:
boolean isMatching = matcher.matches();
33
Wyrażenia regularne- podstawy
Zdefiniujmy metodę, która jako argumenty otrzymuje wyrażenie regularne oraz
analizowany za jego pomocą tekst, a zwraca opis wyników działania metod matches()
i find().
String report(String regex, String text) {
String result = "Wzorzec: \"" + regex + "\"\n" +
"Tekst: \"" + text + "\"";
// Kompilacja wzorca
// Gdy wzorzec jest składniowo błędny
// wystąpi wyjątek PatternSyntaxException
Pattern pattern = null;
try {
pattern = Pattern.compile(regex);
} catch (Exception exc) {
return result + "\n" + exc.getMessage();
// zwracamy komunikat o błędzie
}
34
Wyrażenia regularne- podstawy
// Uzyskanie matchera dla podanego tekstu
Matcher matcher = pattern.matcher(text);
// Próba dopasowania całego tekstu do wzorca
boolean isMatching = matcher.matches();
result += "\nmatches(): Cały tekst" +
(isMatching ? "" : " NIE") +
" pasuje do wzorca.";
// Przywrócenie początkowej pozycji matchera
matcher.reset();
//
//
//
//
//
Teraz stosujemy metodę find()
Jej wywołanie zwraca true po znalezieniu pierwszego
pasującego do wzorca podłańcucha w tekście
Kolejne wywołania pozwalają wyszukiwać kolejne pasujące
podłańcuchy.
35
Wyrażenia regularne- podstawy
// Wynik false oznacza, że w tekście nie ma już
// pasujących podłańcuchów
boolean found = matcher.find();
if (!found)
result += "\nfind():Nie znaleziono żadnego podłańcucha "
+"pasującego do wzorca";
else
do {
result += "\nfind(): Dopasowano podłańcuch \"" +
matcher.group() +
"\" od pozycji " + matcher.start() +
" do pozycji " + matcher.end() + ".";
} while(matcher.find());
return result;
}
36
Wyrażenia regularne- podstawy
Literały. Literały użyte w wyrażeniu regularnym są dopasowywane po kolei.
Na przykład wyrażenie regularne:
String regexp = "ala";
jest interpretetewane w następujący sposób: w podanym łąńcuchu wejściowym
wyszukiwane są kolejno występujące po sobie znaki 'a', 'l' i 'a'. Wynikiem
metody matches() jest true tylko wtedy, gdy cały tekst pasuje do tego wzorca ("ala"),
metoda find() umożliwia znalezienie w tekście wielu podłańcuchów "ala".
Jeśli chcemy wyszukiwać w tekście literalne znaki specjalne to w wyrażeniu
regularnym powinniśmy je poprzedzić odwrotnym ukośnikiem (nie dotyczy to
metaznaków użytych w definicji klas znaków
Klasy znaków. Stosując nawiasy kwadratowe możemy wprowadzać w wyrażeniu
regularnym tzw. klasy znaków. Prosta klasa znaków stanowi ciąg znaków ujętych w
nawiasy kwadratowe
np. [123abc]
Matcher dopasuje do takiego wzorca dowolny z wymienionych znaków. Jest to w
istocie skrót zapisu 1|2|3|a|b|c.
Jeśli pierwszym znakiem w nawiasach kwadratowych jest ^, to dopasowanie nastąpi
dla każdego znaku oprócz wymienionych na liście. Jest to swoista negacja klasy
znaków. Np. do wzorca [^abc] będzie pasował każdy znak oprócz a, b i c.
37
Wyrażenia regularne- podstawy
Możliwe jest także formułowanie zakresów znaków (co już znacznie ułatwia
zapis). Przy formułowaniu zakresów używamy naturalnego symbolu -.
Przykładowe wzorce:
●
[0-9] - dowolna cyfra,
●
[a-zA-Z] - dowolna mała i duża litera alfabetu angielskiego.
●
[a-zA-Z0-9] - dowolna cyfra lub litera
i nieco bardziej skomplikowany przykład, w którym po slowie Numer powinna
następować spacja, potem zaś jedna z cyfr 1,2,3,7,8,9, następnie dowolna
cyfra, ukośnik i dowolny znak oprócz cyfr 0,1,2,3 oraz malej litery alfabetu
angielskiego:
"Numer [1-37-9][0-9]/[^0-3a-z]"
Należy pamiętać, że klasa znaków określa jeden znak należący (lub nie) do
podanego w nawiasach kwadratowych zestawu. Kolejność znaków w zestawie
nie jest istotna, ale zakresy muszą być podawane w porządku rosnącym.
38
Wyrażenia regularne- podstawy
Ułatwieniem zapisu zakresów są tzw. klasy predefiniowane:
●
. Dowolny znak (w zależności od opcji kompilacji wzorca może pasować lub
nie do znaku końca wiersza)
●
\d Cyfra: [0-9]
●
\D Nie-cyfra: [^0-9]
●
\s "Biały" znak: [ \t\n\x0B\f\r]
●
\S Każdy znak, oprócz "białego": [^\s]
●
\w Jeden ze znaków: [a-zA-Z0-9], znak "dopuszczalny w słowie"
●
\W Znak nie będący literą lub cyfrą [^\w]
Uwaga: ogólna reguła - klasy wprowadzane przez duże litery stanowią negację klas
definiowanych przez małe litery.
Bardzo ważną predefiniowaną klasą jest "klasa wszystkich znaków", podawana jako
kropka. Za jej pomocą możemy dopasować dowolny znak.
Przykład: wzorzec który dopasowuje teksty składające się z trzech dowolnych cyfr,
następujących po nich trzech dowolnych znaków i dwóch znaków, nie będących
cyframi.:
Wzorzec: "\d\d\d...\D\D"
39
Wyrażenia regularne- podstawy
Pewnym rozszerzeniem podstawowych predefiniowanych klas są klasy
znakowe, zdefiniowane w standardzie POSIX.
●
\p{Lower} Mała litera: [a-z]
●
\p{Upper}
Duża litera: [A-Z]
●
\p{ASCII}
Dowolny znak ASCII :[\x00-\x7F]
●
\p{Alpha}
Dowolna litera: [\p{Lower}\p{Upper}]
●
\p{Digit}
Cyfra: [0-9]
●
\p{Alnum}
Cyfra bądź litera: [\p{Alpha}\p{Digit}]
●
\p{Punct}
Znak punktuacji: ! "#$%&'()*+,-./:;<=>?@[\]^_`{|}~
●
\p{Graph}
Widzialny znak: [\p{Alnum}\p{Punct}]
●
\p{Print}
Drukowalny znak: [\p{Graph}]
●
\p{Blank}
Spacja lub tabulacja: [ \t]
●
\p{Cntrl}
Znak sterujący: [\x00-\x1F\x7F]
●
\p{XDigit} Cyfra szesnastkowa: [0-9a-fA-F]
●
\p{Space}
Biały znak: [ \t\n\x0B\f\r]
Wypróbujmy tę składnię na przykładzie wzorca, który opisuje tekst zaczynający
się od dowolnej litery, z następującym po niej dowolnym znakiem punktuacji i
dowolną cyfrą.
Wzorzec: "\p{Alpha}\p{Punct}\d"
40
Wyrażenia regularne- podstawy
Do "dopasowywania" znaków Unicodu służą odrębne predefiniowane klasy, np.:
\p{L}
Dowolna litera (Unicode)
\p{Lu}
Dowolna duża litera (Unicode)
\p{Ll}
Dowolna mała litera
\p{Sc}
Symbol waluty
\p{InNazwaBlokuUnicode} Znak należący do podanego bloku Unicode
Np. wzorzec \p{Ll} będzie pasował do dowolnego znaku Unicode, który jest małą
literą, zatem np. do polskich znaków ą, ć, ś itd. (ale oczywiście nie tylko).
Jeśli chodzi nam o znaki tylko z konkretnych bloków Unicode'u (np. alfabetu
greckiego, lub cyrylicy) stosujemy ostatni z podanych w tabeli wzorców, podając po
słowie In (bez spacji) nazwę bloku np.
\p{InGreek}
\p{InCyrillic}
41