Controlling
cloneability
You
might suggest that, to remove clonability,
the
clone( )
method simply be made
private,
but this won’t work since you cannot take a base-class method and make it
more
private
in a derived class. So it’s not that simple. And yet, it’s
necessary to be able to control whether an object can be cloned. There are
actually a number of attitudes you can take to this in a class that you design:
- Indifference.
You don’t do anything about cloning, which means that your class
can’t be cloned but a class that inherits from you can add cloning if it
wants. This works only if the default
Object.clone( )
will do something reasonable with all the fields in your class.
- Support
clone( ).
Follow the standard practice of implementing
Cloneable
and overriding
clone( ).
In the overridden
clone( ),
you call
super.clone( )
and
catch all exceptions (so your overridden
clone( )
doesn’t throw any exceptions).
- Support
cloning conditionally. If your class holds handles to other objects that might
or might not be cloneable (an example of this is a collection class), you can
try to clone all of the objects that you have handles to as part of your
cloning, and if they throw exceptions just pass them through. For example,
consider a special sort of
Vector
that tries to clone all the objects it holds. When you write such a
Vector,
you don’t know what sort of objects the client programmer might put into
your
Vector,
so you don’t know whether they can be cloned.
- Don’t
implement
Cloneable
but override
clone( )
as
protected,
producing the correct copying behavior for any fields. This way, anyone
inheriting from this class can override
clone( )
and call
super.clone( )
to produce the correct copying behavior. Note that your implementation can and
should invoke
super.clone( )
even though that method expects a
Cloneable
object (it will throw an exception otherwise), because no one will directly
invoke it on an object of your type. It will get invoked only through a derived
class, which, if it is to work successfully, implements
Cloneable.
- Try
to prevent cloning by not implementing
Cloneable
and overriding
clone( )
to throw an exception. This is successful only if any class derived from this
calls
super.clone( )
in its redefinition of
clone( ).
Otherwise, a programmer may be able to get around it.
- Prevent
cloning by making your class
final.
If
clone( )
has not been overridden by any of your ancestor classes, then it can’t
be. If it has, then override it again and throw
CloneNotSupportedException.
Making the class
final
is
the only way to guarantee that cloning is prevented. In addition, when dealing
with security objects or other situations in which you want to control the
number of objects created you should make all constructors
private
and provide one or more special methods for creating objects. That way, these
methods can restrict the number of objects created and the conditions in which
they’re created. (A particular case of this is the
singleton
pattern shown in Chapter 16.)
Here’s
an example that shows the various ways cloning can be implemented and then,
later in the hierarchy, “turned off:”
//: CheckCloneable.java
// Checking to see if a handle can be cloned
// Can't clone this because it doesn't
// override clone():
class Ordinary {}
// Overrides clone, but doesn't implement
// Cloneable:
class WrongClone extends Ordinary {
public Object clone()
throws CloneNotSupportedException {
return super.clone(); // Throws exception
}
}
// Does all the right things for cloning:
class IsCloneable extends Ordinary
implements Cloneable {
public Object clone()
throws CloneNotSupportedException {
return super.clone();
}
}
// Turn off cloning by throwing the exception:
class NoMore extends IsCloneable {
public Object clone()
throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
}
class TryMore extends NoMore {
public Object clone()
throws CloneNotSupportedException {
// Calls NoMore.clone(), throws exception:
return super.clone();
}
}
class BackOn extends NoMore {
private BackOn duplicate(BackOn b) {
// Somehow make a copy of b
// and return that copy. This is a dummy
// copy, just to make the point:
return new BackOn();
}
public Object clone() {
// Doesn't call NoMore.clone():
return duplicate(this);
}
}
// Can't inherit from this, so can't override
// the clone method like in BackOn:
final class ReallyNoMore extends NoMore {}
public class CheckCloneable {
static Ordinary tryToClone(Ordinary ord) {
String id = ord.getClass().getName();
Ordinary x = null;
if(ord instanceof Cloneable) {
try {
System.out.println("Attempting " + id);
x = (Ordinary)((IsCloneable)ord).clone();
System.out.println("Cloned " + id);
} catch(CloneNotSupportedException e) {
System.out.println(
"Could not clone " + id);
}
}
return x;
}
public static void main(String[] args) {
// Upcasting:
Ordinary[] ord = {
new IsCloneable(),
new WrongClone(),
new NoMore(),
new TryMore(),
new BackOn(),
new ReallyNoMore(),
};
Ordinary x = new Ordinary();
// This won't compile, since clone() is
// protected in Object:
//! x = (Ordinary)x.clone();
// tryToClone() checks first to see if
// a class implements Cloneable:
for(int i = 0; i < ord.length; i++)
tryToClone(ord[i]);
}
} ///:~
The
first class,
Ordinary,
represents the kinds of classes we’ve seen throughout the book: no
support for cloning, but as it turns out, no prevention of cloning either. But
if you have a handle to an
Ordinary
object that might have been upcast from a more derived class, you can’t
tell if it can be cloned or not.
The
class
WrongClone
shows an incorrect way to implement cloning. It does override
Object.clone( )
and makes that method
public,
but it doesn’t implement
Cloneable,
so when
super.clone( )
is called (which results in a call to
Object.clone( )),
CloneNotSupportedException
is thrown so the cloning doesn’t work.
In
IsCloneable
you can see all the right actions performed for cloning:
clone( )
is overridden and
Cloneable
is implemented. However, this
clone( )
method and several others that follow in this example
do
not
catch
CloneNotSupportedException,
but instead pass it through to the caller, who must then put a try-catch block
around it. In your own
clone( )
methods you will typically catch
CloneNotSupportedException
inside
clone( )
rather than passing it through. As you’ll see, in this example it’s
more informative to pass the exceptions through.
Class
NoMore
attempts to “turn off” cloning in the way that the Java designers
intended: in the derived class
clone( ),
you throw
CloneNotSupportedException.
The
clone( )
method
in class
TryMore
properly calls
super.clone( ),
and this resolves to
NoMore.clone( ),
which throws an exception and prevents cloning.
But
what if the programmer doesn’t follow the “proper” path of
calling super.clone( )
inside the overridden
clone( )
method? In
BackOn,
you can see how this can happen. This class uses a separate method
duplicate( )
to
make a copy of the current object and calls this method inside
clone( )
instead
of calling
super.clone( ).
The exception is never thrown and the new class is cloneable. You can’t
rely on throwing an exception to prevent making a cloneable class. The only
sure-fire solution is shown in
ReallyNoMore,
which is
final
and thus cannot be inherited. That means if
clone( )
throws an exception in the
final
class, it cannot be modified with inheritance and the prevention of cloning is
assured. (You cannot explicitly call
Object.clone( )
from
a class that has an arbitrary level of inheritance; you are limited to calling
super.clone( ),
which has access to only the direct base class.) Thus, if you make any objects
that involve security issues, you’ll want to make those classes
final. The
first method you see in class
CheckCloneable
is
tryToClone( ),
which takes any
Ordinary
object and checks to see whether it’s cloneable with
instanceof.
If so, it casts the object to an
IsCloneable,
calls
clone( )
and casts the result back to
Ordinary,
catching any exceptions that are thrown. Notice the use of run-time type
identification (see Chapter 11) to print out the class name so you can see
what’s happening.
In
main( ),
different types of
Ordinary
objects are created and upcast to
Ordinary
in the array definition. The first two lines of code after that create a plain
Ordinary
object and try to clone it. However, this code will not compile because
clone( )
is a
protected
method in
Object.
The remainder of the code steps through the array and tries to clone each
object, reporting the success or failure of each. The output is:
Attempting IsCloneable
Cloned IsCloneable
Attempting NoMore
Could not clone NoMore
Attempting TryMore
Could not clone TryMore
Attempting BackOn
Cloned BackOn
Attempting ReallyNoMore
Could not clone ReallyNoMore
So
to summarize, if you want a class to be cloneable:
- Implement
the
Cloneable
interface.
- Override
clone( ).
- Call
super.clone( )
inside your
clone( ).
- Capture
exceptions inside your
clone( ).
This
will produce the most convenient effects.
The
copy-constructor
Cloning
can seem to be a complicated process to set up. It might seem like there should
be an alternative. One approach that might occur to you (especially if
you’re a C++ programmer) is to make a special constructor whose job it is
to duplicate an object. In C++, this is called the copy
constructor
.
At first, this seems like the obvious solution. Here’s an example:
//: CopyConstructor.java
// A constructor for copying an object
// of the same type, as an attempt to create
// a local copy.
class FruitQualities {
private int weight;
private int color;
private int firmness;
private int ripeness;
private int smell;
// etc.
FruitQualities() { // Default constructor
// do something meaningful...
}
// Other constructors:
// ...
// Copy constructor:
FruitQualities(FruitQualities f) {
weight = f.weight;
color = f.color;
firmness = f.firmness;
ripeness = f.ripeness;
smell = f.smell;
// etc.
}
}
class Seed {
// Members...
Seed() { /* Default constructor */ }
Seed(Seed s) { /* Copy constructor */ }
}
class Fruit {
private FruitQualities fq;
private int seeds;
private Seed[] s;
Fruit(FruitQualities q, int seedCount) {
fq = q;
seeds = seedCount;
s = new Seed[seeds];
for(int i = 0; i < seeds; i++)
s[i] = new Seed();
}
// Other constructors:
// ...
// Copy constructor:
Fruit(Fruit f) {
fq = new FruitQualities(f.fq);
seeds = f.seeds;
// Call all Seed copy-constructors:
for(int i = 0; i < seeds; i++)
s[i] = new Seed(f.s[i]);
// Other copy-construction activities...
}
// To allow derived constructors (or other
// methods) to put in different qualities:
protected void addQualities(FruitQualities q) {
fq = q;
}
protected FruitQualities getQualities() {
return fq;
}
}
class Tomato extends Fruit {
Tomato() {
super(new FruitQualities(), 100);
}
Tomato(Tomato t) { // Copy-constructor
super(t); // Upcast for base copy-constructor
// Other copy-construction activities...
}
}
class ZebraQualities extends FruitQualities {
private int stripedness;
ZebraQualities() { // Default constructor
// do something meaningful...
}
ZebraQualities(ZebraQualities z) {
super(z);
stripedness = z.stripedness;
}
}
class GreenZebra extends Tomato {
GreenZebra() {
addQualities(new ZebraQualities());
}
GreenZebra(GreenZebra g) {
super(g); // Calls Tomato(Tomato)
// Restore the right qualities:
addQualities(new ZebraQualities());
}
void evaluate() {
ZebraQualities zq =
(ZebraQualities)getQualities();
// Do something with the qualities
// ...
}
}
public class CopyConstructor {
public static void ripen(Tomato t) {
// Use the "copy constructor":
t = new Tomato(t);
System.out.println("In ripen, t is a " +
t.getClass().getName());
}
public static void slice(Fruit f) {
f = new Fruit(f); // Hmmm... will this work?
System.out.println("In slice, f is a " +
f.getClass().getName());
}
public static void main(String[] args) {
Tomato tomato = new Tomato();
ripen(tomato); // OK
slice(tomato); // OOPS!
GreenZebra g = new GreenZebra();
ripen(g); // OOPS!
slice(g); // OOPS!
g.evaluate();
}
} ///:~
This
seems a bit strange at first. Sure, fruit has qualities, but why not just put
data members representing those qualities directly into the
Fruit
class? There are two potential reasons. The first is that you might want to
easily insert or change the qualities. Note that
Fruit
has a
protected
addQualities( )
method to allow derived classes to do this. (You might think the logical thing
to do is to have a
protected
constructor in
Fruit
that takes a
FruitQualities
argument, but constructors don’t inherit so it wouldn’t be
available in second or greater level classes.) By making the fruit qualities
into a separate class, you have greater flexibility, including the ability to
change the qualities midway through the lifetime of a particular
Fruit
object.
The
second reason for making
FruitQualities
a separate object is in case you want to add new qualities or to change the
behavior via inheritance and polymorphism. Note that for
GreenZebra
(which really is a type of tomato – I’ve grown them and
they’re fabulous), the constructor calls
addQualities( )
and passes it a
ZebraQualities
object, which is derived from
FruitQualities
so it can be attached to the
FruitQualities
handle in the base class. Of course, when
GreenZebra
uses the
FruitQualities
it must downcast it to the correct type (as seen in
evaluate( )),
but it always knows that type is
ZebraQualities. You’ll
also see that there’s a
Seed
class, and that
Fruit
(which by definition carries its own seeds) contains an array of
Seeds. Finally,
notice that each class has a copy constructor, and that each copy constructor
must take care to call the copy constructors for the base class and member
objects to produce a deep copy. The copy constructor is tested inside the class
CopyConstructor.
The method
ripen( )
takes a
Tomato
argument
and performs copy-construction on it in order to duplicate the object:
while
slice( )
takes a more generic
Fruit
object and also duplicates it:
These
are tested with different kinds of
Fruit
in
main( ).
Here’s the output:
In ripen, t is a Tomato
In slice, f is a Fruit
In ripen, t is a Tomato
In slice, f is a Fruit
This
is where the problem shows up. After the copy-construction that happens to the
Tomato
inside
slice( ),
the result is no longer a
Tomato
object, but just a
Fruit.
It has lost all of its tomato-ness. Further, when you take a
GreenZebra,
both
ripen( )
and
slice( )
turn it into a
Tomato
and a
Fruit,
respectively. Thus, unfortunately, the copy constructor scheme is no good to us
in Java when attempting to make a local copy of an object.
Why
does it work in C++ and not Java?
The
copy constructor is a fundamental part of C++, since it automatically makes a
local copy of an object. Yet the example above proves that it does not work for
Java. Why? In Java everything that we manipulate is a handle, while in C++ you
can have handle-like entities and you can
also
pass around the objects directly. That’s what the C++ copy constructor is
for: when you want to take an object and pass it in by value, thus duplicating
the object. So it works fine in C++, but you should keep in mind that this
scheme fails in Java, so don’t use it.