The Artima Developer Community
Sponsored Link

Objects and Java by Bill Venners
Chapter 8:
Cloning, Collections, and Inner Classes

Advertisement

Although the previous chapter covered object initialization in great detail, it didn't quite cover all ways to initialize objects in Java, because it didn't cover all ways to create objects in Java. Aside from the new operator, which was the focus of the last chapter, Java offers two other ways to create objects: clone(), which is described in this chapter, and newInstance(), which is described in Part IV.

The newInstance() method, a member of class Class, is most often used in the context of class loaders and dynamic program extension. The clone() method, a member of class Object, is Java's mechanism for making copies of objects.

The clone() Method

In Java, the way to make an identical copy of an object is to invoke clone() on that object. When you invoke clone(), it should either:

  1. return an Object reference to a copy of the object upon which it is invoked, or
  2. throw CloneNotSupportedException

Because clone() is declared in class Object, it is inherited by every Java object. Object's implementation of clone() does one of two things, depending upon whether or not the object implements the Cloneable interface. If the object doesn't implement the Cloneable interface, Object's implementation of clone() throws a CloneNotSupportedException. Otherwise, it creates a new instance of the object, with all the fields initialized to values identical to the object being cloned, and returns a reference to the new object.

The Cloneable interface doesn't have any members. It is an empty interface, used only to indicate cloning is supported by a class. Class Object doesn't implement Cloneable. To enable cloning on a class of objects, the class of the object itself, or one of its superclasses other than Object, must implement the Cloneable interface.

In class Object, the clone() method is declared protected. If all you do is implement Cloneable, only subclasses and members of the same package will be able to invoke clone() on the object. To enable any class in any package to access the clone() method, you'll have to override it and declare it public, as is done below. (When you override a method, you can make it less private, but not more private. Here, the protected clone() method in Object is being overridden as a public method.)

// In Source Packet in file clone/ex1/CoffeeCup.java
class CoffeeCup implements Cloneable {

    private int innerCoffee;

    public Object clone() {
        try {
            return super.clone();
        }
        catch (CloneNotSupportedException e) {
            // This should never happen
            throw new InternalError(e.toString());
        }
    }

    public void add(int amount) {
        innerCoffee += amount;
    }

    public int releaseOneSip(int sipSize) {
        int sip = sipSize;
        if (innerCoffee < sipSize) {
            sip = innerCoffee;
        }
        innerCoffee -= sip;
        return sip;
    }

    public int spillEntireContents() {
        int all = innerCoffee;
        innerCoffee = 0;
        return all;
    }
}

You could make a copy of this CoffeeCup class, which implements Cloneable, as follows:

// In Source Packet in file clone/ex1/Example1.java
class Example1 {
    public static void main(String[] args) {

        CoffeeCup original = new CoffeeCup();
        original.add(75); // Original now contains 75 ml of coffee
        CoffeeCup copy = (CoffeeCup) original.clone();
        copy.releaseOneSip(25); // Copy now contains 50 ml of coffee

        // Figure 15-1 shows the heap at this point in the program

        int origAmount = original.spillEntireContents();
        int copyAmount = copy.spillEntireContents();
        System.out.println("Original has " + origAmount
            + " ml of coffee.");
        System.out.println("Copy has " + copyAmount
            + " ml of coffee.");
    }
}

In this example, a new CoffeeCup object is instantiated and given an initial 75 ml of coffee. The clone() method is then invoked on the CoffeeCup object. Because class CoffeeCup declares a clone() method, that method is executed when clone() is invoked on the CoffeeCup object referred to by the original reference. CoffeeCup's clone() does just one thing: invoke the clone() method in CoffeeCup's superclass, Object. The first thing Object's clone() does is check to see whether the object's class implements the Cloneable interface. This test passes because CoffeeCup, the object's class, does indeed implement Cloneable. The clone() method then creates a new instance of CoffeeCup, and initializes its one field, innerCoffee, to 75--the same value it has in the CoffeeCup object being cloned. Object's clone()returns a reference to the new object, which is then returned by CoffeeCup's clone().

The reference returned by clone() refers to a CoffeeCup object, but the reference itself is of type Object. The code above downcasts the returned reference from Object to CoffeeCup before assigning it to local variable copy. At this point, both CoffeeCup objects-- original and copy--contain 75 ml of coffee. Finally, 25 ml is removed from the copy, so it ends up with only 50 ml of coffee. A graphical representation of the result inside the Java Virtual Machine of executing the first four statements in main() is shown in Figure 15-1. (As mentioned in the last chapter, the native pointer to class information shown here is just one potential way a Java Virtual Machine could connect instance data to its class information.)


Figure 15-1. Cloning a CoffeeCup.

