|
|
|
Sponsored Link •
|
|
Advertisement
|
The value of the canonical object idiom
The canonical object idiom can be useful to you in several ways.
First, this idiom can guide you when you are deciding whether to
support cloning or serialization in a particular class, and which
java.lang.Object methods, if any, you should override in
that class. You can use this idiom as a starting point with each class
you define, and depart from the idiom only if you feel special
circumstances justify such a departure. In addition, knowledge of this
idiom should make your fellow programmers feel a vague sense of guilt
in their attempts to avoid thinking about these issues when they design
a class. And, hopefully, this guilt will encourage your coworkers to
design objects that support the baseline object services defined by the
canonical object idiom, and that should make their objects a bit easier
for you to use. Finally, one promising use of this idiom is that it
can serve as a starting point for discussion when formulating Java
coding standards for a project or organization.
Implementation guidelines
Here are some guidelines to help you make the most of the canonical
object idiom:
Make objects canonical by default
In general, you should implement the canonical object idiom in every
Java class you define, unless you have a specific reason not to.
Although you may not be able to imagine why someone would want to use a
particular class of objects in some of these ways, you have likely met
coworkers who are capable of surprising you in how they use your
classes. Besides, predicting the future is a difficult business. One of
these days even you may reuse your classes in some ways you
didn't imagine when you first designed the class.
The benefit of canonical objects is that they are more flexible (easy
to understand, use, and change) than their non-canonical brethren.
Canonical objects help make code flexible because they are ready to be
manipulated in the ways objects of any type are commonly manipulated.
By now you know that canonical objects can be cloned, serialized, and
semantically compared with equals, but they can also be used in other
common ways. Invoking toString() on a canonical object
will yield a reasonable result provided by the default implementation
of toString() in superclass Object.
Likewise, hashCode() works properly thanks to
Object's default implementation. getClass()
returns a reference to the appropriate Class instance, and
even the wait() and notify() methods work.
Everything works. Canonical objects are ready to do what you want them
to do.
Catch CloneNotSupportedException
The customary first step in any implementation of clone()
is to invoke the superclass's implementation of clone().
If you are writing a clone() method for a direct subclass
of class Object, you will need to either catch
CloneNotSupportedException or declare it in your
throws clause. If you forget to do either of these two
things, the compiler will dutifully inform you of your negligence.
So, given that the compiler will force you to deal with
CloneNotSupportedException in one way or the other, which
way should you deal with it? In general, you should catch
CloneNotSupportedException and throw some kind of
unchecked exception in the catch clause, the approach
demonstrated by the Worker class. Why? Because if you
declare CloneNotSupportedException in your
throws clause, anyone who wants to clone your object will
need to deal with the exception -- either by catching it or declaring
it in their throws clause. And you don't want to bother
clients of your class with all that hard decision-making just because
they want to clone your object.
It turns out that, so long as you implement Cloneable,
Object's implementation of clone() will never
throw CloneNotSupportedException. Object's
implementation of clone() checks to see if the object's
class implements Cloneable; if it does, it clones the
object by making a direct field-by-field copy of the original in the
clone. Only if the object's class doesn't implement
Cloneable will Object's implementation of
clone() throw CloneNotSupportedException. So
if you implement Cloneable, you may as well catch
CloneNotSupportedException just to keep it out of your
clone()'s throws clause.
The one risk to heeding this advice is that when you remove
CloneNotSupportedException from your
clone()'s throws clause, you tie the hands of
anyone who ever wants to disallow cloning in a subclass of your class.
The customary way to disallow cloning in a subclass of some class that
allows and supports cloning is to override clone() and
throw CloneNotSupportedException. Thus, you should
consider whether you want to enable subclasses to disallow cloning when
you implement clone().
My opinion is that if you are not sure, you should catch
CloneNotSupportedException, which effectively sets the
policy that all subclasses will be clonable. I believe situations in
which someone will want to disallow cloning in a subclass will be
rare. Therefore, the ease of use you gain by not forcing clients to
deal with CloneNotSupportedException outweighs the slight
risk that you will be frustrating someone who wants to disallow cloning
in a subclass at some point in the future.
Don't support cloning in immutable objects
If the object is immutable, you don't need to (and shouldn't) make it
clonable. The reason you clone an object is so that the two instances
can evolve independently thereafter. For example, you may clone an
object before passing it to a method that alters the object. Because
immutable objects can't evolve (their state doesn't ever change), there
is no need to clone them. Everyone can safely share the same immutable
instance.
Make equals() do a semantic compare
An important aspect of the canonical object idiom is implementing
equals() such that it does a semantic
comparison. Canonical objects override equals(), because
the default implementation of equals() in class
Object just returns true if one object
'==' the other object. In other words, comparing two
objects with equals() yields the same result, by default,
as comparing to objects with Java's == operator. Why are
there two ways to check objects for equality? Because they are
supposed to be different.
Java's == operator simply checks to see if two references
refer to the same object exactly. Invoking equals() on an
object is supposed to do a semantic compare: if the two objects "mean
the same thing," equals() should return
true.
What does it mean for an object to "mean the same thing" as another object? Well, that's what you, as designer of a class, get to decide. In general, however, two objects are semantically equal when they have the same class and their states are equal. In other words, semantic equality means that:
==).
null or semantically equivalent
(as reported by equals()).
For a bit more help on deciding how you should define
equals(), consider that any implementation of
equals() should have the following properties:
a, a.equals(null)
should return false.
a,
a.equals(a) should return true.
(equals() is reflexive.)
a and b,
a.equals(b) should return true if and only if
b.equals(a) returns true.
(equals() is symmetric.)
a, b, and
c, if a.equals(b) returns true
and b.equals(c) returns true, then
a.equals(c) should return true.
(equals() is transitive.)
a and b,
multiple invocations of a.equals(b) consistently return
true or consistently return false.
(equals() is consistent.)
Override hashCode()
Whenever you override equals(), you should override
hashCode(). hashCode() should return
the same hash value for any two objects that are semantically equal,
as determined by equals().
|
Sponsored Links
|