Combine Consumers with different arguments or different arguments number - java

Let's say, our method receives input String and returns some List output. This output is the result of some number of generators, some of which depend on input and some of them not - they just add predefined values.
I want to implement these generators like a list of some function interfaces (Consumer for example), then combine them into one Consumer and just apply it to the input String.
So, I will able to change small generators easy and independently. But the problem is that not all my generators need input String as a parameter and I just pass this parameter there for only one reason - have an ability to combine such Consumers with others.
public class ConsumersTest {
private static <T, U> BiConsumer<T, U> combine(List<BiConsumer<T, U>> consumers) {
return consumers.stream().reduce((arg1, arg2) -> {}, BiConsumer::andThen);
}
List<String> generate(String input) {
ArrayList<String> output = new ArrayList<>();
combine(getGenerators()).accept(input, output);
return output;
}
private List<BiConsumer<String, List<String>>> getGenerators() {
return Arrays.asList(
this::addFirstDependent,
this::addSecondIndependent
);
}
private void addFirstDependent(String input, List<String> output) {
if (input.contains("some string")) {
output.add("First-Dependent");
}
}
private void addSecondIndependent(String input, List<String> output) {
output.add("Predefined Output");
}}
Is it possible to combine different consumers under one umbrella and apply them in one place? Or this is a bad idea and not the right way to do such things?

It is not an unusual pattern to have a common interface in a modular software and adapters, to make particular implementations fit. E.g.
public class ConsumersTest {
List<String> generate(String input) {
ArrayList<String> output = new ArrayList<>();
generators.accept(input, output);
return output;
}
private static <T, U> BiConsumer<T, U> ignoreFirstArg(Consumer<U> consumer) {
return (t, u) -> consumer.accept(u);
}
private final BiConsumer<String, List<String>> generators =
Stream.<BiConsumer<String, List<String>>>of(
this::addFirstDependent,
ignoreFirstArg(this::addSecondIndependent)
).reduce(BiConsumer::andThen).orElse((arg1, arg2) -> {});
private void addFirstDependent(String input, List<String> output) {
if (input.contains("some string")) {
output.add("First-Dependent");
}
}
private void addSecondIndependent(List<String> output) {
output.add("Predefined Output");
}
}
So ignoreFirstArg is the general adapter for methods not having that first parameter. There can be an arbitrary number of adapter methods. But note that if an adapter is very specific and thus, only use a single time, it’s also possible to write a lambda expression instead of a method reference, right in the combining code. Note that I changed that code, to not get repeatedly evaluated for every generate(String input) call, as otherwise, there would be no point in combining them when you don’t reuse the combined function, as you could also use
List<String> generate(String input) {
ArrayList<String> output = new ArrayList<>();
Stream.<BiConsumer<String, List<String>>>of(
this::addFirstDependent,
ignoreFirstArg(this::addSecondIndependent)
).forEach(g -> g.accept(input, output));
return output;
}
or even simpler
List<String> generate(String input) {
ArrayList<String> output = new ArrayList<>();
this.addFirstDependent(input, output);
this.addSecondIndependent(output);
return output;
}
which is not worse to maintain than the functional code, as still, every generator consists of a single line.

Related

How to avoid multiple Streams with Java 8

