![]() | CONTENTS | ![]() |
In this chapter we'll continue our look at the core Java APIs, covering more of the tools of the java.util package. The java.util package includes a wide range of utilities for mathematical operations, dates and times, structured collections of objects, stored user data, and logging I/O.
Java supports integer and floating-point arithmetic directly in the language. Higher-level math operations are supported through the java.lang.Math class. As we'll discuss later, there are also wrapper classes for all primitive data types, so you can treat them as objects if necessary. These wrapper classes hold some methods for basic conversions. Java provides the java.util.Random class for generating random numbers.
First, a few words about built-in arithmetic in Java. Java handles errors in integer arithmetic by throwing an ArithmeticException:
int zero = 0; try { int i = 72 / zero; } catch ( ArithmeticException e ) { // division by zero }
To generate the error in this example, we created the intermediate variable zero. The compiler is somewhat crafty and would have caught us if we had blatantly tried to perform a division by a literal zero.
Floating-point arithmetic expressions, on the other hand, don't throw exceptions. Instead, they take on the special out-of-range values shown in Table 10-1.
Value | Mathematical representation |
---|---|
POSITIVE_INFINITY | 1.0/0.0 |
NEGATIVE_INFINITY | -1.0/0.0 |
NaN | 0.0/0.0 |
The following example generates an infinite result:
double zero = 0.0; double d = 1.0/zero; if ( d == Double.POSITIVE_INFINITY ) System.out.println( "Division by zero" );
The special value NaN indicates the result is "not a number." The value NaN has the special mathematical distinction of not being equal to itself (NaN != NaN evaluates to true). Use Float.isNaN() or Double.isNaN() to test for NaN.
The java.lang.Math class provides Java's math library. All its methods are static and used directly; you don't have to (and you can't) instantiate a Math object. This kind of degenerate class is used when we really want methods to approximate global functions. It's not very object-oriented, but it provides a means of grouping some related utility functions in a single class and accessing them easily. Table 10-2 summarizes the methods in java.lang.Math.
Method | Argument type(s) | Functionality |
---|---|---|
Math.abs(a) | int, long, float, double | Absolute value |
Math.acos(a) | double | Arc cosine |
Math.asin(a) | double | Arc sine |
Math.atan(a) | double | Arc tangent |
Math.atan2(a,b) | double | Angle part of rectangular-to-polar coordinate transform |
Math.ceil(a) | double | Smallest whole number greater than or equal to a |
Math.cos(a) | double | Cosine |
Math.exp(a) | double | Math.E to the power a |
Math.floor(a) | double | Largest whole number less than or equal to a |
Math.log(a) | double | Natural logarithm of a |
Math.max(a, b) | int, long, float, double | Maximum |
Math.min(a, b) | int, long, float, double | Minimum |
Math.pow(a, b) | double | a to the power b |
Math.random() | None | Random-number generator |
Math.rint(a) | double | Converts double value to integral value in double format |
Math.round(a) | float, double | Rounds to whole number |
Math.sin(a) | double | Sine |
Math.sqrt(a) | double | Square root |
Math.tan(a) | double | Tangent |
log(), pow(), and sqrt() can throw an ArithmeticException. abs(), max(), and min() are overloaded for all the scalar values, int, long, float, or double, and return the corresponding type. Versions of Math.round() accept either float or double and return int or long, respectively. The rest of the methods operate on and return double values:
double irrational = Math.sqrt( 2.0 ); int bigger = Math.max( 3, 4 ); long one = Math.round( 1.125798 );
For convenience, Math also contains the static final double values E and PI:
double circumference = diameter * Math.PI;
If a long or a double just isn't big enough for you, the java.math package provides two classes, BigInteger and BigDecimal, that support arbitrary-precision numbers. These are full-featured classes with a bevy of methods for performing arbitrary-precision math. In the following example, we use BigDecimal to add two numbers:
try { BigDecimal twentyone = new BigDecimal("21"); BigDecimal seven = new BigDecimal("7"); BigDecimal sum = twentyone.add(seven); int answer= sum.intValue( ); // 28 } catch (NumberFormatException nfe) { } catch (ArithmeticException ae) { }
If you implement cryptographic or scientific algorithms for fun, BigInteger is crucial. But other than that, you're not likely to need these classes.
In languages such as Smalltalk, numbers and other simple types are objects, which makes for an elegant language design but has trade-offs in efficiency and complexity. By contrast, there is a schism in the Java world between class types (i.e., objects) and primitive types (i.e., numbers, characters, and boolean values). Java accepts this trade-off simply for efficiency reasons. When you're crunching numbers, you want your computations to be lightweight; having to use objects for primitive types complicates performance optimizations. For the times you want to treat values as objects, Java supplies a wrapper class for each of the primitive types, as shown in Table 10-3.
Primitive | Wrapper |
---|---|
void | java.lang.Void |
boolean | java.lang.Boolean |
char | java.lang.Character |
byte | java.lang.Byte |
short | java.lang.Short |
int | java.lang.Integer |
long | java.lang.Long |
float | java.lang.Float |
double | java.lang.Double |
An instance of a wrapper class encapsulates a single value of its corresponding type. It's an immutable object that serves as a container to hold the value and let us retrieve it later. You can construct a wrapper object from a primitive value or from a String representation of the value. The following statements are equivalent:
Float pi = new Float( 3.14 ); Float pi = new Float( "3.14" );
Like the parsing methods we looked at in the previous chapter, the wrapper constructors throw a NumberFormatException when there is an error in parsing a string:
try { Double bogus = new Double( "huh?" ); } catch ( NumberFormatException e ) { // bad number }
Each of the numeric type wrappers implements the java.lang.Number interface, which provides "value" methods access to its value in all the primitive forms. You can retrieve scalar values with the methods doubleValue(), floatValue(), longValue(), intValue() , shortValue(), and byteValue():
Double size = new Double ( 32.76 ); double d = size.doubleValue( ); // 32.76 float f = size.floatValue( ); // 32.76 long l = size.longValue( ); // 32 int i = size.intValue( ); // 32
This code is equivalent to casting the primitive double value to the various types.
The most common need for a wrapper is when you want to use a primitive value in a situation that requires an object. For example, later in this chapter we'll look at the Java Collections API, which is a sophisticated set of classes for dealing with object groups such as lists, sets, and maps. All the Collections APIs work on object types, so you'll need to use wrappers to hold primitive numbers for them. As we'll see, a List is an extensible array of Objects. We can use wrappers to hold numbers in a List (along with other objects):
List myNumbers = new ArrayList( ); Integer thirtyThree = new Integer( 33 ); myNumbers.add( thirtyThree );
Here we have created an Integer wrapper object so that we can insert the number into the List, using addElement(). Later, when we are extracting elements from the List, we can recover the int value as follows:
Integer theNumber = (Integer)myNumbers.get(0); int n = theNumber.intValue( ); // 33
You can use the java.util.Random class to generate random values. It's a pseudo-random number generator that can be initialized with a 48-bit seed.[1] The default constructor uses the current time as a seed, but if you want a repeatable sequence, specify your own seed with:
long seed = mySeed; Random rnums = new Random( seed );
This code creates a random-number generator. Once you have a generator, you can ask for random values of various types using the methods listed in Table 10-4.
Method | Range |
---|---|
nextBoolean() | true or false |
nextInt() | -2147483648 to 2147483647 |
nextInt(int n) | 0 to (n - 1) inclusive |
nextLong() | -9223372036854775808 to 9223372036854775807 |
nextFloat() | -1.0 to 1.0 |
nextDouble() | -1.0 to 1.0 |
nextGaussian() | Gaussian distribution, SD 1.0 |
By default, the values are uniformly distributed. You can use the nextGaussian() method to create a Gaussian (bell curve) distribution of double values, with a mean of 0.0 and a standard deviation of 1.0.
The static method Math.random() retrieves a random double value. This method initializes a private random number generator in the Math class, using the default Random constructor. Thus every call to Math.random() corresponds to a call to nextDouble() on that random number generator.
Working with dates and times without the proper tools can be a chore.[2] In Java 1.1 and later, you get three classes that do all the hard work for you. The java.util.Date class encapsulates a point in time. The java.util.GregorianCalendar class, which descends from the abstract java.util.Calendar, translates between a point in time and calendar fields like month, day, and year. Finally, the java.text.DateFormat class knows how to generate and parse string representations of dates and times.[3]
The separation of the Date class and the GregorianCalendar class is analogous to having a class representing temperature and a class that translates that temperature to Celsius units. Conceivably, we could define other subclasses of Calendar, say JulianCalendar or LunarCalendar.
The default GregorianCalendar constructor creates an object that represents the current time, as determined by the system clock:
GregorianCalendar now = new GregorianCalendar( );
Other constructors accept values to specify the point in time. In the first statement in the following code, we construct an object representing August 9, 2001; the second statement specifies both a date and a time, which yields an object that represents 9:01 A.M., April 8, 2002.
GregorianCalendar daphne = new GregorianCalendar(2001, Calendar.AUGUST, 9); GregorianCalendar sometime = new GregorianCalendar(2002, Calendar.APRIL, 8, 9, 1); // 9:01 AM
We can also create a GregorianCalendar by setting specific fields using the set() method. The Calendar class contains a torrent of constants representing both calendar fields and field values. The first argument to the set() method is a field constant; the second argument is the new value for the field.
GregorianCalendar kristen = new GregorianCalendar( ); kristen.set(Calendar.YEAR, 1972); kristen.set(Calendar.MONTH, Calendar.MAY); kristen.set(Calendar.DATE, 20);
A GregorianCalendar is created in the default time zone. Setting the time zone of the calendar is as easy as obtaining the desired TimeZone and giving it to the GregorianCalendar:
GregorianCalendar smokey = new GregorianCalendar( ); smokey.setTimeZone(TimeZone.getTimeZone("MST"));
To represent a GregorianCalendar's date as a string, first create a Date object:
Date mydate = smokey.getTime( );
To create a string representing a point in time, create a DateFormat object and apply its format() method to a Date object. Like the NumberFormat object we looked at in the previous chapter, DateFormat itself is abstract, but it has several static ("factory") methods that return useful DateFormat subclass instances. To get a default DateFormat, simply call getInstance():
DateFormat plain = DateFormat.getInstance( ); String now = plain.format(new Date( )); // 4/12/00 6:06 AM
You can generate a date string or a time string, or both, using the getDateInstance(), getTimeInstance(), and getDateTimeInstance() factory methods. The argument to these methods describes what level of detail you'd like to see. DateFormat defines four constants representing detail levels: they are SHORT, MEDIUM, LONG, and FULL. There is also a DEFAULT, which is the same as MEDIUM. The following code creates three DateFormat instances: one to format a date, one to format a time, and one to format a date and time together. Note that getDateTimeInstance() requires two arguments: the first specifies how to format the date, the second how to format the time:
// 12-Apr-00 DateFormat df = DateFormat.getDateInstance(DateFormat.DEFAULT); // 9:18:27 AM DateFormat tf = DateFormat.getTimeInstance(DateFormat.DEFAULT); // Wednesday, April 12, 2000 9:18:27 o'clock AM EDT DateFormat dtf = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL);
We're showing only how to create the DateFormat objects here; to actually generate a String from a date, you'll need to call the format() method of these objects, passing a Date as an argument.
Formatting dates and times for other countries is just as easy. Overloaded factory methods accept a Locale argument:
// 12 avr. 00 DateFormat df = DateFormat.getDateInstance(DateFormat.DEFAULT, Locale.FRANCE); // 9:27:49 DateFormat tf = DateFormat.getTimeInstance(DateFormat.DEFAULT, Locale.GERMANY); // mercoledi 12 aprile 2000 9.27.49 GMT-04:00 DateFormat dtf = DateFormat.getDateTimeInstance( DateFormat.FULL, DateFormat.FULL, Locale.ITALY);
To parse a string representing a date, we use the parse() method of the DateFormat class. The result is a Date object. The parsing algorithms are finicky, so it's safest to parse dates and times that are in the same format produced by the DateFormat. The parse() method throws a ParseException if it doesn't understand the string you give it. All of the following calls to parse() succeed except the last; we don't supply a time zone, but the format for the time is LONG. Other exceptions are occasionally thrown from the parse() method. To cover all the bases, catch NullPointerExceptions and StringIndexOutOfBoundsExceptions, also.
try { Date d; DateFormat df; df = DateFormat.getDateTimeInstance( DateFormat.FULL, DateFormat.FULL); d = df.parse("Wednesday, April 12, 2000 2:22:22 o'clock PM EDT"); df = DateFormat.getDateTimeInstance( DateFormat.MEDIUM, DateFormat.MEDIUM); d = df.parse("12-Apr-00 2:22:22 PM"); df = DateFormat.getDateTimeInstance( DateFormat.LONG, DateFormat.LONG); d = df.parse("April 12, 2000 2:22:22 PM EDT"); // throws a ParseException; detail level mismatch d = df.parse("12-Apr-00 2:22:22 PM"); } catch (Exception e) { ... }
Java includes two handy classes for timed code execution. If you write a clock application, for example, you might want to update the display every second. Or you might want to play an alarm sound at some predetermined time. You could accomplish these tasks with multiple threads and calls to Thread.sleep(). But it's simpler to use the java.util.Timer and java.util.TimerTask classes.
Instances of Timer watch the clock and execute TimerTasks at appropriate times. You could, for example, schedule a task to run at a specific time like this:
import java.util.*; public class Y2K { public static void main(String[] args) { Timer timer = new Timer( ); TimerTask task = new TimerTask( ) { public void run( ) { System.out.println("Y2K!"); } }; Calendar c = new GregorianCalendar(2000, Calendar.JANUARY, 1); timer.schedule(task, c.getTime( )); } }
TimerTask implements the Runnable interface. To create a task, you can simply subclass TimerTask and supply a run() method. Here we've created a simple anonymous subclass of TimerTask that prints a message to System.out. Using the schedule() method of Timer, we've asked that the task be run on January 1, 2000. (Oops too late! But you get the idea.)
There are some other varieties of schedule(); you can run tasks once or at recurring intervals. There are two kinds of recurring tasks fixed delay and fixed rate. Fixed delay means that a fixed amount of time elapses between the end of the task's execution and the beginning of the next execution. Fixed rate means that the task should begin execution at fixed time intervals (the difference may be important if the time it takes to execute the command is substantial).
You could, for example, update a clock display every second with code like this:
Timer timer = new Timer( ); TimerTask task = new TimerTask( ) { public void run( ) { repaint( ); // update the clock display } }; timer.schedule(task, 0, 1000);
Timer can't really make any guarantees about exactly when things are executed; you'd need a real-time operating system for that kind of precision. However, Timer can give you reasonable assurance that tasks will be executed at particular times, provided the tasks are not overly complex; with a slow-running task, the end of one execution might spill into the start time for the next execution.
Collections are fundamental to all kinds of programming. Anywhere we need to keep a group of objects, we have some kind of collection. At the most basic level, Java supports collections in the form of arrays. But since arrays have a fixed length, they are awkward for groups of things that grow and shrink over the lifetime of an application. From the beginning, the Java platform has had two fundamental classes for managing groups of objects: the java.util.Vector class represents a dynamic list of objects, and the java.util.Hashtable class holds a map of key/value pairs. With Java 1.2 came a more comprehensive approach to collections called the Collections Framework. The Vector and Hashtable classes still exist, but they have now been brought into the framework (and have some eccentricities).
If you work with maps, dictionaries, or associative arrays in other languages, you understand how useful these classes are. If you have done a lot of work in C or another static language, you should find collections to be truly magical. They are part of what makes Java powerful and dynamic. Being able to work with groups of objects and make associations between them is an abstraction from the details of the types. It lets you think about the problems at a higher level and saves you from having to reproduce common structures every time you need them.
The Collections Framework is based around a handful of interfaces in the java.util package. These interfaces are divided into two hierarchies. The first hierarchy descends from the Collection interface. This interface (and its descendants) represents a container that holds other objects. The second hierarchy is based on the Map interface, which represents a group of key/value pairs.
The mother of all collections is an interface appropriately named Collection. It serves as a container that holds other objects, its elements. It doesn't specify exactly how the objects are organized; it doesn't say, for example, whether duplicate objects are allowed or whether the objects are ordered in some way. These kinds of details are left to child interfaces. Nevertheless, the Collection interface does define some basic operations:
This method adds the supplied object to this collection. If the operation succeeds, this method returns true. If the object already exists in this collection and the collection does not permit duplicates, false is returned. Furthermore, some collections are read-only. These collections throw an UnsupportedOperationException if this method is called.
This method removes the specified object from this collection. Like the add() method, this method returns true if the object is removed from the collection. If the object doesn't exist in this collection, false is returned. Read-only collections throw an UnsupportedOperationException if this method is called.
This method returns true if the collection contains the specified object.
Use this method to find the number of elements in this collection.
This method returns true if there are no elements in this collection.
Use this method to examine all the elements in this collection. This method returns an Iterator, which is an object you can use to step through the collection's elements. We'll talk more about iterators in the next section.
These methods are common to every Collection implementation. Any class that implements Collection or one of its child interfaces will have these methods.
Converting between collections and arrays is easy. As a special convenience, the elements of a collection can be placed into an array using the following methods:
public Object[] toArray() public Object[] toArray(Object[] a)
The first method returns a plain Object array. With the second form, we can be more specific. If we supply our own array of the correct size, it will be filled in with the values. But there is a special feature; if the array is too short, a new array of the same type will be created, of the correct length, and returned to us. So it is idiomatic to pass in an empty array of the correct type just to specify the type of the array we want, like this:
String [] myStrings = (String [])myCollection( new String[0] );
You can convert an object type array to a List type collection with the static asList() method of the java.util.Arrays class:
List list = Arrays.asList( myStrings );
A Collection is a dynamic container; it can grow to accommodate new items. For example, a List is a kind of Collection that implements a dynamic array. You can insert and remove elements at arbitrary positions within a List. Collections work directly with the type Object, so we can use Collections with instances of any class.[4]
We can even put different kinds of Objects in a Collection together; the Collection doesn't know the difference.
As you might guess, this is where things get tricky. To do anything useful with an Object after we take it back out of a Collection, we have to cast it back (narrow it) to its original type. This can be done safely in Java because the cast is checked at runtime. Java throws a ClassCastException if we try to cast an object to the wrong type. However, this need for casting means that your code must remember types or methodically test them with instanceof. That is the price we pay for having a completely dynamic collection class that operates on all types.
You might wonder if you can implement Collection to produce a class that works on just one type of element in a type-safe way. Unfortunately, the answer is no. We could implement Collection's methods to make a Collection that rejects the wrong type of element at runtime, but this does not provide any new compile time, static type safety. In an upcoming version of Java, templates or generics will provide a safe mechanism for parameterizing types by restricting the types of objects used at compile time. For a glimpse at Java language work in this area, see: http://jcp.org/aboutJava/communityprocess/review/jsr014/.
An iterator is an object that lets you step through a sequence of values. This kind of operation comes up so often that it is given a generic interface: java.util.Iterator. The Iterator interface has only two primary methods:
This method returns the next element of the associated collection.
This method returns true if you have not yet stepped through all the Collection's elements. In other words, it returns true if you can call next() to get the next element.
The following example shows how you could use an Iterator to print out every element of a collection:
public void printElements(Collection c, PrintStream out) { Iterator iterator = c.iterator( ); while (iterator.hasNext( )) out.println(iterator.next( )); }
In addition to the traversal methods, Iterator provides the ability to remove an element from a collection:
This method removes the last object returned from next() from the associated Collection.
But not all iterators implement remove(). It doesn't make sense to be able to remove an element from a read-only collection, for example. If element removal is not allowed, an UnsupportedOperationException is thrown from this method. If you call remove() before first calling next(), or if you call remove() twice in a row, you'll get an IllegalStateException.
Prior to the introduction of the Collections API there was another iterator interface: java.util.Enumeration. It used the slightly more verbose names: nextElement() and hasMoreElements() but accomplished the same thing. Many older classes provide Enumerations where they would now use Iterator. If you aren't worried about performance, you can convert your Enumeration into a List with a static convenience method of the java.util.Collections class:
List list = Collections.list( enumeration );
The Collection interface has two child interfaces. Set represents a collection in which duplicate elements are not allowed, and List is a collection whose elements have a specific order.
Set has no methods besides the ones it inherits from Collection. It does, however, enforce the rule that duplicate elements are not allowed. If you try to add an element that already exists in a Set, the add() method returns false.
SortedSet adds only a few methods to Set. As you call add() and remove(), the set maintains its order. You can retrieve subsets (which are also sorted) using the subSet(), headSet(), and tailSet() methods. The first(), last(), and comparator() methods provide access to the first element, the last element, and the object used to compare elements (more on this later).
The last child interface of Collection is List . The List interface adds the ability to manipulate elements at specific positions in the list.
This method adds the given object at the supplied list position. If the position is less than zero or greater than the list length, an IndexOutOfBoundsException will be thrown. The element that was previously at the supplied position, and all elements after it are moved up by one index position.
This method removes the element at the supplied position. All subsequent elements move down by one index position.
This method returns the element at the given position.
This method changes the element at the given position to the supplied object.
The Collections Framework also includes the concept of a Map, which is a collection of key/value pairs. Another way of looking at a map is that it is a dictionary, similar to an associative array. Maps store and retrieve elements with key values; they are very useful for things like caches or minimalist databases. When you store a value in a map, you associate a key object with that value. When you need to look up the value, the map retrieves it using the key.
The java.util.Map interface specifies a map that, like Collection, operates on the type Object. A Map stores an element of type Object and associates it with a key, also of type Object. In this way, we can index arbitrary types of elements using arbitrary types as keys. As with Collection, casting is generally required to narrow objects back to their original type after pulling them out of a map.
The basic operations are straightforward:
This method adds the specified key/value pair to the map. If the map already contains a value for the specified key, the old value is replaced and returned as the result.
This method retrieves the value corresponding to key from the map.
This method removes the value corresponding to key from the map. The value removed is returned.
Use this method to find the number of key/value pairs in this map.
You can retrieve all the keys or values in the map:
This method returns a Set that contains all the keys in this map.
Use this method to retrieve all the values in this map. The returned Collection can contain duplicate elements.
Map has one child interface, SortedMap . SortedMap maintains its key/value pairs in sorted order according to the key values. It provides the subMap(), headMap(), and tailMap() methods for retrieving sorted map subsets. Like SortedSet, it also provides a comparator() method that returns an object that determines how the map keys are sorted. We'll talk more about this later.
Finally, we should make it clear that although related, Map is not a type of Collection (Map does not extend the Collection interface). You might be wondering why. All of the methods of the Collection interface would appear to make sense for Map, except for iterator(). A Map, remember, has two sets of objects: keys and values and separate iterators for each.
One more note about maps: some map implementations (including Java's standard, HashMap) allow null to be used as a key or value, but others may not.
Up until this point, we've talked only about interfaces. But you can't instantiate interfaces. The Collections Framework includes useful implementations of the collections interfaces. These implementations are listed by interface in Table 10-5.
Interface | Implementation |
---|---|
Set | HashSet |
SortedSet | TreeSet |
List | ArrayList , LinkedList, Vector |
Map | HashMap , Hashtable, LinkedHashMap, IdentityHashMap |
SortedMap | TreeMap |
ArrayList offers good performance if you add to the end of the list frequently, while LinkedList offers better performance for frequent insertions and deletions. Vector has been around since Java 1.0; it's now retrofitted to implement the List methods. Vector's methods are synchronized by default, unlike the other Maps. The old Hashtable has been updated so that it now implements the Map interface. Its methods are also synchronized. As we'll discuss a bit later, there are other, more general ways to get synchronized collections.
The name Hashtable and HashMap refer to the fact that these map collections use object hashcodes in order to maintain their associations. Specifically, an element in a Hashtable or HashMap is not associated with a key strictly by the key object's identity but rather by the key's contents. This allows keys that are equivalent to access the same object. By "equivalent," we mean those objects that compare true with equals(). So, if you store an object in a Hashtable using one object as a key, you can use any other object that equals() tells you is equivalent to retrieve the stored object.
It's easy to see why equivalence is important if you remember our discussion of strings. You may create two String objects that have the same text in them but that come from different sources in Java. In this case, the == operator tells you that the objects are different, but the equals() method of the String class tells you that they are equivalent. Because they are equivalent, if we store an object in a HashMap using one of the String objects as a key, we can retrieve it using the other.
The hashcode of an object makes this association based on content. As we mentioned in Chapter 7, the hashcode is like a fingerprint of the object's data content. HashMap uses it to store the objects so that they can be retrieved efficiently. The hashcode is nothing more than a number (an integer) that is a function of the data. The number always turns out the same for identical data, but the hashing function is intentionally designed to generate as random a number as possible for different combinations of data. In other words, a very small change in the data should produce a big difference in the number. It is unlikely that two similar datasets would produce the same hashcode.
Internally, HashMap really just keeps a number of lists of objects, but it puts objects into the lists based on their hashcode. So when it wants to find the object again, it can look at the hashcode and know immediately how to get to the appropriate list. The HashMap still might end up with a number of objects to examine, but the list should be short. For each object it finds, it does the following comparison to see if the key matches:
if ((keyHashcode == storedKeyHashcode) && key.equals(storedKey)) return object;
There is no prescribed way to generate hashcodes. The only requirement is that they be somewhat randomly distributed and reproducible (based on the data). This means that two objects that are not the same could end up with the same hashcode by accident. This is unlikely (there are 2^32 possible integer values); moreover, it doesn't cause a problem because the HashMap ultimately checks the actual keys, as well as the hashcodes, to see if they are equal. Therefore, even if two objects have the same hashcode, they can still coexist in the HashMap as long as they don't test equal to one another as well.
Hashcodes are computed by an object's hashCode() method, which is inherited from the Object class if it isn't overridden. The default hashCode() method simply assigns each object instance a unique number to be used as a hashcode. If a class does not override this method, each instance of the class will have a unique hashcode. This goes along well with the default implementation of equals() in Object, which only compares objects for identity using ==.
You must override equals() in any classes for which equivalence of different objects is meaningful. Likewise, if you want equivalent objects to serve as equivalent keys, you need to override the hashCode() method as well to return identical hashcode values. To do this, you need to create some suitably complex and arbitrary function of the contents of your object. The only criterion for the function is that it should be almost certain to return different values for objects with different data, but the same value for objects with identical data.
The java.util.Collections class is full of handy static methods that operate on Sets and Maps. (It's not the same as the java.util.Collection interface, which we've already talked about.) Since all the static methods in Collections operate on interfaces, they work regardless of the actual implementation classes you're using. There are lots of useful methods in here and we'll look at only a few now.
All the default collection implementations are not synchronized; that is, they are not safe for concurrent access by multiple threads. The reason for this is performance. In many applications there is no need for synchronization, so the Collections API does not provide it by default. Instead you can create a synchronized version of any collection using the following methods of the Collections class:
public static Collection synchronizedCollection(Collection c) public static Set synchronizedSet(Set s) public static List synchronizedList(List list) public static Map synchronizedMap(Map m) public static SortedSet synchronizedSortedSet(SortedSet s) public static SortedMap synchronizedSortedMap(SortedMap m)
These methods create synchronized, thread-safe versions of the supplied collection, normally by wrapping them. We'll talk a little more about this later in this chapter.
Furthermore, you can use the Collections class to create read-only versions of any collection:
public static Collection unmodifiableCollection(Collection c) public static Set unmodifiableSet(Set s) public static List unmodifiableList(List list) public static Map unmodifiableMap(Map m) public static SortedSet unmodifiableSortedSet(SortedSet s) public static SortedMap unmodifiableSortedMap(SortedMap m)
Collections includes other methods for performing common operations like sorting. Sorting comes in two varieties:
This method sorts the given list. You can use this method only on lists whose elements implement the java.lang.Comparable interface. Luckily, many classes already implement this interface, including String, Date, BigInteger, and the wrapper classes for the primitive types (Integer, Double, etc.).
Use this method to sort a list whose elements don't implement the Comparable interface. The supplied java.util.Comparator does the work of comparing elements. You might, for example, write an ImaginaryNumber class and want to sort a list of them. You would then create a Comparator implementation that knew how to compare two imaginary numbers.
Collections gives you some other interesting capabilities, too. If you're interested in learning more, check out the min(), max(), binarySearch(), and reverse() methods.
Collections is a bread-and-butter topic, which means it's hard to make exciting examples about it. The example in this section reads a text file, parses all its words, counts the number of occurrences, sorts them, and writes the results to another file. It gives you a good feel for how to use collections in your own programs.
//file: WordSort.java import java.io.*; import java.util.*; public class WordSort { public static void main(String[] args) throws IOException { // get the command-line arguments if (args.length < 2) { System.out.println("Usage: WordSort inputfile outputfile"); return; } String inputfile = args[0]; String outputfile = args[1]; /* Create the word map. Each key is a word and each value is an * Integer that represents the number of times the word occurs * in the input file. */ Map map = new TreeMap( ); // read every line of the input file BufferedReader in = new BufferedReader(new FileReader(inputfile)); String line; while ((line = in.readLine( )) != null) { // examine each word on the line StringTokenizer st = new StringTokenizer(line); while (st.hasMoreTokens( )) { String word = st.nextToken( ); Object o = map.get(word); // if there's no entry for this word, add one if (o == null) map.put(word, new Integer(1)); // otherwise, increment the count for this word else { Integer count = (Integer)o; map.put(word, new Integer(count.intValue( ) + 1)); } } } in.close( ); // get the map's keys List keys = new ArrayList(map.keySet( )); // write the results to the output file PrintWriter out = new PrintWriter(new FileWriter(outputfile)); Iterator iterator = keys.iterator( ); while (iterator.hasNext( )) { Object key = iterator.next( ); out.println(key + " : " + map.get(key)); } out.close( ); } }
Suppose, for example, that you have an input file named Ian Moore.txt:
Well it was my love that kept you going Kept you strong enough to fall And it was my heart you were breaking When he hurt your pride So how does it feel How does it feel How does it feel How does it feel
You could run the example on this file using the following command line:
% java WordSort "Ian Moore.txt" count.txt
The output file, count.txt, looks like this:
And : 1 How : 3 Kept : 1 So : 1 Well : 1 When : 1 breaking : 1 does : 4 enough : 1 fall : 1 feel : 4 going : 1 he : 1 heart : 1 how : 1 hurt : 1 it : 6 kept : 1 love : 1 my : 2 pride : 1 strong : 1 that : 1 to : 1 was : 2 were : 1 you : 3 your : 1
The results are case-sensitive: "How" and "how" are recorded separately. You could modify this behavior by converting words to all lowercase after retrieving them from the StringTokenizer:
String word = st.nextToken( ).toLowerCase( );
Earlier we saw that the Collections class provides methods that create a thread-safe version of any Collection. There are methods for each subtype of Collection. The following example shows how to create a thread-safe List:
List list = new ArrayList( ); List syncList = Collections.synchronizedList(list);
Although synchronized collections are thread-safe, the Iterators returned from them are not. This is an important point. If you obtain an Iterator from a collection, you should do your own synchronization to ensure that the collection does not change as you're iterating through its elements. You can do this by convention by synchronizing on the collection itself with the synchronized keyword:
synchronized(syncList) { Iterator iterator = syncList.iterator( ); // do stuff with the iterator here }
In Chapter 5 we introduced the idea of weak references object references that don't prevent their objects from being removed by the garbage collector. WeakHashMap is an implementation of Map that makes use of weak references in its keys and values. This means that you don't have to remove key/value pairs from a Map when you're finished with them. Normally if you removed all references to a key object in the rest of your application, the Map would still contain a reference and keep the object "alive." WeakHashMap changes this; once you remove references to a key object in the rest of the application, the WeakHashMap lets go of it too.
The java.util.Properties class is a specialized hashtable map for strings. Java uses a Properties object to hold environmental information in the way that environment variables are used in other programming environments. You can use a Properties table to hold arbitrary configuration information for an application in an easily accessible format. The Properties table can also load and store its information in text format using streams (see Chapter 11 for information on streams). In Java 1.4, a new Preferences API was introduced which is designed to take over much of the system configuration functionality of properties using XML files. We'll talk about that a bit later.
Any string values can be stored as key/value pairs in a Properties table. However, the convention is to use a dot-separated naming hierarchy to group property names into logical structures, as is done with X Window System resources on Unix systems.[5]
You can create an empty Properties table and add String key/value pairs just as you can with a Hashtable:
Properties props = new Properties( ); props.setProperty("myApp.xsize", "52"); props.setProperty("myApp.ysize", "79");
Thereafter, you can retrieve values with the getProperty() method:
String xsize = props.getProperty( "myApp.xsize" );
If the named property doesn't exist, getProperty() returns null. You can get an Enumeration of the property names with the propertyNames() method:
for ( Enumeration e = props.propertyNames( ); e.hasMoreElements; ) { String name = e.nextElement( ); ... }
When you create a Properties table, you can specify a second table for default property values:
Properties defaults; ... Properties props = new Properties( defaults );
Now when you call getProperty(), the method searches the default table if it doesn't find the named property in the current table. An alternative version of getProperty() also accepts a default value; this value is returned instead of null if the property is not found in the current list or in the default list:
String xsize = props.getProperty( "myApp.xsize", "50" );
You can save a Properties table to an OutputStream using the save() method. The property information is output in a flat ASCII format. We'll talk about I/O in the next chapter, but bear with us for now. Continuing with the previous example, output the property information using the System.out stream as follows:
props.save( System.out, "Application Parameters" );
System.out is a standard output stream that prints to the console or command line of an application. We could also save the information to a file using a FileOutputStream as the first argument to save(). The second argument to save() is a String that is used as a header for the data. The previous code outputs something like the following to System.out:
#Application Parameters #Mon Feb 12 09:24:23 CST 1999 myApp.ysize=79 myApp.xsize=52
The load() method reads the previously saved contents of a Properties object from an InputStream:
FileInputStream fin; ... Properties props = new Properties( ) props.load( fin );
The list() method is useful for debugging. It prints the contents to an OutputStream in a format that is more human-readable but not retrievable by load(). It truncates long lines with an ellipsis (...).
The java.lang.System class provides access to basic system environment information through the static System.getProperty() method. This method returns a Properties table that contains system properties. System properties take the place of environment variables in some programming environments. Table 10-6 summarizes system properties that are guaranteed to be defined in any Java environment.
System property | Meaning |
---|---|
java.vendor | Vendor-specific string |
java.vendor.url | URL of vendor |
java.version | Java version |
java.home | Java installation directory |
java.class.version | Java class version |
java.class.path | The classpath |
os.name | Operating system name |
os.arch | Operating system architecture |
os.version | Operating system version |
file.separator | File separator (such as / or \) |
path.separator | Path separator (such as : or ;) |
line.separator | Line separator (such as \n or \r\n) |
user.name | User account name |
user.home | User's home directory |
user.dir | Current working directory |
Applets are, by current web browser conventions, prevented from reading the following properties: java.home, java.class.path, user.name, user.home, and user.dir. As you'll see later, these restrictions are implemented by a SecurityManager object.
Your application can set system properties with the static method System. setProperty(). You can also set system properties when you run the Java interpreter, using the -D option:
% java -Dfoo=bar -Dcat=Boojum MyApp
Since it is common to use system properties to provide parameters such as numbers and colors, Java provides some convenience routines for retrieving property values and parsing them into their appropriate types. The classes Boolean , Integer, Long , and Color each come with a "get" method that looks up and parses a system property. For example, Integer.getInteger("foo") looks for a system property called foo and then returns it as an Integer. Color.getColor("foo") parses the property as an RGB value and returns a Color object.
Java 1.4 introduced a Preferences API to accommodate the need to store both system and per user configuration data persistently across executions of the Java VM. The Preferences API is like a portable version of the Windows registry, a mini database in which you can keep small amounts of information, accessible to all applications. Entries are stored as name/value pairs, where the values may be of several standard types including strings, numbers, booleans, and even short byte arrays. We should stress that the Preferences API is not intended to be used as a true database and you can't store large amounts of data in it. (That's not to say anything about how it's actually implemented).
Preferences are stored logically in a tree. A preferences object is a node in the tree located by a unique path. You can think of preferences as files in a directory structure; within the file are stored one or more name/value pairs. To store or retrieve items you ask for a preferences object for the correct path. Here is an example; we'll explain the node lookup shortly:
Preferences prefs = Preferences.userRoot( ).node("oreilly/learningjava"); prefs.put("author", "Niemeyer"); prefs.putInt("edition", 4); String author = prefs.get("author", "unknown"); int edition = prefs.getInt("edition", -1);
In addition to the String and int type accessors, there are the following get methods for other types: getLong(), getFloat(), getDouble(), getByteArray(), and getBoolean(). Each of these get methods takes a key name and default value to be used if no value is defined. And of course for each get method, there is a corresponding "put" method that takes the name and a value of the corresponding type. Providing defaults in the get methods is mandatory. The intent is for applications to function even if there is no preference information or if the storage for it is not available, as we'll discuss later.
Preferences are stored in two separate trees: system preferences and user preferences. System preferences are shared by all users of the Java installation. But user preferences are maintained separately for each user; each user sees his or her own preference information. In our example, we used the static method userRoot() to fetch the root node (preference object) for the user preferences tree. We then asked that node to find the child node at the path oreilly/learningjava, using the node() method. The corresponding systemRoot() method provides the system root node.
The node() method accepts either a relative or an absolute path. A relative path asks the node to find the path relative to itself as a base. So we also could have gotten our node this way:
Preferences prefs = Preferences.userRoot( ).node("oreilly").node("learningjava");
But node() also accepts an absolute path, in which case the base node serves only to designate which tree the path is in. So we could use the absolute path /oreilly/learningjava as the argument to any node() method and reach our preferences object.
Java is an object-oriented language, and so it's natural to wish to associate preference data with classes. In Chapter 11, we'll see that Java provides special facilities for loading resource files associated with class files. The Preferences API follows this pattern by associating a node with each Java package. Its convention is simple: the node path is just the package name with the dots (.) converted to slashes (/). All classes in the package share the same node.
You can get the preference object node for a class using the static Preferences.userNodeForPackage() or Preferences.systemNodeForPackage() methods, which take a Class as an argument and return the corresponding package node for the user and system trees respectively. For example:
Preferences datePrefs = Preferences.systemNodeForPackage( Date.class ); Preferences myPrefs = Preferences.userNodeForPackage( MyClass.class ); Preferences morePrefs = Preferences.userNodeForPackage( myObject.getClass( ) );
Here we've used the .class construct to refer to the Class object for the Date class in the system tree and to our own MyClass class in the user tree. The Date class is in the java.util package, so we'll get the node /java/util in that case. You can get the Class for any object instance using the getClass() method.
There is no need to "create" nodes. When you ask for a node you get a preferences object for that path in the tree. If you write something to it, that data is eventually placed in persistent storage, called the backing store. The backing store is the implementation-dependent storage mechanism used to hold the preference data. All the put methods return immediately, and no guarantees are made as to when the data is actually stored. You can force data to the backing store explicitly using the flush() method of the Preferences class. Conversely, you can use the sync() method to guarantee that a preferences object is up to date with respect to changes placed into the backing store by other applications or threads. Both flush() and sync() throw a BackingStoreException if data cannot be read or written for some reason.
You don't have to create nodes, but you can test for the existence of a data node with the nodeExists() method, and you can remove a node and all its children with the removeNode() method. To remove a data item from a node, use the remove() method, specifying the key; or you can remove all the data from a node with the clear() method (which is not the same as removing the node).
Although the details of the backing store are implementation-dependent, the Preferences API provides a simple import/export facility that can read and write parts of a preference tree to an XML file. (The format for the file is available at http://java.sun.com/dtd/). A preference object can be written to an output stream with the exportNode() method. The exportSubtree() method writes the node and all its children. Going the other way, the static Preferences.importPreferences() method can read the XML file and populate the appropriate tree with its data. The XML file records whether it is user or system preferences, but user data is always placed into the current user's tree, regardless of who generated it.
It's interesting to note that since the import mechanism writes directly to the tree, you can't use this as a general data-to-XML storage mechanism; other current and forthcoming APIs play that role. Also, although we said that the implementation details are not specified, it's interesting to note how things really work in the current implementation. Java creates a directory hierarchy for each tree at $JAVA_HOME/jre/.systemPrefs and $HOME/.java/.userPrefs, respectively. In each directory, there is an XML file called prefs.xml corresponding to that node.
Often your application should be notified if changes are made to the preferences while it's running. You can get updates on preference changes using the PreferenceChangeListener and NodeChangeListener interfaces. These interfaces are examples of event listener interfaces, and we'll see many examples of these in Chapter 15 through Chapter 17. We'll talk about the general pattern later, in Section 10.8. For now we'll just say that by registering an object that implements PreferenceChangeListener with a node you can receive updates on added, removed, and changed preference data for that node. The NodeChangeListener allows you to be told when child nodes are added to or removed from a specific node. Here is a snippet that prints all the data changes affecting our /oreilly/learningjava node.
Preferences prefs = Preferences.userRoot( ).node("/oreilly/learningjava"); prefs.addPreferenceChangeListener( new PreferenceChangeListener( ) { public void preferenceChange(PreferenceChangeEvent e) { System.out.println("Value: " + e.getKey( ) + " changed to "+ e.getNewValue( ) ); } } );
In brief, this example listens for changes to preferences and prints them. If this example isn't immediately clear, it should be after you've read about events in Chapter 15 and beyond.
Another feature introduced in Java 1.4 is the Logging API. The java.util.logging package provides a highly flexible and easy to use logging framework for system information, error messages, and fine-grained tracing (debugging) output. With the logging package you can apply filters to select log messages, direct their output to one or more destinations (including files and network services), and format the messages appropriately for their consumers.
Most importantly, much of this basic logging configuration can be set up externally at runtime through the use of a logging setup properties file or an external program. For example, by setting the right properties at runtime, you can specify that log messages are to be sent both to a designated file in XML format and also logged to the system console in a digested, human-readable form. Furthermore, for each of those destinations you can specify the level or priority of messages to be logged, ignoring those below a certain threshold of significance. By following the correct source conventions in your code, you can even make it possible to adjust the logging levels for specific parts of your application, allowing you to target individual packages and classes for detailed logging without being overwhelmed by too much output.
Any good logging API must have at least two guiding principles. First, performance should not inhibit the developer from using log messages freely. As with Java language assertions (discussed in Chapter 4), when log messages are turned off they should not consume any significant amount of processing time. This means there's no performance penalty for including logging statements as long as they're turned off. Second, although some users may want advanced features and configuration, a logging API must have some simple mode of usage that is convenient enough for time-starved developers to use in lieu of the old standby System.out.println(). Java's Logging API provides a simple model and many convenience methods that make it very tempting.
The heart of the logging framework is the logger, an instance of java.util.logging.Logger. In most cases, this is the only class your code will ever have to deal with. A logger is constructed from the static Logger.getLogger() method, with a logger name as its argument. Logger names place loggers into a hierarchy, with a global, root logger at the top and a tree and children below. This hierarchy allows configuration to be inherited by parts of the tree so that logging can be automatically configured for different parts of your application. The convention is to use a separate logger instance in each major class or package and to use the dot-separated package and/or class name as the logger name. For example:
package com.oreilly.learnjava; public class Book { static Logger log = Logger.getLogger("com.oreilly.learnjava.Book");
The logger provides a wide range of methods to log messages; some take very detailed information, and some convenience methods take only a string for ease of use. For example:
log.warning("Disk 90% full."); log.info("New user joined chat room.");
We cover methods of the logger class in detail a bit later. The names warning and info are two examples of logging levels; there are seven levels ranging from SEVERE at the top to FINEST at the bottom. Distinguishing log messages in this way allows us to select the level of information that we want to see at runtime. Rather than simply logging everything and sorting through it later (with negative performance impact) we can tweak which messages are generated. We'll talk more about logging levels in the next section.
We should also mention that for convenience in very simple applications or experiments, a logger for the name "global" is provided in the static field Logger.global. You can use it as an alternative to the old standby System.out.println() for those cases where that is still a temptation:
Logger.global.info("Doing foo...")
Loggers represent the client interface to the logging system, but the actual work of publishing messages to destinations (such as files or the console) is done by handler objects. Each logger may have one or more Handler objects associated with it, which includes several predefined handlers supplied with the Logging API: ConsoleHandler , FileHandler, StreamHandler, and SocketHandler. Each handler knows how to deliver messages to its respective destination. ConsoleHandler is used by the default configuration to print messages on the command line or system console. FileHandler can direct output to files using a supplied naming convention and automatically rotate the files as they become full. The others send messages to streams and sockets, respectively. There is one additional handler, MemoryHandler, that can hold a number of log messages in memory. MemoryHandler has a circular buffer, which maintains a certain number of messages until it is triggered to publish them to another designated handler.
As we said, loggers can be set to use one or more handlers. Loggers also send messages up the tree to each of their parent logger's handlers. In the simplest configuration this means that all messages end up distributed by the root logger's handlers. We'll see how to set up output using the standard handlers for the console, files, etc. shortly.
Before a logger hands off a message to its handlers or its parent's handlers, it first checks whether the logging level is sufficient to proceed. If the message doesn't meet the required level it is discarded at the source. In addition to level, you can implement arbitrary filtering of messages by creating Filter classes which examine the log message before it is processed. A Filter class can be applied to a logger externally, at runtime in the same way that the logging level, handlers, and formatters, discussed next, can be. A Filter may also be attached to an individual Handler to filter records at the output stage (as opposed to the source).
Internally, messages are carried in a neutral format including all the source information provided. It is not until they are processed by a handler that they are formatted for output by an instance of a Formatter object. The logging package comes with two basic formatters: SimpleFormatter and XMLFormatter. The SimpleFormatter is the default used for console output. It produces short, human-readable, summaries of log messages. XMLFormatter encodes all the log message details into an XML record format. The DTD for the format can be found at http://java.sun.com/dtd/.
Table 10-7 lists the logging levels from most significant to least significant.
Level | Meaning |
---|---|
SEVERE | Application failure |
WARNING | Notification of potential problem |
INFO | Messages of general interest to end users |
CONFIG | Detailed system configuration information for administrators |
FINE, FINER, FINEST | Successively more detailed application tracing information for developers |
These levels fall into three camps: end user, administrator, and developer. Applications often default to logging only messages of the INFO level and above (INFO, WARNING, and SEVERE). These levels are generally seen by end users and messages logged to them should be suitable for general consumption. In other words, they should be written clearly so they make sense to an average user of the application. Often these kinds of message are presented to the end user on a system console or in a pop-up message dialog.
The CONFIG level should be used for relatively static but detailed system information that could assist an administrator or installer. This might include information about the installed software modules, host system characteristics, and configuration parameters. These details are important, but probably not as meaningful to an end user.
The FINE, FINER, and FINEST levels are for developers or people who have knowledge of the internals of the application. These should be used for tracing the application at successive levels of detail. You can define your own meanings for these. We'll suggest a rough outline in our example, coming up next.
In the following (admittedly very contrived) example we use all the logging levels so that we can experiment with logging configuration. Although the sequence of messages is nonsensical, the text is representative of messages of that type.
import java.util.logging.*; public class LogTest { public static void main(String argv[]) { Logger logger = Logger.getLogger("com.oreilly.LogTest"); logger.severe("Power lost - running on backup!"); logger.warning("Database connection lost, retrying..."); logger.info("Startup complete."); logger.config("Server configuration: standalone, JVM version 1.4"); logger.fine("Loading graphing package."); logger.finer("Doing pie chart"); logger.finest("Starting bubble sort: value ="+42); } }
There's not much to this example. We ask for a logger instance for our class using the static Logger.getLogger() method, specifying a class name. The convention is to use the fully qualified class name, so we'll pretend that our class is in a com.oreilly package.
Now run LogTest. You should see output like the following on the system console:
Jan 6, 2002 3:24:36 PM LogTest main SEVERE: Power lost - running on backup! Jan 6, 2002 3:24:37 PM LogTest main WARNING: Database connection lost, retrying... Jan 6, 2002 3:24:37 PM LogTest main INFO: Startup complete.
We see the INFO, WARNING, and SEVERE messages, each identified with a date and timestamp and the name of the class and method (LogTest main) from which they came. Notice that the lower level messages did not appear. This is because the default logging level is normally set to INFO, meaning that only messages of severity INFO and above are logged. Also note that the output went to the system console and not to a log file somewhere; that's also the default. Now we'll describe where these defaults are set and how to override them at runtime.
As we said in the introduction, probably the most important feature of the Logging API is the ability to configure so much of it at runtime through the use of external properties or applications. The default logging configuration is stored in the file jre/lib/logging.properties in the directory where Java is installed. It's a standard Java properties file (of the kind we described earlier in this chapter).
The format of this file is simple. You can make changes to it, but you don't have to. Instead you can specify your own logging setup properties file on a case-by-case basis using a system property at runtime, as follows:
% java -Djava.util.logging.config.file=myfile.properties
In this command line, myfile is your properties file that contains directives we'll describe next. If you want to make this file designation more permanent, you can do so by setting the filename in the corresponding entry using the Java Preferences API described earlier in this chapter. You can go even further, and instead of specifying a setup file, supply a class that is responsible for setting up all logging configuration, but we won't get into that here.
A very simple logging properties file might look like this:
# Set the default logging level .level = FINEST # Direct output to the console handlers = java.util.logging.ConsoleHandler
Here we have set the default logging level for the entire application using the .level (that's dot-level) property. We have also used the handlers property to specify that an instance of the ConsoleHandler should be used (just like the default setup) to show messages on the console. If you run our application again, specifying this properties file as the logging setup, you will now see all our log messages.
But we're just getting warmed up. Next let's look at a more complex configuration:
# Set the default logging level .level = INFO # Ouput to file and console handlers = java.util.logging.FileHandler, java.util.logging.ConsoleHandler # Configure the file output java.util.logging.FileHandler.level = FINEST java.util.logging.FileHandler.pattern = %h/Test.log java.util.logging.FileHandler.limit = 25000 java.util.logging.FileHandler.count = 4 java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter # Configure the console output java.util.logging.ConsoleHandler.level = WARNING # Levels for specific classes com.oreilly.LogTest.level = FINEST
In this example, we have configured two log handlers: a ConsoleHandler with the logging level set to WARNING and also an instance of FileHandler that sends the output to an XML file. The file handler is configured to log messages at the FINEST level (all messages) and to rotate log files every 25,000 lines, keeping a maximum of four files.
The filename is controlled by the pattern property. Forward slashes in the filename are automatically localized to backslash (\) if necessary. The special symbol %h refers to the user home. You can use %t to refer to the system temporary directory. If filenames conflict, a number is appended automatically after a dot (starting at zero). Alternatively, you can use %u to indicate where a unique number should be inserted into the name. Similarly, when files rotate, a number is appended after a dot at the end. You can take control of where the rotation number is placed with the %g identifier.
In our example we specified the XMLFormatter class. We could also have used the SimpleFormatter class to send the same kind of simple output to the console. The ConsoleHandler also allows us to specify any formatter we wish, using the formatter property.
Finally, we promised earlier that you could control logging levels for parts of your applications. To do this, set properties on your application loggers using their hierarchical names:
# Levels for specific logger (class) names com.oreilly.LogTest.level = FINEST
Here we've set the logging level for just our test logger, by name. The log properties follow the hierarchy, so we could set the logging level for all classes in the oreilly package with:
com.oreilly.level = FINEST
Logging levels are set in the order they are read in the properties file, so set the general ones first. Also note that the levels set on the handlers allow the file handler to filter only the messages being supplied by the loggers. So setting the file handler to FINEST won't revive messages squelched by a logger set to SEVERE (only the SEVERE messages will make it to the handler from that logger).
In our example we used the seven convenience methods named for the various logging levels. There are also three groups of general methods that can be used to provide more detailed information. The most general are:
log(Level level, String msg) log(Level level, String msg, Object param1) log(Level level, String msg, Object params[]) log(Level level, String msg, Throwable thrown)
These methods accept as their first argument a static logging level identifier from the Level class, followed by a parameter, array, or exception type. The level identifier is one of Level.SEVERE, Level.WARNING, Level.INFO, and so on.
In addition to these four methods, there are four corresponding methods named logp() that also take a source class and method name as the second and third arguments. In our example, we saw Java automatically determine that information, so why would we want to supply it? Well, the answer is that Java may not always be able to determine the exact method name because of runtime dynamic optimization. The p in logp stands for "precise" and allows you to control this yourself.
There is yet another set of methods named logrb() (which probably should have been named "logprb( )") that take both the class and method names and a resource bundle name. The resource bundle localizes the messages (see "Resource Bundles" in Chapter 9). More generally a logger may have a resource bundle associated with it when it is created, using another form of the getLogger method:
Logger.getLogger("com.oreilly.LogTest", "logMessages");
In either case, the resource bundle name is passed along with the log message and can be used by the formatter. If a resource bundle is specified, the standard formatters treat the message text as a key and try to look up a localized message. Localized messages may include parameters using the standard message format notation and the form of log(), which accepts an argument array.
Finally, there are convenience methods called entering(), exiting(), and throwing() which developers can use to log detailed trace information.
In the introduction we said that a priority of the Logging API is performance. To that end we've described that log messages are filtered at the source, using logging levels to cut off processing of messages early. This saves much of the expense of handling them. However it cannot prevent certain kinds of setup work that you might do before the logging call. Specifically, since we're passing things into the log methods, it's common to construct detailed messages or render objects to strings as arguments. Often this kind of operation is costly. To avoid unnecessary string construction, you should wrap expensive log operations in a conditional test using the Logger isLoggable() method to test whether you should carry out the operation:
if ( log.isLoggable( Level.CONFIG ) ) { log.config("Configuration: "+ loadExpensiveConfigInfo( ) ); }
The java.util.Observer interface and java.util.Observable class are relatively small utilities, but they provide a glimpse of a fundamental design pattern in Java. Observers and observables are part of the MVC (Model-View-Controller) framework. It is an abstraction that lets a number of client objects (the observers) be notified whenever a certain object or resource (the observable) changes in some way. We will see this pattern used extensively in Java's event mechanism, covered in Chapter 15 through Chapter 18.
The Observable object has a method an Observer calls to register its interest. When a change happens, the Observable sends a notification by calling a method in each of the Observers. The observers implement the Observer interface, which specifies that notification causes an Observer object's update() method to be called.
In the following example, we create a MessageBoard object that holds a String message. MessageBoard extends Observable, from which it inherits the mechanism for registering observers (addObserver()) and notifying observers (notifyObservers()). To observe the MessageBoard, we have Student objects that implement the Observer interface so that they can be notified when the message changes:
//file: MessageBoard.java import java.util.*; public class MessageBoard extends Observable { private String message; public String getMessage( ) { return message; } public void changeMessage( String message ) { this.message = message; setChanged( ); notifyObservers( message ); } public static void main( String [] args ) { MessageBoard board = new MessageBoard( ); Student bob = new Student( ); Student joe = new Student( ); board.addObserver( bob ); board.addObserver( joe ); board.changeMessage("More Homework!"); } } // end of class MessageBoard class Student implements Observer { public void update(Observable o, Object arg) { System.out.println( "Message board changed: " + arg ); } }
Our MessageBoard object extends Observable, which provides a method called addObserver(). Each Student objects registers itself using this method and receives updates via its update() method. When a new message string is set, using the MessageBoard's changeMessage() method, the Observable calls the setChanged() and notifyObservers() methods to notify the observers. notifyObservers() can take as an argument an Object to pass along as an indication of the change. This object, in this case the String containing the new message, is passed to the observer's update() method, as its second argument. The first argument to update() is the Observable object itself.
The main() method of MessageBoard creates a MessageBoard and registers two Student objects with it. Then it changes the message. When you run the code, you should see each Student object print the message as it is notified.
You can imagine how you could implement the observer/observable relationship yourself using a List to hold the list of observers. In Chapter 15 and beyond, we'll see that the Java AWT and Swing event model extends this design pattern to use strongly typed observables and observers, called events and event listeners. But for now, we turn our discussion of core utilities to another fundamental topic: I/O.
[1] The generator uses a linear congruential formula. See The Art of Computer Programming, Volume 2: Semi-numerical Algorithms by Donald Knuth (Addison-Wesley).
[2] For a wealth of information about time and world time-keeping conventions, see http://tycho.usno.navy.mil, the U.S. Navy Directorate of Time. For a fascinating history of the Gregorian and Julian calendars, try this site: http://www.magnet.ch/serendipity/hermetic/cal_stud/cal_art.htm.
[3] In Java 1.0.2, the Date class performed all three functions. In Java 1.1 and later, most of its methods have been deprecated, so that the only purpose of the Date class is to represent a point in time.
[4] In C++, where classes don't derive from a single Object class that supplies a base type and common methods, the elements of a collection would usually be derived from some common collectable class. This forces the use of multiple inheritance along with its associated problems.
[5] Unfortunately, this is just a naming convention right now, so you can't access logical groups of properties as you can with X resources.
![]() | CONTENTS | ![]() |