IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Thinking in Java, 3rd ed. Revision 4.0


précédentsommairesuivant

A. Passing & Returning Objects

By now you should be reasonably comfortable with the idea that when you're "passing" an object, you're actually passing a reference.

In many programming languages you can use that language's "regular" way to pass objects around, and most of the time everything works fine. But it always seems that there comes a point at which you must do something irregular, and suddenly things get a bit more complicated (or in the case of C++, quite complicated). Java is no exception, and it's important that you understand exactly what's happening as you pass objects around and manipulate them. This appendix will provide that insight.

Another way to pose the question of this appendix, if you're coming from a programming language so equipped, is "Does Java have pointers?" Some have claimed that pointers are hard and dangerous and therefore bad, and since Java is all goodness and light and will lift your earthly programming burdens, it cannot possibly contain such things. However, it's more accurate to say that Java has pointers; indeed, every object identifier in Java (except for primitives) is one of these pointers, but their use is restricted and guarded not only by the compiler but by the run-time system. Or to put it another way, Java has pointers, but no pointer arithmetic. These are what I've been calling "references," and you can think of them as "safety pointers," not unlike the safety scissors of elementary school-they aren't sharp, so you cannot hurt yourself without great effort, but they can sometimes be slow and tedious.

A-1. Passing references around

When you pass a reference into a method, you're still pointing to the same object. A simple experiment demonstrates this:

 
Sélectionnez
//: appendixa:PassReferences.java
// Passing references around.
import com.bruceeckel.simpletest.*;
 
public class PassReferences {
	private static Test monitor = new Test();
	public static void f(PassReferences h) {
		System.out.println("h inside f(): " + h);
	}
	public static void main(String[] args) {
		PassReferences p = new PassReferences();
		System.out.println("p inside main(): " + p);
		f(p);
		monitor.expect(new String[] {
			"%% p inside main\\(\\): PassReferences@[a-z0-9]+",
			"%% h inside f\\(\\): PassReferences@[a-z0-9]+"
		});
	}
} ///:~


The method toString( ) is automatically invoked in the print statements, and PassReferences inherits directly from Object with no redefinition of toString( ). Thus, Object's version of toString( ) is used, which prints out the class of the object followed by the address where that object is located (not the reference, but the actual object storage). The output looks like this:

 
Sélectionnez
p inside main(): PassReferences@ad3ba4
h inside f(): PassReferences@ad3ba4


You can see that both p and h refer to the same object. This is far more efficient than duplicating a new PassReferences object just so that you can send an argument to a method. But it brings up an important issue.

A-1-1. Aliasing

Aliasing means that more than one reference is tied to the same object, as in the preceding example. The problem with aliasing occurs when someone writes to that object. If the owners of the other references aren't expecting that object to change, they'll be surprised. This can be demonstrated with a simple example:

 
Sélectionnez
//: appendixa:Alias1.java
// Aliasing two references to one object.
import com.bruceeckel.simpletest.*;
 
public class Alias1 {
	private static Test monitor = new Test();
	private int i;
	public Alias1(int ii) { i = ii; }
	public static void main(String[] args) {
		Alias1 x = new Alias1(7);
		Alias1 y = x; // Assign the reference
		System.out.println("x: " + x.i);
		System.out.println("y: " + y.i);
		System.out.println("Incrementing x");
		x.i++;
		System.out.println("x: " + x.i);
		System.out.println("y: " + y.i);
		monitor.expect(new String[] {
			"x: 7",
			"y: 7",
			"Incrementing x",
			"x: 8",
			"y: 8"
		});
	}
} ///:~


In the line:

 
Sélectionnez
Alias1 y = x; // Assign the reference


a new Alias1 reference is created, but instead of being assigned to a fresh object created with new, it's assigned to an existing reference. So the contents of reference x, which is the address of the object x is pointing to, is assigned to y, and thus both x and y are attached to the same object. So when x's i is incremented in the statement:

 
Sélectionnez
x.i++;


y's i will be affected as well. This can be seen in the output:

 
Sélectionnez
x: 7
y: 7
Incrementing x
x: 8
y: 8


One good solution in this case is simply not to do it; don't consciously alias more than one reference to an object at the same scope. Your code will be much easier to understand and debug. However, when you're passing a reference in as an argument-which is the way Java is supposed to work-you automatically alias, because the local reference that's created can modify the "outside object" (the object that was created outside the scope of the method). Here's an example:

 
Sélectionnez
//: appendixa:Alias2.java
// Method calls implicitly alias their arguments.
import com.bruceeckel.simpletest.*;
 
public class Alias2 {
	private static Test monitor = new Test();
	private int i;
	public Alias2(int ii) { i = ii; }
	public static void f(Alias2 reference) { reference.i++; }
	public static void main(String[] args) {
		Alias2 x = new Alias2(7);
		System.out.println("x: " + x.i);
		System.out.println("Calling f(x)");
		f(x);
		System.out.println("x: " + x.i);
		monitor.expect(new String[] {
			"x: 7",
			"Calling f(x)",
			"x: 8"
		});
	}
} ///:~


The method is changing its argument, the outside object. When this kind of situation arises, you must decide whether it makes sense, whether the user expects it, and whether it's going to cause problems.

In general, you call a method in order to produce a return value and/or a change of state in the object that the method is called for. It's much less common to call a method in order to manipulate its arguments; this is referred to as "calling a method for its side effects." Thus, when you create a method that modifies its arguments, the user must be clearly instructed and warned about the use of that method and its potential surprises. Because of the confusion and pitfalls, it's much better to avoid changing the argument.

If you need to modify an argument during a method call and you don't intend to modify the outside argument, then you should protect that argument by making a copy inside your method. That's the subject of much of this appendix.

A-2. Making local copies

To review: All argument passing in Java is performed by passing references. That is, when you pass "an object," you're really passing only a reference to an object that lives outside the method, so if you perform any modifications with that reference, you modify the outside object. In addition:

  • Aliasing happens automatically during argument passing.
  • There are no local objects, only local references.
  • References have scopes, objects do not.
  • Object lifetime is never an issue in Java.
  • There is no language support (e.g., "const") to prevent objects from being modified and stop the negative effects of aliasing. You can't simply use the final keyword in the argument list; that simply prevents you from rebinding the reference to a different object.

If you're only reading information from an object and not modifying it, passing a reference is the most efficient form of argument passing. This is nice; the default way of doing things is also the most efficient. However, sometimes it's necessary to be able to treat the object as if it were "local" so that changes you make affect only a local copy and do not modify the outside object. Many programming languages support the ability to automatically make a local copy of the outside object, inside the method. (116) Java does not, but it allows you to produce this effect.

A-2-1. Pass by value

