StringJoiner
Każdy programista Javy, który pisze w tym języku co najmniej od kilku miesięcy zdaje sobie na pewno sprawę z kilku "problemów", które wiążą się z używaniem klasy String i mogą być nie do końca intuicyjne, szczególnie jeśli ktoś ma wcześniejsze doświadczenie z innymi językami programowania.
Podstawowym problemem, który omawialiśmy już w temacie poświęconym klasom String, StringBuilder i StringBuffer jest sposób na łączenie ciągów znaków oraz wydajność takich operacji. Początkowo najbardziej intuicyjne wydaje się skorzystanie z operatora plus, który w przypadku Stringów działa jako konkatenacja. Operacja ta jest jednak kosztowna pod względem złożoności obliczeniowej, ponieważ wymaga tworzenia dodatkowych obiektów.
Rozwiązaniem jest skorzystanie z jednej z dwóch wcześniej wspomnianych klas i wywoływanie na nich metod append() . Problem polega na tym, że API klas StringBuilder i StringBuffer nie było przemyślane pod kątem tego, że w Javie wprowadzone zostaną wyrażenia lambda i programowanie funkcyjne, więc niestety jeśli chcielibyśmy z niej skorzystać np. w połączeniu ze strumieniami z Javy 8, nie jest to tak proste i oczywiste.
Z pomocą przychodzi nowa klasa StringJoiner dodana w Javie 8, której głównym celem jest maksymalne uproszczenie operacji łączenia napisów oraz integracja z funkcyjnymi możliwościami języka.
Podstawowe wykorzystanie polega na wykorzystaniu metody add(), która przyjmuje jako parametr dowolny obiekt CharSequence , czyli np. dowolny String.
import java.util.StringJoiner;
public class StringConcat {
public static void main(String[] args) {
StringJoiner joiner = new StringJoiner("");
String result = joiner.add("Ania").add("ma").add("kota").toString();
System.out.println(result);
}
}
Ok, wszystko fajnie, tylko zasadniczo niczym nie różni się to od wykorzystania StringBuildera . W praktyce StringJoiner przyda się szczególnie wtedy, gdy będziemy chcieli połączyć kilka napisów i dodatkowo rozdzielić je jakimś separatorem. Załóżmy, że chcemy wczytać od użytkownika trzy napisy, które chcemy przerobić na ciąg znaków {napis1, napis2, napis3}.
Konstruktor StringJoiner przyjmuje albo jeden parametr będący separatorem, albo trzy argumenty (separator, prefix, sufix). Skorzystajmy więc od razu z tego bardziej złożonego:
import java.util.StringJoiner;
public class StringConcat {
public static void main(String[] args) {
StringJoiner joiner = new StringJoiner(",", "{", "}");
String result = joiner.add("Ania").add("ma").add("kota").toString();
System.out.println(result);
}
}
Nadal nie ma tutaj jednak szału. Okazuje się jednak, że funkcjonalności z klasy StringJoiner w pewnym stopniu zostały w Javie 8 wbudowane bezpośrednio klasę String. Do dyspozycji otrzymaliśmy dzięki temu metody takie jak:
- join(CharSequence delimiter, CharSequence... elements) - przyjmuje separator oraz dowolną ilość argumentów do dołączenia
- join(CharSequence delimiter, Iterable<? extends CharSequence> elements) - również przyjmuje separator, ale drugim argumentem jest obiekt implementujący interfejs Iterable, czyli np. dowolna kolekcja lub tablica Stringów
Myślę, że do użycia dowolnej z dwóch powyższych metod nie trzeba nikogo przekonywać. Zapewne każdemu zdarzyło się nie raz definiować pętle for-each lub korzystać z zewnętrznych bibliotek w celu rozwiązania podobnych problemów.
import java.util.Arrays;
import java.util.List;
public class StringConcat2 {
public static void main(String[] args) {
List<String> names = Arrays.asList("Ania", "Karol", "Bartek", "Jerzy");
String allNames = "";
allNames = allNames.join(", ", names);
System.out.println(allNames);
}
}
Ze względu na to, że Stringi są niemodyfikowalne metoda join() nie modyfikuje oryginalnego obiektu, ale pozwala zbudować nowy String. Ponieważ metoda join() pod spodem wykorzystuje klasę StringJoiner , a ta z kolei korzysta ze StringBuildera to nie musimy się martwić o wydajność takich operacji.
Prawdziwa przyjemność z wykorzystania StringJoinera pojawia się jednak po dodaniu do wszystkiego strumieni i Collectorów . W poprzednim listingu niezbyt eleganckie wydaje się konieczność inicjowania zmiennej allNames pustym Stringiem. Zamiast tego możemy z naszej listy wyciągnąć strumień, a następnie wykorzystać metodę collect() do złączenia napisów. Oczekuje ona jako argumentu obiektu zgodnego z interfejsem Collector . Nie musimy go tworzyć sami, zamiast tego możemy skorzystać z metody Collectors.joining() , która zwraca odpowiedni reduktor wykorzystujący pod spodem klasę StringJoiner.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StringConcat3 {
public static void main(String[] args) {
List<String> names = Arrays.asList("Ania", "Karol", "Bartek", "Jerzy");
String allNames = names.stream().collect(Collectors.joining(", ", "{", "}"));
System.out.println(allNames);
}
}
Tak naprawdę możemy dzięki temu całkowicie zrezygnować ze zmiennej allNames.
Całość możemy wygodnie połączyć np. z klasą Files pozwalającą odczytać plik wiersz po wierszu z wykorzystaniem strumieni. Załóżmy, że chcemy wczytać plik wiersz po wierszu i po drodze wyrzucić wszystkie napisy krótsze niż 3 znaki, na końcu zebrać je w całość i wyświetlić w znanej nam formie {a, b, c}.
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Collectors;
public class StringConcatFiles {
public static void main(String[] args) throws IOException, URISyntaxException {
String collect = Files.lines(Paths.get(ClassLoader.getSystemResource("example.txt").toURI()))
.filter(str -> str.length() > 3)
.collect(Collectors.joining(", ", "{", "}"));
System.out.println(collect);
}
}
Wynik dla pliku z imionami Ania, Jan, Kasia, Patryk, Ola zapisanymi pod sobą to {Ania, Kasia, Patryk}. Całość wydaje się bardzo elegancka, jest wydajne, a kod mówi sam za siebie jaką mamy intencję.
Dyskusja i komentarze
Masz pytania do tego wpisu? Może chcesz się podzielić spostrzeżeniami? Zapraszamy dyskusji na naszej grupie na Facebooku.