I have this Person class which has a list of Person (s). How do I loop through persons and check if each object inside of that has a list of Person(s) and if each object inside that has a list and so on and so forth? Everything I can think of is pretty limiting as far as how nested it gets. I can write a recursive loop but that gets me to the first level deep, but not sure how to get x levels deep with recursion. I am sure somebody has come accross this problem in the past and it shouldn't be that difficult but I just can't quite wrap my head around it. Any and all ideas are welcomed!
public class Person {
// other fields removed for simplicity
private long id;
private List<Person> persons;
public List<Person> getPersons() {
return debates;
}
}
// essentially I am looking for a way to make this unlimited level nested looping
private void loopPersons() {
Person person = new Person();
if(person.getPersons() != null && !person.getPersons().isEmpty()) {
for(Person person1 : person.getPersons()) {
if(person1.getPersons() != null && !person1.getPersons().isEmpty()) {
System.out.println(person1.getId());
for(Person person2 : person1.getPersons()) {
if(person2.getPersons() != null && !person2.getPersons().isEmpty()) {
System.out.println(person2.getId());
}
}
}
}
}
}
UPDATE:
The answer by Brian in this other post (scroll down) is essentially what does it. iterate through recursive objects
You might just be looking for some flattening on the lines of making use of recursion with a tail condition. This could be similar to the following implementation
// essentially I am looking for a way to make this unlimited level nested looping
private List<Person> loopPersons(Person person, List<Person> flattened) {
if (person.getPersons() == null || person.getPersons().isEmpty()) {
return flattened;
} else {
flattened.addAll(person.getPersons());
person.getPersons().forEach(p -> loopPersons(p, flattened));
}
return flattened;
}
Note: The code is not tested and is to depict a possible approach that you can take if you think over the same lines.
Do it as follows:
import java.util.List;
class Person {
// other fields removed for simplicity
private long id;
private List<Person> persons;
public Person(long id, List<Person> persons) {
this.id = id;
this.persons = persons;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public void setPersons(List<Person> persons) {
this.persons = persons;
}
public List<Person> getPersons() {
return persons;
}
#Override
public String toString() {
return "Person [id=" + id + ", persons=" + persons + "]";
}
public void showAll() {
if (getPersons() == null || getPersons().isEmpty()) {
return;
}
getPersons().get(0).showAll();
System.out.println(getPersons());
}
}
Demo:
public class Main {
public static void main(String[] args) {
Person p1 = new Person(1,List.of(new Person(11, List.of(new Person(111, List.of(new Person(1111, null))),
new Person(112, List.of(new Person(1121, null))),
new Person(113, List.of(new Person(1131, null))))),
new Person(12, List.of(new Person(121, List.of(new Person(1211, null))))),
new Person(13, List.of(new Person(131, List.of(new Person(1311, null)))))));
p1.showAll();
}
}
Output:
[Person [id=1111, persons=null]]
[Person [id=111, persons=[Person [id=1111, persons=null]]], Person [id=112, persons=[Person [id=1121, persons=null]]], Person [id=113, persons=[Person [id=1131, persons=null]]]]
[Person [id=11, persons=[Person [id=111, persons=[Person [id=1111, persons=null]]], Person [id=112, persons=[Person [id=1121, persons=null]]], Person [id=113, persons=[Person [id=1131, persons=null]]]]], Person [id=12, persons=[Person [id=121, persons=[Person [id=1211, persons=null]]]]], Person [id=13, persons=[Person [id=131, persons=[Person [id=1311, persons=null]]]]]]
Related
I have two classes. Child and Product with association many to many. Id like to find child who has the most products (product as a method parameter) using java stream :)
Thanks
You need something like:
private Optional<Child> childWithMostProduct(String productName) {
return childList.stream()
.max(Comparator.comparingLong(ch -> ch.getProducts()
.stream()
.filter(pr -> pr.getName().equals(productName)).count());
}
Or if you have defined equals for Product:
private Optional<Child> childWithMostProduct(Product product) {
return childList.stream()
.max(Comparator.comparingInt(ch -> Collections.frequency(ch.getProducts(), product));
}
Here is a complete working example of the second model:
public record Child(String name, List<Product> productList) {
private record Product(String name) { }
public static Optional<Child> childWithMostProduct(List<Child> childList, Product product) {
return childList.stream()
.max(Comparator.comparingInt(ch -> Collections.frequency(ch.productList, product)));
}
public static void main(String[] args) {
Product p1 = new Product("p1");
Product p2 = new Product("p2");
List<Child> childList = List.of(
new Child("ch1", List.of(p1, p1, p2)),
new Child("ch2", List.of(p1, p2, p2)));
System.out.println(childWithMostProduct(childList, p1));
System.out.println(childWithMostProduct(childList, p2));
}
}
The output of this is:
Optional[Child[name=ch1, productList=[Product[name=p1],
Product[name=p1], Product[name=p2]]]]
Optional[Child[name=ch2,
productList=[Product[name=p1], Product[name=p2], Product[name=p2]]]]
Which is correct.
This code removes duplicates from the original list, but I want to extract the duplicates from the original list -> not removing them (this package name is just part of another project):
Given:
a Person pojo:
package at.mavila.learn.kafka.kafkaexercises;
import org.apache.commons.lang3.builder.ToStringBuilder;
public class Person {
private final Long id;
private final String firstName;
private final String secondName;
private Person(final Builder builder) {
this.id = builder.id;
this.firstName = builder.firstName;
this.secondName = builder.secondName;
}
public Long getId() {
return id;
}
public String getFirstName() {
return firstName;
}
public String getSecondName() {
return secondName;
}
public static class Builder {
private Long id;
private String firstName;
private String secondName;
public Builder id(final Long builder) {
this.id = builder;
return this;
}
public Builder firstName(final String first) {
this.firstName = first;
return this;
}
public Builder secondName(final String second) {
this.secondName = second;
return this;
}
public Person build() {
return new Person(this);
}
}
#Override
public String toString() {
return new ToStringBuilder(this)
.append("id", id)
.append("firstName", firstName)
.append("secondName", secondName)
.toString();
}
}
Duplication extraction code.
Notice here we filter the id and the first name to retrieve a new list, I saw this code someplace else, not mine:
package at.mavila.learn.kafka.kafkaexercises;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static java.util.Objects.isNull;
public final class DuplicatePersonFilter {
private DuplicatePersonFilter() {
//No instances of this class
}
public static List<Person> getDuplicates(final List<Person> personList) {
return personList
.stream()
.filter(duplicateByKey(Person::getId))
.filter(duplicateByKey(Person::getFirstName))
.collect(Collectors.toList());
}
private static <T> Predicate<T> duplicateByKey(final Function<? super T, Object> keyExtractor) {
Map<Object,Boolean> seen = new ConcurrentHashMap<>();
return t -> isNull(seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE));
}
}
The test code.
If you run this test case you will get [alex, lolita, elpidio, romualdo].
I would expect to get instead [romualdo, otroRomualdo] as the extracted duplicates given the id and the firstName:
package at.mavila.learn.kafka.kafkaexercises;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.*;
public class DuplicatePersonFilterTest {
private static final Logger LOGGER = LoggerFactory.getLogger(DuplicatePersonFilterTest.class);
#Test
public void testList(){
Person alex = new Person.Builder().id(1L).firstName("alex").secondName("salgado").build();
Person lolita = new Person.Builder().id(2L).firstName("lolita").secondName("llanero").build();
Person elpidio = new Person.Builder().id(3L).firstName("elpidio").secondName("ramirez").build();
Person romualdo = new Person.Builder().id(4L).firstName("romualdo").secondName("gomez").build();
Person otroRomualdo = new Person.Builder().id(4L).firstName("romualdo").secondName("perez").build();
List<Person> personList = new ArrayList<>();
personList.add(alex);
personList.add(lolita);
personList.add(elpidio);
personList.add(romualdo);
personList.add(otroRomualdo);
final List<Person> duplicates = DuplicatePersonFilter.getDuplicates(personList);
LOGGER.info("Duplicates: {}",duplicates);
}
}
In my job I was able to get the desired result it by using Comparator using TreeMap and ArrayList, but this was creating a list then filtering it, passing the filter again to a newly created list, this looks bloated code, (and probably inefficient)
Does someone has a better idea how to extract duplicates?, not remove them.
Thanks in advance.
Update
Thanks everyone for your answers
To remove the duplicate using same approach with the uniqueAttributes:
public static List<Person> removeDuplicates(List<Person> personList) {
return getDuplicatesMap(personList).values().stream()
.filter(duplicates -> duplicates.size() > 1)
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
private static Map<String, List<Person>> getDuplicatesMap(List<Person> personList) {
return personList.stream().collect(groupingBy(DuplicatePersonFilter::uniqueAttributes));
}
private static String uniqueAttributes(Person person){
if(Objects.isNull(person)){
return StringUtils.EMPTY;
}
return (person.getId()) + (person.getFirstName()) ;
}
Update 2
But also the answer provided by #brett-ryan is correct:
public static List<Person> extractDuplicatesWithIdentityCountingV2(final List<Person> personList){
List<Person> duplicates = personList.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet().stream()
.filter(n -> n.getValue() > 1)
.flatMap(n -> nCopies(n.getValue().intValue(), n.getKey()).stream())
.collect(toList());
return duplicates;
}
EDIT
Above code can be found under:
https://gitlab.com/totopoloco/marco_utilities/-/tree/master/duplicates_exercises
Please see:
Usage:
https://gitlab.com/totopoloco/marco_utilities/-/blob/master/duplicates_exercises/src/test/java/at/mavila/exercises/duplicates/lists/DuplicatePersonFilterTest.java
Implementation:
https://gitlab.com/totopoloco/marco_utilities/-/blob/master/duplicates_exercises/src/main/java/at/mavila/exercises/duplicates/lists/DuplicatePersonFilter.java
To indentify duplicates, no method I know of is better suited than Collectors.groupingBy(). This allows you to group the list into a map based on a condition of your choice.
Your condition is a combination of id and firstName. Let's extract this part into an own method in Person:
String uniqueAttributes() {
return id + firstName;
}
The getDuplicates() method is now quite straightforward:
public static List<Person> getDuplicates(final List<Person> personList) {
return getDuplicatesMap(personList).values().stream()
.filter(duplicates -> duplicates.size() > 1)
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
private static Map<String, List<Person>> getDuplicatesMap(List<Person> personList) {
return personList.stream().collect(groupingBy(Person::uniqueAttributes));
}
The first line calls another method getDuplicatesMap() to create the map as explained above.
It then streams over the values of the map, which are lists of persons.
It filters out everything except lists with a size greater than 1, i.e. it finds the duplicates.
Finally, flatMap() is used to flatten the stream of lists into one single stream of persons, and collects the stream to a list.
An alternative, if you truly identify persons as equal if the have the same id and firstName is to go with the solution by Jonathan Johx and implement an equals() method.
If you could implement equals and hashCode on Person you could then use a counting down-stream collector of the groupingBy to get distinct elements that have been duplicated.
List<Person> duplicates = personList.stream()
.collect(groupingBy(identity(), counting()))
.entrySet().stream()
.filter(n -> n.getValue() > 1)
.map(n -> n.getKey())
.collect(toList());
If you would like to keep a list of sequential repeated elements you can then expand this out using Collections.nCopies to expand it back out. This method will ensure repeated elements are ordered together.
List<Person> duplicates = personList.stream()
.collect(groupingBy(identity(), counting()))
.entrySet().stream()
.filter(n -> n.getValue() > 1)
.flatMap(n -> nCopies(n.getValue().intValue(), n.getKey()).stream())
.collect(toList());
List<Person> duplicates = personList.stream()
.collect(Collectors.groupingBy(Person::getId))
.entrySet().stream()
.filter(e->e.getValue().size() > 1)
.flatMap(e->e.getValue().stream())
.collect(Collectors.toList());
That should give you a List of Person where the id has been duplicated.
In this scenario you need to write your custom logic to extract the duplicates from the list, you will get all the duplicates in the Person list
public static List<Person> extractDuplicates(final List<Person> personList) {
return personList.stream().flatMap(i -> {
final AtomicInteger count = new AtomicInteger();
final List<Person> duplicatedPersons = new ArrayList<>();
personList.forEach(p -> {
if (p.getId().equals(i.getId()) && p.getFirstName().equals(i.getFirstName())) {
count.getAndIncrement();
}
if (count.get() == 2) {
duplicatedPersons.add(i);
}
});
return duplicatedPersons.stream();
}).collect(Collectors.toList());
}
Applied to:
List<Person> l = new ArrayList<>();
Person alex = new
Person.Builder().id(1L).firstName("alex").secondName("salgado").build();
Person lolita = new
Person.Builder().id(2L).firstName("lolita").secondName("llanero").build();
Person elpidio = new
Person.Builder().id(3L).firstName("elpidio").secondName("ramirez").build();
Person romualdo = new
Person.Builder().id(4L).firstName("romualdo").secondName("gomez").build();
Person otroRomualdo = new
Person.Builder().id(4L).firstName("romualdo").secondName("perez").build();
l.add(alex);
l.add(lolita);
l.add(elpidio);
l.add(romualdo);
l.add(otroRomualdo);
Output:
[Person [id=4, firstName=romualdo, secondName=gomez], Person [id=4, firstName=romualdo, secondName=perez]]
I think first you should overwrite equals method of Person class and focus on id and name. And after you can update it adding a filter for that.
#Override
public int hashCode() {
return Objects.hash(id, name);
}
#Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
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(id, other.id)) {
return false;
}
return true;
}
personList
.stream()
.filter(p -> personList.contains(p))
.collect(Collectors.toList());
Solution based on generic key:
public static <T> List<T> findDuplicates(List<T> list, Function<T, ?> uniqueKey) {
if (list == null) {
return emptyList();
}
Function<T, ?> notNullUniqueKey = el -> uniqueKey.apply(el) == null ? "" : uniqueKey.apply(el);
return list.stream()
.collect(groupingBy(notNullUniqueKey))
.values()
.stream()
.filter(matches -> matches.size() > 1)
.map(matches -> matches.get(0))
.collect(toList());
}
// Example of usage:
List<Person> duplicates = findDuplicates(list, el -> el.getFirstName());
List<Person> arr = new ArrayList<>();
arr.add(alex);
arr.add(lolita);
arr.add(elpidio);
arr.add(romualdo);
arr.add(otroRomualdo);
Set<String> set = new HashSet<>();
List<Person> result = arr.stream()
.filter(data -> (set.add(data.name +";"+ Long.toString(data.id)) == false))
.collect(Collectors.toList());
arr.removeAll(result);
Set<String> set2 = new HashSet<>();
result.stream().forEach(data -> set2.add(data.name +";"+ Long.toString(data.id)));
List<Person> resultTwo = arr.stream()
.filter(data -> (set2.add(data.name +";"+ Long.toString(data.id)) == false))
.collect(Collectors.toList());
result.addAll(resultTwo);
The above code will filter based on name and id. The result List will have all the duplicated Person Object
I want to sort a list within an entity with Java 8 Comparators.
Can someone help me to replace this here --> thenComparing(???)
#Test
public void testSort() {
// given
List<Person> values = Lists.newArrayList(
new Person("you", Lists.newArrayList(new Todo("A"))),
new Person("me", Lists.newArrayList(new Todo("B"), new Todo("A"), new Todo("C"))),
new Person("me", Lists.newArrayList(new Todo("A"))),
new Person("me", Lists.newArrayList(new Todo("D")))
);
// when
List<Person> result = values
.stream()
.sorted(Comparator
.comparing(Person::getName))
.thenComparing(???) <-- TODO
.collect(Collectors.toList());
// then
assertEquals(4, result.size());
Person person_1 = result.get(0);
assertEquals("me", person_1.getName());
assertEquals(1, person_1.getTodos().size());
assertEquals("A", person_1.getTodos().get(0).getName());
Person person_2 = result.get(1);
assertEquals("me", person_2.getName());
assertEquals(3, person_2.getTodos().size());
assertEquals("A", person_2.getTodos().get(0).getName());
assertEquals("B", person_2.getTodos().get(1).getName());
assertEquals("C", person_2.getTodos().get(2).getName());
Person person_3 = result.get(2);
assertEquals("me", person_3.getName());
assertEquals(1, person_3.getTodos().size());
assertEquals("D", person_3.getTodos().get(0).getName());
Person person_4 = result.get(3);
assertEquals("you", person_4.getName());
assertEquals(1, person_4.getTodos().size());
assertEquals("A", person_4.getTodos().get(0).getName());
}
#Data
#AllArgsConstructor
class Todo {
private String name;
}
#Data
#AllArgsConstructor
class Person {
private String name;
private List<Todo> todos;
}
Expected sort result:
Person(name=me, todos=[Todo(name=A)])
Person(name=me, todos=[Todo(name=A), Todo(name=B), Todo(name=C)])
Person(name=me, todos=[Todo(name=D)])
Person(name=you, todos=[Todo(name=A)])
By editing the classes Person and Todo, in combination with the fact String sorting is built into java this becomes easier.
so lets first think about this, we can use thenComparing after the first comparing by casting the Todo List to a string, then sorting by that string. we edit the Person class first by adding a function that returns the Todo list as a string. It will end up looking like:
public class Person {
private String name;
private List<Todo> todos;
public Person(String name, List<Todo> todos) {
this.name = name;
this.todos = todos;
}
public String getName() {
return name;
}
public List<Todo> getTodos() {
return todos;
}
public String getTodoListAsString() {
return this.todos.stream()
.map(Todo::getName)
.sorted()
.collect(Collectors.joining());
}
}
We also want to change the Todo class to implement Comparable so that we can map the Todo list in them into a sorted list. We can accomplish this by editing the Todo class as such:
public class Todo implements Comparable<Todo> {
private String name;
public Todo(String name) {
this.name = name;
}
public String getName() {
return name;
}
#Override
public int compareTo(Todo o) {
return this.getName().compareTo(o.getName());
}
}
Now we can use that in the stream as such:
List<Person> result = values
.stream()
.sorted(Comparator.comparing(Person::getName)
.thenComparing(Person::getTodoListAsString))
.map(person -> new Person(person.getName(),
person.getTodos()
.stream()
.sorted()
.collect(Collectors.toList())))
.collect(Collectors.toList())
Here's some imperative code that I'm trying to translate into functional programming code:
public class Person {
String name;
Token token;
public Person(String name, Token token) {
this.name = name;
this.token = token;
}
}
public class Token {
String id;
boolean isValid;
public Token(String id, boolean isValid) {
this.id = id;
this.isValid = isValid;
}
public String getId() { return id; }
public boolean isValid() {return isValid;}
}
public static List<Token> getTokensForPerson(String name) {...}
public static List<Person> getPeople1 (String[] names) {
List<Person> people = new ArrayList<Person> ();
for (String name: names) {
List<Token> tokens = getTokensForPerson(name);
for (Token token: tokens) {
if (token.isValid()) {
people.add(new Person(name, token));
}
}
}
return people;
}
Here's my attempt to do it the functional way.
public static List<Person> getPeople2 (String[] names) {
return Arrays.stream(names).map(name -> getTokensForPerson(name))
.flatMap(tokens -> tokens.stream().filter(token -> token.isValid))
.map(token -> new Person(name, token)) // <== compiler error here. "Cannot resolve symbol 'name'"
.collect(Collectors.toList());
}
However it doesn't compile since in the last map operation I need to refer to name to create the Person object and name is not available at that time. Any ideas?
You can move the map steps inside the flatMap:
return Arrays.stream(names)
.<Person>flatMap(
name -> getTokensForPerson(name).stream()
.filter(Token::isValid)
.map(token -> new Person(name, token)))
.collect(Collectors.toList());
This way you can access name variable as well.
StreamEx-based solution is shorter, though it requires third-party library:
return StreamEx.of(names)
.cross(name -> getTokensForPerson(name).stream())
// Here we have the stream of entries
// where keys are names and values are tokens
.filterValues(Token::isValid)
.mapKeyValue(Person::new)
.toList();
Would it be possible to create TokenExtended class extending Token, and adding the name, and return a List<TokenExtended> from getTokensForPerson instead of the List<Token>?
public class TokenExtended extends Token {
private String name;
public TokenExtended(String name, String id, boolean isValid) {
super(id, isValid);
this.name = name;
}
}
This way your code would work
Arrays.stream(names).map(name -> getTokensForPerson(name)).flatMap(tokens -> tokens.stream().filter(token -> token.isValid))
.map(token -> new Person(token.name, token)).collect(Collectors.toList());
This is the scenario:
<family>
<person>name</person>
<person>
<address> street </adress>
<address> street </address>
</person>
</family>
The person value can be a list of addresses or just the person name.
I think the solution is to use a converter but how do you do that?
Check what input you retrieve?
Tell the converter to just continue using the defaults for class 3?
Example class : (do note that this is for illustration)
public class Person {
private String name;
private List<Address> address;
}
public class Address {
private String street;
}
You do need to use a converter. Here is the converter for your example:
public class PersonConverter implements Converter {
public void marshal(Object value, HierarchicalStreamWriter writer, MarshallingContext context) {
Person person = (Person) value;
if(person.name != null){
writer.setValue(person.name);
} else if(person.address != null){
for (Address address : person.address){
writer.startNode("address");
writer.setValue(address.street);
writer.endNode();
}
}
}
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
Person person = new Person();
person.name = reader.getValue();
if(person.name.trim().length()==0){
person.name = null;
}
List<Address> addresses = getAddress(reader, new ArrayList<Address>());
person.address = addresses;
if(person.address.size() == 0){
person.address = null;
}
return person;
}
private List<Address> getAddress(HierarchicalStreamReader reader, List<Address> addresses){
if (!reader.hasMoreChildren()){
return addresses;
}
reader.moveDown();
if(reader.getNodeName().equals("address")){
addresses.add(new Address(reader.getValue()));
reader.moveUp();
getAddress(reader, addresses);
}
return addresses;
}
public boolean canConvert(Class clazz) {
return clazz.equals(Person.class);
}
}
Here is the main method:
public static void main(String[] args) {
List<Person> persons = new ArrayList<Person>();
persons.add(new Person("John"));
List<Address> adds = new ArrayList<Address>();
adds.add(new Address("123 street"));
adds.add(new Address("456 street"));
persons.add(new Person(adds));
Family family = new Family(persons);
XStream stream = new XStream();
stream.registerConverter(new PersonConverter());
stream.processAnnotations(new Class[]{Family.class});
String xml = stream.toXML(family);
System.out.println(xml);
Family testFam = (Family) stream.fromXML(xml);
System.out.println("family.equals(testFam) => "+family.equals(testFam));
}
If you implement the equals method for the Family, Person, and Address classes it should print that they are equal at the end of the method when ran. Also worth noting is that I used some Annotations on the Family. I used #XStreamAlias("family") on the Class itself, and then on the collection of Person objects I used #XStreamImplicit(itemFieldName="person").
And here is my ouput when I run the main method supplied:
<family>
<person>John</person>
<person>
<address>123 street</address>
<address>456 street</address>
</person>
</family>
family.equals(testFam) => true