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.

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?


Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Instead of only managing projects organizations do need to manage value! "Doing the right things" and "doing things right" are the essential ingredients for successful software and systems delivery. Unfortunately, with distributed delivery spanning multiple disciplines, geographies and time zones, many organizations struggle with teams working in silos, broken lines of communication, lack of collaboration, inadequate traceability, and poor project visibility. This often results in organizations "doing the …

  • On-demand Event Event Date: April 22, 2014 Database professionals -- whether developers or DBAs -- can often save valuable time by learning to get the most from their new or existing productivity tools. Whether you're responsible for managing database projects, performing database health checks and reporting, analyzing code, or measuring software engineering metrics, it's likely you're not taking advantage of some of the lesser-known features of Toad from Dell. Attend this eSeminar with Dell Software's …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds