ZeePedia

Error Handling with Exceptions:Basic exceptions, Catching an exception

<< Holding Your Objects:Container disadvantage, List functionality, Map functionality
The Java I/O System:The File class, Compression, Object serialization, Tokenizing input >>
img
a.getClass().getName());
for(int i = 0; i < tests.length; i++) {
Collections2.fill(a,
Collections2.countries.reset(),
tests[i].size);
System.out.print(tests[i].name);
long t1 = System.currentTimeMillis();
tests[i].test(a, reps);
long t2 = System.currentTimeMillis();
System.out.println(": " + (t2 - t1));
}
}
public static void testArray(int reps) {
System.out.println("Testing array as List");
// Can only do first two tests on an array:
for(int i = 0; i < 2; i++) {
String[] sa = new String[tests[i].size];
Arrays2.fill(sa,
Collections2.countries.reset());
List a = Arrays.asList(sa);
System.out.print(tests[i].name);
long t1 = System.currentTimeMillis();
tests[i].test(a, reps);
long t2 = System.currentTimeMillis();
System.out.println(": " + (t2 - t1));
}
}
public static void main(String[] args) {
int reps = 50000;
// Or, choose the number of repetitions
// via the command line:
if(args.length > 0)
reps = Integer.parseInt(args[0]);
System.out.println(reps + " repetitions");
testArray(reps);
test(new ArrayList(), reps);
test(new LinkedList(), reps);
test(new Vector(), reps);
}
} ///:~
504
Thinking in Java
img
The inner class Tester is abstract, to provide a base class for the specific
tests. It contains a String to be printed when the test starts, a size
parameter to be used by the test for quantity of elements or repetitions of
tests, a constructor to initialize the fields, and an abstract method
test( ) that does the work. All the different types of tests are collected in
one place, the array tests, which is initialized with different anonymous
inner classes that inherit from Tester. To add or remove tests, simply add
or remove an inner class definition from the array, and everything else
happens automatically.
To compare array access to container access (primarily against
ArrayList), a special test is created for arrays by wrapping one as a List
using Arrays.asList( ). Note that only the first two tests can be
performed in this case, because you cannot insert or remove elements
from an array.
The List that's handed to test( ) is first filled with elements, then each
test in the tests array is timed. The results will vary from machine to
machine; they are intended to give only an order of magnitude
comparison between the performance of the different containers. Here is
a summary of one run:
Type
Get
Iteration
Insert
Remove
array
1430
3850
na
na
ArrayList
3070
12200
500
46850
LinkedList
16320
9110
110
60
Vector
4890
16250
550
46850
As expected, arrays are faster than any container for random-access
lookups and iteration. You can see that random accesses (get( )) are
cheap for ArrayLists and expensive for LinkedLists. (Oddly, iteration
is faster for a LinkedList than an ArrayList, which is a bit
counterintuitive.) On the other hand, insertions and removals from the
middle of a list are dramatically cheaper for a LinkedList than for an
ArrayList--especially removals. Vector is generally not as fast as
ArrayList, and it should be avoided; it's only in the library for legacy
code support (the only reason it works in this program is because it was
adapted to be a List in Java 2). The best approach is probably to choose
Chapter 9: Holding Your Objects
505
img
an ArrayList as your default, and to change to a LinkedList if you
discover performance problems due to many insertions and removals
from the middle of the list. And of course, if you are working with a fixed-
sized group of elements, use an array.
Choosing between Sets
You can choose between a TreeSet and a HashSet, depending on the
size of the Set (if you need to produce an ordered sequence from a Set,
use TreeSet). The following test program gives an indication of this
trade-off:
//: c09:SetPerformance.java
import java.util.*;
import com.bruceeckel.util.*;
public class SetPerformance {
private abstract static class Tester {
String name;
Tester(String name) { this.name = name; }
abstract void test(Set s, int size, int reps);
}
private static Tester[] tests = {
new Tester("add") {
void test(Set s, int size, int reps) {
for(int i = 0; i < reps; i++) {
s.clear();
Collections2.fill(s,
Collections2.countries.reset(),size);
}
}
},
new Tester("contains") {
void test(Set s, int size, int reps) {
for(int i = 0; i < reps; i++)
for(int j = 0; j < size; j++)
s.contains(Integer.toString(j));
}
},
new Tester("iteration") {
void test(Set s, int size, int reps) {
506
Thinking in Java
img
for(int i = 0; i < reps * 10; i++) {
Iterator it = s.iterator();
while(it.hasNext())
it.next();
}
}
},
};
public static void
test(Set s, int size, int reps) {
System.out.println("Testing " +
s.getClass().getName() + " size " + size);
Collections2.fill(s,
Collections2.countries.reset(), size);
for(int i = 0; i < tests.length; i++) {
System.out.print(tests[i].name);
long t1 = System.currentTimeMillis();
tests[i].test(s, size, reps);
long t2 = System.currentTimeMillis();
System.out.println(": " +
((double)(t2 - t1)/(double)size));
}
}
public static void main(String[] args) {
int reps = 50000;
// Or, choose the number of repetitions
// via the command line:
if(args.length > 0)
reps = Integer.parseInt(args[0]);
// Small:
test(new TreeSet(), 10, reps);
test(new HashSet(), 10, reps);
// Medium:
test(new TreeSet(), 100, reps);
test(new HashSet(), 100, reps);
// Large:
test(new TreeSet(), 1000, reps);
test(new HashSet(), 1000, reps);
}
} ///:~
Chapter 9: Holding Your Objects
507
img
The following table shows the results of one run. (Of course, this will be
different according to the computer and JVM you are using; you should
run the test yourself as well):
Type
Test size
Add
Contains
Iteration
10
138.0
115.0
187.0
TreeSet
100
189.5
151.1
206.5
1000
150.6
177.4
40.04
10
55.0
82.0
192.0
HashSet
100
45.6
90.0
202.2
1000
36.14
106.5
39.39
The performance of HashSet is generally superior to TreeSet for all
operations (but in particular addition and lookup, the two most important
operations). The only reason TreeSet exists is because it maintains its
elements in sorted order, so you only use it when you need a sorted Set.
Choosing between Maps
When choosing between implementations of Map, the size of the Map is
what most strongly affects performance, and the following test program
gives an indication of this trade-off:
//: c09:MapPerformance.java
// Demonstrates performance differences in Maps.
import java.util.*;
import com.bruceeckel.util.*;
public class MapPerformance {
private abstract static class Tester {
String name;
Tester(String name) { this.name = name; }
abstract void test(Map m, int size, int reps);
}
private static Tester[] tests = {
new Tester("put") {
void test(Map m, int size, int reps) {
for(int i = 0; i < reps; i++) {
m.clear();
508
Thinking in Java
img
Collections2.fill(m,
Collections2.geography.reset(), size);
}
}
},
new Tester("get") {
void test(Map m, int size, int reps) {
for(int i = 0; i < reps; i++)
for(int j = 0; j < size; j++)
m.get(Integer.toString(j));
}
},
new Tester("iteration") {
void test(Map m, int size, int reps) {
for(int i = 0; i < reps * 10; i++) {
Iterator it = m.entrySet().iterator();
while(it.hasNext())
it.next();
}
}
},
};
public static void
test(Map m, int size, int reps) {
System.out.println("Testing " +
m.getClass().getName() + " size " + size);
Collections2.fill(m,
Collections2.geography.reset(), size);
for(int i = 0; i < tests.length; i++) {
System.out.print(tests[i].name);
long t1 = System.currentTimeMillis();
tests[i].test(m, size, reps);
long t2 = System.currentTimeMillis();
System.out.println(": " +
((double)(t2 - t1)/(double)size));
}
}
public static void main(String[] args) {
int reps = 50000;
// Or, choose the number of repetitions
// via the command line:
Chapter 9: Holding Your Objects
509
img
if(args.length > 0)
reps = Integer.parseInt(args[0]);
// Small:
test(new TreeMap(), 10, reps);
test(new HashMap(), 10, reps);
test(new Hashtable(), 10, reps);
// Medium:
test(new TreeMap(), 100, reps);
test(new HashMap(), 100, reps);
test(new Hashtable(), 100, reps);
// Large:
test(new TreeMap(), 1000, reps);
test(new HashMap(), 1000, reps);
test(new Hashtable(), 1000, reps);
}
} ///:~
Because the size of the map is the issue, you'll see that the timing tests
divide the time by the size to normalize each measurement. Here is one
set of results. (Yours will probably be different.)
Type
Test
Put
Get
Iteration
size
10
143.0
110.0
186.0
TreeMap
100
201.1
188.4
280.1
1000
222.8
205.2
40.7
10
66.0
83.0
197.0
HashMap
100
80.7
135.7
278.5
1000
48.2
105.7
41.4
10
61.0
93.0
302.0
Hashtable
100
90.6
143.3
329.0
1000
54.1
110.95
47.3
As you might expect, Hashtable performance is roughly equivalent to
HashMap. (You can also see that HashMap is generally a bit faster.
HashMap is intended to replace Hashtable.) The TreeMap is
generally slower than the HashMap, so why would you use it? So you
could use it not as a Map, but as a way to create an ordered list. The
510
Thinking in Java
img
behavior of a tree is such that it's always in order and doesn't have to be
specially sorted. Once you fill a TreeMap, you can call keySet( ) to get a
Set view of the keys, then toArray( ) to produce an array of those keys.
You can then use the static method Arrays.binarySearch( )
(discussed later) to rapidly find objects in your sorted array. Of course,
you would probably only do this if, for some reason, the behavior of a
HashMap was unacceptable, since HashMap is designed to rapidly find
things. Also, you can easily create a HashMap from a TreeMap with a
single object creation In the end, when you're using a Map your first
choice should be HashMap, and only if you need a constantly sorted
Map will you need TreeMap.
Sorting and searching
Lists
Utilities to perform sorting and searching for Lists have the same names
and signatures as those for sorting arrays of objects, but are static
methods of Collections instead of Arrays. Here's an example, modified
from ArraySearching.java:
//: c09:ListSortSearch.java
// Sorting and searching Lists with 'Collections.'
import com.bruceeckel.util.*;
import java.util.*;
public class ListSortSearch {
public static void main(String[] args) {
List list = new ArrayList();
Collections2.fill(list,
Collections2.capitals, 25);
System.out.println(list + "\n");
Collections.shuffle(list);
System.out.println("After shuffling: "+list);
Collections.sort(list);
System.out.println(list + "\n");
Object key = list.get(12);
int index =
Collections.binarySearch(list, key);
Chapter 9: Holding Your Objects
511
img
System.out.println("Location of " + key +
" is " + index + ", list.get(" +
index + ") = " + list.get(index));
AlphabeticComparator comp =
new AlphabeticComparator();
Collections.sort(list, comp);
System.out.println(list + "\n");
key = list.get(12);
index =
Collections.binarySearch(list, key, comp);
System.out.println("Location of " + key +
" is " + index + ", list.get(" +
index + ") = " + list.get(index));
}
} ///:~
The use of these methods is identical to the ones in Arrays, but you're
using a List instead of an array. Just like searching and sorting with
arrays, if you sort using a Comparator you must binarySearch( )
using the same Comparator.
This program also demonstrates the shuffle( ) method in Collections,
which randomizes the order of a List.
Utilities
There are a number of other useful utilities in the Collections class:
enumeration(Collection)
Produces an old-style
Enumeration for the argument.
max(Collection)
Produces the maximum or
minimum element in the
min(Collection)
argument using the natural
comparison method of the
objects in the Collection.
max(Collection, Comparator)
Produces the maximum or
minimum element in the
min(Collection, Comparator)
Collection using the
Comparator.
reverse( )
Reverses all the elements in
512
Thinking in Java
img
place.
copy(List dest, List src)
Copies elements from src to dest.
fill(List list, Object o)
Replaces all the elements of list
with o.
nCopies(int n, Object o)
Returns an immutable List of
size n whose references all point
to o.
Note that min( ) and max( ) work with Collection objects, not with
Lists, so you don't need to worry about whether the Collection should
be sorted or not. (As mentioned earlier, you do need to sort( ) a List or
an array before performing a binarySearch( ).)
Making a Collection or Map
unmodifiable
Often it is convenient to create a read-only version of a Collection or
Map. The Collections class allows you to do this by passing the original
container into a method that hands back a read-only version. There are
four variations on this method, one each for Collection (if you don't
want to treat a Collection as a more specific type), List, Set, and Map.
This example shows the proper way to build read-only versions of each:
//: c09:ReadOnly.java
// Using the Collections.unmodifiable methods.
import java.util.*;
import com.bruceeckel.util.*;
public class ReadOnly {
static Collections2.StringGenerator gen =
Collections2.countries;
public static void main(String[] args) {
Collection c = new ArrayList();
Collections2.fill(c, gen, 25); // Insert data
c = Collections.unmodifiableCollection(c);
System.out.println(c); // Reading is OK
c.add("one"); // Can't change it
List a = new ArrayList();
Chapter 9: Holding Your Objects
513
img
Collections2.fill(a, gen.reset(), 25);
a = Collections.unmodifiableList(a);
ListIterator lit = a.listIterator();
System.out.println(lit.next()); // Reading OK
lit.add("one"); // Can't change it
Set s = new HashSet();
Collections2.fill(s, gen.reset(), 25);
s = Collections.unmodifiableSet(s);
System.out.println(s); // Reading OK
//! s.add("one"); // Can't change it
Map m = new HashMap();
Collections2.fill(m,
Collections2.geography, 25);
m = Collections.unmodifiableMap(m);
System.out.println(m); // Reading OK
//! m.put("Ralph", "Howdy!");
}
} ///:~
In each case, you must fill the container with meaningful data before you
make it read-only. Once it is loaded, the best approach is to replace the
existing reference with the reference that is produced by the
"unmodifiable" call. That way, you don't run the risk of accidentally
changing the contents once you've made it unmodifiable. On the other
hand, this tool also allows you to keep a modifiable container as private
within a class and to return a read-only reference to that container from a
method call. So you can change it from within the class, but everyone else
can only read it.
Calling the "unmodifiable" method for a particular type does not cause
compile-time checking, but once the transformation has occurred, any
calls to methods that modify the contents of a particular container will
produce an UnsupportedOperationException.
Synchronizing a Collection or Map
The synchronized keyword is an important part of the subject of
multithreading, a more complicated topic that will not be introduced
until Chapter 14. Here, I shall note only that the Collections class
514
Thinking in Java
img
contains a way to automatically synchronize an entire container. The
syntax is similar to the "unmodifiable" methods:
//: c09:Synchronization.java
// Using the Collections.synchronized methods.
import java.util.*;
public class Synchronization {
public static void main(String[] args) {
Collection c =
Collections.synchronizedCollection(
new ArrayList());
List list = Collections.synchronizedList(
new ArrayList());
Set s = Collections.synchronizedSet(
new HashSet());
Map m = Collections.synchronizedMap(
new HashMap());
}
} ///:~
In this case, you immediately pass the new container through the
appropriate "synchronized" method; that way there's no chance of
accidentally exposing the unsynchronized version.
Fail fast
The Java containers also have a mechanism to prevent more than one
process from modifying the contents of a container. The problem occurs if
you're iterating through a container and some other process steps in and
inserts, removes, or changes an object in that container. Maybe you've
already passed that object, maybe it's ahead of you, maybe the size of the
container shrinks after you call size( )--there are many scenarios for
disaster. The Java containers library incorporates a fail-fast mechanism
that looks for any changes to the container other than the ones your
process is personally responsible for. If it detects that someone else is
modifying the container, it immediately produces a
ConcurrentModificationException. This is the "fail-fast" aspect--it
doesn't try to detect a problem later on using a more complex algorithm.
Chapter 9: Holding Your Objects
515
img
It's quite easy to see the fail-fast mechanism in operation--all you have to
do is create an iterator and then add something to the collection that the
iterator is pointing to, like this:
//: c09:FailFast.java
// Demonstrates the "fail fast" behavior.
import java.util.*;
public class FailFast {
public static void main(String[] args) {
Collection c = new ArrayList();
Iterator it = c.iterator();
c.add("An object");
// Causes an exception:
String s = (String)it.next();
}
} ///:~
The exception happens because something is placed in the container after
the iterator is acquired from the container. The possibility that two parts
of the program could be modifying the same container produces an
uncertain state, so the exception notifies you that you should change your
code--in this case, acquire the iterator after you have added all the
elements to the container.
Note that you cannot benefit from this kind of monitoring when you're
accessing the elements of a List using get( ).
Unsupported operations
It's possible to turn an array into a List with the Arrays.asList( )
method:
//: c09:Unsupported.java
// Sometimes methods defined in the
// Collection interfaces don't work!
import java.util.*;
public class Unsupported {
private static String[] s = {
"one", "two", "three", "four", "five",
516
Thinking in Java
img
"six", "seven", "eight", "nine", "ten",
};
static List a = Arrays.asList(s);
static List a2 = a.subList(3, 6);
public static void main(String[] args) {
System.out.println(a);
System.out.println(a2);
System.out.println(
"a.contains(" + s[0] + ") = " +
a.contains(s[0]));
System.out.println(
"a.containsAll(a2) = " +
a.containsAll(a2));
System.out.println("a.isEmpty() = " +
a.isEmpty());
System.out.println(
"a.indexOf(" + s[5] + ") = " +
a.indexOf(s[5]));
// Traverse backwards:
ListIterator lit = a.listIterator(a.size());
while(lit.hasPrevious())
System.out.print(lit.previous() + " ");
System.out.println();
// Set the elements to different values:
for(int i = 0; i < a.size(); i++)
a.set(i, "47");
System.out.println(a);
// Compiles, but won't run:
lit.add("X"); // Unsupported operation
a.clear(); // Unsupported
a.add("eleven"); // Unsupported
a.addAll(a2); // Unsupported
a.retainAll(a2); // Unsupported
a.remove(s[0]); // Unsupported
a.removeAll(a2); // Unsupported
}
} ///:~
You'll discover that only a portion of the Collection and List interfaces
are actually implemented. The rest of the methods cause the unwelcome
appearance of something called an
Chapter 9: Holding Your Objects
517
img
UnsupportedOperationException. You'll learn all about exceptions
in the next chapter, but the short story is that the Collection
interface--as well as some of the other interfaces in the Java
containers library--contain "optional" methods, which might or might not
be "supported" in the concrete class that implements that interface.
Calling an unsupported method causes an
UnsupportedOperationException to indicate a programming error.
"What?!?" you say, incredulous. "The whole point of interfaces and base
classes is that they promise these methods will do something meaningful!
This breaks that promise--it says that not only will calling some methods
not perform a meaningful behavior, they will stop the program! Type
safety was just thrown out the window!"
It's not quite that bad. With a Collection, List, Set, or Map, the
compiler still restricts you to calling only the methods in that interface,
so it's not like Smalltalk (in which you can call any method for any object,
and find out only when you run the program whether your call does
anything). In addition, most methods that take a Collection as an
argument only read from that Collection--all the "read" methods of
Collection are not optional.
This approach prevents an explosion of interfaces in the design. Other
designs for container libraries always seem to end up with a confusing
plethora of interfaces to describe each of the variations on the main theme
and are thus difficult to learn. It's not even possible to capture all of the
special cases in interfaces, because someone can always invent a new
interface. The "unsupported operation" approach achieves an important
goal of the Java containers library: the containers are simple to learn and
use; unsupported operations are a special case that can be learned later.
For this approach to work, however:
1.
The UnsupportedOperationException must be a rare event.
That is, for most classes all operations should work, and only in
special cases should an operation be unsupported. This is true in
the Java containers library, since the classes you'll use 99 percent
of the time--ArrayList, LinkedList, HashSet, and HashMap,
as well as the other concrete implementations--support all of the
operations. The design does provide a "back door" if you want to
518
Thinking in Java
img
create a new Collection without providing meaningful definitions
for all the methods in the Collection interface, and yet still fit it
into the existing library.
2.
When an operation is unsupported, there should be reasonable
likelihood that an UnsupportedOperationException will
appear at implementation time, rather than after you've shipped
the product to the customer. After all, it indicates a programming
error: you've used an implementation incorrectly. This point is less
certain, and is where the experimental nature of this design comes
into play. Only over time will we find out how well it works.
In the example above, Arrays.asList( ) produces a List that is backed
by a fixed-size array. Therefore it makes sense that the only supported
operations are the ones that don't change the size of the array. If, on the
other hand, a new interface were required to express this different kind
of behavior (called, perhaps, "FixedSizeList"), it would throw open the
door to complexity and soon you wouldn't know where to start when
trying to use the library.
The documentation for a method that takes a Collection, List, Set, or
Map as an argument should specify which of the optional methods must
be implemented. For example, sorting requires the set( ) and
Iterator.set( ) methods, but not add( ) and remove( ).
Java 1.0/1.1 containers
Unfortunately, a lot of code was written using the Java 1.0/1.1 containers,
and even new code is sometimes written using these classes. So although
you should never use the old containers when writing new code, you'll still
need to be aware of them. However, the old containers were quite limited,
so there's not that much to say about them. (Since they are in the past, I
will try to refrain from overemphasizing some of the hideous design
decisions.)
Vector & Enumeration
The only self-expanding sequence in Java 1.0/1.1 was the Vector, and so
it saw a lot of use. Its flaws are too numerous to describe here (see the
Chapter 9: Holding Your Objects
519
img
first edition of this book, available on this book's CD ROM and as a free
download from ). Basically, you can think of it as an
ArrayList with long, awkward method names. In the Java 2 container
library, Vector was adapted so that it could fit as a Collection and a
List, so in the following example the Collections2.fill( ) method is
successfully used. This turns out to be a bit perverse, as it may confuse
some people into thinking that Vector has gotten better, when it is
actually included only to support pre-Java 2 code.
The Java 1.0/1.1 version of the iterator chose to invent a new name,
"enumeration," instead of using a term that everyone was already familiar
with. The Enumeration interface is smaller than Iterator, with only
two methods, and it uses longer method names: boolean
hasMoreElements( ) produces true if this enumeration contains more
elements, and Object nextElement( ) returns the next element of this
enumeration if there are any more (otherwise it throws an exception).
Enumeration is only an interface, not an implementation, and even new
libraries sometimes still use the old Enumeration--which is unfortunate
but generally harmless. Even though you should always use Iterator
when you can in your own code, you must be prepared for libraries that
want to hand you an Enumeration.
In addition, you can produce an Enumeration for any Collection by
using the Collections.enumeration( ) method, as seen in this
example:
//: c09:Enumerations.java
// Java 1.0/1.1 Vector and Enumeration.
import java.util.*;
import com.bruceeckel.util.*;
class Enumerations {
public static void main(String[] args) {
Vector v = new Vector();
Collections2.fill(
v, Collections2.countries, 100);
Enumeration e = v.elements();
while(e.hasMoreElements())
System.out.println(e.nextElement());
520
Thinking in Java
img
// Produce an Enumeration from a Collection:
e = Collections.enumeration(new ArrayList());
}
} ///:~
The Java 1.0/1.1 Vector has only an addElement( ) method, but fill( )
uses the add( ) method that was pasted on as Vector was turned into a
List. To produce an Enumeration, you call elements( ), then you can
use it to perform a forward iteration.
The last line creates an ArrayList and uses enumeration( ) to adapt an
Enumeration from the ArrayList Iterator. Thus, if you have old code
that wants an Enumeration, you can still use the new containers.
Hashtable
As you've seen in the performance comparison in this chapter, the basic
Hashtable is very similar to the HashMap, even down to the method
names. There's no reason to use Hashtable instead of HashMap in new
code.
Stack
The concept of the stack was introduced earlier, with the LinkedList.
What's rather odd about the Java 1.0/1.1 Stack is that instead of using a
Vector as a building block, Stack is inherited from Vector. So it has all
of the characteristics and behaviors of a Vector plus some extra Stack
behaviors. It's difficult to know whether the designers explicitly decided
that this was an especially useful way of doing things, or whether it was
just a naive design.
Here's a simple demonstration of Stack that pushes each line from a
String array:
//: c09:Stacks.java
// Demonstration of Stack Class.
import java.util.*;
public class Stacks {
static String[] months = {
"January", "February", "March", "April",
Chapter 9: Holding Your Objects
521
img
"May", "June", "July", "August", "September",
"October", "November", "December" };
public static void main(String[] args) {
Stack stk = new Stack();
for(int i = 0; i < months.length; i++)
stk.push(months[i] + " ");
System.out.println("stk = " + stk);
// Treating a stack as a Vector:
stk.addElement("The last line");
System.out.println(
"element 5 = " + stk.elementAt(5));
System.out.println("popping elements:");
while(!stk.empty())
System.out.println(stk.pop());
}
} ///:~
Each line in the months array is inserted into the Stack with push( ),
and later fetched from the top of the stack with a pop( ). To make a point,
Vector operations are also performed on the Stack object. This is
possible because, by virtue of inheritance, a Stack is a Vector. Thus, all
operations that can be performed on a Vector can also be performed on a
Stack, such as elementAt( ).
As mentioned earlier, you should use a LinkedList when you want stack
behavior.
BitSet
A BitSet is used if you want to efficiently store a lot of on-off information.
It's efficient only from the standpoint of size; if you're looking for efficient
access, it is slightly slower than using an array of some native type.
In addition, the minimum size of the BitSet is that of a long: 64 bits.
This implies that if you're storing anything smaller, like 8 bits, a BitSet
will be wasteful; you're better off creating your own class, or just an array,
to hold your flags if size is an issue.
A normal container expands as you add more elements, and the BitSet
does this as well. The following example shows how the BitSet works:
//: c09:Bits.java
522
Thinking in Java
img
// Demonstration of BitSet.
import java.util.*;
public class Bits {
static void printBitSet(BitSet b) {
System.out.println("bits: " + b);
String bbits = new String();
for(int j = 0; j < b.size() ; j++)
bbits += (b.get(j) ? "1" : "0");
System.out.println("bit pattern: " + bbits);
}
public static void main(String[] args) {
Random rand = new Random();
// Take the LSB of nextInt():
byte bt = (byte)rand.nextInt();
BitSet bb = new BitSet();
for(int i = 7; i >=0; i--)
if(((1 << i) &  bt) != 0)
bb.set(i);
else
bb.clear(i);
System.out.println("byte value: " + bt);
printBitSet(bb);
short st = (short)rand.nextInt();
BitSet bs = new BitSet();
for(int i = 15; i >=0; i--)
if(((1 << i) &  st) != 0)
bs.set(i);
else
bs.clear(i);
System.out.println("short value: " + st);
printBitSet(bs);
int it = rand.nextInt();
BitSet bi = new BitSet();
for(int i = 31; i >=0; i--)
if(((1 << i) &  it) != 0)
bi.set(i);
else
bi.clear(i);
Chapter 9: Holding Your Objects
523
img
System.out.println("int value: " + it);
printBitSet(bi);
// Test bitsets >= 64 bits:
BitSet b127 = new BitSet();
b127.set(127);
System.out.println("set bit 127: " + b127);
BitSet b255 = new BitSet(65);
b255.set(255);
System.out.println("set bit 255: " + b255);
BitSet b1023 = new BitSet(512);
b1023.set(1023);
b1023.set(1024);
System.out.println("set bit 1023: " + b1023);
}
} ///:~
The random number generator is used to create a random byte, short,
and int, and each one is transformed into a corresponding bit pattern in a
BitSet. This works fine because a BitSet is 64 bits, so none of these
cause it to increase in size. Then a BitSet of 512 bits is created. The
constructor allocates storage for twice that number of bits. However, you
can still set bit 1024 or greater.
Summary
To review the containers provided in the standard Java library:
1.
An array associates numerical indices to objects. It holds objects of
a known type so that you don't have to cast the result when you're
looking up an object. It can be multidimensional, and it can hold
primitives. However, its size cannot be changed once you create it.
2.
A Collection holds single elements, while a Map holds associated
pairs.
3.
Like an array, a List also associates numerical indices to objects--
you can think of arrays and Lists as ordered containers. The List
automatically resizes itself as you add more elements. But a List
can hold only Object references, so it won't hold primitives and
524
Thinking in Java
img
you must always cast the result when you pull an Object reference
out of a container.
4.
Use an ArrayList if you're doing a lot of random accesses, and a
LinkedList if you will be doing a lot of insertions and removals in
the middle of the list.
5.
The behavior of queues, deques, and stacks is provided via the
LinkedList.
6.
A Map is a way to associate not numbers, but objects with other
objects. The design of a HashMap is focused on rapid access,
while a TreeMap keeps its keys in sorted order, and thus is not as
fast as a HashMap.
7.
A Set only accepts one of each type of object. HashSets provide
maximally fast lookups, while TreeSets keep the elements in
sorted order.
8.
There's no need to use the legacy classes Vector, Hashtable and
Stack in new code.
The containers are tools that you can use on a day-to-day basis to make
your programs simpler, more powerful, and more effective.
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.
Create an array of double and fill( ) it using
RandDoubleGenerator. Print the results.
2.
Create a new class called Gerbil with an int gerbilNumber
that's initialized in the constructor (similar to the Mouse example
in this chapter). Give it a method called hop( ) that prints out
which gerbil number this is, and that it's hopping. Create an
ArrayList and add a bunch of Gerbil objects to the List. Now
use the get( ) method to move through the List and call hop( )
for each Gerbil.
Chapter 9: Holding Your Objects
525
img
3.
Modify Exercise 2 so you use an Iterator to move through the
List while calling hop( ).
4.
Take the Gerbil class in Exercise 2 and put it into a Map instead,
associating the name of the Gerbil as a String (the key) for each
Gerbil (the value) you put in the table. Get an Iterator for the
keySet( ) and use it to move through the Map, looking up the
Gerbil for each key and printing out the key and telling the
gerbil to hop( ).
5.
Create a List (try both ArrayList and LinkedList) and fill it
using Collections2.countries. Sort the list and print it, then
apply Collections.shuffle( ) to the list repeatedly, printing it
each time so that you can see how the shuffle( ) method
randomizes the list differently each time.
6.
Demonstrate that you can't add anything but a Mouse to a
MouseList.
7.
Modify MouseList.java so that it inherits from ArrayList
instead of using composition. Demonstrate the problem with this
approach.
8.
Repair CatsAndDogs.java by creating a Cats container
(utilizing ArrayList) that will only accept and retrieve Cat
objects.
9.
Create a container that encapsulates an array of String, and that
only adds Strings and gets Strings, so that there are no casting
issues during use. If the internal array isn't big enough for the next
add, your container should automatically resize it. In main( ),
compare the performance of your container with an ArrayList
holding Strings.
10.
Repeat Exercise 9 for a container of int, and compare the
performance to an ArrayList holding Integer objects. In your
performance comparison, include the process of incrementing
each object in the container.
11.
Using the utilities in com.bruceeckel.util, create an array of
each primitive type and of String, then fill each array using an
526
Thinking in Java
img
appropriate generator, and print each array using the appropriate
print( ) method.
12.
Create a generator that produces character names from your
favorite movies (you can use Snow White or Star Wars as a
fallback), and loops around to the beginning when it runs out of
names. Use the utilities in com.bruceeckel.util to fill an array,
an ArrayList, a LinkedList and both types of Set, then print
each container.
13.
Create a class containing two String objects, and make it
Comparable so that the comparison only cares about the first
String. Fill an array and an ArrayList with objects of your class,
using the geography generator. Demonstrate that sorting works
properly. Now make a Comparator that only cares about the
second String and demonstrate that sorting works properly; also
perform a binary search using your Comparator.
14.
Modify Exercise 13 so that an alphabetic sort is used.
15.
Use Arrays2.RandStringGenerator to fill a TreeSet but using
alphabetic ordering. Print the TreeSet to verify the sort order.
16.
Create both an ArrayList and a LinkedList, and fill each using
the Collections2.capitals generator. Print each list using an
ordinary Iterator, then insert one list into the other using a
ListIterator, inserting at every other location. Now perform the
insertion starting at the end of the first list and moving backward.
17.
Write a method that uses an Iterator to step through a
Collection and print the hashCode( ) of each object in the
container. Fill all the different types of Collections with objects
and apply your method to each container.
18.
Repair the problem in InfiniteRecursion.java.
19.
Create a class, then make an initialized array of objects of your
class. Fill a List from your array. Create a subset of your List
using subList( ), and then remove this subset from your List
using removeAll( ).
Chapter 9: Holding Your Objects
527
img
20.
Change Exercise 6 in Chapter 7 to use an ArrayList to hold the
Rodents and an Iterator to move through the sequence of
Rodents. Remember that an ArrayList holds only Objects so
you must use a cast when accessing individual Rodents.
21.
Following the Queue.java example, create a Deque class and
test it.
22.
Use a TreeMap in Statistics.java. Now add code that tests the
performance difference between HashMap and TreeMap in that
program.
23.
Produce a Map and a Set containing all the countries that begin
with `A.'
24.
Using Collections2.countries, fill a Set multiple times with the
same data and verify that the Set ends up with only one of each
instance. Try this with both kinds of Set.
25.
Starting with Statistics.java, create a program that runs the test
repeatedly and looks to see if any one number tends to appear
more than the others in the results.
26.
Rewrite Statistics.java using a HashSet of Counter objects
(you'll have to modify Counter so that it will work in the
HashSet). Which approach seems better?
27.
Modify the class in Exercise 13 so that it will work with HashSets
and as a key in HashMaps.
28.
Using SlowMap.java for inspiration, create a SlowSet.
29.
Apply the tests in Map1.java to SlowMap to verify that it works.
Fix anything in SlowMap that doesn't work correctly.
30.
Implement the rest of the Map interface for SlowMap.
31.
Modify MapPerformance.java to include tests of SlowMap.
32.
Modify SlowMap so that instead of two ArrayLists, it holds a
single ArrayList of MPair objects. Verify that the modified
version works correctly. Using MapPerformance.java, test the
528
Thinking in Java
img
speed of your new Map. Now change the put( ) method so that it
performs a sort( ) after each pair is entered, and modify get( ) to
use Collections.binarySearch( ) to look up the key. Compare
the performance of the new version with the old ones.
33.
Add a char field to CountedString that is also initialized in the
constructor, and modify the hashCode( ) and equals( ) methods
to include the value of this char.
34.
Modify SimpleHashMap so that it reports collisions, and test
this by adding the same data set twice so that you see collisions.
35.
Modify SimpleHashMap so that it reports the number of
"probes" necessary when collisions occur. That is, how many calls
to next( ) must be made on the Iterators that walk the
LinkedLists looking for matches?
36.
Implement the clear( ) and remove( ) methods for
SimpleHashMap.
37.
Implement the rest of the Map interface for SimpleHashMap.
38.
Add a private rehash( ) method to SimpleHashMap that is
invoked when the load factor exceeds 0.75. During rehashing,
double the number of buckets, then search for the first prime
number greater than that to determine the new number of
buckets.
39.
Following the example in SimpleHashMap.java, create and test
a SimpleHashSet.
40.
Modify SimpleHashMap to use ArrayLists instead of
LinkedLists. Modify MapPerformance.java to compare the
performance of the two implementations.
41.
Using the HTML documentation for the JDK (downloadable from
java.sun.com), look up the HashMap class. Create a HashMap,
fill it with elements, and determine the load factor. Test the lookup
speed with this map, then attempt to increase the speed by making
a new HashMap with a larger initial capacity and copying the old
Chapter 9: Holding Your Objects
529
img
map into the new one, running your lookup speed test again on the
new map.
42.
In Chapter 8, locate the GreenhouseControls.java example,
which consists of three files. In Controller.java, the class
EventSet is just a container. Change the code to use a
LinkedList instead of an EventSet. This will require more than
just replacing EventSet with LinkedList; you'll also need to use
an Iterator to cycle through the set of events.
43.
(Challenging). Write your own hashed map class, customized for a
particular key type: String for this example. Do not inherit it from
Map. Instead, duplicate the methods so that the put( ) and get( )
methods specifically take String objects, not Objects, as keys.
Everything that involves keys should not use generic types, but
instead work with Strings, to avoid the cost of upcasting and
downcasting. Your goal is to make the fastest possible custom
implementation. Modify MapPerformance.java to test your
implementation vs. a HashMap.
44.
(Challenging). Find the source code for List in the Java source
code library that comes with all Java distributions. Copy this code
and make a special version called intList that holds only ints.
Consider what it would take to make a special version of List for
all the primitive types. Now consider what happens if you want to
make a linked list class that works with all the primitive types. If
parameterized types are ever implemented in Java, they will
provide a way to do this work for you automatically (as well as
many other benefits).
530
Thinking in Java
img
10: Error Handling
with Exceptions
The basic philosophy of Java is that "badly formed code
will not be run."
The ideal time to catch an error is at compile-time, before you even try to
run the program. However, not all errors can be detected at compile-time.
The rest of the problems must be handled at run-time, through some
formality that allows the originator of the error to pass appropriate
information to a recipient who will know how to handle the difficulty
properly.
In C and other earlier languages, there could be several of these
formalities, and they were generally established by convention and not as
part of the programming language. Typically, you returned a special value
or set a flag, and the recipient was supposed to look at the value or the flag
and determine that something was amiss. However, as the years passed, it
was discovered that programmers who use a library tend to think of
themselves as invincible--as in, "Yes, errors might happen to others, but
not in my code." So, not too surprisingly, they wouldn't check for the error
conditions (and sometimes the error conditions were too silly to check
for1). If you were thorough enough to check for an error every time you
called a method, your code could turn into an unreadable nightmare.
Because programmers could still coax systems out of these languages they
were resistant to admitting the truth: This approach to handling errors
was a major limitation to creating large, robust, maintainable programs.
The solution is to take the casual nature out of error handling and to
enforce formality. This actually has a long history, since implementations
of exception handling go back to operating systems in the 1960s, and even
1 The C programmer can look up the return value of printf( ) for an example of this.
531
img
to BASIC's "on error goto." But C++ exception handling was based on
Ada, and Java's is based primarily on C++ (although it looks even more
like Object Pascal).
The word "exception" is meant in the sense of "I take exception to that."
At the point where the problem occurs you might not know what to do
with it, but you do know that you can't just continue on merrily; you must
stop and somebody, somewhere, must figure out what to do. But you don't
have enough information in the current context to fix the problem. So you
hand the problem out to a higher context where someone is qualified to
make the proper decision (much like a chain of command).
The other rather significant benefit of exceptions is that they clean up
error handling code. Instead of checking for a particular error and dealing
with it at multiple places in your program, you no longer need to check at
the point of the method call (since the exception will guarantee that
someone catches it). And, you need to handle the problem in only one
place, the so-called exception handler. This saves you code, and it
separates the code that describes what you want to do from the code that
is executed when things go awry. In general, reading, writing, and
debugging code becomes much clearer with exceptions than when using
the old way of error handling.
Because exception handling is enforced by the Java compiler, there are
only so many examples that can be written in this book without learning
about exception handling. This chapter introduces you to the code you
need to write to properly handle exceptions, and the way you can generate
your own exceptions if one of your methods gets into trouble.
Basic exceptions
An exceptional condition is a problem that prevents the continuation of
the method or scope that you're in. It's important to distinguish an
exceptional condition from a normal problem, in which you have enough
information in the current context to somehow cope with the difficulty.
With an exceptional condition, you cannot continue processing because
you don't have the information necessary to deal with the problem in the
current context. All you can do is jump out of the current context and
532
Thinking in Java
img
relegate that problem to a higher context. This is what happens when you
throw an exception.
A simple example is a divide. If you're about to divide by zero, it's worth
checking to make sure you don't go ahead and perform the divide. But
what does it mean that the denominator is zero? Maybe you know, in the
context of the problem you're trying to solve in that particular method,
how to deal with a zero denominator. But if it's an unexpected value, you
can't deal with it and so must throw an exception rather than continuing
along that path.
When you throw an exception, several things happen. First, the exception
object is created in the same way that any Java object is created: on the
heap, with new. Then the current path of execution (the one you couldn't
continue) is stopped and the reference for the exception object is ejected
from the current context. At this point the exception handling mechanism
takes over and begins to look for an appropriate place to continue
executing the program. This appropriate place is the exception handler,
whose job is to recover from the problem so the program can either try
another tack or just continue.
As a simple example of throwing an exception, consider an object
reference called t. It's possible that you might be passed a reference that
hasn't been initialized, so you might want to check before trying to call a
method using that object reference. You can send information about the
error into a larger context by creating an object representing your
information and "throwing" it out of your current context. This is called
throwing an exception. Here's what it looks like:
if(t == null)
throw new NullPointerException();
This throws the exception, which allows you--in the current context--to
abdicate responsibility for thinking about the issue further. It's just
magically handled somewhere else. Precisely where will be shown shortly.
Exception arguments
Like any object in Java, you always create exceptions on the heap using
new, which allocates storage and calls a constructor. There are two
Chapter 10: Error Handling with Exceptions
533
img
constructors in all standard exceptions: the first is the default constructor,
and the second takes a string argument so you can place pertinent
information in the exception:
if(t == null)
throw new NullPointerException("t = null");
This string can later be extracted using various methods, as will be shown
shortly.
The keyword throw causes a number of relatively magical things to
happen. Typically, you'll first use new to create an object that represents
the error condition. You give the resulting reference to throw. The object
is, in effect, "returned" from the method, even though that object type
isn't normally what the method is designed to return. A simplistic way to
think about exception handling is as an alternate return mechanism,
although you get into trouble if you take that analogy too far. You can also
exit from ordinary scopes by throwing an exception. But a value is
returned, and the method or scope exits.
Any similarity to an ordinary return from a method ends here, because
where you return is someplace completely different from where you
return for a normal method call. (You end up in an appropriate exception
handler that might be miles away--many levels lower on the call stack--
from where the exception was thrown.)
In addition, you can throw any type of Throwable object that you want.
Typically, you'll throw a different class of exception for each different type
of error. The information about the error is represented both inside the
exception object and implicitly in the type of exception object chosen, so
someone in the bigger context can figure out what to do with your
exception. (Often, the only information is the type of exception object, and
nothing meaningful is stored within the exception object.)
Catching an exception
If a method throws an exception, it must assume that exception is
"caught" and dealt with. One of the advantages of Java exception handling
is that it allows you to concentrate on the problem you're trying to solve in
one place, and then deal with the errors from that code in another place.
534
Thinking in Java
img
To see how an exception is caught, you must first understand the concept
of a guarded region, which is a section of code that might produce
exceptions, and which is followed by the code to handle those exceptions.
The try block
If you're inside a method and you throw an exception (or another method
you call within this method throws an exception), that method will exit in
the process of throwing. If you don't want a throw to exit the method,
you can set up a special block within that method to capture the
exception. This is called the try block because you "try" your various
method calls there. The try block is an ordinary scope, preceded by the
keyword try:
try {
// Code that might generate exceptions
}
If you were checking for errors carefully in a programming language that
didn't support exception handling, you'd have to surround every method
call with setup and error testing code, even if you call the same method
several times. With exception handling, you put everything in a try block
and capture all the exceptions in one place. This means your code is a lot
easier to write and easier to read because the goal of the code is not
confused with the error checking.
Exception handlers
Of course, the thrown exception must end up someplace. This "place" is
the exception handler, and there's one for every exception type you want
to catch. Exception handlers immediately follow the try block and are
denoted by the keyword catch:
try {
// Code that might generate exceptions
} catch(Type1 id1) {
// Handle exceptions of Type1
} catch(Type2 id2) {
// Handle exceptions of Type2
} catch(Type3 id3) {
// Handle exceptions of Type3
Chapter 10: Error Handling with Exceptions
535
img
}
// etc...
Each catch clause (exception handler) is like a little method that takes one
and only one argument of a particular type. The identifier (id1, id2, and
so on) can be used inside the handler, just like a method argument.
Sometimes you never use the identifier because the type of the exception
gives you enough information to deal with the exception, but the identifier
must still be there.
The handlers must appear directly after the try block. If an exception is
thrown, the exception handling mechanism goes hunting for the first
handler with an argument that matches the type of the exception. Then it
enters that catch clause, and the exception is considered handled. The
search for handlers stops once the catch clause is finished. Only the
matching catch clause executes; it's not like a switch statement in which
you need a break after each case to prevent the remaining ones from
executing.
Note that, within the try block, a number of different method calls might
generate the same exception, but you need only one handler.
Termination vs. resumption
There are two basic models in exception handling theory. In termination
(which is what Java and C++ support), you assume the error is so critical
that there's no way to get back to where the exception occurred. Whoever
threw the exception decided that there was no way to salvage the
situation, and they don't want to come back.
The alternative is called resumption. It means that the exception handler
is expected to do something to rectify the situation, and then the faulting
method is retried, presuming success the second time. If you want
resumption, it means you still hope to continue execution after the
exception is handled. In this case, your exception is more like a method
call--which is how you should set up situations in Java in which you want
resumption-like behavior. (That is, don't throw an exception; call a
method that fixes the problem.) Alternatively, place your try block inside
536
Thinking in Java
img
a while loop that keeps reentering the try block until the result is
satisfactory.
Historically, programmers using operating systems that supported
resumptive exception handling eventually ended up using termination-
like code and skipping resumption. So although resumption sounds
attractive at first, it isn't quite so useful in practice. The dominant reason
is probably the coupling that results: your handler must often be aware of
where the exception is thrown from and contain nongeneric code specific
to the throwing location. This makes the code difficult to write and
maintain, especially for large systems where the exception can be
generated from many points.
Creating your own
exceptions
You're not stuck using the existing Java exceptions. This is important
because you'll often need to create your own exceptions to denote a
special error that your library is capable of creating, but which was not
foreseen when the Java exception hierarchy was created.
To create your own exception class, you're forced to inherit from an
existing type of exception, preferably one that is close in meaning to your
new exception (this is often not possible, however). The most trivial way
to create a new type of exception is just to let the compiler create the
default constructor for you, so it requires almost no code at all:
//: c10:SimpleExceptionDemo.java
// Inheriting your own exceptions.
class SimpleException extends Exception {}
public class SimpleExceptionDemo {
public void f() throws SimpleException {
System.out.println(
"Throwing SimpleException from f()");
throw new SimpleException ();
}
public static void main(String[] args) {
Chapter 10: Error Handling with Exceptions
537
img
SimpleExceptionDemo sed =
new SimpleExceptionDemo();
try {
sed.f();
} catch(SimpleException e) {
System.err.println("Caught it!");
}
}
} ///:~
When the compiler creates the default constructor, it which automatically
(and invisibly) calls the base-class default constructor. Of course, in this
case you don't get a SimpleException(String) constructor, but in
practice that isn't used much. As you'll see, the most important thing
about an exception is the class name, so most of the time an exception like
the one shown above is satisfactory.
Here, the result is printed to the console standard error stream by writing
to System.err. This is usually a better place to send error information
than System.out, which may be redirected. If you send output to
System.err it will not be redirected along with System.out so the user
is more likely to notice it.
Creating an exception class that also has a constructor that takes a String
is also quite simple:
//: c10:FullConstructors.java
// Inheriting your own exceptions.
class MyException extends Exception {
public MyException() {}
public MyException(String msg) {
super(msg);
}
}
public class FullConstructors {
public static void f() throws MyException {
System.out.println(
"Throwing MyException from f()");
throw new MyException();
538
Thinking in Java
img
}
public static void g() throws MyException {
System.out.println(
"Throwing MyException from g()");
throw new MyException("Originated in g()");
}
public static void main(String[] args) {
try {
f();
} catch(MyException e) {
e.printStackTrace(System.err);
}
try {
g();
} catch(MyException e) {
e.printStackTrace(System.err);
}
}
} ///:~
The added code is small--the addition of two constructors that define the
way MyException is created. In the second constructor, the base-class
constructor with a String argument is explicitly invoked by using the
super keyword.
The stack trace information is sent to System.err so that it's more likely
it will be noticed in the event that System.out has been redirected.
The output of the program is:
Throwing MyException from f()
MyException
at FullConstructors.f(FullConstructors.java:16)
at FullConstructors.main(FullConstructors.java:24)
Throwing MyException from g()
MyException: Originated in g()
at FullConstructors.g(FullConstructors.java:20)
at FullConstructors.main(FullConstructors.java:29)
You can see the absence of the detail message in the MyException
thrown from f( ).
Chapter 10: Error Handling with Exceptions
539
img
The process of creating your own exceptions can be taken further. You can
add extra constructors and members:
//: c10:ExtraFeatures.java
// Further embellishment of exception classes.
class MyException2 extends Exception {
public MyException2() {}
public MyException2(String msg) {
super(msg);
}
public MyException2(String msg, int x) {
super(msg);
i = x;
}
public int val() { return i; }
private int i;
}
public class ExtraFeatures {
public static void f() throws MyException2 {
System.out.println(
"Throwing MyException2 from f()");
throw new MyException2();
}
public static void g() throws MyException2 {
System.out.println(
"Throwing MyException2 from g()");
throw new MyException2("Originated in g()");
}
public static void h() throws MyException2 {
System.out.println(
"Throwing MyException2 from h()");
throw new MyException2(
"Originated in h()", 47);
}
public static void main(String[] args) {
try {
f();
} catch(MyException2 e) {
e.printStackTrace(System.err);
540
Thinking in Java
img
}
try {
g();
} catch(MyException2 e) {
e.printStackTrace(System.err);
}
try {
h();
} catch(MyException2 e) {
e.printStackTrace(System.err);
System.err.println("e.val() = " + e.val());
}
}
} ///:~
A data member i has been added, along with a method that reads that
value and an additional constructor that sets it. The output is:
Throwing MyException2 from f()
MyException2
at ExtraFeatures.f(ExtraFeatures.java:22)
at ExtraFeatures.main(ExtraFeatures.java:34)
Throwing MyException2 from g()
MyException2: Originated in g()
at ExtraFeatures.g(ExtraFeatures.java:26)
at ExtraFeatures.main(ExtraFeatures.java:39)
Throwing MyException2 from h()
MyException2: Originated in h()
at ExtraFeatures.h(ExtraFeatures.java:30)
at ExtraFeatures.main(ExtraFeatures.java:44)
e.val() = 47
Since an exception is just another kind of object, you can continue this
process of embellishing the power of your exception classes. Keep in
mind, however, that all this dressing-up might be lost on the client
programmers using your packages, since they might simply look for the
exception to be thrown and nothing more. (That's the way most of the
Java library exceptions are used.)
Chapter 10: Error Handling with Exceptions
541
img
The exception specification
In Java, you're required to inform the client programmer, who calls your
method, of the exceptions that might be thrown from your method. This is
civilized, because the caller can know exactly what code to write to catch
all potential exceptions. Of course, if source code is available, the client
programmer could hunt through and look for throw statements, but
often a library doesn't come with sources. To prevent this from being a
problem, Java provides syntax (and forces you to use that syntax) to allow
you to politely tell the client programmer what exceptions this method
throws, so the client programmer can handle them. This is the exception
specification, and it's part of the method declaration, appearing after the
argument list.
The exception specification uses an additional keyword, throws, followed
by a list of all the potential exception types. So your method definition
might look like this:
void f() throws TooBig, TooSmall, DivZero { //...
If you say
void f() { // ...
it means that no exceptions are thrown from the method. (Except for the
exceptions of type RuntimeException, which can reasonably be thrown
anywhere--this will be described later.)
You can't lie about an exception specification--if your method causes
exceptions and doesn't handle them, the compiler will detect this and tell
you that you must either handle the exception or indicate with an
exception specification that it may be thrown from your method. By
enforcing exception specifications from top to bottom, Java guarantees
that exception correctness can be ensured at compile-time2.
2 This is a significant improvement over C++ exception handling, which doesn't catch
violations of exception specifications until run time, when it's not very useful.
542
Thinking in Java
img
There is one place you can lie: you can claim to throw an exception that
you really don't. The compiler takes your word for it, and forces the users
of your method to treat it as if it really does throw that exception. This has
the beneficial effect of being a placeholder for that exception, so you can
actually start throwing the exception later without requiring changes to
existing code. It's also important for creating abstract base classes and
interfaces whose derived classes or implementations may need to throw
exceptions.
Catching any exception
It is possible to create a handler that catches any type of exception. You do
this by catching the base-class exception type Exception (there are other
types of base exceptions, but Exception is the base that's pertinent to
virtually all programming activities):
catch(Exception e) {
System.err.println("Caught an exception");
}
This will catch any exception, so if you use it you'll want to put it at the
end of your list of handlers to avoid preempting any exception handlers
that might otherwise follow it.
Since the Exception class is the base of all the exception classes that are
important to the programmer, you don't get much specific information
about the exception, but you can call the methods that come from its base
type Throwable:
String getMessage( )
String getLocalizedMessage( )
Gets the detail message, or a message adjusted for this particular locale.
String toString( )
Returns a short description of the Throwable, including the detail
message if there is one.
void printStackTrace( )
void printStackTrace(PrintStream)
void printStackTrace(PrintWriter)
Prints the Throwable and the Throwable's call stack trace. The call stack
Chapter 10: Error Handling with Exceptions
543
img
shows the sequence of method calls that brought you to the point at which
the exception was thrown. The first version prints to standard error, the
second and third prints to a stream of your choice (in Chapter 11, you'll
understand why there are two types of streams).
Throwable fillInStackTrace( )
Records information within this Throwable object about the current
state of the stack frames. Useful when an application is rethrowing an
error or exception (more about this shortly).
In addition, you get some other methods from Throwable's base type
Object (everybody's base type). The one that might come in handy for
exceptions is getClass( ), which returns an object representing the class
of this object. You can in turn query this Class object for its name with
getName( ) or toString( ). You can also do more sophisticated things
with Class objects that aren't necessary in exception handling. Class
objects will be studied later in this book.
Here's an example that shows the use of the basic Exception methods:
//: c10:ExceptionMethods.java
// Demonstrating the Exception Methods.
public class ExceptionMethods {
public static void main(String[] args) {
try {
throw new Exception("Here's my Exception");
} catch(Exception e) {
System.err.println("Caught Exception");
System.err.println(
"e.getMessage(): " + e.getMessage());
System.err.println(
"e.getLocalizedMessage(): " +
e.getLocalizedMessage());
System.err.println("e.toString(): " + e);
System.err.println("e.printStackTrace():");
e.printStackTrace(System.err);
}
}
} ///:~
544
Thinking in Java
img
The output for this program is:
Caught Exception
e.getMessage(): Here's my Exception
e.getLocalizedMessage(): Here's my Exception
e.toString(): java.lang.Exception:
Here's my Exception
e.printStackTrace():
java.lang.Exception: Here's my Exception
at ExceptionMethods.main(ExceptionMethods.java:7)
java.lang.Exception:
Here's my Exception
at ExceptionMethods.main(ExceptionMethods.java:7)
You can see that the methods provide successively more information--
each is effectively a superset of the previous one.
Rethrowing an exception
Sometimes you'll want to rethrow the exception that you just caught,
particularly when you use Exception to catch any exception. Since you
already have the reference to the current exception, you can simply
rethrow that reference:
catch(Exception e) {
System.err.println("An exception was thrown");
throw e;
}
Rethrowing an exception causes the exception to go to the exception
handlers in the next-higher context. Any further catch clauses for the
same try block are still ignored. In addition, everything about the
exception object is preserved, so the handler at the higher context that
catches the specific exception type can extract all the information from
that object.
If you simply rethrow the current exception, the information that you
print about that exception in printStackTrace( ) will pertain to the
exception's origin, not the place where you rethrow it. If you want to
install new stack trace information, you can do so by calling
fillInStackTrace( ), which returns an exception object that it creates by
Chapter 10: Error Handling with Exceptions
545
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