Java HR: StringBuffer vs StringBuilder

Java HR: StringBuffer vs StringBuilder

String jest co prawda wspaniałą i jedną z najważniejszych klas w Javie, jednak nie ma rzeczy idealnych i tutaj z pomocą przychodzą klasy StringBuffer oraz StringBuilder.

Co to jest?


Czym tak właściwie są klasy StringBufferStringBuilder i czemu nie wystarczy String?
Są to klasy reprezentujące sekwencję znaków i w przeciwieństwie do Stringa, który jest klasą niezmienną – immutable (więcej w artykule: Java HR: obiekty niezmienne (immutable)),
ich obiekty są zmienne, tzn. mogą modyfikować swój wewnętrzny stan w tym samym miejscu w pamięci.

Obie te klasy działają wewnętrznie na specjalnym buforze.
Fakt, że String jest niezmienny niesie za sobą szereg korzyści, dlatego jest tak wyjątkową klasą w Javie.
Przykładowo, dzięki temu idealnie nadaje się do bycia kluczem w Hashmapie.
Niestety, jest to obarczone też pewnymi wadami – zwłaszcza jeśli chodzi o wykonywanie operacji na Stringach.
Wszystko jest oczywiście zależne od naszej aplikacji – czasami takiego przetwarzania nie używa się wiele, jednak w niektórych przypadkach może to być krytyczne dla jej wydajności.

Przy bezpośredniej operacji na Stringach za każdym razem zostanie stworzony w pamięci nowy obiekt – nawet przy użyciu takich niepozornych metod jak trim().
Klasycznym przykładem takiej operacji jest konkatenacja (przy pomocy operatora +, który jest przeładowany dla Stringów), częstokroć dokonywana w pętli, wtedy różnice w wydajności są tym bardziej odczuwalne. Tworzone jest wtedy wiele nowych obiektów, które od razu zostają porzucane, zaśmiecając pamięć i dokładając pracy Garbage Collectorowi.
Używając metody append() klasy StringBuffer lub StringBuilder unikamy tego problemu.

Ciekawostka: w aktualnych wersjach Javy do bezpośredniej konkatenacji Stringów kompilator i tak wykorzystuje StringBuilder, za każdym razem tworząc tymczasowo jego obiekt, co również nie pozostaje bez wpływu na wydajność.

Jeszcze raz, najważniejsze różnice:
  • String jest immutable, StringBuilderStringBuffermutable (bufor)
  • String przechowywany jest w String Pool, StringBuilderStringBuffer – w normalnej stercie
  • StringBuilderStringBuffer są znacznie wydajniejsze do celów modyfikacji sekwencji znaków

StringBuilder vs StringBuffer


  • StringBuffer powstał jako pierwszy – w Javie 1.0.
  • StringBuilder został dodany w Javie 1.5 (Java 5) jako jego bezpośredni zamiennik (drop-in replacement).

Oznacza to, że jest w pełni kompatybilny z jego API – dostarcza tych samych metod, więc w miejscu można dokonać podmiany klasy.
Dlaczego?

  • StringBuffer był niewydajny w przypadku większości jego zastosowań. Został zaimplementowany jako klasa thread-safe, wszystkie jego metody są z osobna synchronizowane.
  • StringBuilder to jego nowsza i obecnie zalecana wersja – nie jest synchronizowany, lecz w przypadku aplikacji jednowątkowych nie ma to żadnego znaczenia.

Co prawda różnice w wydajności są niewielkie przy małej liczbie operacji (w przeciwieństwie do porównania ze Stringiem, gdzie różnica w wydajności jest kolosalna), jednak nie ma jakiegokolwiek argumentu za użyciem StringBuffera, więc zawsze lepiej zastąpić go StringBuilderem.

StringBuffer obarczony jest dodatkowymi, niepotrzebnymi kosztami blokowania dostępu.
Jeśli potrzebna jest synchronizacja wątków – lepiej jest użyć klasy StringBuilder i zwyczajnie umieścić sekwencję operacji w bloku synchronized {}. StringBuffer jest natomiast synchronizowany na poziomie pojedynczych operacji, co rzadko kiedy się przydaje.

Jak widać, nie bez powodu powstał jego zamiennik.

Podobnie potraktowane zostały:
  • Vector -> ArrayList
  • Hashtable -> HashMap

Użycie w praktyce


Wzajemna relacja
Klasa String posiada konstruktor przyjmujący jako parametr StringBuffer lub StringBuilder.
Można z nich też uzyskać Stringa przy pomocy metody toString(). Można też użyć obiektu którejś z tych dwóch klas bezpośrednio na wyjściu, np. w System.out.println().

Wszystkie 3 klasy rozszerzają CharSequence, ale nie jest możliwe wzajemne rzutowanie (poprzez cast).

Przykłady

String:

StringBuffer:

StringBuilder:

Istotna różnica w użyciu – jeśli przekażemy String do metody i wewnątrz niej zostanie on w jakikolwiek sposób „zmodyfikowany” (pozornie), na zewnątrz oryginalny obiekt pozostanie taki sam.
Jeśli zrobimy to samo ze StringBuilderem lub StringBufferem, wartość obiektu na zewnątrz (poza metodą) również ulegnie zmianie.
[przykład]

Podsumowując:
  • czasami wystarczy tylko String (gdy nie próbujemy zmieniać jego wartości)
  • do manipulacji łańcuchami znaków potrzebny jest StringBuilder
  • w aplikacjach wielowątkowych przydatny jest StringBuffer (chociaż zalecam użycie StringBuildera + jawna synchronizacja)

Ciekawostka: lock elision – niektóre wersje JVM począwszy od Java 6 wprowadziły mechanizm, w którym kompilator może wykryć czy synchronizacja danego kodu jest potrzebna, czy może inne wątki nie będą miały nigdy do niego dostępu.
Ta optymalizacja potrafi wpłynąć na wydajność StringBuffera.

Więcej w tym temacie: https://www.infoq.com/articles/java-threading-optimizations-p1

Informatyk, programista. Obecnie Java Developer (Web Fullstack), właściciel studia Berrygames oraz prezes koła TK Games na Politechnice Wrocławskiej.

Wszystkie artykuły autora>>

Dodaj komentarz