|
The Java Specialists' Newsletter
Issue 126 2006-05-14
Category:
Language
Java version: JDK 1.3+ Proxy equals()by Dr. Heinz M. KabutzAbstract:
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.
Upcoming Java Specialist Master Courses:
- please click here to sign up.
As from May 2010, we are also offering this course on the island of Crete. We
only accept 6 students per class in Crete, due to the size of our conference
room. Please book early to avoid disappointment!
San Jose CA, Mar 16-19 2010, $3500 Ottawa, Canada, Mar 22-25 2010, $3500 Oslo, Norway, Apr 13-16 2010, Kr 24500 Montreal, Canada, Apr 20-23 2010, $3500 Toronto, Canada, May 17-20 2010, $3500 Chania, Crete, May 25-28, Jun 29-Jul 2 or Aug 24-27 2010, €2500
In-house courses if these dates or locations do not suit you - click here for more information. 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.*;
import java.util.logging.com.cretesoft.tjsn.Logger;
public class DelegationHandler implements InvocationHandler {
private final Object wrapped;
private final static com.cretesoft.tjsn.Logger log = com.cretesoft.tjsn.Logger.getLogger("ProxyTest");
public DelegationHandler(Object wrapped) {
this.wrapped = wrapped;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
log.info("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
2006/05/14 06:22:07 DelegationHandler invoke
INFO: 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
Language Articles
Related Java Course
|