Reduce list of object into Immutable map with lambda - java

I have a list of Person object. All person has a unique id, but the person's name can be the same.
Person {
String id,
String name,
}
I want to convert this array of persons into ImmutableMap<String, ImmutableSet<String>>. The key of the map should be the user's name, the immutable set contains the ids of specific user's name.
I know how to do it using HashMap and HashSet:
for (person : personList) {
String id = person.id;
String name = person.name;
if (!hashMap.containsKey(name)) {
hashMap.put(name, new HashSet<String>());
}
hashMap.get(name).add(id);
}
I want to know how to do it using ImmutableMap, ImmutableSet with lambda.

Here is one possible solution.
First, generate some data. Three names and 18 ids. Put them in a list.
Random r = new Random();
int[] ids = r.ints(1000, 1, 1000).distinct().limit(18).toArray();
int id = 0;
List<Person> people = new ArrayList<>();
for (int i = 0; i < 6; i++) {
for (String name : List.of("Bob", "Joe", "Mary")) {
people.add(new Person(name, ids[id++]));
}
}
Now creating the map.
Use groupingBy to create a key pointing to a collection. The key is the
name and the collection is the map.
The collection(a set) holds the ids
Map<String, Set<Integer>> nameToID =
Collections.unmodifiableMap(people.stream().collect(
Collectors.groupingBy(Person::getName, Collectors.mapping(
Person::getID, Collectors.toUnmodifiableSet()))));
Print them.
nameToID.entrySet().forEach(
e -> System.out.println(e.getKey() + " -> " + e.getValue()));
}
}
Here is the Person class with some additional methods and a constructor.
class Person {
String name;
int id;
public Person(String name, int id) {
this.name = name;
this.id = id;
}
public String getName() {
return name;
}
public int getID() {
return id;
}
public String toString() {
return "(" + name + "," + id + ")";
}
}

Related

Unable to get desired output in "Predicate" "and" of Java

I have the following class Person -
Person.java -
public class Person {
private int id;
private String department;
private double salary;
public Person(int id, String department, double salary) {
this.id = id;
this.department = department;
this.salary = salary;
}
public String getDepartment() {
return department;
}
public double getSalary() {
return salary;
}
#Override
public String toString() {
return "Person{" +
"id=" + id +
", department='" + department + '\'' +
", salary=" + salary +
'}';
}
}
It has the fields -
id, department, salary
Now I have first predicate -
Predicate<List<Person>> hasSalaryOf40k = list -> {
boolean myReturn = false;
Iterator<Person> iterator = list.iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
double salary = person.getSalary();
if (salary == 40000) {
myReturn = true;
break;
}
}
return myReturn;
};
Here, I want to filter out those lists having persons with salary as 40K.
Second predicate -
Predicate<List<Person>> isDeveloper = list -> {
boolean myReturn = false;
Iterator<Person> iterator = list.iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
String department = person.getDepartment();
if (department.equals("Developer")) {
myReturn = true;
break;
}
}
return myReturn;
};
Here, I want to filter out those lists having persons with department as 'developer'
Third predicate -
Predicate<List<Person>> hasSalaryOf40kAndIsDeveloper = list ->
hasSalaryOf40k.and(isDeveloper).test(list);
Here, I want to filter out those lists having persons with both salary as 40K and department as "developer"
Now I have the following two lists -
List<Person> list1 = new ArrayList<>(List.of(
new Person(1, "Developer", 35000),
new Person(2, "Accountant", 40000),
new Person(3, "Clerk", 20000),
new Person(4, "Manager", 50000)
));
List<Person> list2 = new ArrayList<>(List.of(
new Person(1, "Developer", 40000),
new Person(2, "Accountant", 35000),
new Person(3, "Clerk", 22000),
new Person(4, "Manager", 55000)
));
The list1 does not match the desired criteria while list2 matches the desired criteria.
Now I call the predicate method test -
System.out.println(hasSalaryOf40kAndIsDeveloper.test(list1));
System.out.println(hasSalaryOf40kAndIsDeveloper.test(list2));
Output -
true
true
Desired output -
false
true
Where am I going wrong and how to correct my code?
You're applying the predicate to the whole list and not each element of the list, so it's true that the list contains a developer and its true that the list contains a salary over 40k. You need to apply the predicate to the Person object rather than the List<Person> object

