QR code

How I Would Re-design equals()

  • Copenhagen, Denmark
  • comments

Javajava OOPoop

I want to rant a bit about Java design, in particular about the methods Object.equals() and Comparable.compareTo(). I've hated them for years, because, no matter how hard I try to like them, the code inside looks ugly. Now I know what exactly is wrong and how I would design this "object-to-object comparing" mechanism better.

L'ultimo capodanno (1998) by Marco Risi
L'ultimo capodanno (1998) by Marco Risi

Say we have a simple primitive class Weight, objects of which represent the weight of something in kilos:

class Weight {
  private int kilos;
  Weight(int k) {
    this.kilos = k;
  }
}

Next, we want two objects of the same weight to be equal to each other:

new Weight(15).equals(new Weight(15));

Here is how such a method may look:

class Weight {
  private int kilos;
  Weight(int k) {
    this.kilos = k;
  }
  public boolean equals(Object obj) {
    if (!(obj instanceof Weight)) {
      return false;
    }
    Weight weight = Weight.class.cast(obj);
    return weight.kilos == this.kilos;
  }
}

The ugly part here is, first of all, the type casting with instanceof. The second problem is that we touch the internals of the incoming object. This design makes polymorphic behavior of the Weight impossible. We simply can't pass anything else to the equals() method, besides an instance of the class Weight. We can't turn it into an interface and introduce multiple implementations of it:

interface Weight {
  boolean equals(Object obj);
}

This code will not work:

class DefaultWeight implements Weight {
  // attribute and ctor skipped
  public boolean equals(Object obj) {
    if (!(obj instanceof Weight)) {
      return false;
    }
    Weight weight = Weight.class.cast(obj);
    return weight.kilos == this.kilos; // error here!
  }
}

The problem is that one object decides for the other whether they are equal. This inevitably leads to a necessity to touch private attributes in order to do the actual comparison.

What is the solution?

This is what I'm offering. Any comparison, no matter what types we are talking about, is about comparing two digital values. Either we compare a weight with a weight, text with text, or a user with a user—our CPUs can only compare numbers. Thus, we introduce a new interface Digitizable:

interface Digitizable {
  byte[] digits();
}

Next, we introduce a new class Comparison, which is the comparison of two streams of bytes (I'm not sure the code is perfect, I tested it here, feel free to improve and contribute with a pull request):

class Comparison<T extends Digitizable> {
  private T lt;
  private T rt;
  Comparison(T left, T right) {
    this.lt = left;
    this.rt = right;
  }
  int value() {
    final byte[] left = this.lt.digits();
    final byte[] right = this.rt.digits();
    int result = 0;
    int max = Math.max(left.length, right.length);
    for (int idx = max; idx > 0; --idx) {
      byte lft = 0;
      if (idx <= left.length) {
        lft = left[max - idx];
      }
      byte rht = 0;
      if (idx <= right.length) {
        rht = right[max - idx];
      }
      result = lft - rht;
      if (result != 0) {
        break;
      }
    }
    return (int) Math.signum(result);
  }
}

Now, we need Weight to implement Digitizable:

class Weight implements Digitizable {
  private int kilos;
  Weight(int k) {
    this.kilos = k;
  }
  @Override
  public byte[] digits() {
    return ByteBuffer.allocate(4)
      .putInt(this.kilos).array();
  }
}

Finally, this is how we compare them:

int v = new Comparison<Weight>(
  new Weight(400), new Weight(500)
).value();

This v will either be -1, 0, or 1. In this particular case it will be -1, because 400 is less than 500.

No more violation of encapsulation, no more type casting, no more ugly code inside those equals() and compareTo() methods. The class Comparison will work with all possible types. All our objects need to do in order to become comparable is to implement Digitizable and "provide" their bytes for inspection/comparison.

This approach is actually very close to the printers I described earlier.

sixnines availability badge