Stuck with java8 lambda expression - java

I have Map<Integer,Doctor> docLib=new HashMap<>(); to save class of Doctor.
Class Doctor has methods:getSpecialization() return a String,
getPatients() to return a collection of class Person.
In the main method, I type:
public Map<String,Set<Person>> getPatientsPerSpecialization(){
Map<String,Set<Person>> res=this.docLib.entrySet().stream().
map(d->d.getValue()).
collect(groupingBy(d->d.getSpecialization(),
d.getPatients()) //error
);
return res;
}
As you can see, I have problem with groupingBy,I try to send the same value d to the method, but it's wrong.
How to solve this?

You need a second Collector for that mapping :
public Map<String,Set<Person>> getPatientsPerSpecialization(){
return this.docLib
.values()
.stream()
.collect(Colectors.groupingBy(Doctor::getSpecialization,
Collectors.mapping(Doctor::getPatients,toSet()))
);
}
EDIT:
I think my original answer may be wrong (it's hard to say without being able to test it). Since Doctor::getPatients returns a Collection, I think my code may return a Map<String,Set<Collection<Person>>> instead of the desired Map<String,Set<Person>>.
The easiest way to overcome that is to iterate over that Map again to produce the desired Map :
public Map<String,Set<Person>> getPatientsPerSpecialization(){
return this.docLib
.values()
.stream()
.collect(Colectors.groupingBy(Doctor::getSpecialization,
Collectors.mapping(Doctor::getPatients,toSet()))
)
.entrySet()
.stream()
.collect (Collectors.toMap (e -> e.getKey(),
e -> e.getValue().stream().flatMap(c -> c.stream()).collect(Collectors.toSet()))
);
}
Perhaps there's a way to get the same result with a single Stream pipeline, but I can't see it right now.

Instead of groupingBy, you could use toMap:
public Map<String, Set<Person>> getPatientsPerSpecialization() {
return docLib.values()
.stream()
.collect(toMap(Doctor::getSpecialization,
d -> new HashSet<>(d.getPatients()),
(p1, p2) -> Stream.concat(p1.stream(), p2.stream()).collect(toSet())));
}
What it does is that it groups the doctors per specialization and map each one to a set of the patients it has (so a Map<String, Set<Person>>).
If, when collecting the data from the pipeline, you encounter a doctor with a specialization that is already stored as a key in the map, you use the merge function to produce a new set of values with both sets (the set that is already stored as a value for the key, and the set that you want to associate with the key).

Related

Filter Key from a hashmap Map<String, List<String>> when a string matches one of the elements in the list

I'm fairly new to java in general so help is very appreciated.
I have a structure like this:
private Map<String, List<String>> campaign = new Hashmap<>();
This hashmap has values like this:
campaign = {
"John": {["1234"]},
"Doe": {["5555","2222"]},
"Smith": {["Smith"]}
}
I'm trying to filter the key of this hashMap when one of the elements matches an element of the list.
I've tried this so far based on similar solutions I found:
public String getKey(String id) {
campaign.entrySet().stream()
.filter(map -> map.getValue().stream().
anymatch(list -> list.contains(id)))
.collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue() ))
}
// I see toMap is not what I need but don't know what to use
I expecto to get: getKey(1234) = "John"
You don't need to collect to a map, but to the key(s) you found.
.filter(entry -> entry.getValue().contains(id))
.map(Entry::getKey)
.collect(toSet());
You might find more than one key, hence the Set.
You may want to use findAny():
public String getKey(String id) {
return campaign
.entrySet()
.stream()
.filter(map -> map
.getValue()
.stream()
.anyMatch(list -> list.contains(id))
)
.map(Entry::getKey)
.findAny()
.orElse(null);
}
anyMatch() returns an Optional that contains any value of the (filtered) Stream (if it is present).
If you want to get the first element in the Stream, you could use findFirst instead but order is not relevant in most Maps anyways.
With .map, you can map the entry to its key.
In case you want to return all keys, you could use methods like toList or .toSet.
With orElse(null), you specify that null should be used if no matching entry/key was found.
As an alternatives to orElse, you could return the Optional or use orElseThrow if the value needs to be present.