Get all first value of List<Object[]>

how can I retrieve only the pet types using lambda expression? I want a new list containing only "dog", "cat", etc,
I have list:
List<Object[]> list;
in this list I have structure:
list.get(0) returns ("dog", 11)
list.get(1) returns ("cat", 22)
etc.
how can I retrieve only the pet types using lambda expression? I want a new list containing only "dog", "cat", etc,
An easy way is the use of the stream api:
List firstElements = list.stream().map(o -> o[0]).collect(Collectors.toList());
It is as simple as using map and collect.
private void test(String[] args) {
List<Animal> list = new ArrayList<>();
list.add(new Animal("dog",11));
list.add(new Animal("cat",22));
List<String> names = list.stream()
// Animal -> animal.type.
.map(a -> a.getType())
// Collect into a list.
.collect(Collectors.toList());
System.out.println(names);
}
I used Animal as:
class Animal {
final String type;
final int age;
public Animal(String type, int age) {
this.type = type;
this.age = age;
}
public String getType() {
return type;
}
public int getAge() {
return age;
}
#Override
public String toString() {
return "Animal{" +
"type='" + type + '\'' +
", age=" + age +
'}';
}
}

Build a sorted list based on multiple parameters?

I have three arrays
String[] persons = {"jack","james","hill","catnis","alphonso","aruba"};
int[] points = {1,1,2,3,4,5};
int[] money = {25,66,24,20,21,22};
The nth position in all three arrays belong to the same entity, for eg:-
persons[0] == points[0] == money[0] i.e jack has 1 point and 25 bucks.
I want to build a list that sorts person alphabetically(ascending) , if the starting letter is same , then it should check points(descending) and if those are same too then it must check the money(descending).
The final list after sorting should be {aruba , alphonso , catnis , hill , james , jack}.
So I think you want something like this:
public class Person {
String name;
int points;
int money;
public Person(String name, int points, int money) {
this.name = name;
this.points = points;
this.money = money;
}
// getters
}
Then create a List<Person> with the data you have (e.g., new Person("jack", 1, 25)). And then sort them:
Collections.sort(persons, (person1, person2) -> {
// could be written more concisely, but this should make things clear
char letter1 = person1.getName().charAt(0);
char letter2 = person2.getName().charAt(0);
if (letter1 != letter2) {
return letter1 - letter2;
}
int points1 = person1.getPoints();
int points2 = person2.getPoints();
if (points1 != points2) {
return points2 - points1; // notice the order is reversed here
}
int money1 = person1.getMoney();
int money2 = person2.getMoney();
if (money1 != money2) {
return money2 - money1;
}
return 0; // unless you want to do something fancy for tie-breaking
});
That will give you a sorted List<Person> according to your criteria.
If you're up to something quick and dirty:
Comparator<Integer> cName = (i, j) -> Character.compare( persons[i].charAt(0), persons[j].charAt(0));
Comparator<Integer> cPoints = (i, j) -> Integer.compare( points[i], points[j]);
Comparator<Integer> cMoney = (i, j) -> Integer.compare( money[i], money[j]);
List<String> l =
IntStream.range(0, persons.length).boxed()
.sorted( cName.thenComparing(cPoints.reversed()).thenComparing(cMoney.reversed()) )
.map( i -> persons[i] )
.collect(Collectors.toList());
System.out.println(l);
The first 3 lines use lambdas to define comparators based on arrays indexes.
The following line uses streams:
Create an int stream of indexes from 0 to persons.length-1
Sort indexes of the stream based on the sequence of comparators
Map sorted indexes to person names
Collect it into a List
Ain't lambda and streams cool?
If you can have a Person model:
final class Person {
private final String name;
private final int points;
private final int money;
public Person(final String name, final int points, final int money) {
this.name = name;
this.points = points;
this.money = money;
}
// getters and setters (if you want)
#Override
public String toString() {
final StringBuffer sb = new StringBuffer("Person {")
.append("name=")
.append(name)
.append(", points=")
.append(points)
.append(", money=")
.append(money)
.append('}');
return sb.toString();
}
}
Then you could do something like this:
public static void main(final String... args) throws Exception {
Person[] persons = new Person[6]; // Use a List (if you can)
persons[0] = new Person("jack", 1, 25);
persons[1] = new Person("james", 1, 66);
persons[2] = new Person("hill", 2, 24);
persons[3] = new Person("catnis", 3, 20);
persons[4] = new Person("alphonso", 4, 21);
persons[5] = new Person("aruba", 5, 22);
System.out.printf("persons = %s%n%n", Arrays.toString(persons));
System.out.printf("Person[0] = %s%n%n", persons[0]);
Collections.sort(Arrays.asList(persons), (c1, c2) -> {
final int charComp = Character.compare(c1.name.charAt(0), c2.name.charAt(0));
if (0 == charComp) {
final int pointsComp = Integer.compare(c2.points, c1.points);
if (0 == pointsComp) { return Integer.compare(c2.money, c1.money); }
return pointsComp;
}
return charComp;
});
// The collection was modified at this point because of the "sort"
System.out.printf("persons = %s%n", Arrays.toString(persons));
}
Results:
persons = [Person {name=jack, points=1, money=25}, Person {name=james,
points=1, money=66}, Person {name=hill, points=2, money=24}, Person
{name=catnis, points=3, money=20}, Person {name=alphonso, points=4,
money=21}, Person {name=aruba, points=5, money=22}]
Person[0] = Person {name=jack, points=1, money=25}
persons = [Person {name=aruba, points=5, money=22}, Person
{name=alphonso, points=4, money=21}, Person {name=catnis, points=3,
money=20}, Person {name=hill, points=2, money=24}, Person {name=james,
points=1, money=66}, Person {name=jack, points=1, money=25}]
A more compact sort (but a little bit less efficient since you have to run all comparisons upfront):
Collections.sort(Arrays.asList(persons), (c1, c2) -> {
final int names = Character.compare(c1.name.charAt(0), c2.name.charAt(0));
final int points = Integer.compare(c2.points, c1.points);
final int money = Integer.compare(c2.money, c1.money);
return (0 == names) ? ((0 == points) ? money : points) : names;
});
Similar to EvanM's answer, you should group the three pieces of data into a single class.
public class Person {
private String name;
public String getName() { return name; }
private int points;
public int getPoints() { return points; }
private int money;
public int getMoney() { return money; }
}
Then you could sort them like so:
List<Person> persons = ...;
persons.sort(Comparator
.comparing(p -> p.getName().charAt(0))
.thenComparing(Comparator.comparing(Person::getPoints).reversed())
.thenComparing(Comparator.comparing(Person::getMoney) .reversed())
);

