ZeePedia

Java Programming Guidelines:Design, Implementation

<< B: The Java Native Interface (JNI):Calling a native method, the JNIEnv argument
Resources:Software, Books, My own list of books >>
img
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
img
}
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
img
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
img
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
img
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
img
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
img
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
img
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
img
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
img
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
img
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
img
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
img
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
img
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:
  1. Introduction to Objects:The progress of abstraction, An object has an interface
  2. Everything is an Object:You manipulate objects with references, Your first Java program
  3. Controlling Program Flow:Using Java operators, Execution control, true and false
  4. Initialization & Cleanup:Method overloading, Member initialization
  5. Hiding the Implementation:the library unit, Java access specifiers, Interface and implementation
  6. Reusing Classes:Composition syntax, Combining composition and inheritance
  7. Polymorphism:Upcasting revisited, The twist, Designing with inheritance
  8. Interfaces & Inner Classes:Extending an interface with inheritance, Inner class identifiers
  9. Holding Your Objects:Container disadvantage, List functionality, Map functionality
  10. Error Handling with Exceptions:Basic exceptions, Catching an exception
  11. The Java I/O System:The File class, Compression, Object serialization, Tokenizing input
  12. Run-time Type Identification:The need for RTTI, A class method extractor
  13. Creating Windows & Applets:Applet restrictions, Running applets from the command line
  14. Multiple Threads:Responsive user interfaces, Sharing limited resources, Runnable revisited
  15. Distributed Computing:Network programming, Servlets, CORBA, Enterprise JavaBeans
  16. A: Passing & Returning Objects:Aliasing, Making local copies, Cloning objects
  17. B: The Java Native Interface (JNI):Calling a native method, the JNIEnv argument
  18. Java Programming Guidelines:Design, Implementation
  19. Resources:Software, Books, My own list of books
  20. Index