Listopad 30, 2018

Jak połączyć Stringi w Javie?

String, StringBuilder, StringBuffer…

Powyżej widzisz trzy hasła: String, StringBuilder, StringBuffer. W tym artykule to one będą moją ofiarą. Zabiorę je do swojego labolatorium – sprawdzę jak działają wrzucając je do swojej wirtualnej maszyny.

Brzmi jak złowieszczy plan, ale spokojnie… Nic im się dzisiaj nie stanie, wrócą bezpieczne na swoje miejsce i bez żadnych problemów będziecie mogli z nich korzystać.

String Concatenation

Dwa hasła z trzech wymienionych na początku wpisu są przypisywane do zagadnienia zwanego String Concatenation – czyli konkatenacji Stringów? – Po prostu łączenia. 😉

Sprawdzimy, jakie mamy możliwości łączenia Stringów, a na koniec może… a to już sobie zostawimy na koniec.

Operator +

Najbardziej popularnym sposobem łączenia Stringów jest oczywiście użycie operatora +, który potrafi sklejać dla nas wiele Stringów w jeden. Dla każdej osoby, która miała trochę styczności z programowaniem – szczególnie z tym wysoko poziomym – jest to naturalne.

Użycie operatora jest banalne i potrafi je raczej każda osoba, która poświęciła około 1-2h na naukę programowania Javy (nie wiem czy naprawdę tyle, po prostu sobie wymyśliłem małą liczbę godzin). 😉

String name = "Pablo";
String lastName = "Escabo";

String pablo1 = name + " " + lastName;

I tadam, wynik mamy taki:

Pablo Escabo

Efekt fascynujący, przejdźmy teraz może do mniej znanych metod łączenia Stringów.

StringBuilder

Klasa StringBuilder pojawiła się w pakiecie java.lang w Javie 5 i została stworzona do łączenia Stringów dla aplikacji jednowątkowych. Szybkie podsumowanie – czyli nie do wielowątkowych.

Po co nam ona skoro mamy operator +?

Nie jest to czas na takie pytanie skoro nawet nie wiemy jak używać tej klasy!

Na początku trzeba stworzyć obiekt typu StringBuilder, który możemy zainicjalizować:

  • konstruktorem bezparametrowym:
StringBuilder stringBuilder = new StringBuilder();
  • konstruktorem parametrowym
StringBuilder stringBuilder = new StringBuilder("START_STRING");

I to właśnie do Stringa podanego jako argument będziemy dodawać inne Stringi.

Do naszego Stringa możemy dodawać nowe liczby, znaki, łańcuchy znaków przy użyciu metody append:

stringBuilder.append(name);

Która jest zdefiniowana dla każdego typu prymitywnego oraz typów osłonowych.

stringBuilder.append(1);
stringBuilder.append(1.3f);
stringBuilder.append(1.4d);
stringBuilder.append(true);
stringBuilder.append('H');

Jednak nadal to jest obiekt typu StringBuilder – aby otrzymać cały zbudowany String wystarczy wywołać popularną metodę toString().

String pablo2 = stringBuilder.toString();

Czyli przykładowe użycie StringBuilder może wyglądać tak:

String name = "Pablo";
String lastName = "Escabo";

StringBuilder stringBuilder = new StringBuilder("Info: ");
stringBuilder.append(name);
stringBuilder.append(" ");
stringBuilder.append(lastName);

String pablo2 = stringBuilder.toString();

StringBuffer

W pakiecie java.lang od JDK 1.0 znajdziemy (1996r.) klasę StringBuffer, która również służy do łączenia Stringów. Jest to już trzeci sposób łączenia Stringów, ten jednak się różni od innych. Klasa StringBuffer umożliwia łączenie Stringów w aplikacjach wielowątkowych – w końcu jakaś zaleta, a nie jak jakiś niepotrzebny StringBuilder.

Przejdźmy przez użycie klasy StringBuffer, choć dużo szybciej niż w poprzednim przykładzie – użycie wygląda analogicznie co klasy StringBuilder.

String name = "Pablo";
String lastName = "Escabo";

StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(name);
stringBuffer.append(" ");
stringBuffer.append(lastName);

String pablo3 = stringBuffer.toString();

Tadam, zmieniliśmy tylko typ i nazwę obiektu.

Nic dziwnego, w końcu klasa StringBuilder powstała na bazie StringBuffer – w Javie 8 obie te klasy rozszerzają abstrakcyjną klasę AbstractStringBuilder.

Po co to wszystko?

Skoro na początku powiedziałem, że wezmę powyższe klasy do swojego labolatorium to właśnie nadszedł teraz na to czas. Sprawdźmy je w praktyce.

A jak możemy sprawdzić praktykę? Podczas korzystania z aplikacji wyznacznikiem może być np. czas przetwarzania informacji. Zróbmy tak samo, niech naszym wyznacznikiem będzie czas.