How can I label the duplicated elements?

I got a name list that needs to convert to the result that person with same last name are all labeled. For example:
origin list:
JayReese
ClaraSmith
JohnSmith
output:
JayReese
ClaraSmith1
JohnSmith2
The code of Person class are written below, how can I compare all the lastName and when there are duplicated lastName, unique index is added to each? What method should be added?
I'd really appreciate any input and or help. Thank you very much.
import java.util.*;
public class Person implements Comparable<Person> {
private String firstName;
private String lastName;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String toString() {
return lastName + firstName;
}
}
First, either edit the Person class or create a new class that has an index field that can be set. Comparability is completely unnecessary for your usecase.
public class Person {
private String firstName;
private String lastName;
public int index;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String toString() {
return lastName + firstName + (index != 0 ? index : "");
}
}
You can use a HashMap<String, Integer> to add the numbering to the last name, like this:
public static void numberDuplicateLastNames(List<Person> people) {
HashMap<String, Integer> duplicatedLastNames = new HashMap<>();
for(Person p : people) {
if(! duplicatedLastNames.containsKey(p.lastName)) {
duplicatedLastNames.put(p.lastName, 1);
p.index = 1;
} else {
int i = duplicatedLastNames.get(p.lastName) + 1;
duplicatedLastNames.put(p.lastName, i);
p.index = i;
}
}
//Remove index from the people who don't have a duplicate last name
for(Person p : people) {
if (duplicatedLastNames.get(p.lastName) == 1) {
p.index = 0;
}
}
}
You could do a compare method like this
public compairLastName(String LastName){
if(lastName.equals(LastName){
System.out.println("These last names are the same");
}else{
System.out.println("These last names are not the same");
}
So you asked about labelling (and by extension sorting), but refer to uniquely identifying - here is an answer to both :)
For Sorting
Real life programs deal with that every day, just ensure your comparable code by default sorts by 2 attributes
LastName
FirstName
If they compare in that order, you should end up with:
XavierAllen
JayReese
BillySmith
ClaraSmith
JohnSmith
To compare multiple attributes, I would refer you to this stackoverflow topic, which shows how to do both single and multiple comparisons on a Person object with the same field names even ; )
How to compare objects by multiple fields
For Unique Referance
Also, if you were concerned about uniquely identifying the Person outside of simple comparison sorting, then you would add an int field (or guid, or whatever your flavour) that would hold a unique value
Basically same as a Database PK - you would never use a persons name as a PK in a database, so your Person ideally should have a property like that too.
If you wanted to add this PK to your toString() then go for it
import java.util.*;
public class Person implements Comparable<Person> {
private int personID;
private String firstName;
private String lastName;
public Person(String firstName, String lastName, int personID) {
this.firstName = firstName;
this.lastName = lastName;
this.personID = personID;
}
public int getID(){
return this.personID;
}
public String toString() {
return this.personID + ": " + this.lastName + ", " + this.firstName;
}
}
#Mshnik did post a method for dynamically adding PK, but youd be much better off checking the Person collection or some high level variable to find the last personID and go from there - else that dynamic value can only be used in the most limited of contexts, and you may as well just use the index its located at in the Person collection you are retrieving it from
Here's brute force way of labeling your Person objects that doesn't rely on using Maps to count last name occurrences.
I've also include another method to exploit the power of Java 8 streams. IMO, the brute force method is more understandable, but I have not done any benchmark testing to know which is more efficient. So I'll leave that up to commenting.
public static void main(String[] args) throws Exception {
List<Person> persons = new ArrayList() {
{
add(new Person("Jay", "Reese"));
add(new Person("Clara", "Smith"));
add(new Person("John", "Smith"));
add(new Person("James", "Smith"));
add(new Person("Christie", "Mayberry"));
add(new Person("Matthew", "Mayberry"));
}
};
// Toggle calls to see results
bruteForceLabel(persons);
// java8StreamLabel(persons);
}
public static void bruteForceLabel(List<Person> persons) {
for (int i = 0; i < persons.size(); i++) {
Person currentPerson = persons.get(i);
char lastCharacter = currentPerson.lastName.charAt(currentPerson.lastName.length() - 1);
// Only process last names that are not labeled
if (lastCharacter < '0' || '9' < lastCharacter) { // Not a digit
int counter = 2;
boolean foundDuplicateLastName = false;
for (int j = i + 1; j < persons.size(); j++) {
Person nextPerson = persons.get(j);
if (nextPerson.lastName.equals(currentPerson.lastName)) {
foundDuplicateLastName = true;
// Label the next person with the counter then
nextPerson.lastName = nextPerson.lastName + counter;
counter++;
}
}
// Label the current person with the starting sequence
if (foundDuplicateLastName) {
currentPerson.lastName = currentPerson.lastName + 1;
}
}
}
System.out.println(persons);
}
public static void java8StreamLabel(List<Person> persons) {
// Get a distinct count of all last names
Map<String, Long> lastNames = persons
.stream()
.map(p -> p.lastName)
.distinct()
.collect(Collectors
.toMap(p -> p, p -> persons
.stream()
.filter(p2 -> p2.lastName.equals(p)).count()));
// Apply number sequence to duplicate last names
lastNames.keySet().stream().filter((key) -> (lastNames.get(key) > 1)).forEach((key) -> {
int counter = 1;
for (Person person : persons.stream().filter(p -> p.lastName.equals(key)).toArray(size -> new Person[size])) {
person.lastName = person.lastName + counter;
counter++;
}
});
// Display the modified list
System.out.println(persons);
}
public static class Person {
private String firstName;
private String lastName;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String toString() {
return firstName + " " + lastName;
}
}
Results:
[Jay Reese, Clara Smith1, John Smith2, James Smith3, Christie Mayberry1, Matthew Mayberry2]

