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 is to use serialization, as shown earlier. Another 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, but in fact it doesn’t work. Here’s an example:
//: appendixa:CopyConstructor.java
// A constructor for copying an object of the same
// type, as an attempt to create a local copy.
import com.bruceeckel.simpletest.*;
import java.lang.reflect.*;
class FruitQualities {
private int weight;
private int color;
private int firmness;
private int ripeness;
private int smell;
// etc.
public FruitQualities() { // Default constructor
// Do something meaningful...
}
// Other constructors:
// ...
// Copy constructor:
public FruitQualities(FruitQualities f) {
weight = f.weight;
color = f.color;
firmness = f.firmness;
ripeness = f.ripeness;
smell = f.smell;
// etc.
}
}
class Seed {
// Members...
public Seed() { /* Default constructor */ }
public Seed(Seed s) { /* Copy constructor */ }
}
class Fruit {
private FruitQualities fq;
private int seeds;
private Seed[] s;
public 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:
public Fruit(Fruit f) {
fq = new FruitQualities(f.fq);
seeds = f.seeds;
s = new Seed[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 {
public Tomato() {
super(new FruitQualities(), 100);
}
public Tomato(Tomato t) { // Copy-constructor
super(t); // Upcast for base copy-constructor
// Other copy-construction activities...
}
}
class ZebraQualities extends FruitQualities {
private int stripedness;
public ZebraQualities() { // Default constructor
super();
// do something meaningful...
}
public ZebraQualities(ZebraQualities z) {
super(z);
stripedness = z.stripedness;
}
}
class GreenZebra extends Tomato {
public GreenZebra() {
addQualities(new ZebraQualities());
}
public GreenZebra(GreenZebra g) {
super(g); // Calls Tomato(Tomato)
// Restore the right qualities:
addQualities(new ZebraQualities());
}
public void evaluate() {
ZebraQualities zq = (ZebraQualities)getQualities();
// Do something with the qualities
// ...
}
}
public class CopyConstructor {
private static Test monitor = new Test();
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 ripen2(Tomato t) {
try {
Class c = t.getClass();
// Use the "copy constructor":
Constructor ct = c.getConstructor(new Class[] { c });
Object obj = ct.newInstance(new Object[] { t });
System.out.println("In ripen2, t is a " +
obj.getClass().getName());
}
catch(Exception e) { System.out.println(e); }
}
public static void slice2(Fruit f) {
try {
Class c = f.getClass();
Constructor ct = c.getConstructor(new Class[] { c });
Object obj = ct.newInstance(new Object[] { f });
System.out.println("In slice2, f is a " +
obj.getClass().getName());
}
catch(Exception e) { System.out.println(e); }
}
public static void main(String[] args) {
Tomato tomato = new Tomato();
ripen(tomato); // OK
slice(tomato); // OOPS!
ripen2(tomato); // OK
slice2(tomato); // OK
GreenZebra g = new GreenZebra();
ripen(g); // OOPS!
slice(g); // OOPS!
ripen2(g); // OK
slice2(g); // OK
g.evaluate();
monitor.expect(new String[] {
"In ripen, t is a Tomato",
"In slice, f is a Fruit",
"In ripen2, t is a Tomato",
"In slice2, f is a Tomato",
"In ripen, t is a Tomato",
"In slice, f is a Fruit",
"In ripen2, t is a GreenZebra",
"In slice2, f is a GreenZebra"
});
}
} ///:~
This seems a bit strange at first. Sure, fruit has qualities, but why not just put fields 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 and using composition, 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 reference 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)[119] 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( ). From the output, you can see the problem. 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. Furthermore, 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 preceding example proves that it does not work for Java. Why? In Java, everything that we manipulate is a reference, but in C++, you can have reference-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.