Nested For Loop conversion to Lambda & Map Comparison - java

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());

Related

How to use forEach after computeIfPresent

I have this snippet of code, and I want to use forEach after computeIfpresent function. Mainly, if the key is found, then we should loop over the values (list), and fetch each entry and add it to another list. Any idea how I can do that?
List<Long> myArrayList = new ArrayList();
Map<Long, Set<Long>> myMap = new HashMap();
Set<Long> mySet = MyMap().get(id);
if (mySet != null)
{
for (Long ex : mySet)
{
myArrayList.add(ex);
}
}
-->??
myMap.computeIfPresent(id, (key, value) -> value.forEach(ex -> myArrayList.add(ex)));
computeIfPresent is for changing the value inside the HashMap under the given key (id in your case). It means that such operation is not allowed because by running forEach on the key:value pair you are not providing any new mapping value for a map
What you could do would be
myMap.computeIfPresent("a", (k, v) -> {
((Set<Long>)v).forEach(e -> myArrayList.add(e));
return v;
});
but it looks bad and is violating the purpose of computeIfPresent method
You should just use rather traditional approach
if (myMap.containsKey(id)) {
myArrayList.addAll(myMap.get(id));
}
or use an Optional
Optional.ofNullable(map.get(id)).ifPresent(set ->
myArrayList.addAll((Set<Long>)set)
);
...but is this more readable? :)
If I get it right, you are looking for Map.getOrDefault instead of Map.computeIfPresent chained with a foreach. Using Map.getOrDefault the task could be rewritten to:
List<Long> myArrayList = new ArrayList<>();
Map<Long, Set<Long>> myMap = new HashMap<>();
long id = ...;
myArrayList.addAll(myMap.getOrDefault(id, Collections.emptySet()));

Filter map by its value using Java Stream API

I am new to Java streams and want to run the below code using streams.
List<String> roles = new ArrayList<>();
if (stats.containsKey("roles")) {
roles = stats.get("roles");
} else {
Map<String, String> roleMap = stats.containsKey("attributes") ?
stats.get("attributes") : new HashMap<>();
for (Map.Entry<String, String> e : roleMap.entrySet()) {
if (e.getValue().equals("true")) {
roles.add(e.getKey());
}
}
}
In the above code I am doing the following steps :
I have a stats hashmap in which first I am checking if the roles key is present, if it is present I am returning the corresponding value.
if the stats hashmap does not contain the roles key, I am checking if stats hashmap contains key attributes.
If the attribute key present then its value is Hashmap and then I am traversing the hashmap and checking wherever its value is equal to "true", I am adding the corresponding key to my roles list.
Input:
{
"attributes":{
"id":false
"name":true
}
}
Output:
["name"]
Can this whole code be reduced by streams?
Not sure how much this will help. Using streams in your use case according to me is not simplifying the solution, but if you want to use then you can do something like this in your code.
Map<String,Object> stats=new HashMap<>();
List<String> roles = new ArrayList<>();
stats.entrySet().stream()
.forEach(i -> {
if (i.getKey().contains("roles")) {
roles.add((String)i.getValue());
} else if(i.getKey().contains("attributes")){
Map<String, String> roleMap= (Map<String, String>)i.getValue();
roleMap.entrySet().stream().forEach(j-> {
if (j.getValue()
.equalsIgnoreCase("true"))
roles.add(j.getKey());
});
}
});
stats.keySet().stream().filter(s->s.equals("roles")).findFirst().map(s->stats.get("roles")).orElseGet(()->{
List list= Lists.newArrayList();
stats.get("attributes").foreach((k,v)->{
if v;list.add(k)
})
return list;
})
If the values of the keys roles and attributes were of same type (List<String>) you could easily do something like
List<String> roles = stats.getOrDefault("roles",
stats.getOrDefault("attributes", new ArrayList<>()));
You could still use this approach, but since the assosiated values are of different types you need to make some casting which makes your code maybe a little bit unintuitive:
Map<String, Object> stats = new HashMap<>();
stats.put("roles", Arrays.asList("role1", "role2"));
stats.put("attributes", Map.of("id", "false", "name", "true"));
List<String> roles = (List<String>) stats.getOrDefault("roles",
((Map<String, String>) stats.getOrDefault("attributes", new HashMap<>()))
.entrySet()
.stream()
.filter(i -> i.getValue().equalsIgnoreCase("true"))
.map(Map.Entry::getKey)
.collect(Collectors.toList()));
System.out.println(roles);

Reduce the size of list with Java 8 stream

I want to reduce the size (delete some elements) of an ordered list of map objects. All objects of list should be discarded unless a certain condition is met. And when that condition is met all next elements of that list should remained in the list. I have following piece of code. I want to do the same with Java 8.
public List<Map<String, String>> doAction(List<Map<String, String>> dataVoMap) {
List<Map<String,String>> tempMap = new ArrayList<>();
boolean found = false;
for(Map<String, String> map: dataVoMap){
if(map.get("service_id").equalsIgnoreCase("passed value") || found){
found = true;
tempMap.add(map);
}
}
dataVoMap = tempMap;
return dataVoMap;
}
You are looking for a dropWhile operation, but an in-built implementation of that would require Java-9 and above:
public List<Map<String, String>> doAction(List<Map<String, String>> dataVoMap) {
return dataVoMap.stream()
.dropWhile(m -> !"passed value".equalsIgnoreCase(m.get("service_id")))
.collect(Collectors.toList());
}
Note: I have made an edit to the existing code to avoid NPE when there could be a Map in the List without the key service_id.
There is a solution with using a little hack:
public static List<Map<String, String>> doAction(List<Map<String, String>> dataVoMap) {
AtomicBoolean found = new AtomicBoolean(false);
return dataVoMap.stream()
.filter(map -> found.get() || "passed value".equalsIgnoreCase(map.get("service_id")))
.peek(map -> found.set(true))
.collect(Collectors.toList());
}
If you don't need to parallel the stream you can use this solution with Java 8.

