I have a flux of response form below responses as Flux.<Response>fromIterable(responses). I want to convert this to Mono of map as follows:
Mono< Map< String, Map< String, Collection< Response>>>> collectMap = ?
where company is first key for which another map of response will be generated with category as key.
List< Response> responses = new ArrayList(){
{
add(Response.builder().company("Samsung").category("Tab").price("$2000").itemName("Note").build());
add(Response.builder().company("Samsung").category("Phone").price("$2000").itemName("S9").build());
add(Response.builder().company("Samsung").category("Phone").price("$1000").itemName("S8").build());
add(Response.builder().company("Iphone").category("Phone").price("$5000").itemName("Iphone8").build());
add(Response.builder().company("Iphone").category("Tab").price("$5000").itemName("Tab").build());
}
};
Though I am able to achieve initial map as follow
Mono<Map<String, Collection<Response>>> collect = Flux.<Response>fromIterable( responses )
.collectMultimap( Response::getCompany );
Do someone has an idea how I can achieve my goal here.
I don't think collectMultiMap or collectMap helps you directly in this case:
The collectMultiMap (and its overloads) only can return Map<T, Collection<V> which is clearly different than what you want. Of course, you can process the resulting value set (namely the Collection<V> part of the map) with a O(n) complexity.
On the other hand collectMap (and its overloads) look a bit more promising, if you provide the value function. However, you don't have access to other V objects, which forbids you to build the Collection<V>.
The solution I came up with is using reduce; though the return type is:
Mono<Map<String, Map<String, List<Response>>>> (mind the List<V> instead of Collection<V>)
return Flux.<Response>fromIterable( responses )
.reduce(new HashMap<>(), (map, user) -> {
map.getOrDefault(user.getId(), new HashMap<>())
.getOrDefault(user.getEmail(), new ArrayList<>())
.add(user);
return map;
});
The full type for the HashMap in reduce is HashMap<String, Map<String, List<AppUser>>>, thankfully Java can deduce that from the return type of the method or the type of the assigned variable.
Related
I'm fairly new to Java and trying to learn how to use streams for easier code writing. If I can code like this:
Map<String, SomeConfig> temp = new HashMap<>();
resultStorage.forEach((key, value) -> key.getUsers().forEach(user -> {
if (!temp.containsKey(user.getMeta())) {
SomeConfig emailConfiguration = key
.withCheck1(masterAccountId)
.withCheck2(getClientTimezone())
.withCheck3(user.getMeta());
temp.put(user.getMeta(), emailConfiguration);
}
temp.get(user. getMeta()).getStreams().add(value);
}));
return new ArrayList<>(temp.values());
resultStorage declaration:
private Map< SomeConfig, byte[]> resultStorage = new ConcurrentHashMap<>();
getStreams is a getter on SomeConfig that returns a List<byte[]> as here:
private List<byte[]> attachmentStreams = new ArrayList<>();
public List<byte[]> getAttachmentStreams() {
return attachmentStreams;
}
My first attempt was something similar to this:
resultStorage.entrySet().stream()
.forEach(entry -> entry.getKey().getUsers().forEach(user -> {
}));
Are we able to use a forEach within one of the streams terminating operation, forEach? How would a stream benefit in this case as I saw documentation that it can significantly improve readability and performance of older pre-Java8 code?
Edit:
resultStorage holds a ConcurrentHashMap. It will contain Map<SomeConfig, byte[]> for email and attachments. Using another HashMap temp that is initially empty - we analyze resultStorage , see if temp contains a specific email key, and then put or add based on the existence of a user's email
The terminal operation of entrySet().stream().forEach(…) is entirely unrelated to the getUsers().forEach(…) call within the Consumer. So there’s no problem of “multiple terminal operations” here.
However, replacing the Map operation forEach((key, value) -> … with an entrySet() .stream() .forEach(entry -> …) rarely adds a benefit. So far, you’re not only made the code longer, you introduced the necessity to deal with a Map.Entry instead of just using key and value.
But you can simplify your operation by using a single computeIfAbsent instead of containsKey, put, and get:
resultStorage.forEach((key, value) -> key.getUsers().forEach(user ->
temp.computeIfAbsent(user.getMeta(), meta ->
key.withCheck1(masterAccountId).withCheck2(getClientTimezone()).withCheck3(meta))
.getStreams().add(value)));
Notes after the code.
Map<String, SomeConfig> temp = resultStorage.keySet()
.stream()
.flatMap(key -> key.getUsers()
.stream()
.map(user -> new AbstractMap.SimpleEntry(user, key)))
.collect(Collectors.toMap(e -> e.getKey().getMeta(),
e -> e.getValue()
.withCheck1(masterAccountId)
.withCheck2(getClientTimezone())
.withCheck3(e.getKey().getMeta())
resultStorage.keySet()
This returns Set<SomeConfig>.
stream()
This returns a stream where every element in the stream is an instance of SomeConfig.
.flatMap(key -> key.getUsers()
.stream()
.map(user -> new AbstractMap.SimpleEntry(user, key)))
Method flatMap() must return a Stream. The above code returns a Stream where every element is an instance of AbstractMap.SimpleEntry. The "entry" key is the user and the entry value is the key from resultStorage.
Finally I create a Map<String, SomeConfig> via [static] method toMap of class Collectors.
The first argument to method toMap is the key mapper, i.e. a method that extracts the [map] key from the AbstractMap.SimpleEntry. In your case this is the value returned by method getMeta() of the user – which is the key from AbstractMap.SimpleEntry, i.e. e.getKey() returns a user object.
The second argument to toMap is the value mapper. e.getValue() returns a SomeConfig object and the rest is your code, i.e. the withChecks.
There is no way I can test the above code because not only did you not post a minimal, reproducible example, you also did not post any sample data. Hence the above may be way off what you actually require.
Also note that the above code simply creates your Map<String, SomeConfig> temp. I could not understand the code in your question that processes that Map so I did not try to implement that part at all.
How can I write the logic to obtain a Map<String, Map<String, String>> using streams?
Here is the code I have tried :
public Map<String, Map<String, String>> getData(Set<String> Ids) {
List<Events> events = repository.findAllByIdIn(Ids);
int i = 0;
return events.stream()
.sorted(Comparator.comparing(Events::getMajor).thenComparing(Events::getMinor))
.collect(Collectors.groupingBy(Events::getId,
//Error-Cannot resolve method 'getMajor()'
Collectors.mapping(cae -> cae.getMajor() + "." + cae.getMinor(),
cae.getDates().get(i).getEndDate() != null ? "Predicted" : "Not Predicted"),
Collectors.toMap())));//Error-Cannot resolve method 'toMap()'
}
So how do I loop through a map inside another map?
Collector toMap() contrary to collectors toList() and toSet() isn't parameterless, you need to provide information how a key and a value should be extracted from a single stream element.
Collectors.toMap() expects at least two functions: keyMapper and valueMapper. In case if there could be duplicates, you need to use the flavor of toMap which takes a mergeFunction as a third argument to resolve the values that collide on the same key.
So you need to place toMap() as a second argument (downstream collector) into the groupingBy. That will be a syntactically correct structure of the collectors and functions:
Collectors.groupingBy(classifier, Collectors.toMap(keyMapper, valueMapper))
And that how the code might look like:
return events.stream()
.sorted(Comparator.comparing(Events::getMajor)
.thenComparing(Events::getMinor))
.collect(Collectors.groupingBy(Events::getId,
Collectors.toMap(cae -> cae.getMajor() + "." + cae.getMinor(),
cae -> cae.getDates().get(i).getEndDate() != null ?
"Predicted" : "Not Predicted")));
Note, Collectors.mapping() isn't meant to produce a Map but to transform the elements of the stream, you can think of it as an analog of the map() operation that is done not is the middle of the stream pipeline, but during the process of collecting the data.
And if I understood your logic correctly, there's no need to apply mapping() here.
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));
}
The simplest form of question is I have a collection and I want to delete/remove all elements that their value == null. I don't want delete in place (I want to get new Map so my original Map is safe).
public Map<String, String> ADDITIONAL_QUERY = new HashMap<>();
this.ADDITIONAL_QUERY.put("deviceManufacturer", payload.getManufacturer());
this.ADDITIONAL_QUERY.put("deviceModel", payload.getModel());
this.ADDITIONAL_QUERY.put("source", payload.getSource());
this.ADDITIONAL_QUERY.put("adrIMEI", payload.getIMEI());
this.ADDITIONAL_QUERY.put("adrMEID", payload.getMEID());
this.ADDITIONAL_QUERY.put("adrUDID", payload.getUuid() == null ? null : payload.getUuid().toString());
this.ADDITIONAL_QUERY.put("adrID", payload.getAndroidId());
this.ADDITIONAL_QUERY.put("adrSERIAL", payload.getSerial());
I add more elements to ADDITIONAL_QUERY depends of what API I'm calling later.
More details
I just upgraded my Retrofit lib to version 2.0. I have #FieldMap Map<String, String> additionalQuery in majority of my APIs (around 100 APIs). According to their doc regard FieldMap,"Named key/value pairs for a form-encoded request. A null value for the map, as a key, or as a value is not allowed.", as mentioned on java doc (It's so funny that their doc on web says you can https://square.github.io/retrofit/2.x/retrofit/index.html?retrofit2/http/class-use/FieldMap.html).
As APIs are getting called a lot, I'm looking for a way with minor overhead and fast of course.
You can use:
map.values().removeAll(Collections.singleton(null));
This should work:
map.values().removeIf(Objects::isNull);
If you have Guava, you can create a filtered view to avoid copying to a new map:
Map<String, String> filtered = Maps.filterValues(ADDITIONAL_QUERY, new Predicate<String>() {
#Override
public boolean apply(String value) {
return value != null;
}
});
Since you're on Java 8, you can shorten the predicate like this:
Map<String, String> filtered = Maps.filterValues(ADDITIONAL_QUERY, Objects::nonNull);
A possible solution is the following:
public <K, V> Map<K,V> removeNulls(Map<K,V> original) {
return original.entrySet().stream().filter(e -> e.getValue() != null)
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
}
If the map is huge, then you might even use parallelStream() instead of stream()
You can use this :
Map newMap = (HashMap) ADDITIONAL_QUERY.clone();;
newMap.values().remove(null);
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")) { ... }