Jak pogrupować Stringi według długości dzięki Stream API?

sztukakodu.pl 1 rok temu

Podczas rozmowy o pracę możesz zostać spytany o sposób na pewne proste operacje do wykonania na obiektach w Javie.

Przykładem takiego pytania może być prośba o wydajne pogrupowanie Stringów według ich długości.

Jak mogła by wyglądać taka implementacja? #

Najprościej skorzystać z API do Strumieni - Stream API.

Wystarczy wejściową kolekcję Stringów zamienić na strumień i skorzystać z dedytkowanego collectora w metodzie collect.

Collectors#groupingBy(Function
super
T,?
extends
K>
classifier)

W nim możemy przekazać dzięki jakiej funkcji chcemy pogrupować nasze obiekty.

Jeśli ma to być długość Stringa to wystarczy nam metoda String#length.

Całość mogłaby wyglądać jak na fragmencie poniżej.

List<String>
words
=
List.of("apple",
"banana",
"cherry",
"date",
"elderberry",
"fig",
"grape");
Map<Integer,
List<String>>
groupedWords
=
words.stream()
.collect(Collectors.groupingBy(String::length));

A wynik działania jest następujący.

{
3:
["fig"],
4:
["date"],
5:
["apple",
"grape"],
6:
["banana",
"cherry"],
10:
["elderberry"]
}

Jak policzyć liczbę elementów w grupie? #

W tym celu należy użyć przeciążenia metody groupingBy przyjmującej drugi argument Collector downstream.

Collector#groupingBy(Function
super
T,?
extends
K>
classifier,
Collector
super
T,A,D>
downstream)

Aby policzyć liczbę elementów trafiających do jednej grupy potrzebujemy więc zastosować Collectors.counting().

Tak wygląda to w całości.

List<String>
words
=
List.of("apple",
"banana",
"cherry",
"date",
"elderberry",
"fig",
"grape");
Map<Integer,
Set<String>>
groupedWords
=
words.stream()
.collect(Collectors.groupingBy(String::length,
Collectors.counting());

A wynik powinien być następujący.

{
3:
1,
4:
1,
5:
2,
6:
2,
10:
1
}

Jak wykorzystać do tego współbieżność? #

Wystarczy, iż zamiast zwykłego strumienia utworzysz strumień współbieżny - metodą Collection#parallelStream():

Map<Integer,
List<String>>
groupedWords
=
words.parallelStream()
.collect(Collectors.groupingBy(String::length));

Lub Stream#parallel().

Map<Integer,
List<String>>
groupedWords
=
words.stream().parallel()
.collect(Collectors.groupingBy(String::length));

Pamiętaj, iż ta operacja wykona się na współdzielonej puli wątków - ForkJoinPool.

Jak wykorzystać do tego standardowe API Javy? #

Możemy pokusić się o samodzielną implementację.

Przykładowa mogłaby wyglądać jak na fragmencie poniżej.

public
Map<Integer,
List<String>>
groupByLength(List<String>
words)
{
Map<Integer,
List<String>>
groups
=
new
HashMap<>();
words.forEach(word
->
{
groups.compute(word.length(),
(integer,
list)
->
{
List<String>
newList
=
list
!=
null
?
list
:
new
ArrayList<>();
newList.add(word);
return
newList;
});
});
return
groups;
}
var
groups
=
groupByLength("apple",
"banana",
"cherry",
"date",
"elderberry",
"fig",
"grape")

I ponownie otrzymany wynik to:

{
3:
["fig"],
4:
["date"],
5:
["apple",
"grape"],
6:
["banana",
"cherry"],
10:
["elderberry"]
}

Podsumowanie #

Najprostszym sposobem grupowania elementów w Javie jest skorzystanie z API strumieni, obiektu Collector i jego wbudowanej statycznej metody groupingBy.

Jeśli chcesz możesz skorzystać z przetwarzania współbieżnego, a także zdefiniować efekt końcowy grupowania - na przykład zliczenie ile obiektów trafia do tej samej grupy.

Idź do oryginalnego materiału