Search code examples
javafunctional-programmingguava

Check if only specific properties of 2 objects are equal


Say I have a a class Person (assume all of the properties can be set and can also be null):

public class Person {
  private String firstName;
  private String secondName;
  private Address address;

  public String getFirstName() { return firstName; }
  public String getSecondName() { return secondName; }
  public String getAddress() { return address; }
}

If I have two instances of Person, and I want to check if they both have the same firstName and secondName, I cannot simply call equals() as that will also check address for equality (and any other properties for that matter).

I would have to write a function like this:

boolean areNamesEqual(Person person1, Person person2) {
  if(person1 == null || person2 == null) {
    return person1 == person2;
  }
  if(person1.getFirstName() != null ? !person1.getFirstName().equals(person2.getFirstName()) : person2.getFirstName() != null) {
    return false;
  }
  if(person1.getSecondName() != null ? !person1.getSecondName().equals(person2.getSecondName()) : person2.getSecondName() != null) {
    return false;
  }
  return true;
}

Is there any cleaner way to express this? It feels like Java is making jump through quite a few hoops to express this simple idea. I have started looking at Google Guava, and have seen I can use Objects.equal() to improve matters:

boolean areNamesEqual(Person person1, Person person2) {
  if(person1 == null || person2 == null) {
    return person1 == person2;
  }
  if(Objects.equal(person1.getFirstName(), person2.getFirstName())) {
    return false;
  }
  if(Objects.equal(person1.getSecondName(), person2.getSecondName())) {
    return false;
  }
  return true;
}

But I still have to check for the Person objects themselves being null, and write getFirstName() and getSecondName() twice each. It feels like there must be a better way expressing this.

Code like this would be ideal:

(person1, person2).arePropertiesEqual(firstName, secondName)

With this I don't have check for null anywhere, I don't have to return early, and I don't have to write firstName or secondName more than once.

Any ideas?


Solution

  • With Java (at least before Java 8 and the lambdas) and Guava, you won't get far as Java is not a functional language. See the Caveats section in Functional Explained in the Guava Wiki for an extra opinion.

    Actually, you can do it, but at the cost of more code, so you should really evaluate whether you need it. Something like:

    private <T> boolean arePropertiesEqual(T t1, T t2, Function<T, ?>... functions) {
        if (t1 == null || t2 == null) {
            return t1 == t2; // Shortcut
        }
        for (Function<T, ?> function : functions) {
            if (!Objects.equal(function.apply(t1), function.apply(t2))) {
                return false;
            }
        }
        return true;
    }
    
    private static class PersonFirstNameFunction 
            implements Function<Person, String> {
        @Override
        public String apply(Person input) {
            return input.getFirstName();
        }
    }
    
    private static class PersonLastNameFunction
            implements Function<Person, String> {
        @Override
        public String apply(Person input) {
            return input.getLastName();
        }
    }
    
    private void someMethod(Person p1, Person p2) {
        boolean b = arePropertiesEqual(p1, p2, 
                new PersonFirstNameFunction(), new PersonLastNameFunction());
    }
    

    Note: I've neither compiled nor run the code above, it probably has at least warnings due to the use of generic arrays (in the varargs).

    It's already shorter with Java 8, looking something like:

    private <T> boolean arePropertiesEqual(T t1, T t2, Function<T, ?>... functions) {
        if (t1 == null || t2 == null) {
            return t1 == t2; // Shortcut
        }
        for (Function<T, ?> function : functions) {
            if (!Objects.equals(function.apply(t1), function.apply(t2))) {
                return false;
            }
        }
        return true;
    }
    
    private void someMethod(Person p1, Person p2) {
        boolean b = arePropertiesEqual(p1, p2,
                Person::getFirstName, Person::getLastName);
    }
    

    Note that here it's using java.util.Objects and java.util.functions.Function, not Guava.


    Now, is that really "better" than the imperative code?