Adding two lists of own type

I have a simple User class with a String and an int property.
I would like to add two Lists of users this way:
if the String equals then the numbers should be added and that would be its new value.
The new list should include all users with proper values.
Like this:
List1: { [a:2], [b:3] }
List2: { [b:4], [c:5] }
ResultList: {[a:2], [b:7], [c:5]}
User definition:
public class User {
private String name;
private int comments;
}
My method:
public List<User> addTwoList(List<User> first, List<User> sec) {
List<User> result = new ArrayList<>();
for (int i=0; i<first.size(); i++) {
Boolean bsin = false;
Boolean isin = false;
for (int j=0; j<sec.size(); j++) {
isin = false;
if (first.get(i).getName().equals(sec.get(j).getName())) {
int value= first.get(i).getComments() + sec.get(j).getComments();
result.add(new User(first.get(i).getName(), value));
isin = true;
bsin = true;
}
if (!isin) {result.add(sec.get(j));}
}
if (!bsin) {result.add(first.get(i));}
}
return result;
}
But it adds a whole lot of things to the list.
This is better done via the toMap collector:
Collection<User> result = Stream
.concat(first.stream(), second.stream())
.collect(Collectors.toMap(
User::getName,
u -> new User(u.getName(), u.getComments()),
(l, r) -> {
l.setComments(l.getComments() + r.getComments());
return l;
}))
.values();
First, concatenate both the lists into a single Stream<User> via Stream.concat.
Second, we use the toMap collector to merge users that happen to have the same Name and get back a result of Collection<User>.
if you strictly want a List<User> then pass the result into the ArrayList constructor i.e. List<User> resultSet = new ArrayList<>(result);
Kudos to #davidxxx, you could collect to a list directly from the pipeline and avoid an intermediate variable creation with:
List<User> result = Stream
.concat(first.stream(), second.stream())
.collect(Collectors.toMap(
User::getName,
u -> new User(u.getName(), u.getComments()),
(l, r) -> {
l.setComments(l.getComments() + r.getComments());
return l;
}))
.values()
.stream()
.collect(Collectors.toList());
You have to use an intermediate map to merge users from both lists by summing their ages.
One way is with streams, as shown in Aomine's answer. Here's another way, without streams:
Map<String, Integer> map = new LinkedHashMap<>();
list1.forEach(u -> map.merge(u.getName(), u.getComments(), Integer::sum));
list2.forEach(u -> map.merge(u.getName(), u.getComments(), Integer::sum));
Now, you can create a list of users, as follows:
List<User> result = new ArrayList<>();
map.forEach((name, comments) -> result.add(new User(name, comments)));
This assumes User has a constructor that accepts name and comments.
EDIT: As suggested by #davidxxx, we could improve the code by factoring out the first part:
BiConsumer<List<User>, Map<String, Integer>> action = (list, map) ->
list.forEach(u -> map.merge(u.getName(), u.getComments(), Integer::sum));
Map<String, Integer> map = new LinkedHashMap<>();
action.accept(list1, map);
action.accept(list2, map);
This refactor would avoid DRY.
There is a pretty direct way using Collectors.groupingBy and Collectors.reducing which doesnt require setters, which is the biggest advantage since you can keep the User immutable:
Collection<Optional<User>> d = Stream
.of(first, second) // start with Stream<List<User>>
.flatMap(List::stream) // flatting to the Stream<User>
.collect(Collectors.groupingBy( // Collecting to Map<String, List<User>>
User::getName, // by name (the key)
// and reducing the list into a single User
Collectors.reducing((l, r) -> new User(l.getName(), l.getComments() + r.getComments()))))
.values(); // return values from Map<String, List<User>>
Unfortunately, the result is Collection<Optional<User>> since the reducing pipeline returns Optional since the result might not be present after all. You can stream the values and use the map() to get rid of the Optional or use Collectors.collectAndThen*:
Collection<User> d = Stream
.of(first, second) // start with Stream<List<User>>
.flatMap(List::stream) // flatting to the Stream<User>
.collect(Collectors.groupingBy( // Collecting to Map<String, List<User>>
User::getName, // by name (the key)
Collectors.collectingAndThen( // reduce the list into a single User
Collectors.reducing((l, r) -> new User(l.getName(), l.getComments() + r.getComments())),
Optional::get))) // and extract from the Optional
.values();
* Thanks to #Aomine
As alternative fairly straight and efficient :
stream the elements
collect them into a Map<String, Integer> to associate each name to the sum of comments (int)
stream the entries of the collected map to create the List of User.
Alternatively for the third step you could apply a finishing transformation to the Map collector with collectingAndThen(groupingBy()..., m -> ...
but I don't find it always very readable and here we could do without.
It would give :
List<User> users =
Stream.concat(first.stream(), second.stream())
.collect(groupingBy(User::getName, summingInt(User::getComments)))
.entrySet()
.stream()
.map(e -> new User(e.getKey(), e.getValue()))
.collect(toList());

Refactor two for's into java 8 streams

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)
);

Categories