Read-only classes

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

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

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

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



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: August 20, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT When you look at natural user interfaces as a developer, it isn't just fun and games. There are some very serious, real-world usage models of how things can help make the world a better place – things like Intel® RealSense™ technology. Check out this upcoming eSeminar and join the panel of experts, both from inside and outside of Intel, as they discuss how natural user interfaces will likely be getting adopted in a wide variety …

  • With 81% of employees using their phones at work, companies have stopped asking: "Is corporate data leaking from personal devices?" and started asking: "How do we effectively prevent corporate data from leaking from personal devices?" The answer has not been simple. ZixOne raises the bar on BYOD security by not allowing email data to reside on the device. In addition, Zix allows employees to maintain complete control of their personal device, therefore satisfying privacy demands of valued employees and the …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds