Group by n fields in Java using stream API - java

Need to group List<Object> based upon N property fields, This fields are decided at run time. How can I achieve this ?
Group by multiple field names in java 8
Referred this question, it is using fixed number of fields.
For Example:
Person{ int age, String city, Date doj, double salary}
record1: 25, NYC, 02/25/2018, 50000
record2: 25, MEX, 02/25/2017, 70000
record3: 26, MEX, 02/25/2017, 80000
GroupBy(city, doj)
Record1: = MEX, 02/25/2017, 150000
Record2: = NYC, 02/25/2018, 50000
Salary will added.
I am storing result in Map<Object, List<Object>>
I have achieved most of it. Only problem I am facing is how to alter key in groupingBy.
Collectors.groupingBy( date ) : second iteration will mess data for all city which is to be grouped by city+date.This will be solved if I can alter the key to be City+Date
How can I alter my key in second iteration Collectors.groupingBy( date )

Here is a complete example:
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
public class Grouping {
static final class Person {
private final int age;
private final String city;
private final String doj;
private final double salary;
public Person(int age, String city, String doj, double salary) {
this.age = age;
this.city = city;
this.doj = doj;
this.salary = salary;
}
public int getAge() {
return age;
}
public String getCity() {
return city;
}
public String getDoj() {
return doj;
}
public double getSalary() {
return salary;
}
#Override
public String toString() {
return "Person{" +
"age=" + age +
", city='" + city + '\'' +
", doj='" + doj + '\'' +
", salary=" + salary +
'}';
}
}
enum Property {
AGE {
#Override
protected Object extractValue(Person person) {
return person.getAge();
}
},
CITY {
#Override
protected Object extractValue(Person person) {
return person.getCity();
}
},
DOJ {
#Override
protected Object extractValue(Person person) {
return person.getDoj();
}
};
protected abstract Object extractValue(Person person);
public PropertyValue toValue(Person person) {
return new PropertyValue(this, extractValue(person));
}
}
static final class PropertyValue {
private final Property property;
private final Object value;
public PropertyValue(Property property, Object value) {
this.property = property;
this.value = value;
}
#Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PropertyValue that = (PropertyValue) o;
return property == that.property &&
Objects.equals(value, that.value);
}
#Override
public int hashCode() {
return Objects.hash(property, value);
}
#Override
public String toString() {
return "PropertyValue{" +
"property=" + property +
", value=" + value +
'}';
}
}
private static List<PropertyValue> createGroupingKey(List<Property> properties, Person person) {
return properties.stream().map(property -> property.toValue(person)).collect(Collectors.toList());
}
public static void main(String[] args) {
List<Person> persons = Arrays.asList(
new Person(25, "NYC", "02/25/2018", 50000),
new Person(25, "MEX", "02/25/2017", 70000),
new Person(26, "MEX", "02/25/2017", 80000)
);
// TODO ask the user, rather than hardcoding
List<Property> groupingProperties = Arrays.asList(Property.CITY, Property.DOJ);
Map<List<PropertyValue>, Double> salaryAggregatedByChosenProperties =
persons.stream()
.collect(Collectors.groupingBy(p -> createGroupingKey(groupingProperties, p),
Collectors.summingDouble(Person::getSalary)));
System.out.println("salaryAggregatedByChosenProperties = " + salaryAggregatedByChosenProperties);
}
}
What it does:
ask the user which properties should be used for grouping (this is actually not done, but simulated, since that's not the core of your question). You get back a List<Property>, containing (for example) the properties CITY and DOJ
You transform each person into a grouping key, of type List<PropertyValue>, so, the first person will be transformed into [NYC, 02/25/2018], whereas the second and third ones will both be be transformed into [MEX, 02/25/2017] (and thus have the same key).
You group the persons by their key
You sum the salaries of the persons of the same group

Using JB Nizet suggested solution, I have put together an entire working solution in which you can group by n number of fields.
Grouping on any number of fields are possible
Result is independent of grouping field order
User can define aggregation stragery
This nested property will help us store the key for our grouping.
public class NestedProperty {
private final Field property;
private final Object value;
}
Field here is a simple object which will be feed at runtime. We can have better alternative to decide its type.
public class Field{
String name;
Class type;
}
This interface should be implementation by POGO to define what is the aggregation strategy.
public interface Aggregatable<T> {
public void add(T o);
}
Then using NestedProperty object we group the records till n-1 fields using streams.groupby function.
Map<List<NestedProperty>, List<T>> aggregatedRecords = objects.stream()
.collect(Collectors.groupingBy(r -> createGroupingKey(Nminus1fields, r), Collectors.toList()));
private static List<NestedProperty> createGroupingKey(java.util.List<Field> fields, Object r) {
return fields.stream().map(p -> p.toValue(r, p)).collect(Collectors.toList());
}
Then we can run the main aggregation method
List<?> result = objects.stream().filter( r -> r!=null )
.collect(Collectors.groupingBy(
record -> {
try {
return cast.cast(PropertyUtils.getNestedProperty(record, field.getName()));
}catch(Exception e) {
System.out.println("Property not found.");
}
return null;
}))
.entrySet().stream()
.map( e -> e.getValue().stream()
.reduce((f1, f2) -> {
try {
return (T) add(classDefination, f1, f2);
} catch (Exception e1) {
System.out.println("Error is method add()");
}
return null;
})
).map(f -> f.get())
.collect(Collectors.toList());
Please refer to answer in below link:
http://www.unbounded.in/group-by-n-fields-in-java-like-sql-using-streams-api/

Related

How to return a Collector in Java, that forms a special printable sting?

I have been trying to write a method called printableStringCollector, which basically returns a collector that forms a special printable String as described below -
Printable String spec
It is a multi-line String containing a formatted table of all results and total values and marks.
The format of your table should meet the following requirements:
Headers are required.
Column width depends on values lengths.
Rows are sorted by students' last names.
Columns are sorted by the names of tasks.
Double values are displayed with 2 digits after the decimal point.
Values of the column "Mark" are defined by the average total score.
Example:
Student........|Phalaxing |Shieldwalling |Tercioing |Wedging |Total |Mark |
Eco Betty......|0 ........|83............|89........|59......|57.75 |F ...|
Lodbrok Johnny |61 .......|92............|67........|0.......|55.00 |F....|
Paige Umberto..|75....... |94............|0.........|52......|55.25 |F....|
Average........|45.33.....|89.67.........|52.00.....|37.00...|56.00 |F....|
wrote the dots for better visual representation
Can anyone help me implement this method?
public Collector<CourseResult, ?, String> printableStringCollector() {
}
CourseResult class:
public class CourseResult {
private final Person person;
private final Map<String, Integer> taskResults;
public CourseResult(final Person person, final Map<String, Integer> taskResults) {
this.person = person;
this.taskResults = taskResults;
}
public Person getPerson() {
return person;
}
public Map<String, Integer> getTaskResults() {
return taskResults;
}
}
Person class:
public class Person {
private final String firstName;
private final String lastName;
private final int age;
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public int getAge() {
return age;
}
#Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final Person person = (Person) o;
return age == person.age &&
Objects.equals(firstName, person.firstName) &&
Objects.equals(lastName, person.lastName);
}
#Override
public int hashCode() {
return Objects.hash(firstName, lastName, age);
}
#Override
public String toString() {
return new StringJoiner(", ", Person.class.getSimpleName() + "[", "]")
.add("firstName='" + firstName + "'")
.add("lastName='" + lastName + "'")
.add("age=" + age)
.toString();
}
}
Most of the time, when I need to create a custom collector, I create a new class for it and give it methods accumulate, combine and finish, to match the operators in a collector. That class can do whatever is needed. For instance, where the finish is optional:
Collector<T, ?, R> collector = Collector.of(
MyCollector::new,
MyCollector::accumulate,
MyCollector::combine,
MyCollector::finish);
In your case you will have to stream twice though, as the collector needs to have seen the length of each student name before it can output anything. The collector would store its CourseResult instances, as well as the maximum length(s). It would then stream again, and use String.format to do all the hard work in adding padding (e.g. "%" + maxNameLength + "s | %10s | ...").
The finish method would look like this (simplified):
public String finish() {
String header = "...\n"; // use same padding
String pattern = "...";
return results.stream()
.map(result -> String.format(pattern, result.getPerson().getName(), ...)
.collect(Collectors.joining("\n", header, ""));
}
I wouldn't use the exact same pattern for the header, because the rows contain numbers where the header does not.

Get duplicated items with specific format from Java Stream

I'm new with Java streams and I'm playing around with them right now. Given that I receive a list of persons I want to detect which of them are duplicated and print them as "{Id1} is duplicated with {Id3}{Id4} and its duplicated values are Name, Lastname, FamilyName and Birthday"
So this is my person class, I have already override the equals method in order to get the duplicated based on my criterias
public class Person {
private int id;
private String name;
private String familyName;
private String birthday;
private String city;
public Person(int id, String name, String familyName, String birthday, String city) {
this.id = id;
this.name = name;
this.familyName = familyName;
this.birthday = birthday;
this.city = city;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getFamilyName() {
return familyName;
}
public void setFamilyName(String familyName) {
this.familyName = familyName;
}
public String getBirthday() {
return birthday;
}
public void setBirthday(String birthday) {
this.birthday = birthday;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
#Override
public int hashCode() {
return Objects.hash( name,familyName,birthday,city);
}
#Override
public boolean equals(Object obj) {
if (this == obj) {
return false;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Person other = (Person) obj;
if (!Objects.equals(name, other.name)) {
return false;
}
if (!Objects.equals(familyName, other.familyName)) {
return false;
}
if (!Objects.equals(birthday, other.birthday)) {
return false;
}
return true;
}
}
Then, I'm getting the list of duplicates in the following method
personList.stream()
.filter(p -> personList.contains(p))
.collect(Collectors.toList()).forEach(p-> {
System.out.println(p.getId() + " " + p.getName() + " " + p.getFamilyName() + " " + p.getBirthday());
});
It prints the following:
2 Andres Gonzalez 12/4/1990
4 Maureen Perez 15/07/92
7 Andres Gonzalez 12/4/1990
9 Maureen Perez 15/07/92
11 Maureen Perez 15/07/92
As you can see ID's 2 and 7 are duplicated and also 4,9 and 11 are duplicated and those ones are the ones that I need to print in that format but I don't know how to do it with streams so far.
First of all, you should fix your hashCode() implementation to match your equals. If two objects are equal, they must have the same hashCode().
Now, your Stream pipeline returns all elements, since your filter's Predicate will always return true.
Instead, you can group equal elements of the List:
Map<Person,List<Integer>> grouped =
personList.stream()
.collect(Collectors.groupingBy(Function.identity(),
Collectors.mapping(Person::getId,
Collectors.toList())));
Now, for each Person, you have an associated List of identifiers.
You can iterate this Map and print the Persons having Lists with size > 1.
For example:
personList.stream()
.collect(Collectors.groupingBy(Function.identity(),
Collectors.mapping(Person::getId,
Collectors.toList())));
.entrySet()
.stream()
.filter(e -> e.getValue().size() > 1)
.forEach(e -> System.out.println(e.getKey().getId() + " " + e.getKey().getName() + " " + e.getKey().getFamilyName() + " " + e.getKey().getBirthday() + " " + e.getValue()));

Java8 Collecting data from 2 different object streams to a sorted list of 3rd object

So here's the problem. I have three classes as below:
class ObjectClassA
{
private final long id;
private final String name;
private final boolean isReadOnly;
//<some more fields>
long getId()
{
return this.id;
}
String getName()
{
return this.name;
}
boolean isReadOnly()
{
return this.isReadOnly;
}
}
class ObjectClassB
{
private final long id;
private final String location;
private final boolean isReadOnly;
//<some more fields>
long getId()
{
return this.id;
}
String getLocation()
{
return this.location;
}
boolean isReadOnly()
{
return this.isReadOnly;
}
}
and
class ObjectClassC
{
private final String location;
private final boolean isReadOnly;
private final String location;
String getName()
{
return this.name;
}
boolean isReadOnly()
{
return this.isReadOnly;
}
String getLocation()
{
return this.location;
}
}
I have 2 maps -
Map<Id,ObjectClassA> mapObjectClassA
and
Map<Id,ObjectClassB> mapObjectClassB
Both these maps are of the same size. The Id keys are common to both maps. The aim is to iterate over either maps to create a sorted (by ObjectClassC.name) List<ObjectClassC> objects such that:
ObjectClassC.name = ObjectClassA.name
ObjectClassC.isReadOnly = ObjectClassA.isReadOnly || ObjectClassB.isReadOnly
ObjectClassC.location = ObjectClassB.location
This is the logic that I have right now is as follows:
final List<ObjectClassC> list =
mapObjectClassA.values()
.stream()
.map(a -> {
new ObjectClassC(a.getName(),
a.isReadOnly() || mapObjectClassB.get(a.getId).isReadOnly(),
mapObjectClassB.get(a.getId).getLocation())
})
.sorted(Comparator.comparing(ObjectClassC::getName))
.collect(Collectors.<ObjectClassC> toList());
My mapping and collection are working fine, but I don't get a sorted collection of ObjectClassC objects. Could someone please point out where I'm going wrong?
Not so nice, but works.
public class MapMerge_38837365 {
public static void main(String[] args) {
Map<Integer, A> as = new HashMap();
as.put(1, new A("Anna"));
as.put(2, new A("Maria"));
as.put(3, new A("Eva"));
Map<Integer, B> bs = new HashMap();
bs.put(1, new B("Adam"));
bs.put(2, new B("Edward"));
bs.put(3, new B("Jon"));
Stream.concat(
as.entrySet().stream(),
bs.entrySet().stream()
).collect(Collectors.groupingBy(Map.Entry::getKey))
.entrySet().stream()
.map(e -> e.getValue())
.map(e -> {
if (e.get(0).getValue() instanceof A)
return new AB((A) e.get(0).getValue(), (B) e.get(1).getValue());
return new AB((A) e.get(1).getValue(), (B) e.get(0).getValue());
})
.map(ab -> new C(ab.a.name+" " +ab.b.name))
.forEach(System.out::println);
}
}
"The high magic" happens in Stream.concat then groupingBy. In this two lines we create stream of all entries from both maps. Then we group them by key to map. Elements with the same key (id) will go to one List<Object>. Next step is create new stream from grouping map entrySet().stream().
Then we extract value - entries with the same Id > map(e -> e.getValue()).
Next step is to build AB objects and could be omitted but I want to extract type check and cast to another function. Now we have pseudo pair AB, that is very nice candidate for create C class map(ab -> new C(ab.a.name+" " +ab.b.name)).
Classes A, B, C and AB:
class A {
final String name;
A(String name) {
this.name = name;
}
#Override
public String toString() {
return "A{" +
"name='" + name + '\'' +
'}';
}
}
class B {
final String name;
B(String name) {
this.name = name;
}
#Override
public String toString() {
return "B{" +
"name='" + name + '\'' +
'}';
}
}
class C {
final String name;
C(String name) {
this.name = name;
}
#Override
public String toString() {
return "C{" +
"name='" + name + '\'' +
'}';
}
}
class AB {
final A a;
final B b;
AB(A a, B b) {
this.a = a;
this.b = b;
}
#Override
public String toString() {
return "AB{" +
"a=" + a +
", b=" + b +
'}';
}
}

How to make two different comparing criteria for a class in java?

I have an assignment to make a Country class with two fields(name, capital) which at first has to be compared with another Country based on the name. Afterwards without changing anything I have to make another test which cares only about the capital regarding comparison. How to do two different comparison methods in the same class ?
This is what I've done so far :
Country class :
public class Country implements Comparable {
private String name;
private String capital;
public Country(String name, String capital) {
this.name = name;
this.capital = capital;
}
public String getName() {
return name;
}
public String getCapital() {
return capital;
}
#Override
public String toString() {
return "Country{" +
"name='" + name + '\'' +
", capital='" + capital + '\'' +
'}';
}
public static final Comparator<Country> nameComparator = (country, secondCountry) -> country.getName().compareTo(secondCountry.getName());
public static final Comparator<Country> capitalComparator = (country, secondCountry) -> country.getCapital().compareTo(secondCountry.getCapital());
#Override
public int compareTo(Object o) {
return 0;
}
}
Method which checks if countries are sorted correctly:
public boolean isSorted(List<Country> countryList) {
boolean sorted = Ordering.natural().isOrdered(countryList);
return sorted;
}
And my test:
#org.junit.Test
public void testIfTheListIsSorted () {
CountryDAO countryDAO = new CountryDAO();
List<Country> countryList = countryDAO.getCountryList();
Collections.sort(countryList, Country.nameComparator);
assertTrue(countryDAO.isSorted(countryList));
}
What do I have to change in order to compare at my choice , whether by name or by capital ? And both ways being testable. Thanks in advance..

Any way to write generic sorting code for object fields in a List?

In an interview i came across this question.
Write a class which accepts a field of a bean class and sorts a list containing bean objects according to the field passed.
1) What should be the technique used for sorting?
I answered Comparator.
2) I don't want many Comparator classes created for each field. Can you write a generic Comparator which works for all fields.
Below is my code.
Please let me know if this is a correct way of doing or is there better way to do this. I request you to correct me if i am wrong.
public class GenericComparatorDemo {
static List<Employee> al = new ArrayList<Employee>();
static{
al.add(new Employee(45, "Vijay", "Bangalore", "Banking", 88, 99999));
al.add(new Employee(13, "Manoz", "Chennai", "Insurance", 48, 28000));
al.add(new Employee(79, "Ajay", "Hyderabad", "Real Estate", 54, 24000));
al.add(new Employee(21, "Sindu", "Noida", "Analyst", 89, 99998));
al.add(new Employee(67, "Honey", "Mumbai", "Social", 88, 111111));
al.add(new Employee(12, "Lucky", "Mysore", "Social", 86, 99997));
}
/**
* #param args
*/
public static void main(String[] args) {
Scanner scn = new Scanner(System.in);
System.out.println("Please enter the field on which you want to sort employee's...");
final String input = scn.nextLine();
if(null != input && !"".equals(input)){
Collections.sort(al, new Comparator<Employee>() {
#Override
public int compare(Employee o1, Employee o2) {
if("id".equals(input)){
return (o1.getId() < o2.getId()) ? -1 : ((o1.getId() == o2.getId()) ? 0 : 1);
}else if("name".equals(input)){
return o1.getName().compareTo(o2.getName());
}else if("location".equals(input)){
return o1.getLocation().compareTo(o2.getLocation());
}else if("department".equals(input)){
return o1.getDepartment().compareTo(o2.getDepartment());
}else if("rewardPoints".equals(input)){
return (o1.getRewardPoints() < o2.getRewardPoints()) ? -1 : ((o1.getRewardPoints() == o2.getRewardPoints()) ? 0 : 1);
}else if("salary".equals(input)){
return (o1.getSalary() < o2.getSalary()) ? -1 : ((o1.getSalary() == o2.getSalary()) ? 0 : 1);
}else{
return 0;// when proper field is not entered sorting will not happen
}
}
});
}else{
System.out.println("Please enter valid employee field to sort employee's...");
}
for(Employee alObj:al){
System.out.println("\n" + alObj.toString());
}
}
}
/// Employee Class ///
public class Employee {
private long id;
private String name;
private String location;
private String department;
private int rewardPoints;
private double salary;
public Employee(long id, String name, String location, String department,
int rewardPoints, double salary) {
this.id = id;
this.name = name;
this.location = location;
this.department = department;
this.rewardPoints = rewardPoints;
this.salary = salary;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public String getDepartment() {
return department;
}
public void setDepartment(String department) {
this.department = department;
}
public int getRewardPoints() {
return rewardPoints;
}
public void setRewardPoints(int rewardPoints) {
this.rewardPoints = rewardPoints;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
#Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", location="
+ location + ", department=" + department + ", rewardPoints="
+ rewardPoints + ", salary=" + salary + "]";
}
}
//// According to comments from tieTYT and radai. I have made the following changes. Please correct me if any thing is wrong ////
public class GenericComparatorReflectionDemo {
static List<Employee> al = new ArrayList<Employee>();
static{
al.add(new Employee(45, "Vijay", "Bangalore", "Banking", 88, 99999));
al.add(new Employee(13, "Manoz", "Chennai", "Insurance", 48, 28000));
al.add(new Employee(79, "Ajay", "Hyderabad", "Real Estate", 54, 24000));
al.add(new Employee(21, "Sindu", "Noida", "Analyst", 89, 99998));
al.add(new Employee(67, "Honey", "Mumbai", "Social", 88, 111111));
al.add(new Employee(12, "Lucky", "Mysore", "Social", 86, 99997));
}
/**
* #param args
*/
public static void main(String[] args) {
Scanner scn = new Scanner(System.in);
System.out.println("Please enter the field on which you want to sort employee's...");
final String input = scn.nextLine();
if(null != input && !"".equals(input)){
Collections.sort(al, new Comparator<Employee>() {
#Override
public int compare(Employee o1, Employee o2) {
try {
Field employeeField = Employee.class.getDeclaredField(input);
employeeField.setAccessible(true);
Comparable employeeFieldValue1 = (Comparable)employeeField.get(o1);
Comparable employeeFieldValue2 = (Comparable)employeeField.get(o2);
return employeeFieldValue1.compareTo(employeeFieldValue2);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
// when proper field is not entered sorting or any exception occurs
return 0;
}
}
});
}else{
System.out.println("Please enter valid employee field to sort employee's...");
}
for(Employee alObj:al){
System.out.println("\n" + alObj.toString());
}
}
}
This may be subjective as I can't read the interviewer's mind. But if I were you, I'd use reflection to find the field. If the field is Comparable, then use that interface. Otherwise, you'll have to ask the interviewer how you want it to work on certain types of fields.
The problem with your code is it's very specific to the current class. If a new field gets added, it won't be able to sort on that unless you write and compile new code. It also only works on this class. Using reflection, you could have it work on almost any class.
Yes, you can write a general purpose comparator using reflection. Once you have the field name you can retrieve the field value using reflection. After you do that you can use the fact that all the basic value holding classes in java (things like Boolean, Integer, String, uuid, date etc) are comparable, cast the values you got using reflection to Comparable, and compare them
To do this neatly you should abstract the concept of getting the value to compare into an interface (or function in Java 8) as follows:
This function will create you a Comparator that uses the getter Function to read the value from the a bean:
public static <T> Comparator<T> createComparator(Function<T, R extends Comparable> getter)
{
return (obj1, obj2) -> getter.apply(obj1).compareTo(getter.apply(obj2);
}
That can be used like this:
Collections.sort(employees, createComparator((employee) -> employee.getName()));
If you're set on using String names to read the field then use just use a reflective implementation of the getter function like this:
private Function<Employee, Comparable> reflectiveGetter(String fieldName) throws NoSuchFieldException
{
Field field = Employee.class.getDeclaredField(fieldName);
field.setAccessible(true);
return (employee) ->
{
try
{
return (Comparable)field.get(employee);
}
catch (IllegalAccessException e)
{
throw new RuntimeException(e);
}
}
}
That can be used like this:
Collections.sort(employees, reflectiveGetter("name"));
The exact same thing can be acheived in any version of Java using interfaces only it's a bit more verbose.

Categories