Refactor two for's into java 8 streams - java

I'm facing a small problem to rewrite my two for's into java 8 streams.
// This is a method parameter
Map<String, Collection<String>> preSelectedValues;
List<PersonModel> parameters = parameterSearchService.getParameterNames();
for(Iterator<Map.Entry<String, Collection<String>>> it = preSelectedValues.entrySet().iterator(); it.hasNext();) {
Map.Entry<String, Collection<String>> entry = it.next();
for(int i = 0; i < parameters.size(); i++) {
if (entry.getKey().startsWith(parameters.get(i).getName())) {
it.remove();
}
}
}
I've tried following streams to have the same behaviour as before:
Map<String, Collection<String>> filteredParameters = preSelectedValues.keySet().stream()
.filter(x -> isParameterValid(x, parameters))
.collect(Collectors.toMap(k -> k, v -> preSelectedValues.get(v)));
isParameterValid method:
private boolean isParameterValid(String parameterId, List<PersonModel> preSelectedValues) {
return preSelectedValues.stream()
.anyMatch(v -> !v.getName().startsWith(parameterId));
}
Basically what I'm trying to do is filter the "preSelectedValues" map which starts with "parameterId". But somehow when I'm using streams either it filters everything or nothing.

Your isParameterValid method doesn't have the same logic as the original loops, since:
You switched the instance and argument in the call to startsWith.
Calling anyMatch with a !v.getName().startsWith(parameterId) only tells you whether at least one element of the List<PersonModel> doesn't start with parameterId. Your original condition for keeping the entry in the Map was that all the elements of List<PersonModel> don't start with parameterId (or actually, the other way around - parameterId doesn't start with any of the names of elements of List<PersonModel>).
Therefore I negated the method to return the condition for removing an entry from the Map:
private boolean isParameterInvalid(String parameterId, List<PersonModel> preSelectedValues) {
return preSelectedValues.stream()
.anyMatch(v -> parameterId.startsWith(v.getName()));
}
And then the stream pipeline can look like this:
Map<String, Collection<String>> filteredParameters = preSelectedValues.entrySet().stream()
.filter(x -> !isParameterInvalid(x.getKey(), parameters))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
BTW, since your original loops mutate the original Map, you can achieve the same with removeIf.

If you are trying to modify the original map you can use removeIf:
preSelectedValues.keySet().removeIf(
key -> parameters.stream()
.map(PersonModel::getName)
.anyMatch(key::startsWith)
);

Related

java stream is closed

I want to use stream getfirst method two times but an error occurred says (java.lang.IllegalStateException: stream has already been operated upon or closed) and this stream code begins at comment named here.
//code
Stream<Map.Entry<String,Integer>> sorted =
map.entrySet().stream()
.sorted(Collections.reverseOrder(Map.Entry.comparingByValue()));
Supplier<Stream<Map.Entry<String,Integer>>> sort2 = () -> sorted;
Optional<String> kk = Optional.of(sort2.get().findFirst().get().getKey());
Optional<Integer> vv = Optional.of(sort2.get().findFirst().get().getValue());
int vmax = vv.get().intValue() ;
int count=0;
ArrayList<String> a = new ArrayList<String>() ;
for(Map.Entry<String,Integer> h: map.entrySet() ) {
if(h.getValue()==vmax) {
a.add(h.getKey()) ;
count++;
}
}
if(count>1) {
Collections.sort(a);
System.out.println(a.get(0));
}
else {
System.out.println(kk.get());
}
map.clear();
}
}
}
You can't use a stream twice. Create a variable to hold the value of findFirst()
sort2 is not doing anything for you, and simply hiding the issue that you can't reuse sorted.
In this situation, by far the best solution is to store sorted.findFirst() in a variable and reuse it rather than call findFirst() twice. If you wanted to do it like this, then you would have to write
Supplier<Stream<Map.Entry<String,Integer>>> sort2 = () -> map.entrySet().stream()
.sorted(Collections.reverseOrder(Map.Entry.comparingByValue()));
...which would sort the entries twice, which would be very inefficient.
Stream.findFirst is a terminal operation - calling findFirst twice on the same stream like you are doing is not possible.
It looks like you don't need to call it twice: even if you restarted a new stream, both times you would expect to get the same map entry. So call findFirst only once, and use the Optional it returns to you:
Optional<Map.Entry<String, Integer>> firstEntry = sorted.findFirst();
Optional<String> kk = firstEntry.map(Map.Entry::getKey);
Optional<Integer> vv = firstEntry.map(Map.Entry::getValue);
Note that there's a better way to get the maximum item from the stream than sorting: using the max operator.
Optional<Map.Entry<String, Integer>> maxEntry = map.entrySet().stream()
.max(Map.Entry.comparingByValue());

