Kurs Java Podstawy - rozszerzony

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 maxymalne 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ę.

Komentarze