Apply a function to a nested list grouped by stream - java

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

Related

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

Need to print last digit of string using lambda expression using java

I want to print the last digit from a string using a lambda expression. Using the below code I was able to print a complete number I but want to print the last digit
public static void main(String[] args) {
List<TestDTO> studs = new ArrayList<>();
studs.add(new TestDTO("101", "Test 101"));
studs.add(new TestDTO("102", "Test 102"));
Map<String, TestDTO> mapDbCardDtl = studs.stream().collect(Collectors.toMap(TestDTO::getId, Function.identity()));
Set<String> s = mapDbCardDtl.keySet();
System.out.println("s: " + s.toString());
}
Below is the DTO
public class TestDTO {
String id;
String name;
public TestDTO(String id, String name) {
super();
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Output from the above code:
s: [101, 102]
The expected output:
S : [1, 2]
You can't use Funcion#identity and expect to have different entity with modified values. One way is to convert Map<String, TestDTO> to Map<String, String> and use the following code:
Map<String, String> mapDbCardDtl = studs
.stream()
.collect(Collectors.toMap(TestDTO::getId,
(testDto) -> String.valueOf(testDto.getId().charAt(testDto.getId().length() - 1))));
Set<String> s = mapDbCardDtl.keySet();
System.out.println("s: " + s.toString());
If you are interested only in printing last number from the id, which is a number written as String, then:
List<String> s = studs.stream()
.map(dto->dto.getId())
.map(id -> String.valueOf(id.charAt(id.length() - 1))) // take last character and cast to String
.collect(Collectors.toList());
If you want to get. last digit from name value:
final Pattern numberPattern = Pattern.compile(".*([0-9]+).*$");
List<String> s = studs.stream()
// find nunmber in name
.map(dto -> numberPattern.matcher(dto.getName()))
.filter(Matcher::matches)
.map(matcher -> matcher.group(1))
// find last digit
.map(lastNumber ->String.valueOf(lastNumber.charAt(lastNumber.length()-1)))
.collect(Collectors.toList());
TIP:
If you wanted mapDbCardDtl to have last digit as the key, then you may fail, when more than one number ends with same digit. You will have to use overwrite merge function in toMap collector.
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction)
Second solution would be using groupBy method, that will aggregate TestDTO into Map<String,List< TestDTO >>. Here the key is your digit and value : list of Dto's with this digit.
If you simply want to print the last character of the keys, add this line right before the println statement:
s = s.stream().map(x -> x.substring(x.length() - 1)).collect(Collectors.toSet());
If you actually wanted the keys in the map to only be the last character, change the stream logic to:
Map<String, TestDTO> mapDbCardDtl = studs.stream()
.collect(Collectors.toMap(t -> t.getId().substring(t.getId().length() - 1), Function.identity()));
You can transform your Student's object into the last digit of id then collect in a list.
List<String> lastDigits =
studs.stream()
.map(s -> s.getId().substring(s.getId().length() - 1)))
.collect(Collectors.toList());
Note: If you collect in Set then it will contain only unique digits.
You have already done the most part of it, Just a little set manipulation was needed.
Look at the edit I made in the main method it will print your desire result.
public static void main(String[] args) {
List<TestDTO> studs = new ArrayList<>();
studs.add(new TestDTO("101", "Test 101"));
studs.add(new TestDTO("102", "Test 102"));
Map<String, TestDTO> mapDbCardDtl = studs.stream().collect(Collectors.toMap(TestDTO::getId, Function.identity()));
Set<String> s = mapDbCardDtl.keySet().stream()
.map(number -> String.valueOf(number.charAt(number.length() - 1))) // take last character and cast to String
.collect(Collectors.toSet());;
System.out.println("s: " + s);
}

Finding duplicated objects by two properties

Considering that I have a list of Person objects like this :
Class Person {
String fullName;
String occupation;
String hobby;
int salary;
}
Using java8 streams, how can I get list of duplicated objects only by fullName and occupation property?
By using java-8 Stream() and Collectors.groupingBy() on firstname and occupation
List<Person> duplicates = list.stream()
.collect(Collectors.groupingBy(p -> p.getFullName() + "-" + p.getOccupation(), Collectors.toList()))
.values()
.stream()
.filter(i -> i.size() > 1)
.flatMap(j -> j.stream())
.collect(Collectors.toList());
I need to find if they were any duplicates in fullName - occupation pair, which has to be unique
Based on this comment it seems that you don't really care about which Person objects were duplicated, just that there were any.
In that case you can use a stateful anyMatch:
Collection<Person> input = new ArrayList<>();
Set<List<String>> seen = new HashSet<>();
boolean hasDupes = input.stream()
.anyMatch(p -> !seen.add(List.of(p.fullName, p.occupation)));
You can use a List as a 'key' for a set which contains the fullName + occupation combinations that you've already seen. If this combination is seen again you immediately return true, otherwise you finish iterating the elements and return false.
I offer solution with O(n) complexity. I offer to use Map to group given list by key (fullName + occupation) and then retrieve duplicates.
public static List<Person> getDuplicates(List<Person> persons, Function<Person, String> classifier) {
Map<String, List<Person>> map = persons.stream()
.collect(Collectors.groupingBy(classifier, Collectors.mapping(Function.identity(), Collectors.toList())));
return map.values().stream()
.filter(personList -> personList.size() > 1)
.flatMap(List::stream)
.collect(Collectors.toList());
}
Client code:
List<Person> persons = Collections.emptyList();
List<Person> duplicates = getDuplicates(persons, person -> person.fullName + ':' + person.occupation);
First implement equals and hashCode in your person class and then use.
List<Person> personList = new ArrayList<>();
Set<Person> duplicates=personList.stream().filter(p -> Collections.frequency(personList, p) ==2)
.collect(Collectors.toSet());
If objects are more than 2 then you use Collections.frequency(personList, p) >1 in filter predicate.

