Pobierz prezentację
Pobieranie prezentacji. Proszę czekać
OpublikowałTymon Szerszeń Został zmieniony 11 lat temu
1
Napisy Oczywiście, napisy (łańcuchy znakowe) są obiektami klasy String, zatem wszystko co dotąd powiedziano o obiektach i referencjach dotyczy także obiektów klasy String. Dodatkowo jednak, ponieważ operacje na łańcuchach znakowych są w programowaniu dość częste, kompilator dostarcza nam tu pewnych udogodnień. Powtórzmy. Gdy napiszemy String napis; to w tym momencie nie będzie jeszcze żadnego łańcucha znakowego, jedynie zmienna napis, która może zawierać referencję do obiektu (ale jeszcze nie zawiera).
2
Tak jak w przypadku każdej innej klasy obiekty klasy String musimy bezpośrednio tworzyć. Pierwsza udogodnienie polega na tym, że zamiast: String s = new String("Ala ma kota"); możemy napisać: String s = "Ala ma kota"; Zapis ten spowoduje: stworzenie obiektu klasy String z zawartością "Ala ma kota„ przypisanie referencji do tego obiektu zmiennej s Zatem, wyjątkowo, tworząc obiekty klasy String nie musimy używać wyrażenia new. Innym (wyjątkowym) udogodnieniem przy korzystaniu z łańcuchów znakowych jest możliwość użycia operatora + w znaczeniu konkatenacji (łączenia łańcuchów znakowych). Np. String s1 = "Ala ma kota"; String s2 = " szaroburego"; String s3; s3 = s1 + s2;
3
spowoduje, że: wyrażenie s1 + s2 stworzy (nowy) obiekt klasy String, który jest połączeniem napisów oznaczanych przez zmienne s1 i s2, referencja do nowoutworzonego obiektu zostanie przypisana zmiennej s3, s3 będzie teraz oznaczać napis "Ala ma kota szaroburego". Co więcej, za pomocą operatora + do łańcuchów znakowych możemy dołączać innego rodzaju dane, np. liczby (a także dane reprezentujące obiekty dowolnych klas). Na przykład: String s1 = "Ala ma kota"; String s2 = " szaroburego"; String s3; s3 = s1 + s2 + " w wieku " " lat "; Teraz zmienna s3 będzie oznaczać napis "Ala ma kota szaroburego w wieku 3 lat". Oczywiście, nic nie stoi na przeszkodzie, by w konkatenacji zamiast literału 3 pojawiła się zmienna typu int o wartości 3. Np. int lata = 3; ... s3 = s1 + s2 + " w wieku " + lata + " lat ";
4
Zwróćmy uwagę, zmienna lata lub literał 3 w wyrażeniu konkatenacji łańcuchów znakowych są typu int. Przy opracowaniu wyrażenia (wyliczeniu jego wyniku) następuje przekształcenie wartości zmiennej lub literału (dziesiętne 3, binarne to jest tzw. wewnętrzna reprezentacja wartości) w kod znaku Unicodu (dziesiętnie 33) i dzięki temu znak cyfry ('3') pojawi się w łańcuchu znakowym (znak cyfry 3 jest znakową reprezentacją wartości 3). To samo dotyczy innych wartości numerycznych (typów float, double, itp.) Jeśli w wyrażeniu konkatenacji łańcuchów znakowych wystąpi referencja do obiektu jakiejś klasy, to obiekt (dane zawarte w obiekcie) zostaną za pomocą metody toString() przekształcone do postaci znakowej i dołączone do łańcucha. Na przykład jeśli napiszemy: Para p = new Para(10,11); String s = "Ta para jest równa" + p; to w wyrażeniu konkatenacji zostanie automatycznie wywołana metoda toString() z klasy Para i dostaniemy w wyniku napis: Ta para jest równa ( 10, 11)
5
Należy także zwrócić uwagę na dwie ważne kwestie:
Po pierwsze, operator + jest traktowany jako operator konkatenacji łańcuchów znakowych tylko wtedy, gdy jeden z jego argumentów jest typu String Zatem np. takie fragmenty będą niepoprawne: String s = 1 + 3; wynikiem prawej strony operatora przypisania jest liczba 4 (typ int), a danej typu int nie można podstawić na zmienną typu referencyjnego (którą jest s) int a = 1, b = 3; String s = a + b; j.w. Po drugie, przy konkatenacji należy baczną uwagę zwracać na kolejność opracowywania wyrażeń Np. String s = "Nr " ; da napis "Nr 12", bo: najpierw zostanie wyliczone wyrażenie "Nr " + 1, co w wyniku da napis "Nr 1", po czym drugi operator + dołączy do tego napisu napis "2" (znakową wartość liczby 2).
6
Natomiast: String s = 100 + " Nr " + (1 +2); da napis "100 Nr 3", bo:
najpierw będzie opracowane wyrażenie " Nr " (jego wynik - napis " 100 Nr ") następnie zostanie opracowane wyrażenie (ponieważ nawiasy zmieniają kolejność opracowania wyrażeń), a jego wynikiem będzie liczba 3 w końcu zostanie zastosowany drugi operator +, który do wyniku pierwszego wyrażenia (napisu "100 Nr ") dołączy przekształconą do postaci znakowej wartość drugiego wyrażenia (liczbę 3) Przy operowaniu na łańcuchach znakowych trzeba szczególnie pamiętać, że dostęp do nich uzyskujemy za pomocą referencji, co ma swoje konsekwencje przy operacjach porównania na równość - nierówność. Jeszcze raz: Operatory równości (==) i nierówności (!=) zastosowane wobec zmiennych oznaczających obiekty , porównują referencje do obiektów, a nie zawartość obiektów
7
Zatem poniższy fragment:
String s1 = "Al"; String s2 = "a"; String s3 = s1 + s2; String s4 = "Ala"; System.out.println(s3 + " " + s4); if (s3 == s4) System.out.println("To jest Ala"); else System.out.println("To nie Ala"); Wyprowadzi (wbrew intuicyjnym oczekiwaniom): Ala Ala To nie Ala Zwróćmy uwagę: zawartością obiektu oznaczanego przez s3 jest napis "Ala". Również - zawartością obiektu oznaczanego przez s4 jest taki sam napis "Ala". Ale porównanie obu zmiennych da wartość false, bo s3 wskazuje na inny obiekt niż s4. Porównanie byłoby prawdziwe tylko wtedy, gdyby s3 wskazywało na ten sam obiekt co s4. Do porównywania łańcuchów znakowych (ich zawartości) nie należy używać operatorów == i !=
8
Ktoś mógłby powiedzieć: czyżby? A co z takim fragmentem programu?
String s1 = "Ala"; String s2 = "Ala"; if (s1 == s2) System.out.println("To jest Ala"); Cóż - wyprowadzi on napis "To jest Ala", jakby wbrew sformułowanej przed chwilą regule. Ale dzieje się tak tylko dlatego, że wszystkie literały łańcuchowe mające ten sam tekst, są jednym i tym samym obiektem. Obiekty-literały są tworzone w fazie kompilacji i dodawane do puli literałów. W instrukcji: s1 = "Ala"; zmiennej s1 jest przypisywana referencja do literału "Ala". To samo dzieje się w instrukcji s2 = "Ala"; zatem obie zmienne wskazują ten sam obiekt. Dlatego porównanie da wartość true. Jednak nie należy wykorzystywać tej właściwości języka i zawsze, zamiast operatora == należy stosować polecenie equals
9
String, StringBuffer i StringBuilder
Istotnym zagadnieniem w każdym języku programowania oraz w większości programów jest przetwarzanie ciągów znaków – Stringów. W języku Java Stringi zostały utworzone jako niemodyfikowalne – oznacza, to, że nie możemy na przykład dynamicznie dodawać do tego samego ciągu znaków czegoś nowego, aby tego dokonać zawsze musimy utworzyć nowy obiekt String. Aby to zobrazować spójrzmy na przykład: String s = "Kasia"; s = s+" i Tomek";
10
Utworzyliśmy jeden obiekt typu String, a następnie za pomocą operatora konkatenacji (łączenia) + dodaliśmy do niego kolejny człon. Tak naprawdę nie zmodyfikowaliśmy jednak obiektu s, a przypisaliśmy do niego całkiem nową referencję. Gdy używamy operatora + w przypadku obiektów String, tak naprawdę najpierw tworzymy nowy obiekt StringBuffer, następnie wywołujemy jego metodę append() z argumentem w postaci liczby, innego ciągu znaków, lub pojedynczego znaku. Na końcu musimy taki obiekt zamienić oczywiście z powrotem na String za pomocą metody toString(). Podany przykład wygląda więc tak naprawdę(niejawnie) następująco: String s = "Kasia"; s = new StringBuffer(s).append(" i Tomek").toString(); System.out.println(s);
11
Spójrzmy więc na przykład i zastanówmy się jak można by go usprawnić.
Jak widzimy złożoność obliczeniowa jest w tym wypadku po prostu fatalna i jeśli ktoś chciałby używać operatora + w przypadku Stringów na przykład w pętli to jest to bardzo złą praktyka programistyczną. Spójrzmy więc na przykład i zastanówmy się jak można by go usprawnić. public class Strings1 { public static void main(String[] args) { String s = "a"; long start = System.nanoTime(); for(int i=0; i<10000; i++) { s = s+"a"; //s = new StringBuffer(s).append("a").toString(); } System.out.println("Time1: "+(System.nanoTime()-start));
12
Jak widzimy nie wykonujemy tutaj zbyt skomplikowanej operacji dodajemy jedynie na końcu naszego Stringa razy literę „a” (równie dobrze mógłby to być znak ‚a’). W komentarzu widać co tak naprawdę się dzieje. Konkretnie tworzymy obiektów typu StringBuffer oraz na każdym z nich wywołujemy 3 metody (w sumie to 2 i konstruktor). Dodatkowo wyświetlamy czas w nanosekundach jaki zajęło wykonanie tego przykładu – przyda się do porównania dalszych, efektywniejszych rozwiązań. Jak sobie z tym problemem poradzić? Przecież można utworzyć tylko jeden obiekt StringBuffer jeszcze przed pętlą, a wewnątrz niej wywoływać jedynie metodę append(), na końcu przypisując do s wynik metody toString().
13
Poprawmy więc nasz przykład:
public class Strings2 { public static void main(String[] args) { String s = "a"; long start = System.nanoTime(); StringBuffer sB = new StringBuffer(s); for(int i=0; i<10000; i++) { sB.append("a"); } s = sB.toString(); System.out.println("Time2: "+(System.nanoTime()-start)); Porównajmy jeszcze czasy jakie uzyskałem na swoim komputerze (Intel Dual Core T4200, 3GB DDR2): Time1: Time2:
14
Jak widać zyskaliśmy aż 2 rzędy czasu, konkretnie metoda z wykorzystaniem jednego obiektu StringBuffer wykonała się ok 55 razy szybciej niż program używający +. Co ciekawe programiści Javy dają nam do dyspozycji jeszcze jedną klasę do „modyfikowania” ciągów znaków. Jest to konkretnie StringBuilder. Posiada ona identyczne metody jak StringBuffer i na pierwszy rzut oka nie widać między nimi żadnej różnicy, ponieważ jest ona dosyć subtelna – StringBuffer jest klasą synchroniczną, natomiast StringBuilder nie. W drugim przypadku powoduje to jeszcze dodatkowe przyspieszenie i na poziomie początkującego programisty i programów jednowątkowych jest najlepszym rozwiązaniem. Sprawdźmy więc co zyskujemy dzięki jej zastosowaniu.
15
public static void main(String[] args) { String s = "a";
public class Strings { public static void main(String[] args) { String s = "a"; long start = System.nanoTime(); StringBuilder strB = new StringBuilder(s); for(int i=0; i<10000; i++) strB.append("a"); s = strB.toString(); System.out.println("Time3: "+(System.nanoTime()-start)); } W tym przypadku czas wyniósł: Time3: Zestawiając wszystkie 3 metody: Time1: Time2: Time3: Jak widać klasa StringBuilder okazała się dodatkowo ponad 1,5 razy szybsza od StringBuffer. Najważniejsze jest to, aby używać operatora + na Stringach, gdy nie kosztuje nas to ani dużo czasu, ani pamięci – czyli w zasadzie jedyne słuszne miejsce, gdzie można to robić to wnętrze metody print i podobnych. W każdym innym przypadku powinniśmy używać klasy StringBuffer, lub StringBuilder, szczególnie, gdy wielokrotnie dodajemy coś do naszego obiektu.
16
Metody klasy String W praktycznych programach bardzo często będziemy operować na łańcuchach znakowych (napisach). Wiemy doskonale, że są one reprezentowane przez obiekty klasy String. W klasie tej znajdziemy wiele użytecznych metod przeznaczonych do operowania na łańcuchach znakowych. Dokumentację klas i ich metod standardowych pakietów Javy znajdziemy w podkatalogu docs katalogu instalacyjnego Javy. Jest ona w postaci HTML: klasy podzielone są według pakietów a także dostępna jest alfabetyczna lista wszystkich klas.
17
Dla wygody poniżej przedstawiono wybrane metody klasy String
Dla wygody poniżej przedstawiono wybrane metody klasy String. Zwróćmy uwagę, że: kolejne znaki napisów występują na pozycjach, które są indeksowane poczynając od 0: np. napis "Ala" ma trzy znaki na pozycjach 1, 2, 3; pierwsza pozycja ma indeks 0, druga - 1, trzecia 2. Możemy też powiedzieć, że pierwszy znak ma indeks 0, a ostatni - indeks o 1 mniejszy od długości napisu części napisów (łańcuchów znakowych) określa się terminem "podłańcuch" (substring) większość z omawianych dalej metod (wszystkie metody niestatyczne) używana jest "na rzecz" obiektów klasy String; o obiekcie na rzecz którego wywołano metodę mówimy ten napis przedstawiono tu nie wszystkie metody klasy String, a jedynie te najbardziej użyteczne.
18
Wybrane metody klasy String
char charAt(int index) Zwraca znak na pozycji, oznaczonej indeksem index. Pierwsza pozycja ma indeks 0. int compareTo(String anotherString) Porównuje dwa napisy: ten (this) na rzecz którego użyto metody oraz przekazany jako argument. Metoda zwraca 0, gdy napisy są takie same. Jeżeli się różnią, to - gdy występują w nich różne znaki - zwracana jest wartość: this.charAt(k) - anotherString.charAt(k), gdzie k - indeks pierwszej pozycji, na której występuje różnica znaków. Jeżeli długość napisów jest różna (a znaki napisów są takie same w części określanej przez dlugośc krótszego napisu) - zwracana jest różnica dlugości: this.length() - anotherString.length(). Oznacza to, że wynik jest ujemny, gdy ten (this) łańcuch poprzedza leksykograficznie (alfabetycznie) argument (anothetString) oraz dodatni - gdy ten łańcuch jest leksykograficznie większy od argumentu. compareToIgnoreCase(String str) Porównuje leksykograficznie dwa napisy, bez rozróżnienia małych i wielkich liter. boolean endsWith(String suffix) Zwraca true, gdy napis kończy się łańcuchem znakowym podanym jako argument, false - w przeciwnym razie. equals(Object anObject) Zwraca true gdy anObject jest takim samym co do zawartości napisem jak ten napis; w każdym innym przypadku - zwraca false.
19
boolean equalsIgnoreCase(String anotherString) J.w. - ale bez rozróżniania małych i wielkich liter. Int indexOf(String str) Zwraca indeks pozycji pierwszego wystąpienia w danym napisie napisu podanego jako argument str; jeżeli str nie występuje w tym napisie - zwraca -1 int indexOf(String str, int fromIndex) Poszukuje pierwszego wystąpienia napisu str poczynając od pozycji oznaczonej przez indeks fromIndex; zwraca indeks pozycji na której zaczyna się str lub - 1 gdy str nie występuje w tym napisie. Jeśli fromIndex jest ujemne lub zero - przeszukiwany jest cały napis; jeśli fromIndex jest większe od długości napisu - zwracane jest -1. lastIndexOf(String str) Jak indexOf - ale zwracany jest indeks pozycji ostatniego wystąpienia. lastIndexOf(String str, int fromIndex) J.w. Uwaga: metody indexOf i lastIndexOf mają również swoje wersje dla argumentów - znaków (typu char). length() Zwraca długość napisu. String replace(char oldChar, char newChar) Zwraca nowy obiekt klasy String, w którym zastąpiono wszystkie wystąpienia znaku oldChar na znak newChar.
20
String replace(CharSequence target, CharSequence replacement) Zwraca nowy obiekt klasy String, w którym zastąpiono wszystkie wystąpienia podnapisu target na napis replacement. replaceAll(String regex, String replacement) Zwraca nowy obiekt klasy String, w którym zastąpiono wszystkie wystąpienia podnapisów pasujących do wzorca podanego przez wyrażenie regularne regex na napis target. split(String regex) Rozkłada napis na jego podnapisy rozdzielone dowolnymi separatorami, pasującymi do wzorca regex. boolean startsWith(String prefix) Zwraca true, gdy napis zaczyna się podanym jako argument łańcuchem znakowym; false - w przeciwnym razie. startsWith(String prefix, int toffset) Zwraca true, gdy podłańcuch tego łańcucha znakowego zaczynający się na pozycji o indeksie toffset zaczyna się napisem prefiks; zwraca false w przeciwnym razie, lub gdy toffset jest < 0 albo większy od dlugości napisu. substring(int beginIndex) Zwraca podłańcuch tego łańcucha znakowego zaczynający się na pozycji o indeksie beginIndex (do końca łańcucha). substring(int beginIndex, int endIndex) Zwraca podłańcuch tego łańcucha jako nowy obiekt klasy String. Podłańcuch zaczynay się na pozycji o indeksie beginIndex, a kończy (uwaga!) - na pozycji o indeksie endIndex-1. Długość podlańcucha równa jest endIndex - beginIndex.
21
char[] toCharArray() Znaki łańcucha -> do tablicy znaków (typ char[]). String toLowerCase() Zamiana liter na małe. toUpperCase() Zamiana liter na duże. trim() Usuwa znaki spacji, tabulacji, końca wiersza itp. tzw. biale znaki z obu końców łańcucha znakowego. Zwraca wynik jako nowy łańcuch. static String valueOf(boolean b) Zwraca wartość boolowską (boolean) jako napis (String). valueOf(char c) Zwraca wartość typu char jako napis. valueOf(char[] data) Zwraca napis złożony ze znakow tablicy. valueOf(double d) Zwraca znakową treprezentację liczby typu double. valueOf(float f) Zwraca znakową treprezentację liczby typu float. valueOf(int i) Zwraca znakową treprezentację liczby typu int. valueOf(long l) Zwraca znakową reprezentację liczby typu long.
22
W pierwszym przykładowym programie wykorzystamy metodę charAt(), zwracającą znak znajdujący się w napisie na podanej pozycji oraz metody length() i equals(). Problem: napisać program, który prosi użytkownika o wybranie jednej z możliwych wycieczek oznaczanych dużymi literami A, B, C ..., po czym podaje cenę tej wycieczki. Miejsca docelowe wycieczek oraz ich ceny mają być zapisane w tablicach, np.: String[] dest = { "Bali", "Cypr", "Ibiza", "Kenia", "Kuba" }; double[] price = { 5000, 2500, 2800, 4500, 6000 }; a program winien dawać użytkownikowi możliwość wyboru za pomocą pokazanego obok okna dialogowego. A zatem użytkownik wprowadza napis, składający się z jednej litery "A" lub "B" lub "C', ... itd. Dalsze działanie programu zależy od tego jaką literę wprowadził.
23
Jeśli res oznacza wprowadzony napis, to moglibyśmy np. napisać: if (res.equals("A")) System.out.println(dest[0] + " - cena: " + price[0]); else if (res.equals("B")) System.out.println(dest[1] + " - cena: " + price[1]); else if (res.equals("C")) .. else if (res.equals("D")) ... else if (res.equals("E")) .. else ... Ale jest to dość uciążliwe i nieeleganckie. Narażone na błędy. Trudne do modyfikacji. A przecież wprowadzona litera daje nam natychmiastowe odniesienie do odpowiednich elementów tablic dest i price. Litera to znak. Znak ma swój kod. Kod jest liczbą. Łatwo jest więc przekształcić znaki w odpowiednie indeksy tablic. Znak A powinien dać indeks 0, znak B - indeks 1, znak C - indeks 2. Zauważmy, że: 'A' - 'A' = 0 , 'B'- 'A' = 1, 'C' - 'A' = Zatem wyliczenie odpowiedniego indeksu można zapisać tak: indeks = <wprowadzony_znak> - 'A'
24
No, ale musimy jeszcze sięgnąć po ten znak
No, ale musimy jeszcze sięgnąć po ten znak. Z dialogu dostajemy napis (łańcuch znakowy). To jest dana typu String, a nie char. Napis ten składa się z jednego znaku, znajdującego się na pierwszej pozycji łańcucha (czyli pod indeksem 0). Znak ten otrzymamy stosując metodę charAt z klasy String. Jeśli res oznacza wprowadzony napis, to - zamiast poprzedniej "piętrowej" konstrukcji if-else możemy po prostu napisać: int i = res.charAt(0) - 'A'; System.out.println(dest[i] + " - cena: " + price[i]); Cały program poniżej.
25
import static javax.swing.JOptionPane.*;
public class Wycieczki { public static void main(String[] args) { String[] dest = { "Bali", "Cypr", "Ibiza", "Kenia", "Kuba" }; double[] price = { 5000, 2500, 2800, 4500, 6000 }; String msg = "Wybierz kierunek - " + " wpisując literę A-"+ (char) ('A'+dest.length-1)+ ":\n"; for (int i=0; i < dest.length; i++) msg += (char) ('A' + i) + " - " + dest[i] + '\n'; String res; while ((res = showInputDialog(msg)) != null) { if (res.length() == 1) { int i = res.toUpperCase().charAt(0) - 'A'; if (i < 0 || i > dest.length -1) continue; showMessageDialog(null, dest[i] + " - cena: " + price[i]); }
26
Wyrażenia regularne W dokumentacji klasy StringTokenizer można przeczytać, że do rozbioru tekstów lepiej jest stosować metodę split z klasy String. Wywołanie: txt.split(sep); zwraca tablicę symboli napisu txt rozdzielonych separatorami pasującymi do wzorca podanego przez napis - wyrażenie regularne sep. Trzeba więc pamiętać o tym, że w metodzie split podajemy jako argument wyrażenie regularne. Niezbędna jest zatem wiedza o składni i znaczeniu wyrażeń regularnych. Bez tego łatwo jest wpaść w pułapkę i otrzymać nieoczekiwane wyniki. Przykładowe różnice w działaniu StringTokenizera i metody split pokazuje poniższa tablica.
27
Tekst txt Separator sep 1 " " 5 2 " " 7 3 "." "ala ma kota i psa"
StringTokenizer st = new StringTokenizer(txt, sep) String[] s = txt.split(sep) Liczba symboli: st.countTokens() Wyróżnione symbole: st.nextToken() Liczba symboli: s.length Wyróżnione symbole: s[i] 1 "ala ma kota i psa" " " 5 0: Ala 1: ma 2: kota 3: i 4: psa 2 "ala ma kota i psa" " " 7 0: Ala 1: ma 2: kota 3: 4: i 5: 6: psa 3 "Pierwszy.Drugi.Trzeci" "." 0: Pierwszy 1: Drugi 2: Trzeci
28
W przypadku (1) wyniki są identyczne, ale podobieństwo jest mylące
W przypadku (1) wyniki są identyczne, ale podobieństwo jest mylące. Przypadek (2) pokazuje, że split traktuje separator bardzo dosłownie: ma to być jedna spacja. W tekście wyszukiwane są podnapisy ograniczane separatorem (jedną spacją) lub końcem wiersza. Dlatego, jako wyróżnione symbole pojawią się (na pozycji 3 i 5) puste podnapisy (np. podnapis "kota" na poz. 2 ograniczony jest spacją, za nią jest jeszcze jedna spacja - pomiędzy tymi dwoma spacjami znajdzie się więc element - pusty podnapis). Po to, by uzyskać zamierzony (taki sam jak przy użyciu StringTokenizera) efekt trzeba w split podać wyrażenie regularne "jedna lub więcej spacji", co zapisujemy za pomocą tzw. kwantyfikatora +: " +". String[] s = txt.split(" +"); da w wyniku tablicę wszystkich podnapisów napisu txt rozdzielonych co najmniej jedną spacją.
29
W przypadku (3) StringTokenizer bardzo ładnie rozbił podany tekst na separatorze "." (kropka), natomiast metoda split nie wyróżniła żadnych symboli. Stało się tak dlatego, że kropka w składni wyrażeń regularnych ma specjalne znaczenie (dowolny znak), wobec tego w tekście nie ma żadnego podnapisu zakończonego separatorem (są tylko same separatory). Aby użyć w wyrażeniu regularnym znaku, który ma specjalne znaczenie należy go poprzedzić odwrotnym ukośnikiem, przy czym ze względu na to, że odwrotny ukośnik ma w zapisie Stringów znaczenie "symbolu ucieczki" musimy zapisać go literalnie jako dwa odwrotne ukośniki. Zatem dopiero poprzez: String[] s = txt.split("\\."); uzyskamy tablicę podnapisów napisu txt rozdzielonych kropką. W prostych przypadkach użycie StringTokenizera może się wydać wygodniejsze, ale jest wiele sytuacji, w których za jego pomocą nie sposób osiągnąć wymaganego efektu i wtedy wyrażenia regularne mogą okazać się bardzo pomocne. Na pewno więc warto się z nimi zaznajomić. Tutaj przedstawione zostanie tylko kilka ogólnych informacji wprowadzających w to zagadnienie, a także krótkie praktyczne przykłady.
30
Regularne wyrażenie stanowi opis wspólnych cech (składni) zbioru łańcuchów znakowych
Możemy sobie wyobrażać, że regularne wyrażenie 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: * jeśli 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.
31
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: prosta klasa znaków stanowi ciąg znaków ujętych w nawiasy kwadratowe np. [123abc] określa dowolny ze znaków 1, 2, 3, a, b, c, zakres znaków (zapisywany z użyciem -), np. [0-9] - dowolna cyfra, negacja klasy znaków - jeśli pierwszym znakiem w nawiasach kwadratowych jest ^, to dopasowanie nastąpi dla każdego znaku oprócz wymienionych na liście. Np. do wzorca [^abc] będzie pasował każdy znak oprócz a, b i c, klasy predefiniowane (wprowadzane za pomocą specjalnych symboli) np. . (kropka) - klasa wszystkich znaków (każdy znak pasuje do tego wzorca), \d - cyfra, \D - nie-cyfra, \w - jeden ze znaków: [a-zA-Z0-9] (znak dopuszczalny w słowie), \p{Punct} -znak interpunkcji - skrót dla [! \p{L} - dowolna litera (Unicode). początek lub koniec ograniczonego ciągu znaków (np. wiersza lub słowa) - granice, np. ^ początek wiersza, $ - koniec wiersza, powtórzenia - w składni wyrażeń regularnych opisywane przez tzw. kwantyfikatory, np. kwantyfikator * oznacza wystąpienie 0 lub więcej razy, a kwantyfikator + wystąpienie co najmniej jeden raz.
32
w przypadku gdy kwantyfikator następuje po literale - wymagane jest wystąpienie (liczba wystąpień zależy od kwantyfikatora, w szczególności może być 0) tego literału np. "12a+" oznacza 1, potem 2, następnie wystąpienie znaku 'a' jeden lub więcej razy (uwaga: "12a+" nie oznacza wystąpienia ciągu znaków 12a jeden lub więcej razy), gdy kwantyfikator występuje po klasie znaków - dotyczy dowolnego znaku z tej klasy. Np. [abc]+ oznacza wystąpienie jeden lub więcej razy znaku a, lub znaku b, lub znaku c, jeśli natomiast chcemy, by kwantyfikator dotyczył dowolnego wyrażenia regularnego X - to powinniśmy zastosować jedną z poniższych konstrukcji składniowych: (X)symbol_kwantyfikatora (?:X)symbol_kwantyfikatora Konstrukcje takie tworzą tzw. grupy. Grupy ujęte w nawiasy okrągłe (pierwsza z w/w form składniowych) służą też do zapamiętywania tekstu pasującego do wzorca podanego w nawiasach (możemy się później do tego dopasowania odwołać). Druga forma służy wyłącznie grupowaniu, bez zapamiętywania. Przykład: (?:12a)+ - jedno lub więcej wystąpień napisu "12a"; kwantyfikatory mogą być: zachłanne (domyślnie) lub wstrzemięźliwe. Przy zastosowaniu kwantyfikatorów zachłannych uzyskujemy najdłuższe możliwe dopasowanie np. regularne wyrażenie "1.3" zastosowane wobec tekstu "123123" dopasuje cały tekst "123123". Kwantyfikatory wstrzemięźliwe odnajdują możliwie najkrótsze dopasowanie, wprowadza się je poprzez dodanie znaku zapytania do kwantyfikatora np. wyrażenie regularne "1.*?3" w tekście "123123" dopasuje podnapis "123";
33
flagi, które modyfikują sposób interpretacji wyrażenia regularnego, np
(?i) porównywanie liter bez uwzględnienia ich wielkości, (?s) dopasowanie kropki (symbolu dowolnego znaku) również do znaku końca wiersza; logiczne kombinacje wyrażeń regularnych, np. a+|b - jedno lub więcej wystąpień znaku a lub znak b. 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 ciągami znaków pasującymi do podanego wzorca.
34
W Javie do najogólniejszego posługiwania się wyrażeniami regularnymi służą 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 tekst wyrażenia regularnego. Obiekty klasy Matcher wykonują operacje wyszukiwania w tekście za pomocą interpretacji skompilowanego wyrażenia regularnego i dopasowywania go do tekstu lub jego części. 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 i zwraca true, jeśli się to udało, false w przeciwnym razie, metoda find() przeszukuje wejściowy łańcuch znakowy i wyszukuje kolejne pasujące do wzorca jego podłańcuchy
35
Przejdźmy do przykładów …
metoda group() zwraca ostatnio dopasowany tekst, metoda start() zwraca początkową pozycję ostatnio dopasowanego tekstu, metoda end() zwraca końcową pozycję ostatnio dopasowanego tekstu, metoda group(int n) zwraca n-tą grupę (n >=1) ostatnio dopasowanego tekstu (grupy w wyrażeniu regularnym oznaczamy nawiasami okrągłymi), metoda replaceFirst(String rpl) zastępuje pierwsze wystąpienie dopasowanego tekstu tekstem podanym jako rpl (w tekście zastępującym możemy odwoływać się do zapamiętanych grup), metoda replaceAll(String rpl) zastępuje wszystkie wystąpienia dopasowanego tekstu tekstem podanym w rpl (w tekście zastępującym możemy odwoływać się do zapamiętanych grup). Do rozbioru tekstów służą natomiast metody split(...) z klasy Pattern. Przejdźmy do przykładów …
36
A. Metoda matches() stwierdza czy cały tekst pasuje do wzorca.
import java.util.regex.*; public class Sample1 { public static void main(String[] args) { // Wzorzec: jedno lub więcej wystąpień dowolnej cyfry String regex = "[0-9]+"; // Kompilacja wzorca Pattern pattern = Pattern.compile(regex); // Tekst wejściowy String txt = "196570"; // Uzyskanie matchera Matcher matcher = pattern.matcher(txt); // Czy tekst pasuje do wzorca? boolean match = matcher.matches(); …
37
wynik: … System.out.println("Tekst: " + txt + '\n' +
(match ? " " : " NIE ") + "pasuje do wzorca: " + regex); // Nowy tekst wejściowy txt = " "; // reset matchera "zeruje" jego stany i pozwala też na podanie nowego tekstu matcher.reset(txt); match = matcher.matches(); } wynik: Tekst: pasuje do wzorca: [0-9]+ Tekst: NIE pasuje do wzorca: [0-9]+
38
B. Metoda find() odnajduje w napisie kolejne podnapisy pasujące do wzorca.
iimport java.util.regex.*; public class Sample2 { public static void main(String[] args) { // Wzorzec: jedno lub więcej wystąpień dowolnej cyfry String regex = "[0-9]+"; // Tekst wejściowy String txt = " "; System.out.println("Tekst: \n" + "'" + txt + "'" + "\nWzorzec: " + "'" + regex + "'"); // Kompilacja wzorca Pattern pattern = Pattern.compile(regex); // Uzyskanie matchera Matcher matcher = pattern.matcher(txt); …
39
… String result = ""; // do prezentacji wyników wyszukiwania // Zastosujemy 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; // wynik false oznacza, że w tekście nie ma pasujących podłańcuchów while (matcher.find()) { result += "\nDopasowano podłańcuch '" + matcher.group() + "'" // group() zwraca ostatni dopasowany tekst "\nod pozycji " + matcher.start() + // start() zwraca jego poczatkową pozycję "\ndo pozycji " + matcher.end(); // end() zwraca pozycję po ostatnim dopasowanym znaku } if (result.equals("")) result = "Nie znaleziono żadnego podnapisu " + "pasującego do wzorca"; System.out.println(result);
40
wynik: Tekst: ' ' Wzorzec: '[0-9]+' Dopasowano podłańcuch '123' od pozycji 0 do pozycji 3 Dopasowano podłańcuch '996' od pozycji 4 do pozycji 7
41
C. Używając grup (ujmując odpowiednie fragmenty wyrażenia regularnego w nawiasy okrągłe) możemy łatwo wyłuskiwać fragmenty dopasowanego tekstu. import java.util.regex.*; public class Sample3 { public static void main(String[] args) { // Wzorzec: // jedno lub więcej wystąpień dowolnej cyfry (grupa, bo w nawiasach) // po czym jeden lub więcej białych znaków // po czym jedna lub więcej liter Unicode (grupa 2, w nawiasach) // po czym dowolna liczba całkowita > 1 (grupa 3, w nawiasach) String regex = "([0-9]+)\\s+(\\p{L}+)\\s+([1-9][0-9]*)"; // Tekst wejściowy String txt = "1111 Odkurzacz 20"; …
42
… System.out.println("Tekst: " + "'" + txt + "'" + "\nWzorzec: " + "'" + regex + "'"); // Kompilacja wzorca Pattern pattern = Pattern.compile(regex); // Uzyskanie matchera Matcher matcher = pattern.matcher(txt); // Dopasowanie tekstu boolean isMatching = matcher.matches(); if (isMatching) { int n = matcher.groupCount(); // ile jest grup for (int i = 1; i <=n; i++) { String grupa = matcher.group(i); // pobranie zawartości i-ej grupy (numeracja od 1) System.out.println("Grupa " + i + " = '" + grupa + "'"); } } else System.out.println("Tekst nie pasuje do wzorca");
43
wynik: Tekst: '1111 Odkurzacz 20' Wzorzec: '([0-9]+)\s+(\p{L}+)\s+([1-9][0-9]*)' Grupa 1 = '1111' Grupa 2 = 'Odkurzacz' Grupa 3 = '20‘
44
D. Używając metody split() z klasy Pattern można dokonać rozbioru tekstu
public class Sample4 { public static void main(String[] args) { // ogólny wzorzec separatorów do wyróżniania słów: // separatorem jest 1 lub więcej "białych znaków" lub znaków interpunkcji String regex = "[\\s\\p{Punct}]+"; // Tekst wejściowy String txt = "Ala(11), kot,; pies-1 <kot2>[mrówka]"; // Kompilacja wzorca Pattern pattern = Pattern.compile(regex); String[] words = pattern.split(txt); // inaczej wołane niż split() z klasy String System.out.println("Liczba wyróżnionych słów: " + words.length); for (String w : words) { System.out.println(w); }
45
wynik: Liczba wyróżnionych słów: 7 Ala 11 kot pies 1 kot2 mrówka
46
E. Metoda replaceFirst usuwa z napisu pierwsze wystąpienie podnapisu pasującego do wzorca
import java.io.*; import java.util.*; import java.util.regex.*; public class Sample5 { public static void main(String[] args) throws Exception { // Usuniemy z tekstu z pliku wszystkie komentarze jednowierszowe // (zaczynające się od dwóch ukosników - składnia jak w Javie) // wynik zapiszemy do innego pliku Scanner in = new Scanner(new File("test1.txt")); // skaner dla pliku wejściowego BufferedWriter out = new BufferedWriter(new FileWriter("test2.txt")); // plik wyjściowy // Wzorzec komentarzy: // 0 lub więcej białych znaków, potem dwa ukosniki po których występują bądź nie inne znaki String regex = "\\s*//.*"; …
47
… Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(""); try { while (in.hasNextLine()) { String line = in.nextLine(); matcher.reset(line); String nline = matcher.replaceFirst(""); // komentarz zastępujemy pustym napisem if (!nline.equals("")) { // wynikowy wiersz zapiszemy, jeśli nie jest pusty out.write(nline); out.newLine(); } } finally { in.close(); out.close();
48
wynik: Przykładowy plik wejściowy tekst // to jest komentarz a tu jest tekst // i komentarz // cos tam // kom1 // kom2 idzie dalej tekst Wygenerowany plik wyjściowy tekst a tu jest tekst idzie dalej tekst
49
F. W tekście zastępującym dopasowanie w metodach replaceFirst i replaceAll możemy odwoływać się do zawartości grup wyrażenia regularnego. import java.util.regex.*; public class Sample6 { public static void main(String[] args) throws Exception { // Zastąpimy w tekście wszystkie napisy: // (liczbaCałkowita1:liczbaCałkowita2) // na napisy: // [liczbaCałkowita2:liczbaCałkowita1] // czyli zmienimy nawiasy na kwadratowe i przestawimy miejscami liczby // Wzorzec: // nawias,liczba,dwukropek,liczba, nawias - uwaga nawias jest znakiem specjalnym - // użyjemy ukośnika // zastosujemy dwie grupy: dla liczby1 i liczby2 String regex = "\\((\\d):(\\d)\\)"; // liczby jednocyfrowe Pattern pattern = Pattern.compile(regex); …
50
… String txt = "tekst 1 (ale) (2) (1:2) wołanie f() (3:4) (8:9)(10:11)"; Matcher matcher = pattern.matcher(txt); // W wywołaniu metody replaceAll (i replaceFirst) podając tekst zastępujący // możemy odwoływać się do zawartości grup wzorca. // Wtedy tekst zastępujący będzie zawierał zawartość grupy z dopasowania wyrażenia. // W tekście zastępującym stosujemy znak $ z następującym po nim numerem grupy // (a więc znak $ jest w tym kontekście zarezerwowany!) // W naszym przykładzie mamy dwie grupy: pierwszą liczbę i drugą liczbę // oznaczamy je $1 i $2 // zamiana nawiasów i przestawienie liczb String newTxt = matcher.replaceAll("[$2:$1]"); System.out.println("Tekst przed zamianą:"); System.out.println(txt); System.out.println("Tekst po zamianie:"); System.out.println(newTxt); }
51
wynik: Tekst przed zamianą:
tekst 1 (ale) (2) (1:2) wołanie f() (3:4) (8:9) (10:11) Tekst po zamianie: tekst 1 (ale) (2) [2:1] wołanie f() [4:3] [9:8] (10:11) Tekst przed zamianą: tekst 1 (ale) (2) (1:2) wołanie f() (3:4) (8:9) (5:6) Tekst po zamianie: tekst 1 (ale) (2) [2:1] wołanie f() [4:3] [9:8] [6:5]
52
Tablice w Javie. Deklarowanie i tworzenie
Dane w programie mogą być organizowane w różny sposób. W szczególności jako zestawy (powiązanych i/lub w określony sposób uporządkowanych) wartości. W tym kontekście mówimy o strukturach danych. Jednym z ważnych rodzajów struktur danych - są tablice. Tablice są zestawami elementów (wartości) tego samego typu, ułożonych na określonych pozycjach. Do każdego z tych elementów mamy bezpośredni (swobodny - nie wymagający przeglądania innych elementów zestawu) dostęp poprzez nazwę tablicy i pozycję elementu w zestawie, określaną przez indeks lub indeksy tablicy. Na przykład, tablica czterech liczb całkowitych może wyglądać tak. Pierwszy element - liczba 21 ma indeks 0, drugi - liczba 13 indeks 1 itd. Do elementów tablicy odwołujemy się za pomocą nazwy tablicy oraz indeksu umieszczonego w nawiasach kwadratowych. Jeżeli ta tablica ma nazwę tab, to do pierwszego elementu odwołujemy się poprzez nazwę tablicy i indeks 0: tab[0], do drugiego - tab[1] itd. Jak widać, odwołanie np. do 3-go elementu - nie wymaga przeglądania innych elementów.
53
W Javie tablice są obiektami, a nazwa tablicy jest nazwą zmiennej, będącej referencją do obiektu-tablicy. Obiekt-tablica zawiera elementy tego samego typu. Może to być dowolny z typów prostych lub referencyjnych. Zatem, w szczególności elementami tablic mogą być referencje do innych tablic. Mamy wtedy do czynienia z odpowiednikiem tablic wielowymiarowych. Tak samo jak wszystkie inne zmienne - tablice musimy deklarować przed użyciem ich nazw w programie. Deklaracja tablicy składa się z: nazwy typu elementów tablicy, pewnej liczby par nawiasów kwadratowych (liczba par okresla liczbę wymiarów tablicy) nazwy zmiennej, która identyfikuje tablicę. Np. int[] arr; // jest deklaracją tablicy liczb całkowitych (typu int), String[] s; // jest deklarację tablicy referencji do obiektów klasy String JButton[] b; // jest deklarację tablicy referencji do obiektów klasy JButton double[][] d; // jest deklaracją dwuwymiarowej tablicy liczb rzeczywistych
54
Ściślej można powiedzieć, że deklarowane są tu zmienne tablicowe
Ściślej można powiedzieć, że deklarowane są tu zmienne tablicowe. Typ takiej zmiennej jest typem referencyjnym, a jego nazwa składa się z nazwy typu elementów tablicy i nawiasów kwadratowych. W powyższych przykładach: zmienna arr jest typu int[] zmienna s jest typu String[] zmienna d jest typu double[][] Uwaga: rozmiar tablicy nie stanowi składnika deklaracji tablicy !! Np. taka deklaracja: int[5] arr; jest niedopuszczalna. !! Jeżeli oswoimy się z myślą, że tablice są obiektami, to - przez analogię do innych obiektów - będzie nam łatwo zrozumieć różnicę pomiędzy deklaracją i utworzeniem tablicy. Deklaracja tablicy tworzy referencję. int[] arr; // arr jest referencją // arr jest zmienną typu int[], który jest typem referencyjnym Taka deklaracja nie alokuje pamięci dla samej tablicy!
55
Pamięć jest alokowana dynamicznie albo przy deklaracji z inicjacją za pomocą nawiasów klamrowych albo w wyniku użycia wyrażenia new. Deklaracja tablicy z inicjacją za pomocą nawiasów klamrowych ma postać: typ[] zm_tab = { wart_1, wart_2, .... wart_N } gdzie: typ - typ elementów tablicy, zm_tab - nazwa zmiennej tablicowej, wart_i - wartość i-go elementu tablicy Np. int[] arr = { 1, 2, 7, 21 }; deklaruje tablicę o nazwie arr, tworzy ją i inicjuje jej elementy; kolejno: Wydzielana jest pamięć dla zmiennej arr, która będzie przechowywać referencję do obiektu-tablicy. Wydzielana jest pamięć (dynamicznie, na stercie) potrzebna do przechowania 4 liczb całkowitych (typu int). Kolejne wartości 1,2,7,21 są zapisywane kolejno w tym obszarze pamięci. Adres tego obszaru (referencja) jest przypisywany zmiennej arr.
56
Drugi sposób utworzenia tablicy polega na zastosowaniu wyrażenia new.
Tworzenie tablicy za pomocą wyrażenia new (bez inicjacji elementów) ma postać new T[n]; gdzie: T - typ elementów tablicy n - rozmiar tablicy (liczba elementów tablicy) Na przykład: int[] arr; // deklaracja tablicy arr = new int[4]; // utworzenie tablicy 4 elementów typu int Można to też zapisać od razu w wierszu deklaracji: int[] arr = new int[4]; Uwaga: nawiasy są kwadratowe, a nie okrągłe, jak w przypadku użycia new z konstruktorem jakiejś klasy
57
Mechanizm działania jest tu identyczny jak w przypadku innych obiektów
Mechanizm działania jest tu identyczny jak w przypadku innych obiektów. Przypomina go poniższy rysunek. Zauważmy, że rozmiar tablicy może być ustalony dynamicznie, w fazie wykonania programu. Np. int n; //... n uzyskuje wartość // np. na skutek obliczeń opartych na wprowadzonych przez użytkownika danych //... int[] tab = new int[n]; Ale - uwaga - po ustaleniu rozmiar nie może być zmieniony.
58
Elementy tablic tworzonych za pomocą wyrażenia new T[n] mają inicjalne wartości ZERO (zera arytmetyczne, false dla typu boolean, null dla typów referencyjnych). Wyrażenia new można też użyć do utworzenia i równoczesnej inicjacji elementów tablicy wartościami zapisanymi w nawiasach klamrowych. Tworzenie tablicy za pomocą wyrażenia new (z inicjacją elementów) ma postać new T[] { wart_1, wart_2, .... wart_N }; gdzie: T - typ elementów tablicy wart_i - wartość i-go elementu tablicy
59
Np. int[] a; // .... a = new int[] {1, 2, 3, 4 }; Jest to szczególnie wygodne, gdy chcemy ad hoc utworzyć i zainicjować tablicę elementami np. przy przekazywaniu tablicy jako argumentu jakiejś metodzie. Przykładowo, w kontekście: // definicja metody, działającej na tablicy przekazanej jako argument void metoda(int[] tab) { //.... } // ... // wywołanie metody z ad hoc utworzoną tablicą metoda(new int[] { 7, 7, 7, 7 });
60
Odwołania do elementów tablic
Jak już wspominaliśmy do elementów tablic odwołujemy się za pomocą indeksów. Indeksy tablicy mogą być wyłącznie wartościami typu int. Mogą być dowolnymi wyrażeniami, których wyliczenie daje wartość typu int. Tablice zawsze indeksowane są poczynając od 0. Czyli pierwszy element n-elementowej tablicy ma indeks 0, a ostatni - indeks n-1. Ze względu na to, że wartości typu byte, char i short są w wyrażeniach "promowane" (przekształcane) do typu int), to również wartości tych typów możemy używać przy indeksowaniu tablic. Niedopuszczalne natomiast jest użycie wartości typu long. Odwołanie do i-go elementu tablicy o nazwie tab ma postać: tab[i] Ta konstrukcja składniowa traktowana jest jako zmienna, stanowi nazwę zmiennej - zatem możemy tej zmiennej przypisywać wartości innych wyrażeń oraz możemy używać jej wartości w innych wyrażeniach
61
Na przykład: int[] a = new int[3]; a[1] = 1; // nadanie DRUGIEMU elementowi tablicy a wartości 1 int c = a [1] + 1; // c będzie miało wartość 2 int i = 1, j = 1; a[i +j] = 7; // nadanie elementowi o indeksie i+j (=2) wartości 7 Odwołania do elementów tablic są przez JVM sprawdzane w trakcie wykonania programu pod względem poprawności indeksów. Java nie dopuści do odwołania do nieistniejącego elementu tablicy lub podania indeksu mniejszego od 0. Próba takiego odwołania spowoduje powstanie wyjątku ArrayIndexOutOfBoundsException, na skutek czego zostanie wyprowadzony odpowiedni komunikat i wykonanie programu zostanie przerwane (ew. taki wyjątek możemy obsłużyć). Zobaczmy przykład: public class Test { public static void main(String[] args) { int[] a = {1, 2, 3, 4 }; System.out.println(a[4]); System.out.println(a[3]); System.out.println(a[2]); System.out.println(a[1]); }
62
Zauważmy - mamy tu tablicę składającą się z 4 liczb całkowitych
Zauważmy - mamy tu tablicę składającą się z 4 liczb całkowitych. Chcemy po kolei wyprowadzić jej elementy od ostatniego poczynając. Częstym błędem jest zapominanie o tym, że tablice indeksowane są od zera: w tym programie zapomniano o tym i próbowano odwołać się do ostatniego elementu tablicy a za pomocą a [4] (ktoś pomyślał: skoro są cztery elementy - to ostatni jest a[4]). Tymczasem jest to odwołanie poza zakres tablicy, do nieistniejącego 5-go elementu! Ten błąd zostanie wykryty, na konsoli pojawi się komunikat i program zostanie przerwany. Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException at Test.main(Test.java:5) W powyższym przykładzie było nieco żmudne wypisywanie kolejnych elementów tablicy. W naturalny sposób powinniśmy to robić w pętli. Może tak? : public class Test { public static void main(String[] args) { int[] a = {1, 2, 3, 4 }; for (int i=3; i>=0; i--) System.out.println(a[i]); }
63
A co się stanie gdy zmienimy rozmiar tablicy dodając kilka nowych elementów w inicjacji? Będziemy musieli od nowa policzyć elementy i zmienić inicjację licznika pętli. Trochę niewygodne, a do tego naraża nas na błędy. A przecież rozmiar tablicy znany jest JVM, niech zatem "liczeniem" elementów zajmuje się komputer. Zawsze możemy uzyskać informacje o rozmiarze (liczbie elementów) tablicy za pomocą odwołania: nazwa_tablicy.length Uwaga: częstym błędem jest traktowanie tego wyrażenia jako wywołania metody. W tym przypadku length nie jest nazwą metody (lecz pola niejawnie stworzonej klasy, opisującej tablicę), dlatego NIE STAWIAMY po nim nawiasów okrągłych Zatem poprzedni program można by zapisać tak: public class Test { public static void main(String[] args) { int[] a = {1, 2, 3, 4 }; for (int i=a.length-1; i>=0; i--) System.out.println(a[i]); }
64
Spróbujmy teraz odwrócić kolejność wypisywania elementów tablicy (czyli po kolei od pierwszego poczynając). Jak powinna wyglądać pętla for? public class Test { public static void main(String[] args) { int[] a = {1, 2, 3, 4 }; for (int i=0; i<a.length; i++) System.out.println(a[i]); } Przebiegając w pętli przez wszystkie (poczynając od pierwszego) elementy tablicy tab musimy zmieniać indeksy od 0 do tab.length-1, czyli zastosować następującą postać pętli for: for (int i = 0; i < tab.length; i++) ... tab[i] ... ; Użycie length wobec tablicy jest szczególnie wygodne w metodach, które otrzymują jako argumenty referencje do tablic: możemy w ten sposób pisać uniwersalne metody działające na tablicach o różnych rozmiarach.
65
Rozszerzona instrukcja for dla tablic
Rozszerzona instrukcja znana jest rownież jako instrukcja "for-each". Służy do przebiegania po zestawach danych (kolekcjach, tablicach, innych). Dla każdego elementu takiego zestawu wykonywane są instrukcje zawarte w ciele rozszerzonego for. Składnia for ( Typ id : expr ) stmt gdzie: expr - wyrażenie, którego typem jest (m.in.) typ tablicowy, Typ - nazwa typu elementów zestawu danych (np. int albo String) id - identyfikator zmiennej, na którą będzie podstawiany kolejny element zestawu danych; do tej zmiennej mamy dostęp w stmt (czyli instrukcji wykonywanej w każdym kroku for). Np. double[] nums = { 1, 2, 3 }; for (double d : nums) System.out.println(d + 1); wypisze w kolejnych wierszach 2.0, 3.0 i 4.0 String[] names = { "A", "B", "C" } for (String s : names) System.out.println(s); wypisze w kolejnych wierszach A, B, C.
66
Nieprzypadkowo w opisie składni "for-each" mówi się o "wyrażeniu, którego typem jest typ tablicowy". Naturalnie, zmienna tablicowa jest wyrażeniem typu tablicowego, ale będą nim również: wyrażenie new ad-hoc tworzące tablicę. wywołanie metody, zwracającej referencję do tablicy Pokazuje to poniższy program (w którym też jeszcze raz przyjrzymy się róznym metodom tworzenia tablic). import java.util.*; public class DeclCreSamples { // Metoda wypisująca elementy tablicy // przekazanej jako argument private static void show(int[] a) { for (int elt : a) { // rozszerzone for System.out.print(elt + " "); } System.out.println(); } // Metoda tworzy tablice napisow w postaci // 1.a 1.b 1.c ... private static String[] generateStringTab(int n) { …
67
… String[] stab = new String[n]; for (int i = 0; i < stab.length; i++) { stab[i] = i "." + (char)('a' + i); } return stab; } public static void main(String[] args) { // Deklaracja z inicjacją int[] a1 = {1, 2, 3, 4 }; show(a1); // Deklaracja tablicy n-elementowej Scanner sc = new Scanner(System.in); System.out.println("Podaj rozmiar tablicy"); int n = sc.nextInt(); int[] a2 = new int[n]; // nadanie wartości elementom tablicy for (int i = 0; i < a2.length; i++) { a2[i] = n; } show(a2); // Tworzenie tablicy z inicjacją ad hoc show (new int[] { 7, 9, 11 });
68
… // W for-each użyjemy wyrażenia new for (boolean b : new boolean[] { true, false, true } ) { System.out.print(!b + " "); } System.out.println(); // W for-each użyjemy wywolania metody zwracającej referencję do tablicy for (String s : generateStringTab(5)) System.out.print(s + " "); Wynik: Podaj rozmiar tablicy false true false 1.a 2.b 3.c 4.d 5.e
69
Przypomnijmy w tym kontekście, że dla typów wyliczeniowych dostępna jest statyczna metoda values(), która zwraca zestaw wartości (stałych) danego typu jako tablicę. Zatem poniższy program: public class EnumsVals { enum Month { JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC } public static void main(String[] args) { for (Month m : Month.values()) { System.out.print(" " + m); if (m == Month.JUN) System.out.println(); } wyprowadzi na konsolę: JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC
70
Warto zauważyć, że instrukcja "for-each" : for ( Typ id : expr ) stmt w przypadku tablic jest równoważna następującemu zapisowi: Typ[] $a = expr; for (int $i = 0; $i < $a.length; $i++) { Typ id = $a[ $i ] ; stmt } Uwaga: symbolem $ oznaczane są tymczasowe, wewnętrzne zmienne, niedostępne w ciele instrukcji for-each. Oznacza to, że rozszerzone for ma swoje ograniczenia. W ciele (zestawie instrukcji rozszerzonego for) mamy dostęp do elementów tablicy, ale nie możemy ich zmieniać. Istotnie, taki zapis nie ma sensu: int[] tab = {1, 2, 3 }; for(int a : tab) a = a*2; bowiem a jest zmienną lokalną w ciele instrukcji for-each. Oczywiście nie mamy też dostępu do indeksów kolejnych elementów (numerów iteracji). Wprowadzenie i rozwój domknięć w nowych wersjach Javy może zmienić tę sytuację i być może uzyskamy strukturę sterująca znaną z innych języków jako "forEachWithIndex".
71
Rekurencja Z ciała (kodu) metody możemy wywołać ją samą. Takie wywołanie nazywa się wywołaniem rekurencyjnym. Rekurencyjne wywołanie metody polega na wywołaniu tej metody z jej własnego ciała Rozpatrzmy najprostsze przykłady. public class Recurs { public static void show1(int i) { System.out.println("show1 " + i); if (i > 10) return; show1(i+1); } public static void main(String[] args) { show1(1);
72
show1 1 show1 2 show1 3 show1 4 show1 5 show1 6 show1 7 show1 8 show1 9 show1 10 show1 11
Tutaj sprawa jest dość prosta i do przewidzenia. Wywołanie show1(1) z metody main uruchamia łańcuch wywołań rekurencyjnych. Każde wywołanie wyprowadza przekazany argument, po czym sprawdzany jest warunek (i>10), i dopóki jest on nieprawdziwy ponownie wywoływana jest metoda show1 z powiększoną o 1 wartością argumentu. Np. po wypisaniu 1 (pierwsze wywołanie) wywoływana jest metoda show1 z argumentem 2 = 1+1, ten argument jest wypisywany, wywoływana jest metoda show1 z argumentem 3 (= 2+1), ten jest wypisywany itd. aż w "wewnętrznym wywołaniu" show1 argument nie osiągnie wartości 11. Wtedy - po jej wypisaniu - warunek okaże się prawdziwy i dopiero teraz sterowanie zwrócone zostanie do metody main.
73
Przy wywołaniach rekurencyjnych należy zapewnić warunek, którego spełnienie zakończy łańcuch rekurencji i spowoduje zwrócenie sterowania do miejsca, w którym po raz pierwszy wywołano metodę rekurencyjną Zobaczmy drugi fragment kodu. Tym razem wywołamy z metody main - za pomocą odwołania show2(1) - następującą metodę. public static void show2(int i) { if (i > 10) return; show2(i+1); System.out.println("show2 " + i); } Wynik działania tej metody show2 10 show2 9 show2 8 show2 7 show2 6 show2 5 show2 4 show2 3 show2 2 show2 1
74
Wywołanie show2(1) przekazuje jako argument 1, i z wnętrza show2 następuje wywołanie show2 z argumentem 2 (1+1). W tym momencie wykonanie dalszego kodu metody zostaje wstrzymane. Wykonanie zaczyna się od początku! Zaczyna działać jakby "nowy egzemplarz" metody show2. Tym razem z nowym argumentem (2). I tak dalej. Gdy parametr i osiągnie wartość 11 powinna zadziałać instrukcja return. Ale najpierw muszą być "dokończone" wszystkie poprzednie, "wstrzymane", wykonania metody show2. Ostatnie było z argumentem 10. Zatem wyprowadzona zostanie liczba 10 i wykonanie "tego egzemplarza" metody zostanie zakończone. Poprzedzało go wywołanie show2 z argumentem 9 - zostanie więc dokończone itd., aż dojdziemy do pierwszego wywołania show2 (z argumentem 1). Po zakończeniu wykonania metody z tym argumentem zostanie wykonana instrukcja return i sterowanie wróci do main. Zauważmy, że w pierwszym przypadku mieliśmy tak naprawdę do czynienia z takim samym wstrzymywaniem wykonania kodu metody show1 inicjowanego przez kolejne wywołania rekurencyjne, tyle, że nie mogliśmy tego dostrzec, ponieważ wstrzymywanie następowało na ostatniej instrukcji metody, już po wyprowadzeniu liczby. Oczywiście metody rekurencyjne mogą zwracać wartości. Zobaczmy, jak np. można rekurencyjnie zapisać zadanie sumowania dodatnich liczb całkowitych. W istocie rekurencja oznacza "zdefiniowanie czegoś przez to samo coś". Weźmy sumę Możemy powiedzieć tak: suma(1..5) = 5 + suma(1..4) suma(1..4) = 4 + suma(1..3)
75
Ogólnie, suma liczb od 1 do n równa jest n + suma(1
Ogólnie, suma liczb od 1 do n równa jest n + suma(1..n-1) Zatem jeśli nasza metoda sumowania otrzymuje argument n (oznaczający, że mamy zsumować liczby od 1 do n), to moglibyśmy spróbować zapisać: int sum(int n) { return n + sum(n-1); } Łatwo jednak zauważyć, że wpadamy tu w "nieskończoną" rekurencję. Metoda sum będzie wywoływana teoretycznie bez końca ze swojego wnętrza (praktycznym ograniczeniem będzie pamięć komputera - program skończy działanie z komunikatem "Stack overflow"). Musimy zatem zapewnić jakiś warunek zakończenia wywołań rekurencyjnych, Uwzględnić jakiś szczególny przypadek wartości przekazanego argumentu, który przerwie nieskończone rekurencyjne wywołania. W przypadku sumowania liczb od 1 do n, takim szczególnym przypadkiem jest wartość n = 1 (zwracamy wtedy 1).
76
public class Recurs { public static int sum(int n) { if (n == 1) return 1; else return n + sum(n-1); } public static void main(String[] args) { System.out.println(sum(100)); Wyprowadzi: 5050. Warto dokładnie przeanalizować działanie tej metody np. dla n = 5 , pamiętając, że kolejne zwroty wyników rekurencyjnego wywołania sum(...) są wstrzymywane dopóki n nie osiągnie wartości 1 i zauważając, że odtwarzanie tych wyników następuje w else, po kolei: 1, 2 + 1, 3 + (2 + 1), 4 + ( ), 5 + ( ) i ta ostatnia wartość jest właśnie zwracana do punktu wywołania sum(..) w metodzie main. Można się domyślić (choćby z przykładu sumowania), że rekurencyjne wywołania funkcji można zastąpić pętlami iteracyjnymi.
77
Bardzo często rekurencja będzie jednak prostsza do oprogramowania, bowiem odzwierciedla ona bezpośrednio pewien sposób rozumowania: nie wiemy jak rozwiązać cały problem, na którego rozwiązanie składa się powiedzmy n kroków, ale wiemy jak wykonać jeden krok, gdy już n-1 poprzednich zostało wykonane. I to właśnie możemy (dość prosto) zapisać w postaci rekurencyjnej. Trzeba jednak też wiedzieć o tym, że nie zawsze podejście rekurencyjne prowadzi do efektywnych algorytmów; czasami iteracyjne wersje rozwiązania jakichś problemów są wielokrotnie szybsze od rekurencyjnych, a nawet - przy ograniczeniach na pamięć operacyjną i moc procesora - jedynie możliwe. Sztandarowym przykładem jest tu rekursywne oprogramowanie wyliczenia liczb ciągu Fibonacciego, które szybko prowadzi do wyczerpania pamięci. Ciąg Fibonacciego dany jest za pomocą następującego równania, określającego wartości Fn kolejnych liczb ciągu (dla n = 0, 1, 2, ...): F0 = 0, F1 = 1, Fn = Fn-2 + Fn-1, dla n > 1. Czyli jest to ciąg liczb zaczynający się od liczb 0 i 1, przy czym każda następna liczba ciągu (poczynając od trzeciej) jest sumą dwóch poprzednich liczb ciągu:
78
Liczby Fibonacciego mają niezwykle ciekawe właściwości
Liczby Fibonacciego mają niezwykle ciekawe właściwości. Nader często ciągi takich liczb obserwowane są w zjawiskach naturalnych, mają też intrygujące właściwości matematyczne. Jak widać. ciąg Fibonanciego jest ciągiem rekurencyjnym, zatem wyliczenie jego kolejnych wyrazów w naturalny sposób można zapisać w postaci rekurencyjnej. int fib(int n) { if (n < 2) return n; else return fib(n-1) + fib(n-2); } Jednak wraz ze zwiększaniem wartości n czas obliczeń za pomocą tej metody rośnie katastrofalnie. Dzieje się tak dlatego, że katastrofalnie rośnie liczba rekurencyjnych wywołań metody fib. Większą część czasu zajmuje obliczanie już policzonych wartości! Zmodyfikowana metoda fib: int fib(int n) { System.out.println("Wywołanie fib z argumentem " + n); int wynik = 0; if (n < 2) wynik = n; else wynik = fib(n-1) + fib(n-2); System.out.println("Zwrot wyniku: " + wynik); return wynik; }
79
Analizując wydruk po wywołaniu tej metody z jakimś argumentem (np
Analizując wydruk po wywołaniu tej metody z jakimś argumentem (np. 8) - zobaczymy, że wielokrotnie powtarzają się rekurencyjne wywołania metody fib z tymi samymi argumentami i wielokrotnie powtarzają się zwroty tych samych wyników. Możemy też automatycznie policzyć liczbę wywołań z różnymi argumentami za pomocą np. takiego programu: public class ShowFibRec { int[] calls; ShowFibRec(int n) { calls = new int[n+1]; fib(n); for(int i=0; i <= n; i++) System.out.println("Liczba wywołań fib z argumentem " + i + " " + calls[i]); } int fib(int n) { calls[n]++; if (n < 2) return n; else return fib(n-1) + fib(n-2); public static void main(String[] args) { int n = Integer.parseInt(args[0]); new ShowFibRec(n);
80
Po kompilacji, możemy porównać liczbę "powtórnych" wywołań dla różnych n podawanych jako argument wywołania, np. 8 i 20. Uzyskamy następujący wyniki: Dla n = 20 Liczba wywołań fib z argumentem Liczba wywołań fib z argumentem Liczba wywołań fib z argumentem Liczba wywołań fib z argumentem Liczba wywołań fib z argumentem Liczba wywołań fib z argumentem Liczba wywołań fib z argumentem Liczba wywołań fib z argumentem Liczba wywołań fib z argumentem Liczba wywołań fib z argumentem Liczba wywołań fib z argumentem Liczba wywołań fib z argumentem Liczba wywołań fib z argumentem Liczba wywołań fib z argumentem Liczba wywołań fib z argumentem Liczba wywołań fib z argumentem 15 8 Liczba wywołań fib z argumentem 16 5 Liczba wywołań fib z argumentem 17 3 Liczba wywołań fib z argumentem 18 2 Liczba wywołań fib z argumentem 19 1 Liczba wywołań fib z argumentem 20 1 Dla n = 8 Liczba wywołań fib z argumentem 0 13 Liczba wywołań fib z argumentem 1 21 Liczba wywołań fib z argumentem 2 13 Liczba wywołań fib z argumentem 3 8 Liczba wywołań fib z argumentem 4 5 Liczba wywołań fib z argumentem 5 3 Liczba wywołań fib z argumentem 6 2 Liczba wywołań fib z argumentem 7 1 Liczba wywołań fib z argumentem 8 1 Ze względu na konstrukcję metody rekurencyjnej, liczba wielokrotnych wywołań metody z tym samym argumentem i , dla i =1,2...n, jest liczbą Fibonacciego: F(n-i+1) ! Zatem przy dużych n bardzo dużo czasu tracone jest na powtarzanie tych samych wywołań i zwracanie tych samych wyników.
81
Strumienie W większości języków programowania biblioteki wejścia/wyjścia ukrywają szczegóły obsługi poszczególnych mediów pod abstrakcją strumienia (ang. stream). Strumienie są używane zarówno do wysyłania/zapisywania jak i pobierania/odczytywania porcji danych. Główną zaletą takiego podejścia jest jego uniwersalność. W Javie hierarchia strumieni oparta jest na czterech klasach InputStream, OutputStream, Reader i Writer. InputStream i Reader reprezentują strumienie danych wejściowych, a OutputStream i Writer strumienie danych wyjściowych. Para InputStream i OutputStream jest przeznaczona do obsługi danych binarnych. Reader i Writer dodano do języka w wersji 1.1 i służą one do obsługi danych znakowych. Strumienie znakowe oferują podobną funkcjonalność co binarne. Jeżeli jest to możliwe, należy używać klas z hierarchii Reader/Writer. W niektórych zastosowaniach (np. kompresja) posługiwanie się danymi binarnymi jest jednak bardziej naturalne, dlatego strumienie znakowe nie zastępują strumieni binarnych, ale je uzupełniają. Możliwa jest przy tym bardzo łatwa konwersja strumieni binarnych na znakowe.
82
Strumienie dla poszczególnych mediów
Strumienie ujednolicają obsługę poszczególnych rodzajów mediów. Standardowe biblioteki Javy zawierają klasy reprezentujące strumienie wejściowe i wyjściowe na: pliku, tablicy bajtów/znaków, obiekcie String oraz łączu (ang. pipe) służącym do komunikacji procesów. Dodatkowo dalsze strumienie można uzyskać bezpośrednio z obiektów reprezentujących niektóre media, np. z gniazda sieciowego czy zasobu sieci WWW wskazanego przez adres URL. Strumienie ze standardowych bibliotek Javy do obsługi różnych rodzajów mediów Podklasy InputStream i OutputStream Podklasy Reader i Writer Opis ByteArrayInputStream i ByteArrayOutputStream FileReader i FileWriter Pozwalają odczytywać i zapisywać pliki dyskowe. Jako parametr konstruktora przekaż nazwę pliku dyskowego lub wskazujący go obiekt File. Tworząc obiekt wyjściowy, jako drugi argument konstruktora, możesz przekazać wartość logicznią określającą czy zamiast zamazywać istniejący plik dopisywać kolejne dane na jego końcu.
83
Podklasy InputStream i OutputStream
Reader i Writer Opis ByteArrayInputStream i ByteArrayOutputStream FileReader i FileWriter Bufor w pamięci oparty na tablicy odpowiednio bajtów lub znaków. Tworząc obiekt wejściowy, przekaż konstruktorowi tablicę, na której ma być oparty. Tworząc obiekt wyjściowy, przekaż konstruktorowi początkowy rozmiar bufora. StringBufferInputStream (nie ma odpowiednika do zapisu) StringReader StringWriter Bufor w pamięci oparty na napisie String (implementacja posługuje się obiektem StringBuffer). Tworząc obiekt wejściowy, przekaż konstruktorowi napis, na którym ma być oparty. Tworząc obiekt wyjściowy przekaż konstruktorowi początkowy rozmiar bufora. Zaleca się używanie klas z hierarchii Reader/Writer. StringBufferInputStream jest oznaczony jako deprecated. PipedInputStream PipedOutputStream PipedReader PipedWriter Łącze do komunikacji między procesami. Przy pomocy konstruktora bezparametrowego należy najpierw utworzyć obiekt jednego rodzaju (wejściowy lub wyjściowy), a następnie przekazać go jako parametr konstruktora obiektu drugiego rodzaju (odpowiednio wyjściowego lub wejściowego). Strumienie zostaną połączone łączem, które będzie przesyłać dane od strumienia wyjściowego do wejściowego.
84
Po swoich nadklasach InputStream/OutputStream lub Reader/Writer strumienie dziedziczą podstawowe metody pozwalające odczytywać/zapisywać porcje danych. Do czytania danych ze strumienia służą metody read(). W wersji bezparametrowej dają jako wynik wartość całkowitą reprezentującą odczytany bajt (dla InputStream) lub odczytany znak (dla Reader). Jeżeli osiągnięto koniec strumienia bezparametrowy read() daje -1. Przeciążone wersje metody read() odczytują dane do tablicy lub do jej części. Do wysyłania danych do strumienia służą metody write(). W podstawowej wersji przyjmują jako parametr liczbę całkowitą zawierającą zapisywany bajt (dla OutputStream) lub zapisywany znak (dla Writer). Przeciążone wersje metody write() zapisują dane z przekazanej tablicy lub jej części. Dodatkowe wersje metody z klasy Writer zapisują wszystkie znaki z przekazanego obiektu String lub jego wskazanego wycinka. W poniższym przykładzie plik tekstowy jest odczytywany znak po znaku przy pomocy strumienia FileReader i wypisywany na standardowe wyjście. import java.io.FileReader; import java.io.IOException; public class ZnakPoZnaku { public static void main(String[] args) throws IOException { // wersja dla Linuxa FileReader rd = new FileReader("/tmp/io_test.txt"); …
85
… // wersja dla Windows // FileReader rd = new FileReader("c:\\io_test.txt"); try { int i; // Reader.read() Daje wartość z przedziału 0 to 65535, // jeżeli odczyt się powiódł lub -1 jak nie while ((i = rd.read()) != -1) System.out.print((char) i); } finally { rd.close(); } Po użyciu, strumień trzeba zamknąć przy pomocy metody close(). Trzeba o tym pamiętać. Szczególnie, że dla niektórych zasobów, np. dla plików dyskowych oraz połączeń sieciowych, obowiązują limity na liczbę naraz otwartych egzemplarzy. Żeby zagwarantować zwalnianie zasobów również w przypadku wystąpienia wyjątku, powinno się to robić w bloku finally lub skorzystać z instrukcji try- z-zasobami dodanej w Javie 7. Dla zwiększenia przejrzystości w pozostałych przykładach cały kod związany z obsługą błędów i zamykaniem zasobów został usunięty.
86
Konwersja między strumieniami binarnymi i znakowymi
Strumień binarny można przekształcić na strumień znakowy. Służą do tego klasy InputStreamReader i OutputStreamWriter. Taka konwersja czasami jest bardzo przydatna, np. podczas kompresji i dekompresji danych. W poniższym przykładzie bufor oparty na tablicy bajtów jest przekształcany na obiekt Writer. import java.io.*; public class KonwersjaStrumieni { public static void main(String[] args) throws IOException { String napis = "Test strumieni.\nąćęłńóśźż\n"; ByteArrayOutputStream os = new ByteArrayOutputStream(); // OutputStream jest przekształcany na Writer Writer wr = new OutputStreamWriter(os); wr.write(napis); wr.close(); ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray()); // InputStream jest przekształcany na Reader Reader rd = new InputStreamReader(is); int i; while ((i = rd.read()) != -1) System.out.print((char) i); rd.close(); }
87
Kodowanie znaków W Javie znaki są wewnętrznie kodowane w standardzie Unicode[1]. Jeżeli używamy jednoparametrowego konstruktora, klasa OutputStreamWriter wykonuje konwersję na domyślne kodowanie dla danej platformy[2]. Klasa InputStreamReader wykonuje konwersję odwrotną. Konstruktory obu klas są przeciążone i jako drugi parametr można wskazać jakiego kodowania używać zamiast domyślnego. Domyślne kodowanie znaków jest również stosowane podczas obsługi plików za pomocą klas FileWriter i FileReader. Klasy te same nie implementują operacji na plikach. Wewnętrznie korzystają ze strumieni binarnych przekształconych przy pomocy OutputStreamWriter i InputStreamReader. Aby obsługiwać pliki przy pomocy innego niż domyślne kodowania znaków należy samemu stworzyć odpowiedni strumień binarny i przekształcić go na wersję znakową wskazując kodowanie jako drugi parametr konstruktora klas InputStreamReader bądź OutputStreamWriter.
88
Pobieranie danych po kolei z grupy strumieni
Klasa SequenceInputStream pozwala używać grupy strumieni InputStream tak, jakby były skonkatenowane. Najpierw pobierane są dane z pierwszego strumienia. Gdy ten się skończy, pobierane są dane z następnego, itd. SequenceInputStream posiada dwa konstruktory. Pierwszy przyjmuje jako parametr jedynie dwa strumienie, a drugi obiekt Enumeration<? extends InputStream>. Poniższy przykład pokazuje użycie klasy SequenceInputStream do odczytania po kolei dwóch buforów w pamięci. import java.io.*; public class KonkatenacjaStrumieni { public static void main(String[] args) throws IOException { String daneDlaBufora1 = "Dane dla bufora 1.\nąćęłńóśźż\n"; String daneDlaBufora2 = "Dane dla bufora 2.\nąćęłńóśźż\n"; // getBytes() daje tablicę bajtów reprezentujących kolejne znaki napisu // w domyślnym dla danej platformy kodowaniu znaków (są też wersje przeciążone) ByteArrayInputStream is1 = new ByteArrayInputStream(daneDlaBufora1.getBytes()); ByteArrayInputStream is2 = new ByteArrayInputStream(daneDlaBufora2.getBytes()); SequenceInputStream seq = new SequenceInputStream(is1, is2); Reader rd = new InputStreamReader(seq); int i; while ((i = rd.read()) != -1) System.out.print((char) i); }
89
Standardowe wejście/wyjście
Pomysł, aby dane wejściowe dla programu były odczytywane z jednego strumienia – standardowego wejścia (ang. standard input), dane wyjściowe były wysyłane do standardowego wyjścia (ang. standard output), a informacje o błędach do standardowego wyjścia błędu (ang. standard error) pochodzi z systemów Unixowych. Dzięki temu staje się możliwe łączenia programów w potoki przetwarzania – standardowe wyjście jednego programu staje się standardowym wyjściem drugiego, itd. Ten pomysł został zaadoptowany w wielu innych systemach operacyjnych, m.in. również w Windows. W Javie do standardowego wyjścia/wejścia mamy dostęp poprzez zmienne statyczne klasy System. Na System.out i System.err przypisane są obiekt PrintStream. System.in to zwykły InputStream. Najczęściej ze standardowego wejścia będziemy wczytywać dane tekstowe, np. polecenia od użytkownika, dlatego warto standardowe wejście opakować w BufferedReader i używać jego metody readLine(). BufferedReader stin = new BufferedReader(new InputStreamReader(System.in)); Przekierowywanie standardowego wejścia/wyjścia Możliwe jest przekierowanie standardowego wyjścia, wejścia i wyjścia dla komunikatach o błędach. Służą do tego metody setOut(PrintStream), setIn(InputStream) i setErr(PrintStream). Może to, np. ułatwić testowanie aplikacji. Sekwencje poleceń użytkownika wystarczy zapisać w pliku i przekierować standardowe wejście na strumień go odczytujący. Standardowe wyjście można przekierować do pliku, żeby łatwiej móc sprawdzić, czy wyniki są zgodne z oczekiwaniami.
Podobne prezentacje
© 2024 SlidePlayer.pl Inc.
All rights reserved.