How to combine these 2 lambdas into a single GroupBy call? - java

I have a function like this:
private static Map<String, ResponseTimeStats> perOperationStats(List<PassedMetricData> scopedMetrics, Function<PassedMetricData, String> classifier)
{
Map<String, List<PassedMetricData>> operationToDataMap = scopedMetrics.stream()
.collect(groupingBy(classifier));
return operationToDataMap.entrySet().stream()
.collect(toMap(Map.Entry::getKey, e -> StatUtils.mergeStats(e.getValue())));
}
Is there any way to have the groupBy call do the transformation that i do explicitly in line 2 so i dont have to separately stream over the map?
Update
Here is what mergeStats() looks like:
public static ResponseTimeStats mergeStats(Collection<PassedMetricData> metricDataList)
{
ResponseTimeStats stats = new ResponseTimeStats();
metricDataList.forEach(data -> stats.merge(data.stats));
return stats;
}

If you can rewrite StatUtils.mergeStats into a Collector, you could just write
return scopedMetrics.stream().collect(groupingBy(classifier, mergeStatsCollector));
And even if you can't do this, you could write
return scopedMetrics.stream().collect(groupingBy(classifier,
collectingAndThen(toList(), StatUtils::mergeStats)));

In order to group the PassedMetricData instances, you must consume the entire Stream since, for example, the first and last PassedMetricData might be grouped into the same group.
That's why the grouping must be a terminal operation on the original Stream and you must create a new Stream in order to do the transformation on the results of this grouping.
You could chain these two statements, but it won't make much of a difference :
private static Map<String, ResponseTimeStats> perOperationStats(List<PassedMetricData> scopedMetrics, Function<PassedMetricData, String> classifier)
{
return scopedMetrics.stream()
.collect(groupingBy(classifier)).entrySet().stream()
.collect(toMap(Map.Entry::getKey, e -> StatUtils.mergeStats(e.getValue())));
}

Related

Stream().map() result as Collectors.toMap() value