Java 8 groupingBy to obtain LinkedHashMap and mapping the values of the map to a different object

I have this method which returns a Map:
public Map<String, List<ResourceManagementDTO>> getAccountsByGroupNameMap(final List<AccountManagement> accountManagementList) {
return new LinkedHashMap<>(accountManagementList.stream().collect(Collectors.groupingBy(acc -> acc.getGroup().getName(),
Collectors.mapping(ResourceManagementDTOMapper::toResourceManagementDTO, Collectors.toList()))));
}
I need my map to be a LinkedHaspMap, but the above code doesn't seem to work, because the order of the keys isn't preserved. I managed to find another approach which would return a LinkedHashMap, however with that syntax I wasn't able to do the mapping operation anymore (to map AccountManagement to ResourceManagementDTO). Here's the code:
public Map<String, List<AccountManagement>> getAccountsByGroupNameMap(final List<AccountManagement> accountManagementList) {
return accountManagementList.stream()
.collect(groupingBy(acc -> acc.getGroup().getName(), LinkedHashMap::new, Collectors.toList()));
}
Is there a way to get the LinkedHashMap and also perform the mapping operation in a single Java 8 pipeline? I really couldn't figure an syntax that combines both operations.
Try the following: groupingBy takes a supplier for the map type.
public Map<String, List<ResourceManagementDTO>>
getAccountsByGroupNameMap(
final List<AccountManagement> accountManagementList) {
return accountManagementList.stream()
.collect(Collectors.groupingBy(
acc -> acc.getGroup().getName(),
LinkedHashMap::new,
Collectors.mapping(
ResourceManagementDTOMapper::toResourceManagementDTO,
Collectors.toList())));

Flattening a list of lists within a map

I have a stream of orders (the source being a list of orders).
Each order has a Customer, and a list of OrderLine.
What I'm trying to achieve is to have a map with the customer as the key, and all order lines belonging to that customer, in a simple list, as value.
What I managed right now returns me a Map<Customer>, List<Set<OrderLine>>>, by doing the following:
orders
.collect(
Collectors.groupingBy(
Order::getCustomer,
Collectors.mapping(Order::getOrderLines, Collectors.toList())
)
);
I'm either looking to get a Map<Customer, List<OrderLine>>directly from the orders stream, or by somehow flattening the list from a stream of the Map<Customer>, List<Set<OrderLine>>> that I got above.
You can simply use Collectors.toMap.
Something like
orders
.stream()
.collect(Collectors
.toMap(Order::getCustomer
, Order::getOrderLines
, (v1, v2) -> { List<OrderLine> temp = new ArrayList<>(v1);
temp.addAll(v2);
return temp;});
The third argument to the toMap function is the merge function. If you don't explicitly provide that and it there is a duplicate key then it will throw the error while finishing the operation.
Another option would be to use a simple forEach call:
Map<Customer, List<OrderLine>> map = new HashMap<>();
orders.forEach(
o -> map.computeIfAbsent(
o.getCustomer(),
c -> new ArrayList<OrderLine>()
).addAll(o.getOrderLines())
);
You can then continue to use streams on the result with map.entrySet().stream().
For a groupingBy approach, try Flat-Mapping Collector for property of a Class using groupingBy

Group, Collectors, Map (Int to String), Map (Map to Object)

This is a continuation of my previous question at Group, Sum byType then get diff using Java streams.
As suggested, I should post as a separate thread instead of updating the original one.
So with my previous set of question, I have achieved that, and now, with the continuation.
Background:
I have the following dataset
Sample(SampleId=1, SampleTypeId=1, SampleQuantity=5, SampleType=ADD),
Sample(SampleId=2, SampleTypeId=1, SampleQuantity=15, SampleType=ADD),
Sample(SampleId=3, SampleTypeId=1, SampleQuantity=25, SampleType=ADD),
Sample(SampleId=4, SampleTypeId=1, SampleQuantity=5, SampleType=SUBTRACT),
Sample(SampleId=5, SampleTypeId=1, SampleQuantity=25, SampleType=SUBTRACT)
Sample(SampleId=6, SampleTypeId=2, SampleQuantity=10, SampleType=ADD),
Sample(SampleId=7, SampleTypeId=2, SampleQuantity=20, SampleType=ADD),
Sample(SampleId=8, SampleTypeId=2, SampleQuantity=30, SampleType=ADD),
Sample(SampleId=9, SampleTypeId=2, SampleQuantity=15, SampleType=SUBTRACT),
Sample(SampleId=10, SampleTypeId=2, SampleQuantity=35, SampleType=SUBTRACT)
I am currently using this:
sampleList.stream()
.collect(Collectors.groupingBy(Sample::getTypeId,
Collectors.summingInt(
sample -> SampleType.ADD.equalsIgnoreCase(sample.getSampleType())
? sample.getSampleQuantity() :
-sample.getSampleQuantity()
)));
And also this
sampleList.stream()
.collect(Collectors.groupingBy(Sample::getSampleTypeId,
Collectors.collectingAndThen(
Collectors.groupingBy(Sample::getSampleType,
Collectors.summingInt(Sample::getSampleQuantity)),
map -> map.getOrDefault(SampleType.ADD, 0)
- map.getOrDefault(SampleType.SUBTRACT, 0))));
as the accepted answer to get the desired output to group in a Map<Long, Integer>:
{1=15, 2=10}
With that, I was wondering, if this could be expanded into something more.
First, how could I have it return as a Map<String, Integer> instead of the original Map<Long, Integer>. Basically, for the SampleTypeId; 1 refers to HELLO, 2 refers to WORLD.
So I would need like a .map (or maybe other function) to transform the data from 1 to HELLO and 2 to WORLD by calling a function say convertType(sampleTypeId)?. So the expected output would then be {"HELLO"=15, "WORLD"=10}. Is that right? How should I edit the current suggested solution to this?
Lastly, I would like to know if it is also possible to return it to a Object instead of a Map. So let's say I have a Object; SummaryResult with (String) name and (int) result. So it returns a List<SummaryResult> instead of the original Map<Long, Integer>. How can I use the .map (or other) feature to do this? Or is there other way to doing so? The expected output would be something along this line.
SummaryResult(name="hello", result=15),
SummaryResult(name="world", result=10),
Would really appreciate it with the explanation in steps as given previously by #M. Prokhorov.
Update:
After updating to
sampleList.stream()
.collect(Collectors.groupingBy(sample -> convertType(sample.getSampleTypeId()),
Collectors.collectingAndThen(
Collectors.groupingBy(Sample::getSampleType,
Collectors.summingInt(Sample::getSampleQuantity)),
map -> map.getOrDefault(SampleType.ADD, 0)
- map.getOrDefault(SampleType.SUBTRACT, 0))));
private String convertType(int id) {
return (id == 1) ? "HELLO" : "WORLD";
}
For first part, considering you have somewhere the method
String convertType(int typeId)
You simply need to change first classifier from this
groupingBy(SampleType::getTypeId)
to this
groupingBy(sample -> convertType(sample.getTypeId()))
Everything else remains the same.
Latter type is a little trickier, and technically doesn't benefit from it being a stream-related solution at all.
What you need is this:
public List<SummaryResult> toSummaryResultList(Map<String, Integer> resultMap) {
List<SummaryResult> list = new ArrayList<>(resultMap.size());
for (Map.Entry<String, Integer> entry : resultMap.entrySet()) {
String name = entry.getKey();
Integer value = entry.getValue();
// replace below with construction method you actually have
list.add(SummaryResult.withName(name).andResult(value));
}
return list;
}
You can use this as part of collector composition, where your whole collector will get wrapped into a collectingAndThen call:
collectingAndThen(
groupingBy(sample -> convertType(sample.getTypeId()),
collectingAndThen(
groupingBy(Sample::getSampleType,
summingInt(Sample::getSampleQuantity)),
map -> map.getOrDefault(SampleType.ADD, 0)
- map.getOrDefault(SampleType.SUBTRACT, 0))),
result -> toSummaryResultList(result))
However, as you can see, it is the whole collector that gets wrapped, so there is no real benefit in my eyes to the above version to a simpler and easier to follow (at least to me) version below that uses an intermediate variable, but isn't so much of a wall of code:
// do the whole collecting thing like before
Map<String, Integer> map = sampleList.stream()
.collect(Collectors.groupingBy(sample -> convertType(sample.getTypeId()),
Collectors.collectingAndThen(
Collectors.groupingBy(Sample::getSampleType,
Collectors.summingInt(Sample::getSampleQuantity)),
map -> map.getOrDefault(SampleType.ADD, 0)
- map.getOrDefault(SampleType.SUBTRACT, 0))));
// return the "beautified" result
return toSummaryResultList(map);
Another point to consider in above is: convertType method will be called as many times as there are elements in sampleList, so if convertType call is "heavy" (for example, uses database or IO), then it's better to call it as part of toSummaryResultList conversion, not as stream element classifier. In which case you will be collecting from map of type Map<Integer, Integer> still, and using convertType inside the loop. I will not add any code with this in consideration, as I view this change as trivial.
You could indeed use a map() function
sampleList.stream()
.collect(Collectors.groupingBy(Sample::getSampleTypeId,
Collectors.collectingAndThen(
Collectors.groupingBy(Sample::getSampleType,
Collectors.summingInt(Sample::getSampleQuantity)),
map -> map.getOrDefault(SampleType.ADD, 0)
- map.getOrDefault(SampleType.SUBTRACT, 0))))
.entrySet()
.stream()
.map(entry->new SummaryResult(entry.getKey()),entry.getValue())
.collect(Collectors.toList());
ToIntFunction<Sample> signedQuantityMapper= sample -> sample.getQuantity()
* (sample.getType() == Type.ADD ? 1 : -1);
Function<Sample, String> keyMapper = s -> Integer.toString(s.getTypeId());
Map<String, Integer> result = sampleList.stream().collect(
Collectors.groupingBy(
keyMapper,
Collectors.summingInt(signedQuantityMapper)));

Java Stream.collect using groupingBy

I have a small snippet of code where I want to group the results by a combination of 2 properties of the type in the stream. After appropriate filtering, I do a map where I create an instance of a simple type that holds those 2 properties (in this case called AirportDay). Now I want to group them together and order them descending by the count. The trouble I am having is coming up with the correct arguments for the groupingBy method. Here is my code so far:
final int year = getYear();
final int limit = getLimit(10, 1, 100);
repository.getFlightStream(year)
.filter(f -> f.notCancelled())
.map(f -> new AirportDay(f.getOriginAirport(), f.getDate()))
.collect(groupingBy( ????? , counting())) // stuck here
.entrySet()
.stream()
.sorted(comparingByValue(reverseOrder()))
.limit(limit)
.forEach(entry -> {
AirportDay key = entry.getKey();
printf("%-30s\t%s\t%,10d\n",
key.getAirport().getName(),
key.getDate(),
entry.getValue()
);
});
My first instinct was to pass AirportDay::this but that obviously doesn't work...
I'd appreciate any assistance you can provide in coming up with a solution to the above problem.
-Tony
If you want to group by AirportDay, provide the function to create the key to groupingBy:
repository.getFlightStream(year)
.filter(f -> f.notCancelled())
.collect(groupingBy(f -> new AirportDay(f.getOriginAirport(), f.getDate()), counting()))
Note: The AirportDay class must implement sensible equals() and hashCode() methods for this to work.

Categories