By this time you should
be reasonably comfortable with the idea that when you’re
“passing” an object, you’re actually passing a
handle.
In many programming languages, if
not all of them, 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 with them as you pass them around and assign to
them. This chapter will provide that insight.
Another way to pose the question of
this chapter, 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 in another way, Java has pointers, but no pointer arithmetic.
These are what I’ve been calling “handles,” 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.
When you pass a
handle
into a method, you’re still pointing to the same object. A simple
experiment demonstrates this:
//: PassHandles.java // Passing handles around package c12; public class PassHandles { static void f(PassHandles h) { System.out.println("h inside f(): " + h); } public static void main(String[] args) { PassHandles p = new PassHandles(); System.out.println("p inside main(): " + p); f(p); } } ///:~
The method toString( )
is automatically invoked in the print statements, and PassHandles
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 handle, but the
actual object storage). The output looks like this:
p inside main(): PassHandles@1653748 h inside f(): PassHandles@1653748
You can see that both p and
h refer to the same object. This is far more efficient than duplicating a
new PassHandles object just so that you can send an argument to a method.
But it brings up an important
issue.
Aliasing means that more than one
handle is tied to the same object, as in the above example. The problem with
aliasing occurs when someone writes to that object. If the owners of the
other handles aren’t expecting that object to change, they’ll be
surprised. This can be demonstrated with a simple example:
//: Alias1.java // Aliasing two handles to one object public class Alias1 { int i; Alias1(int ii) { i = ii; } public static void main(String[] args) { Alias1 x = new Alias1(7); Alias1 y = x; // Assign the handle 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); } } ///:~
In the line:
Alias1 y = x; // Assign the handle
a new Alias1 handle is
created, but instead of being assigned to a fresh object created with
new, it’s assigned to an existing handle. So the contents of handle
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:
x.i++;
y’s i will be
affected as well. This can be seen in the output:
x: 7 y: 7 Incrementing x x: 8 y: 8
One good solution in this case is
to simply not do it: don’t consciously alias more than one handle to an
object at the same scope. Your code will be much easier to understand and debug.
However, when you’re passing a handle in as an argument – which is
the way Java is supposed to work – you automatically alias because the
local handle that’s created can modify the “outside object”
(the object that was created outside the scope of the method). Here’s an
example:
//: Alias2.java // Method calls implicitly alias their // arguments. public class Alias2 { int i; Alias2(int ii) { i = ii; } static void f(Alias2 handle) { handle.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); } } ///:~
The output is:
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. (A method is how you “send a message”
to that object.) 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
chapter.
To review: all argument passing in
Java is performed by passing handles. That is, when you pass “an
object,” you’re really passing only a handle to an object that lives
outside the method, so if you perform any modifications with that handle, you
modify the outside object. In addition:
If
you’re only reading information from an object and not modifying it,
passing a handle 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.[46]
Java does not, but it allows you to produce this
effect.
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:
Having given both
camps a good airing and after saying “It depends on how you think of a
handle,” I will attempt to sidestep the issue for the rest of the book. In
the end, it isn’t that important – what is important is that
you understand that passing a handle allows the caller’s object to be
changed
unexpectedly.
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, you simply use the clone( ) method to
perform the operation. This is a method that’s defined as protected
in the base class Object and which you must override as public
in any derived classes that you want to clone. For example, the standard
library class Vector overrides clone( ), so we can call
clone( ) for Vector:
//: Cloning.java // The clone() operation works for only a few // items in the standard Java library. 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 { public static void main(String[] args) { Vector v = new Vector(); for(int i = 0; i < 10; i++ ) v.addElement(new Int(i)); System.out.println("v: " + v); Vector v2 = (Vector)v.clone(); // Increment all v2's elements: for(Enumeration e = v2.elements(); e.hasMoreElements(); ) ((Int)e.nextElement()).increment(); // See if it changed v's elements: System.out.println("v: " + v); } } ///:~
The clone( ) method
produces an Object, which must then be recast to the proper type. This
example shows how Vector’s clone( ) method does
not automatically try to clone each of the objects that the Vector
contains – the old Vector and the cloned Vector 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 handles 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:
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 Vector is probably a fair assumption
because there’s no guarantee that those objects are
cloneable.[47]
Even though the clone method is
defined in the base-of-all-classes Object, cloning is not
automatically available in every
class.[48]
This would seem to be counterintuitive to the idea that base-class methods are
always available in derived classes. Cloning in Java goes against this idea; if
you want it to exist for a class, you must specifically add code to make cloning
work.
To prevent default clonability 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 handle 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:
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
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: virtually always call super.clone( )
and 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 clonability 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.
There’s one more thing you
need to do to complete the clonability of an object: implement the
Cloneable interface. This
interface is a bit strange because it’s
empty!
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 here
is considered by some to be a “hack” because it’s using a
feature for something other than its original intent. Implementing the
Cloneable interface acts as a kind of a 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 handle 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 11) to find out whether the handle is connected to an object that can
be cloned:
if(myHandle instanceof Cloneable) // ...
The second reason is that mixed
into this design for clonability 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.
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:
//: LocalCopy.java // Creating local copies with clone() import java.util.*; class MyObject implements Cloneable { int i; MyObject(int ii) { i = ii; } public Object clone() { Object o = null; try { o = super.clone(); } catch (CloneNotSupportedException e) { System.out.println("MyObject can't clone"); } return o; } public String toString() { return Integer.toString(i); } } public class LocalCopy { static MyObject g(MyObject v) { // Passing a handle, modifies outside object: v.i++; return v; } static MyObject f(MyObject v) { v = (MyObject)v.clone(); // Local copy v.i++; return v; } public static void main(String[] args) { MyObject a = new MyObject(11); MyObject b = g(a); // Testing handle equivalence, // not object equivalence: if(a == b) System.out.println("a == b"); else System.out.println("a != b"); System.out.println("a = " + a); System.out.println("b = " + b); MyObject c = new MyObject(47); MyObject d = f(c); if(c == d) System.out.println("c == d"); else System.out.println("c != d"); System.out.println("c = " + c); System.out.println("d = " + d); } } ///:~
First of all, clone( )
must be accessible so 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-catch 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. g( ) shows passing by
reference in which it modifies the outside object and returns a reference to
that outside object, while f( ) clones the argument, thereby
decoupling it and leaving the original object alone. It can then proceed to do
whatever it wants, and even to return a handle to this new object without any
ill effects to the original. Notice the somewhat curious-looking
statement:
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 everything that has a
name is actually a handle. So the handle v is used to
clone( ) a copy of what it refers to, and this returns a handle 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
in the two different methods is tested. The output is:
a == b a = 12 b = 12 c != d c = 47 d = 48
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 contents of the handles.
If the addresses inside the
handles are the same, the
handles are pointing to the same object and are therefore “equal.”
So what the operators are really testing is whether the handles are aliased to
the same
object!
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 precise 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 bitcopy 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
handles? The following example tests this:
//: Snake.java // Tests cloning to see if destination of // handles are also cloned. public class Snake implements Cloneable { private Snake next; private char c; // Value of i == number of segments Snake(int i, char x) { c = x; if(--i > 0) next = new Snake(i, (char)(x + 1)); } 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) {} 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); } } ///:~
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. The output
is:
s = :a:b:c:d:e s2 = :a:b:c:d:e after s.increment, s2 = :a:c:d:e:f
This means that only the first
segment is duplicated by Object.clone( ), so 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 handle in your object; otherwise those handles
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.
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 handles, 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:
//: DeepCopy.java // Cloning a composed object 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; } } 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; } } 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 handles: o.depth = (DepthReading)o.depth.clone(); o.temperature = (TemperatureReading)o.temperature.clone(); return o; // Upcasts back to Object } } public class DeepCopy { public static void main(String[] args) { OceanReading reading = new OceanReading(33.9, 100.5); // Now clone it: OceanReading r = (OceanReading)reading.clone(); } } ///:~
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 handles 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
handles).
Let’s revisit the
Vector example from
earlier in this chapter. This time the Int2 class is cloneable so the
Vector can be deep copied:
//: AddingClone.java // You must go through a few gyrations to // add cloning to your own class. 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.out.println("Int2 can't clone"); } return o; } } // Once it's cloneable, inheritance // doesn't remove cloneability: class Int3 extends Int2 { private int j; // Automatically duplicated public Int3(int i) { super(i); } } public class AddingClone { 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(); Vector v = new Vector(); for(int i = 0; i < 10; i++ ) v.addElement(new Int2(i)); System.out.println("v: " + v); Vector v2 = (Vector)v.clone(); // Now clone each element: for(int i = 0; i < v.size(); i++) v2.setElementAt( ((Int2)v2.elementAt(i)).clone(), i); // Increment all v2's elements: for(Enumeration e = v2.elements(); e.hasMoreElements(); ) ((Int2)e.nextElement()).increment(); // See if it changed v's elements: System.out.println("v: " + v); System.out.println("v2: " + v2); } } ///:~
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 handles 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 a Vector: after the Vector is
cloned, you have to step through and clone each one of the objects pointed to by
the Vector. You’d have to do something similar to this to do a deep
copy of a Hashtable.
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.
When you consider Java
1.1 object serialization (introduced in Chapter 10), 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:
//: 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.out.println("Thing3 can't clone"); } return o; } } class Thing4 implements Cloneable { Thing3 o3 = new Thing3(); public Object clone() { Thing4 o = null; try { o = (Thing4)super.clone(); } catch (CloneNotSupportedException e) { System.out.println("Thing4 can't clone"); } // Clone the field, too: o.o3 = (Thing3)o3.clone(); return o; } } public class Compete { static final int SIZE = 5000; public static void main(String[] args) { 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(); try { 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"); } catch(Exception e) { e.printStackTrace(); } } } ///:~
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 really tell the tale. Here is the
output from three different runs:
Duplication via serialization: 3400 Milliseconds Duplication via cloning: 110 Milliseconds Duplication via serialization: 3410 Milliseconds Duplication via cloning: 110 Milliseconds Duplication via serialization: 3520 Milliseconds Duplication via cloning: 110 Milliseconds
Despite the obviously huge time
difference between serialization and cloning, you’ll also notice that the
serialization technique seems to vary significantly in its duration, while
cloning takes the same amount of time every
time.
If you create a new class, its base
class defaults to Object, which defaults to non-clonability (as
you’ll see in the next section). As long as you don’t explicitly add
clonability, 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:
//: HorrorFlick.java // You can insert Cloneability at any // level of inheritance. 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 InternalError(); } } } 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 clonability was added, the
compiler stopped you from trying to clone things. When clonability is added in
Scientist, then Scientist and all its descendants are
cloneable.
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? What follows is not a substantiated
story – probably because much of the marketing around Java makes it out to
be a perfectly-designed language – but it does go a long way toward
explaining how things ended up the way they did.
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 use 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.
You might suggest that, to remove
clonability, the
clone( ) method simply be made private, but this won’t
work since you cannot take a base-class method and make it more private
in a derived class. So it’s not that simple. And yet, it’s necessary
to be able to control whether an object can be cloned. There are actually a
number of attitudes you can take to this in a class that you
design:
Here’s an
example that shows the various ways cloning can be implemented and then, later
in the hierarchy, “turned off:”
//: CheckCloneable.java // Checking to see if a handle can be cloned // Can't clone this because it doesn't // override clone(): class Ordinary {} // Overrides clone, but doesn't implement // Cloneable: class WrongClone extends Ordinary { public Object clone() throws CloneNotSupportedException { return super.clone(); // Throws exception } } // Does all the right things for cloning: class IsCloneable extends Ordinary implements Cloneable { public Object clone() throws CloneNotSupportedException { return super.clone(); } } // Turn off cloning by throwing the exception: class NoMore extends IsCloneable { public Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); } } class TryMore extends NoMore { public Object clone() throws CloneNotSupportedException { // Calls NoMore.clone(), throws exception: return super.clone(); } } class BackOn extends NoMore { private BackOn duplicate(BackOn b) { // Somehow make a copy of b // and return that copy. This is a dummy // copy, just to make the point: return new BackOn(); } public Object clone() { // Doesn't call NoMore.clone(): return duplicate(this); } } // Can't inherit from this, so can't override // the clone method like in BackOn: final class ReallyNoMore extends NoMore {} public class CheckCloneable { static Ordinary tryToClone(Ordinary ord) { String id = ord.getClass().getName(); Ordinary x = null; if(ord instanceof Cloneable) { try { System.out.println("Attempting " + id); x = (Ordinary)((IsCloneable)ord).clone(); System.out.println("Cloned " + id); } catch(CloneNotSupportedException e) { System.out.println( "Could not clone " + id); } } return x; } public static void main(String[] args) { // Upcasting: Ordinary[] ord = { new IsCloneable(), new WrongClone(), new NoMore(), new TryMore(), new BackOn(), new ReallyNoMore(), }; Ordinary x = new Ordinary(); // This won't compile, since clone() is // protected in Object: //! x = (Ordinary)x.clone(); // tryToClone() checks first to see if // a class implements Cloneable: for(int i = 0; i < ord.length; i++) tryToClone(ord[i]); } } ///:~
The first class, Ordinary,
represents the kinds of classes we’ve seen throughout the book: no support
for cloning, but as it turns out, no prevention of cloning either. But if you
have a handle to an Ordinary object that might have been upcast from a
more derived class, you can’t tell if it can be cloned or
not.
The class WrongClone shows
an incorrect way to implement cloning. It does override
Object.clone( ) and makes that method public, but it
doesn’t implement Cloneable, so when super.clone( ) is
called (which results in a call to Object.clone( )),
CloneNotSupportedException is thrown so the cloning doesn’t
work.
In IsCloneable you can see
all the right actions performed for cloning: clone( ) is overridden
and Cloneable is implemented. However, this clone( ) method
and several others that follow in this example do not catch
CloneNotSupportedException, but instead pass it through to the caller,
who must then put a try-catch block around it. In your own clone( )
methods you will typically catch CloneNotSupportedException inside
clone( ) rather than passing it through. As you’ll see, in
this example it’s more informative to pass the exceptions
through.
Class NoMore attempts to
“turn off” cloning in the way that the Java designers intended: in
the derived class clone( ), you throw
CloneNotSupportedException. The clone( ) method in class
TryMore properly calls super.clone( ), and this resolves to
NoMore.clone( ), which throws an exception and prevents
cloning.
But what if the programmer
doesn’t follow the “proper” path of calling
super.clone( )
inside the overridden clone( ) method? In BackOn, you can see
how this can happen. This class uses a separate method duplicate( )
to make a copy of the current object and calls this method inside
clone( ) instead of calling super.clone( ). The
exception is never thrown and the new class is cloneable. You can’t rely
on throwing an exception to prevent making a cloneable class. The only sure-fire
solution is shown in ReallyNoMore, which is final and thus cannot
be inherited. That means if clone( ) throws an exception in the
final class, it cannot be modified with inheritance and the prevention of
cloning is assured. (You cannot explicitly call Object.clone( ) from
a class that has an arbitrary level of inheritance; you are limited to calling
super.clone( ), which has access to only the direct base class.)
Thus, if you make any objects that involve security issues, you’ll want to
make those classes final.
The first method you see in class
CheckCloneable is tryToClone( ), which takes any
Ordinary object and checks to see whether it’s cloneable with
instanceof. If so, it casts the object to an IsCloneable, calls
clone( ) and casts the result back to Ordinary, catching any
exceptions that are thrown. Notice the use of run-time type identification (see
Chapter 11) to print out the class name so you can see what’s
happening.
In main( ), different
types of Ordinary objects are created and upcast to Ordinary in
the array definition. The first two lines of code after that create a plain
Ordinary object and try to clone it. However, this code will not compile
because clone( ) is a protected method in Object. The
remainder of the code steps through the array and tries to clone each object,
reporting the success or failure of each. The output is:
Attempting IsCloneable Cloned IsCloneable Attempting NoMore Could not clone NoMore Attempting TryMore Could not clone TryMore Attempting BackOn Cloned BackOn Attempting ReallyNoMore Could not clone ReallyNoMore
So to summarize, if you want a
class to be cloneable:
Cloning can seem to be a
complicated process to set up. It might seem like there should be an
alternative. One 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. Here’s an
example:
//: CopyConstructor.java // A constructor for copying an object // of the same type, as an attempt to create // a local copy. class FruitQualities { private int weight; private int color; private int firmness; private int ripeness; private int smell; // etc. FruitQualities() { // Default constructor // do something meaningful... } // Other constructors: // ... // Copy constructor: FruitQualities(FruitQualities f) { weight = f.weight; color = f.color; firmness = f.firmness; ripeness = f.ripeness; smell = f.smell; // etc. } } class Seed { // Members... Seed() { /* Default constructor */ } Seed(Seed s) { /* Copy constructor */ } } class Fruit { private FruitQualities fq; private int seeds; private Seed[] s; Fruit(FruitQualities q, int seedCount) { fq = q; seeds = seedCount; s = new Seed[seeds]; for(int i = 0; i < seeds; i++) s[i] = new Seed(); } // Other constructors: // ... // Copy constructor: Fruit(Fruit f) { fq = new FruitQualities(f.fq); seeds = f.seeds; // Call all Seed copy-constructors: for(int i = 0; i < seeds; i++) s[i] = new Seed(f.s[i]); // Other copy-construction activities... } // To allow derived constructors (or other // methods) to put in different qualities: protected void addQualities(FruitQualities q) { fq = q; } protected FruitQualities getQualities() { return fq; } } class Tomato extends Fruit { Tomato() { super(new FruitQualities(), 100); } Tomato(Tomato t) { // Copy-constructor super(t); // Upcast for base copy-constructor // Other copy-construction activities... } } class ZebraQualities extends FruitQualities { private int stripedness; ZebraQualities() { // Default constructor // do something meaningful... } ZebraQualities(ZebraQualities z) { super(z); stripedness = z.stripedness; } } class GreenZebra extends Tomato { GreenZebra() { addQualities(new ZebraQualities()); } GreenZebra(GreenZebra g) { super(g); // Calls Tomato(Tomato) // Restore the right qualities: addQualities(new ZebraQualities()); } void evaluate() { ZebraQualities zq = (ZebraQualities)getQualities(); // Do something with the qualities // ... } } public class CopyConstructor { public static void ripen(Tomato t) { // Use the "copy constructor": t = new Tomato(t); System.out.println("In ripen, t is a " + t.getClass().getName()); } public static void slice(Fruit f) { f = new Fruit(f); // Hmmm... will this work? System.out.println("In slice, f is a " + f.getClass().getName()); } public static void main(String[] args) { Tomato tomato = new Tomato(); ripen(tomato); // OK slice(tomato); // OOPS! GreenZebra g = new GreenZebra(); ripen(g); // OOPS! slice(g); // OOPS! g.evaluate(); } } ///:~
This seems a bit strange at first.
Sure, fruit has qualities, but why not just put data members representing those
qualities directly into the Fruit class? There are two potential reasons.
The first is that you might want to easily insert or change the qualities. Note
that Fruit has a protected addQualities( ) method to
allow derived classes to do this. (You might think the logical thing to do is to
have a protected constructor in Fruit that takes a
FruitQualities argument, but constructors don’t inherit so it
wouldn’t be available in second or greater level classes.) By making the
fruit qualities into a separate class, you have greater flexibility, including
the ability to change the qualities midway through the lifetime of a particular
Fruit object.
The second reason for making
FruitQualities a separate object is in case you want to add new qualities
or to change the behavior via inheritance and polymorphism. Note that for
GreenZebra (which really is a type of tomato – I’ve grown
them and they’re fabulous), the constructor calls
addQualities( ) and passes it a ZebraQualities object, which
is derived from FruitQualities so it can be attached to the
FruitQualities handle in the base class. Of course, when
GreenZebra uses the FruitQualities it must downcast it to the
correct type (as seen in evaluate( )), but it always knows that type
is ZebraQualities.
You’ll also see that
there’s a Seed class, and that Fruit (which by definition
carries its own seeds) contains an array of Seeds.
Finally, notice that each class has
a copy constructor, and that each copy constructor must take care to call the
copy constructors for the base class and member objects to produce a deep copy.
The copy constructor is tested inside the class CopyConstructor. The
method ripen( ) takes a Tomato argument and performs
copy-construction on it in order to duplicate the object:
t = new Tomato(t);
while slice( ) takes a
more generic Fruit object and also duplicates it:
f = new Fruit(f);
These are tested with different
kinds of Fruit in main( ). Here’s the
output:
In ripen, t is a Tomato In slice, f is a Fruit In ripen, t is a Tomato In slice, f is a Fruit
This is where the problem shows up.
After the copy-construction that happens to the Tomato inside
slice( ), the result is no longer a Tomato object, but just a
Fruit. It has lost all of its tomato-ness. Further, when you take a
GreenZebra, both ripen( ) and slice( ) turn it
into a Tomato and a Fruit, respectively. Thus, unfortunately, the
copy constructor scheme is no good to us in Java when attempting to make a local
copy of an object.
The copy constructor is a
fundamental part of C++, since it automatically makes a local copy of an object.
Yet the example above proves that it does not work for Java. Why? In Java
everything that we manipulate is a handle, while in C++ you can have handle-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.
While 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 which
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 collection such as a Vector (which takes
only Object handles), you can wrap your int inside the standard
library Integer class:
//: ImmutableInteger.java // The Integer class cannot be changed import java.util.*; public class ImmutableInteger { public static void main(String[] args) { Vector v = new Vector(); for(int i = 0; i < 10; i++) v.addElement(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: they have 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:
//: MutableInteger.java // A changeable wrapper class import java.util.*; class IntValue { int n; IntValue(int x) { n = x; } public String toString() { return Integer.toString(n); } } public class MutableInteger { public static void main(String[] args) { Vector v = new Vector(); for(int i = 0; i < 10; i++) v.addElement(new IntValue(i)); System.out.println(v); for(int i = 0; i < v.size(); i++) ((IntValue)v.elementAt(i)).n++; System.out.println(v); } } ///:~
Note that n is friendly to
simplify coding.
IntValue can be even simpler
if 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( )):
class IntValue { int n; }
Fetching the element out and
casting it is a bit awkward, but that’s a feature of Vector, not of
IntValue.
It’s possible to create your
own read-only class. Here’s an example:
//: Immutable1.java // Objects that cannot be modified // are immune to aliasing. public class Immutable1 { private int data; public Immutable1(int initVal) { data = initVal; } public int read() { return data; } public boolean nonzero() { return data != 0; } public Immutable1 quadruple() { return new Immutable1(data * 4); } static void f(Immutable1 i1) { Immutable1 quad = i1.quadruple(); 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()); } } ///:~
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
quadruple( ), 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.
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 example above can be modified
to show this:
//: Immutable2.java // A companion class for making changes // to immutable objects. 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 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()); } } ///:~
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:
Consider the following
code:
//: Stringer.java public class Stringer { 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 } } ///:~
When q is passed in to
upcase( ) it’s actually a copy of the handle to q. The
object this handle is connected to stays put in a single physical location. The
handles are copied as they are passed around.
Looking at the definition for
upcase( ), you can see that the handle 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 handle s vanishes. upcase( ) returns the result,
which is the original string with all the characters set to uppercase. Of
course, it actually returns a handle to the result. But it turns out that the
handle that it returns is for a new object, and the original q is left
alone. How does this happen?
If you say:
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
handle (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.
Objects of the String class
are designed to be immutable, using the technique shown previously. If you
examine the online documentation for the String class (which is
summarized a little later in this chapter), 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 handle will change something that will affect the other
handles. 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[49]).
When used with String
objects, the ‘+’ allows you to concatenate Strings
together:
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:
//: ImmutableStrings.java // Demonstrating StringBuffer public class ImmutableStrings { 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); } } ///:~
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.
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:
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 handle to the original
String. This saves storage and overhead.
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- |
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’s 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.
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 chapter
you’ve seen the remaining special case: the carefully-built immutability
using the companion StringBuffer and some extra magic in the
compiler.
Because everything is a handle 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. Your
programs 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 handle. Sometimes the simplification that this
provides is unnoticed, other times it is staggering.
The downside to all this underlying
magic is twofold:
Some people say
that cloning in Java is a botched design, and to heck with it, so they implement
their own version of
cloning[50]
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. But as long as you don’t call Object.clone( ) you
don’t need to implement Cloneable or catch the exception, so that
would seem acceptable as well.
It’s interesting to notice
that one of the “reserved but not implemented” keywords in Java is
byvalue. After seeing the issues of aliasing and cloning, you can imagine
that byvalue might someday be used to implement an automatic local copy
in Java. This could eliminate the more complex issues of cloning and make coding
in these situations simpler and more
robust.
[46]
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.
[47]
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.
[48]
You can apparently create a simple counter-example to this statement, like
this:
public class Cloneit implements Cloneable { public static void main (String[] args) throws CloneNotSupportedException { Cloneit a = new Cloneit(); Cloneit b = (Cloneit)a.clone(); } }
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.
[49]
C++ allows the programmer to overload operators at will. Because this can often
be a complicated process (see Chapter 10 of Thinking in C++
Prentice-Hall, 1995), 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++.
[50]
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.