|
[ Seminars ] [ Seminars on CD ROM ] [ Consulting ] Thinking in Java, 2nd edition, Revision 12©2000 by Bruce Eckel[ Previous Chapter ] [ Short TOC ] [ Table of Contents ] [ Index ] [ Next Chapter ]A: Passing & Returning ObjectsIn 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.
Passing references aroundWhen you pass a
reference
into a method, you’re still pointing to the same object. A simple
experiment demonstrates this: //: appendixa:PassReferences.java // Passing references around. public class PassReferences { 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); } } ///:~ 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: p inside main(): PassReferences@1653748 h inside f(): PassReferences@1653748 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.
Aliasing
Aliasing means that more than one
reference 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 references aren’t expecting that object to change, they’ll be
surprised. This can be demonstrated with a simple example: //: appendixa:Alias1.java // Aliasing two references 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 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); } } ///:~ In the line: 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:
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 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:
//: appendixa:Alias2.java // Method calls implicitly alias their // arguments. public class Alias2 { int i; Alias2(int ii) { i = ii; } 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); } } ///:~ 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 appendix.
Making local copiesTo 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:
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[79]. Java
does not, but it allows you to produce this effect.
Pass by valueThis 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 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.
Cloning objectsThe 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 ArrayList overrides clone( ), so we can call
clone( ) for ArrayList: //: appendixa: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) { 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); } } ///:~ 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: 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[80].
Adding cloneability to a classEven though the clone method is defined
in the base-of-all-classes Object, cloning is not automatically
available in every
class[81]. 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.
Using a trick with protectedTo 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: 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:
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.
Implementing the Cloneable interfaceThere’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! 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
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 12) to find out whether the reference is connected to an object that can
be cloned: 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.
Successful cloningOnce 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: //: appendixa: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.err.println("MyObject can't clone"); } return o; } public String toString() { return Integer.toString(i); } } public class LocalCopy { static MyObject g(MyObject v) { // Passing a reference, // 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 reference 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 reference 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 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 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 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!
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 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
references? The following example tests this: //: appendixa:Snake.java // Tests cloning to see if destination // of references 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) { 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); } } ///:~ 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( ), 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.
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: //: appendixa: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(System.err); } 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(System.err); } 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(System.err); } // Must clone references: 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 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 deep copy with ArrayListLet’s revisit the
ArrayList example from
earlier in this appendix. This time the Int2 class is cloneable, so the
ArrayList can be deep copied: //: appendixa: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.err.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(); 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(); // 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 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.
Deep copy via serializationWhen you consider Java’s object
serialization (introduced in Chapter 11), 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: //: 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 { 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 { static final int SIZE = 5000; 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 really tell the tale. Here is the output from three
different runs: Duplication via serialization: 940 Milliseconds Duplication via cloning: 50 Milliseconds Duplication via serialization: 710 Milliseconds Duplication via cloning: 60 Milliseconds Duplication via serialization: 770 Milliseconds Duplication via cloning: 50 Milliseconds Despite the significant time difference
between serialization and cloning, you’ll also notice that the
serialization technique seems to vary more in its duration, while cloning tends
to be more stable.
Adding cloneability
|
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
ref 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.
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 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.
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.
Because everything is a reference 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
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:
Some
people say that cloning in Java is a botched design, and to heck with it, so
they implement their own version of
cloning[84] 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.
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.
[79]
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.
[80]
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.
[81]
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.
[82]
Except for the poor avocado, which has been reclassified to simply
“fat.”
[83]
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.
[84]
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.