This brings up the terminology issue, which always seems good for an argument. The term is "pass by value," and the meaning depends on how you perceive the operation of the program. The general meaning is that you get a local copy of whatever you're passing, but the real question is how you think about what you're passing. When it comes to the meaning of "pass by value," there are two fairly distinct camps:

  • Java passes everything by value. When you're passing primitives into a method, you get a distinct copy of the primitive. When you're passing a reference into a method, you get a copy of the reference. Ergo, everything is pass by value. Of course, the assumption is that you're always thinking (and caring) that references are being passed, but it seems like the Java design has gone a long way toward allowing you to ignore (most of the time) that you're working with a reference. That is, it seems to allow you to think of the reference as "the object," since it implicitly dereferences it whenever you make a method call.
  • Java passes primitives by value (no argument there), but objects are passed by reference. This is the world view that the reference is an alias for the object, so you don't think about passing references, but instead say "I'm passing the object." Since you don't get a local copy of the object when you pass it into a method, objects are clearly not passed by value. There appears to be some support for this view within Sun, since at one time, one of the "reserved but not implemented" keywords was byvalue (This will probably never be implemented).

Having given both camps a good airing, and after saying "It depends on how you think of a reference," I will attempt to sidestep the issue. In the end, it isn't that important-what is important is that you understand that passing a reference allows the caller's object to be changed unexpectedly.

A-2-2. Cloning objects

The most likely reason for making a local copy of an object is if you're going to modify that object and you don't want to modify the caller's object. If you decide that you want to make a local copy, one approach is to use the clone( ) method to perform the operation. This is a method that's defined as protected in the base class Object, and that you must override as public in any derived classes that you want to clone. For example, the standard library class ArrayList overrides clone( ), so we can call clone( ) for ArrayList:

 
Sélectionnez
//: appendixa:Cloning.java
// The clone() operation works for only a few
// items in the standard Java library.
import com.bruceeckel.simpletest.*;
import java.util.*;
 
class Int {
	private int i;
	public Int(int ii) { i = ii; }
	public void increment() { i++; }
	public String toString() { return Integer.toString(i); }
}
 
public class Cloning {
	private static Test monitor = new Test();
	public static void main(String[] args) {
		ArrayList v = new ArrayList();
		for(int i = 0; i < 10; i++ )
			v.add(new Int(i));
		System.out.println("v: " + v);
		ArrayList v2 = (ArrayList)v.clone();
		// Increment all v2's elements:
		for(Iterator e = v2.iterator(); e.hasNext(); )
			((Int)e.next()).increment();
		// See if it changed v's elements:
		System.out.println("v: " + v);
		monitor.expect(new String[] {
			"v: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]",
			"v: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
		});
	}
} ///:~


The clone( ) method produces an Object, which must then be recast to the proper type. This example shows how ArrayList's clone( ) method does not automatically try to clone each of the objects that the ArrayList contains-the old ArrayList and the cloned ArrayList are aliased to the same objects. This is often called a shallow copy, since it's copying only the "surface" portion of an object. The actual object consists of this "surface," plus all the objects that the references are pointing to, plus all the objects those objects are pointing to, etc. This is often referred to as the "web of objects." Copying the entire mess is called a deep copy.

You can see the effect of the shallow copy in the output, where the actions performed on v2 affect v:

 
Sélectionnez
v: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
v: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]


Not trying to clone( ) the objects contained in the ArrayList is probably a fair assumption, because there's no guarantee that those objects are cloneable. (117)

A-2-3. Adding cloneability to a class

Even though the clone method is defined in the base-of-all-classes Object, cloning is not automatically available in every class. (118) This would seem to be counterintuitive to the idea that base-class methods are always available in derived classes. Cloning in Java does indeed go against this idea; if you want it to exist for a class, you must specifically add code to make cloning work.

A-2-3-1. Using a trick with protected

To prevent default cloneability in every class you create, the clone( ) method is protected in the base class Object. Not only does this mean that it's not available by default to the client programmer who is simply using the class (not subclassing it), but it also means that you cannot call clone( ) via a reference to the base class. (Although that might seem to be useful in some situations, such as to polymorphically clone a bunch of Objects.) It is, in effect, a way to give you, at compile time, the information that your object is not cloneable-and oddly enough, most classes in the standard Java library are not cloneable. Thus, if you say:

 
Sélectionnez
Integer x = new Integer(1);
x = x.clone();


