Read-only classes

Bruce Eckel’s Thinking in Java Contents | Prev | Next

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?

//: 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.

Creating
read-only classes

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.

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:

//: 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:

  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:

//: 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?


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.


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 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.

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.

The
String and StringBuffer classes

First,
the
String
class:

Method

Arguments,
Overloading

Use

Constructor

Overloaded:
Default,
String,
StringBuffer,
char

arrays,
byte
arrays.

Creating
String
objects.

length( )

Number
of characters in
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.

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-

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.

Strings
are special


[52]
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++.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read