I'm having List<InstanceWrapper>, for each element I want to do some logic that will result in some String message. Then, I want to take create Map<String, String>, where key is InstanceWrapper:ID and value is message;
private String handle(InstanceWrapper instance, String status) {
return "logic result...";
}
private Map<String, String> handleInstances(List<InstanceWrapper> instances, String status) {
return instances.stream().map(instance -> handle(instance, status))
.collect(Collectors.toMap(InstanceWrapper::getID, msg -> msg));
}
But it wont compile, I'm getting, how do I put stream().map() result into collectors.toMap() value?
The method collect(Collector<? super String,A,R>) in the type Stream<String> is not applicable for the arguments (Collector<InstanceWrapper,capture#5-of ?,Map<String,Object>>)
You cannot map before collecting to map, because then you're getting Stream of Strings and loosing information about InstanceWrapper. Stream#toMap takes two lambdas - one generating keys and second - generating values. It should be like that:
instances.stream()
.collect(Collectors.toMap(InstanceWrapper::getID, instance -> handle(instance, status));
The first lambda generates keys: InstanceWrapper::getID, the second one - associated values: instance -> handle(instance, status).
You map every InstanceWrapper to a String but if you want to use the InstanceWrapper later to extract its ID you can not do this. Try something like this instead:
return instances.stream()
.collect(Collectors.toMap(InstanceWrapper::getID, (InstanceWrapper instanceWrapper) -> this.handle(instanceWrapper, status)));
Edit:
To beautify this, you could simulate currying a little bit like this:
private Function<InstanceWrapper, String> handle(String status) {
return (InstanceWrapper instanceWrapper) -> "logic result...";
}
private Map<String, String> handleInstances(List<InstanceWrapper> instances, String status) {
return instances.stream()
.collect(Collectors.toMap(InstanceWrapper::getID, this.handle(status)));
}

How can I properly make a multilevel map using lambdas?

I'm comparing files in folders (acceptor & sender) using JCIFS. During comparation two situations may occur:
- file not exists at acceptor
- file exists at acceptor
I need to get a map, where compared files are groupped by mentioned two types, so i could copy non-existing files or chech size and modification date of existing...
I want to make it using lambdas and streams, because i woult use parallel streams in near future, and it's also convinient...\
I've managed to make a working prototype method that checks whether file exists and creates a map:
private Map<String, Boolean> compareFiles(String[] acceptor, String[] sender) {
return Arrays.stream(sender)
.map(s -> new AbstractMap.SimpleEntry<>(s, Stream.of(acceptor).anyMatch(s::equals)))
Map.Entry::getValue)));
.collect(collectingAndThen(
toMap(Map.Entry::getKey, Map.Entry::getValue),
Collections::<String,Boolean> unmodifiableMap));
}
but i cant add higher level grouping by map value...
I have such a non-working piece of code:
private Map<String, Boolean> compareFiles(String[] acceptor, String[] sender) {
return Arrays.stream(sender)
.map(s -> new AbstractMap.SimpleEntry<>(s, Stream.of(acceptor).anyMatch(s::equals)))
.collect(groupingBy(
Map.Entry::getValue,
groupingBy(Map.Entry::getKey, Map.Entry::getValue)));
}
}
My code can't compile, because i missed something very important.. Could anyone help me please and exlain how to make this lambda correct?
P.S. arrays from method parameters are SmbFiles samba directories:
private final String master = "smb://192.168.1.118/mastershare/";
private final String node = "smb://192.168.1.118/nodeshare/";
SmbFile masterDir = new SmbFile(master);
SmbFile nodeDir = new SmbFile(node);
Map<Boolean, <Map<String, Boolean>>> resultingMap = compareFiles(masterDir, nodeDir);
Collecting into nested maps with the same values, is not very useful. The resulting Map<Boolean, Map<String, Boolean>> can only have two keys, true and false. When you call get(true) on it, you’ll get a Map<String, Boolean> where all string keys redundantly map to true. Likewise, get(false) will give a you map where all values are false.
To me, it looks like you actually want
private Map<Boolean, Set<String>> compareFiles(String[] acceptor, String[] sender) {
return Arrays.stream(sender)
.collect(partitioningBy(Arrays.asList(acceptor)::contains, toSet()));
}
where get(true) gives you a set of all strings where the predicate evaluated to true and vice versa.
partitioningBy is an optimized version of groupingBy for boolean keys.
Note that Stream.of(acceptor).anyMatch(s::equals) is an overuse of Stream features. Arrays(acceptor).contains(s) is simpler and when being used as a predicate like Arrays.asList(acceptor)::contains, the expression Arrays.asList(acceptor) will get evaluated only once and a function calling contains on each evaluation is passed to the collector.
When acceptor gets large, you should not consider parallel processing, but replacing the linear search with a hash lookup
private Map<Boolean, Set<String>> compareFiles(String[] acceptor, String[] sender) {
return Arrays.stream(sender)
.collect(partitioningBy(new HashSet<>(Arrays.asList(acceptor))::contains, toSet()));
}
Again, the preparation work of new HashSet<>(Arrays.asList(acceptor)) is only done once, whereas the contains invocation, done for every element of sender, will not depend on the size of acceptor anymore.
I've managed to solve my problem. I had a type mismatch, so the working code is:
private Map<Boolean, Map<String, Boolean>> compareFiles(String[] acceptor, String[] sender) {
return Arrays.stream(sender)
.map(s -> new AbstractMap.SimpleEntry<>(s, Stream.of(acceptor).anyMatch(s::equals)))
.collect(collectingAndThen(
groupingBy(Map.Entry::getValue, toMap(Map.Entry::getKey, Map.Entry::getValue)),
Collections::<Boolean, Map<String, Boolean>> unmodifiableMap));
}

Java 8 stream groupingBy String value

