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

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

Related

Java - Converting to Lambda with a boolean to return

I haven't used much of Lambda and I have the following method that I'd like to rewrite as Lambda, so it still maintains the functionality, but in a more compact way. The method gets a map out of a map by an id. Then looks for 2 conditions in the map to return true. My current issue is that I'm not finding a proper syntax to do a "return true;" statement from my Lambda. That was part #1. Part #2 question is about using nested maps. The nested map represent static data from a small csv file - under 100 rows. The inner map contains between 1 to 3 records. Is there a better way storing this in memory besides using a nested map? Would love to have some help here.
Old function as follows:
private Map<String, Map<String, Integer>> availability;
public boolean hasAvailability(int quantity, String currentLocation, Product product) {
Map<String, Integer> m = this.availability.get(product.getProductId());
for (Map.Entry<String, Integer> entry : m.entrySet()) {
if(entry.getKey().equalsIgnoreCase(currentLocation) && entry.getValue().intValue() > quantity)
return true;
}
return false;
}
You can use anyMatch on a stream of the entrySet, also you don't need to call toString() on a String. Like,
public boolean hasAvailability(int quantity, String currentLocation, Product product) {
Map<String, Integer> m = this.availability.get(product.getProductId());
return m.entrySet().stream().anyMatch(
entry -> entry.getKey().equalsIgnoreCase(currentLocation) &&
entry.getValue().intValue() > quantity);
}
How about something like this. Just stream the entrySet and return true or false if the entry is located.
public boolean hasAvailability(int quantity, String currentLocation, Product product) {
Map<String, Integer> m = this.availability.get(product.getProductId());
return m.entrySet().stream()
.anyMatch(entry->entry.getKey().equalsIgnoreCase(currentLocation) &&
entry.getValue() > quantity);
}
Note that you could turn the above into a lambda but you would need to declare a functional interface that accepted three paramaters and returned a boolean. I don't believe that is what you really want or should do. The above approach still streamlines the requirement a bit.

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 combine these 2 lambdas into a single GroupBy call?

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

Java: how to convert a List<?> to a Map<String,?> [duplicate]

This question already has answers here:
How to convert List to Map?
(20 answers)
Closed 7 years ago.
I would like to find a way to take the object specific routine below and abstract it into a method that you can pass a class, list, and fieldname to get back a Map.
If I could get a general pointer on the pattern used or , etc that could get me started in the right direction.
Map<String,Role> mapped_roles = new HashMap<String,Role>();
List<Role> p_roles = (List<Role>) c.list();
for (Role el : p_roles) {
mapped_roles.put(el.getName(), el);
}
to this? (Pseudo code)
Map<String,?> MapMe(Class clz, Collection list, String methodName)
Map<String,?> map = new HashMap<String,?>();
for (clz el : list) {
map.put(el.methodName(), el);
}
is it possible?
Using Guava (formerly Google Collections):
Map<String,Role> mappedRoles = Maps.uniqueIndex(yourList, Functions.toStringFunction());
Or, if you want to supply your own method that makes a String out of the object:
Map<String,Role> mappedRoles = Maps.uniqueIndex(yourList, new Function<Role,String>() {
public String apply(Role from) {
return from.getName(); // or something else
}});
Here's what I would do. I am not entirely sure if I am handling generics right, but oh well:
public <T> Map<String, T> mapMe(Collection<T> list) {
Map<String, T> map = new HashMap<String, T>();
for (T el : list) {
map.put(el.toString(), el);
}
return map;
}
Just pass a Collection to it, and have your classes implement toString() to return the name. Polymorphism will take care of it.
Java 8 streams and method references make this so easy you don't need a helper method for it.
Map<String, Foo> map = listOfFoos.stream()
.collect(Collectors.toMap(Foo::getName, Function.identity()));
If there may be duplicate keys, you can aggregate the values with the toMap overload that takes a value merge function, or you can use groupingBy to collect into a list:
//taken right from the Collectors javadoc
Map<Department, List<Employee>> byDept = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
As shown above, none of this is specific to String -- you can create an index on any type.
If you have a lot of objects to process and/or your indexing function is expensive, you can go parallel by using Collection.parallelStream() or stream().parallel() (they do the same thing). In that case you might use toConcurrentMap or groupingByConcurrent, as they allow the stream implementation to just blast elements into a ConcurrentMap instead of making separate maps for each thread and then merging them.
If you don't want to commit to Foo::getName (or any specific method) at the call site, you can use a Function passed in by a caller, stored in a field, etc.. Whoever actually creates the Function can still take advantage of method reference or lambda syntax.
Avoid reflection like the plague.
Unfortunately, Java's syntax for this is verbose. (A recent JDK7 proposal would make it much more consise.)
interface ToString<T> {
String toString(T obj);
}
public static <T> Map<String,T> stringIndexOf(
Iterable<T> things,
ToString<T> toString
) {
Map<String,T> map = new HashMap<String,T>();
for (T thing : things) {
map.put(toString.toString(thing), thing);
}
return map;
}
Currently call as:
Map<String,Thing> map = stringIndexOf(
things,
new ToString<Thing>() { public String toString(Thing thing) {
return thing.getSomething();
}
);
In JDK7, it may be something like:
Map<String,Thing> map = stringIndexOf(
things,
{ thing -> thing.getSomething(); }
);
(Might need a yield in there.)
Using reflection and generics:
public static <T> Map<String, T> MapMe(Class<T> clz, Collection<T> list, String methodName)
throws Exception{
Map<String, T> map = new HashMap<String, T>();
Method method = clz.getMethod(methodName);
for (T el : list){
map.put((String)method.invoke(el), el);
}
return map;
}
In your documentation, make sure you mention that the return type of the method must be a String. Otherwise, it will throw a ClassCastException when it tries to cast the return value.
If you're sure that each object in the List will have a unique index, use Guava with Jorn's suggestion of Maps.uniqueIndex.
If, on the other hand, more than one object may have the same value for the index field (which, while not true for your specific example perhaps, is true in many use cases for this sort of thing), the more general way do this indexing is to use Multimaps.index(Iterable<V> values, Function<? super V,K> keyFunction) to create an ImmutableListMultimap<K,V> that maps each key to one or more matching values.
Here's an example that uses a custom Function that creates an index on a specific property of an object:
List<Foo> foos = ...
ImmutableListMultimap<String, Foo> index = Multimaps.index(foos,
new Function<Foo, String>() {
public String apply(Foo input) {
return input.getBar();
}
});
// iterate over all Foos that have "baz" as their Bar property
for (Foo foo : index.get("baz")) { ... }

Categories

Resources