1

Is there a way to write a custom equals method compactly when trying to compare two objects but not relying on those objects' internal equals() method? For example, if I had two Foo objects like so:

public class Foo {
    int id;
    String name;

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        Foo item = (Foo) o;
        return id == item.id && listId == item.name;
    }
}

But, in a use case for the foo objects, lets say I just want them equated by their id. Keep in mind this is a toy example and the real use case has many more fields so I may have an object with 6 fields all being used in the overridden equals method but may want to only use 3 of them to do an equals outside the class when comparing two objects.

List<Foo> objType1;
List<Foo> objType2;

Compare the two lists and assert each Foo object is equal but only use a subset of the fields of Foo in the comparison. I dont want to touch the actual Foo object in any way. How can i do this outside of asserting by handing that each field I am interested in is equal?

John Baum
  • 3,183
  • 11
  • 42
  • 90

3 Answers3

1

The complexity of the solution really depends on the use case. Universally - no, one cannot do it without direct bytecode manipulation. A lot of Java APIs allow using custom Comparator as an option. E.g. if you want to compare two lists of Foo's with custom comparator:

List<Foo> list1 = ...
List<Foo> list2 = ...
Comparator<Foo> c = Comparator.comparing(Foo::getId);
boolean equal = list1.size() == list2.size() &&
                IntStream.range(0, list1.size())
                .allMatch(i -> c.compare(list1.get(i), list2.get(i)) == 0);

Note, this solution does not check for list1 or list2 being null's and assumes your Foo has standard getters like getId(). Also, if you don't deal with lists but with abstract iterable collections, you might want to look into zip implementations. Comparator.comapring() is chainable like this Comparator.comparing(Foo::getId).thenComparing(Foo::getAttrX).thenComparing(Foo:getAttrY)...; which is fairly convenient and readable.

Another option you might want to look at to customize equals() for a variety of cases is to use Proxy.newProxyInstance() with your custom equals override, i.e. auto-create proxy wrapper around your instances when filling collections etc.

UPDATE

Using Comparator.comparing().thenComparing()... chain might seem to be tricky. It helps to understand that lambdas for these functions need to extract either primitives or Comparable descendants (i.e. implements Comparable and has compareTo() method) from the given top level object reference - in our case, Foo. If Foo would have a Bar getBar() accessor that has to be included into comparison, then either go down to the primitive - .thenComparing(f -> f.getBar().getName()) or make Bar implement Comparable. Don't forget to treat nulls properly, if you go the route of custom lambda functions - which is, sometimes, a challenge on it's own.

The positive of an approach in this answer is that Comparator defines a total order over the set of objects stored in the lists. The negative of this approach is, this total order is not really needed for simple comparison - if it is really all you need. In some cases, writing a good old for loop and doing all the comparisons "manually" might be less confusing. From experience, in most cases having an order is beneficial, if not now, then in the next release.

Alex Pakka
  • 9,466
  • 3
  • 45
  • 69
  • If I were to use the chaining approach, how would the usage of that change? Right now, with one id comparison, you use .allMatch(i -> c.compare(list1.get(i), list2.get(i)) == 0); – John Baum Sep 26 '16 at 22:18
  • @JohnBaum Usage does not change at all. It all turns into one for-loop with a single comparison block for all the chained attributes in its body. Also, it stops looping as soon as first mismatch is found. – Alex Pakka Sep 26 '16 at 23:53
  • I could only get the Comparator.comparing to work for primitives. ie. if I have a custom object as a field in Foo (like new Bar()), there would be a compilation error. – John Baum Sep 26 '16 at 23:55
  • @AlexPakka: What do you mean by "Don't forget to treat nulls properly, if you go the route of custom lambda functions"? Don't I have to worry about nulls when using e.g. Foo:getAttrX? What if that getter can return null? – Karsten Spang Apr 09 '21 at 14:41
  • @KarstenSpang `f.getBar().getName()` will NPE, if `f.getBar()` returns `null` - this is where you need to take care of checking for it. However, if `getAttrX` sometimes returns `null`, you can use a construct: `Comparator.nullsFirst(comparing(Foo:getAttrX))`. – Alex Pakka Apr 09 '21 at 15:32
0

You could add some extra field(s) to your Foo class that will be used in your overriden equals method to specify which fields should be used to deteremine whether two instances are equal. You could then set those fields before comparing. Then your equals method might contain:

   if ( useFiledA ) {
      if ( this.a != item.a ) return false;
   }
   if ( useFiledB ) {
      if ( this.b != item.b ) return false;
   }
   // etc.
   return true;
FredK
  • 4,094
  • 1
  • 9
  • 11
0

implements Comparable from your classes. then write the compareTo method. iterate ocer the first list and call colpareTo over all the elements of the second list

Tokazio
  • 516
  • 2
  • 18