I have a JSON file containing data in the form:
{
"type":"type1",
"value":"value1",
"param": "param1"
}
{
"type":"type2",
"value":"value2",
"param": "param2"
}
I also have an object like this:
public class TestObject {
private final String value;
private final String param;
public TestObject(String value, String param) {
this.value = value;
this.param = param;
}
}
What I want is to create a Map<String, List<TestObject>> that contains a list of TestObjects for each type.
This is what I coded:
Map<String, List<TestObject>> result = jsonFileStream
.map(this::buildTestObject)
.collect(Collectors.groupingBy(line -> JsonPath.read(line, "$.type")));
Where the method buildTestObject is:
private TestObject buildTestObject(String line) {
return new TestObject(
JsonPath.read(line, "$.value"),
JsonPath.read(line, "$.param"));
}
This does not work because the map() function returns a TestObject, so that the collect function does not work on the JSON String line anymore.
In real life, I cannot add the "type" variable to the TestObjectfile, as it is a file from an external library.
How can I group my TestObjects by the type in the JSON file?
You can move the mapping operation to a down stream collector of groupingBy:
Map<String, List<TestObject>> result = jsonFileStream
.collect(Collectors.groupingBy(line -> JsonPath.read(line, "$.type"),
Collectors.mapping(this::buildTestObject, Collectors.toList())));
This will preserve the string so you can extract the type as a classifier, and applies the mapping to the elements of the resulting groups.
You can also use the toMap collector to accomplish the task at hand.
Map<String, List<TestObject>> resultSet = jsonFileStream
.collect(Collectors.toMap(line -> JsonPath.read(line, "$.type"),
line -> new ArrayList<>(Collections.singletonList(buildTestObject(line))),
(left, right) -> {
left.addAll(right);
return left;
}
));
In addition to the Stream solution, it's worth pointing out that Java 8 also significantly improved the Map interface, making this kind of thing
much less painful to achieve with a for loop than had previously been the case. I am not familiar with the library you are using, but something like this will work (you can always convert a Stream to an Iterable).
Map<String, List<TestObject>> map = new HashMap<>();
for (String line : lines) {
map.computeIfAbsent(JsonPath.read(line, "$.type"), k -> new ArrayList<>())
.add(buildTestObject(line));
}

How to specify Predicate in Java Stream when found too many items

I am looking for some object in map:
mapObjects.entrySet().stream().map(map -> map.getValue()).filter(predicateA)
When I find more then one item, I want to specify a second predicate to filter on some additional attribute. Is there some way I can do this in just one iteration of stream, or do I need to iterate once and when count > 1 then I need to iterate a second time with another predicate ?
For example, say I have list of persons. First I am looking for name=John. When there is more than one John, I look for surname=Smith. Now I don't care if there is more than one and I just take the first.
It could be done by first filtering the Person instances by name then grouping by surname. The result will be put into a LinkedHashMap in order to get the first match if there is no full match (name and surname), finally we rely on Map#getOrDefault(key, defaultValue) to get the full match if it exists otherwise it will get the first entry as default value.
Map<String, Person> map = mapObjects.values().stream()
.filter(p -> Objects.equals(p.getName(), name))
.collect(
Collectors.groupingBy(
Person::getSurname,
LinkedHashMap::new,
Collectors.collectingAndThen(Collectors.toList(), list -> list.get(0))
)
);
Optional<Person> result =
map.isEmpty() ?
Optional.absent() :
Optional.of(
map.getOrDefault(surname, map.entrySet().iterator().next().getValue())
);
This way you iterate only once to get your result and you don't use a stateful Predicate.
You can use a reduction operation which prioritizes the second predicate when possible:
mapObjects.values().stream()
.filter(predicateA)
.reduce((acc, obj) -> predicateB.test(obj) ? obj : acc)
.ifPresent(doThing);
Unfortunately, the reduction can't be short-circuited. If this is important, keep reading.
You could give predicateB a wrapping class which only tries to return true if it never has before and the argument meets the criteria. Here is an atomic implementation so that it still works in parallel streams.
public class ShortCircuitPredicate<T> implements Predicate<T> {
private final AtomicBoolean hasBeenTrue;
private final Predicate<T> predicate;
private ShortCircuitPredicate(Predicate<T> pred) {
hasBeenTrue = new AtomicBoolean(false);
predicate = pred;
}
public static <T> ShortCircuitPredicate<T> of(Predicate<T> pred) {
return new ShortCircuitPredicate<>(pred);
}
#Override
public boolean test(T t) {
return hasBeenTrue.get()
? false
: predicate.test(t) && hasBeenTrue.compareAndSet(false, true);
}
}
You can wrap predicateB using ShortCircuitPredicate.of(predicateB).
I am not convinced this problem is a good fit for using the stream API, but an option would be to rely on Stream#peek() to keep a reference to one of the elements that matches the first filter, if none match the second one:
List<Person> people = ...
Person[] holder = new Person[1];
Person result = people.stream()
.filter(p -> p.getName().equals("John"))
.peek(p -> holder[0] = p)
.filter(p -> p.getSurname().equals("Smith"))
.findAny()
.orElse(holder[0]);
This is short-circuiting in case there is any match for both filters. On the other hand, peek() and the second filter will have to be executed on all matches of the first predicate before findAny() returns an empty optional. Consequently, the holder will always be filled, except when there is no match to the first predicate.
I suggested carefully reading In Java streams is peek really only for debugging? though, and make your own opinion on whether this is an appropriate option for your specific case.

