|
|||||
![]() 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.
The
drawback to immutability
Creating
an immutable class seems at
first to provide an elegant
solution.
However,
whenever you do need a
modified object of that new
type you
must
suffer the overhead of a new
object creation, as well as
potentially
causing
more frequent garbage
collections. For some
classes this is not a
problem,
but for others (such as
the String
class)
it is prohibitively
expensive.
The
solution is to create a companion
class that can
be
modified. Then,
when
you're doing a lot of
modifications, you can
switch to using the
modifiable
companion class and switch
back to the immutable class
when
you're
done.
The
example above can be
modified to show
this:
//:
appendixa: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;
1050
Thinking
in Java
![]() }
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());
Appendix
A: Passing & Returning
Objects
1051
![]() 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:
1.
You
need immutable objects
and
2.
You
often need to make a lot of
modifications or
3.
It's
expensive to create new
immutable objects.
Immutable
Strings
Consider
the following code:
//:
appendixa:Stringer.java
public
class Stringer {
static
String upcase(String s) {
1052
Thinking
in Java
![]() 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
reference to q.
The
object this reference is
connected to stays put in a
single physical
location.
The references are copied as
they are passed
around.
Looking
at the definition for
upcase(
),
you can see that
the reference
that's
passed in has the name
s,
and it exists for only as
long as the body
of
upcase( )
is
being executed. When
upcase( )
completes,
the local
reference
s vanishes.
upcase( )
returns
the result, which is the
original
string
with all the characters
set to uppercase. Of course, it
actually
returns
a reference to the result.
But it turns out that
the reference that it
returns
is for a new object, and
the original q
is
left alone. How does
this
happen?
Implicit
constants
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
reference
(pointer or reference in C++)
could not be used to modify
the
original
object. But then the
C++ programmer was required
to be diligent
Appendix
A: Passing & Returning
Objects
1053
![]() and
remember to use const
everywhere.
It can be confusing and easy
to
forget.
Overloading
`+' and the StringBuffer
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
appendix),
you'll
see that every method in
the class that appears to
modify a String
really
creates and returns a brand
new String
object
containing the
modification.
The original String
is
left untouched. Thus,
there's no
feature
in Java like C++'s const
to
make the compiler support
the
immutability
of your objects. If you want
it, you have to wire it in
yourself,
like
String
does.
Since
String
objects
are immutable, you can
alias to a particular String
as
many times as you want.
Because it's read-only
there's no possibility
that
one reference will change
something that will affect
the other
references.
So a read-only object solves
the aliasing problem
nicely.
It
also seems possible to
handle all the cases in
which you need a
modified
object
by creating a brand new
version of the object with
the
modifications,
as String
does.
However, for some operations
this isn't
efficient.
A case in point is the
operator `+'
that has been overloaded
for
String
objects.
Overloading means that it
has been given an
extra
meaning
when used with a particular
class. (The `+'
and `+='
for String
are
the only operators that
are overloaded in Java, and
Java does not
allow
the programmer to overload
any others)5.
When
used with String
objects,
the `+'
allows you to concatenate
Strings
together:
String
s = "abc" + foo + "def" + Integer.toString(47);
5
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.
1054
Thinking
in Java
![]() 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:
//:
appendixa: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
Appendix
A: Passing & Returning
Objects
1055
![]() 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.
The
String
and
StringBuffer
classes
Here
is an overview of the methods
available for both String
and
StringBuffer
so
you can get a feel
for the way they
interact. These
tables
don't
contain every single method,
but rather the ones
that are important
to
this discussion. Methods
that are overloaded are
summarized in a
single
row.
First,
the String
class:
Method
Arguments,
Use
Overloading
Creating
String
Constructor
Overloaded:
Default,
objects.
String,
StringBuffer,
char
arrays,
byte
arrays.
length(
)
Number
of characters
in
the String.
charAt()
int
Index
The
char at a location in
the
String.
getChars(
),
The
beginning and
Copy
chars
or bytes
getBytes(
)
into
an external array.
end
from which to
copy,
the array to copy
into,
an index into the
destination
array.
toCharArray(
)
Produces
a char[]
containing
the
characters
in the
String.
equals(
), equals-
A
String
to
compare
An
equality check on
IgnoreCase(
)
with.
the
contents of the two
1056
Thinking
in Java
![]() Method
Arguments,
Use
Overloading
Strings.
compareTo(
)
A
String
to
compare
Result
is negative, zero,
with.
or
positive depending
on
the lexicographical
ordering
of the String
and
the argument.
Uppercase
and
lowercase
are not equal!
boolean
result
regionMatches(
)
Offset
into this
indicates
whether the
String,
the other
region
matches.
String
and
its offset
and
length to
compare.
Overload
adds
"ignore case."
startsWith(
)
String
that
it might
boolean
result
start
with. Overload
indicates
whether the
adds
offset into
String
starts
with the
argument.
argument.
endsWith(
)
String
that
might be
boolean
result
a
suffix of this String.
indicates
whether the
argument
is a suffix.
indexOf(
),
Overloaded:
char,
Returns
-1 if the
lastIndexOf(
)
char
and
starting
argument
is not found
index,
String,
within
this String,
String,
and starting
otherwise
returns the
index.
index
where the
argument
starts.
lastIndexOf(
)
searches
backward from
end.
substring(
)
Overloaded:
Starting
Returns
a new String
index,
starting index,
object
containing the
and
ending index.
specified
character set.
concat(
)
The
String
to
Returns
a new String
concatenate
object
containing the
original
String's
characters
followed by
Appendix
A: Passing & Returning
Objects
1057
![]() Method
Arguments,
Use
Overloading
the
characters in the
argument.
replace(
)
The
old character to
Returns
a new String
search
for, the new
object
with the
character
to replace it
replacements
made.
with.
Uses
the old String
if
no
match is found.
toLowerCase(
)
Returns
a new String
toUpperCase(
)
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,
Returns
a String
char[],
char[]
and
containing
a character
offset
and count,
representation
of the
argument.
boolean,
char,
int,
long,
float,
double.
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.
Here's
the StringBuffer
class:
Method
Arguments,
overloading
Use
1058
Thinking
in Java
![]() Method
Arguments,
overloading
Use
Constructor
Overloaded:
default, length
Create
a new
StringBuffer
object.
of
buffer to create, String
to
create from.
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
Makes
the
Capacity(
)
capacity.
StringBuffer
hold
at
least
the desired
number
of spaces.
setLength(
)
Integer
indicating new
Truncates
or expands
length
of character string in
the
previous character
buffer.
string.
If expanding,
pads
with nulls.
charAt(
)
Integer
indicating the
Returns
the char
at
location
of the desired
that
location in the
element.
buffer.
setCharAt(
)
Integer
indicating the
Modifies
the value at
that
location.
location
of the desired
element
and the new char
value
for the element.
Copy
chars
into an
getChars(
)
The
beginning and end
external
array. There
from
which to copy, the
is
no getBytes(
) as
array
to copy into, an
index
in
String.
into
the destination
array.
append(
)
Overloaded:
Object,
The
argument is
String,
char[],
char[]
converted
to a string
with
offset and length,
and
appended to the
boolean,
char,
int,
long,
end
of the current
float,
double.
buffer,
increasing the
buffer
if necessary.
insert(
)
Overloaded,
each with a
The
second argument
first
argument of the
offset
is
converted to a
Appendix
A: Passing & Returning
Objects
1059
![]() Method
Arguments,
overloading
Use
at
which to start
inserting:
string
and inserted
Object,
String,
char[],
into
the current buffer
boolean,
char,
int,
long,
beginning
at the
float,
double.
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.
Strings
are special
By
now you've seen that
the String
class
is not just another class in
Java.
There
are a lot of special cases
in String,
not the least of which is
that it's
a
built-in class and
fundamental to Java. Then
there's the fact that
a
quoted
character string is converted to a
String
by
the compiler and
the
special
overloaded operators +
and
+=.
In this appendix you've seen
the
remaining
special case: the carefully
built immutability using
the
companion
StringBuffer
and
some extra magic in the
compiler.
Summary
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
1060
Thinking
in Java
![]() 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:
1.
You
always take the efficiency
hit for the extra
memory
management
(although this can be quite
small), and there's
always
a
slight amount of uncertainty
about the time something
can take
to
run (since the garbage
collector can be forced into
action
whenever
you get low on memory).
For most applications,
the
benefits
outweigh the drawbacks, and
particularly time-critical
sections
can be written using
native
methods
(see Appendix B).
2.
Aliasing:
sometimes you can
accidentally end up with
two
references
to the same object, which is
a problem only if
both
references
are assumed to point to a
distinct
object.
This is where
you
need to pay a little closer
attention and, when
necessary,
clone(
) an
object to prevent the other
reference from being
surprised
by an unexpected change. Alternatively,
you can support
aliasing
for efficiency by creating
immutable objects
whose
operations
can return a new object of
the same type or
some
different
type, but never change
the original object so that
anyone
aliased
to that object sees no
change.
Some
people say that cloning in
Java is a botched design,
and to heck with
it,
so they implement their own
version of cloning6 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.
6
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.
Appendix
A: Passing & Returning
Objects
1061
![]() Exercises
Solutions
to selected exercises can be
found in the electronic
document The
Thinking in Java
Annotated
Solution Guide, available
for a small fee from
.
1.
Demonstrate
a second level of aliasing.
Create a method that
takes
a
reference to an object but
doesn't modify that
reference's object.
However,
the method calls a second
method, passing it
the
reference,
and this second method
does modify the
object.
2.
Create
a class myString
containing
a String
object
that you
initialize
in the constructor using the
constructor's argument.
Add
a
toString(
) method
and a method concatenate(
) that
appends
a String
object
to your internal string.
Implement
clone(
) in
myString.
Create two static
methods
that each take
a
myString x
reference
as an argument and
call
x.concatenate("test"),
but in the second method
call clone(
)
first.
Test the two methods
and show the different
effects.
3.
Create
a class called Battery
containing
an int
that
is a battery
number
(as a unique identifier).
Make it cloneable and give
it a
toString(
) method.
Now create a class called
Toy that
contains
an
array of Battery
and
a toString(
) that
prints out all
the
batteries.
Write a clone(
) for
Toy that
automatically clones all
of
its
Battery
objects.
Test this by cloning
Toy and
printing the
result.
4.
Change
CheckCloneable.java
so
that all of the clone(
)
methods
catch the CloneNotSupportedException
rather
than
passing
it to the caller.
5.
Using
the mutable-companion-class technique,
make an
immutable
class containing an int,
a double
and
an array of
char.
6.
Modify
Compete.java
to
add more member objects to
classes
Thing2
and
Thing4
and
see if you can determine
how the
timings
vary with complexity--whether
it's a simple linear
relationship
or if it seems more
complicated.
1062
Thinking
in Java
![]() 7.
Starting
with Snake.java,
create a deep-copy version of
the
snake.
8.
Inherit
an ArrayList
and
make its clone(
) perform
a deep copy.
Appendix
A: Passing & Returning
Objects
1063
Table of Contents:
|
|||||