I am having the below code
trainResponse.getIds().stream()
.filter(id -> id.getType().equalsIgnoreCase("Company"))
.findFirst()
.ifPresent(id -> {
domainResp.setId(id.getId());
});
trainResponse.getIds().stream()
.filter(id -> id.getType().equalsIgnoreCase("Private"))
.findFirst()
.ifPresent(id ->
domainResp.setPrivateId(id.getId())
);
Here I'm iterating/streaming the list of Id objects 2 times.
The only difference between the two streams is in the filter() operation.
How to achieve it in single iteration, and what is the best approach (in terms of time and space complexity) to do this?
You can achieve that with Stream IPA in one pass though the given set of data and without increasing memory consumption (i.e. the result will contain only ids having required attributes).
For that, you can create a custom Collector that will expect as its parameters a Collection attributes to look for and a Function responsible for extracting the attribute from the stream element.
That's how this generic collector could be implemented.
/** *
* #param <T> - the type of stream elements
* #param <F> - the type of the key (a field of the stream element)
*/
class CollectByKey<T, F> implements Collector<T, Map<F, T>, Map<F, T>> {
private final Set<F> keys;
private final Function<T, F> keyExtractor;
public CollectByKey(Collection<F> keys, Function<T, F> keyExtractor) {
this.keys = new HashSet<>(keys);
this.keyExtractor = keyExtractor;
}
#Override
public Supplier<Map<F, T>> supplier() {
return HashMap::new;
}
#Override
public BiConsumer<Map<F, T>, T> accumulator() {
return this::tryAdd;
}
private void tryAdd(Map<F, T> map, T item) {
F key = keyExtractor.apply(item);
if (keys.remove(key)) {
map.put(key, item);
}
}
#Override
public BinaryOperator<Map<F, T>> combiner() {
return this::tryCombine;
}
private Map<F, T> tryCombine(Map<F, T> left, Map<F, T> right) {
right.forEach(left::putIfAbsent);
return left;
}
#Override
public Function<Map<F, T>, Map<F, T>> finisher() {
return Function.identity();
}
#Override
public Set<Characteristics> characteristics() {
return Collections.emptySet();
}
}
main() - demo (dummy Id class is not shown)
public class CustomCollectorByGivenAttributes {
public static void main(String[] args) {
List<Id> ids = List.of(new Id(1, "Company"), new Id(2, "Fizz"),
new Id(3, "Private"), new Id(4, "Buzz"));
Map<String, Id> idByType = ids.stream()
.collect(new CollectByKey<>(List.of("Company", "Private"), Id::getType));
idByType.forEach((k, v) -> {
if (k.equalsIgnoreCase("Company")) domainResp.setId(v);
if (k.equalsIgnoreCase("Private")) domainResp.setPrivateId(v);
});
System.out.println(idByType.keySet()); // printing keys - added for demo purposes
}
}
Output
[Company, Private]
Note, after the set of keys becomes empty (i.e. all resulting data has been fetched) the further elements of the stream will get ignored, but still all remained data is required to be processed.
IMO, the two streams solution is the most readable. And it may even be the most efficient solution using streams.
IMO, the best way to avoid multiple streams is to use a classical loop. For example:
// There may be bugs ...
boolean seenCompany = false;
boolean seenPrivate = false;
for (Id id: getIds()) {
if (!seenCompany && id.getType().equalsIgnoreCase("Company")) {
domainResp.setId(id.getId());
seenCompany = true;
} else if (!seenPrivate && id.getType().equalsIgnoreCase("Private")) {
domainResp.setPrivateId(id.getId());
seenPrivate = true;
}
if (seenCompany && seenPrivate) {
break;
}
}
It is unclear whether that is more efficient to performing one iteration or two iterations. It will depend on the class returned by getIds() and the code of iteration.
The complicated stuff with two flags is how you replicate the short circuiting behavior of findFirst() in your 2 stream solution. I don't know if it is possible to do that at all using one stream. If you can, it will involve something pretty cunning code.
But as you can see your original solution with 2 stream is clearly easier to understand than the above.
The main point of using streams is to make your code simpler. It is not about efficiency. When you try to do complicated things to make the streams more efficient, you are probably defeating the (true) purpose of using streams in the first place.
For your list of ids, you could just use a map, then assign them after retrieving, if present.
Map<String, Integer> seen = new HashMap<>();
for (Id id : ids) {
if (seen.size() == 2) {
break;
}
seen.computeIfAbsent(id.getType().toLowerCase(), v->id.getId());
}
If you want to test it, you can use the following:
record Id(String getType, int getId) {
#Override
public String toString() {
return String.format("[%s,%s]", getType, getId);
}
}
Random r = new Random();
List<Id> ids = r.ints(20, 1, 100)
.mapToObj(id -> new Id(
r.nextBoolean() ? "Company" : "Private", id))
.toList();
Edited to allow only certain types to be checked
If you have more than two types but only want to check on certain ones, you can do it as follows.
the process is the same except you have a Set of allowed types.
You simply check to see that your are processing one of those types by using contains.
Map<String, Integer> seen = new HashMap<>();
Set<String> allowedTypes = Set.of("company", "private");
for (Id id : ids) {
String type = id.getType();
if (allowedTypes.contains(type.toLowerCase())) {
if (seen.size() == allowedTypes.size()) {
break;
}
seen.computeIfAbsent(type,
v -> id.getId());
}
}
Testing is similar except that additional types need to be included.
create a list of some types that could be present.
and build a list of them as before.
notice that the size of allowed types replaces the value 2 to permit more than two types to be checked before exiting the loop.
List<String> possibleTypes =
List.of("Company", "Type1", "Private", "Type2");
Random r = new Random();
List<Id> ids =
r.ints(30, 1, 100)
.mapToObj(id -> new Id(possibleTypes.get(
r.nextInt((possibleTypes.size()))),
id))
.toList();
You can group by type and check the resulting map.
I suppose the type of ids is IdType.
Map<String, List<IdType>> map = trainResponse.getIds()
.stream()
.collect(Collectors.groupingBy(
id -> id.getType().toLowerCase()));
Optional.ofNullable(map.get("company")).ifPresent(ids -> domainResp.setId(ids.get(0).getId()));
Optional.ofNullable(map.get("private")).ifPresent(ids -> domainResp.setPrivateId(ids.get(0).getId()));
I'd recommend a traditionnal for loop. In addition of being easily scalable, this prevents you from traversing the collection multiple times.
Your code looks like something that'll be generalised in the future, thus my generic approch.
Here's some pseudo code (with errors, just for the sake of illustration)
Set<String> matches = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
for(id : trainResponse.getIds()) {
if (! matches.add(id.getType())) {
continue;
}
switch (id.getType().toLowerCase()) {
case "company":
domainResp.setId(id.getId());
break;
case "private":
...
}
}
Something along these lines can might work, it would go through the whole stream though, and won't stop at the first occurrence.
But assuming a small stream and only one Id for each type, why not?
Map<String, Consumer<String>> setters = new HashMap<>();
setters.put("Company", domainResp::setId);
setters.put("Private", domainResp::setPrivateId);
trainResponse.getIds().forEach(id -> {
if (setters.containsKey(id.getType())) {
setters.get(id.getType()).accept(id.getId());
}
});
We can use the Collectors.filtering from Java 9 onwards to collect the values based on condition.
For this scenario, I have changed code like below
final Map<String, String> results = trainResponse.getIds()
.stream()
.collect(Collectors.filtering(
id -> id.getType().equals("Company") || id.getIdContext().equals("Private"),
Collectors.toMap(Id::getType, Id::getId, (first, second) -> first)));
And getting the id from results Map.