Best data structure to "group by" and aggregate values in Java?

I created an ArrayList of Array type like below,
ArrayList<Object[]> csvArray = new ArrayList<Object[]>();
As you can see, each element of the ArrayList is an array like {Country, City, Name, Age}.
Now I'm wanting to do a "group by" on Country and City (combined), followed by taking the average Age of the people for each Country+City.
May I know what is the easiest way to achieve this? Or you guys have suggestions to use data structures better than ArrayList for this "group by" and aggregation requirements?
Your answers are much appreciated.
You will get lot of options in Java 8.
Example
Stream<Person> people = Stream.of(new Person("Paul", 24), new Person("Mark",30), new Person("Will", 28));
Map<Integer, List<String>> peopleByAge = people
.collect(groupingBy(p -> p.age, mapping((Person p) -> p.name, toList())));
System.out.println(peopleByAge);
If you can use Java 8 and no specific reason for using a data structure, you can go through below tutorial
http://java.dzone.com/articles/java-8-group-collections
You could use Java 8 streams for this and Collectors.groupingBy. For example:
final List<Object[]> data = new ArrayList<>();
data.add(new Object[]{"NL", "Rotterdam", "Kees", 38});
data.add(new Object[]{"NL", "Rotterdam", "Peter", 54});
data.add(new Object[]{"NL", "Amsterdam", "Suzanne", 51});
data.add(new Object[]{"NL", "Rotterdam", "Tom", 17});
final Map<String, List<Object[]>> map = data.stream().collect(
Collectors.groupingBy(row -> row[0].toString() + ":" + row[1].toString()));
for (final Map.Entry<String, List<Object[]>> entry : map.entrySet()) {
final double average = entry.getValue().stream()
.mapToInt(row -> (int) row[3]).average().getAsDouble();
System.out.println("Average age for " + entry.getKey() + " is " + average);
}
You can check the collections recommended by #duffy356. I can give you an standard solution related with java.utils
I'd use a common Map<Key,Value> and being specific a HashMap.
For the keys, as I can see, you'll need and extra plain object which relates country and city. The point is create a working equals(Object) : boolean method. I'd use the Eclipse-auto generator; for me it gives me the following:
class CountryCityKey {
// package visibility
String country;
String city;
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((country == null) ? 0 : country.hashCode());
result = prime * result + ((region == null) ? 0 : region.hashCode());
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
CountryCityKey other = (CountryCityKey) obj;
if (country == null) {
if (other.country != null)
return false;
} else if (!country.equals(other.country))
return false;
if (region == null) {
if (other.region != null)
return false;
} else if (!region.equals(other.region))
return false;
return true;
}
}
Now we can group or objects in a HashMap<CountryCityKey, MySuperObject>
The code for that could be:
Map<CountryCityKey, List<MySuperObject>> group(List<MySu0perObject> list) {
Map<CountryCityKey, MySuperObject> response = new HashMap<>(list.size());
for (MySuperObject o : list) {
CountryCityKey key = o.getKey(); // I consider this done, so simply
List<MySuperObject> l;
if (response.containsKey(key)) {
l = response.get(key);
} else {
l = new ArrayList<MySuperObject>();
}
l.add(o);
response.put(key, l);
}
return response;
}
And you have it :)
you could use the brownies-collections library of magicwerk.org (http://www.magicwerk.org/page-collections-overview.html)
they offer keylists, which fit your requirements.(http://www.magicwerk.org/page-collections-examples.html)
I would recommend an additional step. You gather your data from CSV in Object[]. If you wrap your data into a class containing these data java8 collections will easily help you. (also without but it is more readable and understandable)
Here is an example - it introduces a class Information which contains your given data (country, city,name, age). The class has a constructor initializing these fields by a given Object[] array which might help you to do so - BUT: the fields have to be fixed (which is usual for CSV):
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class CSVExample {
public static void main(String[] args) {
ArrayList<Information> csvArray = new ArrayList<>();
csvArray.add(new Information(new Object[] {"France", "Paris", "Pierre", 34}));
csvArray.add(new Information(new Object[] {"France", "Paris", "Madeleine", 26}));
csvArray.add(new Information(new Object[] {"France", "Toulouse", "Sam", 34}));
csvArray.add(new Information(new Object[] {"Italy", "Rom", "Paul", 44}));
// combining country and city with whitespace delimiter to use it as the map key
Map<String, List<Information>> collect = csvArray.stream().collect(Collectors.groupingBy(s -> (s.getCountry() + " " + s.getCity())));
//for each key (country and city) print the key and the average age
collect.forEach((k, v) -> System.out.println(k + " " + v.stream().collect(Collectors.averagingInt(Information::getAge))));
}
}
class Information {
private String country;
private String city;
private String name;
private int age;
public Information(Object[] information) {
this.country = (String) information[0];
this.city = (String) information[1];
this.name = (String) information[2];
this.age = (Integer) information[3];
}
public Information(String country, String city, String name, int age) {
super();
this.country = country;
this.city = city;
this.name = name;
this.age = age;
}
public String getCountry() {
return country;
}
public String getCity() {
return city;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
#Override
public String toString() {
return "Information [country=" + country + ", city=" + city + ", name=" + name + ", age=" + age + "]";
}
}
The main shows a simple output for your question.
In java 8 the idea of grouping objects in a collection based on the values of one or more of their properties is simplified by using a Collector.
First, I suggest you add a new class as follow
class Info {
private String country;
private String city;
private String name;
private int age;
public Info(String country,String city,String name,int age){
this.country=country;
this.city=city;
this.name=name;
this.age=age;
}
public String toString() {
return "("+country+","+city+","+name+","+age+")";
}
// getters and setters
}
Setting up infos
ArrayList<Info> infos =new ArrayList();
infos.add(new Info("USA", "Florida", "John", 26));
infos.add(new Info("USA", "Florida", "James", 18));
infos.add(new Info("USA", "California", "Alan", 30));
Group by Country+City:
Map<String, Map<String, List<Info>>>
groupByCountryAndCity = infos.
stream().
collect(
Collectors.
groupingBy(
Info::getCountry,
Collectors.
groupingBy(
Info::getCity
)
)
);
System.out.println(groupByCountryAndCity.get("USA").get("California"));
Output
[(USA,California,James,18), (USA,California,Alan,30)]
The average Age of the people for each Country+City:
Map<String, Map<String, Double>>
averageAgeByCountryAndCity = infos.
stream().
collect(
Collectors.
groupingBy(
Info::getCountry,
Collectors.
groupingBy(
Info::getCity,
Collectors.averagingDouble(Info::getAge)
)
)
);
System.out.println(averageAgeByCountryAndCity.get("USA").get("Florida"));
Output:
22.0
/* category , list of cars*/
Please use the below code : I have pasted it from my sample app !Happy Coding .
Map<String, List<JmCarDistance>> map = new HashMap<String, List<JmCarDistance>>();
for (JmCarDistance jmCarDistance : carDistanceArrayList) {
String key = jmCarDistance.cartype;
if(map.containsKey(key)){
List<JmCarDistance> list = map.get(key);
list.add(jmCarDistance);
}else{
List<JmCarDistance> list = new ArrayList<JmCarDistance>();
list.add(jmCarDistance);
map.put(key, list);
}
}
Best data structure is a Map<Tuple, List>.
Tuple is the key, i.e. your group by columns.
List is used to store the row data.
Once you have your data in this structure, you can iterate through each key, and perform the aggregation on the subset of data.

Categories