Przypuśćmy, że chcemy dodać milion razy pewien znak do naszego łańcucha znakowego – zrobimy test dodania miliona razy znaku * dla każdej z wyżej wymienionych method.

Na początek najbardziej popularny (Gwiazda!) operator +:

String s = "";
long startStringTime = System.currentTimeMillis();

for (long i=0;i<count;i++) {
    s += "*";
}
long endStringTime = System.currentTimeMillis();

long stringTime = endStringTime - startStringTime;
System.out.println("String: " + (stringTime / (float) 1000));

Czas wykonania automatycznie przeliczam na sekundy dzieląc przez 1000.

Kolejnie wystąpi dla nas StringBuffer – ustąpimy staruszkowi, mającego swoje korzenie w JDK 1.0:

StringBuffer buffer = new StringBuffer();
long startBufferTime = System.currentTimeMillis();

for (long i=0;i<count;i++) {
    buffer.append("*");
}
buffer.toString();
long endBufferTime = System.currentTimeMillis();

long bufferTime = endBufferTime - startBufferTime;
System.out.println("Buffer: " + (bufferTime / (float) 1000));

Przy StringBuffer wywołuj również metodę toString żeby nie było, że testy są oszukane. Celem wszystkich testów jest otrzymanie takiego samego obiektu String.

Ostatni nasz uczestnik – StringBuilder – zostanie użyty analogicznie jak StringBuffer:

StringBuilder builder = new StringBuilder();
long startBuilderTime = System.currentTimeMillis();

for (long i=0;i<count;i++) {
    builder.append("*");
}
builder.toString();
long endBuilderTime = System.currentTimeMillis();

long builderTime = endBuilderTime - startBuilderTime;
System.out.println("Builder: " + (builderTime / (float) 1000));

Na koniec zostało mi jeszcze powiedzieć, że:

final long count = 1000000L;

No i co, zostało chyba nam to wszystko uruchomić?

Pokażę poniżej wyniki jakie otrzymałem, ale zróbcie również podobne testy w swoim labolatorium JVM.

N: 1000000
String: 226.455
Buffer: 0.036
Builder: 0.016

Czas został wyświetlony w sekundach – zaskakujące? Mam nadzieje, że nie zaskoczyłem Cię, jeśli tak to spokojnie – wszystko za chwilę wytłumaczę.

Ale zatrzymajmy się jeszcze chwilę i przyjrzyjmy się jeszcze raz liczbom i wykonajmy prostą matematykę:

  • StringBuilder wypadł lepiej od operatora + o około 14152 razy lepiej,
  • StringBuffer wypadł lepiej od operatora + o około 6290 razy lepiej,
  • StringBuilder wypadł lepiej od StringBuffer o około 2.5 razy lepiej.

Dwie pierwsze różnice są naprawdę spore, wytłumaczmy sobie dlaczego jakaś tam klasa, która ma przedrostek Strin

Niby bez sens – póki co.

Dlaczego tak wolno, panie plus?

Na początku trzeba zacząć od fundamentalnej rzeczy – String jest obiektem immutable. Jest to taki obiekt, który jest niezmienny. Defakto nie pozwala on na zmiany swojego stanu – przy każdej zmianie swojego stanu tworzy kopię swojego obiektu i to właśnie ona jest zwracana wraz z nowym stanem.

Co można przez to zrozumieć? Przypuśćmy, że mamy taki to sobie o to String:

String blog = "1024kb";

Przypuśćmy, że chcę dodać jeszcze sufiks pl.

blog += ".pl";

Wyświetlmy sobie jeszcze w między czasie adres Stringa bloga.

String blog = "1024kb";
System.out.println("Adres String przed dodaniem: " + Integer.toHexString(blog.hashCode()));
blog += ".pl";
System.out.println("Adres String po dodaniu nowego Stringa: " + Integer.toHexString(blog.hashCode()));

Szybki zwrot akcji na konsolę, a tam:

Adres String przed dodaniem: 565969b8
Adres String po dodaniu nowego Stringa: 8f1655f2

I no wychodzi na to, że niby ta sama nazwa zmiennej, a adres w pamięci się zmienił… jak to?

No właśnie jest to konsekwencja tego, że String jest immutable – po dodaniu nowego Stringa (po zmianie stanu obiektu) – została zwrócona jego kopia, czyli obiekt z nowym adresem.

Już kumasz?

Za każdym razem w pętli, gdy dodawaliśmy nowy znak do naszego String – czyli tutaj:

for (long i=0;i<count;i++) {
    s += "*";
}

Tak naprawde za każdym razem tworzyliśmy nowy obiekt (co wiąże się z kopiowaniem poprzedniego stanu obiektu) co właśnie wydłużyło czas dodawania znaku * do naszego Stringa.