Multiple function composition

Today I encountered a following Java assignment, and I can't figure out how to get past type erasure.
The task is to create a generic InputConverter class, which takes an input of type T and converts it using chain of multiple functions received as a method argument. It has to support a following notation:
Function<String, List<String>> lambda1 = ...;
Function<List<String>, String> lambda2 = ...;
Function<String, Integer> lambda3 = ...;
String input = ...;
List<String> res1 = new InputConverter(input).convertBy(lambda1);
Integer res2 = new InputConverter(input).convertBy(lambda1, lambda2, lambda3);
This is what I came up with:
import java.util.Arrays;
import java.util.function.Function;
public class InputConverter<T> {
private final T input;
public InputConverter(T input) {
this.input = input;
}
public <B> B convertBy(Function<T, ?> first, Function<?, ?>... functions) {
var res = first.apply(input);
Function<?, B> composed = Arrays.stream(functions)
.reduce(Function::andThen)
.orElse(Function.identity());
return composed.apply(res);
}
}
This doesn't work of course, since I can't find a way to determine the return type of the last function.
Notes:
InputConverter should define only one convertBy method, so method overloading is not an option.
This method should return the result of last function in the chain without the need of explicit casting.
Problem
You would need to chain the generics for each number of expected functions and chain the generic parameters as on the snippet below with five functions:
public <D> E convertBy(
Function<T, A> first, Function<A, B> second, Function<B, C> third,
Function<C, D> fourth, Function<D, E> fifth) {
...
}
However, this is not possible for unknown number of parameters (varargs). There is no such thing as "vargenerics" which would dynamically create and chain the generic parameters as above.
Solution
You can instead treat the InputConverter as a builder instead which returns self with each convertBy call and finally packs a result. This recursive behavior allows indefinite number of calls. Try it out:
public static class InputConverter<T> {
private final T data;
public InputConverter(T data) {
this.data = data;
}
public <U> InputConverter<U> convertBy(Function<T, U> function) {
return new InputConverter<>(function.apply(data));
}
public T pack() {
return data;
}
}
Pretty neat, isn't it? Let's see the usage on a minimal sample:
// let lambda1 split String by characters and create a List
Function<String, List<String>> lambda1 = str -> Arrays.stream(str.split(""))
.collect(Collectors.toList());
// let lambda2 get the first item
Function<List<String>, String> lambda2 = list -> list.get(0);
// let lambda3 parse a String into the Integer
Function<String, Integer> lambda3 = Integer::parseInt;
String input = "123"; // out sample numeric input
List<String> res1 = new InputConverter<String>(input) // don't forget the generics
.convertBy(lambda1)
.pack();
Integer res2 = new InputConverter<String>(input)
.convertBy(lambda1)
.convertBy(lambda2)
.convertBy(lambda3)
.pack();
System.out.println(res1); // [1, 2, 3]
System.out.println(res2); // 1
So, according to task's author the correct solution is this:
import java.util.Arrays;
import java.util.function.Function;
public class InputConverter<T> {
private final T input;
public InputConverter(T input) {
this.input = input;
}
public <B> B convertBy(Function<T, ?> first, Function... functions) {
var res = first.apply(input);
Function<Object, B> composed = Arrays.stream(functions)
.reduce(Function::andThen)
.orElse(Function.identity());
return composed.apply(res);
}
}
Which is not satisfying to me at all. It allows for using vararg parameters, but using raw, unparameterized Function makes no sense. Nikolas Charalambidis' answer is a much better solution as we preserve return type information and safety.

