LINQ has Enuemerable.Select, does Java have any equivalent?
Say I have some class Record with three fields id, price, itemsSold and a list of these records from which I want to filter out the most popular item measured by which product sold the most items.
In LINQ I could do something like this
var mostPopularItem = records.GroupBy(sr => sr.Id)
.Select(g => new
{
Id = g.Key,
TotalSold = g.Sum(r => r.ItemsSold)
})
.OrderByDescending(i => i.TotalSold).First();
by using the Select(...) method to reconstruct into a suitable form. How would I do the same in Java? I could simply use streams to extract the actual number
records.stream().map(r -> r.getItemsSold()).sorted(Comparator.reverseOrder()).collect(Collectors.toList)).get(0)
but this would only give me an array of the items sold sorted in descending order. Optimally I would like to end up with an object that contains the id and the itemsSold.
The equivalent of Select in Java is the map-function on Stream. But in your case, you probably want to use the second parameter of groupingBy-collector to sum up the ItemsSold-values.
Java doesn't have is anonymous objects, like you create in your Select-call. So you would need to define a class or record to hold that data.
Using records, you could do this:
record TotalSoldRecord(String id, int totalSold) {}
var mostPopularItem = records.stream()
// Grouping by ID into a Map<String, int>, where the keys are
// the recordIds and the values are the sum of "itemsSold".
.collect(Collectors.groupingBy(Record::getId,
Collectors.summingInt(Record::getItemsSold)))
.entrySet()
.stream()
.map(entry -> new TotalSoldRecord(entry.getKey(), entry.getValue()))
// Finding the TotalSoldRecord with the maximum totalSold.
.max(Comparator.comparingInt(TotalSoldRecord::totalSold));
Note: You could also use Map.Entry as the result instead of mapping it to TotalSoldRecord.
List<Record> recordList = new ArrayList<>();
recordList.add(new Record(1L, new BigDecimal(2), 4));
recordList.add(new Record(1L, new BigDecimal(2), 5));
recordList.add(new Record(1L, new BigDecimal(2), 7));
recordList.add(new Record(2L, new BigDecimal(2), 10));
Map.Entry<Long, Integer> mostPopularItem = recordList.stream().collect(groupingBy(Record::getId, summingInt(Record::getItemsSold))).entrySet().stream().max(Map.Entry.comparingByValue()).orElse(null);
Output:
Key = 1, Value = 16
public class Record {
public Record(Long id, BigDecimal price, Integer itemsSold) {
this.id = id;
this.price = price;
this.itemsSold = itemsSold;
}
private Long id;
private BigDecimal price;
private Integer itemsSold;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
public Integer getItemsSold() {
return itemsSold;
}
public void setItemsSold(Integer itemsSold) {
this.itemsSold = itemsSold;
}
}
This should return the most popular item:
var itemsSoldById = records.stream()
.collect(groupingBy(rec -> rec.getId(), summingInt(rec -> rec.getItemsSold())));
var mostPopularItem = itemsSoldById
.entrySet()
.stream()
.max(Comparator.comparingInt(Map.Entry::getValue))
.orElseThrow(()-> new IllegalStateException("The list of records is empty"));
Previous version before my edit that did not group records by id:
var mostPopularItem = records.stream()
.sorted(Comparator.comparing(rec -> rec.getItemsSold()))
.reduce((rec1 , rec2) -> rec1.getItemsSold() > rec2.getItemsSold() ? rec1 : rec2)
.orElseThrow(()-> new IllegalStateException("The list of records is empty"));
Something like this?
package example.stackoverflow;
import java.util.List;
import java.util.stream.Collectors;
public class Select {
public static void main(String[] args) {
record Thing(String id, int price, int itemsSold){}
List<Thing> records = List.of(
new Thing("a", 5, 14),
new Thing("b", 4, 33),
new Thing("c", 6, 10),
new Thing("a", 5, 21),
new Thing("c", 6, 12)
);
record PopularThing(String id, int totalSold){}
records.stream()
.collect(Collectors.groupingBy(Thing::id, Collectors.summingInt(Thing::itemsSold)))
.entrySet().stream().map(e -> new PopularThing(e.getKey(), e.getValue()))
.max((a,b) -> a.totalSold - b.totalSold)
.ifPresent(System.out::println);
}
}
Related
I have a problem about writing a java stream by filtering multiple condition, grouping by month of the date and calculating the sum of person.
I store the values as Map<String(pId),List<Person>>
Here is my Person class shown below
public class Person{
private String id;
private String name;
private String surname;
private Statement event; // JOIN , EXIT
private Object value;
private LocalDate eventDate;
}
Here is the list
ID,Info,Date (All values are stored in Person object defined in the List)
per1, JOIN, 10-01-2022
per2, JOIN, 10-01-2022
per3, EXIT, 10-01-2022
per3 EXIT, 10-02-2022
per4, JOIN, 10-03-2022
What I want to do is to get this result.
Month Info Total Number
1 JOIN 2
1 EXIT 1
2 EXIT 1
3 JOIN 1
Here is my dto shown below.
public class DTO {
private int month;
private State info; // State -> enum
private int totalEmployees;
}
Here is my GroupDto shown below.
public class GroupDto {
private int month;
private State info;
}
Here is the code snippet but I cannot complete it.
List<DTO > result = persons .values().stream()
.flatMap(List::stream)
.filter(person -> person .getInfo() == Value.ONBOARD || person .getInfo() == Value.EXIT)
.collect(Collectors.groupingBy(
p -> new GroupDto(p.getEventDate().getMonthValue(), p.getEvent()),
Collectors.counting()
))
.entrySet().stream()
.map(e -> new DTO(p.getKey().get, p.getKey(), (int) (long) e.getValue())) // -> ERROR Line
.sorted(Comparator.comparing(MonthWiseDto::getMonth))
.toList();
How can I do that?
Assuming that you want to get the total number of people for every combination of Month and State you need to group the data based on these two properties.
For that, you can define an object that would serve as a key in the intermediate Map. It can be implemented as a Java 16 record like that (you can use a class with earlier JDK version) :
public record MonthState(int mont, State info) {}
In the stream, we can make use of the Collector groupingBy() with Collector counting() passed as a downstream to generate the total number of Person objects having particular combination of month and state.
Map<String, List<Person>> personListById = // initializing the map
List<DTO> result = personListById.values().stream()
.flatMap(List::stream)
.filter(per -> per.getInfo() == State.EXIT || per.getInfo() == State.JOIN)
.collect(Collectors.groupingBy(
p -> new MonthState(p.getEventDate().getMonthValue(), p.getInfo()),
Collectors.counting()
))
.entrySet().stream()
.map(e -> new DTO(e.getKey().mont(), e.getKey().info(), (int) (long) e.getValue()))
.sorted(Comparator.comparing(DTO::getMonth))
.toList();
Link to online Demo
public class Main {
public static void main(String... args) {
List<Person> persons = List.of(
new Person("per1", Statement.JOIN, LocalDate.of(2022, 1, 10)),
new Person("per2", Statement.JOIN, LocalDate.of(2022, 1, 10)),
new Person("per3", Statement.EXIT, LocalDate.of(2022, 1, 10)),
new Person("per3", Statement.EXIT, LocalDate.of(2022, 2, 10)),
new Person("per4", Statement.JOIN, LocalDate.of(2022, 3, 10)));
List<Dto> groupResults = calculateGroupResults(persons);
groupResults.forEach(System.out::println);
}
public static List<Dto> calculateGroupResults(List<Person> persons) {
Map<GroupKey, Long> map = persons.stream()
.filter(person -> person.getEvent() == Statement.EXIT || person.getEvent() == Statement.JOIN)
.collect(Collectors.groupingBy(
person -> new GroupKey(person.getEventDate().getMonthValue(), person.getEvent()),
Collectors.counting()));
return map.entrySet().stream()
.map(entry -> new Dto(entry.getKey().getMonth(), entry.getKey().getInfo(), entry.getValue()))
.sorted(Dto.SORT_BY_MONTH_INFO)
.collect(Collectors.toList());
}
enum Statement {
JOIN,
EXIT
}
#Getter
#RequiredArgsConstructor
public static final class Person {
private final String id;
private final Statement event;
private final LocalDate eventDate;
}
#Getter
#EqualsAndHashCode
#RequiredArgsConstructor
public static final class GroupKey {
private final int month;
private final Statement info;
}
#Getter
#RequiredArgsConstructor
public static final class Dto {
public static final Comparator<Dto> SORT_BY_MONTH_INFO =
Comparator.comparing(Dto::getMonth)
.thenComparingInt(one -> one.getInfo().ordinal());
private final int month;
private final Statement info;
private final long totalEmployees;
#Override
public String toString() {
return month + "-" + info + '-' + totalEmployees;
}
}
}
Demo
1-JOIN-2
1-EXIT-1
2-EXIT-1
3-JOIN-1
I got a list of objects from class A in a list.
Some of these objects are equal in id and name but not in list <B> , and list b is ALWAYS different.
I need to merge these so that my list is only made out of object a's with same name and id exists and all the b from same group are collected
I can make use of jdk 8 plus utils so streams are ok to use here.. Although I think reflection here is more usable?
PS: I can not change content of a of b class as they are generated classes and no access / expansion possibility
#Test
public void test() {
List.of(new A(1, "a1", List.of(new B(1, "1b"))),
new A(1, "a1", List.of(new B(2, "2b"))),
new A(2, "a2", List.of(new B(3, "3b"))));
//expected
List.of(new A(1, "a1", List.of(new B(1, "1b"), new B(2, "2b"))),
new A(2, "a2", List.of(new B(3, "3b"))));
}
class A {
public A(int id, String name, List<B> listB) {
this.id = id;
this.name = name;
this.listB = listB;
}
int id;
String name;
List<B> listB;
}
class B {
public B(int id, String name) {
this.id = id;
this.name = name;
}
int id;
String name;
}
You could use
record Key(int id, String name) {};
List<A> result = input.stream().collect(
Collectors.groupingBy(a -> new Key(a.getId(), a.getName()),
LinkedHashMap::new,
Collectors.flatMapping(a -> a.getListB().stream(), Collectors.toList())))
.entrySet().stream()
.map(e -> new A(e.getKey().id(), e.getKey().name(), e.getValue()))
.collect(Collectors.toList());
if(!result.equals(expected)) {
throw new AssertionError("expected " + expected + " but got " + result);
}
This constructs new lists with new A objects, which is suitable for immutable objects. Your use of List.of(…) suggests a preference towards immutable objects. If you have mutable objects and want to perform the operation in-place, you could do
List<A> result = new ArrayList<>(input); // only needed if input is an immutable list
record Key(int id, String name) {};
HashMap<Key,A> previous = new HashMap<>();
result.removeIf(a -> previous.merge(new Key(a.getId(), a.getName()), a, (old, newA) -> {
var l = old.getListB();
if(l.getClass() != ArrayList.class) old.setListB(l = new ArrayList<>(l));
l.addAll(newA.getListB());
return old;
}) != a);
if(!result.equals(expected)) {
throw new AssertionError("expected " + expected + " but got " + result);
}
This removes the duplicates from the list and adds their Bs to the previously encountered original. It does the minimum of changes required to get the intended list, e.g. if there are no duplicates, it does nothing.
If A objects with the same id always have the same name, in other words, there is no need for a key object checking both, you could simplify this approach to
List<A> result = new ArrayList<>(input); // only needed if input is an immutable list
HashMap<Integer,A> previous = new HashMap<>();
result.removeIf(a -> previous.merge(a.getId(), a, (old, newA) -> {
var l = old.getListB();
if(l.getClass() != ArrayList.class) old.setListB(l = new ArrayList<>(l));
l.addAll(newA.getListB());
return old;
}) != a);
if(!result.equals(expected)) {
throw new AssertionError("expected " + expected + " but got " + result);
}
If you need preserve an instance for each id you can write (I assume objects have getters and setters)
System.out.println(xs.stream()
.collect(groupingBy(A::getId, toList()))
.values().stream()
.peek(g -> g.get(0).setListB(
g.stream()
.flatMap(h -> h.getListB().stream())
.collect(groupingBy(B::getId, toList()))
.values().stream()
.map(i -> i.get(0))
.collect(toList())))
.map(g -> g.get(0))
.collect(toList()));
your input case with output
[A(id=1, name=a1, listB=[B(id=1, name=b1), B(id=2, name=b2)]), A(id=2, name=a2, listB=[B(id=3, name=b3)])]
if you can create new instances then you can renormalize the lists
System.out.println(xs.stream()
.flatMap(a -> a.getListB().stream().map(b -> List.<Object>of(a.id, a.name, b.id, b.name)))
.distinct()
.collect(groupingBy(o -> o.get(0), toList()))
.values()
.stream()
.map(zs -> new A((int) zs.get(0).get(0), (String) zs.get(0).get(1),
zs.stream().map(z -> new B((int) z.get(2), (String) z.get(3))).collect(toList())))
.collect(toList()));
(you can change the ugly .get(0).get(0) using some intermediate class called DenormalizedRow or so)
Task here is to practise Maps & Collections.
We have 2 classes with its attributes in the brackets
1st Class: Client (String name, String lastName, Integer age, BigDecimal cash)
Below list of random clients in Main
List<Client> clients = List.of(
new Client("Ola", "Chrzaszcz", 34, new BigDecimal("200")),
new Client("Ala", "Kowalsky", 24, new BigDecimal("4000")),
new Client("Olaf", "Chrzaszcz", 19, new BigDecimal("3999")),
new Client("Piotr", "Nowak", 21, new BigDecimal("2099")),
new Client("Ola", "Szwed", 45, new BigDecimal("3000"))
)
;
2nd Class: Product (String name, Enum category, BigDecimal Price)
List<Product> products = List.of(
new Product("Szynka", Category.A, new BigDecimal(29)),
new Product("Ser", Category.B, new BigDecimal(22)),
new Product("Chleb", Category.C, new BigDecimal(6)),
new Product("Maslo", Category.D, new BigDecimal(4)),
new Product("Kielbasa", Category.A, new BigDecimal(25)),
new Product("Jajka", Category.A, new BigDecimal(8)),
new Product("Szynka", Category.C, new BigDecimal(25))
);
Based on these 2 list new Map is created (key: person, value: Map<Product, quantity of sold products)
//s1 - new map creation
Map<Client, Map<Product,Integer>> customersWithTheirShopping = new HashMap<>();
//adding values to the map
customersWithTheirShopping.put(clients.get(0), Map.of(products.get(0),5));
customersWithTheirShopping.put(clients.get(1), Map.of(products.get(1),6));
customersWithTheirShopping.put(clients.get(2), Map.of(products.get(6),16));
customersWithTheirShopping.put(clients.get(3), Map.of(products.get(5),11));
customersWithTheirShopping.put(clients.get(4), Map.of(products.get(4),5));
customersWithTheirShopping.put(clients.get(4), Map.of(products.get(3),6));
customersWithTheirShopping.put(clients.get(2), Map.of(products.get(2),8));
customersWithTheirShopping.put(clients.get(4), Map.of(products.get(1),9));
GOAL -> EXPECTED RESULTS: using stream to create method that return client with top money spent (price x quantity)
QUESTION:
How to do it? How to get to the value of the map which is value itself?!
As this is map and key (client) is unique, we can not expect duplication in
instances of Client class, right (with this map structure)? It means that each customer can do order just once, right?
WHAT I DID -> CURRENT BEHAVIOUR:
public Client clientWithMostExpShoping() {
return quantitiesOfProductsBoughtByClient
.entrySet()
.stream()
.max(Comparator.comparing(p -> p.getValue().entrySet().stream().max(
Comparator.comparing(x -> x.getKey().getPrice().multiply(BigDecimal.valueOf(x.getValue()))))))
};
//btw did not find similar case unfortunately
Yes, currently in your code create Immutable Maps of Product and Integers, so you can't add new Products for the Client. Also you overwrite the Map Entries for the client, for example client with index 4 has Map.of(products.get(1),9) as value, because it is the last one and overwrites every previous assignment
If you want to have multiple products per client you could refactor to:
customersWithTheirShopping.put(clients.get(0), new HashMap<>(Map.of(products.get(0), 5)));
customersWithTheirShopping.put(clients.get(1), new HashMap<>(Map.of(products.get(1), 6)));
customersWithTheirShopping.put(clients.get(2), new HashMap<>(Map.of(products.get(6), 16)));
customersWithTheirShopping.put(clients.get(3), new HashMap<>(Map.of(products.get(5), 11)));
customersWithTheirShopping.put(clients.get(4), new HashMap<>(Map.of(products.get(4), 5)));
customersWithTheirShopping.get(clients.get(4)).put(products.get(3), 6);
customersWithTheirShopping.get(clients.get(2)).put(products.get(2), 8);
customersWithTheirShopping.get(clients.get(4)).put(products.get(1), 9);
Then you can use streams to get the correct client
Client clientWithMostExpense = customersWithTheirShopping
.entrySet()
.stream()
.max(
// check all clients and compare their mostExpensiveProducts
Comparator.comparing(
client -> {
Map.Entry<Product, Integer> mostExpensiveProductForClient = client.getValue().entrySet()
.stream().max(
//Find most expensive product of all for client
Comparator.comparing(product ->
product.getKey().getPrice().multiply(BigDecimal.valueOf(product.getValue())))
).get();
// return the full price of the most expensive product for comparison
return mostExpensiveProductForClient.getKey().getPrice().multiply(BigDecimal.valueOf(mostExpensiveProductForClient.getValue()));
}
)).get().getKey();
Another solution would be to refactor quantity and product to a new class called BoughtProduct or something similiar, in which you could already calculate its price
public class BoughtProduct {
Product product;
Integer quantity;
public BoughtProduct(Product product, Integer quantity) {
this.quantity = quantity;
this.product = product;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public Product getProduct() {
return product;
}
public void setProduct(Product product) {
this.product = product;
}
public BigDecimal getTotalPrice(){
return this.product.getPrice().multiply(BigDecimal.valueOf(quantity));
}
}
Then you could use lists instead of a map:
Map<Client, List<BoughtProduct>> customersWithTheirShopping = new HashMap<>();
customersWithTheirShopping.put(clients.get(0), new ArrayList<>(List.of(new BoughtProduct(products.get(0), 5))));
customersWithTheirShopping.put(clients.get(1), new ArrayList<>(List.of(new BoughtProduct(products.get(1), 6))));
customersWithTheirShopping.put(clients.get(2), new ArrayList<>(List.of(new BoughtProduct(products.get(6), 16))));
customersWithTheirShopping.put(clients.get(3), new ArrayList<>(List.of(new BoughtProduct(products.get(5), 11))));
customersWithTheirShopping.put(clients.get(4), new ArrayList<>(List.of(new BoughtProduct(products.get(4), 5))));
customersWithTheirShopping.get(clients.get(4)).add(new BoughtProduct((products.get(3)), 6)); //could use a check whether product exists, then you could increase quantity
customersWithTheirShopping.get(clients.get(2)).add(new BoughtProduct(products.get(2),8));
customersWithTheirShopping.get(clients.get(4)).add(new BoughtProduct(products.get(1),9));
And make the streams also a bit more readable:
Client clientWithMostExpense = customersWithTheirShopping
.entrySet()
.stream()
.max(
// check all clients and compare their mostExpensive BoughtProduct in their list
Comparator.comparing(
//For each client get their most expensive product
client -> client.getValue()
.stream().max(Comparator.comparing(BoughtProduct::getTotalPrice)).get()
.getTotalPrice()
)).get().getKey();
I have a simple Customer class like so
public class Customer {
public int age;
public int discount;
public String name;
public Customer(String name) {
this.name = name;
}
public Customer(String name, int age) {
this.name = name;
this.age = age;
}
public Customer(String name, int age, int discount) {
this.name = name;
this.age = age;
this.discount = discount;
}
#Override
public String toString() {
return "Customer [age=" + age + ", discount=" + discount + ", name=" + name + "]";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Integer getDiscount() {
return discount;
}
public void setDiscount(int discount) {
this.discount = discount;
}
}
I populate a list of these objects using this
List<Customer> customerList = new ArrayList<>(Arrays.asList(
new Customer("John", 2, 15),
new Customer("John", 4, 15),
new Customer("John", 6, 25),
new Customer("Joe", 3, 15),
new Customer("Joe", 3, 15),
new Customer("Joe", 3, 15),
new Customer("Goerge", 6, 25),
new Customer("Goerge", 6, 25),
new Customer("Mary", 7, 25),
new Customer("Jane", 1, 15),
new Customer("Jane", 2, 15),
new Customer("Jane", 8, 25),
new Customer("Jane", 8, 25)
));
Now I want to group and count the names and discounts, using a collector like this
Map<Object, Long> collected = customerList
.stream()
.collect(Collectors.groupingBy(x -> Arrays.asList(x.name, x.discount), Collectors.counting()));
I can review my output using this
collected.entrySet().forEach(c -> {
System.out.println(c);
});
Which outputs the following
[Jane, 15]=2
[Joe, 15]=3
[John, 15]=2
[Mary, 25]=1
[John, 25]=1
[Jane, 25]=2
[Goerge, 25]=2
The question is how do I sort the Map by name and discount so it looks like this
[Goerge, 25]=2
[Jane, 15]=2
[Jane, 25]=2
[Joe, 15]=3
[John, 15]=2
[John, 25]=1
[Mary, 25]=1
I keep bumping up against the Object type that is returned by the collector?
Can I cast the collector so that it returns a class, maybe something like
private class DiscountCounts
{
public String name;
public Integer discount;
}
Is it possible to convert the Map<**Object**, Long>() to something like Map<DiscountCounts, Long>(), would this allow access to the fields of the Map key using lambda or Comparator constructs?
I tried something like this, iterate over the map and manually convert to the Map I want but I can't get to the original collection's keys?
Map<DiscountCounts, Long> collected2 = new HashMap<>();
collected.entrySet().forEach(o -> {
DiscountCounts key1 = (DiscountCounts)o.getKey(); //--> Fails here
collected2.put((DiscountCounts)o.getKey(), o.getValue());
});
One way you can do it without using DiscountCounts class is, first sort the list and then perform the groping by operation, and use LinkedHashMap to save the sorted order
Map<List<Object>, Long> map = customerList.stream()
.sorted(Comparator.comparing(Customer::getName).thenComparing(Customer::getDiscount))
.collect(Collectors.groupingBy(x -> Arrays.asList(x.name, x.discount),LinkedHashMap::new, Collectors.counting()));
The another way using DiscountCounts class is, by override the equals and hashcode of DiscountCounts class and do a groupingBy creating DiscountCounts object for every Customer object as key in Map and use TreeMap with Comparator to sort the result
Map<DiscountCounts, Long> result = customerList.stream().collect(Collectors.groupingBy(
c -> new DiscountCounts(c.getName(), c.getDiscount()),
() -> new TreeMap<DiscountCounts, Long>(
Comparator.comparing(DiscountCounts::getName).thenComparing(DiscountCounts::getDiscount)),
Collectors.counting()));
#Andreas suggest in the comment enlighten me another way of doing it, and i feel this is one of the best approach you can implement Comparable on DiscountCounts and provide the sorting logic so that you don't need to provide Comparator to TreeMap
#Override
public int compareTo(DiscountCounts cust) {
int last = this.getName().compareTo(cust.getName());
return last == 0 ? this.getDiscount().compareTo(cust.getDiscount()) : last;
}
Map<DiscountCounts, Long> result1 = customerList.stream().collect(Collectors.groupingBy(
c -> new DiscountCounts(c.getName(), c.getDiscount()), TreeMap::new, Collectors.counting()));
With proper equals and hashcode implementation for DiscountCounts, you might be looking for something over the lines of :
Map<DiscountCounts, Long> collectSortedEntries = customerList
.stream()
.collect(Collectors.groupingBy(x -> new DiscountCounts(x.name, x.discount),
Collectors.counting()))
.entrySet()
.stream()
.sorted(Comparator.comparing((Map.Entry<DiscountCounts, Long> e) -> e.getKey().getName())
.thenComparing(e -> e.getKey().getDiscount()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(a, b) -> a, LinkedHashMap::new));
I would like to get the highest score group by Id .If two highest score's are same then i would like get the highest score based on lowest Optional ID.I would like to get it in Java Stream.So far i am trying the following codes which is not working
Example :
Array List:
ID: 1 Score:80 OptionalId:1
ID: 1 Score:90 OptionalId:2
ID: 1 Score:90 OptionalId:3
ID: 2 Score:80 OptionalId:1
ID: 2 Score:100 OptionalId:3
ID: 2 Score:100 OptionalId:5
The result should be
ID: 1 Score 90 OptionalId:2
ID 2 Score 100 OptionalId:3
Map<Long, Optional<Person>> result1 = records.stream()
.collect(Collectors.groupingBy(Person::getId,
Collectors.maxBy(Comparator.comparing(Person::getScore)),
Collector.minBy(Comparator.comparing(Person::getOptionalId))));
for(Person ns: result1) {
sb.append(ns.getBatchNumber());
sb.append(',');
I suggest you start with a custom Comparator<Person> which prioritizes a max of score and then a min of optionalId. It's a good way to pass it to a variable for sake of brevity:
final Comparator<Person> comparator = Comparator
.comparing(Person::getScore) // descending score first
.thenComparing(Comparator // then ..
.comparing(Person::getOptionalId) // .. optionalId
.reversed()); // .. but ascending
Now use some collectors using java-stream.
Collectors::groupingBy to group all the Persons by the id and downstream the value
Collectors::reducing to reduce all the Persons with the same id into one using the Comparator<Person> to get the one with the highest score and lowest optionalId.
Collectors::collectingAndThen to flatten the structure from Collection<Optional<Person>> to Collection<Person> since any reducing operation results as Optional - this step is optional and might be skipped according to your sample.
Here is the code:
Collection<Person> filtered = records.stream() // Stream<Person>
.collect(Collectors.groupingBy( // from Map<Long, List<Person>>
Person::getId,
Collectors.collectingAndThen( // .. downstream to ..
Collectors.reducing((a, b) -> // .. Map<Long, Optional<Person>>
comparator.compare(a, b) > 0 ? a : b),
Optional::get)) // .. Map<Long, Person>
.values(); // .. Collection<Person>
[Person [id=1, score=90, optionalId=2], Person [id=2, score=100, optionalId=3]]
For a given Id value, there must be a Person. Existence of the Id value solely depends on the Person. So if there exists an Id, there must be a Person too. Therefore what is the point of having an Optional<Person> as the value of the map. In contrast, it makes more sense to merely have a Person instance as the value in the map. Here I am using the toMap collector in concert with the BinaryOperator.maxBy to get the work done. Here's how it looks. Notice how the BinaryOperator.maxBy is used as the mergeFunction.
Map<Integer, Person> maxPersonById = records.stream()
.collect(Collectors.toMap(Person::getId, Function.identity(),
BinaryOperator.maxBy(Comparator.comparing(Person::getScore)
.thenComparing(Comparator.comparing(Person::getOptionalId).reversed()))));
And here's the output for the above given input.
{1=Person [id=1, score=90, optionalId=2], 2=Person [id=2, score=100, optionalId=3]}
You may try the following stream code which aggregates by ID, and then finds the max score using a two level sort, first by score, then, by optional ID in case of a tie for score:
import static java.util.Collections.reverseOrder;
import static java.util.Comparator.comparing;
Map<Long, Optional<Person>> result1 = records.stream()
.collect(Collectors.groupingBy(Person::getId,
Collectors.maxBy(
Comparator.comparing(Person::getScore)
.thenComparing(reverseOrder(comparing(Person::getOptionalId))))));
Optional[ID: 1 Score: 90 OptionalId: 2]
Optional[ID: 2 Score: 100 OptionalId: 3]
The trick here is to reverse the sorting order for only the optional ID, which we want to be ascending, not descending. The sort order by default would be descending, because we are invoking Collections.maxBy.
I cite this great SO question for help with the reverse syntax. Also, I borrowed the boiler plate code from #mayamar to setup the following demo:
Demo
(demo only for demonstration purposes)
I changed a bit and introduced a helper class that compares score and optionalId.
public class T21Group {
public static void main(String[] args) {
List<Person> records = new ArrayList<>();
records.add(new Person(1, 80, 1));
records.add(new Person(1, 90, 2));
records.add(new Person(1, 90, 3));
records.add(new Person(2, 80, 1));
records.add(new Person(2, 100, 3));
records.add(new Person(2, 100, 5));
Map<Long, Optional<Person>> result1 = records.stream()
.collect(Collectors.groupingBy(Person::getId, Collectors.maxBy(Comparator.comparing(Pair::new))));
for (Optional<Person> ns : result1.values()) {
System.out.println(ns);
}
}
public static class Pair implements Comparable<Pair> {
long score;
long optionalId;
public Pair(Person p) {
score = p.getScore();
optionalId = p.getOptionalId();
}
#Override
public int compareTo(Pair o) {
if (this.score == o.score) {
return Long.compare(o.optionalId, this.optionalId);
}
return Long.compare(this.score, o.score);
}
}
public static class Person {
private long id;
private long score;
private long optionalId;
public Person(long id, long score, long optionalId) {
this.id = id;
this.score = score;
this.optionalId = optionalId;
}
#Override
public String toString() {
return "ID: " + id + " Score: " + score + " OptionalId: " + optionalId;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public long getScore() {
return score;
}
public void setScore(long score) {
this.score = score;
}
public long getOptionalId() {
return optionalId;
}
public void setOptionalId(long optionalId) {
this.optionalId = optionalId;
}
}
}