Java Stream: List of objects to HashMap without duplicates - java

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

Related

Append list according to their size in Java

I am trying to append two list according to their size. With list with bigger size in front.
I have few lists like this.
List<Pair<Double, String>> masterList = new ArrayList<>();
and this is the working Java code that I tried first - with a simple if else loop:
if (listOne.size() >= listTwo.size()){
masterList.addAll(listOne);
masterList.addAll(listTwo);
} else {
masterList.addAll(listTwo);
masterList.addAll(listOne);
}
masterList.addAll(otherList); // and at the end all other list can be added without any condition
I am fairly new to the Java, so I was studying about it and came across Comparators and Lambda. So, I tried to use that for my code, something like this:
List<Pair<Double, String>> masterList = Stream.concat(listOne.stream(), listTwo.stream())
.filter(Comparator.comparingInt(List::size))
.collect(Collectors.toList())
But I am not able to achieve proper results.
Can someone point out my mistake, I am still trying to learn.
The for-loop is very nice, Stream isn't necessary, but to answer the question, you may
not use concat as it'll already join the lists, and you loose the concept of different list
don't use filter but rather sorted
then flatMap to pass from Stream<List<Pair<>>> to Stream<Pair<>>
List<Pair<Double, String>> masterList = Stream.of(listOne, listTwo)
.sorted(Comparator.comparing(List::size, Comparator.reverseOrder()))
.flatMap(List::stream)
.collect(Collectors.toList());
masterList.addAll(otherList);
It may be possible to use Stream.concat to join the contents of otherList and thus to get rid of masterList.addAll
Also, there is an example of using Comparator.reversed() method:
List<Pair<Double, String>> masterList = Stream.concat(
Stream.of(listOne, listTwo) // Stream<List<Pair>>
.sorted(Comparator.<List>comparingInt(List::size).reversed())
.flatMap(List::stream), // Stream<Pair>
otherList.stream() // Stream<Pair>
)
.collect(Collectors.toList());
However, a ternary operator should do fine as well to detect a longer list to place in the beginning:
List<Pair<Double, String>> masterList2 = Stream.concat(
(listOne.size() >= listTwo.size()
? Stream.of(listOne, listTwo)
: Stream.of(listTwo, listOne)
)
.flatMap(List::stream),
otherList.stream()
)
.collect(Collectors.toList());

Java Map - How to retrieve list of custom objects as a single list

I have the following Java Map, where the values are lists of a custom type (EmployeeInfo):
Map<String, List<EmployeeInfo>> myMap;
My goal is to retrieve all the values as one single List from this map. So far I have tried the following, but haven't made it work yet:
// ERROR: The constructor ArrayList<EmployeeInfo>(Collection<List<EmployeeInfo>>) is undefined
List<EmployeeInfo> info = new ArrayList<EmployeeInfo>(myMap.values());
// ERROR: java.lang.ClassCastException: java.util.HashMap$Values cannot be cast to java.util.List
List<EmployeeInfo> info = (List)myMap.values();
Could anyone provide any help? Thanks in advance!
You need to go through every key in myMap, and append every element in the current List to your result List.
List<EmployeeInfo> res = new LinkedList<>(); // Can be any list, not just linkedlist, but linkedlist works best for this.
for(List<EmployeeInfo> l : myMap.values()) {
for(EmployeeInfo e : l) {
res.add(e);
}
}
Note: I made this on the stop in StackOverflow itself, so please fix any small syntax errors.
You need to convert the Map<String, List<EmployeeInfo>> or {[e1,e2], [e3,e4]} to List<EmployeeInfo> or [e1,e2,e3,e4]. You can do this by flat mapping the values to list. Here is a approach using streams:
List<EmployeeInfo> list = myMap.values() // gives you [[e1,e2],[e3,e4]]
.stream() // stream over them
.flatMap(List::stream) // convert to [e1,e2,e3,e4]
.collect(Collectors::toList); // collect back
Like #Nishant Chatterjee KMS said, you need to flatten the values.
Here is a java 8 =< way of doing so:
Map<String, List<String>> foo = new HashMap<>();
List<String> bar = new ArrayList<>();
foo.values().forEach(bar::addAll);

Simplifying loop with Java 8

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.

Collect into Guava's ListMultiMap using Java 8 streams

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

Java 8: Convert a map with string values to a list containg a different type

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

Categories

Resources