Najprostsze łączenie kolekcji w Javie 8

Czyli mówiąc po ludzku zaimplementujemy sobie najprostszą metodę zip, która będzie łączyła dwie kolekcje w jedną.

Przykładowy program

Zaczniemy od bardzo prostego, kontrolnego programu, który będzie pokazywał o co nam chodzi:

Listing 1. Program testowy

public class App {

	public static void main(String[] args) {
		List<String> names = Lists.newArrayList("Ala", "Henio", "Edek");
		List<Integer> numbers = Lists.newArrayList(1, 2, 3, 4);

		Collection<String> zipped = Zip.zip(names, numbers, (n, nb) -> nb + ". " + n);

		System.out.println(zipped); // [1. Ala, 2. Henio, 3. Edek]
	}
}

Mamy dwie kolekcje, różnej długości w tym przypadku, na wyjściu oczekujemy kolekcji, która powstanie z połączenia tychże. Samo łączenie powinno mieć charakter dowolnej BiFunction, która jako parametry przyjmie wartości z kolekcji i wypluje „coś”. Dodatkowo finalna kolekcja powinna być nie większa niż mniejsza z kolekcji podanych do zippera.

Zipper, pierwsze podejście

Na początek napiszmy zipper, który będzie miał imperatywny charakter. Taki hardkorek, bo zapewne trafią tu ludzie ruchający trupy pracujący w starszych wersjach języka.

Listing 2. Prosta, imperatywna implementacja

class Zip {
	public static <A, B, C> Collection zip(Collection<A> a, Collection<B> b, BiFunction<A, B, C> zipper) {
		List<C> c = new ArrayList<>(Math.min(a.size(), b.size()));
		Iterator<A> ai = a.iterator();
		Iterator<B> bi = b.iterator();
		
		while (ai.hasNext() && bi.hasNext())
			c.add(zipper.apply(ai.next(), bi.next()));
		return c;
	}
}

W sumie to wyszedł nam super kod. Jest banalnie prosty. W miarę łatwy w przetestowaniu i ładnie będzie wyglądać. Jednak nie do końca. Podejście to ma pewne wady. Nie widać ich tutaj, bo oparliśmy implementację o kolekcje, ale spróbujmy zaimplementować całość w trochę inny sposób, tak by w przyszłości można było rozwinąć nasz kod by wspierał dowolny Stream, a nie tylko kolekcję.

Zipper z wykorzystaniem Splieteratora

Na początek, czym jest Splieterator. Jest to obiekt, którego głównym zadaniem jest wspieranie poruszania się po kolekcji, kanale IO, Streamie. Jest w pewnym sensie bardziej zaawansowanym mechanizmem niż iterator ponieważ potrafi też wydzielić część przetwarzanych danych do nowego obiektu. Tym samym „out of box” wspiera równoległe przetwarzanie (o ile oczywiście sobie tego życzymy). Dla nas będzie on o tyle wygodnym rozwiązaniem, że pozwala na pracę ze Streamami.

Przyjrzyjmy się teraz implementacji

Listing 3. Implementacja z wykorzystaniem Splieteratora

class Zip {

	public static <A, B, C> Collection zip(Collection<A> a, Collection<B> b, BiFunction<A, B, C> zipper) {
		Iterator<A> ai = Spliterators.iterator(a.spliterator());
		Iterator<B> bi = Spliterators.iterator(b.spliterator());

		Iterator<C> ci = new Iterator<C>() {
			@Override
			public boolean hasNext() {
				return ai.hasNext() && bi.hasNext();
			}

			@Override
			public C next() {
				return zipper.apply(ai.next(), bi.next());
			}
		};

		Spliterator<C> cs = Spliterators.spliterator(ci, Math.min(a.size(), b.size()), Spliterator.SIZED);
		return StreamSupport.stream(cs, false).collect(Collectors.toList());
	}
}

To co rzuca się w oczy w tej wersji to, poza znacznie większą ilością kodu, sposób w jaki transformujemy Splieterator na iterator i odwrotnie. Na początku tworzymy dwa iteratory, które są wrapperami na Splieteratory. Następnie budujemy z nich prosty iterator, który zwraca kolejne elementy dopóki nie wyczerpie się jedna z kolekcji, a zwrócony element to wynik działania funkcji łączącej zipper. Następnie z powrotem tworzymy Splieterator z naszego iteratora. I tu ważny jest ostatni z parametrów. Jest to charakterystyka, czyli wartość opisująca jak będzie zachowywać się Splieterator. W naszym przypadku wybraliśmy SIZED, który oznacza iż mamy z góry wiadomy rozmiar przetwarzanych danych. W końcu najpierw budujemy Stream, który zbieramy do kolekcji.

Podsumowanie

Kod z wykorzystaniem Splieteratora jest niewątpliwie bardziej skomplikowany jeżeli weźmiemy pod uwagę tylko najprostszy przypadek łączenia dwóch kolekcji. Pozwala on jednak, po kilku zmianach, na stworzenie uniwersalnego rozwiązania pracującego na Streamach, a co za tym idzie „naprawienie” API poprzez dodanie brakującej metody.

2 myśli na temat “Najprostsze łączenie kolekcji w Javie 8

  1. Ciekawe, ale jednak brakuje konkretnego przykładu, gdzie spliterator byłby lepszym rozwiązaniem. Poza tym nazwy zmiennych a, ai, b, bi, c, ci, cs… Ciężko to się czyta.

  2. 1. nazewnictwo – trochę inaczej to się czyta, ale mamy generyczny kod i kolekcje A i B. Zatem ai i bi mają sens.

    2. Przykład z życia gdzie spliteratory się przydają będzie później jak będziemy zipować dowolne streamy 🙂 Ale już to pozwala domyślić się dlaczego spliteratory są OK.

Napisz odpowiedź

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax