« poprzedni punkt  następny punkt »

2. Wyjątkowa klasa String

Oczywiście, ł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ń (specjalnych właściwości), które - niestety - powodują też trochę zamieszania pojęciowego.

Powtórzmy sobie znowu.
Gdy napiszemy

String napis;

to w tym momencie nie będzie jeszcze żadnego łańcucha znakowego, jedynie zmienną napis, która może zawierać referencję do obiektu (ale jeszcze nie zawiera).

Tak jak w przypadku każdej innej klasy obiekty klasy String musimy bezpośrednio tworzyć.

Mówimy tu o "sztuczkach kompilatora", gdyż omawiane tu wyjątkowe właściwości klasy String są zapewnianie dzięki niewidocznemu dla programisty przekształceniu przez kompilator fragmentów kodu w inne (od zastosowanych) konstrukcje składniowe Javy

Pierwsza "sztuczka" kompilatora 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;

spowoduje, że:

  • wyrażenie s1 + s2 stworzy 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"

Takie zastosowanie operatora + jest wyjątkowe, gdyż (wizualnie) stosujemy go wobec referencji (np. s1 + s2), co jest niedozwolone. Ale "druga sztuczka" kompilatora (i tylko kompilatora, bo definicja klasy String w żaden sposób nie umożliwia takiego zastosowania) powoduje, że możemy to zrobić.

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 " + 3 + " 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 ";

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 00000011 - 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.)
Jest to trzecia sztuczka kompilatora, bowiem składnia języka nie umożliwia wykonywania takich przekształceń w innych niż przy okazji konkatenacji łańcuchow znakowych kontekstach.

Jeśli w wyrażeniu konkatenacji łańcuchow znakowych wystąpi referencja do obiektu jakiejś klasy, to również obiekt (dane zawarte w obiekcie) zostaną przekształcone do postaci znakowej i dołączone do łańcucha. Znowu jest to udogodnienie ze strony kompilatora, ale sposób reprezentacji danych obiektu określa definicja klasy (jest to bardzo wygodna właściwość; o tym dlaczego i w jaki sposob jest możliwa i jak z niej korzystać - dowiemy się później).

Na przykład jeśli napiszemy:

Date data = new Date();
String s = "Teraz jest " + data;

to zostanie utworzony obiekt klasy Date (jedna ze standardowych klas Javy; jej obiekty opisują daty), który "w środku" zawiera dane (w postaci binarnej) opisujące bieżącą datę i czas. Po konkatenacji, zmienna s będzie oznaczać napis, w którym data i czas są podane w postaci znakowej np. "Teraz jest : Mon Jul 01 05:55:25 CEST 2002".

Należy także zwrócić uwagę na dwie ważne kwestie:

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.

Przy konkatenacji należy baczną uwagę zwracać na kolejność opracowywania wyrażeń

Np.

String s = "Nr " + 1 + 2;

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).

Natomiast:

String s = 100 + " Nr " + (1 +2);

da napis "100 Nr 3", bo:

  • najpierw będzie opracowane wyrażenie 100 + " Nr " (jego wynik - napis " 100 Nr ")
  • następnie zostanie opracowane wyrażenie 1 + 2 (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 przeksztalconą 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

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 łąńcuchów znakowych (ich zawartości) nie należy używać operatorów == i !=

Zamiast operatorów == i != używamy metody (polecenia) equals.

Aby porównać dwa napisy reprezentowane przez zmienne typu String s1 i s2 piszemy:

                s1.equals(s2)

Aby porównać napis reprezentowany przez zmienną s typu String z literałem łańcuchowym piszemy:

                s.equals(literał_łańcuchowy)

Wynikiem jest wartość true jeśli oba napisy są takie same i false w przeciwnym razie.

Poniższy przykładowy program pokazuje różne niuanse operowania na łańcuchach znakowych.

public class Strings1 {

  public static void main(String[] args) {

    String napis = "Waga ";
    double w = 10.234;
    System.out.println(napis + w + " kg");

    String txt = napis + w + 10 + " kg";
    System.out.println(txt);               // dlaczego 10.23410?
    txt = napis + (w + 10) + " kg";
    System.out.println(txt);               // a tu  20.234?
    txt = napis + w * 10 + " kg";          // w*10 bez nawiasów ?..
    System.out.println(txt);               // ... i dobrze - dlaczego?
                                           // (pomoc: priorytety operatorow)

    String bzdura = "Bzdura";
    bzdura = napis;
    System.out.println(bzdura);  // dlaczego wyprowadzi "Waga " a nie "Bzdura"

    txt = napis;
    txt = txt + "Ali ?";
    System.out.println(txt);      // a tu inaczej:
    System.out.println(napis);    // dlaczego napis jest "Waga "?
                                  // a nie "Waga Ali ?"

    System.out.println( napis == bzdura );           // dlaczego true?
    System.out.println( txt == napis );              // dlaczego false?
    System.out.println( txt == "Waga Ali ?");        // dlaczego false?
    System.out.println( txt.equals("Waga Ali ?") );  // dlaczego true?

    // a teraz coś dziwnego:
    napis = "Ala";
    System.out.println( napis == "Ala");      // true czy false???

  }
}
Waga 10.234 kg
Waga 10.23410 kg
Waga 20.234 kg
Waga 102.34 kg
Waga
Waga Ali ?
Waga
true
false
false
true
true

Działanie programu przedstawia wydruk (obok). Warto zauważyć, że jako informację do wyprowadzenia przez println (argument w nawiasach okrągłych) możemy podawać również wyrażenia (dodajmy, że dowolnego typu!). Fenomen ten wyjaśnimy sobie w następnych wykładach.

Proszę dokładnie przeanalizować tekst programu i przy okazji, odpowiadając na pytania zawarte w komentarzach, sprawdzić swoje rozumienie materialu wykladu.

Ostatni fragment programu pokazuje kolejne "odchylenie" Javy od prostych reguł. Okazuje się oto, że wszystkie literały łańcuchowe mające ten sam tekst są jednym i tym samym obiektem. W instrukcji: napis = "Ala"; jest tworzony nowy obiekt, zawierający napis "Ala". Przy każdym kolejnym użyciu literału "Ala" w miejsce literału podstawiona będzie referencja do tego obiektu (a nie do ew. nowotworzonego innego obiektu zawierającego napis "Ala"; taki obiekt nie będzie ponownie tworzony).
Dlatego ostatnie 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


« poprzedni punkt  następny punkt »