You will get, at compile time, an error message that says clone( ) is not accessible (since Integer doesn't override it and it defaults to the protected version).

If, however, you're in a method of a class derived from Object (as all classes are), then you have permission to call Object.clone( ) because it's protected and you're an inheritor. The base class clone( ) has useful functionality; it performs the actual bitwise duplication of the derived-class object, thus acting as the common cloning operation. However, you then need to make your clone operation public for it to be accessible. So, two key issues when you clone are:

  • Call super.clone( )
  • Make your clone public

You'll probably want to override clone( ) in any further derived classes; otherwise, your (now public) clone( ) will be used, and that might not do the right thing (although, since Object.clone( ) makes a copy of the actual object, it might). The protected trick works only once: the first time you inherit from a class that has no cloneability and you want to make a class that's cloneable. In any classes inherited from your class, the clone( ) method is available since it's not possible in Java to reduce the access of a method during derivation. That is, once a class is cloneable, everything derived from it is cloneable unless you use provided mechanisms (described later) to "turn off" cloning.

A-2-3-2. Implementing the Cloneable interface

There's one more thing you need to do to complete the cloneability of an object: implement the Cloneable interface. This interface is a bit strange, because it's empty!

 
Sélectionnez
interface Cloneable {}


The reason for implementing this empty interface is obviously not because you are going to upcast to Cloneable and call one of its methods. The use of interface in this way is called a tagging interface because it acts as a kind of flag, wired into the type of the class.

There are two reasons for the existence of the Cloneable interface. First, you might have an upcast reference to a base type and not know whether it's possible to clone that object. In this case, you can use the instanceof keyword (described in Chapter 10) to find out whether the reference is connected to an object that can be cloned:

 
Sélectionnez
if(myReference instanceof Cloneable) // ...


The second reason is that mixed into this design for cloneability was the thought that maybe you didn't want all types of objects to be cloneable. So Object.clone( ) verifies that a class implements the Cloneable interface. If not, it throws a CloneNotSupportedException exception. So in general, you're forced to implement Cloneable as part of support for cloning.

A-2-4. Successful cloning

Once you understand the details of implementing the clone( ) method, you're able to create classes that can be easily duplicated to provide a local copy:

 
Sélectionnez
//: appendixa:LocalCopy.java
// Creating local copies with clone().
import com.bruceeckel.simpletest.*;
import java.util.*;
 
class MyObject implements Cloneable {
	private int n;
	public MyObject(int n) { this.n = n; }
	public Object clone() {
		Object o = null;
		try {
			o = super.clone();
		} catch(CloneNotSupportedException e) {
			System.err.println("MyObject can't clone");
		}
		return o;
	}
	public int getValue() { return n; }
	public void setValue(int n) { this.n = n; }
	public void increment() { n++; }
	public String toString() { return Integer.toString(n); }
}
 
public class LocalCopy {
	private static Test monitor = new Test();
	public static MyObject g(MyObject v) {
		// Passing a reference, modifies outside object:
		v.increment();
		return v;
	}
	public static MyObject f(MyObject v) {
		v = (MyObject)v.clone(); // Local copy
		v.increment();
		return v;
	}
	public static void main(String[] args) {
		MyObject a = new MyObject(11);
		MyObject b = g(a);
		// Reference equivalence, not object equivalence:
		System.out.println("a == b: " + (a == b) +
			"\na = " + a + "\nb = " + b);
		MyObject c = new MyObject(47);
		MyObject d = f(c);
		System.out.println("c == d: " + (c == d) +
			"\nc = " + c + "\nd = " + d);
		monitor.expect(new String[] {
			"a == b: true",
			"a = 12",
			"b = 12",
			"c == d: false",
			"c = 47",
			"d = 48"
		});
	}
} ///:~


First of all, for clone( ) to be accessible, you must make it public. Second, for the initial part of your clone( ) operation, you should call the base-class version of clone( ). The clone( ) that's being called here is the one that's predefined inside Object, and you can call it because it's protected and thereby accessible in derived classes.

Object.clone( ) figures out how big the object is, creates enough memory for a new one, and copies all the bits from the old to the new. This is called a bitwise copy, and is typically what you'd expect a clone( ) method to do. But before Object.clone( ) performs its operations, it first checks to see if a class is Cloneable-that is, whether it implements the Cloneable interface. If it doesn't, Object.clone( ) throws a CloneNotSupportedException to indicate that you can't clone it. Thus, you've got to surround your call to super.clone( ) with a try block to catch an exception that should never happen (because you've implemented the Cloneable interface).

In LocalCopy, the two methods g( ) and f( ) demonstrate the difference between the two approaches for argument passing. The g( ) method shows passing by reference in which it modifies the outside object and returns a reference to that outside object, whereas f( ) clones the argument, thereby decoupling it and leaving the original object alone. It can then proceed to do whatever it wants-even return a reference to this new object without any ill effects to the original. Notice the somewhat curious-looking statement:

 
Sélectionnez
v = (MyObject)v.clone();


This is where the local copy is created. To prevent confusion by such a statement, remember that this rather strange coding idiom is perfectly feasible in Java because every object identifier is actually a reference. So the reference v is used to clone( ) a copy of what it refers to, and this returns a reference to the base type Object (because it's defined that way in Object.clone( )) that must then be cast to the proper type.

In main( ), the difference between the effects of the two different argument-passing approaches is tested. It's important to notice that the equivalence tests in Java do not look inside the objects being compared to see if their values are the same. The == and != operators are simply comparing the references. If the addresses inside the references are the same, the references are pointing to the same object and are therefore "equal." So what the operators are really testing is whether the references are aliased to the same object!

A-2-5. The effect of Object.clone( )

What actually happens when Object.clone( ) is called that makes it so essential to call super.clone( ) when you override clone( ) in your class? The clone( ) method in the root class is responsible for creating the correct amount of storage and making the bitwise copy of the bits from the original object into the new object's storage. That is, it doesn't just make storage and copy an Object; it actually figures out the size of the real object (not just the base-class object, but the derived object) that's being copied and duplicates that. Since all this is happening from the code in the clone( ) method defined in the root class (that has no idea what's being inherited from it), you can guess that the process involves RTTI to determine the actual object that's being cloned. This way, the clone( ) method can create the proper amount of storage and do the correct bitwise copy for that type.

Whatever you do, the first part of the cloning process should normally be a call to super.clone( ). This establishes the groundwork for the cloning operation by making an exact duplicate. At this point you can perform other operations necessary to complete the cloning.

To know for sure what those other operations are, you need to understand exactly what Object.clone( ) buys you. In particular, does it automatically clone the destination of all the references? The following example tests this:

 
Sélectionnez
//: appendixa:Snake.java
// Tests cloning to see if destination
// of references are also cloned.
import com.bruceeckel.simpletest.*;
 
public class Snake implements Cloneable {
	private static Test monitor = new Test();
	private Snake next;
	private char c;
	// Value of i == number of segments
	public Snake(int i, char x) {
		c = x;
		if(--i > 0)
			next = new Snake(i, (char)(x + 1));
	}
	public void increment() {
		c++;
		if(next != null)
			next.increment();
	}
	public String toString() {
		String s = ":" + c;
		if(next != null)
			s += next.toString();
		return s;
	}
	public Object clone() {
		Object o = null;
		try {
			o = super.clone();
		} catch(CloneNotSupportedException e) {
			System.err.println("Snake can't clone");
		}
		return o;
	}
	public static void main(String[] args) {
		Snake s = new Snake(5, 'a');
		System.out.println("s = " + s);
		Snake s2 = (Snake)s.clone();
		System.out.println("s2 = " + s2);
		s.increment();
		System.out.println("after s.increment, s2 = " + s2);
		monitor.expect(new String[] {
			"s = :a:b:c:d:e",
			"s2 = :a:b:c:d:e",
			"after s.increment, s2 = :a:c:d:e:f"
		});
	}
} ///:~


A Snake is made up of a bunch of segments, each of type Snake. Thus, it's a singly linked list. The segments are created recursively, decrementing the first constructor argument for each segment until zero is reached. To give each segment a unique tag, the second argument, a char, is incremented for each recursive constructor call.

The increment( ) method recursively increments each tag so you can see the change, and the toString( ) recursively prints each tag. From the output, you can see that only the first segment is duplicated by Object.clone( ), therefore it does a shallow copy. If you want the whole snake to be duplicated-a deep copy-you must perform the additional operations inside your overridden clone( ).

You'll typically call super.clone( ) in any class derived from a cloneable class to make sure that all of the base-class operations (including Object.clone( )) take place. This is followed by an explicit call to clone( ) for every reference in your object; otherwise those references will be aliased to those of the original object. It's analogous to the way constructors are called: base-class constructor first, then the next-derived constructor, and so on, to the most-derived constructor. The difference is that clone( ) is not a constructor, so there's nothing to make it happen automatically. You must make sure to do it yourself.

A-2-6. Cloning a composed object

There's a problem you'll encounter when trying to deep copy a composed object. You must assume that the clone( ) method in the member objects will in turn perform a deep copy on their references, and so on. This is quite a commitment. It effectively means that for a deep copy to work, you must either control all of the code in all of the classes, or at least have enough knowledge about all of the classes involved in the deep copy to know that they are performing their own deep copy correctly.

This example shows what you must do to accomplish a deep copy when dealing with a composed object:

 
Sélectionnez
//: appendixa:DeepCopy.java
// Cloning a composed object.
// {Depends: junit.jar}
import junit.framework.*;
 
class DepthReading implements Cloneable {
	private double depth;
	public DepthReading(double depth) { this.depth = depth; }
	public Object clone() {
		Object o = null;
		try {
			o = super.clone();
		} catch(CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return o;
	}
	public double getDepth() { return depth; }
	public void setDepth(double depth){ this.depth = depth; }
	public String toString() { return String.valueOf(depth);}
}
 
class TemperatureReading implements Cloneable {
	private long time;
	private double temperature;
	public TemperatureReading(double temperature) {
		time = System.currentTimeMillis();
		this.temperature = temperature;
	}
	public Object clone() {
		Object o = null;
		try {
			o = super.clone();
		} catch(CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return o;
	}
	public double getTemperature() { return temperature; }
	public void setTemperature(double temperature) {
		this.temperature = temperature;
	}
	public String toString() {
		return String.valueOf(temperature);
	}
}
 
class OceanReading implements Cloneable {
	private DepthReading depth;
	private TemperatureReading temperature;
	public OceanReading(double tdata, double ddata) {
		temperature = new TemperatureReading(tdata);
		depth = new DepthReading(ddata);
	}
	public Object clone() {
		OceanReading o = null;
		try {
			o = (OceanReading)super.clone();
		} catch(CloneNotSupportedException e) {
			e.printStackTrace();
		}
		// Must clone references:
		o.depth = (DepthReading)o.depth.clone();
		o.temperature =
			(TemperatureReading)o.temperature.clone();
		return o; // Upcasts back to Object
	}
	public TemperatureReading getTemperatureReading() {
		return temperature;
	}
	public void setTemperatureReading(TemperatureReading tr){
		temperature = tr;
	}
	public DepthReading getDepthReading() { return depth; }
	public void setDepthReading(DepthReading dr) {
		this.depth = dr;
	}
	public String toString() {
		return "temperature: " + temperature +
			", depth: " + depth;
	}
}
 
public class DeepCopy extends TestCase {
	public DeepCopy(String name) { super(name); }
	public void testClone() {
	OceanReading reading = new OceanReading(33.9, 100.5);
	// Now clone it:
	OceanReading clone = (OceanReading)reading.clone();
	TemperatureReading tr = clone.getTemperatureReading();
	tr.setTemperature(tr.getTemperature() + 1);
	clone.setTemperatureReading(tr);
	DepthReading dr = clone.getDepthReading();
	dr.setDepth(dr.getDepth() + 1);
	clone.setDepthReading(dr);
	assertEquals(reading.toString(),
		"temperature: 33.9, depth: 100.5");
	assertEquals(clone.toString(),
		"temperature: 34.9, depth: 101.5");
	}
	public static void main(String[] args) {
		junit.textui.TestRunner.run(DeepCopy.class);
	}
} ///:~


DepthReading and TemperatureReading are quite similar; they both contain only primitives. Therefore, the clone( ) method can be quite simple: it calls super.clone( ) and returns the result. Note that the clone( ) code for both classes is identical.

OceanReading is composed of DepthReading and TemperatureReading objects and so, to produce a deep copy, its clone( ) must clone the references inside OceanReading. To accomplish this, the result of super.clone( ) must be cast to an OceanReading object (so you can access the depth and temperature references).

A-2-7. A deep copy with ArrayList

Let's revisit Cloning.java from earlier in this appendix. This time the Int2 class is cloneable, so the ArrayList can be deep copied:

 
Sélectionnez
//: appendixa:AddingClone.java
// You must go through a few gyrations
// to add cloning to your own class.
import com.bruceeckel.simpletest.*;
import java.util.*;
 
class Int2 implements Cloneable {
	private int i;
	public Int2(int ii) { i = ii; }
	public void increment() { i++; }
	public String toString() { return Integer.toString(i); }
	public Object clone() {
		Object o = null;
		try {
			o = super.clone();
		} catch(CloneNotSupportedException e) {
			System.err.println("Int2 can't clone");
		}
		return o;
	}
}
 
// Inheritance doesn't remove cloneability:
class Int3 extends Int2 {
	private int j; // Automatically duplicated
	public Int3(int i) { super(i); }
}
 
public class AddingClone {
	private static Test monitor = new Test();
	public static void main(String[] args) {
		Int2 x = new Int2(10);
		Int2 x2 = (Int2)x.clone();
		x2.increment();
		System.out.println("x = " + x + ", x2 = " + x2);
		// Anything inherited is also cloneable:
		Int3 x3 = new Int3(7);
		x3 = (Int3)x3.clone();
		ArrayList v = new ArrayList();
		for(int i = 0; i < 10; i++ )
			v.add(new Int2(i));
		System.out.println("v: " + v);
		ArrayList v2 = (ArrayList)v.clone();
		// Now clone each element:
		for(int i = 0; i < v.size(); i++)
			v2.set(i, ((Int2)v2.get(i)).clone());
		// Increment all v2's elements:
		for(Iterator e = v2.iterator(); e.hasNext(); )
			((Int2)e.next()).increment();
		System.out.println("v2: " + v2);
		// See if it changed v's elements:
		System.out.println("v: " + v);
		monitor.expect(new String[] {
			"x = 10, x2 = 11",
			"v: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]",
			"v2: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]",
			"v: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
		});
	}
} ///:~


Int3 is inherited from Int2, and a new primitive member, int j,is added. You might think that you'd need to override clone( ) again to make sure j is copied, but that's not the case. When Int2's clone( ) is called as Int3's clone( ), it calls Object.clone( ), which determines that it's working with an Int3 and duplicates all the bits in the Int3. As long as you don't add references that need to be cloned, the one call to Object.clone( ) performs all of the necessary duplication regardless of how far down in the hierarchy clone( ) is defined.

You can see what's necessary in order to do a deep copy of an ArrayList: After the ArrayList is cloned, you have to step through and clone each one of the objects pointed to by the ArrayList. You'd have to do something similar to this to do a deep copy of a HashMap.

The remainder of the example shows that the cloning did happen by showing that, once an object is cloned, you can change it, and the original object is left untouched.

A-2-8. Deep copy via serialization

When you consider Java's object serialization (introduced in Chapter 12), you might observe that an object that's serialized and then deserialized is, in effect, cloned.

So why not use serialization to perform deep copying? Here's an example that compares the two approaches by timing them:

 
Sélectionnez
//: appendixa:Compete.java
import java.io.*;
 
class Thing1 implements Serializable {}
class Thing2 implements Serializable {
	Thing1 o1 = new Thing1();
}
 
class Thing3 implements Cloneable {
	public Object clone() {
		Object o = null;
		try {
			o = super.clone();
		} catch(CloneNotSupportedException e) {
			System.err.println("Thing3 can't clone");
		}
		return o;
	}
}
 
class Thing4 implements Cloneable {
	private Thing3 o3 = new Thing3();
	public Object clone() {
		Thing4 o = null;
		try {
			o = (Thing4)super.clone();
		} catch(CloneNotSupportedException e) {
			System.err.println("Thing4 can't clone");
		}
		// Clone the field, too:
		o.o3 = (Thing3)o3.clone();
		return o;
	}
}
 
public class Compete {
	public static final int SIZE = 25000;
	public static void main(String[] args) throws Exception {
		Thing2[] a = new Thing2[SIZE];
		for(int i = 0; i < a.length; i++)
			a[i] = new Thing2();
		Thing4[] b = new Thing4[SIZE];
		for(int i = 0; i < b.length; i++)
			b[i] = new Thing4();
		long t1 = System.currentTimeMillis();
		ByteArrayOutputStream buf= new ByteArrayOutputStream();
		ObjectOutputStream o = new ObjectOutputStream(buf);
		for(int i = 0; i < a.length; i++)
			o.writeObject(a[i]);
		// Now get copies:
		ObjectInputStream in = new ObjectInputStream(
			new ByteArrayInputStream(buf.toByteArray()));
		Thing2[] c = new Thing2[SIZE];
		for(int i = 0; i < c.length; i++)
			c[i] = (Thing2)in.readObject();
		long t2 = System.currentTimeMillis();
		System.out.println("Duplication via serialization: " +
			(t2 - t1) + " Milliseconds");
		// Now try cloning:
		t1 = System.currentTimeMillis();
		Thing4[] d = new Thing4[SIZE];
		for(int i = 0; i < d.length; i++)
			d[i] = (Thing4)b[i].clone();
		t2 = System.currentTimeMillis();
		System.out.println("Duplication via cloning: " +
			(t2 - t1) + " Milliseconds");
	}
} ///:~


Thing2 and Thing4 contain member objects so that there's some deep copying going on. It's interesting to notice that while Serializable classes are easy to set up, there's much more work going on to duplicate them. Cloning involves a lot of work to set up the class, but the actual duplication of objects is relatively simple. The results are interesting. Here is the output from three different runs:

 
Sélectionnez
Duplication via serialization: 547 Milliseconds
Duplication via cloning: 110 Milliseconds
 
Duplication via serialization: 547 Milliseconds
Duplication via cloning: 109 Milliseconds
 
Duplication via serialization: 547 Milliseconds
Duplication via cloning: 125 Milliseconds


In earlier versions of the JDK, the time required for serialization was much longer than that of cloning (roughly 15 times slower), and the serialization time tended to vary a lot. More recent versions of the JDK have sped up serialization and apparently made the time more consistent, as well. Here, it's approximately four times slower, which brings it into the realm of reasonability for use as a cloning alternative.

A-2-9. Adding cloneability farther down a hierarchy

If you create a new class, its base class defaults to Object, which defaults to noncloneability (as you'll see in the next section). As long as you don't explicitly add cloneability, you won't get it. But you can add it in at any layer and it will then be cloneable from that layer downward, like this:

 
Sélectionnez
//: appendixa:HorrorFlick.java
// You can insert Cloneability at any level of inheritance.
package appendixa;
import java.util.*;
 
class Person {}
class Hero extends Person {}
class Scientist extends Person implements Cloneable {
	public Object clone() {
		try {
			return super.clone();
		} catch(CloneNotSupportedException e) {
			// This should never happen: It's Cloneable already!
			throw new RuntimeException(e);
		}
	}
}
class MadScientist extends Scientist {}
 
public class HorrorFlick {
	public static void main(String[] args) {
		Person p = new Person();
		Hero h = new Hero();
		Scientist s = new Scientist();
		MadScientist m = new MadScientist();
		//! p = (Person)p.clone(); // Compile error
		//! h = (Hero)h.clone(); // Compile error
		s = (Scientist)s.clone();
		m = (MadScientist)m.clone();
	}
} ///:~


Before cloneability was added in the hierarchy, the compiler stopped you from trying to clone things. When cloneability is added in Scientist, then Scientist and all its descendants are cloneable.

A-2-10. Why this strange design?

If all this seems to be a strange scheme, that's because it is. You might wonder why it worked out this way. What is the meaning behind this design?

Originally, Java was designed as a language to control hardware boxes, and definitely not with the Internet in mind. In a general-purpose language like this, it makes sense that the programmer be able to clone any object. Thus, clone( ) was placed in the root class Object, but it was a public method so you could always clone any object. This seemed to be the most flexible approach, and after all, what could it hurt?

Well, when Java was seen as the ultimate Internet programming language, things changed. Suddenly, there are security issues, and of course, these issues are dealt with using objects, and you don't necessarily want anyone to be able to clone your security objects. So what you're seeing is a lot of patches applied on the original simple and straightforward scheme: clone( ) is now protected in Object. You must override it and implement Cloneable and deal with the exceptions.

It's worth noting that you must implement the Cloneable interface only if you're going to call Object's clone( ) method, since that method checks at run time to make sure that your class implements Cloneable. But for consistency (and since Cloneable is empty anyway), you should implement it.

A-3. Controlling cloneability

You might suggest that, to remove cloneability, the clone( ) method should simply be made private, but this won't work, because you cannot take a base-class method and make it less accessible in a derived class. And yet, it's necessary to be able to control whether an object can be cloned. There are a number of attitudes you can take to this for your classes:

  • 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 references to other objects that might or might not be cloneable (a container class, for example), your clone( ) can try to clone all of the objects for which you have references, and if they throw exceptions, just pass those exceptions out to the programmer. For example, consider a special sort of ArrayList that tries to clone all the objects it holds. When you write such an ArrayList, you don't know what sort of objects the client programmer might put into your ArrayList, 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 Thinking in Patterns (with Java) at www.BruceEckel.com.)

Here's an example that shows the various ways cloning can be implemented and then, later in the hierarchy, "turned off":

 
Sélectionnez
//: appendixa:CheckCloneable.java
// Checking to see if a reference can be cloned.
import com.bruceeckel.simpletest.*;
 
// 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);
	}
}
 
// You can't inherit from this, so you can't override
// the clone method as you can in BackOn:
final class ReallyNoMore extends NoMore {}
 
public class CheckCloneable {
	private static Test monitor = new Test();
	public static Ordinary tryToClone(Ordinary ord) {
		String id = ord.getClass().getName();
		System.out.println("Attempting " + id);
		Ordinary x = null;
		if(ord instanceof Cloneable) {
			try {
				x = (Ordinary)((IsCloneable)ord).clone();
				System.out.println("Cloned " + id);
			} catch(CloneNotSupportedException e) {
				System.err.println("Could not clone " + id);
			}
		} else {
			System.out.println("Doesn't implement Cloneable");
		}
		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; clone() is protected in Object:
		//! x = (Ordinary)x.clone();
		// Checks first to see if a class implements Cloneable:
		for(int i = 0; i < ord.length; i++)
			tryToClone(ord[i]);
		monitor.expect(new String[] {
			"Attempting IsCloneable",
			"Cloned IsCloneable",
			"Attempting WrongClone",
			"Doesn't implement Cloneable",
			"Attempting NoMore",
			"Could not clone NoMore",
			"Attempting TryMore",
			"Could not clone TryMore",
			"Attempting BackOn",
			"Cloned BackOn",
			"Attempting ReallyNoMore",
			"Could not clone ReallyNoMore"
		});
	}
} ///:~


The first class, Ordinary, represents the kinds of classes we've seen throughout this book: no support for cloning, but as it turns out, no prevention of cloning either. But if you have a reference 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.

IsCloneable performs all the right actions 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 (RTTI; see Chapter 10) to print 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.

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.

A-3-1. 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:

 
Sélectionnez
//: 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:

 
Sélectionnez
t = new Tomato(t);


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

 
Sélectionnez
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.

A-3-1-1. 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.

A-4. Read-only classes

Although the local copy produced by clone( ) gives the desired results in the appropriate cases, it is an example of forcing the programmer (the author of the method) to be responsible for preventing the ill effects of aliasing. What if you're making a library that's so general purpose and commonly used that you cannot make the assumption that it will always be cloned in the proper places? Or more likely, what if you want to allow aliasing for efficiency-to prevent the needless duplication of objects-but you don't want the negative side effects of aliasing?

One solution is to create immutable objects that belong to read-only classes. You can define a class such that no methods in the class cause changes to the internal state of the object. In such a class, aliasing has no impact since you can read only the internal state, so if many pieces of code are reading the same object, there's no problem.

As a simple example of immutable objects, Java's standard library contains "wrapper" classes for all the primitive types. You might have already discovered that, if you want to store an int inside a container such as an ArrayList (which takes only Object references), you can wrap your int inside the standard library Integer class:

 
Sélectionnez
//: appendixa:ImmutableInteger.java
// The Integer class cannot be changed.
import java.util.*;
 
public class ImmutableInteger {
	public static void main(String[] args) {
		List v = new ArrayList();
		for(int i = 0; i < 10; i++)
			v.add(new Integer(i));
		// But how do you change the int inside the Integer?
	}
} ///:~


The Integer class (as well as all the primitive "wrapper" classes) implements immutability in a simple fashion: It has no methods that allow you to change the object.

If you do need an object that holds a primitive type that can be modified, you must create it yourself. Fortunately, this is trivial. The following class uses the JavaBeans naming conventions:

 
Sélectionnez
//: appendixa:MutableInteger.java
// A changeable wrapper class.
import com.bruceeckel.simpletest.*;
import java.util.*;
 
class IntValue {
	private int n;
	public IntValue(int x) { n = x; }
	public int getValue() { return n; }
	public void setValue(int n) { this.n = n; }
	public void increment() { n++; }
	public String toString() { return Integer.toString(n); }
}
 
public class MutableInteger {
	private static Test monitor = new Test();
	public static void main(String[] args) {
		List v = new ArrayList();
		for(int i = 0; i < 10; i++)
			v.add(new IntValue(i));
		System.out.println(v);
		for(int i = 0; i < v.size(); i++)
			((IntValue)v.get(i)).increment();
		System.out.println(v);
		monitor.expect(new String[] {
			"[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]",
			"[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
		});
	}
} ///:~


IntValue can be even simpler if privacy is not an issue, the default initialization to zero is adequate (then you don't need the constructor), and you don't care about printing it out (then you don't need the toString( )):

 
Sélectionnez
class IntValue { int n; }


Fetching the element out and casting it is a bit awkward, but that's a feature of ArrayList, not of IntValue.

A-4-1. Creating read-only classes

It's possible to create your own read-only class. Here's an example:

 
Sélectionnez
//: appendixa:Immutable1.java
// Objects that cannot be modified are immune to aliasing.
import com.bruceeckel.simpletest.*;
 
public class Immutable1 {
	private static Test monitor = new Test();
	private int data;
	public Immutable1(int initVal) {
		data = initVal;
	}
	public int read() { return data; }
	public boolean nonzero() { return data != 0; }
	public Immutable1 multiply(int multiplier) {
		return new Immutable1(data * multiplier);
	}
	public static void f(Immutable1 i1) {
		Immutable1 quad = i1.multiply(4);
		System.out.println("i1 = " + i1.read());
		System.out.println("quad = " + quad.read());
	}
	public static void main(String[] args) {
		Immutable1 x = new Immutable1(47);
		System.out.println("x = " + x.read());
		f(x);
		System.out.println("x = " + x.read());
		monitor.expect(new String[] {
			"x = 47",
			"i1 = 47",
			"quad = 188",
			"x = 47"
		});
	}
} ///:~


All data is private, and you'll see that none of the public methods modify that data. Indeed, the method that does appear to modify an object is multiply( ), but this creates a new Immutable1 object and leaves the original one untouched.

The method f( ) takes an Immutable1 object and performs various operations on it, and the output of main( ) demonstrates that there is no change to x. Thus, x's object could be aliased many times without harm, because the Immutable1 class is designed to guarantee that objects cannot be changed.

A-4-2. The drawback to immutability

Creating an immutable class seems at first to provide an elegant solution. However, whenever you do need a modified object of that new type, you must suffer the overhead of a new object creation, as well as potentially causing more frequent garbage collections. For some classes this is not a problem, but for others (such as the String class) it is prohibitively expensive.

The solution is to create a companion class that can be modified. Then, when you're doing a lot of modifications, you can switch to using the modifiable companion class and switch back to the immutable class when you're done.

The preceding example can be modified to show this:

 
Sélectionnez
//: appendixa:Immutable2.java
// A companion class to modify immutable objects.
import com.bruceeckel.simpletest.*;
 
class Mutable {
	private int data;
	public Mutable(int initVal) { data = initVal; }
	public Mutable add(int x) {
		data += x;
		return this;
	}
	public Mutable multiply(int x) {
		data *= x;
		return this;
	}
	public Immutable2 makeImmutable2() {
		return new Immutable2(data);
	}
}
 
public class Immutable2 {
	private static Test monitor = new Test();
	private int data;
	public Immutable2(int initVal) { data = initVal; }
	public int read() { return data; }
	public boolean nonzero() { return data != 0; }
	public Immutable2 add(int x) {
		return new Immutable2(data + x);
	}
	public Immutable2 multiply(int x) {
		return new Immutable2(data * x);
	}
	public Mutable makeMutable() {
		return new Mutable(data);
	}
	public static Immutable2 modify1(Immutable2 y) {
		Immutable2 val = y.add(12);
		val = val.multiply(3);
		val = val.add(11);
		val = val.multiply(2);
		return val;
	}
	// This produces the same result:
	public static Immutable2 modify2(Immutable2 y) {
		Mutable m = y.makeMutable();
		m.add(12).multiply(3).add(11).multiply(2);
		return m.makeImmutable2();
	}
	public static void main(String[] args) {
		Immutable2 i2 = new Immutable2(47);
		Immutable2 r1 = modify1(i2);
		Immutable2 r2 = modify2(i2);
		System.out.println("i2 = " + i2.read());
		System.out.println("r1 = " + r1.read());
		System.out.println("r2 = " + r2.read());
		monitor.expect(new String[] {
			"i2 = 47",
			"r1 = 376",
			"r2 = 376"
		});
	}
} ///:~


Immutable2 contains methods that, as before, preserve the immutability of the objects by producing new objects whenever a modification is desired. These are the add( ) and multiply( ) methods. The companion class is called Mutable, and it also has add( ) and multiply( ) methods, but these modify the Mutable object rather than making a new one. In addition, Mutable has a method to use its data to produce an Immutable2 object and vice versa.

The two static methods modify1( ) and modify2( ) show two different approaches to producing the same result. In modify1( ), everything is done within the Immutable2 class and you can see that four new Immutable2 objects are created in the process. (And each time val is reassigned, the previous object becomes garbage.)

In the method modify2( ), you can see that the first action is to take the Immutable2 y and produce a Mutable from it. (This is just like calling clone( ) as you saw earlier, but this time a different type of object is created.) Then the Mutable object is used to perform a lot of change operations without requiring the creation of many new objects. Finally, it's turned back into an Immutable2. Here, two new objects are created (the Mutable and the result Immutable2) instead of four.

This approach makes sense, then, when:

  • You need immutable objects and
  • You often need to make a lot of modifications or
  • It's expensive to create new immutable objects.

A-4-3. Immutable Strings

Consider the following code:

 
Sélectionnez
//: appendixa:Stringer.java
import com.bruceeckel.simpletest.*;
 
public class Stringer {
	private static Test monitor = new Test();
	public static String upcase(String s) {
		return s.toUpperCase();
	}
	public static void main(String[] args) {
		String q = new String("howdy");
		System.out.println(q); // howdy
		String qq = upcase(q);
		System.out.println(qq); // HOWDY
		System.out.println(q); // howdy
		monitor.expect(new String[] {
			"howdy",
			"HOWDY",
			"howdy"
		});
	}
} ///:~


When q is passed in to upcase( ) it's actually a copy of the reference to q. The object this reference is connected to stays put in a single physical location. The references are copied as they are passed around.

Looking at the definition for upcase( ), you can see that the reference that's passed in has the name s, and it exists for only as long as the body of upcase( ) is being executed. When upcase( ) completes, the local reference s vanishes. upcase( ) returns the result, which is the original string with all the characters set to uppercase. Of course, it actually returns a reference to the result. But it turns out that the reference that it returns is for a new object, and the original q is left alone. How does this happen?

A-4-3-1. Implicit constants

If you say:

 
Sélectionnez
String s = "asdf";
String x = Stringer.upcase(s);


do you really want the upcase( ) method to change the argument? In general, you don't, because an argument usually looks to the reader of the code as a piece of information provided to the method, not something to be modified. This is an important guarantee, since it makes code easier to write and understand.

In C++, the availability of this guarantee was important enough to put in a special keyword, const, to allow the programmer to ensure that a reference (pointer or reference in C++) could not be used to modify the original object. But then the C++ programmer was required to be diligent and remember to use const everywhere. It can be confusing and easy to forget.

A-4-3-2. Overloading '+' and the StringBuffer

Objects of the String class are designed to be immutable, using the companion-class technique shown previously. If you examine the JDK documentationfor the String class (which is summarized a little later in this appendix), you'll see that every method in the class that appears to modify a String really creates and returns a brand new String object containing the modification. The original String is left untouched. Thus, there's no feature in Java like C++'s const to make the compiler support the immutability of your objects. If you want it, you have to wire it in yourself, like String does.

Since String objects are immutable, you can alias to a particular String as many times as you want. Because it's read-only, there's no possibility that one reference will change something that will affect the other references. So a read-only object solves the aliasing problem nicely.

It also seems possible to handle all the cases in which you need a modified object by creating a brand new version of the object with the modifications, as String does. However, for some operations this isn't efficient. A case in point is the operator '+' that has been overloaded for String objects. Overloading means that it has been given an extra meaning when used with a particular class. (The '+' and '+=' for String are the only operators that are overloaded in Java, and Java does not allow the programmer to overload any others). (120)

When used with String objects, the '+' allows you to concatenate Strings together:

 
Sélectionnez
String s = "abc" + foo + "def" + Integer.toString(47);


You could imagine how this might work. The String "abc" could have a method append( ) that creates a new String object containing "abc" concatenated with the contents of foo. The new String object would then create another new String that added "def," and so on.

This would certainly work, but it requires the creation of a lot of String objects just to put together this new String, and then you have a bunch of the intermediate String objects that need to be garbage-collected. I suspect that the Java designers tried this approach first (which is a lesson in software design-you don't really know anything about a system until you try it out in code and get something working). I also suspect they discovered that it delivered unacceptable performance.

The solution is a mutable companion class similar to the one shown previously. For String, this companion class is called StringBuffer, and the compiler automatically creates a StringBuffer to evaluate certain expressions, in particular when the overloaded operators '+' and '+=' are used with String objects. This example shows what happens:

 
Sélectionnez
//: appendixa:ImmutableStrings.java
// Demonstrating StringBuffer.
import com.bruceeckel.simpletest.*;
 
public class ImmutableStrings {
	private static Test monitor = new Test();
	public static void main(String[] args) {
		String foo = "foo";
		String s = "abc" + foo + "def" + Integer.toString(47);
		System.out.println(s);
		// The "equivalent" using StringBuffer:
		StringBuffer sb =
			new StringBuffer("abc"); // Creates String!
		sb.append(foo);
		sb.append("def"); // Creates String!
		sb.append(Integer.toString(47));
		System.out.println(sb);
		monitor.expect(new String[] {
			"abcfoodef47",
			"abcfoodef47"
		});
	}
} ///:~


In the creation of String s, the compiler is doing the rough equivalent of the subsequent code that uses sb: a StringBuffer is created, and append( ) is used to add new characters directly into the StringBuffer object (rather than making new copies each time). While this is more efficient, it's worth noting that each time you create a quoted character string such as "abc" and "def", the compiler turns those into String objects. So there can be more objects created than you expect, despite the efficiency afforded through StringBuffer.

A-4-4. The String and StringBuffer classes

Here is an overview of the methods available for both String and StringBuffer so you can get a feel for the way they interact. These tables don't contain every single method, but rather the ones that are important to this discussion. Methods that are overloaded are summarized in a single row.

First, the String class:

Method Arguments, Overloading Use
Constructor Overloaded: default, String, StringBuffer, char arrays, byte arrays. Creating String objects.
length( )   Number of characters in the String.
charAt( ) int Index The char at a location in the String.
getChars( ), getBytes( ) The beginning and end from which to copy, the array to copy into, an index into the destination array. Copy chars or bytes into an external array.
toCharArray( )   Produces a char[] containing the characters in the String.
equals( ), equals-IgnoreCase( ) A String to compare with. An equality check on the contents of the two Strings.
compareTo( ) A String to compare with. Result is negative, zero, or positive depending on the lexicographical ordering of the String and the argument. Uppercase and lowercase are not equal!
regionMatches( ) Offset into this String, the other String and its offset and length to compare. Overload adds "ignore case." boolean result indicates whether the region matches.
startsWith( ) String that it might start with. Overload adds offset into argument. boolean result indicates whether the String starts with the argument.
endsWith( ) String that might be a suffix of this String. boolean result indicates whether the argument is a suffix.
indexOf( ), lastIndexOf( ) Overloaded: char, char and starting index, String, String, and starting index. Returns -1 if the argument is not found within this String, otherwise returns the index where the argument starts. lastIndexOf( ) searches backward from end.
substring( ) Overloaded: starting index, starting index, and ending index. Returns a new String object containing the specified character set.
concat( ) The String to concatenate. Returns a new String object containing the original String's characters followed by the characters in the argument.
replace( ) The old character to search for, the new character to replace it with. Returns a new String object with the replacements made. Uses the old String if no match is found.
toLowerCase( ) toUpperCase( )   Returns a new String object with the case of all letters changed. Uses the old String if no changes need to be made.
trim( )   Returns a new String object with the white space removed from each end. Uses the old String if no changes need to be made.
valueOf( ) Overloaded: Object, char[], char[] and offset and count, boolean, char, int, long, float, double. Returns a String containing a character representation of the argument.
intern( )   Produces one and only one String reference per unique character sequence.


You can see that every String method carefully returns a new String object when it's necessary to change the contents. Also notice that if the contents don't need changing, the method will just return a reference to the original String. This saves storage and overhead.

Here's the StringBuffer class:

Method Arguments, overloading Use
Constructor Overloaded: default, length of buffer to create, String to create from. Create a new StringBuffer object.
toString( )   Creates a String from this StringBuffer.
length( )   Number of characters in the StringBuffer.
capacity( )   Returns current number of spaces allocated.
ensure-
Capacity( )
Integer indicating desired capacity. Makes the StringBuffer hold at least the desired number of spaces.
setLength( ) Integer indicating new length of character string in buffer. Truncates or expands the previous character string. If expanding, pads with nulls.
charAt( ) Integer indicating the location of the desired element. Returns the char at that location in the buffer.
setCharAt( ) Integer indicating the location of the desired element and the new char value for the element. Modifies the value at that location.
getChars( ) The beginning and end from which to copy, the array to copy into, an index into the destination array. Copy chars into an external array. There is no getBytes( ) as in String.
append( ) Overloaded: Object, String, char[], char[] with offset and length, boolean, char, int, long, float, double. The argument is converted to a string and appended to the end of the current buffer, increasing the buffer if necessary.
insert( ) Overloaded, each with a first argument of the offset at which to start inserting: Object, String, char[], boolean, char, int, long, float, double. The second argument is converted to a string and inserted into the current buffer beginning at the offset. The buffer is increased if necessary.
reverse( )   The order of the characters in the buffer is reversed.


The most commonly used method is append( ), which is used by the compiler when evaluating String expressions that contain the '+' and '+=' operators. The insert( ) method has a similar form, and both methods perform significant manipulations to the buffer instead of creating new objects.

A-4-5. Strings are special

By now you've seen that the String class is not just another class in Java. There are a lot of special cases in String, not the least of which is that it's a built-in class and fundamental to Java. Then there's the fact that a quoted character string is converted to a String by the compiler and the special overloaded operators '+' and '+='. In this appendix you've seen the remaining special case: the carefully-built immutability using the companion StringBuffer and some extra magic in the compiler.

A-5. Summary

Because all object identifiers are references in Java, and because every object is created on the heap and garbage collected only when it is no longer used, the flavor of object manipulation changes, especially when passing and returning objects. For example, in C or C++, if you wanted to initialize some piece of storage in a method, you'd probably request that the user pass the address of that piece of storage into the method. Otherwise, you'd have to worry about who was responsible for destroying that storage. Thus, the interface and understanding of such methods is more complicated. But in Java, you never have to worry about responsibility or whether an object will still exist when it is needed, since that is always taken care of for you. You can create an object at the point that it is needed (and no sooner) and never worry about the mechanics of passing around responsibility for that object; you simply pass the reference. Sometimes the simplification that this provides is unnoticed. Other times it is staggering.

The downside to all this underlying magic is twofold:

  • You always take the efficiency hit for the extra memory management (although this can be quite small), and there's always a slight amount of uncertainty about the time something can take to run (since the garbage collector can be forced into action whenever you get low on memory). For most applications, the benefits outweigh the drawbacks, and the hotspot technologies in particular have sped things up to the point where it's not much of an issue.
  • Aliasing: Sometimes you can accidentally end up with two references to the same object, which is a problem only if both references are assumed to point to a distinct object. This is where you need to pay a little closer attention and, when necessary, clone( ) or otherwise duplicate an object to prevent the other reference from being surprised by an unexpected change. Alternatively, you can support aliasing for efficiency by creating immutable objects whose operations can return a new object of the same type or some different type, but never change the original object so that anyone aliased to that object sees no change.

Some people say that cloning in Java is a botched design that shouldn't be used, so they implement their own version of cloning (121) and never call the Object.clone( ) method, thus eliminating the need to implement Cloneable and catch the CloneNotSupportedException. This is certainly a reasonable approach, and since clone( ) is supported so rarely within the standard Java library, it is apparently a safe one as well.

A-6. Exercises

Solutions to selected exercises can be found in the electronic document The Thinking in Java Annotated Solution Guide, available for a small fee from www.BruceEckel.com.

  1. Demonstrate a second level of aliasing. Create a method that takes a reference to an object but doesn't modify that reference's object. However, the method calls a second method, passing it the reference, and this second method does modify the object.
  2. Create a class MyString containing a String object that you initialize in the constructor using the constructor's argument. Add a toString( ) method and a method concatenate( ) that appends a String object to your internal string. Implement clone( ) in MyString. Create two static methods that each take a MyString x reference as an argument and call x.concatenate("test"), but in the second method call clone( ) first. Test the two methods and show the different effects.
  3. Create a class called Battery containing an int that is a battery number (as a unique identifier). Make it cloneable and give it a toString( ) method. Now create a class called Toy that contains an array of Battery and a toString( ) that prints out all the batteries. Write a clone( ) for Toy that automatically clones all of its Battery objects. Test this by cloning Toy and printing the result.
  4. Change CheckCloneable.java so that all of the clone( ) methods catch the CloneNotSupportedException rather than passing it to the caller.
  5. Using the mutable-companion-class technique, make an immutable class containing an int, a double, and an array of char.
  6. Modify Compete.java to add more member objects to classes Thing2 and Thing4 and see if you can determine how the timings vary with complexity-whether it's a simple linear relationship or if it seems more complicated.
  7. Starting with Snake.java, create a deep-copy version of the snake.
  8. Implement the Collection interface in a class called CloningCollection by using a private ArrayList to provide the container functionality. Override the clone( ) method so that CloningCollection performs a "conditional deep copy"; it attempts to clone( ) all the elements it contains, but if it cannot it leaves the reference(s) aliased.

A-7. Appendix

 
Sélectionnez
public class Cloneit implements Cloneable {
	public static void main (String[] args) 
	throws CloneNotSupportedException {
		Cloneit a = new Cloneit();
		Cloneit b = (Cloneit)a.clone();
	}
}

précédentsommairesuivant
In C, which generally handles small bits of data, the default is pass by value. C++ had to follow this form, but with objects pass by value isn't usually the most efficient way. In addition, coding classes to support pass by value in C++ is a big headache.
This is not the dictionary spelling of the word, but it's what is used in the Java library, so I've used it here, too, in some hopes of reducing confusion.
You can apparently create a simple counter-example to this statement, like in appendix.
However, this only works because main( ) is a method of Cloneit and thus has permission to call the protected base-class method clone( ). If you call it from a different class, it won't compile.
Except for the poor avocado, which has been reclassified to simply "fat."
C++ allows the programmer to overload operators at will. Because this can often be a complicated process (see Chapter 10 of Thinking in C++, 2nd edition, Prentice Hall, 2000), the Java designers deemed it a "bad" feature that shouldn't be included in Java. It wasn't so bad that they didn't end up doing it themselves, and ironically enough, operator overloading would be much easier to use in Java than in C++. This can be seen in Python (see www.Python.org) which has garbage collection and straightforward operator overloading.
Doug Lea, who was helpful in resolving this issue, suggested this to me, saying that he simply creates a function called duplicate( ) for each class.

Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.