Bruce Eckel’s Thinking in Java | Contents | Prev | Next |
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?
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.
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? } } ///:~
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.
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); } } ///:~
that
n
is friendly to simplify coding.
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( )):
IntValue { int n; }
the element out and casting it is a bit awkward, but that’s a feature of
Vector,
not of
IntValue.
Creating
read-only classes
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()); } } ///:~
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.
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
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.
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.
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()); } } ///:~
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.
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.)
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.
approach makes sense, then, when:
- You
need immutable objects and - You
often need to make a lot of modifications or - It’s
expensive to create new immutable objects
Immutable
Strings
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 } } ///:~
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.
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?
you say:
s = "asdf";
String
x = Stringer.upcase(s);
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.
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.
Overloading
‘+’ and the StringBuffer
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.
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.
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
[52]).
used with
String
objects, the ‘
+’
allows you to concatenate
Strings
together:
s = "abc" + foo + "def" + Integer.toString(47);
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.
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.
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); } } ///:~
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.
The
String and StringBuffer classes
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.
the
String
class:
Constructor
|
Overloaded:
Default, String, StringBuffer, char arrays, byte arrays. |
Creating
String objects. |
length( )
|
Number
of characters in 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 handle for each unique character sequence. |
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.
Constructor
|
Overloaded:
default, length of buffer to create, String to create from. |
Create
a new StringBuffer object. |
toString( )
|
Creates
a String from this StringBuffer. |
|
length( )
|
Number
of characters in the StringBuffer. |
|
capacity( )
|
Returns
current number of spaces allocated. |
|
ensure-
Capacity( ) |
Integer
indicating desired capacity. |
Makes
the StringBuffer hold at least the desired number of spaces. |
setLength( )
|
Integer
indicating new length of character string in buffer. |
Truncates
or expands the previous character string. If expanding, pads with nulls. |
charAt( )
|
Integer
indicating the location of the desired element. |
Returns
the char at that location in the buffer. |
setCharAt( )
|
Integer
indicating the location of the desired element and the new char value for the element. |
Modifies
the value at that location. |
getChars( )
|
The
beginning and end from which to copy, the array to copy into, an index into the destination array. |
Copy
chars into an external array. There’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. |
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
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.
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++.