Combine two sets conditionally - java

I have a Person object which has a name attribute and some other attributes. I have two HashSet with Person objects. Note that name is not an unique attribute meaning that two Persons with same name can have different height so using HashSet does not guarantee that two Persons with same name are not in the same set.
I need to add one set to another so there are no Persons in the result with the same name. So something like this:
public void combine(HashSet<Person> set1, HashSet<Person> set2){
for (String item2 : set2) {
boolean exists = false;
for (String item1 : set1) {
if(item2.name.equals(item1.name)){
exists = true;
}
}
if(!exists){
set1.add(item2);
}
}
}
Is there a cleaner way of doing this in java8?

set1.addAll(set2.stream().filter(e -> set1.stream()
.noneMatch(p -> p.getName().equals(e.getName())))
.collect(Collectors.toSet()));

If it makes sense for you to override equals and hashCode you can use something like this:
Set<Parent> result = Stream.concat(set1.stream(), set2.stream())
.collect(Collectors.toSet());
Without the Java 8 streams you can easily just do this:
Set<Parent> result = new HashSet<>();
result.addAll(set1);
result.addAll(set2);
But remember this solution is only feasible when it makes sense to have equals and hashCode overridden.
`

You can use a HashMap with name as key, then you avoid the O(n²) runtime complexity of your method. If you need HashSet, then there is no faster way. Even if you use Java 8 Streams. They add just more overhead.
public Map<String, Person> combine(Set<Person> set1, Set<Person> set2) {
Map<String, Person> persons = new HashMap<>();
set1.forEach(pers -> persons.computeIfAbsent(pers.getName(), key -> pers));
set2.forEach(pers -> persons.computeIfAbsent(pers.getName(), key -> pers));
return persons;
}

Alternatively, you could create your own collector. Assuming that you're certain that two persons with the same name are in fact the same person:
First you can define a collector:
static Collector<Person, ?, Map<String, Person>> groupByName() {
return Collector.of(
HashMap::new,
(a,b) -> a.putIfAbsent(b.name, b),
(a,b) -> { a.putAll(b); return a;}
);
}
Then you can use it to group persons by name:
Stream.concat(s1.stream(), s2.stream())
.collect(groupByName());
However, this would give you a Map<String, Person> and you just want the whole set of Persons found, right?
So, you could just do:
Set<Person> p = Stream.concat(s1.stream(), s2.stream())
.collect(collectingAndThen(groupByName(), p -> new HashSet<>(p.values())));

Related

Combining to operations using java streams

I'm doing the below two operations
Iterating through a list of Objects and creating a map of String, Boolean based on a condition.
Map<String,Boolean> myMap = new HashMap<>();
Iterator<Person> iterator = personList.iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
if (isValidperson(person)) {
if (person.getName() != null) {
myMap.put(person.getName(), true);
} else {
myMap.put(person.getName(), false);
}
}
}
Now Im checking a list of Names against that map that I created above and if the value is true then adding to a final list
List<String> refinedList = new ArrayList<>();
for (String name : nameList) {
if (myMap.get(name) != null && myMap.get(name)) {
refinedList.add(name);
}
}
I need to simplify the logic using Java streams. The above works fine otherwise.
In the first operation you are filtering out all the non-valid persons, and collecting the valid persons to a map, so:
Map<String,Boolean> myMap = personList.stream()
.filter(YourClass::isValidPerson)
.collect(Collectors.toMap(x -> x.getName(), x -> x.getName() != null))
But really though, the map is going to have at most one false entry, since you can't add multiple nulls into a HashMap, so there isn't much point in using a HashMap at all.
I suggest using a HashSet:
Set<String> mySet = personList.stream()
.filter(YourClass::isValidPerson)
.map(Person::getName)
.filter(Objects::nonNull)
.collect(Collectors.toSet())
And then you can easily check contains with O(1) time:
List<String> refinedList = nameList.stream().filter(mySet::contains).collect(Collectors.toList());
You can directly filter the list by checking contains in nameList and collect the names in list
List<String> refinedList =
personList.stream()
.filter(e -> isValidperson(e))
.map(e -> e.getName())
.filter(Objects::nonNull)
.distinct()
.filter(e -> nameList.contains(e))
.collect(Collectors.toList());
And it better to create a set from nameList to make the contains() operation faster in O(1)
Set<String> nameSet = new HashSet<String>(nameList);
Note: This will works if nameList doesn't contains duplicate.
This should work.
First, create a list of People.
List<Person> personList = List.of(new Person("Joe"),
new Person(null), new Person("Barb"), new Person("Anne"), new Person("Gary"));
Then the nameList. Note it is best to put this in a set to
avoid duplicates, and
make the lookup process more efficient.
Set<String> nameSet = Set.of("Joe", "Anne", "Ralph");
Now this works by
filtering on a valid vs invalid person.
mapping those people to a name
filtering on whether null and then if the name is in the set of names
and placing in a list.
Note: In some cases, lambdas could be replaced by Method References depending on method types and calling contexts.
List<String> names = personList.stream()
.filter(person -> isValidperson(person))
.map(Person::getName)
.filter(name -> name != null && nameSet.contains(name))
.collect(Collectors.toList());
System.out.println(names);
Prints
[Joe, Anne]
Dummy method since criteria not provided
public static boolean isValidperson(Person person) {
return true;
}
Simple person class
class Person {
String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

Remove duplicates from List<Object> based on condition

Starting point:
public class Employee {
private String id;
private String name;
private String age;
}
I have a list of Employee: List<Employee> employee;
Employee examples from the list:
{id="1", name="John", age=10}
{id="2", name="Ana", age=12}
{id="3", name="John", age=23}
{id="4", name="John", age=14}
Let's assume that age is unique.
How can I remove all duplicates from the list based on the name property and to keep in the output the entry with the largest age?
The output should look like:
{id="2", name="Ana", age=12}
{id="3", name="John", age=23}
The way I tried:
HashSet<Object> temp = new HashSet<>();
employee.removeIf(e->!temp.add(e.getName()));
..but the this way the first match will be kept in employee
{id="1", name="John", age=10}
{id="2", name="Ana", age=12}
...and I have no idea how to put an another condition here to keep the one with the largest age.
Here's a way that groups elements by name and reduces groups by selecting the one with max age:
List<Employee> uniqueEmployees = employees.stream()
.collect(Collectors.groupingBy(Employee::getName,
Collectors.maxBy(Comparator.comparing(Employee::getAge))))
.values()
.stream()
.map(Optional::get)
.collect(Collectors.toList());
Which returns [[id=2, name=Ana, age=12], [id=3, name=John, age=23]] with your test data.
Apart from the accepted answer, here are two variants:
Collection<Employee> employeesWithMaxAge = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Function.identity(),
BinaryOperator.maxBy(Comparator.comparing(Employee::getAge))))
.values();
This one uses Collectors.toMap to group employees by name, letting Employee instances as the values. If there are employees with the same name, the 3rd argument (which is a binary operator), selects the employee that has max age.
The other variant does the same, but doesn't use streams:
Map<String, Employee> map = new LinkedHashMap<>(); // preserves insertion order
employees.forEach(e -> map.merge(
e.getName(),
e,
(e1, e2) -> e1.getAge() > e2.getAge() ? e1 : e2));
Or, with BinaryOperator.maxBy:
Map<String, Employee> map = new LinkedHashMap<>(); // preserves insertion order
employees.forEach(e -> map.merge(
e.getName(),
e,
BinaryOperator.maxBy(Comparator.comparing(Employee::getAge))));
ernest_k answer is great but if you maybe want to avoid adding duplicates you can use this:
public void addToEmployees(Employee e) {
Optional<Employee> alreadyAdded = employees.stream().filter(employee -> employee.getName().equals(e.getName())).findFirst();
if(alreadyAdded.isPresent()) {
updateAgeIfNeeded(alreadyAdded.get(), e);
}else {
employees.add(e);
}
}
public void updateAgeIfNeeded(Employee alreadyAdded, Employee newlyRequested) {
if(Integer.valueOf(newlyRequested.getAge()) > Integer.valueOf(alreadyAdded.getAge())) {
alreadyAdded.setAge(newlyRequested.getAge());
}
}
Just use addToEmployees method to add Employee to your list.
You can also create class extending ArrayList and override add method like so and then use your own list :)
you can add values in a Map<String, Employee> (where string is name) only if age is greater of equals than the one in the map.

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 lambda intersection two list and remove from result

I want to use Java lambda expression for an intersection of two lists and then ever with lambda expression I want to delete from the list.
Example: I have
List<Person> first = new ArrayList<Person>();
List<Person> second = new ArrayList<Person>();
Suppose that both lists have some Person Object.
I want put into List temp intersection of two list filtered by name for example:
List<Person> temp = new ArrayList<>();
for (Person one : first) {
for (Person two : second) {
if(one.getName.equals(two.getName)) {
temp.add(two);
}
}
}
Then I want to remove some Person from temp using a filter, for example, using the Surname.
for (Person tmp : temp) {
for (Person one : first) {
if (one.getSurname.equals(tmp.getSurname)) {
temp.remove(tmp);
}
}
}
I want to use lambda expression, How i can do?
Intersection between first and second:
List<Person> intersection = first.stream()
.filter(p1 -> second.stream().anyMatch(p2 -> p2.getName().equals(p1.getName())))
.collect(Collectors.toList());
Remove elements from myList based on elements from first:
first.stream()
.filter(p1 -> myList.stream().anyMatch(p2 -> p1.getSurName().equals(p2.getSurName())))
.forEach(myList::remove);
You may do it like so,
Map<String, Set<String>> nameToSurname = second.stream().collect(
Collectors.groupingBy(Person::getName,
Collectors.mapping(Person::getSurname, Collectors.toSet())));
List<Person> temp = first.stream().
filter(p -> nameToSurname.containsKey(p.getName()))
.filter(p -> !nameToSurname.get(p.getName()).contains(p.getSurname()))
.collect(Collectors.toList());
First create a map from mame to all the surnames with that name using the second list. Then iterate over the first list, for each person, check whether there's a value in the map by passing the name as the key. If it does, then check whether the surnames matches that of the current person. If both the criteria are satisfied, then collect it into a container.
Both for-loops might be compressed into two Stream::filter methods:
List<Person> temp = second.stream()
.filter(s -> first.stream().anyMatch(f -> f.getName().equals(s.getName())))
.filter(s -> !first.stream().anyMatch(f -> f.getSurame().equals(s.getSurame())))
.collect(Collectors.toList());
Lambdas are not always the best solution.
You can use retainAll method from Collection<E>.
fist.retainAll(second);
Here is solution by method reference and not.
second.stream()
.filter(first.stream.map(Person::getName).collect(toSet())::contains)
.filter(not(first.stream.map(Person::getSurname).collect(toSet())::contains))
.collect(toList());
I didn't write the code in IDE. there may be some compiler errors.

Default return value for Collectors.toMap

If we image, we have a object called person and person looks like the follwing:
class Person {
int id;
String name;
String country
// ...
// getter/setter
}
And we have a List of Person objects and we want to "convert" it to a map. We can use the following:
Map<Long, List<Person>> collect = personList.stream().
collect(Collectors.toMap(Person::getId, p -> p));
But it is possible to return a default value for the valuemapper and change the type of the valuemapper?
I thought on something like that:
Map<Long, List<Person>> collect =
personList.stream().collect(Collectors.groupingBy(Person::getId, 0));
but with this i get the following error is not applicable for the arguments
I have a workaround but i think it's not really pretty.
Map<Long, Object> collect2 = personList.stream().
collect(Collectors.toMap(Person::getId, pe -> {
return 0;
}));
If you want to map every ID of each person to the same value, that's exactly what you need to do, although you can simplify it by writing Collectors.toMap(Person::getId, pe -> 0).

Categories