Cleaning a list of data in Java8

For cleaning a list of data, I have created a method which accepts the list of data and list of cleaning operation to be performed.
public <T> List<T> cleanData(List<T> data, List<Function<T, T>> cleanOps) {
List<T>dataNew=data.stream().map((str) -> {
T cleanData = str;
for(Function<T,T> function:cleanOps) {
cleanData=function.apply(cleanData);
}
return cleanData;
}).collect(Collectors.toList());
return dataNew;
}
The issue here is that we are creating the whole list again as Collectors.toList() returns a new list.
Can we achieve the same result without using the extra space?
Below is the code for invocation:
public void processData() {
List<Function<String, String>> cleanOps = new ArrayList<>();
cleanOps.add(String::toLowerCase);
cleanOps.add(str -> str.replaceAll(" ", ""));
List<String> data = new ArrayList<>();
data.add("John Doe");
data.add("Jane Doe");
System.out.println(Arrays.toString(cleanData(data, cleanOps).toArray()));
}
If modifying the list in-place is allowed, you could use
public <T> List<T> cleanData(List<T> data, List<Function<T, T>> cleanOps) {
cleanOps.stream().reduce(Function::andThen).ifPresent(f -> data.replaceAll(f::apply));
return data;
}
andThen combines two Function instances and if at least one function was present, i.e. the cleanOps list is not empty, the resulting combined function will be applied to all list elements and the elements replaced by the result, using replaceAll.
Unfortunately, replaceAll requires a UnaryOperator<T> rather than a Function<T,T>, despite being functionally equivalent, so we have to use the adapter f::apply.
Since these function types are equivalent, we could change the list to List<UnaryOperator<T>>, but then, we have to face the fact that there is no specialized andThen implementation for UnaryOperator, so we would need:
public <T> List<T> cleanData(List<T> data, List<UnaryOperator<T>> cleanOps) {
cleanOps.stream()
.reduce((f1,f2) -> t -> f2.apply(f1.apply(t)))
.ifPresent(data::replaceAll);
return data;
}
The caller’s source changes to
List<UnaryOperator<String>> cleanOps = new ArrayList<>();
cleanOps.add(String::toLowerCase);
cleanOps.add(str -> str.replaceAll(" ", ""));
List<String> data = new ArrayList<>();
data.add("John Doe");
data.add("Jane Doe");
System.out.println(cleanData(data, cleanOps));
then.
As a side note, there is no need for a construct like
System.out.println(Arrays.toString(cleanData(data, cleanOps).toArray()));
as the toString() method of a List produces exactly the same output. Since the println(Object) method calls toString() implicitly, you can just use
System.out.println(cleanData(data, cleanOps));
It looks like you need to use List.replaceAll(), which replaces each element of this list with the result of applying the given operator to that element.
public <T> List<T> cleanString(List<T> data, List<Function<T, T>> cleanOps) {
data.replaceAll(str -> {
T cleanData = str;
for (Function<T,T> function : cleanOps) {
cleanData = function.apply(cleanData);
}
return cleanData;
});
return data;
}
I'd rename the method, though, since it's generic, so it doesn't necessarily process a List of Strings.

Add prefix and suffix to Collectors.joining() only if there are multiple items present

