Java collection - group by multiple attributes - java

I have a Java class:
class Person {
String firstName;
String lastName;
int income;
public Person(String firstName, String lastName, int income)
{
this.firstName = firstName;
this.lastName = lastName;
this.income = income;
}
I have a Collection<Person>, with 4 x Person objects:
Collection<Person> persons = new ArrayList<>();
persons.add(new Person("John", "Smith", 5));
persons.add(new Person("Mary", "Miller", 2));
persons.add(new Person("John", "Smith", 4));
persons.add(new Person("John", "Wilson", 4));
I want to make a new Collection instance, but from the elements with the same "firstName" and "lastName", make 1 element, and the result "income" will be the sum of the "incomes" of each element. So, for this particular case, the resulting collection will have 3 elements, and "John Smith" will have the "income" = 9.
In SQL, the equivalent query is:
SELECT FIRSTNAME, LASTNAME, SUM(INCOME) FROM PERSON GROUP BY FIRSTNAME, LASTNAME
I found only answers which contain Map as result, and "key" contains the column(s) used for grouping by. I want to obtain directly a similar type of collection from the initial (ArrayList<Person>), and not a Map, because if in my collection I have millions of elements, it will decrease the performance of the code. I know it was easier if I worked on SQL side, but in this case I must work on Java side.

I don't know if that is the most beautiful solution, but you can try to groupBy firstName and lastName with a delimiter between them, let's say .. After you collect your data into Map<String, Integer> that contains your firstName.lastName, you create new list of Person from it.
List<Person> collect = persons.stream()
.collect(Collectors.groupingBy(person -> person.getFirstName() + "." + person.getLastName(),
Collectors.summingInt(Person::getIncome)))
.entrySet().stream().map(entry -> new Person(entry.getKey().split(".")[0],
entry.getKey().split(".")[1],
entry.getValue()))
.collect(Collectors.toList());

I think JoSQL is your way to go here, it allow you to run SQL queries over java objects:
JoSQL (SQL for Java Objects) provides the ability for a developer to apply a SQL statement to a collection of Java Objects. JoSQL provides the ability to search, order and group ANY Java objects and should be applied when you want to perform SQL-like queries on a collection of Java Objects.
And this is how to use it in your case:
Query q=new Query();
q.parse("SELECT firstname, lastname, SUM(income) FROM package.Person GROUP BY firstname, lastname");
List<?> results=q.execute(names).getResults();
You can also follow this JoSQL tutorial for further reading.

I found the answer below:
List<Person> collect = persons.stream()
.collect(Collectors.groupingBy(person -> person.getFirstName() + "." + person.getLastName(),
Collectors.summingInt(Person::getIncome)))
.entrySet().stream().map(entry -> new Person(entry.getKey().split(".")[0],
entry.getKey().split(".")[1],
entry.getValue()))
.collect(Collectors.toList());
Do not do that way! It uses memory a lot. Use Wrapper (PersonComparator) over the fields you need to group by.
public class Main {
public static void main(String[] args) {
Collection<Person> persons = new ArrayList<>();
persons.add(new Person("John", "Smith", 5));
persons.add(new Person("Mary", "Miller", 2));
persons.add(new Person("John", "Smith", 4));
persons.add(new Person("John", "Wilson", 4));
Map<Person, Integer> groupedByIncomes = persons.stream()
.collect(
Collectors.groupingBy(
Person::getPersonComparator,
Collectors.summingInt(Person::getIncome)
)
)
.entrySet()
.stream()
.collect(Collectors.toMap(
e -> e.getKey().person,
Map.Entry::getValue
));
System.out.println(groupedByIncomes);
}
static class Person {
String firstName;
String lastName;
int income;
public Person(String firstName, String lastName, int income) {
this.firstName = firstName;
this.lastName = lastName;
this.income = income;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public int getIncome() {
return income;
}
PersonComparator getPersonComparator() {
return new PersonComparator(this);
}
static class PersonComparator {
Person person;
PersonComparator(Person person) {
this.person = person;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PersonComparator that = (PersonComparator) o;
if (!person.getFirstName().equals(that.person.getFirstName())) return false;
return person.getLastName().equals(that.person.getLastName());
}
#Override
public int hashCode() {
int result = person.getFirstName().hashCode();
result = 31 * result + person.getLastName().hashCode();
return result;
}
}
}
}
If you need framework solution f.e. when you need some abstraction over the data types you have (SQL, Mongo or Collections) I suggest you to use QueryDSL: http://www.querydsl.com/

You can use Java 8 streams' Collector's groupingBy:
Map<String, Integer> sum = items.stream().collect(
Collectors.groupingBy(p -> p.getFirstName()+p.getSecondName(), Collectors.summingInt(Person::getIncome)));

Related

Is there a Java equivalent of C#'s LINQ "Select" function

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

Write two for instructions as a stream

I have two lists of Pearson (variables: FirstName, LastName, AllFirstName). One of them contains duplicates (if a pearson has two first names then in that list will have two entries for each name but the lastname will be the same) and one of them has only unique values (listWithUniquePearsons). The second list will be created by itereting over the first list and putting all the first name in a list of objects. I wrote this with two for. Is there any way to write it as a stream?
for (Pearson prs : listWithUniquePearsons) {
List<String> firstNames = new ArrayList<String>();
for (Pearson tempPerson : allPearsons) {
if (prs.getLastName().equals(tempPerson.getLastName())) {
firstNames.add(tempPerson.firstNames());
}
}
if (firstNames.size()>1) {
prs.setAllFirstNames(firstNames);
}
}
List<String> firstNames = listWithUniquePearsons
.stream()
.map(prs -> allPearsons.stream()
.filter(tempPerson -> prs.getLastName().equals(tempPerson.getLastName()))
.map(Person::getFirstName)
.collect(Collectors.toList());
You should build a map with a key lastName and values List<FirstName> and then remap its entries back to Pearson class setting allFirstNames. This can be done using Java 8 streams and collectors.
Let's assume that class Pearson is implemented as follows:
import java.util.*;
public class Pearson {
private String firstName;
private String lastName;
private List<String> allFirstNames;
public Pearson(String first, String last) {
this.firstName = first;
this.lastName = last;
}
public Pearson(List<String> allFirst, String last) {
this.allFirstNames = allFirst;
this.lastName = last;
}
public String getFirstName() {return firstName; }
public String getLastName() {return lastName; }
public List<String> getAllFirstNames() {return allFirstNames; }
}
Test code:
import java.util.*;
import java.util.stream.*;
public class Test {
public static void main(String[] args) {
List<Pearson> duplicates = Arrays.asList(
new Pearson("John", "Doe"),
new Pearson("William", "Doe"),
new Pearson("Edgar", "Poe"),
new Pearson("Allan", "Poe"),
new Pearson("Don", "King")
);
List<Pearson> uniques = duplicates.stream()
// map LastName -> List<FirstName>
.collect(Collectors.groupingBy(
Pearson::getLastName,
LinkedHashMap::new, // keep order of entries
Collectors.mapping(
Pearson::getFirstName,
Collectors.toList())
)).entrySet()
.stream()
.map(e -> new Pearson(e.getValue(), e.getKey()))
.collect(Collectors.toList());
uniques.forEach(u ->
System.out.println(
String.join(" ", u.getAllFirstNames())
+ " " + u.getLastName()
));
}
}
Output
John William Doe
Edgar Allan Poe
Don King

Java 8 Sort HashMap where map key is an object of <String, Integer>

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

Apply a function to a nested list grouped by stream

There is a list of objects for the following class:
class A {
String firstName;
String lastName;
//.....
}
There is also a method that takes in three parameters:
List<B> someMethod(String firstName, String lastName, List<A> l);
I want to group this list based on firstName and lastName and then apply this method to the items in the list of items that have firstName and lastName
I tried the following:
Map<String, Map<String, List<B>>> groupedItems = l.stream()
.collect(groupingBy(A::getFirstName, groupingBy(A::getLastName)));
List<B> result = groupedItems.keySet().stream()
.map(firstName -> groupedItems.get(firstName).keySet().stream()
.map(lastName -> someMethod(firstName, lastName, groupedItems.get(firstName).get(lastName))
.collect(Collectors.toList()))
.flatMap(Collection::stream)
.collect(Collectors::toList);
Is there a way to do this in one shot instead of the way it is now?
You can do it using collectingAndThen, but this, in my opinion, is far less readable.
List<B> result = l.stream().collect(Collectors.collectingAndThen(
groupingBy(A::getFirstName, groupingBy(A::getLastName)),
groupedItems -> groupedItems.keySet().stream()
.flatMap(firstName ->
groupedItems.get(firstName)
.keySet()
.stream()
.map(lastName ->
someMethod(
firstName,
lastName,
groupedItems.get(firstName).get(lastName)
)
)
)
.flatMap(Collection::stream)
.collect(toList())));
What about just iterate over gien List<A> and cache calculated results of Lit<B> for every unique fileName + lastName. It takes O(n) time and O(n) space.
public class Foo {
private final Map<String, List<B>> map = new HashMap<>();
public List<B> someMethod(String firstName, String lastName, List<A> as) {
return map.compute(firstName + '|' + lastName, (key, bs) -> {
if (bs == null) {
// create List<B> based on given List<A>
bs = Collections.emptyList();
}
return bs;
});
}
}

What is equivalent to C#'s Select clause in JAVA's streams API

I wanted to filter list of Person class and finally map to some anonymous class in Java using Streams. I am able to do the same thing very easily in C#.
Person class
class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
}
Code to map the result in desire format.
List<Person> lst = new List<Person>();
lst.Add(new Person() { Name = "Pava", Address = "India", Id = 1 });
lst.Add(new Person() { Name = "tiwari", Address = "USA", Id = 2 });
var result = lst.Select(p => new { Address = p.Address, Name = p.Name }).ToList();
Now if I wanted to access any property of newly created type I can easily access by using below mentioned syntax.
Console.WriteLine( result[0].Address);
Ideally I should use loop to iterate over the result.
I know that in java we have collect for ToList and map for Select.
But i am unable to select only two property of Person class.
How can i do it Java
Java does not have structural types. The closest you could map the values to, are instances of anonymous classes. But there are significant drawbacks. Starting with Java 16, using record would be the better solution, even if it’s a named type and might be slightly more verbose.
E.g. assuming
class Person {
int id;
String name, address;
public Person(String name, String address, int id) {
this.id = id;
this.name = name;
this.address = address;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public String getAddress() {
return address;
}
}
you can do
List<Person> lst = List.of(
new Person("Pava", "India", 1), new Person("tiwari", "USA", 2));
var result = lst.stream()
.map(p -> {
record NameAndAddress(String name, String address){}
return new NameAndAddress(p.getName(), p.getAddress());
})
.collect(Collectors.toList());
result.forEach(x -> System.out.println(x.name() + " " + x.address()));
The anonymous inner class alternative would look like
List<Person> lst = List.of(
new Person("Pava", "India", 1), new Person("tiwari", "USA", 2));
var result = lst.stream()
.map(p -> new Object(){ String address = p.getAddress(); String name = p.getName();})
.collect(Collectors.toList());
result.forEach(x -> System.out.println(x.name + " " + x.address));
but as you might note, it’s still not as concise as a structural type. Declaring the result variable using var is the only way to refer to the type we can not refer to by name. This requires Java 10 or newer and is limited to the method’s scope.
It’s also important to keep in mind that inner classes can create memory leaks due to capturing a reference to the surrounding this. In the example, each object also captures the value of p used for its initialization. The record doesn’t have these problems and further, it automatically gets suitable equals, hashCode, and toString implementations, which implies that printing the list like System.out.println(result); or transferring it to a set like new HashSet<>(result) will have meaningful results.
Also, it’s much easier to move the record’s declaration to a broader scope.
Prior to Java 10, lambda expressions are the only Java feature that supports declaring variables of an implied type, which could be anonymous. E.g., the following would work even in Java 8:
List<String> result = lst.stream()
.map(p -> new Object(){ String address = p.getAddress(); String name = p.getName();})
.filter(anon -> anon.name.startsWith("ti"))
.map(anon -> anon.address)
.collect(Collectors.toList());
It seems that you want to transform your Person with 3 properties to a Holder that has 2 properties. And that is a simple map operation:
lst.stream().map(p -> new AbstractMap.SimpleEntry(p.address, p.name))
.collect(Collectors.toList());
This is collecting your entries to SimpleEntry that is just a Holder for two values. If you need more then two, you are out of luck - you will need to create your own holder(class).
If you know which attributes to select and this does not change, I would recommend writing a small class with that subset of Person's attributes. You can then map every person to an instance of that class and collect them into a list:
Stream.of(new Person(1, "a", "aa"), new Person(2, "b", "bb"), new Person(3, "b", "bbb"),
new Person(4, "c", "aa"), new Person(5, "b", "bbb"))
.filter(person -> true) // your filter criteria goes here
.map(person -> new PersonSelect(person.getName(), person.getAddress()))
.collect(Collectors.toList());
// result in list of PersonSelects with your name and address
If the set of desired attributes varies, you could use an array instead. It will look more similar to your C# code, but does not provide type safety:
Stream.of(new Person(1, "a", "aa"), new Person(2, "b", "bb"), new Person(3, "b", "bbb"),
new Person(4, "c", "aa"), new Person(5, "b", "bbb"))
.filter(person -> true)
.map(person -> new Object[] {person.getName(), person.getAddress()})
.collect(Collectors.toList())
.forEach(p -> System.out.println(Arrays.asList(p)));
// output: [a, aa], [b, bb], [b, bbb], [c, aa], [b, bbb]
If you want to create a list of new Person instances you first should provide a constructor, e.g. like this:
class Person {
public int id;
public String name;
public String address;
public Person( int pId, String pName, String pAddress ) {
super();
id = pId;
name = pName;
address = pAddress;
}
}
Then you could use the stream:
List<Person> lst = new ArrayList<>();
lst.add(new Person(1, "Pava", "India" ));
lst.add(new Person( 2, "tiwari", "USA" ) );
//since id is an int we can't use null and thus I used -1 here
List<Person> result = lst.stream().map(p -> new Person(-1, p.name, p.address)).collect(Collectors.toList());
If you want to filter persons then just put a filter() in between stream() and map():
List<Person> result = lst.stream().filter(p -> p.name.startsWith( "P" )).map(p -> new Person( -1, p.name, p.address )).collect(Collectors.toList());

Categories