I ot, cała tajemnica operator + dla Stringa została rozwiana.

Możesz się zastanowić, no dobra – ale co z StringBuilder i StringBuffer?

Klasy zostały zaimplementowane tak, aby umożliwiały szybkie łączenie Stringów – developerzy mieli świadomość, że String musi być immutable (o tym za chwilę).

Klasy te bazują na tablicy znaków, do której za każdy dodaniem nowego znaku (append) jest dodawany znak. Dopiero na sam koniec operacji – podczas wywołania toString() – jest tworzony obiekt String. Tak, tworzymy tylko jeden raz obiekt String, dlatego to wszystko wykonuje się dużo szybciej.

Dlaczego String jest immutable?

Głównym celem zaimplementowania klasy String jako immutable była optymalizacja maszyny wirtualnej. Jak wiadomo w każdej aplikacji lata pełno Stringów, jest ich multum. Często je porównujmy – czy to czyjeś imię, czy nazwę ulicy lub instytucji. Wszędzie są Stringi.

Dużą optymalizację w JVM wprowadził tzw. String pool – czyli basen Stringów, gdzie znajdują się tylko unikalne Stringi utworzone w obrębie aplikacji. Do String poola nie są wrzucane Stringi tworzone przy użycia operatora new, dlatego w swoim IDE możesz otrzymać podpowiedź, aby nie tworzyć Stringa w taki właśnie sposób.

W przypadku, gdy używamy w wielu miejscach naszej aplikacji Stringa o tej samej wartości to w pamięci JVM jest to ten sam obiekt – oba Stringów, choć nawet nie leżały obok siebie mają te same adresy. Zostało wprowadzone to w celu obniżenia zużycia pamięci JVM – Garbage Collector ma mniej pracy do wykonania – oraz zmniejszenia kosztu porównań Stringów – nie musimy zazwyczaj porównywać Stringów przy użyciu metody equals, wystarczy zwykłe użycie operatora == w celu sprawdzenia czy adresy obu obiektów są sobie równe.. 😉

Dodatkowym powodem dla zrobieniam Stringa immutable jest to, że jest to najpopularniejsza klasa w Javie, która jest używana m.in. jako klucze w kolekcjach. A jak wiadomo klucze powinny być unikalne, aby nie utracić dostępu do naszych wartości.

Map<String, Long> map = new HashMap<>();

Więcej o mapach możesz przeczytać tutaj.

Klasa String jest również wykorzystywana do przechowywania hashy, tokenów oraz haseł. Immutable String ma swoje powody również od strony bezpieczeństwa. Zapewniając niemożliwość zmiany stanu konkretnego obiektu chronimy się przed możliwością podmiany danych w pamięci JVM w celu wykradnięcia hasła, tokena itd.

Skoro String jest najczęściej używaną klasą w Javie, a w Javie często występuje temat wątków to dzięki immutable String jest klasą bezpieczną na aplikację wielowątkowe. Nie ma żadnych obaw, aby wykorzystywać przy wielu wątkach.

Z grubsza pokazałem Ci powody dlaczego String jest immutable, więcej możesz przeczytać m.in tutaj.

Podsumowanie

Postarałem się w swoim labolatorium tak sprawdzić obiekty: String, StringBuilder, StringBuffer abyś mógł wiedzieć o nich jak najwięcej – i mam taką nadzieję, że już wiesz prawie wszystko o łączeniu Stringów.

Jednak warto zrobić małe podsumowanie dzisiejszego doświadczenia:

  • W przypadku prostego łączenia Stringów nie ma co tworzyć obiektu: StringBuilder/StringBuffer – można użyć operatora +
  • Gdy mamy doczynienia z łączeniem Stringów w pętli musimy już wykorzystać klasę StringBuilder/StringBuffer
  • Klasę StringBuilder wykorzystujemy w aplikacjach jednowątkowych, ponieważ jest 2.5 raza szybsza
  • Klasę StringBuffer jesteśmy zmuszeni wykorzystywać w aplikacjach wielowątkowych kosztem wolniejszego łączenia Stringów
  • Dzięki immutable String zyskujemy::
    1. Optymalizację JVM przy użyciu String Pool,
    2. Bezpieczeństwo (przechowywanie haseł, tokenów itd.),
    3. Odporna na wielowątkowość
    4. Możliwość użycia jako klucza w kolekcji

I to wszystko na temat łączenia Stringów w Javie, czekam na Twoją opinię mojego doświadczenia.

Daj znać w komentarzu jeśli o czymś zapomniałem, a uważasz, że powinno się tutaj znaleźć.

Skomentuj również, gdy coś cię zaskoczyło – chętnie o tym usłyszę.

Albo po prostu skomentuj. 😉

Kod z artykułu możesz znaleźć w tym repozytorium.