Running on Java 24-ea+24-2960 (Preview)
Home of The JavaSpecialists' Newsletter

126Proxy equals()

Author: Dr. Heinz M. KabutzDate: 2006-05-14Java Version: 1.3Category: Language
 

Abstract: When we make proxies that wrap objects, we have to remember to write an appropriate equals() method. Instead of comparing on object level, we need to either compare on interface level or use a workaround to achieve the comparisons on the object level, described in this newsletter.

 

Welcome to the 126th edition of The Java(tm) Specialists' Newsletter, which I wrote whilst sitting in a train driving through the Black Forest in Germany, en route to visiting my brother. According to Encarta, the Black Forest has 22,950 km of long-distance paths, making it a great place for cycling, hiking and cross-country skiing. This week I am in Karlsruhe, so please let me know if you would like to meet one evening for a beer and chat.

javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.

Proxy equals()

The topic for this newsletter was inspired by Rodolphe Huard from Montpellier in France. Rodolphe sent me an email reminding me of the problem of an incorrectly coded equals() method in proxies. Special care needs to be taken to ensure its correctness.

Since my train is bumping me through the Black Forest, let's define a Forest interface and a ForestImpl class:

    public interface Forest {
      String getColour();
    }

    public class ForestImpl implements Forest {
      private final String colour;
      public ForestImpl(String colour) {
        this.colour = colour;
      }
      public String getColour() {
        return colour;
      }
    }

We are going to create a dynamic proxy of the Forest that will wrap the ForestImpl. For our example, we are not going to add any additional functionality or control to the proxy object. To create a dynamic proxy, we need an InvocationHandler that takes care of method calls and delegates them to the wrapped object.

    import java.lang.reflect.*;

    public class DelegationHandler implements InvocationHandler {
      private final Object wrapped;
      public DelegationHandler(Object wrapped) {
        this.wrapped = wrapped;
      }
      public Object invoke(Object proxy, Method method, Object[] args)
          throws Throwable {
        System.out.println("Called: " + method);
        return method.invoke(wrapped, args);
      }
    }

In our test code, we create two proxy instances that are wrapping the same object. We would expect the objects to be different, but the equals() to be the same. But this is not what happens:

    import java.lang.reflect.Proxy;

    public class ProxyEquality {
      public static void main(String[] args) {
        DelegationHandler dh = new DelegationHandler(
            new ForestImpl("Black"));
        Forest i0 = make(dh);
        Forest i1 = make(dh);
        System.out.println(i0 == i1); // should be false
        System.out.println(i0.equals(i1)); // should be true

      }
      private static Forest make(DelegationHandler dh) {
        return (Forest) Proxy.newProxyInstance(
            Forest.class.getClassLoader(),
            new Class[] {Forest.class}, dh);
      }
    }

The output however is different to what we would expect. The equals() method does get called, but it returns "false".

    false
    Called: public boolean Object.equals(Object)
    false

If we want to compare on object identity inside our equals() method (i.e. with "=="), then we might try this:

    public class ForestImpl implements Forest {
      private final String colour;
      public ForestImpl(String colour) {
        this.colour = colour;
      }
      public String getColour() {
        return colour;
      }
      public boolean equals(Object obj) {
        return this == obj;
      }
    }

However, this does not work, because we are comparing the wrapped object to a proxy object, which is obviously false. You can verify this by changing the equals() method to this:

    public boolean equals(Object obj) {
      System.out.println(System.identityHashCode(obj));
      System.out.println(System.identityHashCode(this));
      return this == obj;
    }

This means that we will have to write a proper equals method, one that takes object identity into account using some fields. (Don't forget to pair the equals() and hashCode() methods). This should work, right?

    public boolean equals(Object o) {
      if (this == o) return true;
      if (!(o instanceof ForestImpl)) return false;
      return colour.equals(((ForestImpl) o).colour);
    }

When we run it, the equals() method still returns "false"! It actually fails on the instanceof test. The object we are comparing ourselves to is the proxy object, not the wrapped object. We therefore need to change the test to look at the common interface:

One disadvantage is that we have to now expose our object's identifying state so that we can compare it. In Java, we can access private fields of other objects of the same type, as is typically done in the classical equals method. However, if we are comparing on the interface level, we need to expose the internal identifying state in the interface:

    public boolean equals(Object o) {
      if (this == o) return true;
      if (!(o instanceof Forest)) return false;
      Forest forest = (Forest) o;
      return colour.equals(forest.getColour());
    }

There is a workaround that would help you to avoid exposing the identity state of the object to the public. This might be important to you, but would obviously complicate matters a bit. We start by defining an interface for comparing objects:

    public interface ProxyEquals {
      boolean equalsCallBack(Object o, boolean calledOnWrappedObject);
    }

We let the Forest interface extend the ProxyEquals interface and implement it in the ForestImpl class:

    public boolean equals(Object o) {
      return equalsCallBack(o, false);
    }
    public boolean equalsCallBack(Object o, boolean calledOnWrappedObject) {
      if (!(o instanceof Forest)) return false;
      Forest forest = (Forest) o;
      if (calledOnWrappedObject) {
        if (!(forest instanceof ForestImpl)) return false;
        return colour.equals(((ForestImpl)o).colour);
      } else {
        return forest.equalsCallBack(this, true);
      }
    }

This more complicated mechanism now returns "true" when we run the ProxyEquality test class, without us needing to expose the inner state of our objects.

This morning was the first time that I sat in a Porsche. My buddy Marcus, whom I am staying with in Karlsruhe, took me to the train station and demonstrated the cornering to me. "I want one" best describes my sentiments. This was the first vehicle I sat in that cornered better than my Alfa 156. Problem in South Africa is that such vehicles get slapped with a prohibitive import duty so that they will cost approximately double to what they do in Germany. So it will remain just a dream :)

Kind regards

Heinz

 

Comments

We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack '23

Superpack '23 Our entire Java Specialists Training in one huge bundle more...

Free Java Book

Dynamic Proxies in Java Book
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...