1. Napisy
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 Java SDK. Jest ona w postaci HTML: klasy podzielone
są według pakietów a także dostępna jest alfabetyczna lista wszystkich klas.
Proszę koniecznie otworzyć dokumentację i oswoić się z metodami nawigacji pośród różnych pakietów, klas oraz ich metod.
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ż powiedzicć, ż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" obiektow 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; o niektórych innych użytecznych metodach dowiemy się przy okazji
omawiania wyrażeń regularnych w przyszłym semestrze.
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.
| int | 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. | boolean | 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. | 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.
| int | lastIndexOf(String str)
Jak indexOf - ale zwracany jest indeks pozycji ostatniego wystąpienia. | int | lastIndexOf(String str,
int fromIndex) J.w.
Uwaga: metody indexOf i lastIndexOf mają również swoje wersje dla argumentów - znaków (typu char).
| int | 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. | boolean | startsWith(String prefix)
Zwraca true, gdy napis zaczyna się podanym jako argument łańcuchem znakowym; false - w przeciwnym razie. | boolean | 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. | String | substring(int beginIndex)
Zwraca podłańcuch tego łańcucha znakowego zaczynający się na pozycji o indeksie beginIndex (do końca łańcucha). | String | 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. | char[] | toCharArray()
Znaki łańcucha -> do tablicy znaków (typ char[]). | String | toLowerCase()
Zamiana liter na małe. | String | toUpperCase() Zamiana liter na duże. | String | 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). | static String | valueOf(char c)
Zwraca wartość typu char jako napis. | static String | valueOf(char[] data)
Zwraca napis złożony ze znakow tablicy. | static String | valueOf(double d)
Zwraca znakową treprezentację liczby typu double. | static String | valueOf(float f)
Zwraca znakową treprezentację liczby typu float . | static String | valueOf(int i)
Zwraca znakową treprezentację liczby typu int . | static String | valueOf(long l)
Zwraca znakową reprezentację liczby typu long . |
W pierwszym przykładowym programie wykorzystamy metodę charAt, zwracającą znak znajdujący się w napisie na podanej pozycji.
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.
Przed lekturą dalszego tekstu proszę to zadanie rozwiązać samodzielnie
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ę wprowadzil.
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' = 2 ...
Zatem wyliczenie odpowiedniego indeksu można zapisac tak:
indeks = <wprowadzony_znak> - 'A'
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 pokazano poniżej.
import javax.swing.*;
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 = JOptionPane.showInputDialog(msg)) != null) {
int i = res.charAt(0) - 'A';
if (i < 0 || i > dest.length -1) continue;
JOptionPane.showMessageDialog(null, dest[i] + " - cena: " + price[i]);
}
System.exit(0);
}
}
Dodatkowe komentarze:
- w oknie dialogowym wprowadzania danych tekst komunikatu (msg) składa
się z kilku wierszy; tekst dzielimy na wiersze za pomocą znaku '\n',
- program napisano w taki sposób, że przy zmianie liczby wycieczek należy
zmienić tylko inicjacje tablic dest i price; inne fragmenty kodu nie ulegną
zmianie!,
- przy tworzeniu komunikatu (msg) znowu wykorzystano możliwość traktowania
znaków jako liczb (kody znaków); jednak operacje arytmetyczne na znakach
dają w wyniku wartości typu int, a ponieważ chcemy pokazać znak , a nie jego
kod (int) - to musimy jawnie przekształcić te wartości do typu char - stąd
konieczność użycia operatora konwersji (char)
- poprawność pierwszego znaku wprowadzonego napisu (czy mieści się w
przedziale A-E) sprawdzamy za pomocą if; innym sposobem obsługi błędu byłaby
obsługa wyjątku (ArrayIndexOutOfBoundsException).
Spróbujmy teraz rozwiązać inne zadanie. Wyobraźmy sobie, że mamy dokument
html o prostej strukturze, w którym kolejne tytuły punktów treści znajdują
się między znacznikami <h2> ... </h2> (tekst zawarty pomiędzy
otwierającym znacznikiem <h2> i zamykającym znacznikiem </h2>
- traktowany jest jako nagłówek drugiego poziomu i odpowiednio do tego formatowany
przy wyświetlaniu w przeglądarce).
Naszym zadaniem jest wypisanie wszystkich takich nagłówków.
Uwaga. W tej chwili zajmujemy się najbardziej prymitywnym rozwiązaniem
problemu, w oparciu o "tradycyjne" metody klasy String. Głównie po to, by
przećwiczyć stosowanie tych metod. Inne rozwązania mogą wykorzystywac parsery
HTML lub wyrażenia regularne
Na razie jeszcze nie umiemy wczytywac plików,
ale możemy już przygotować i przetestować klasę, która pozwoli "wyłuskiwać"
z dokumentu nagłówki drugiego poziomu.
Niech ta klasa (trochę na wyrost) nazywa się Toc. Przy tworzeniu obiektu
tej klasy przekazujemy konstruktorowi cały dokument HTML w postaci łańcucha
znakowego, a wywołanie metody String getToc() - ma zwrócić listę nagłówków
drugiego pioziomu, rozdzielonych separatorami nowego wiersza. Listę tę możemy
następnie wypisać na konsoli lub skierować standardowy strumień wyjściowy
do pliku.
Jak podejść do tego problemu?
W tekście dokumentu musimy kolejno znajdować początki nagłówków ("<h2>"),
a następnie "wyłuskiwać" podłańcuchy, które są zawarte pomiędzy znacznikami
"<h2>" i "</h2>". Do znajdowania napisów w napisie służy metoda
indexOf, do "wyłuskiwania" podłacuchów metod substring. Uwaga: powinniśmy
zastosować tę wersję metody indexOf, która zaczyna poszukiwanie od podanej
pozycji łańcucha i wraz z postępem przeszukiwania odpowiednio zmieniać tę
pozycję.
Przed lekturą dalszego tekstu proszę to zadanie rozwiązać samodzielnie
Klasa Toc może wyglądać tak.
import javax.swing.*;
public class Toc {
private String doc; // przekazany dokument
private String toc = ""; // wynikowy spis treści
// separator końca wiersza; ponieważ jest zależny od systemu
// pobieramy go jako wartość tzw. właściwości systemowej
private final String ls = System.getProperty("line.separator");
public Toc(String doc) { // Konstruktor
this.doc = doc;
}
public String getToc() {
int p = 0; // pozycja od której zaczynamy szukanie "<h2>"
while ((p = doc.indexOf("<h2>", p)) != -1) { // dopóki są "<h2>"
// poszukajmy znacznika zamykającego
// end jest indeksem pozycji na której on występuje
int end = doc.indexOf("</h2>", p+4);
// jeżeli go nie ma ...
if (end == -1) return Emsg("Invalid document structure");
// w przeciwnym razie: wyłuskujemy nagłówek
toc += doc.substring(p+4, end) + ls; // ls - separator wierszy
p = end + 5; // i przesuwamy pozycję od której będziemy dalej szukać
}
return toc;
}
private String Emsg(String txt) { // komunikat o błędzie
JOptionPane.showMessageDialog(null, txt);
return toc;
}
Uwaga: powyższa wersja programu nie obsługuje przypadku wadliwej struktury
dokumentu, gdy znacznik <h2> występuje pomiędzy znacznikami <h2>
i </h2>.
Przy okazji tego programu wykorzystaliśmy możliwość pobierania właściwości systemowych. Za pomocą odwołania:
System.getProperty("line.separator")
zapytaliśmy Javy jaki na danej platformie systemowej obowiązuje separator
wierszy (w plikach), Zauważmy, że różne systemy stosują różne separatory (np.
pod Unixem jest to 0d , a pod Windows 0d0a - szesnastkowo). Ponieważ nasz
spis treści chcemy zapisywać do pliku to musimy użyć znaku separatora wierszy.
A zgodnie z założeniami Javy (wieloplatformowość) powinniśmy przygotować
program tak by działal bez rekompilacji na każdej platformie.
Klasę Toc możemy na razie przetestować na "sztywno" zadanym dokumencie. Za
chwilę, gdy nauczymy się wczytywać pliki - zobaczymy jak działa na konkretnych
plikach html.
class TocTest {
public static void main(String[] args) {
String doc = "<h2>1.1. Punkt 1</h2>" +
"ble ble ble ble ble " +
"ble ble ble ble ble " +
"<h2>1.2. Punkt 2</h2>" +
"ble ble ble ble ble " +
"ble ble ble ble ble " +
"<h2>1.3. Punkt 3</h2>" +
"ble ble ble ble ble " +
"ble ble ble ble ble " +
"ble ble ble ble ble " +
"ble ble ble ble ble " +
"<h2>1.4. Punkt 4</h2>" +
"ble ble ble ble ble " +
"ble ble ble ble ble " +
"<h2>1.5. Punkt 5</h2>" +
"ble ble ble ble ble " +
"ble ble ble ble ble " ;
System.out.println(new Toc(doc).getToc());
}
}
Wydruk programu (możemy go przekierować do pliku np. tak: java TocTest > out):
1.1. Punkt 1
1.2. Punkt 2
1.3. Punkt 3
1.4. Punkt 4
1.5. Punkt 5
Pewną zaskakującą może właściwością klasy String jest to, że jej obiekty są niemodyfikowalne - to znaczy utworzonych za pomocą klasy String napisów nie możemy zmieniać (np. do napisu dodać inny).
Jak to ??? Przecież wielokrotnie zajmowaliśmy się konkatenacją łańcuchów.
No tak, ale wynikowy napis, powstający z dodania do łańucha znakowego innego napisu, jest nowym obiektem i np. w takiej sekwencji:
String s = "Ala";
s = s + " ma kota";
tworzony jest nowy obiekt, zawierający napis "Ala ma kota" i referencja do
niego przypisywana jest zmiennej s, która poprzednio zawierała referencję do napisu "Ala".
Zwróćmy też uwagę, że w klasie String nie ma żadnych metod pozwalających modyfikować istniejący obiekt-napis.
Czasami jednak zmiany obiektów napisów są potrzebne.
Modyfikowalne obiekty-łańcuchy znakowe definiuje klasa StringBuffer.
Obiekty klasy StringBuffer to "bufory", które dynamicznie możemy wypełniać
napisami. W szczególności możemy utworzyć pusty - na razie - bufor:
StringBuffer sb = new StringBuffer();
po czym wupelniać go zawartośćią dopisując do niego jakieś kolejne napisy:
sb.append("jakiś napis 1");
sb.append("jakiś napis 2");
...
Obiekt klasy StringBuffer łatwo można przekształćić w obiekt klasy String za pomocą metod toString():
String s = sb.toString();
Proszę zapoznać się z dokumentacją klasy StringBuffer.
Wykorzystanie klasy StringBuffer zamiast String jest wskazane przy dużej liczbie "kumulatywnych" operacji konkatenacji.
Jak już wspomniano, każda operacja konkatenacji obiektów klasy String zapisana
za pomocą operatora + powoduje stworzenie nowego obiektu klasy String i skopiowanie
do "jego wnętrza" znaków łączonych łańcuchów. Jest to dosyć czasochłonne.
Różnicę pokazuje poniższy program (przygotowany w oparciu o materiały JDC TechTips - biuletynu Java Developer Connection).
Do mierzenia czasu operacji korzystamy w nim z klasy QTimer, w której zdefiniowaliśmy
metodę getElapsed() zwracającą upływ czasu od mometu utworzenia obiektu tej
klasy. Czas mierzymy za pomocą statycznej metody klasy System currentTimeMillis(),
która zwraca bieżący czas w milisekundach.
import javax.swing.*;
class QTimer {
private final long start;
public QTimer() {
start = System.currentTimeMillis();
}
public long getElapsed() {
return System.currentTimeMillis() - start;
}
}
public class Test {
public static void main(String args[]) {
int n = Integer.parseInt(JOptionPane.showInputDialog("Liczba operacji"));
QTimer t = new QTimer();
String strA = "";
for (int i = 1; i <= n; i++) strA += "A";
long etA = t.getElapsed();
System.out.println("String operator +; Czas: " + etA + " ms");
t = new QTimer();
StringBuffer sb = new StringBuffer();
for (int i = 1; i <= n; i++) sb.append("B");
String strB = sb.toString();
long etB = t.getElapsed();
System.out.println("StringBuffer append. Czas: " + etB + " ms");
System.out.println("Wykonano " + n + " operacji.");
System.out.println("Relacja A/B = " + (double) etA/etB);
System.exit(0);
}
}
Wynik:
String operator +; Czas: 3900 ms
StringBuffer append. Czas: 60 ms
Wykonano 9000 operacji.
Relacja A/B = 65.0
Jak widać, "kumulatywna" konkatenacja za pomocą klasy StringBuffer jest (przy
9 tys. operacji konkatenacji) 65-krotnie szybsza niż za pomocą operatora
+.
|