I have a stream of strings:
Stream<String> stream = ...;
I want to construct a string which concatenates these items with , as a separator. I do this as following:
stream.collect(Collectors.joining(","));
Now I want add a prefix [ and a suffix ] to this output only if there were multiple items. For example:
a
[a,b]
[a,b,c]
Can this be done without first materializing the Stream<String> to a List<String> and then checking on List.size() == 1? In code:
public String format(Stream<String> stream) {
List<String> list = stream.collect(Collectors.toList());
if (list.size() == 1) {
return list.get(0);
}
return "[" + list.stream().collect(Collectors.joining(",")) + "]";
}
It feels odd to first convert the stream to a list and then again to a stream to be able to apply the Collectors.joining(","). I think it's suboptimal to loop through the whole stream (which is done during a Collectors.toList()) only to discover if there is one or more item(s) present.
I could implement my own Collector<String, String> which counts the number of given items and use that count afterwards. But I am wondering if there is a directer way.
This question intentionally ignores there case when the stream is empty.
Yes, this is possible using a custom Collector instance that will use an anonymous object with a count of items in the stream and an overloaded toString() method:
public String format(Stream<String> stream) {
return stream.collect(
() -> new Object() {
StringJoiner stringJoiner = new StringJoiner(",");
int count;
#Override
public String toString() {
return count == 1 ? stringJoiner.toString() : "[" + stringJoiner + "]";
}
},
(container, currentString) -> {
container.stringJoiner.add(currentString);
container.count++;
},
(accumulatingContainer, currentContainer) -> {
accumulatingContainer.stringJoiner.merge(currentContainer.stringJoiner);
accumulatingContainer.count += currentContainer.count;
}
).toString();
}
Explanation
Collector interface has the following methods:
public interface Collector<T,A,R> {
Supplier<A> supplier();
BiConsumer<A,T> accumulator();
BinaryOperator<A> combiner();
Function<A,R> finisher();
Set<Characteristics> characteristics();
}
I will omit the last method as it is not relevant for this example.
There is a collect() method with the following signature:
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
and in our case it would resolve to:
<Object> Object collect(Supplier<Object> supplier,
BiConsumer<Object, ? super String> accumulator,
BiConsumer<Object, Object> combiner);
In the supplier, we are using an instance of StringJoiner (basically the same thing that Collectors.joining() is using).
In the accumulator, we are using StringJoiner::add() but we increment the count as well
In the combiner, we are using StringJoiner::merge() and add the count to the accumulator
Before returning from format() function, we need to call toString() method to wrap our accumulated StringJoiner instance in [] (or leave it as is is, in case of a single-element stream
The case for an empty case could also be added, I left it out in order not to make this collector more complicated.
There is already an accepted answer and I upvoted it too.
Still I would like to offer potentially another solution. Potentially because it has one requirement:
The stream.spliterator() of Stream<String> stream needs to be Spliterator.SIZED.
If that applies to your case, you could use also this solution:
public String format(Stream<String> stream) {
Spliterator<String> spliterator = stream.spliterator();
StringJoiner sj = spliterator.getExactSizeIfKnown() == 1 ?
new StringJoiner("") :
new StringJoiner(",", "[", "]");
spliterator.forEachRemaining(sj::add);
return sj.toString();
}
According to the JavaDoc Spliterator.getExactSizeIfKnown() "returns estimateSize() if this Spliterator is SIZED, else -1." If a Spliterator is SIZED then "estimateSize() prior to traversal or splitting represents a finite size that, in the absence of structural source modification, represents an exact count of the number of elements that would be encountered by a complete traversal."
Since "most Spliterators for Collections, that cover all elements of a Collection report this characteristic" (API Note in JavaDoc of SIZED) this could be the desired directer way.
EDIT:
If the Stream is empty we can return an empty String at once. If the Stream has only one String there is no need to create a StringJoiner and to copy the String to it. We return the single String directly.
public String format(Stream<String> stream) {
Spliterator<String> spliterator = stream.spliterator();
if (spliterator.getExactSizeIfKnown() == 0) {
return "";
}
if (spliterator.getExactSizeIfKnown() == 1) {
AtomicReference<String> result = new AtomicReference<String>();
spliterator.tryAdvance(result::set);
return result.get();
}
StringJoiner result = new StringJoiner(",", "[", "]");
spliterator.forEachRemaining(result::add);
return result.toString();
}
I know that I'm late to the party, but I too came across this very same problem recently, and I thought it might be worthwhile documenting how I solved it.
While the accepted solution does work, my opinion is that it is more complicated than it needs to be, making it both hard to read and understand. As opposed to defining an Object implementation and then separate accumulator and combiner functions that access and modify the state of said object, why not just define your own collector? In the problem presented above, this can be achieved as follows:
Collector<CharSequence, StringJoiner, String> collector = new Collector<>() {
int count = 0;
#Override
public Supplier<StringJoiner> supplier() {
return () -> new StringJoiner(",");
}
#Override
public BiConsumer<StringJoiner, CharSequence> accumulator() {
return (joiner, sequence) -> {
count++;
joiner.add(sequence);
};
}
#Override
public BinaryOperator<StringJoiner> combiner() {
return StringJoiner::merge;
}
#Override
public Function<StringJoiner, String> finisher() {
return (joiner) -> {
String joined = joiner.toString();
return (count > 1) ? "[" + joined + "]" : joined;
};
}
#Override
public Set<Characteristics> characteristics() {
return Collections.emptySet();
}
};
The principle is the same, you have a counter that is incremented whenever elements are added to the StringJoiner. Based on the total number of elements added, the finisher decides wheater the StringJoiner result should be wrapped in square brackets. It goes without saying, but you can even define a separate class specific to this implementation, allowing it to be used throughout your codebase. You can even go a step further by making it more generic to support a wider range of input types, adding custom prefix/suffix parameters, and so on. Speaking of use, just pass instances of the collector to the collect method of your Stream:
return list.stream().collect(collector);

Idiomatically enumerating a Stream of objects in Java 8

How can one idiomatically enumerate a Stream<T> which maps each T instance to a unique integer using Java 8 stream methods (e.g. for an array T[] values, creating a Map<T,Integer> where Map.get(values[i]) == i evaluates to true)?
Currently, I'm defining an anonymous class which increments an int field for use with the Collectors.toMap(..) method:
private static <T> Map<T, Integer> createIdMap(final Stream<T> values) {
return values.collect(Collectors.toMap(Function.identity(), new Function<T, Integer>() {
private int nextId = 0;
#Override
public Integer apply(final T t) {
return nextId++;
}
}));
}
However, is there not a more concise/elegant way of doing this using the Java 8 stream API? — bonus points if it can be safely parallelized.
Your approach will fail, if there is a duplicate element.
Besides that, your task requires mutable state, hence, can be solved with Mutable reduction. When we populate a map, we can simple use the map’s size to get an unused id.
The trickier part is the merge operation. The following operation simply repeats the assignments for the right map, which will handle potential duplicates.
private static <T> Map<T, Integer> createIdMap(Stream<T> values) {
return values.collect(HashMap::new, (m,t) -> m.putIfAbsent(t,m.size()),
(m1,m2) -> {
if(m1.isEmpty()) m1.putAll(m2);
else m2.keySet().forEach(t -> m1.putIfAbsent(t, m1.size()));
});
}
If we rely on unique elements, or insert an explicit distinct(), we can use
private static <T> Map<T, Integer> createIdMap(Stream<T> values) {
return values.distinct().collect(HashMap::new, (m,t) -> m.put(t,m.size()),
(m1,m2) -> { int leftSize=m1.size();
if(leftSize==0) m1.putAll(m2);
else m2.forEach((t,id) -> m1.put(t, leftSize+id));
});
}
I would do it in this way:
private static <T> Map<T, Integer> createIdMap2(final Stream<T> values) {
List<T> list = values.collect(Collectors.toList());
return IntStream.range(0, list.size()).boxed()
.collect(Collectors.toMap(list::get, Function.identity()));
}
For sake or parallelism, it can be changed to
return IntStream.range(0, list.size()).parallel().boxed().
(...)
Comparing to convert the input stream to List first in the solution provided by Andremoniy. I would prefer to do it in different way because we don't know the cost of "toList()" and "list.get(i)", and it's unnecessary to create an extra List, which could be small or bigger
private static <T> Map<T, Integer> createIdMap2(final Stream<T> values) {
final MutableInt idx = MutableInt.of(0); // Or: final AtomicInteger idx = new AtomicInteger(0);
return values.collect(Collectors.toMap(Function.identity(), e -> idx.getAndIncrement()));
}
Regardless to the question, I think it's a bad design to pass streams as parameters in a method.

Categories