I have the following collection type:
Map<String, Collection<String>> map;
I would like to create unique combinations of each of map.size()
from a single value in the collection for each Key.
For example suppose the map looks like the following:
A, {a1, a2, a3, ..., an} B, {b1, b2, b3, ..., bn} C, {c1, c2, c3, ..., cn}
The result I would like to get would a List<Set<String>>
result, looking similar to (ordering is not important, it just needs to be a 'complete' result consisting of all possible combinations):
{a1, b1, c1}, {a1, b1, c2}, {a1, b1, c3}, {a1, b2, c1}, {a1, b2, c2}, {a1, b2, c3}, ... {a2, b1, c1}, {a2, b1, c2}, ... {a3, b1, c1}, {a3, b1, c2}, ... {an, bn, cn}
This is basically a counting problem, but I would like to see if a solution is possible using Java 8 streams.
Java 8 offers the possibility to create streams out of three primitive types: int, long and double. As Stream<T> is a generic interface, and there is no way to use primitives as a type parameter with generics, three new special interfaces were created: IntStream, LongStream, DoubleStream.
Let A and B be two sets, Cartesian productA × B is the set of all ordered pair of elements from A and B. A × B = {{x, y} : x ∈ A, y ∈ B} Let A = {a, b, c} and B = {d, e, f} The Cartesian product of two sets is. A x B = {a, d}, {a, e}, {a, f}, {b, d}, {b, e}, {b, f}, {c, d}, {c, e}, {c, f}}
The Java 8 Streams API is fully based on the 'process only on demand' strategy and hence supports laziness. In the Java 8 Streams API, the intermediate operations are lazy and their internal processing model is optimised to make it being capable of processing the large amount of data with high performance.
With Java 8, Collection interface has two methods to generate a Stream. stream() − Returns a sequential stream considering collection as its source. parallelStream() − Returns a parallel Stream considering collection as its source.
You can solve this using the recursive flatMap
chain.
First as we need to move back and forth by the map values, it's better to copy them to the ArrayList
(this is not the deep copy, in your case it's ArrayList
of 3 elements only, so the additional memory usage is low).
Second, to maintain a prefix of previously visited elements, let's create a helper immutable Prefix
class:
private static class Prefix<T> { final T value; final Prefix<T> parent; Prefix(Prefix<T> parent, T value) { this.parent = parent; this.value = value; } // put the whole prefix into given collection <C extends Collection<T>> C addTo(C collection) { if (parent != null) parent.addTo(collection); collection.add(value); return collection; } }
This is very simple immutable linked list which can be used like this:
List<String> list = new Prefix<>(new Prefix<>(new Prefix<>(null, "a"), "b"), "c") .addTo(new ArrayList<>()); // [a, b, c];
Next, let's create the internal method which chains flatMaps:
private static <T, C extends Collection<T>> Stream<C> comb( List<? extends Collection<T>> values, int offset, Prefix<T> prefix, Supplier<C> supplier) { if (offset == values.size() - 1) return values.get(offset).stream() .map(e -> new Prefix<>(prefix, e).addTo(supplier.get())); return values.get(offset).stream() .flatMap(e -> comb(values, offset + 1, new Prefix<>(prefix, e), supplier)); }
Looks like recursion, but it's more complex: it doesn't call itself directly, but passed lambda which calls the outer method. Parameters:
List
of original values (new ArrayList<>(map.values)
in your case).null
if offset == 0
). It contains currently selected elements from the collections list.get(0)
, list.get(1)
up to list.get(offset-1)
.When we reached the end of the values list (offset == values.size() - 1
), we map the elements of the last collection from the values to the final combination using the supplier. Otherwise we use the flatMap
which for each intermediate element enlarges the prefix and calls the comb
method again for the next offset.
Finally here's public method to use this feature:
public static <T, C extends Collection<T>> Stream<C> ofCombinations( Collection<? extends Collection<T>> values, Supplier<C> supplier) { if (values.isEmpty()) return Stream.empty(); return comb(new ArrayList<>(values), 0, null, supplier); }
A usage example:
Map<String, Collection<String>> map = new LinkedHashMap<>(); // to preserve the order map.put("A", Arrays.asList("a1", "a2", "a3", "a4")); map.put("B", Arrays.asList("b1", "b2", "b3")); map.put("C", Arrays.asList("c1", "c2")); ofCombinations(map.values(), LinkedHashSet::new).forEach(System.out::println);
We collect individual combinations to the LinkedHashSet
again to preserve the order. You can use any other collection instead (e.g. ArrayList::new
).
Cartesian product in Java 8 with forEach:
List<String> listA = Arrays.asList("0", "1"); List<String> listB = Arrays.asList("a", "b"); List<String> cartesianProduct = new ArrayList<>(); listA.forEach(a -> listB.forEach(b -> cartesianProduct.add(a + b))); System.out.println(cartesianProduct); // Output: [0a, 0b, 1a, 1b]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With