CoffeeCup's clone() implementation surrounds the call to Object's clone implementation with a try block so it can catch CloneNotSupportedException. This exception should never actually be thrown by Object's clone(), because in this case, CoffeeCup correctly implements Cloneable. If CoffeeCup's clone() didn't explicitly catch it, however, then clone() would have to declare in a throws clause that it may throw CloneNotSupportedException. This would force any method invoking clone() on a CoffeeCup object to deal with the exception, either by explicitly catching it or declaring it in their own throws clause. Thus, CoffeeCup's clone() catches CloneNotSupportedException to make it simpler for other methods to invoke clone() on a CoffeeCup.

Cloning Objects that Contain Other Objects

If you wish to enable cloning of an object that includes object references as part of its instance data, you may have to do more work in clone() than just calling super.clone(). Clone should return an independent copy of the object. Object's clone() will copy the value of each instance variable from the original object into the corresponding instance variables of the copy object. If one of those variables is an object reference, the copy object will get a duplicate reference to the same object.

As an example, consider this version of CoffeeCup, in which the innerCoffee field has been upgraded from a mere int to a full fledged object reference:

// In Source Packet in file clone/ex2/CoffeeCup.java
class CoffeeCup implements Cloneable {

    private Coffee innerCoffee = new Coffee(0);

    public Object clone() {
        try {
            return super.clone();
        }
        catch (CloneNotSupportedException e) {
            // This should never happen
            throw new InternalError(e.toString());
        }
    }

    public void add(int amount) {
        innerCoffee.add(amount);
    }

    public int releaseOneSip(int sipSize) {
        return innerCoffee.remove(sipSize);
    }

    public int spillEntireContents() {
        return innerCoffee.removeAll();
    }
}

// In Source Packet in file clone/ex2/Coffee.java
public class Coffee implements Cloneable {

    private int volume; // Volume in milliliters

    Coffee(int volume) {
        this.volume = volume;
    }

    public Object clone() {
        try {
            return super.clone();
        }
        catch (CloneNotSupportedException e) {
            // This should never happen
            throw new InternalError(e.toString());
        }
    }

    public void add(int amount) {
        volume += amount;
    }

    public int remove(int amount) {
        int v = amount;
        if (volume < amount) {
            v = volume;
        }
        volume -= v;
        return v;
    }

    public int removeAll() {
        int all = volume;
        volume = 0;
        return all;
    }
}

Given these declarations of CoffeeCup and Coffee, there would be a surprise waiting for any method that attempts to clone a CoffeeCup object:

// In Source Packet in file clone/ex2/Example2.java
class Example2 {
    public static void main(String[] args) {

        CoffeeCup original = new CoffeeCup();
        original.add(75); // Original now contains 75 ml of coffee
        CoffeeCup copy = (CoffeeCup) original.clone();
        copy.releaseOneSip(25);
        // Copy now contains 50 ml of coffee.
        // Unfortunately, so does original.

        // Figure 15-2 shows the heap at this point in the program

        int origAmount = original.spillEntireContents();
        int copyAmount = copy.spillEntireContents();
        System.out.println("Original has " + origAmount
            + " ml of coffee.");
        System.out.println("Copy has " + copyAmount
            + " ml of coffee.");
    }
}

Here, when releaseOneSip() is invoked on copy with a parameter of 25 ml, that amount of coffee is correctly removed from the CoffeeCup object referenced by copy. The trouble is that 25 ml of coffee is also removed from the cup referenced by original. The reason is that both the original and copy objects contain a reference to the same Coffee object. A graphical representation of the result of these statements is shown in Figure 15-2.


Figure 15-2. Incorrect cloning of a CoffeeCup that contains object references.

To rectify this situation, you need to modify CoffeeCup's clone() method:

// In Source Packet in file clone/ex3/CoffeeCup.java
class CoffeeCup implements Cloneable {

    private Coffee innerCoffee = new Coffee(0);

    public Object clone() {
        CoffeeCup copyCup = null;
        try {
            copyCup = (CoffeeCup) super.clone();
        }
        catch (CloneNotSupportedException e) {
            // this should never happen
            throw new InternalError(e.toString());
        }
        copyCup.innerCoffee = (Coffee) innerCoffee.clone();
        return copyCup;
    }

    public void add(int amount) {
        innerCoffee.add(amount);
    }

    public int releaseOneSip(int sipSize) {
        return innerCoffee.remove(sipSize);
    }

    public int spillEntireContents() {
        return innerCoffee.removeAll();
    }
}

In this version of clone(), Object's clone() is invoked as before. But instead of simply returning the reference to the new CoffeeCup object created by Object's clone(), the new CoffeeCup object is modified before it is returned. First, the Coffee object referenced by innerCoffee is cloned. A reference to the cloned Coffee object is then stored in the innerCoffee variable of the cloned CoffeeCup object . At this point, the original object and the clone refer to their own Coffee objects, but those Coffee objects are exact duplicates of each other.

