Common element between 2 collections in java

Viewed 119

I am trying to find common elements between 2 lists where elements are of type Object.

For example

class Student {
 String firstName;
 String lastName;
 float gpa;
... other attributes
}

similarly

class Employee{
     String firstName;
     String lastName;
     float salary;
    ... other attributes
    }

I need to find out the entries from employees which has same first name as that of student.

I did the following

    Set<String> studentFirstNames = studentList.stream().
map(student-> student.getFirstName()).
collect(Collectors.toSet());

List<Employee> employeeAndStudents = employeeList.stream()
.filter(employee-> studentFirstNames.contains(employee.getFirstName())
.collect(Collectors.toList());

This works, however I am trying to find a solution where common attributes are not limited to a single field (say for example first and last name).

The list can contain many elements but irrespective of the size, I am wondering if a better solution exist using FP.

3 Answers

You might want to create a custom "equalizer" class, that takes an Object from either types to be checked and delegates to the fields of the other class.

class Equalizer {
   private final String firstName;
   private final String lastName;
   Equalizer(Student student){
     this.firstName  = student.getFirstName;
     this.lastName  = student.getLastName;
   }

   Equalizer(Employee employee){
     this.firstName  = employee.getFirstName;
     this.lastName  = employee.getLastName;
   }

  // have equals() and hashcode() implemented by your IDE based on the fields.
}

Then you convert the first collection to Equalizer objects.

 Set<Equalizer> equalizers = studentList.stream()
      .map(student-> new Equalizer(student))
      .collect(Collectors.toSet());

And then you can do the compare against them:

List<Employee> employeeAndStudents = employeeList.stream()
    .filter(employee-> equalizers.contains(new Equalizer(employee))
    .collect(Collectors.toList());

What about using two hash functions?

Function<Student,Integer> studentHashFunction = s -> Objects.hash(s.getFirstName(), s.getLastName());
Function<Employee,Integer> employeeHashFunction = e -> Objects.hash(e.getFirstName(), e.getLastName());

Set<Integer> studentHash = studentList
   .stream()
   .map(studentHashFunction)
   .collect(Collectors.toSet());

List<Employee> employeeAndStudents = employeeList
   .stream()
   .filter(employee-> studentHash.contains(employeeHashFunction.apply(employee)))
   .collect(Collectors.toList());

If you don't want to add any new classes, and if you have the flexibility to modify them (implement an interface), this solution will work. First define an interface that contains the fields (getters) you want to compare:

interface ComparisionType {
    String getFirstName();
    String getLastName();
    int getAge();
}

Next, implement this in your Student and Employee class like so:

@Builder
@Getter
@Setter
@ToString
class Student implements ComparisionType {
    String firstName;
    String lastName;
    int age;
    float gpa;
}

@Builder
@Getter
@Setter
@ToString
class Employee implements ComparisionType {
    String firstName;
    String lastName;
    int age;
    float salary;
}

Below is the sample executable code (with java-doc telling what each method does).

public class StackOverflowTest {

    public static void main(String[] args) {
        List<ComparisionType> students = List.of(
                Student.builder().firstName("firstName").lastName("lastName00").age(35).build(),
                Student.builder().firstName("firstName1").lastName("lastName11").age(22).build(),
                Student.builder().firstName("firstName2").lastName("lastName44").age(30).build());

        List<ComparisionType> employees = List.of(
                Employee.builder().firstName("firstName2").lastName("lastName11").age(12).build(),
                Employee.builder().firstName("firstName3").lastName("lastName22").age(23).build(),
                Employee.builder().firstName("firstName4").lastName("lastName33").age(35).build());

        List<Student> commonStudents = getCommon(students, employees, Student.class, ComparisionType::getAge);
        System.out.println(commonStudents);

        List<Employee> commonEmployees = getCommon(students, employees, Employee.class, ComparisionType::getLastName);
        System.out.println(commonEmployees);
    }

    /**
     * Supply the 2 lists in first and second argument
     * Third argument is which type of common objects you like as the output
     * Fourth argument is which field do you want to compare to determine similar objects
     */
    @SuppressWarnings("unchecked")
    private static <T> List<T> getCommon(List<ComparisionType> students, List<ComparisionType> employees, Class<T> clazz,
            Function<ComparisionType, Object> function) {
        Map.Entry<List<ComparisionType>, List<ComparisionType>> comparingAndComparedTo = getComparingAndComparedTo(students, employees, clazz);

        return (List<T>) comparingAndComparedTo.getKey().stream()
                .filter(isMatch(comparingAndComparedTo.getValue(), function))
                .collect(Collectors.toList());
    }

    /**
     * Identifies the list to loop through
     * to compare against the other list.
     */
    private static <T> Map.Entry<List<ComparisionType>, List<ComparisionType>> getComparingAndComparedTo(List<ComparisionType> list1,
            List<ComparisionType> list2, Class<T> clazz) {
        return Optional.of(list1)
                .filter(list -> list.get(0).getClass().equals(clazz)) // <-- Add better class check here
                .map(list -> Map.entry(list1, list2))
                .orElse(Map.entry(list2, list1));
    }

    /**
     * Takes a list and checks if a field of an element
     * matches the supplied field (from the function).
     */
    private static Predicate<ComparisionType> isMatch(List<ComparisionType> list, Function<ComparisionType, Object> function) {
        return type -> {
            return list.stream()
                    .map(function)
                    .anyMatch(field -> field.equals(function.apply(type)));
        };
    }
}

The getCommon method will give you the list of objects of your desired type and also takes in the field you want to compare, so you have the flexibility to choose.

NOTE: I haven't tested the performance for this code. If you managed to calculate the performance metrics, feel free to mention it, i'm curious to know.

PS: I'm using Java11.

Related