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 StringBuffer i StringBuilder 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ść.
- String jest immutable, StringBuilder i StringBuffer – mutable (bufor)
- String przechowywany jest w String Pool, StringBuilder i StringBuffer – w normalnej stercie
- StringBuilder i StringBuffer 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.
- 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
1 2 3 |
String ab = "a" + "b"; String result = ab + "c"; System.out.println(result); |
1 2 3 |
StringBuffer sb = new StringBuffer("a").append("b"); sb.append("c"); System.out.println(sb); |
1 2 3 4 5 6 |
StringBuilder sb = new StringBuilder("a").append("b"); sb.append("c"); System.out.println(sb); String result = sb.toString(); System.out.println(result); |
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]
- 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.