If you now performed the same statements on this version of CoffeeCup, you would once again have the expected behavior:

// In Source Packet in file clone/ex3/Example3.java
class Example3 {
    public static void main(String[] args) {

        CoffeeCup original = new CoffeeCup();
        original.add(75); // original now contains 75 ml of coffee
        CoffeeCup copy = (CoffeeCup) original.clone();
        copy.releaseOneSip(25);
        // Copy now contains 50 ml of coffee.
        // Original still has 75 ml of coffee.

        // Figure 15-3 shows the heap at this point in the program

        int origAmount = original.spillEntireContents();
        int copyAmount = copy.spillEntireContents();
        System.out.println("Original has " + origAmount
            + " ml of coffee.");
        System.out.println("Copy has " + copyAmount
            + " ml of coffee.");
    }
}

Because the CoffeeCup objects referenced by original and copy each have their own Coffee objects, when copy's was reduced by 25 ml, original's wasn't affected. A graphical representation of the result of these statements is shown in Figure 15-3.


Figure 15-3. Proper cloning of a CoffeeCup that contains object references.

These examples demonstrate the customary approach to writing clone(). The first thing to do in any clone() method (besides Object's) is invoke super.clone(). This will cause Object's implementation of clone() to be executed first. This scheme is similar to that of constructors, in which an invocation of the superclass's constructor is always executed first. Object's clone() will create a new instance of the class and copy the values contained in the original's instance data to the new object's instance data. Catching CloneNotSupportedException is also a good idea, to make calling clone() on that class of objects simpler to code.

When super.clone() returns, a clone() method should make clones of any mutable objects referenced by its instance variables, and assign these clones to the instance variables of the copy. A mutable object is one whose state can change over the course of its lifetime. An object whose state can't change is immutable.

An example of an immutable object is String. You must give a value to a String when you create it. Once created, a String's value can't change over the lifetime of the String object. The same is true for the wrapper objects Integer, Float, and so on. You assign them a value when they are created, and there is no way to change it for the remainder of their lifetimes.

The real trouble with the clone() method shown above that didn't clone Coffee was that Coffee is mutable. When the state of the Coffee object changed (volume changed from 75 to 50), both CoffeeCup objects saw their own internal state change. Had CoffeeCup included an instance variable of type String, you wouldn't have had to clone it because Strings are immutable. (In fact, you couldn't have cloned it, because String doesn't implement Cloneable. Since Strings are immutable, it doesn't make sense to clone them.)

Disallowing Cloning

Java's cloning mechanism enables you to allow cloning, allow cloning conditionally, or forbid cloning altogether. If you wish to completely forbid cloning, you have a few different approaches to choose from. To decide which way to forbid cloning upon a particular class of objects, you must know something about the class's superclasses.

If none of the superclasses implement Cloneable or override Object's clone() method, you can prevent cloning of objects of that class quite easily. Simply don't implement the Cloneable interface and don't override the clone() method in that class. The class will inherit Object's clone() implementation, which will throw CloneNotSupportedException anytime clone() is invoked on objects of that class. All the classes shown as examples in this book prior to the CoffeeCup class declared immediately above used this method of preventing cloning. By doing nothing, they disallowed cloning. Thus, forbidding cloning is the default behavior for an object.

In cases where a superclass already implements Cloneable, and you don't want the subclass to be cloned, you'll have to override clone() in the subclass and throw a CloneNotSupportedException yourself. In this case, instances of the superclass will be clonable, but instances of the subclass will not.

Clone and the Copy Constructor

For those of you who know C++, Java's clone() method is what happened to C++'s copy constructor. For those of you who don't know C++, a copy constructor is one which takes a single parameter of the same type of the class. In the body of the copy constructor, you have to copy all values from the object passed as a parameter to the object under construction. Like Java's clone() method, in a C++ copy constructor, you should allocate new memory for objects referenced (or pointed to) from instance (member) variables. For example, a copy constructor for class CoffeeCup would be:

// In Source Packet in file clone/ex4/CoffeeCup.java
// Copy constructors are not the Java way...
class CoffeeCup {

    private int innerCoffee;

    public CoffeeCup(CoffeeCup cup) {
        innerCoffee = cup.innerCoffee;
    }
    //...
}

One of the primary uses of the copy constructor in C++ is to pass objects by value. The copy constructor is used to create a copy of an object that is passed by value to a function. This is not an issue in Java, because all objects in Java programs are passed by reference.

If you are a C++ programmer and feel the urge to write a copy constructor in a Java class, STOP! Close your eyes. Take a few deep breaths. Then--when you feel your ready--open your eyes, implement Cloneable and write clone(). It will be OK.


Sponsored Links



Google
  Web Artima.com   
Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use