How to groupBy and Collect with Java 8? - java

I have a list of elements, let's call it "keywords", like this:
public class Keyword {
Long id;
String name;
String owner;
Date createdTime;
Double price;
Date metricDay;
Long position;
}
The thing is that there is a keyword for every single day. For example:
Keyword{id=1, name="kw1", owner="Josh", createdTime="12/12/1992", price="0.1", metricDay="11/11/1999", position=109}
Keyword{id=1, name="kw1", owner="Josh", createdTime="12/12/1992", price="0.3", metricDay="12/11/1999", position=108}
Keyword{id=1, name="kw1", owner="Josh", createdTime="12/12/1992", price="0.2", metricDay="13/11/1999", position=99}
Keyword{id=2, name="kw2", owner="Josh", createdTime="13/12/1992", price="0.6", metricDay="13/11/1999", position=5}
Keyword{id=2, name="kw2", owner="Josh", createdTime="13/12/1992", price="0.1", metricDay="14/11/1999", position=4}
Keyword{id=3, name="kw3", owner="Josh", createdTime="13/12/1992", price="0.1", metricDay="13/11/1999", position=8}
Then, from this list I would like to create a new list with all the metrics from all those different days on one single list. First, I created a class like this:
public class KeywordMetric {
Double price;
Date metricDay;
Long position;
}
And what I would like to archive is go from the first list, to a structure like this:
public class KeywordMeged {
Long id;
String name;
String owner;
List<KeywordMetric> metricList;
}
Example of what I expect:
KeywordMerged{id=1, name="kw1", owner="Josh", createdTime="12/12/1992", metricList=[KeywordMetric{price=0.1,metricDay="11/11/1999",position=109},KeywordMetric{price=0.3,metricDay="12/11/1999",position=108},KeywordMetric{price=0.2,metricDay="13/11/1999",position=99}]
KeywordMerged{id=2, name="kw2", owner="Josh", createdTime="13/12/1992", metricList=[KeywordMetric{price=0.6,metricDay="13/11/1999",position=5},KeywordMetric{price=0.1,metricDay="14/11/1999",position=4}]
KeywordMerged{id=3, name="kw3", owner="Josh", createdTime="13/12/1992", metricList=[KeywordMetric{price=0.1,metricDay="13/11/1999",position=8}]
I know how to do this with a lot of loops and mutable varibles, but I can't figure out how to do this with streams and lambda operations. I was able to group all related keywords by Id with this:
Map<Long, List<Keyword>> kwL = kwList.stream()
.collect(groupingBy(Keyword::getId))
And I know that with .forEach() I could iterate over that Map, but can't figure out how to make the collect() method of streams pass from List to KeywordMerged.

You can try to use the Collectors.toMap(...) instead. Where:
Keyword::getId is a key mapper function.
KeywordMerged.from(...) performs a transformation: Keyword => KeywordMerged
(left, right) -> { .. } combines metrics for entities with identical ids.
Collection<KeywordMerged> result = keywords.stream()
.collect(Collectors.toMap(
Keyword::getId,
k -> KeywordMerged.from(k), // you can replace this lambda with a method reference
(left, right) -> {
left.getMetricList().addAll(right.getMetricList());
return left;
}))
.values();
A transformation method might look something like this:
public class KeywordMerged {
public static KeywordMerged from(Keyword k) {
KeywordMetric metric = new KeywordMetric();
metric.setPrice(k.getPrice());
metric.setMetricDay(k.getMetricDay());
metric.setPosition(k.getPosition());
KeywordMerged merged = new KeywordMerged();
merged.setId(k.getId());
merged.setName(k.getName());
merged.setOwner(k.getOwner());
merged.setMetricList(new ArrayList<>(Arrays.asList(metric)));
return merged;
}
}
I think you've got the basic idea. So, refactor according to your needs...

A slightly different approach. First you collect the Map of keywords grouped by id:
Map<Integer, List<Keyword>> groupedData = keywords.stream()
.collect(Collectors.groupingBy(k -> k.getId()));
Further you convert your map to the list of desired format:
List<KeywordMerged> finalData = groupedData.entrySet().stream()
.map(k -> new KeywordMerged(k.getValue().get(0).getId(),
k.getValue().stream()
.map(v -> new KeywordMetric(v.getMetricDay(), v.getPrice(), getPosition()))
.collect(Collectors.toList())))
.collect(Collectors.toList());
This will work on the grouped data, but transforming the map it will create KeywordMerged object, which as argument will receive id (you can extent it further yourself) and converted to List<KeywordMetric> previously grouped by ID data.
EDIT: I believe with some extraction to methods you can make it look much nicer :)

Related

Generate Map using list of list using stream in Java 8

I have the following domain classes Trip and Employee:
#Data
#NoArgsConstructor
#AllArgsConstructor
public class Trip {
private Date startTime;
private Date endTime;
List<Employee> empList;
}
#Data
#NoArgsConstructor
#AllArgsConstructor
public class Employee {
private String name;
private String empId;
}
I have a list of Trip instances. And I want to create a map of type Map<String,List<Trip>> associating id of each employee empId with a list of trips using Stream API.
Here's my attempt:
public static void main(String[] args) {
List<Trip> trips = new ArrayList<>();
Map<Stream<String>, List<Trip>> x = trips.stream()
.collect(Collectors.groupingBy(t -> t.getEmpList()
.stream().map(Employee::getEmpId)
));
}
How can I generate the map of the required type?
When the type of map is Map<String,List<Trip>> it gives me a compilation error:
Unresolved compilation problem: Type mismatch:
cannot convert from Map<Object,List<Trip>> to Map<String,List<Trip>>
To group the data by the property of a nested object and at the same time preserve a link to the enclosing object, you need to flatten the stream using an auxiliary object that would hold references to both employee id and enclosing Trip instance.
A Java 16 record would fit into this role perfectly well. If you're using an earlier JDK version, you can implement it a plain class (a quick and dirty way would be to use Map.Entry, but it decreases the readability, because of the faceless methods getKey() and getValue() require more effort to reason about the code). I will go with a record, because this option is the most convenient.
The following line is all we need (the rest would be automatically generated by the compiler):
public record TripEmployee(String empId, Trip trip) {}
The first step is to flatten the stream data and turn the Stream<Trip> into Stream<TripEmployee>. Since it's one-to-many transformation, we need to use flatMap() operation to turn each Employee instance into a TripEmployee.
And then we need to apply collect. In order to generate the resulting Map, we can make use of the collector groupingBy() with collector mapping() as a downstream. In collector mapping always requires a downstream collector and this case we need to provide toList().
List<Trip> trips = // initializing the list
Map<String, List<Trip>> empMap = trips.stream()
.flatMap(trip -> trip.getEmpList().stream()
.map(emp -> new TripEmployee(emp.getEmpId(), trip))
)
.collect(Collectors.groupingBy(
TripEmployee::empId,
Collectors.mapping(TripEmployee::trip,
Collectors.toList())
));
A Java 8 compliant solution is available via this Link
Not sure which Java version you are using but since you have mentioned Stream, I will assume Java 8 at least.
Second assumption, not sure why but looking at your code (using groupingBy ) you want the whole List<Trip> which you get against an empId in a Map.
To have the better understanding first look at this code (without Stream):
public Map<String, List<Trip>> doSomething(List<Trip> listTrip) {
List<Employee> employeeList = new ArrayList<>();
for (Trip trip : listTrip) {
employeeList.addAll(trip.getEmployee());
}
Map<String, List<Trip>> stringListMap = new HashMap<>();
for (Employee employee : employeeList) {
stringListMap.put(employee.getEmpId(), listTrip);
}
return stringListMap;
}
You can see I pulled an employeeList first , reason being your use case. And now you can see how easy was to create a map out of it.
You may use Set instead of List if you're worried about the duplicates.
So with StreamApi above code could be:
public Map<String, List<Trip>> doSomethingInStream(List<Trip> listTrip) {
List<Employee> employeeList = listTrip.stream().flatMap(e -> e.getEmployee().stream()).collect(Collectors.toList());
return employeeList.stream().collect(Collectors.toMap(Employee::getEmpId, employee -> listTrip));
}
You can take care of duplicates while creating map as well, as:
public Map<String, List<Trip>> doSomething3(List<Trip> listTrip) {
List<Employee> employeeList = listTrip.stream().flatMap(e -> e.getEmployee().stream()).collect(Collectors.toList());
return employeeList.stream().collect(Collectors.toMap(Employee::getEmpId, employee -> listTrip, (oldValue, newValue) -> newValue));
}
Like the first answer says, if you are Java 16+ using record will ease your task a lot in terms of model definition.
Using Java 8 stream
You can use the below approach to get the desired results using stream function groupingBy.
Since you have mentioned above to use java 8, so my solution is inclined to java 8 itself.
Logic:
Here,
First I have created an additional list of EmployeeTripMapping object
with Trip data corresponding to the empId by iterating the
listOfTrips.
I have used Collectors.groupingBy on the List<EmployeeTripMapping>
and grouped the data based on the empId and using Collectors.mapping
collect the list of Trip corresponding to the empId.
Few Suggestions:
Records in java 14 : As I can see in your problem statement, you
are using lombok
annotations to create getters, setters and constructors, so instead of
that we can replace our data classes
with records. Records are immutable classes that require only the type
and name of fields. We do not need to create constructor, getters,
setters, override toString() methods, override hashcode and equals
methods. Here
JavaTimeAPI in java 8: Instead of Date, you can use LocalDateTime available in java time API in java 8. Here
Code:
public class Test {
public static void main(String[] args) {
Trip t1 = new Trip(LocalDateTime.of(2022,10,28,9,00,00),
LocalDateTime.of(2022,10,28,18,00,00),
Arrays.asList(new Employee("emp1","id1")));
Trip t2 = new Trip(LocalDateTime.of(2021,10,28,9,00,00),
LocalDateTime.of(2021,10,28,18,00,00),
Arrays.asList(new Employee("emp1","id1")));
Trip t3 = new Trip(LocalDateTime.of(2020,10,28,9,00,00),
LocalDateTime.of(2020,10,28,18,00,00),
Arrays.asList(new Employee("emp2","id2")));
Trip t4 = new Trip(LocalDateTime.of(2019,10,28,9,00,00),
LocalDateTime.of(2019,10,28,18,00,00),
Arrays.asList(new Employee("emp2","id2")));
List<Trip> listOfTrips = Arrays.asList(t1,t2,t3,t4);
List<EmployeeTripMapping> empWithTripMapping = new ArrayList<>();
listOfTrips.forEach(x -> x.getEmpList().forEach(y ->
empWithTripMapping.add(new EmployeeTripMapping(y.getEmpId(),x))));
Map<String,List<Trip>> employeeTripGrouping = empWithTripMapping.stream()
.collect(Collectors.groupingBy(EmployeeTripMapping::getEmpId,
Collectors.mapping(EmployeeTripMapping::getTrip,
Collectors.toList())));
System.out.println(employeeTripGrouping);
}
}
EmployeeTripMapping.java
public class EmployeeTripMapping {
private String empId;
private Trip trip;
//getters and setters
}
Output:
{emp2=[Trip{startTime=2020-10-28T09:00, endTime=2020-10-28T18:00, empList=[Employee{empId='emp2', name='id2'}]}, Trip{startTime=2019-10-28T09:00, endTime=2019-10-28T18:00, empList=[Employee{empId='emp2', name='id2'}]}],
emp1=[Trip{startTime=2022-10-28T09:00, endTime=2022-10-28T18:00, empList=[Employee{empId='emp1', name='id1'}]}, Trip{startTime=2021-10-28T09:00, endTime=2021-10-28T18:00, empList=[Employee{empId='emp1', name='id1'}]}]}

Filter an object by specific properties java

So I have this class:
public class Seat {
private Long id;
private float positionX;
private float positionY;
private int numOfSeats;
private String label;
//getters and setters
}
I have List of Seat class on:
List<Seat> seatList = // get data from repository;
I also have this arraylist contains list of ids:
List<Long> idList; // for example : [1, 2, 3]
I want to filter seatList so that the filtered ArrayList does not contain a Seat object with id from idList, so I tried to use stream:
List<Seat> filteredSeat = seatList.stream()
.filter(seat -> {
// function to filter seat.getId() so it would return the Seat object with id that does not equals to ids from idList
})
.collect(Collectors.toList());
I cant find the correct function to do it. Does anyone have suggestion for me to try?
You want to use the overriden method from Collection#contains(Object) with the negation implying the id was not found in the List.
Set<Seat> filteredSeat = seatList.stream()
.filter(seat -> !idList.contains(seat.getId()))
.collect(Collectors.toSet());
Few notes:
You want to use Set<Long> instead of List<Long> for an efficient look-up. Moreover, it doesn't make sense to have duplicate values among ids, so Set is a good choice.
Collectors.toSet() results Set, so the Stream's return type is Set<Seat>.
The most simple solution for be a for-each loop in which you check each idList against the Seat's id.
perhaps something like
List<Seat> FilteredList;
for ( Seat CurSeat : seatList ){
for(int i = 0; i < idList.size(); i++){
and if the ID of CurSeat is part of idList, it doesn't get added to the new List.
This is definitely not the simplest way, but if you're looking for something easy, this is probably it.
Hope this helped!
Assuming you implement the equals method accordingly (like the doc mentions it), there is a much shorter solution:
seatList.stream()
.distinct()
.collect( Collectors.toList() );

how to use java stream to group fields and create a summary

public class Call {
private String status;
private String callName;
}
I have a list of calls and i have to create a summary, like this:
public class CallSummary {
private String callName;
private List<ItemSummary> items;
}
public class itemSummary {
private String status;
private Integer percentage;
}
My goal is show a percentage of calls with some status
like :
INBOUND_CALL : {
FAILED = 30%
SUCCESS = 70%
}
how can i do it using java 8 stream and Collectors ?
The idea behind the grouping would be to nest is in such a way that you have a call name and then status based count lookup available. I would also suggest using an enumeration for the status
enum CallStatus {
FAILED, SUCCESS
}
and adapting it in other classes as
class Call {
private CallStatus status;
private String callName;
}
Then you can implement a nested grouping and start off with an intermediate result such as:
List<Call> sampleCalls = List.of(new Call(CallStatus.SUCCESS,"naman"),new Call(CallStatus.FAILED,"naman"),
new Call(CallStatus.SUCCESS,"diego"), new Call(CallStatus.FAILED,"diego"), new Call(CallStatus.SUCCESS,"diego"));
Map<String, Map<CallStatus, Long>> groupedMap = sampleCalls.stream()
.collect(Collectors.groupingBy(Call::getCallName,
Collectors.groupingBy(Call::getStatus, Collectors.counting())));
which would give you an output of
{diego={FAILED=1, SUCCESS=2}, naman={FAILED=1, SUCCESS=1}}
and you can further evaluate the percentages as well. (though representing them in Integer might lose precision depending on how you evaluate them further.)
To solve it further, you can keep another Map for the name-based count lookup as:
Map<String, Long> nameBasedCount = calls.stream()
.collect(Collectors.groupingBy(Call::getCallName, Collectors.counting()));
and further, compute summaries of type CallSummary in a List as :
List<CallSummary> summaries = groupedMap.entrySet().stream()
.map(entry -> new CallSummary(entry.getKey(), entry.getValue().entrySet()
.stream()
.map(en -> new ItemSummary(en.getKey(), percentage(en.getValue(),
nameBasedCount.get(entry.getKey()))))
.collect(Collectors.toList()))
).collect(Collectors.toList());
where percentage count be implemented by you using the signature int percentage(long val, long total) aligned with the datatype chosen in ItemSummary as well.
Sample result:
[
CallSummary(callName=diego, items=[ItemSummary(status=FAILED, percentage=33), ItemSummary(status=SUCCESS, percentage=66)]),
CallSummary(callName=naman, items=[ItemSummary(status=FAILED, percentage=50), ItemSummary(status=SUCCESS, percentage=50)])
]
The following collects to a status -> percent map which you can then convert to you output model. This code assumes a getStatus method.
List<Call> calls;
Map<String,Double> statusPercents = calls.stream()
.collect(Collectors.groupingBy(Call::getStatus,
Collectors.collectingAndThen(Collectors.counting(),
n -> 100.0 * n / calls.size())));
I realise this code is a bit hard to read. The chain of collectors group the calls by status and then counts each group and finally converts to a percent. You could (arguably) make it more readable by having interim variables for the collectors:
var percentFunction = n -> 100.0 * n / calls.size();
var collectPercent = collectingAndThen(count(), percentFunction);
var collectStatusPercentMap = groupingBy(Call::getStatus, collectPercent);
You also want to group by call name but that's really just the same thing - using groupingBy and then reducing the list of calls to a CallSummary.

How can I get original elements in a class after groupBy in RxJava2?

I have a list of JSON elements. Each JSON element is represented as Java class named "Foo" for example. This Foo class has others fields which are also a Java class. I am trying to group these Foo elements by Bar id and after that I want to do other operations on grouped elements like filter, sort.
public class Foo {
private int id;
private Bar bar;
private Baz baz;
private int qty;
}
public class Bar {
private int id;
private String name;
}
public class Baz {
private int id;
private String type;
}
I tried something like this to see the result after groupBy operation but it didn't print anything. But if I provide simple elements like String or Integer instead of Foo and try to group those with the same approach, it works.
List<Foo> myInput = new ArrayList<>();
myInput.add(...);
myInput.add(...);
Observable.fromIterable(myInput)
.groupBy(el -> el.getBar().getId())
.concatMapSingle(Observable::toList)
.subscribe(System.out::println);
This code works with this input and prints out grouped elements afterwards:
Observable<String> animals = Observable.just(
"Tiger", "Elephant", "Cat", "Chameleon", "Frog", "Fish", "Flamingo");
animals.groupBy(animal -> animal.charAt(0))
.concatMapSingle(Observable::toList)
.subscribe(System.out::println);
What I am trying to do is something like this:
Observable.fromIterable(myInput)
.filter(el -> el.getBaz().getType().equals("type1"))
.groupBy(el -> el.getBar().getId())
//.filter(...)
//.sorted(...)
How can I group elements and retrieve that grouped elements and apply another operations on it? And could you also explain it a little bit so that I can understand what is happening under the hood?
From the docs of Observable#groupBy:
Groups the items emitted by an ObservableSource according to a specified criterion, and emits these grouped items as GroupedObservables.
From the docs of GroupedObservable:
An Observable that has been grouped by key, the value of which can be obtained with getKey().
So, GroupedObservable is a subclass of Observable so you can apply the usual map, filter, flatMap, etc. operators available.
For example, if you want to sum the qtys of the grouped Foos per each Bar, you can map the GroupedObservable in this way:
Observable.fromIterable(foos)
.groupBy(foo -> foo.getBar().getId())
.flatMapSingle(grouped -> grouped.map(foo -> foo.qty).reduce(0, Integer::sum))
.subscribe(System.out::println);

Remake list with some condition

There are two entities:
class GiftCertificate {
Long id;
List<Tag> tags;
}
class Tag {
Long id;
String name;
}
There is a list
List<GiftCertificate>
which contains, for example, the following data:
<1, [1, "Tag1"]>, <2, null>, <1, [2, "Tag2"]>. (It does not contain a set of tags, but only one tag or does not have it at all).
I need to do so that in the result it was this:
<1, {[1," Tag1 "], [2," Tag2 "]}>, <2, null>. I mean, add to the set of the first object a tag from the third GiftCertificate and at the same time delete the 3rd one. I would like to get at least some ideas on how to do this. it would be nice to use stream.
Probably not the most effective way, but it might help
private List<GiftCertificate> joinCertificates(List<GiftCertificate> giftCertificates) {
return giftCertificates.stream()
.collect(Collectors.groupingBy(GiftCertificate::getId))
.entrySet().stream()
.map(entry -> new GiftCertificate(entry.getKey(), joinTags(entry.getValue()))).collect(Collectors.toList());
}
private List<Tag> joinTags(List<GiftCertificate> giftCertificates) {
return giftCertificates.stream()
.flatMap(giftCertificate -> Optional.ofNullable(giftCertificate.getTags()).stream().flatMap(Collection::stream))
.collect(Collectors.toList());
}
You can do what you want with streams and with the help of a dedicated custom constructor and a couple of helper methods in GiftCertificate. Here's the constructor:
public GiftCertificate(GiftCertificate another) {
this.id = another.id;
this.tags = new ArrayList<>(another.tags);
}
This just works as a copy constructor. We're creating a new list of tags, so that if the list of tags of either one of the GiftCertificate instances is modified, the other one won't. (This is just basic OO concepts: encapsulation).
Then, in order to add another GiftCertificate's tags to this GiftCertificate's list of tags, you could add the following method to GiftCertificate:
public GiftCertificate addTagsFrom(GiftCertificate another) {
tags.addAll(another.tags);
return this;
}
And also, a helper method that returns whether the list of tags is empty or not will come in very handy:
public boolean hasTags() {
return tags != null && !tags.isEmpty();
}
Finally, with these three simple methods in place, we're ready to use all the power of streams to solve the problem in an elegant way:
Collection<GiftCertificate> result = certificates.stream()
.filter(GiftCertificate::hasTags) // keep only gift certificates with tags
.collect(Collectors.toMap(
GiftCertificate::getId, // group by id
GiftCertificate::new, // use our dedicated constructor
GiftCertificate::addTagsFrom)) // merge the tags here
.values();
This uses Collectors.toMap to create a map that groups gift certificates by id, merging the tags. Then, we keep the values of the map.
Here's the equivalent solution, without streams:
Map<Long, GiftCertificate> map = new LinkedHashMap<>(); // preserves insertion order
certificates.forEach(cert -> {
if (cert.hasTags()) {
map.merge(
cert.getId(),
new GiftCertificate(cert),
GiftCertificate::addTagsFrom);
}
});
Collection<GiftCertificate> result = map.values();
And here's a variant with a slight performance improvement:
Map<Long, GiftCertificate> map = new LinkedHashMap<>(); // preserves insertion order
certificates.forEach(cert -> {
if (cert.hasTags()) {
map.computeIfAbsent(
cert.getId(),
k -> new GiftCertificate(k)) // or GitCertificate::new
.addTagsFrom(cert);
}
});
Collection<GiftCertificate> result = map.values();
This solution requires the following constructor:
public GiftCertificate(Long id) {
this.id = id;
this.tags = new ArrayList<>();
}
The advantage of this approach is that new GiftCertificate instances will be created only if there's no other entry in the map with the same id.
Java 9 introduced flatMapping collector that is particularly well-suited for problems like this. Break the task into two steps. First, build a map of gift certificate IDs to list of tags and then assemble a new list of GiftCertificate objects:
import static java.util.stream.Collectors.flatMapping;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
......
Map<Long, List<Tag>> gcIdToTags = gcs.stream()
.collect(groupingBy(
GiftCertificate::getId,
flatMapping(
gc -> gc.getTags() == null ? Stream.empty() : gc.getTags().stream(),
toList()
)
));
List<GiftCertificate> r = gcIdToTags.entrySet().stream()
.map(e -> new GiftCertificate(e.getKey(), e.getValue()))
.collect(toList());
This assumes that GiftCertificate has a constructor that accepts Long id and List<Tag> tags
Note that this code deviates from your requirements by creating an empty list instead of null in case there are no tags for a gift certificate id. Using null instead of an empty list is just a very lousy design and forces you to pollute your code with null checks everywhere.
The first argument to flatMapping can also be written as gc -> Stream.ofNullable(gc.getTags()).flatMap(List::stream) if you find that more readable.

Categories