I am trying to collect into a ListMultiMap using java 8 without using the forEach operation.
If I were to write the code in Java 7, it will be something like this:
ListMultimap<String, String> result = ArrayListMultimap.create();
for(State state: states) {
for(City city: state.getCities()) {
result.put(state.getName(), city.getName());
}
}
I found online a website that talks about creating your own collectors to use in scenarios such as this one.
I used this implementation for the collector. I then wrote the following code:
ListMultimap<String, String> result = states
.stream()
.flatMap(state -> state.getCities().stream()
.map(city -> {
return new Pair(state, city);
}))
.map(pair -> {
return new Pair(pair.first().getName(), pair.second().getName()));
})
.collect(MultiMapCollectors.listMultimap(
Pair::first,
Pair::second
)
);
But at the collect level, I can only pass just one parameter, and I can seem to find a way to pass two parameters.
Following the example from the website, I understood that to use both, I need to store a "pair" in the multimap such as the following:
ArrayListMultimap<String, Pair> testMap = testObjectList.stream().collect(MultiMapCollectors.listMultimap((Pair p) -> p.first().getName()));
However this is not what I'm looking for, I want to collect into ListMultimap using the state's name and the city's name using java 8's collector (and no forEach).
Can someone help me with that ?
Thank you!
ImmutableListMultimap.flatteningToImmutableListMultimap
return states.stream()
.collect(flatteningToImmutableListMultimap(
State::getName,
state -> state.getCities().stream().map(City::getName)));
You could create a custom collector for that (note that Louis Wasserman's answer will do a forEachOrdered internally, you can't really escape a forEach either internally or externally).
ListMultimap<String, String> list = states.collect(Collector.of(ArrayListMultimap::create, (multimap, s) -> {
multimap.putAll(s.getName(), s.getCities().stream().map(City::getName).collect(Collectors.toList()));
}, (multi1, multi2) -> {
multi1.putAll(multi2);
return multi1;
}));
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.
I'm trying to convert a List to Map without duplicates using a stream but I can't achieve it.
I can do it using a simple loop like this:
List<PropertyOwnerCommunityAddress> propertyOwnerCommunityAddresses = getPropertyOwnerAsList();
Map<Community, List<Address>> hashMap = new LinkedHashMap<>();
for (PropertyOwnerCommunityAddress poco : propertyOwnerCommunityAddresses) {
if (!hashMap.containsKey(poco.getCommunity())) {
List<Address> list = new ArrayList<>();
list.add(poco.getAddress());
hashMap.put(poco.getCommunity(), list);
} else {
hashMap.get(poco.getCommunity()).add(poco.getAddress());
}
}
but when I try to use a stream, my mind crash.
I have to say the PropertyOwnerCommunityAddress contains two object more: Community and Address and the goal of all of this is for each community save the addresses in a key:value pair without duplicate the Community object.
Anyone can help me? Thank you!
As you can have multiple Addresses per Community you can't use the toMap() collector, but you need to use groupingBy():
Map<Community, List<Address>> map = propertyOwnerCommunityAddresses.stream()
.collect(Collectors.groupingBy(
PropertyOwnerCommunityAddress::getCommunity,
Collectors.mapping(
PropertyOwnerCommunityAddress::getAddress,
Collectors.toList())
)
);
Depending on your personal preference, this can look messy and maybe more complicated than the simple for-loop, which can also be optimized:
for(PropertyOwnerCommunityAddress poco : propertyOwnerCommunityAddresses) {
hashMap.computeIfAbsent(poco.getCommunity(), c -> new ArrayList<>()).add(poco.getAddress());
}
Depending if you only want to have unique addresses you may want to use Set, so change the Collectors.toList() to Collectors.toSet() or when you stay with your for-loop change the definition of hashMap to Map<Community, Set<Address>> and in the loop exchange new ArrayList<>() with new HashSet<>()
You have to use
groupingBy to get the Community as key
mapping to get the Address as list
Map<Community, List<Address>> hashMap = propertyOwnerCommunityAddresses.stream()
.collect(Collectors.groupingBy(PropertyOwnerCommunityAddress::getCommunity,
Collectors.mapping(PropertyOwnerCommunityAddress::getAddress, Collectors.toList())));
I have a method that adds maps to a cache and I was wondering what I could do more to simplify this loop with Java 8.
What I have done so far:
Standard looping we all know:
for(int i = 0; i < catalogNames.size(); i++){
List<GenericCatalog> list = DummyData.getCatalog(catalogNames.get(i));
Map<String, GenericCatalog> map = new LinkedHashMap<>();
for(GenericCatalog item : list){
map.put(item.name.get(), item);
}
catalogCache.put(catalogNames.get(i), map);};
Second iteration using forEach:
catalogNames.forEach(e -> {
Map<String, GenericCatalog> map = new LinkedHashMap<>();
DummyData.getCatalog(e).forEach(d -> {
map.put(d.name.get(), d);
});
catalogCache.put(e, map);});
And third iteration that removes unnecessary bracers:
catalogNames.forEach(objName -> {
Map<String, GenericCatalog> map = new LinkedHashMap<>();
DummyData.getCatalog(objName).forEach(obj -> map.put(obj.name.get(), obj));
catalogCache.put(objName, map);});
My question now is what can be further done to simplify this?
I do understand that it's not really necessary to do anything else with this method at this point, but, I was curios about the possibilities.
There is small issue with solution 2 and 3 they might cause a side effects
Side-effects in behavioral parameters to stream operations are, in
general, discouraged, as they can often lead to unwitting violations
of the statelessness requirement, as well as other thread-safety
hazards.
As an example of how to transform a stream pipeline that
inappropriately uses side-effects to one that does not, the following
code searches a stream of strings for those matching a given regular
expression, and puts the matches in a list.
ArrayList<String> results = new ArrayList<>();
stream.filter(s -> pattern.matcher(s).matches())
.forEach(s -> results.add(s)); // Unnecessary use of side-effects!
So instead of using forEach to populate the HashMap it is better to use Collectors.toMap(..). I am not 100% sure about your data structure, but I hope it is close enough.
There is a List and corresponding Map:
List<Integer> ints = Arrays.asList(1,2,3);
Map<Integer,List<Double>> catalog = new HashMap<>();
catalog.put(1,Arrays.asList(1.1,2.2,3.3,4.4));
catalog.put(2,Arrays.asList(1.1,2.2,3.3));
catalog.put(3,Arrays.asList(1.1,2.2));
now we would like to get a new Map where a map key is element from the original List and map value is an other Map itself. The nested Map's key is transformed element from catalog List and value is the List element itself. Crazy description and more crazy code below:
Map<Integer, Map<Integer, Double>> result = ints.stream().collect(
Collectors.toMap(
el -> el,
el -> catalog.get(el).stream().
collect(Collectors.toMap(
c -> c.intValue(),
c -> c
))
)
);
System.out.println(result);
// {1={1=1.1, 2=2.2, 3=3.3, 4=4.4}, 2={1=1.1, 2=2.2, 3=3.3}, 3={1=1.1, 2=2.2}}
I hope this helps.
How about utilizing Collectors from the stream API? Specifically, Collectors#toMap
Map<String, Map<String, GenericCatalog>> cache = catalogNames.stream().collect(Collectors.toMap(Function.identity(),
name -> DummyData.getCatalog(name).stream().collect(Collectors.toMap(t -> t.name.get(), Function.identity(),
//these two lines only needed if HashMap can't be used
(o, t) -> /* merge function */,
LinkedHashMap::new));
This avoids mutating an existing collection, and provides you your own individual copy of a map (which you can use to update a cache, or whatever you desire).
Also I would disagree with arbitrarily putting end braces at the end of a line of code - most style guides would also be against this as it somewhat disturbs the flow of the code to most readers.
I have this Map:
Map<Integer, Set<String>> map = ...
And I have this class Foo:
class Foo {
int id;
String name;
}
I want to convert the map to List<Foo>. Is there a convenient manner in Java 8 to do this?
Currently, my way is:
List<Foo> list = new ArrayList<>((int) map.values().flatMap(e->e.stream()).count()));
for(Integer id : map.keySet()){
for(String name : map.get(id)){
Foo foo = new Foo(id,name);
list.add(foo);
}
}
I feel it's too cumbersome.
You can have the following:
List<Foo> list = map.entrySet()
.stream()
.flatMap(e -> e.getValue().stream().map(name -> new Foo(e.getKey(), name)))
.collect(toList());
For each entry of the map, we create a Stream of the value and map it to the corresponding Foo and then flatten it using flatMap.
The main reason for your version being cumbersome is that you have decided to calculate the capacity of the ArrayList first. Since this calculation requires iterating over the entire map, there is unlikely to be any benefit in this. You definitely should not do such a thing unless you have proved using a proper benchmark that it is needed.
I can't see anything wrong with just using your original version but with the parameterless constructor for ArrayList instead. If you also get rid of the redundant local variable foo, it's actually fewer characters (and clearer to read) than the stream version.
final Map<Integer, Set<String>> map = new HashMap<>();
map
.entrySet()
.stream()
.flatMap(e -> e.getValue().stream().map(s -> new Foo(e.getKey(), s)))
.collect(Collectors.toList());
I have a input which is of type: List<List<String>>.
An example input:
[A, A1Name]
[B, B1Name]
I want to convert it to BiMap
A -> A1Name
B -> B1Name
What is the best way to achieve this:
Currently I am doing:
final BiMap<String, String> myMap = HashBiMap.create();
lines.forEach(
(tokens) -> {
myMap.put(tokens.get(0), tokens.get(1));
}
);
Since BiMap implements Map, you can use the toMap collector. To replicate the behavior of your loop (duplicate keys silently override values, duplicate values throw exception), you can do the following:
BiMap<String,String> m = lines.stream().collect(toMap(
x->x.get(0), x->x.get(1), (a,b)->b, HashBiMap::create
));
As an aside, when you are not sure how to convert your code to streams, 3-argument collect provides a way to convert the iterative code almost verbatim:
BiMap<String,String> m = lines.stream().collect(
HashBiMap::create,
(bm,t) -> bm.put(t.get(0), t.get(1)),
BiMap::putAll
);