Java Sort list of objects by name but one object should remain always in the top

class Manager
{
private int id;
private String name;
public Manager(int id, String name)
{
this.id = id;
this.name = name;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getId() { return id; }
public void setId(int id) { this.id = id; }
}
List<Manager> names = new ArrayList<>();
names.add(new Manager(1, "Robert"));
names.add(new Manager(2, "Paul"));
names.add(new Manager(3, "None"));
names.add(new Manager(4, "Nancy"));
names.add(new Manager(4, "Nancy"));
names.stream().sorted(
Comparator.comparing(n->n.getName())).collect(Collectors.toList());
List<String> names = new List<String>();
names.add("Robert");
names.add("Paul");
names.add("None");
names.add("Nancy");
names.add("Nancy");
names.stream().sorted().collect(Collectors.toList());
I need to sort a list of strings in Java 8 which I can do it easily using below logic but I want to display the None in the below list at top always. The result has to be. I need to obtain a sort order for the List of Managers objects
None
Jhon
Nancy
Paul
My code:
List<String> names = new List<String>();
names.add("Robert");
names.add("Paul");
names.add("None");
names.add("Nancy");
names.add("Jhon");
names.stream().sorted().collect(Collectors.toList());
string needs to be on the first place always.
You can use
List<Manager> sorted = names.stream()
.sorted(Comparator.comparing(Manager::getName,
Comparator.comparing((String s) -> !s.equals("None"))
.thenComparing(Comparator.naturalOrder())))
.collect(Collectors.toList());
to sort into a new list or use
names.sort(Comparator.comparing(Manager::getName,
Comparator.comparing((String s) -> !s.equals("None"))
.thenComparing(Comparator.naturalOrder())));
to sort in-place, which is possible with a mutable list like ArrayList.
You can pass in a comparator object to the sorted intermediate method defining your criteria like so:
List<String> resultSet =
names.stream()
.sorted((e, a) -> "None".equals(e) ? -1:
"None".equals(a) ? 1 : e.compareTo(a))
.collect(Collectors.toList());
One approach is to filter out "None", and pre-pend it to a sorted list:
List<String> ordered = Stream.concat(
names.stream().filter(s -> s.equals("None"))
, names.stream().filter(s -> !s.equals("None")).sorted()
).collect(Collectors.toList());
Demo.
using sorted and partitioningBy
Map<Boolean, List<String>> partition = names
.stream()
.sorted()
.collect(Collectors.partitioningBy("None"::equals));
System.out.println(partition);
List<String> noneFirst = new ArrayList<>(partition.get(true));
noneFirst.addAll(partition.get(false));
System.out.println(noneFirst);
output
[None, Jhon, Nancy, Paul, Robert]
List<String> sorted = names.stream()
.sorted((o1, o2) -> o1 == null ?
-1 :
o2 == null ?
1 :
o1.equals("None") ?
-1 :
o2.equals("None") ?
1 :
o1.compareTo(o2))
.collect(Collectors.toList());
This is my contribution: try to make it as good formatted/looking as it can be.
Notice that this will fail.
List<String> unsorted = ... what-ever ...;
// ...
List<String> sorted = unsorted.stream()
.filter(Objects::nonNull) // optional: get rid of nulls before an exception is thrown bellow.
.sorted((a, b) -> Objects.equals(a, b) ? 0
: "None".equals(a) ? -1
: "None".equals(b) ? 1
: a.compareTo(b))
.distinct() // optional: if you want to get rid of repeats.
.collect(Collectors.toList());
Check this out
names.stream()
.sorted((o1, o2) ->
{
if("None".equals(o1.getName()) && "None".equals(o2.getName())) return 0;
return "None".equals(o1.getName()) ? -1:
"None".equals(o2.getName()) ? 1 : o1.getName().compareTo(o2.getName());
}
).collect(Collectors.toList());

Java collection - group by multiple attributes

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

Categories