using java 8 groupBy with conditions - java

Group by number to find groups with size bigger then 1
2. Check if a group contains a condition (string !=null)
2a. if yes ---> remove all rows which do not have a condition (string == null)
2b. if no ---> return the group as it is (no filtering at all)
I am trying below code but I am not able to filter the condition here. Is there any simple way to do it?
groupByList = list.stream().collect(Collectors.groupingBy(student::no));
groupByList.values().stream().map(group -> group.size() > 1)
.collect(Collectors.toList());

You need to use both Stream.filter(..) and Stream.map(..)
// Groups students by group number
Map<String, List<Student>> allGroups = list.stream().collect(Collectors.groupingBy(student::no));
// Filters groups that have more than 1 member
Map<String, List<Student>> filteredGroups = allGroups.entrySet().stream().filter(entry -> entry.getValue().size() > 1).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
// Removes all non-null members
Map<String, List<Student>> cleanGroups = filteredGroups.entrySet().stream()
.map(entry -> {
entry.setValue(entry.getValue().stream().filter(student -> student.name != null).collect(Collectors.toList()));
return entry;
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
You can pipe the execution into one big chain:
Map<String, List<Student>> cleanGroups = list.stream()
.collect(Collectors.groupingBy(student::no))
.entrySet().stream()
.filter(entry -> entry.getValue().size() > 1)
.map(entry -> {
entry.setValue(entry.getValue().stream()
.filter(student -> student.name != null)
.collect(Collectors.toList()));
return entry;
})
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Please note, that you have a hidden object transformation withing the .filter(..).collect(..) above. If you need to skip that (at cost of double comparison), you can extend the lambda method to:
.map(entry -> {
if (entry.getValue().stream.anyMatch(student.name == null)) {
entry.setValue(entry.getValue().stream().filter(student -> student.name != null).collect(Collectors.toList()));
}
return entry;
})

Related

How to filter multivalued map with list

I have request parameters coming in and trying to remove the parameters that are empty.
http://example.com/get/users?dept=admin&dept=&area=999
Here I want to remove empty dept from my request parameters before passing it on to the database query.
private Map<String, List<String>> getUsers(MultiValueMap<String, String> args) {
return args.entrySet().stream()
.filter(i -> i.getValue().stream()
.filter(StringUtils::isNotBlank). // return only non empty values
.anyMatch(s -> !s.isEmpty()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
I'm expecting the output to be like
"dept" -> ["admin"]
"area" -> ["999"]
If http://example.com/get/users?dept=&dept=&area=999 all the dept values are empty then totally remove dept from the map
"area" -> ["999"]
Edit: Current behaviour
If http://example.com/get/users?dept=&dept=&area=999 then
"area" -> ["999"]
If http://example.com/get/users?dept=admin&dept=&area=999 then
"area" -> ["999"]
"dept" -> ["", "admin"]
Instead I want just
"area" -> ["999"]
"dept" -> ["admin"]
What about using flatMap. See this demo on IDEone:
Map<String, List<String>> requestParams = Map.of("dept", List.of("a", " "), "area", List.of("x"));
var result = requestParams.entrySet().stream()
.map(entry -> new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue().stream()
.flatMap(Stream::of) // flatten the list to a stream to process further
// .filter( StringUtils::isNotBlank )
.filter(s -> s != null && !s.isBlank()) // return only non empty values
.collect(Collectors.toList()) // collect as List (to return as entry)
))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
System.out.println(result);
It has two levels:
outer: map to a new entry and collect
inner: filter the values (List) for all non-blank Strings
If you also want to exclude parameters that have a key but no value (i.e. null or empty List), then you would add a filter for that before the last .collect to Map:
.filter(entry -> entry.getValue() != null && !entry.getValue().isEmpty())
See also:
Java 8 convert Map<Integer, List<String>> to Map<String, List<Integer>> with

how do I use java 8 streams groupingby to calculate average with filters

I need to calculate the average of a List of objects that I'm streaming.
The objects have:
ClassX.id
ClassX.name
ClassX.value
ClassX.startTime
ClassX.endTime
The objects must be grouped by ClassX.name and having the average calculated using ClassX.value.
Each object streamed represents either an start or end of a transaction.
Start transactions has ClassX.endTime == null.
End transactions has ClassX.startTime == null.
End transactions has ClassX.name == null
The value to be aggregated is within the start object, but it must be summed to the average only if the stream also process the corresponding end object of the transation.
Here's what I have so far(based on the suggestion of Andreas):
List<ClassX> classXList = ...
Map<String, Double> average = classXListStrings.stream()
.map(ClassX::new) //convert to ClassX(the input list is actually String)
.filter(x -> x.getName() != null) //avoid null entries for getName
.collect(Collectors.groupingBy(ClassX::getName, Collectors.toList()))
.entrySet().stream()
// skip group if no end transaction exists
.filter(e -> e.getValue().stream().anyMatch(x -> x.getStartTime() != null))
.collect(Collectors.toMap(Entry::getKey,
e -> e.getValue().stream()
// only average values of start transactions
.filter(x -> x.getEndTime() == null)
.collect(Collectors.averagingDouble(ClassX::getValue))
));
Is there a way to maybe store the objects streamed into a data structure and then aggregate the value only if the pair of objects begin/end transaction are streamed based on a filter?
It's hard to associate one object in a stream with another that appears later.
One solution is run through the list twice: First you find the end transactions, collecting them to a set. Then you process the list again, computing the averages.
List<ClassX> inputList = ...
Set<String> endSet = inputList.stream()
.filter(o -> o.endTime != null)
.map(o -> o.id)
.collect(Collectors.toSet());
Map<String, Double> average = inputList.stream()
.filter(o -> o.startTime != null && endSet.contains(o.id))
.collect(Collectors.groupingBy(
o -> o.name,
Collectors.averagingDouble(o -> o.value)));
You can do it like this:
List<ClassX> classXList = ...
Map<String, Double> average = classXList.stream()
.collect(Collectors.groupingBy(ClassX::getName, Collectors.toList()))
.entrySet().stream()
// skip group if no end transaction exists
.filter(e -> e.getValue().stream().anyMatch(x -> x.getStartTime() == null))
.collect(Collectors.toMap(Entry::getKey,
e -> e.getValue().stream()
// only average values of start transactions
.filter(x -> x.getEndTime() == null)
.collect(Collectors.averagingDouble(ClassX::getValue))
));

Nested repeating stream filtering

I am learning Java streams and have a situation where I need to use different filter on a stream in case filtering with the first filter didn't return any results. Currently I have this, thinking if there is a better way to write this:
String str = requestParams.entrySet()
.stream()
.filter(e -> e.getKey().contains("string1") && e.getValue().length() > 0)
.map(Map.Entry::getValue)
.findFirst().orElseGet(() -> requestParams.entrySet()
.stream()
.filter(e -> e.getKey().contains("string2") && e.getValue().length() > 0)
.map(Map.Entry::getValue)
.findFirst().orElse(""));
As you want to search the entire source for this criteria e.getKey().contains("string1") && e.getValue().length() > 0 and only then if it's not met then you want to do another search for e.getKey().contains("string2") && e.getValue().length() > 0 I would say your current approach is fine but just encapsulate the logic into a method so you DRY i.e.
static Optional<String> getValueByX(Map<String, String> requestParams, String searchString) {
return requestParams.entrySet()
.stream()
.filter(e -> e.getKey().contains(searchString) &&
e.getValue().length() > 0)
.map(Map.Entry::getValue)
.findFirst();
}
Then you would call it as:
String result = getValueByX(requestParams, "string1")
.orElseGet(() -> getValueByX(requestParams, "string2").orElse(""));
However, if you don't care about searching for "string1" in the entire source before applying the criteria for "string2" then you're better of doing:
String str = requestParams.entrySet()
.stream()
.filter(e -> (e.getKey().contains("string1") || e.getKey().contains("string2")) && e.getValue().length() > 0)
.map(Map.Entry::getValue)
.findFirst().orElse("")
if there could be more options than the aforementioned two then I would create a list to contain those values and then perform:
List<String> search = Arrays.asList("string1", "string2"..........);
String result = requestParams.entrySet()
.stream()
.filter(e -> search.stream().anyMatch(a -> e.getKey().contains(a)) &&
e.getValue().length() > 0)
.map(Map.Entry::getValue)
.findFirst().orElse("");
You can do it using the Collectors.teeing method by processing two streams in parallel and filtering. This returns an Optional and will be either s1, s2, or Non-existent depending on the map contents.
This presumes that you want to favor string1 before accepting string2
Map<String, String> requestParams = Map.of("string1","s1","string3",
"s3", "string2", "s2", "string4","s4");
Optional<String> op = requestParams.entrySet().stream()
.filter(e -> e.getValue().length() > 0)
.collect(Collectors.teeing(Collectors.filtering(
e -> e.getKey().contains("string1"),
Collectors.reducing((a, b) -> a)),
Collectors.filtering(
e -> e.getKey().contains("string2"),
Collectors.reducing((a, b) -> a)),
(opt1, opt2) -> opt1.or(() -> opt2)
.map(Entry::getValue)));
System.out.println(op.orElse("Non Existent"));
The above use of Collectors.teeing was modified to incorporate suggestion by Holger to use Collectors.reducing to get first item.
You can check both conditions simultaneously, since the other logic is the same:
List<String> keys = Arrays.asList("string1", "string2");
requestParams.entrySet()
.stream()
.filter(e -> keys.contains(e.getKey()) && e.getValue().length() > 0)
.map(Map.Entry::getValue)
.findFirst().orElse("");

Proper usage of Streams in Java

I've a use-case where I need to parse key-value pairs (separated by =) and put these key-value pairs in a LinkedHashMap.
I want to ignore the following type of Strings
key is empty or contains only spaces
value is empty or contains only spaces
those Strings which don't contain a =.
Now, I have solved it using imperative style and by using streams also.
The following are the 2 variants:
Solution by iterative style - for loop and lots of if
public static Map<String, String> getMap1(String[] array) {
Map<String, String> map = new LinkedHashMap<>();
for (int i = 0; i < array.length; i++) {
String currentString = array[i];
int index = currentString.indexOf('=');
// ignoring strings that don't contain '='
if (index == -1) continue;
String key = currentString.substring(0, index).trim();
String value = currentString.substring(index + 1).trim();
// ignoring strings with empty key or value
if (key.length() == 0 || value.length() == 0) continue;
map.put(key, value);
}
return map;
}
Solution that uses Streams - pretty clean code
public static Map<String, String> getMap(String[] array) {
return Arrays.stream(array)
.filter(s -> s.indexOf('=') != -1) // ignore strings that don't contain '='
.filter(s -> s.substring(0, s.indexOf('=')).trim().length() != 0) // key should be present
.filter(s -> s.substring(s.indexOf('=') + 1).trim().length() != 0) // value should be present
.collect(Collectors.toMap(
s -> s.substring(0, s.indexOf('=')).trim(),
s -> s.substring(s.indexOf('=') + 1).trim(),
(first, second) -> second,
LinkedHashMap::new));
}
I'm worried here because while using Streams, I'm calling the indexOf method multiple times. (And for big strings, I can end-up recalculating the same thing again and again).
Is there a way I can avoid re-computation done by indexOf method in such a way that the code is still clean. (I know talking about clean-code is very subjective, but I want don't want to open multiple streams, of loop through the original string-array and subsequently pre-computing the indices of = and re-using that).
Clubbing multiple filters into a single filter again seem to be an option but that would make my predicate pretty ugly.
(This is a result of my idle musing where I wish to learn/improve).
What about this:
String[] array = {"aaa2=asdas","aaa=asdasd"};
LinkedHashMap<String, String> aaa = Arrays.stream(array)
.map(s -> s.split("=", 2))
.filter(s -> s.length == 2) // ignore strings that don't contain '='
.peek(s -> { s[0] = s[0].trim(); })
.peek(s -> { s[1] = s[1].trim(); })
.filter(s -> s[0].length() != 0) // key should be present
.filter(s -> s[1].length() != 0) // value should be present
.collect(Collectors.toMap(
s -> s[0],
s -> s[1],
(first, second) -> second,
LinkedHashMap::new));
I'd use split instead of indexOf and StringUtils to check that your keys and values are not empty.
public static Map<String, String> getMap(String[] array) {
return Arrays.stream(array)
.filter(s -> s.contains("="))
.map(s -> s.split("="))
.filter(s -> s.length == 2 && isNotBlank(s[0]) && isNotBlank(s[1]))
.collect(Collectors.toMap(
s -> s[0].trim(),
s -> s[1].trim()));
}

Filtering with Java 8: Map<String, Set<Object>> from Map<String, Set<Object>> based on an attribute of Object

This is class Item.
public class Item {
String id;
String name;
Integer value;
Boolean status;
}
I have a Map(String, Set(Item)). I want to write a method that returns a Map(String, Set(Item)) such that only Items with status = false or status = null are present in the resulting map. I don't want a set-wide operation. I want the resulting subsets to only contain those Item that have status == Boolean.FALSE OR status == null. I don't want the entire set to get included or excluded. I only want those individual items included or excluded as per the status value.
Here's what I've tried so far.
public Map<String,Set<Item>> filterByStatus(Map<String, Set<Item>> changes) {
return changes.entrySet()
.stream()
.filter(p -> p.getValue()
.stream()
.anyMatch(item -> BooleanUtils.isNotTrue(item.isStatus())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
It didn't work! I get back the same results as I would if I didn't call filterByStatus.
UPDATE
public Map<String,Set<Item>> filterByStatus(Map<String, Set<Item>> changes) {
return changes.entrySet()
.stream()
.map(p -> p.getValue()
.stream()
.filter(item -> BooleanUtils.isNotTrue(item.isStatus())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
Result: There's an error in the collect(Collectors.toMap()) line saying Non-static method cannot be referenced from static context.
public Map<String, Set<Item>> filterByStatus(Map<String, Set<Item>> changes) {
return changes.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry ->
entry.getValue()
.stream()
.filter(item -> item.status == null || item.status == Boolean.FALSE)
.collect(Collectors.toSet())
));
}
Alternatively to a Stream solution, you may use
public Map<String, Set<Item>> filterByStatus(Map<String, Set<Item>> changes) {
Map<String, Set<Item>> result = new HashMap<>(changes);
result.replaceAll((key, set) -> {
set = new HashSet<>(set);
set.removeIf(item -> Boolean.TRUE.equals(item.status));
return set;
});
// if you want to remove empty sets afterwards:
result.values().removeIf(Set::isEmpty);
return result;
}
You could even do the operation in-place if the sets are mutable and you don’t need the old state anymore:
changes.values().forEach(set -> set.removeIf(item -> Boolean.TRUE.equals(item.status)));
// if you want to remove empty sets afterwards (and the map is mutable):
changes.values().removeIf(Set::isEmpty);
you could even remove these items, followed by removing the set only if they became empty due to the removal, in one statement:
changes.values().removeIf(set ->
set.removeIf(item -> Boolean.TRUE.equals(item.status)) && set.isEmpty());
Judging from your description you are looking for allMatch rather than anyMatch.
Currently you get all the sets which contain at least one non-True value. What you seem to want is having only sets that consist of non-True values only.
If you are rather looking for filtering out the negative values from all sets, you should use a mapping, not just filter, on the Map. In the mapping you could create copies of the sets with True values excluded.
This avoid include in new Map entrys with 0 items.
private Map<String,Set<Item>> filterByStatus(Map<String, Set<Item>> changes) {
return changes.entrySet()
.stream()
.filter(entry -> entry.getValue()
.stream()
.anyMatch(item -> item.status == null || item.status == false))
.collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue()
.stream()
.filter(item -> item.status == null || item.status == false)
.collect(Collectors.toSet()))
);
}

Categories