Java 8 way to convert collection into one key multiple values map

Any way to perform the below code using Java 8.
final Map<String, Collection<ProductStrAttributeOverrideRulesModel>> attributeRulesMap = new HashMap<String, Collection<ProductStrAttributeOverrideRulesModel>>();
for (final ProductStrAttributeOverrideRulesModel rule : rules)
{
final String key = rule.getProductStrAttributeOverride().getProductStrTypeField().getAttributeDescriptorQualifier();
if (attributeRulesMap.containsKey(key))
{
final Collection<ProductStrAttributeOverrideRulesModel> currentRules = attributeRulesMap.get(key);
currentRules.add(rule);
}
else
{
final Collection<ProductStrAttributeOverrideRulesModel> list = new LinkedList<ProductStrAttributeOverrideRulesModel>();
list.add(rule);
attributeRulesMap.put(key, list);
}
}
if it is only
final Map<String, ProductStrAttributeOverrideRulesModel> attributeRulesMap
than i can do like following but i need to arrange the whole collection inside a map based on key and each key can have multiple values stored in collection.
Map<String, ProductStrAttributeOverrideRulesModel> result =
choices.stream().collect(Collectors.toMap(ProductStrAttributeOverrideRulesModel::getProductStrAttributeOverride.getProductStrTypeField.getAttributeDescriptorQualifier,
Function.identity()));
You could use groupingBy :
Map<String,List<ProductStrAttributeOverrideRulesModel>>
map =
choices.stream()
.collect(Collectors.groupingBy(rule -> rule.getProductStrAttributeOverride().getProductStrTypeField().getAttributeDescriptorQualifier()));
And if you don't want a List, you can pass a second argument to groupingBy and specify whatever Collection you want. For example :
Map<String,Collection<ProductStrAttributeOverrideRulesModel>>
map =
choices.stream()
.collect(Collectors.groupingBy(rule -> rule.getProductStrAttributeOverride().getProductStrTypeField().getAttributeDescriptorQualifier(),
Collectors.toCollection(HashSet::new)));
Note that it doesn’t always have to be a Stream operation. Your code would also benefit from using the “diamond operator” (though not new to Java 8) and from using new collection operations, i.e. computeIfAbsent, which allows to elide the entire conditional inside the loop and its code duplication. Putting both together, you’ll get:
final Map<String, Collection<ProductStrAttributeOverrideRulesModel>>
attributeRulesMap = new HashMap<>();
for(final ProductStrAttributeOverrideRulesModel rule: rules)
{
final String key = rule.getProductStrAttributeOverride()
.getProductStrTypeField().getAttributeDescriptorQualifier();
attributeRulesMap.computeIfAbsent(key, x->new LinkedList<>()).add(rule);
}
You could also replace the loop by a forEach invocation, if you wish:
rules.forEach(rule -> attributeRulesMap.computeIfAbsent(
rule.getProductStrAttributeOverride()
.getProductStrTypeField().getAttributeDescriptorQualifier(),
x->new LinkedList<>()).add(rule)
);
though it’s debatable whether this is an improvement over the classical loop here…

Categories

Resources