Nested For Loop conversion to Lambda & Map Comparison

I am struggling to convert an existing forloop to a lambda expression.
I have a list of objects (List<Task>) which contains a map called 'InputData' (Map<String, String>).
I also have another Map<String, String> (Acceptance).
I want to filter the list of Task objects where the Task.InputData Map contains all the 'Acceptance' map entries. Task.InputData may contain additional entries but I want to enforce the vars that exist in Acceptance must exist in Task.InputData.
The existing for loop looks like so:
boolean addTask;
List<Task> finalResults = new ArrayList<>();
for (Task t : results) {
addTask = true;
for (Entry<String, String> k : kvVars.entrySet()) {
if (!t.getInputData().containsKey(k) || !t.getInputData().get(k).equals(k.getValue())) {
addTask = false;
}
}
if (addTask) {
finalResults.add(t);
}
}
I'm a bit confused about an approach, whether I should be trying to combine flatmap and filter criteria or if I should follow the logic in the existing for loop.
You can use the filter to collect all Task's that having all the 'Acceptance' k/v in InputData
List<Task> finalResults = results.stream()
.filter(t->kvVars.entrySet()
.stream()
.allMatch(k->t.getInputData().containsKey(k.getKey())
&& t.getInputData().get(k.getKey()).equals(k.getValue()))
.collect(Collectors.toList());
You can use Set.containsAll on the entry set of the input data map to check your condition:
List<Task> finalResults = results.stream()
.filter(t -> t.getInputData().entrySet().containsAll(kvVars.entrySet()))
.collect(Collectors.toList());

Collect to map the order/position value of a sorted stream

I am sorting a populated set of MyObject (the object has a getName() getter) in a stream using a predefined myComparator.
Then once sorted, is there a way to collect into a map the name of the MyObject and the order/position of the object from the sort?
Here is what I think it should look like:
Set<MyObject> mySet; // Already populated mySet
Map<String, Integer> nameMap = mySet.stream()
.sorted(myComparator)
.collect(Collectors.toMap(MyObject::getName, //HowToGetThePositionOfTheObjectInTheStream));
For example, if the set contain three objects (object1 with name name1, object2 with name name2, object3 with name name3) and during the stream they get sorted, how do I get a resulting map that looks like this:
name1, 1
name2, 2
name3, 3
Thanks.
A Java Stream doesn't expose any index or positioning of elements, so I know no way of replacing /*HowToGetThePositionOfTheObjectInTheStream*/ with streams magic to obtain the desired number.
Instead, one simple way is to collect to a List instead, which gives every element an index. It's zero-based, so when converting to a map, add 1.
List<String> inOrder = mySet.stream()
.sorted(myComparator)
.map(MyObject::getName)
.collect(Collectors.toList());
Map<String, Integer> nameMap = new HashMap<>();
for (int i = 0; i < inOrder.size(); i++) {
nameMap.put(inOrder.get(i), i + 1);
}
Try this one. you could use AtomicInteger for value of each entry of map. and also to guarantee order of map use LinkedHashMap.
AtomicInteger index = new AtomicInteger(1);
Map<String, Integer> nameMap = mySet.stream()
.sorted(myComparator)
.collect(Collectors
.toMap(MyObject::getName, value -> index.getAndIncrement(),
(e1, e2) -> e1, LinkedHashMap::new));
The simplest solution would be a loop, as a formally correct stream solution that would also work in parallel requires a nontrivial (compared to the rest) merge functions:
Map<String,Integer> nameMap = mySet.stream()
.sorted(myComparator)
.collect(HashMap::new, (m, s) -> m.put(s.getName(), m.size()),
(m1, m2) -> {
int offset = m1.size();
m2.forEach((k, v) -> m1.put(k, v + offset));
});
Compare with a loop/collection operations:
List<MyObject> ordered = new ArrayList<>(mySet);
ordered.sort(myComparator);
Map<String, Integer> result = new HashMap<>();
for(MyObject o: ordered) result.put(o.getName(), result.size());
Both solutions assume unique elements (as there can be only one position). It’s easy to change the loop to detect violations:
for(MyObject o: ordered)
if(result.putIfAbsent(o.getName(), result.size()) != null)
throw new IllegalStateException("duplicate " + o.getName());
Dont use a stream:
List<MyObject> list = new ArrayList<>(mySet);
list.sort(myComparator);
Map<String, Integer> nameMap = new HashMap<>();
for (int i = 0; i < list.size(); i++) {
nameMap.put(list.get(i).getName(), i);
}
Not only will this execute faster than a stream based approach, everyone knows what's going on.
Streams have their place, but pre-Java 8 code does too.

How to convert a Collection<Pair<K, Collection<V>> to a List<MyObject<K,V>> using Java stream API?

My traditional code would look like this:
List<MyObject> transform(Collection<java.util.Map.Entry<String, List<String>>> input) {
List<MyObject> output = new LinkedList<>();
for (Entry<String, List<String>> pair : input) {
for (String value : pair.getValue()) {
output.add(new MyObject(pair.getKey(), value));
}
}
return output;
}
Can I do the same with lambda expressions? I’ve tried around, but I don’t get it. The outer collection is unsorted, but the List<String> is sorted. The result objects may return in the result list without any order, with the exception that objects created from the same key String should follow each other to preserve the order of the value. Is this at all possible?
input.stream()
.flatMap(e -> e.getValue()
.stream()
.map(v -> new MyObject(e.getKey(), v)))
.collect(Collectors.toCollection(LinkedList::new));
You can use streams and Stream.flatMap as in this answer, however I think that the code is much clearer if you stick to loops, either traditional ones as in your question, or modern ones:
List<MyObject> output = new LinkedList<>();
input.forEach(pair -> pair.getValue()
.forEach(value -> output.add(new MyObject(pair.getKey(), value))));
By the way, I'd use an ArrayList instead of a LinkedList.

Transform and filter a Java Map with streams

I have a Java Map that I'd like to transform and filter. As a trivial example, suppose I want to convert all values to Integers then remove the odd entries.
Map<String, String> input = new HashMap<>();
input.put("a", "1234");
input.put("b", "2345");
input.put("c", "3456");
input.put("d", "4567");
Map<String, Integer> output = input.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> Integer.parseInt(e.getValue())
))
.entrySet().stream()
.filter(e -> e.getValue() % 2 == 0)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
System.out.println(output.toString());
This is correct and yields: {a=1234, c=3456}
However, I can't help but wonder if there's a way to avoid calling .entrySet().stream() twice.
Is there a way I can perform both transform and filter operations and call .collect() only once at the end?
Yes, you can map each entry to another temporary entry that will hold the key and the parsed integer value. Then you can filter each entry based on their value.
Map<String, Integer> output =
input.entrySet()
.stream()
.map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), Integer.valueOf(e.getValue())))
.filter(e -> e.getValue() % 2 == 0)
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue
));
Note that I used Integer.valueOf instead of parseInt since we actually want a boxed int.
If you have the luxury to use the StreamEx library, you can do it quite simply:
Map<String, Integer> output =
EntryStream.of(input).mapValues(Integer::valueOf).filterValues(v -> v % 2 == 0).toMap();
One way to solve the problem with much lesser overhead is to move the mapping and filtering down to the collector.
Map<String, Integer> output = input.entrySet().stream().collect(
HashMap::new,
(map,e)->{ int i=Integer.parseInt(e.getValue()); if(i%2==0) map.put(e.getKey(), i); },
Map::putAll);
This does not require the creation of intermediate Map.Entry instances and even better, will postpone the boxing of int values to the point when the values are actually added to the Map, which implies that values rejected by the filter are not boxed at all.
Compared to what Collectors.toMap(…) does, the operation is also simplified by using Map.put rather than Map.merge as we know beforehand that we don’t have to handle key collisions here.
However, as long as you don’t want to utilize parallel execution you may also consider the ordinary loop
HashMap<String,Integer> output=new HashMap<>();
for(Map.Entry<String, String> e: input.entrySet()) {
int i = Integer.parseInt(e.getValue());
if(i%2==0) output.put(e.getKey(), i);
}
or the internal iteration variant:
HashMap<String,Integer> output=new HashMap<>();
input.forEach((k,v)->{ int i = Integer.parseInt(v); if(i%2==0) output.put(k, i); });
the latter being quite compact and at least on par with all other variants regarding single threaded performance.
Guava's your friend:
Map<String, Integer> output = Maps.filterValues(Maps.transformValues(input, Integer::valueOf), i -> i % 2 == 0);
Keep in mind that output is a transformed, filtered view of input. You'll need to make a copy if you want to operate on them independently.
You could use the Stream.collect(supplier, accumulator, combiner) method to transform the entries and conditionally accumulate them:
Map<String, Integer> even = input.entrySet().stream().collect(
HashMap::new,
(m, e) -> Optional.ofNullable(e)
.map(Map.Entry::getValue)
.map(Integer::valueOf)
.filter(i -> i % 2 == 0)
.ifPresent(i -> m.put(e.getKey(), i)),
Map::putAll);
System.out.println(even); // {a=1234, c=3456}
Here, inside the accumulator, I'm using Optional methods to apply both the transformation and the predicate, and, if the optional value is still present, I'm adding it to the map being collected.
Another way to do this is to remove the values you don't want from the transformed Map:
Map<String, Integer> output = input.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> Integer.parseInt(e.getValue()),
(a, b) -> { throw new AssertionError(); },
HashMap::new
));
output.values().removeIf(v -> v % 2 != 0);
This assumes you want a mutable Map as the result, if not you can probably create an immutable one from output.
If you are transforming the values into the same type and want to modify the Map in place this could be alot shorter with replaceAll:
input.replaceAll((k, v) -> v + " example");
input.values().removeIf(v -> v.length() > 10);
This also assumes input is mutable.
I don't recommend doing this because It will not work for all valid Map implementations and may stop working for HashMap in the future, but you can currently use replaceAll and cast a HashMap to change the type of the values:
((Map)input).replaceAll((k, v) -> Integer.parseInt((String)v));
Map<String, Integer> output = (Map)input;
output.values().removeIf(v -> v % 2 != 0);
This will also give you type safety warnings and if you try to retrieve a value from the Map through a reference of the old type like this:
String ex = input.get("a");
It will throw a ClassCastException.
You could move the first transform part into a method to avoid the boilerplate if you expect to use it alot:
public static <K, VO, VN, M extends Map<K, VN>> M transformValues(
Map<? extends K, ? extends VO> old,
Function<? super VO, ? extends VN> f,
Supplier<? extends M> mapFactory){
return old.entrySet().stream().collect(Collectors.toMap(
Entry::getKey,
e -> f.apply(e.getValue()),
(a, b) -> { throw new IllegalStateException("Duplicate keys for values " + a + " " + b); },
mapFactory));
}
And use it like this:
Map<String, Integer> output = transformValues(input, Integer::parseInt, HashMap::new);
output.values().removeIf(v -> v % 2 != 0);
Note that the duplicate key exception can be thrown if, for example, the old Map is an IdentityHashMap and the mapFactory creates a HashMap.
Here is code by abacus-common
Map<String, String> input = N.asMap("a", "1234", "b", "2345", "c", "3456", "d", "4567");
Map<String, Integer> output = Stream.of(input)
.groupBy(e -> e.getKey(), e -> N.asInt(e.getValue()))
.filter(e -> e.getValue() % 2 == 0)
.toMap(Map.Entry::getKey, Map.Entry::getValue);
N.println(output.toString());
Declaration: I'm the developer of abacus-common.

Categories