Controlling cloneability

Bruce Eckel’s Thinking in Java Contents | Prev | Next

  1. 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.
  2. 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).
  3. 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.
  4. 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.
  5. 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.
  6. 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:

  1. Implement
    the
    Cloneable
    interface.
  2. Override
    clone( ).
  3. Call
    super.clone( )
    inside your
    clone( ).
  4. Capture
    exceptions inside your
    clone( ).

The
copy-constructor

//: 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:

t
= new Tomato(t);

while
slice( )
takes a more generic
Fruit
object and also duplicates it:

f
= new Fruit(f);

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?

More by Author

Must Read