| Exploring Java:By Patrick Niemeyer & Joshua Peck source ref: ebookjava.html |
Contents:
Strings
If you've been reading this book sequentially, you've read all about the core Java language constructs, including the object-oriented aspects of the language and the use of threads. Now it's time to shift gears and talk about the Java Application Programming Interface (API), the collection of classes that comes with every Java implementation. The Java API encompasses all the public methods and variables in the classes that comprise the core Java packages, listed in Table 7.1. This table also lists the chapters in this book that describe each of the packages.
| Package | Contents | Chapter(s) |
|---|---|---|
| java.lang | Basic language classes | 4, 5, 6, 7 |
| java.io | Input and output | 8 |
| java.util | Utilities and collections classes | 7 |
| java.text | International text classes | 7 |
| java.net | Sockets and URLs | 9 |
| java.applet | The applet API | 10 |
| java.awt | The Abstract Windowing Toolkit | 10, 11, 12, 13, 14 |
| java.awt.image | AWT image classes | 13, 14 |
| java.beans | Java Beans API | |
| java.rmi | RMI classes | |
| java.security | Encryption and signing | |
| java.sql | JDBC classes |
As you can see in Table 7.1, we've already examined some of the classes in java.lang in earlier chapters on the core language constructs. Starting with this chapter, we'll throw open the Java toolbox and begin examining the rest of the classes in the API.
We'll begin our exploration with some of the fundamental language classes in java.lang, including strings and math utilities. Figure 7.1 shows the class hierarchy of the java.lang package.
We cover some of the classes in java.util, such as classes that support date and time values, random numbers, vectors, and hashtables. Figure 7.2 shows the class hierarchy of the java.util package.
In this section, we take a closer look at the Java String class (or more specifically, java.lang.String). Because strings are used so extensively throughout Java (or any programming language, for that matter), the Java String class has quite a bit of functionality. We'll test drive most of the important features, but before you go off and write a complex parser or regular expression library, you should probably refer to a Java class reference manual for additional details.
Strings are immutable; once you create a String object, you can't change its value. Operations that would otherwise change the characters or the length of a string instead return a new String object that copies the needed parts of the original. Because of this feature, strings can be safely shared. Java makes an effort to consolidate identical strings and string literals in the same class into a shared string pool.
To create a string, assign a double-quoted constant to a String variable:
String quote = "To be or not to be";
Java automatically converts the string literal into a String object. If you're a C or C++ programmer, you may be wondering if quote is null-terminated. This question doesn't make any sense with Java strings. The String class actually uses a Java character array internally. It's private to the String class, so you can't get at the characters and change them. As always, arrays in Java are real objects that know their own length, so String objects in Java don't require special terminators (not even internally). If you need to know the length of a String, use the length() method:
int length = quote.length();
Strings can take advantage of the only overloaded operator in Java, the + operator, for string concatenation. The following code produces equivalent strings:
String name = "John " + "Smith";
String name = "John ".concat("Smith");
Literal strings can't span lines in Java source files, but we can concatenate lines to produce the same effect:
String poem =
"'Twas brillig, and the slithy toves\n" +
" Did gyre and gimble in the wabe:\n" +
"All mimsy were the borogoves,\n" +
" And the mome raths outgrabe.\n";
Of course, embedding lengthy text in source code should now be a thing of the past, given that we can retrieve a String from anywhere on the planet via a URL. In Chapter 9, Network Programming, we'll see how to do things like:
String poem =
(String) new URL
("http://server/~dodgson/jabberwocky.txt").getContent();
In addition to making strings from literal expressions, we can construct a String from an array of characters:
char [] data = { 'L', 'e', 'm', 'm', 'i', 'n', 'g' };
String lemming = new String( data );
Or from an array of bytes:
byte [] data = { 97, 98, 99 };
String abc = new String(data, "8859_5");
The second argument to the String constructor for byte arrays is the name of an encoding scheme. It's used to convert the given bytes to the string's Unicode characters. Unless you know something about Unicode, you can probably use the form of the constructor that accepts only a byte array; the default encoding scheme will be used.
We can get the string representation of most things with the static String.valueOf() method. Various overloaded versions of this method give us string values for all of the primitive types:
String one = String.valueOf( 1 ); String two = String.valueOf( 2.0f ); String notTrue = String.valueOf( false );
All objects in Java have a toString() method, inherited from the Object class (see Chapter 5, Objects in Java). For class-type references, String.valueOf() invokes the object's toString() method to get its string representation. If the reference is null, the result is the literal string "null":
String date = String.valueOf( new Date() ); System.out.println( date ); // Sun Dec 19 05:45:34 CST 1999 date = null; System.out.println( date ); // null
Producing primitives like numbers from String objects is not a function of the String class. For that we need the primitive wrapper classes; they are described in the next section on the Math class. The wrapper classes provide valueOf() methods that produce an object from a String, as well as corresponding methods to retrieve the value in various primitive forms. Two examples are:
int i = Integer.valueOf("123").intValue();
double d = Double.valueOf("123.0").doubleValue();
In the above code, the Integer.valueOf() call yields an Integer object that represents the value 123. An Integer object can provide its primitive value in the form of an int with the intValue() method.
Although the techniques above may work for simple cases, they will not work internationally. Let's pretend for a moment that we are programming Java in the rolling hills of Tuscany. We would follow the local customs for representing numbers and write code like the following.
double d = Double.valueOf("1.234,56").doubleValue(); // oops!
Unfortunately, this code throws a NumberFormatException. The java.text package, which we'll discuss later, contains the tools we need to generate and parse strings in different countries and languages.
The charAt() method of the String class lets us get at the characters of a String in an array-like fashion:
String s = "Newton"; for ( int i = 0; i < s.length(); i++ ) System.out.println( s.charAt( i ) );
This code prints the characters of the string one at a time. Alternately, we can get the characters all at once with toCharArray(). Here's a way to save typing a bunch of single quotes:
char [] abcs = "abcdefghijklmnopqrstuvwxyz".toCharArray();
Just as in C, you can't compare strings for equality with "==" because as in C, strings are actually references. If your Java compiler doesn't happen to coalesce multiple instances of the same string literal to a single string pool item, even the expression "foo" == "foo" will return false. Comparisons with <, >, <=, and >= don't work at all, because Java can't convert references to integers.
Use the equals() method to compare strings:
String one = "Foo";
char [] c = { 'F', 'o', 'o' };
String two = new String ( c );
if ( one.equals( two ) ) // yes
An alternate version, equalsIgnoreCase(), can be used to check the equivalence of strings in a case-insensitive way:
String one = "FOO"; String two = "foo"; if ( one.equalsIgnoreCase( two ) ) // yes
The compareTo() method compares the lexical value of the String against another String. It returns an integer that is less than, equal to, or greater than zero, just like the C routine string():
String abc = "abc"; String def = "def"; String num = "123"; if ( abc.compareTo( def ) < 0 ) // yes if ( abc.compareTo( abc ) == 0 ) // yes if ( abc.compareTo( num ) > 0 ) // yes
On some systems, the behavior of lexical comparison is complex, and obscure alternative character sets exist. Java avoids this problem by comparing characters strictly by their position in the Unicode specification.
In Java 1.1, the java.text package provides a sophisticated set of classes for comparing strings, even in different languages. German, for example, has vowels with umlauts (those funny dots) over them and a weird-looking beta character that represents a double-s. How should we sort these? Although the rules for sorting these characters are precisely defined, you can't assume that the lexical comparison we used above works correctly for languages other than English. Fortunately, the Collator class takes care of these complex sorting problems. In the following example, we use a Collator designed to compare German strings. (We'll talk about Locales in a later section.) You can obtain a default Collator by calling the Collator.getInstance() method that has no arguments. Once you have an appropriate Collator instance, you can use its compare() method, which returns values just like String's compareTo() method. The code below creates two strings for the German translations of "fun" and "later," using Unicode constants for these two special characters. It then compares them, using a Collator for the German locale; the result is that "later" (spaeter) sorts before "fun" (spass).
String fun = "Spa\u00df"; String later = "sp\u00e4ter"; Collator german = Collator.getInstance(Locale.GERMAN); if (german.compare(later, fun) < 0) // yes
Using collators is essential if you're working with languages other than English. In Spanish, for example, "ll" and "ch" are treated as separate characters, and alphabetized separately. A collator handles cases like these automatically.
The String class provides several methods for finding substrings within a string. The startsWith() and endsWith() methods compare an argument String with the beginning and end of the String, respectively:
String url = "http://foo.bar.com/";
if ( url.startsWith("http:") )
// do HTTP
Overloaded versions of indexOf() search for the first occurrence of a character or substring:
int i = abcs.indexOf( 'p' ); // i = 15 int i = abcs.indexOf( "def" ); // i = 3
Correspondingly, overloaded versions of lastIndexOf() search for the last occurrence of a character or substring.
A number of methods operate on the String and return a new String as a result. While this is useful, you should be aware that creating lots of strings in this manner can affect performance. If you need to modify a string often, you should use the StringBuffer class, as I'll discuss shortly.
trim() is a useful method that removes leading and trailing white space (i.e., carriage return, newline, and tab) from the String:
String abc = " abc "; abc = abc.trim(); // "abc"
In the above example, we have thrown away the original String (with excess white space), so it will be garbage collected.
The toUpperCase() and toLowerCase() methods return a new String of the appropriate case:
String foo = "FOO".toLowerCase(); String FOO = foo.toUpperCase();
substring() returns a specified range of characters. The starting index is inclusive; the ending is exclusive:
String abcs = "abcdefghijklmnopqrstuvwxyz"; String cde = abcs.substring(2, 5); // "cde"
Many people complain when they discover the Java String class is final (i.e., it can't be subclassed). There is a lot of functionality in String, and it would be nice to be able to modify its behavior directly. Unfortunately, there is also a serious need to optimize and rely on the performance of String objects. As I discussed in Chapter 5, Objects in Java, the Java compiler can optimize final classes by inlining methods when appropriate. The implementation of final classes can also be trusted by classes that work closely together, allowing for special cooperative optimizations. If you want to make a new string class that uses basic String functionality, use a String object in your class and provide methods that delegate method calls to the appropriate String methods.
Table 7.2 summarizes the methods provided by the String class.
| Method | Functionality |
|---|---|
| charAt() | Gets at a particular character in the string |
| compareTo() | Compares the string with another string |
| concat() | Concatenates the string with another string |
| copyValueOf() | Returns a string equivalent to the specified character array |
| endsWith() | Checks if the string ends with a suffix |
| equals() | Compares the string with another string |
| equalsIgnoreCase() | Compares the string with another string and ignores case |
| getBytes() | Copies characters from the string into a byte array |
| getChars() | Copies characters from the string into a character array |
| hashCode() | Returns a hashcode for the string |
| indexOf() | Searches for the first occurrence of a character or substring in the string |
| intern() | Fetches a unique instance of the string from a global shared string pool |
| lastIndexOf() | Searches for the last occurrence of a character or substring in a string |
| length() | Returns the length of the string |
| regionMatches() | Checks whether a region of the string matches the specified region of another string |
| replace() | Replaces all occurrences of a character in the string with another character |
| startsWith() | Checks if the string starts with a prefix |
| substring() | Returns a substring from the string |
| toCharArray() | Returns the array of characters from the string |
| toLowerCase() | Converts the string to uppercase |
| toString() | Converts the string to a string |
| toUpperCase() | Converts the string to lowercase |
| trim() | Removes the leading and trailing white space from the string |
| valueOf() | Returns a string representation of a value |
The java.lang.StringBuffer class is a growable buffer for characters. It's an efficient alternative to code like the following:
String ball = "Hello"; ball = ball + " there."; ball = ball + " How are you?";
The above example repeatedly produces new String objects. This means that the character array must be copied over and over, which can adversely affect performance. A more economical alternative is to use a StringBuffer object and its append() method:
StringBuffer ball = new StringBuffer("Hello");
ball.append(" there.");
ball.append(" How are you?");
The StringBuffer class actually provides a number of overloaded append() methods, for appending various types of data to the buffer.
We can get a String from the StringBuffer with its toString() method:
String message = ball.toString();
StringBuffer also provides a number of overloaded insert() methods for inserting various types of data at a particular location in the string buffer.
The String and StringBuffer classes cooperate, so that even in this last operation, no copy has to be made. The string data is shared between the objects, unless and until we try to change it in the StringBuffer.
So, when should you use a StringBuffer instead of a String? If you need to keep adding characters to a string, use a StringBuffer; it's designed to efficiently handle such modifications. You'll still have to convert the StringBuffer to a String when you need to use any of the methods in the String class. You can print a StringBuffer directly using System.out.println()because println() calls the toString() for you.
Another thing you should know about StringBuffer methods is that they are thread-safe, just like all public methods in the Java API. This means that any time you modify a StringBuffer, you don't have to worry about another thread coming along and messing up the string while you are modifying it. If you recall our discussion of synchronization in Chapter 6, Threads, you know that being thread-safe means that only one thread at a time can change the state of a StringBuffer instance.
On a final note, I mentioned earlier that strings take advantage of the single overloaded operator in Java, +, for concatenation. You might be interested to know that the compiler uses a StringBuffer to implement concatenation. Consider the following expression:
String foo = "To " + "be " + "or";
This is equivalent to:
String foo = new
StringBuffer().append("To ").append("be ").append("or").toString();
This kind of chaining of expressions is one of the things operator overloading hides in other languages.
A common programming task involves parsing a string of text into words or "tokens" that are separated by some set of delimiter characters. The java.util.StringTokenizer class is a utility that does just this. The following example reads words from the string text:
String text = "Now is the time for all good men (and women)...";
StringTokenizer st = new StringTokenizer( text );
while ( st.hasMoreTokens() ) {
String word = st.nextToken();
...
}
First, we create a new StringTokenizer from the String. We invoke the hasMoreTokens() and nextToken() methods to loop over the words of the text. By default, we use white space (i.e., carriage return, newline, and tab) as delimiters.
The StringTokenizer implements the java.util.Enumeration interface, which means that StringTokenizer also implements two more general methods for accessing elements: hasMoreElements() and nextElement(). These methods are defined by the Enumeration interface; they provide a standard way of returning a sequence of values, as we'll discuss a bit later. The advantage of nextToken() is that it returns a String, while nextElement() returns an Object. The Enumeration interface is implemented by many items that return sequences or collections of objects, as you'll see when we talk about hashtables and vectors later in the chapter. Those of you who have used the C strtok()function should appreciate how useful this object-oriented equivalent is.
You can also specify your own set of delimiter characters in the StringTokenizer constructor, using another String argument to the constructor. Any combination of the specified characters is treated as the equivalent of white space for tokenizing:
text = "http://foo.bar.com/"; tok = new StringTokenizer( text, "/:" ); if ( tok.countTokens() < 2 ) // bad URL String protocol = tok.nextToken(); // protocol = "http" String host = tok.nextToken(); // host = "foo.bar.com"
The example above parses a URL specification to get at the protocol and host components. The characters "/" and ":" are used as separators. The countTokens() method provides a fast way to see how many tokens will be returned by nextToken(), without actually creating the String objects.
An overloaded form of nextToken() accepts a string that defines a new delimiter set for that and subsequent reads. And finally, the StringTokenizer constructor accepts a flag that specifies that separator characters are to be returned individually as tokens themselves. By default, the token separators are not returned.
Java supports integer and floating-point arithmetic directly. Higher-level math operations are supported through the java.lang.Math class. Java provides wrapper classes for all primitive data types, so you can treat them as objects if necessary. Java also provides the java.util.Random class for generating random numbers.
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 the above 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 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 7.3.
| 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 distinction of not being equal to itself (NaN != NaN). Use Float.isNaN() or Double.isNaN() to test for NaN.
The java.lang.Math class serves as Java's math library. All its methods are static and used directly ; you can't instantiate a Math object. We use this kind of degenerate class when we really want methods to approximate normal functions in C. While this tactic defies the principles of object-oriented design, it makes sense in this case, as it provides a means of grouping some related utility functions in a single class. Table 7.4 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 | Converts rectangular to polar coordinates |
| Math.ceil(a) | double | Smallest whole number greater than or equal to a |
| Math.cos(a) | double | Cosine |
| Math.exp(a) | double | Exponential number to the power of 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 of 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 |
| 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 BigInteger to add two numbers together.
try {
BigDecimal twentyone = new BigDecimal("21");
BigDecimal seven = new BigDecimal("7");
BigDecimal sum = twentyone.add(seven);
int twentyeight = sum.intValue();
}
catch (NumberFormatException nfe) { }
catch (ArithmeticException ae) { }
In languages like 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 would seriously affect performance. 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 7.5.
| 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 code is equivalent:
Float pi = new Float( 3.14 ); Float pi = new Float( "3.14" );
Wrapper classes throw a NumberFormatException when there is an error in parsing from a string:
try {
Double bogus = new Double( "huh?" );
}
catch ( NumberFormatException e ) { // bad number
}
You should arrange to catch this exception if you want to deal with it. Otherwise, since it's a subclass of RuntimeException, it will propagate up the call stack and eventually cause a run-time error if not caught.
Sometimes you'll use the wrapper classes simply to parse the String representation of a number:
String sheep = getParameter("sheep");
int n = new Integer( sheep ).intValue();
Here we are retrieving the value of the sheep parameter. This value is returned as a String, so we need to convert it to a numeric value before we can use it. Every wrapper class provides methods to get primitive values out of the wrapper; we are using intValue() to retrieve an int out of Integer. Since parsing a String representation of a number is such a common thing to do, the Integer and Long classes also provide the static methods Integer.parseInt() and Long.parseLong() that read a String and return the appropriate type. So the second line above is equivalent to:
int n = Integer.parseInt( sheep );
All wrappers provide access to their values in various forms. You can retrieve scalar values with the methods doubleValue(), floatValue(), longValue(), and intValue():
Double size = new Double ( 32.76 ); double d = size.doubleValue(); float f = size.floatValue(); long l = size.longValue(); int i = size.intValue();
The code above is equivalent to the primitive double value cast to the various types. For convenience, you can cast between the wrapper classes like Double class and the primitive data types.
Another common use of wrappers occurs when we have to treat a primitive value as an object in order to place it in a list or other structure that operates on objects. As you'll see shortly, a Vector is an extensible array of Objects. We can use wrappers to hold numbers in a Vector, along with other objects:
Vector myNumbers = new Vector(); Integer thirtyThree = new Integer( 33 ); myNumbers.addElement( thirtyThree );
Here we have created an Integer wrapper so that we can insert the number into the Vector using addElement(). Later, when we are taking elements back out of the Vector, we can get the number back out of the Integer as follows:
Integer theNumber = (Integer)myNumbers.firstElement(); int n = theNumber.intValue(); // n = 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:
[1] The generator uses a linear congruential formula. See The Art of Computer Programming, Volume 2 "Semi-numerical Algorithms," by Donald Knuth (Addison-Wesley).
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 7.6.
| Method | Range |
|---|---|
| nextInt() | -2147483648 to 2147483647 |
| nextLong() | -9223372036854775808 to 9223372036854775807 |
| nextFloat() | -1.0 to 1.0 |
| nextDouble() | -1.0 to 1.0 |
By default, the values are uniformly distributed. You can use the nextGaussian() method to create a Gaussian 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. So 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] Java 1.1 gives you three classes that do all the hard work for you. The java.util.Date 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. In Java 1.0.2, the Date class performed all three functions. In Java 1.1, most of its methods have been deprecated, so that its only purpose in life is to represent a point in time.
[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 http://barroom.visionsystems.com/serendipity/date/jul_greg.html.
The separation of the Date class and the GregorianCalendar class is analagous 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 initialize the calendar. In the first statement below, we construct an object representing August 9, 1996; the second statement specifies both a date and a time, yielding an object that represents 9:01 AM, April 8, 1997.
GregorianCalendar daphne =
new GregorianCalendar(1996, Calendar.AUGUST, 9);
GregorianCalendar sometime =
new GregorianCalendar(1997, 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 create a string representing a point in time, use the DateFormat class. Although DateFormat itself is abstract, it has several 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/9/97 6:06 AM
Those of you who don't live on the West coast will notice that the example above produces a result that is not quite right. DateFormat instances stubbornly insist on using Pacific Standard Time, so you have to tell them what time zone you're in:
DateFormat plain = DateFormat.getInstance(); plain.setTimeZone(TimeZone.getDefault()); String now = plain.format(new Date()); // 4/9/97 9: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 code below 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 says how to format the time.
DateFormat df = DateFormat.getDateInstance(DateFormat.DEFAULT); // 09-Apr-97
DateFormat tf = DateFormat.getTimeInstance(DateFormat.DEFAULT); // 9:18:27 AM
DateFormat dtf =
DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL);
// Wednesday, April 09, 1997 9:18:27 o'clock AM EDT
Formatting dates and times for other countries is just as easy. There are overloaded factory methods that accept a Locale argument:
DateFormat df =
DateFormat.getDateInstance(DateFormat.DEFAULT, Locale.FRANCE);
// 9 avr. 97
DateFormat tf =
DateFormat.getTimeInstance(DateFormat.DEFAULT, Locale.GERMANY);
// 9:27:49
DateFormat dtf =
DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL,
Locale.ITALY);
// mercoledi 9 aprile 1997 9.27.49 GMT-04:00
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 that is produced by the DateFormat. The parse() method throws a ParseException if it doesn't understand the string you give it. Occasionally other exceptions are 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 09, 1997 2:22:22 o'clock PM EST"); // ok
df = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM);
d = df.parse("09-Apr-97 2:22:22 PM"); //ok
df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG);
d = df.parse("April 09, 1997 2:22:22 PM EST"); // ok
d = df.parse("09-Apr-97 2:22:22 PM"); // ParseException - detail level mismatch
}
catch (Exception e) {}
There's been a lot of talk about the "millenium bug" lately. This refers to the expected failure of software in the year 2000, when programs that use two digits to represent years interpret "00" as 1900 instead of 2000. Java is mostly safe from this error. The Date class has no specific field for year and is thus immune to this problem. The only time you'll run into this error in Java is when you use a DateFormat to parse a date string with a two-digit year. Two-digit years are automatically prefixed with 19. My advice is to always use a four-digit year when you expect to parse a date string.
Vectors and hashtables are collection classes. Each stores a group of objects according to a particular retrieval scheme. Aside from that, they are not particularly closely related things. A hashtable is a dictionary; it stores and retrieves objects by a key value. A vector, on the other hand, holds an ordered collection of elements. It's essentially a dynamic array. Both of these, however, have more subtle characteristics in common. First, they are two of the most useful aspects of the core Java distribution. Second, they both take full advantage of Java's dynamic nature at the expense of some of its more static type safety.
If you work with dictionaries or associative arrays in other languages, you should understand how useful these classes are. If you are someone who has worked 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 lists 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.
A Vector is a dynamic array; it can grow to accommodate new items. You can also insert and remove elements at arbitrary positions within it. As with other mutable objects in Java, Vector is thread-safe. The Vector class works directly with the type Object, so we can use them with instances of any kind of class.[3] We can even put different kinds of Objects in a Vector together; the Vector doesn't know the difference.
[3] 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 and brings its associated problems.
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 Vector, we have to cast it back (narrow) it to its original type. This can be done with safety in Java because the cast is checked at run-time. 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 subclass Vector to produce a class that looks like a Vector, but that works on just one type of element in a type-safe way. Unfortunately, the answer is no. We could override Vector's methods to make a Vector that rejects the wrong type of element at run-time, but this does not provide any new compile-time, static type safety. In C++, templates provide a safe mechanism for parameterizing types by restricting the types of objects used at compile-time. The keyword generic is a reserved word in Java. This means that it's possible that future versions might support C++-style templates, using generic to allow statically checked parameterized types.
We can construct a Vector with default characteristics and add elements to it using addElement() and insertElement():
Vector things = new Vector(); String one = "one"; String two = "two"; String three = "three"; things.addElement( one ); things.addElement( three ); things.insertElementAt( two, 1 );
things now contains three String objects in the order "one," "two," and "three." We can retrieve objects by their position with elementAt(), firstElement(), and lastElement():
String s1 = (String)things.firstElement(); // "one" String s3 = (String)things.lastElement(); // "three" String s2 = (String)things.elementAt(1); // "two"
We have to cast each Object back to a String in order to assign it a String reference. ClassCastException is a type of RuntimeException, so we can neglect to guard for the exception if we are feeling confident about the type we are retrieving. Often, as in this example, you'll just have one type of object in the Vector. If we were unsure about the types of objects we were retrieving, we would want to be prepared to catch the ClassCastException or test the type explicitly with the instanceof operator.
We can search for an item in a Vector with the indexOf() method:
int i = things.indexOf( three ); // i = 2
indexOf() returns a value of -1 if the object is not found. As a convenience, we can also use contains() simply to test for the presence of the object.
Finally, removeElement() removes a specified Object from the Vector:
things.removeElement( two );
The element formerly at position three now becomes the second element.
The size() method reports the number of objects currently in the Vector. You might think of using this to loop through all elements of a Vector, using elementAt() to get at each element. This works just fine, but there is a more general way to operate on a complete set of elements like those in a Vector.
The java.util.Enumeration interface can be used by any sort of set to provide serial access to its elements. An object that implements the Enumeration interface presents two methods: nextElement() and hasMoreElements(). nextElement() returns an Object type, so it can be used with any kind of collection. As with taking objects from a Vector, you need to know or determine what the objects are and cast them to the appropriate types before using them.
Enumeration is useful because any type of object can implement the interface and then use it to provide access to its elements. If you have an object that handles a set of values, you should think about implementing the Enumeration interface. Simply provide a hasMoreElements() test and a nextElement() iterator and declare that your class implements java.util.Enumeration. One advantage of an Enumeration is that you don't have to provide all values up front; you can provide each value as it's requested with nextElement(). And since Enumeration is an interface, you can write general routines that operate on all of the elements Enumeration.
An Enumeration does not guarantee the order in which elements are returned, however, so if order is important you don't want to use an Enumeration. You can iterate through the elements in an Enumeration only once; there is no way to reset it to the beginning or move backwards through the elements.
A Vector returns an Enumeration of its contents when we call the elements() method:
Enumeration e = things.elements();
while ( e.hasMoreElements() ) {
String s = (String)e.nextElement();
System.out.println( s ):
}
The above code loops three times, as call nextElement(), to fetch our strings. The actual type of object returned by elements() is a VectorEnumeration, but we don't have to worry about that. We can always refer to an Enumeration simply by its interface.
Note that Vector does not implement the Enumeration interface. If it did, that would put a serious limitation on Vector because we could cycle through the elements in it only once. That's clearly not the purpose of a Vector, which is why Vector instead provides a method that returns an Enumeration.
As I said earlier, a hashtable is a dictionary, similar to an associative array. A hashtable stores and retrieves elements with key values; they are very useful for things like caches and minimalist databases. When you store a value in a hashtable, you associate a key with that value. When you need to look up the value, the hashtable retrieves it efficiently using the key. The name hashtable itself refers to how the indexing and storage of elements is performed, as we'll discuss shortly. First I want to talk about how to use a hashtable.
The java.util.Hashtable class implements a hashtable that, like Vector, operates on the type Object. A Hashtable 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 Vector, casting is generally required to narrow objects back to their original type after pulling them out of a hashtable.
A Hashtable is quite easy to use. We can use the put() method to store items:
Hashtable dates = new Hashtable();
dates.put( "christmas",
new GregorianCalendar( 1997, Calendar.DECEMBER, 25 ) );
dates.put( "independence",
new GregorianCalendar( 1997, Calendar.JULY, 4 ) );
dates.put( "groundhog",
new GregorianCalendar( 1997, Calendar.FEBRUARY, 2 ) );
First we create a new Hashtable. Then we add three GregorianCalendar objects to the hashtable, using String objects as keys. The key is the first argument to put(); the value is the second. Only one value can be stored per key. If we try to store a second object under a key that already exists in the Hashtable, the old element is booted out and replaced by the new one. The return value of the put() method is normally null, but if the call to put() results in replacing an element, the method instead returns the old stored Object.
We can now use the get() method to retrieve each of the above dates by name, using the String key by which it was indexed:
GregorianCalendar g = (GregorianCalendar)dates.get( "christmas" );
The get() method returns a null value if no element exists for the given key. The cast is required to narrow the returned object back to type GregorianCalendar. I hope you can see the advantage of using a Hashtable over a regular array. Each value is indexed by a key instead of a simple number, so unlike a simple array, we don't have to remember where each GregorianCalendar is stored.
Once we've put a value in a Hashtable, we can take it back out with the remove() method, again using the key to access the value:
dates.remove("christmas");
We can test for the existence of a key with containsKey():
if ( dates.containsKey( "groundhog" ) ) { // yes
Just like with a Vector, we're dealing with a set of items. Actually, we're dealing with two sets: keys and values. The Hashtable class has two methods, keys() and elements(), for getting at these sets. The keys() method returns an Enumeration of the keys for all of the elements in the Hashtable. We can use this Enumeration to loop through all of the keys:
for (Enumeration e = dates.keys(); e.hasMoreElements(); ) {
String key = (String)e.nextElement();
...
}
Similarly, elements() provides an Enumeration of the elements themselves.
If you've used a hashtable before, you've probably guessed that there's more going on behind the scenes than I've let on so far. An element in a hashtable is not associated with its key by identity, but by something called a hashcode. Every object in Java has an identifying hashcode value determined by its hashCode() method, which is inherited from the Object class. When you store an element in a hashtable, the hashcode of the key object registers the element internally. Later, when you retrieve the item, that same hashcode looks it up efficiently.
A hashcode is usually a random-looking integer value based on the contents of an object, so it's different for different instances of a class. Two objects that have different hashcodes serve as unique keys in a hashtable; each object can reference a different stored object. Two objects that have the same hashcode value, on the other hand, appear to a hashtable as the same key. They can't coexist as keys to different objects in the hashtable.
Generally, we want our object instances to have unique hash codes, so we can put arbitrary items in a hashtable and index them with arbitrary keys. The default hashCode() method in the Object class 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 is sufficient for most objects.
However, it's also useful to allow equivalent objects to serve as equivalent keys. String objects provide a good example of this case. Although Java does its best to consolidate them, a literal string that appears multiple times in Java source code is often represented by different String objects at run-time. If each of these String objects has a different hash code, even though the literal value is the same, we could not use strings as keys in a hashtable, like we did the in above examples.
The solution is to ensure that equivalent String objects return the same hashcode value so that they can act as equivalent keys. The String class overrides the default hashCode() method so that equivalent String objects return the same hash code, while different String objects have unique hashcodes. This is possible because String objects are immutable; the contents can't change, so neither can the hashcode.
A few other classes in the Java API also override the default hashCode() method in order to provide equivalent hashcodes for equivalent objects. For example, each of the primitive wrapper classes provides a hashCode() method for this purpose. Other objects likely to be used as hashtable keys, such as Color, Date, File, and URL, also implement their own hashCode()methods.
So now maybe you're wondering when you need to override the default hashCode() method in your objects. If you're creating a class to use for keys in a hashtable, think about whether the class supports the idea of "equivalent objects." If so, you should implement a hashCode() method that returns the same hashcode value for equivalent objects.
To accomplish this, you need to define the hashcode of an object to be some suitably complex and arbitrary function of the contents of that object. The only criterion for the function is that it should be almost certain to provide different values for different contents of the object. Because the capacity of an integer is limited, hashcode values are not guaranteed to be unique. This limitation is not normally a problem though, as there are 2^32 possible hashcodes to choose from. The more sensitive the hashcode function is to small differences in the contents the better. A hashtable works most efficiently when the hashcode values are as randomly and evenly distributed as possible. As an example, you could produce a hashcode for a String object by adding the character values at each position in the string and multiplying the result by some number, producing a large random-looking integer.
java.util.Dictionary is the abstract superclass of Hashtable. It lays out the basic get(), put(), and remove() functionality for dictionary-style collections. You could derive other types of dictionaries from this class. For example, you could implement a dictionary with a different storage format, such as a binary tree.
The java.util.Properties class is a specialized hashtable for strings. Java uses the Properties object to replace the environment variables 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 object can also load and store information using streams (see Chapter 8, Input/Output Facilities for information on streams).
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 resources on UNIX systems.[4] The java.lang.System class provides system-environment information in this way, through a system Properties table I'll describe shortly.
[4] Unfortunately, this is just a naming convention right now, so you can't access logical groups of properties as you can with X resources.
Create an empty Properties table and add String key/value pairs just as with any Hashtable:
Properties props = new Properties();
props.put("myApp.xsize", "52");
props.put("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 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 flat ASCII format. Continuing with the above example, output the property information to System.out as follows:
props.save( System.out, "Application Parameters" );
As we'll discuss in Chapter 8, Input/Output Facilities, System.out is a standard output stream similar to C's stdout. We could also save the information to a file by 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 above code outputs something like the following to System.out:
#Application Parameters #Mon Feb 12 09:24:23 CST 1997 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().
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 other programming environments.
Table 7.7 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 class path |
| 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 in the next section, these restrictions are implemented by a SecurityManager object.
As I described in Chapter 1, Yet Another Language?, a Java application's access to system resources, such as the display, the filesystem, threads, external processes, and the network, can be controlled at a single point with a security manager. The class that implements this functionality in the Java API is the java.lang.SecurityManager class.
An instance of the SecurityManager class can be installed once, and only once, in the life of the Java run-time environment. Thereafter, every access to a fundamental system resource is filtered through specific methods of the SecurityManager object by the core Java packages. By installing a specialized SecurityManager, we can implement arbitrarily complex (or simple) security policies for allowing access to individual resources.
When the Java run-time system starts executing, it's in a wide-open state until a SecurityManager is installed. The "null" security manager grants all requests, so the Java virtual environment can perform any activity with the same level of access as other programs running under the user's authority. If the application that is running needs to ensure a secure environment, it can install a SecurityManager with the static System.setSecurityManager() method. For example, a Java-enabled Web browser like Netscape Navigator installs a SecurityManager before it runs any Java applets.
java.lang.SecurityManager must be subclassed to be used. This class does not actually contain any abstract methods; it's abstract as an indication that its default implementation is not very useful. By default, each security method in SecurityManager is implemented to provide the strictest level of security. In other words, the default SecurityManager simply rejects all requests.
The following example, MyApp, installs a trivial subclass of SecurityManager as one of its first activities:
class FascistSecurityManager extends SecurityManager { }
public class MyApp {
public static void main( Strings [] args ) {
System.setSecurityManager( new FascistSecurityManager() );
// No access to files, network, windows, etc.
...
}
}
In the above scenario, MyApp does little aside from reading from System.in and writing to System.out. Any attempts to read or write files, access the network, or even open an window, results in a SecurityException being thrown.
After this draconian SecurityManager is installed, it's impossible to change the SecurityManager in any way. The security of this feature is not dependent on the SecurityManager; you can't replace or modify the SecurityManager under any circumstances. The upshot of this is that you have to install one that handles all your needs up front.
To do something more useful, we can override the methods that are consulted for access to various kinds of resources. Table 7.7 lists some of the more important access methods. You should not normally have to call these methods yourself, although you could. They are called by the core Java classes before granting particular types of access.
| Method | Can I...? |
|---|---|
| checkAccess(Thread g) | Access this thread? |
| checkExit(int status) | Execute a System.exit()? |
| checkExec(String cmd) | exec() this process? |
| checkRead(String file) | Read a file? |
| checkWrite(String file) | Write a file? |
| checkDelete(String file) | Delete a file? |
| checkConnect(String host, int port) | Connect a socket to a host? |
| checkListen(int port) | Create a server socket? |
| checkAccept(String host, int port) | Accept this connection? |
| checkPropertyAccess(String key) | Access this system property? |
| checkTopLevelWindow(Object window) | Create this new top-level window? |
All these methods, with the exception of checkTopLevelWindow(), simply return to grant access. If access is not granted, they throw a SecurityException. checkTopLevelWindow() returns a boolean value. A value of true indicates the access is granted; a value of false indicates the access is granted with the restriction that the new window should provide a warning border that serves to identify it as an untrusted window.
Let's implement a silly SecurityManager that allows only files beginning with the name foo to be read:
class FooFileSecurityManager extends SecurityManager {
public void checkRead( String s ) {
if ( !s.startsWith("foo") )
throw new SecurityException("Access to non-foo file: " +
s + " not allowed." );
}
}
Once the FooFileSecurityManager is installed, any attempt to read a filename other than foo* from any class will fail and cause a SecurityException to be thrown. All other security methods are inherited from SecurityManager, so they are left at their default restrictiveness.
All restrictions placed on applets by an applet-viewer application are enforced through a SecurityManager, which allows untrusted code loaded from over the network to be executed safely. The restrictions placed on applets are currently fairly harsh. As time passes and security considerations related to applets are better understood and accepted, the applet API will hopefully become more powerful and allow forms of persistence and access to designated public information.
In order to deliver on the promise "Write once, run anywhere," the engineers at Java designed the famous Java Virtual Machine. True, your program will run anywhere there is a JVM, but what about users in other countries? Will they have to know English to use your application? Java 1.1 answers that question with a resounding "no," backed up by various classes that are designed to make it easy for you to write a "global" application. In this section we'll talk about the concepts of internationalization and the classes that support them.
Internationalization programming revolves around the Locale class. The class itself is very simple; it encapsulates a country code, a language code, and a rarely used variant code. Commonly used languages and countries are defined as constants in the Locale class. (It's ironic that these names are all in English.) You can retrieve the codes or readable names, as follows:
Locale l = Locale.ITALIAN; System.out.println(l.getCountry()); // IT System.out.println(l.getDisplayCountry()); // Italy System.out.println(l.getLanguage()); // it System.out.println(l.getDisplayLanguage()); // Italian
The country codes comply with ISO 639. A complete list of country codes is at http://www.ics.uci.edu/pub/ietf/http/related/iso639.txt. The language codes comply with ISO 3166. A complete list of language codes is at http://www.chemie.fu-berlin.de/diverse/doc/ISO_3166.html. There is no official set of variant codes; they are designated as vendor-specific or platform-specific.
Various classes throughout the Java API use a Locale to decide how to represent themselves. We have already seen how the DateFormat class uses Locales to determine how to format and parse strings.
If you're writing an internationalized program, you want all the text that is displayed by your application to be in the correct language. Given what you have just learned about Locale, you could print out different messages by testing the Locale. This gets cumbersome quickly, however, because the messages for all Locales are embedded in your source code. ResourceBundle and its subclasses offer a cleaner, more flexible solution.
A ResourceBundle is a collection of objects that your application can access by name, much like a Hashtable with String keys. The same ResourceBundle may be defined for many different Locales. To get a particular ResourceBundle, call the factory method ResourceBundle.getBundle(), which accepts the name of a ResourceBundle and a Locale. The following example gets a ResourceBundle for two Locales, retrieves a string message from it, and prints the message. We'll define the ResourceBundles later to make this example work.
import java.util.*;
public class Hello {
public static void main(String[] args) {
ResourceBundle bun;
bun = ResourceBundle.getBundle("Message", Locale.ITALY);
System.out.println(bun.getString("HelloMessage"));
bun = ResourceBundle.getBundle("Message", Locale.US);
System.out.println(bun.getString("HelloMessage"));
}
}
The getBundle() method throws the runtime exception MissingResourceException if an appropriate ResourceBundle cannot be located.
Locales are defined in three ways. They can be stand-alone classes, in which case they will either be subclasses of ListResourceBundle or direct subclasses of ResourceBundle. They can also be defined by a property file, in which case they will be represented at run-time by a PropertyResourceBundle object. When you call ResourceBundle.getBundle(), either a matching class is returned or an instance of PropertyResourceBundle corresponding to a matching property file. The algorithm used by getBundle() is based on appending the country and language codes of the requested Locale to the name of the resource. Specifically, it searches for resources using this order:
name_language_country_variant
name_language_country
name_language
name
name_default-language_default-country_default-variant
name_default-language_default-country
name_default-language
In the example above, when we try to get the ResourceBundle named Message, specific to Locale.ITALY, it searches for the following names (note that there are no variant codes in the Locales we are using):
Message_it_IT
Message_it
Message
Message_en_US
Message_en
Let's define the Message_it_IT ResourceBundle now, using a subclass of ListResourceBundle.
import java.util.*;
public class Message_it_IT extends ListResourceBundle {
public Object[][] getContents() {
return contents;
}
static final Object[][] contents = {
{"HelloMessage", "Buon giorno, world!"},
{"OtherMessage", "Ciao."},
};
}
ListResourceBundle makes it easy to define a ResourceBundle class; all we have to do is override the getContents() method.
Now let's define a ResourceBundle for Locale.US. This time, we'll make a property file. Save the following data in a file called Message_en_US.properties:
HelloMessage=Hello, world! OtherMessage=Bye.
So what happens if somebody runs your program in Locale.FRANCE, and there is no ResourceBundle defined for that Locale? To avoid a run-time MissingResourceException, it's a good idea to define a default ResourceBundle. So in our example, you could change the name of the property file to Message.properties. That way, if a language-specific or country-specific ResourceBundle cannot be found, your application can still run.
The java.text package includes, among other things, a set of classes designed for generating and parsing string representations of objects. We have already seen one of these classes, DateFormat. In this section we'll talk about the other format classes, NumberFormat, ChoiceFormat, and MessageFormat.
The NumberFormat class can be used to format and parse currency, percents, or plain old numbers. Like DateFormat, NumberFormat is an abstract class. However, it has several useful factory methods. For example, to generate currency strings, use getCurrencyInstance():
double salary = 1234.56;
String here =
NumberFormat.getCurrencyInstance().format(salary);
// $1,234.56
String italy =
NumberFormat.getCurrencyInstance(Locale.ITALY).format(salary);
// L 1.234,56
The first statement generates an American salary, with a dollar sign, a comma to separate thousands, and a period as a decimal point. The second statement presents the same string in Italian, with a lire sign, a period to separate thousands, and a comma as a decimal point. Remember that the NumberFormat worries about format only; it doesn't attempt to do currency conversion. (Among other things, that would require access to a dynamically updated table and exchange rates: a good opportunity for a Java Bean, but too much to ask of a simple formatter.)
Likewise, getPercentInstance() returns a formatter you can use for generating and parsing percents. If you do not specify a Locale when calling a getInstance() method, the default Locale is used.
NumberFormat pf = NumberFormat.getPercentInstance();
System.out.println(pf.format(progress)); // 44%
try {
System.out.println(pf.parse("77.2%")); // 0.772
}
catch (ParseException e) {}
And if you just want to generate and parse plain old numbers, use a NumberFormat returned by getInstance() or its equivalent, getNumberInstance().
NumberFormat guiseppe = NumberFormat.getInstance(Locale.ITALY);
NumberFormat joe = NumberFormat.getInstance(); // defaults to Locale.US
try {
double theValue = guiseppe.parse("34.663,252").doubleValue();
System.out.println(joe.format(theValue)); // 34,663.252
}
catch (ParseException e) {}
We use guiseppe to parse a number in Italian format (periods separate thousands, comma as the decimal point). The return type of parse() is Number, so we use the doubleValue() method to retrieve the value of the Number as a double. Then we use joe to format the number correctly for the default (US) locale.
Table 7.8 summarizes the factory methods for text formatters in the java.text package.
| Factory Method |
|---|
| DateFormat.getDateInstance() |
| DateFormat.getDateInstance(int style) |
| DateFormat.getDateInstance(int style, Locale aLocale) |
| DateFormat.getDateTimeInstance() |
| DateFormat.getDateTimeInstance(int dateStyle, int timeStyle) |
| DateFormat.getDateTimeInstance(int dateStyle, int timeStyle, Locale aLocale) |
| DateFormat.getInstance() |
| DateFormat.getTimeInstance() |
| DateFormat.getTimeInstance(int style) |
| DateFormat.getTimeInstance(int style, Locale aLocale) |
| NumberFormat.getCurrencyInstance() |
| NumberFormat.getCurrencyInstance(Locale inLocale) |
| NumberFormat.getInstance() |
| NumberFormat.getInstance(Locale inLocale) |
| NumberFormat.getNumberInstance() |
| NumberFormat.getNumberInstance(Locale inLocale) |
| NumberFormat.getPercentInstance() |
| NumberFormat.getPercentInstance(Locale inLocale) |
Thus far we've seen how to format dates and numbers as text. Now we'll take a look at a class, ChoiceFormat, that maps numerical ranges to text. ChoiceFormat is constructed by specifying the numerical ranges and the strings that correspond to them. One constructor accepts an array of doubles and an array of Strings, where each string corresponds to the range running from the matching number up through the next number:
double[] limits = {0, 20, 40};
String[] labels = {"young", "less young", "old"};
ChoiceFormat cf = new ChoiceFormat(limits, labels);
System.out.println(cf.format(12)); // young
System.out.println(cf.format(26)); // less young
You can specify both the limits and the labels using a special string in another ChoiceFormat constructor:
ChoiceFormat cf = new ChoiceFormat("0#young|20#less young|40#old");
System.out.println(cf.format(40)); // old
System.out.println(cf.format(50)); // old
The limit and value pairs are separated by pipe characters (|; also known as "vertical bar"), while the number sign serves to separate each limit from its corresponding value.
To complete our discussion of the formatting classes, we'll take a look at another class, MessageFormat, that helps you construct human-readable messages. To construct a MessageFormat, pass it a pattern string. A pattern string is a lot like the string you feed to printf() in C, although the syntax is different. Arguments are delineated by curly brackets, and may include information about how they should be formatted. Each argument consists of a number, an optional type, and an optional style. These are summarized in table Table 7.9.
| Type | Styles |
|---|---|
| choice | pattern |
| date | short, medium, long, full, pattern |
| number | integer, percent, currency, pattern |
| time | short, medium, long, full, pattern |
Let's use an example to clarify all of this.
MessageFormat mf = new MessageFormat("You have {0} messages.");
Object[] arguments = {"no"};
System.out.println(mf.format(arguments)); // You have no messages.
We start by constructing a MessageFormat object; the argument to the constructor is the pattern on which messages will be based. The special incantation {0} means "in this position, substitute element 0 from the array passed as an argument to the format() method." Thus, we construct a MessageFormat object with a pattern, which is a template on which messages are based. When we generate a message, by calling format(), we pass in values to fill the blank spaces in the template. In this case, we pass the array arguments[] to mf.format; this substitutes arguments[0], yielding the result "You have no messages."
Let's try this example again, except we'll show how to format a number and a date instead of a string argument.
MessageFormat mf = new MessageFormat(
"You have {0, number, integer} messages on {1, date, long}.");
Object[] arguments = {new Integer(93), new Date()};
System.out.println(mf.format(arguments));
// You have 93 messages on April 10, 1997.
In this example, we need to fill in two spaces in the template, and therefore need two elements in the arguments[] array. Element 0 must be a number, and is formatted as an integer. Element 1 must be a Date, and will be printed in the long format. When we call format(), the arguments[] array supplies these two values.
This is still sloppy. What if there is only one message? To make this grammatically correct, we can embed a ChoiceFormat-style pattern string in our MessageFormat pattern string.
MessageFormat mf = new MessageFormat(
"You have {0, number, integer} message{0, choice, 0#s|1#|2#s}.");
Object[] arguments = {new Integer(1)};
System.out.println(mf.format(arguments)); // You have 1 message.
In this case, we use element 0 of arguments[] twice: once to supply the number of messages, and once to provide input to the ChoiceFormat pattern. The pattern says to add an "s" if argument 0 has the value zero, or is two or more.
Finally, a few words on how to be clever. If you want to write international programs, you can use resource bundles to supply the strings for your MessageFormat objects. This way, you can automatically format messages that are in the appropriate language with dates and other language-dependent fields handled appropriately.
In this context, it's helpful to realize that messages don't need to read elements from the array in order. In English, you would say "Disk C has 123 files"; in some other language, you might say "123 files are on Disk C." You could implement both messages with the same set of arguments:
MessageFormat m1 = new MessageFormat(
"Disk {0} has {1, number, integer} files.");
MessageFormat m2 = new MessageFormat(
"{1, number, integer} files are on disk {0}.");
Object[] arguments = {"C", new Integer(123)};
In real life, you'd only use a single MessageFormat object, initialized with a string taken from a resource bundle.
|
|
Unless otherwise restricted, a Java application can read and write to the host filesystem with the same level of access as the user who runs the Java interpreter. Java applets and other kinds of networked applications can, of course, be restricted by the SecurityManager and cut off from these services. We'll discuss applet access at the end of this section. First, let's take a look at the tools for basic file access.
Working with files in Java is still somewhat problematic. The host filesystem lies outside of Java's virtual environment, in the real world, and can therefore still suffer from architecture and implementation differences. Java tries to mask some of these differences by providing information to help an application tailor itself to the local environment; I'll mention these areas as they occur.
The java.io.File class encapsulates access to information about a file or directory entry in the filesystem. It gets attribute information about a file, lists the entries in a directory, and performs basic filesystem operations like removing a file or making a directory. While the File object handles these tasks, it doesn't provide direct access for reading and writing file data; there are specialized streams for that purpose.
You can create an instance of File from a String pathname as follows:
File fooFile = new File( "/tmp/foo.txt" ); File barDir = new File( "/tmp/bar" );
You can also create a file with a relative path like:
File f = new File( "foo" );
In this case, Java works relative to the current directory of the Java interpreter. You can determine the current directory by checking the user.dir property in the System Properties list (System.getProperty('user.dir')).
An overloaded version of the File constructor lets you specify the directory path and filename as separate String objects:
File fooFile = new File( "/tmp", "foo.txt" );
With yet another variation, you can specify the directory with a File object and the filename with a String:
File tmpDir = new File( "/tmp" ); File fooFile = new File ( tmpDir, "foo.txt" );
None of the File constructors throw any exceptions. This means the object is created whether or not the file or directory actually exists; it isn't an error to create a File object for an nonexistent file. You can use the exists() method to find out whether the file or directory exists.
One of the reasons that working with files in Java is problematic is that pathnames are expected to follow the conventions of the local filesystem. Java's designers intend to provide an abstraction that deals with most system-dependent filename features, such as the file separator, path separator, device specifier, and root directory. Unfortunately, not all of these features are implemented in the current version.
On some systems, Java can compensate for differences such as the direction of the file separator slashes in the above string. For example, in the current implementation on Windows platforms, Java accepts paths with either forward slashes or backslashes. However, under Solaris, Java accepts only paths with forward slashes.
Your best bet is to make sure you follow the filename conventions of the host filesystem. If your application is just opening and saving files at the user's request, you should be able to handle that functionality with the java.awt.FileDialog class. This class encapsulates a graphical file-selection dialog box. The methods of the FileDialog take care of system-dependent filename features for you.
If your application needs to deal with files on its own behalf, however, things get a little more complicated. The File class contains a few static variables to make this task easier. File.separator defines a String that specifies the file separator on the local host (e.g., "/" on UNIX and Macintosh systems and "\" on Windows systems), while File.separatorChar provides the same information in character form. File.pathSeparator defines a String that separates items in a path (e.g., ":" on UNIX systems; ";" on Macintosh and Windows systems); File.pathSeparatorChar provides the information in character form.
You can use this system-dependent information in several ways. Probably the simplest way to localize pathnames is to pick a convention you use internally, say "/", and do a String replace to substitute for the localized separator character:
// We'll use forward slash as our standard
String path = "mail/1995/june/merle";
path = path.replace('/', File.separatorChar);
File mailbox = new File( path );
Alternately, you could work with the components of a pathname and built the local pathname when you need it:
String [] path = { "mail", "1995", "june", "merle" };
StringBuffer sb = new StringBuffer(path[0]);
for (int i=1; i< path.length; i++)
sb.append( File.separator + path[i] );
File mailbox = new File( sb.toString() );
One thing to remember is that Java interprets the backslash character (\) as an escape character when used in a String. To get a backslash in a String, you have to use " \\".
Once we have a valid File object, we can use it to ask for information about the file itself and to perform standard operations on it. A number of methods lets us ask certain questions about the File. For example, isFile() returns true if the File represents a file, while isDirectory() returns true if it's a directory. isAbsolute() indicates whether the File has an absolute or relative path specification.
The components of the File pathname are available through the following methods: getName(), getPath(), getAbsolutePath(), and getParent(). getName() returns a String for the filename without any directory information; getPath() returns the directory information without the filename. If the File has an absolute path specification, getAbsolutePath() returns that path. Otherwise it returns the relative path appended to the current working directory. getParent() returns the parent directory of the File.
Interestingly, the string returned by getPath() or getAbsolutePath() may not be the same, case-wise, as the actual name of the file. You can retrieve the case-correct version of the file's path using getCanonicalPath(). In Windows 95, for example, you can create a File object whose getAbsolutePath() is C:\Autoexec.bat but whose getCanonicalPath() is C:\AUTOEXEC.BAT.
We can get the modification time of a file or directory with lastModified(). This time value is not useful as an absolute time; you should use it only to compare two modification times. We can also get the size of the file in bytes with length(). Here's a fragment of code that prints some information about a file:
File fooFile = new File( "/tmp/boofa" ); String type = fooFile.isFile() ? "File " : "Directory "; String name = fooFile.getName(); long len = fooFile.length(); System.out.println(type + name + ", " + len + " bytes " );
If the File object corresponds to a directory, we can list the files in the directory with the list() method:
String [] files = fooFile.list();
list() returns an array of String objects that contains filenames. (You might expect that list() would return an Enumeration instead of an array, but it doesn't.)
If the File refers to a nonexistent directory, we can create the directory with mkdir() or mkdirs(). mkdir() creates a single directory; mkdirs() creates all of the directories in a File specification. Use renameTo() to rename a file or directory and delete() to delete a file or directory. Note that File doesn't provide a method to create a file; creation is handled with a FileOutputStream as we'll discuss in a moment.
Table 8.1 summarizes the methods provided by the File class.
| Method | Return type | Description |
|---|---|---|
| canRead() | boolean | Is the file (or directory) readable? |
| canWrite() | boolean | Is the file (or directory) writable? |
| delete() | boolean | Deletes the file (or directory) |
| exists() | boolean | Does the file (or directory) exist? |
| getAbsolutePath() | String | Returns the absolute path of the file (or directory) |
| getCanonicalPath() | String | Returns the absolute, case-correct path of the file (or directory) |
| getName() | String | Returns the name of the file (or directory) |
| getParent() | String | Returns the name of the parent directory of the file (or directory) |
| getPath() | String | Returns the path of the file (or directory) |
| isAbsolute() | boolean | Is the filename (or directory name) absolute? |
| isDirectory() | boolean | Is the item a directory? |
| isFile() | boolean | Is the item a file? |
| lastModified() | long | Returns the last modification time of the file (or directory) |
| length() | long | Returns the length of the file |
| list() | String [] | Returns a list of files in the directory |
| mkdir() | boolean | Creates the directory |
| mkdirs() | boolean | Creates all directories in the path |
| renameTo(File dest) | boolean | Renames the file (or directory) |
Java provides two specialized streams for reading and writing files in the filesystem: FileInputStream and FileOutputStream. These streams provide the basic InputStream and OutputStream functionality applied to reading and writing the contents of files. They can be combined with the filtered streams described earlier to work with files in the same way we do other stream communications.
Because FileInputStream is a subclass of InputStream, it inherits all standard InputStream functionality for reading the contents of a file. FileInputStream provides only a low-level interface to reading data, however, so you'll typically wrap another stream like a DataInputStream around the FileInputStream.
You can create a FileInputStream from a String pathname or a File object:
FileInputStream foois = new FileInputStream( fooFile ); FileInputStream passwdis = new FileInputStream( "/etc/passwd" );
When you create a FileInputStream, Java attempts to open the specified file. Thus, the FileInputStream constructors can throw a FileNotFoundException if the specified file doesn't exist, or an IOException if some other I/O error occurs. You should be sure to catch and handle these exceptions in your code. When the stream is first created, its available() method and the File object's length() method should return the same value. Be sure to call the close() method when you are done with the file.
To read characters from a file, you can wrap an InputStreamReader around a FileInputStream. If you want to use the default character encoding scheme, you can use the FileReader class instead, which is provided as a convenience. FileReader works just like FileInputStream, except that it reads characters instead of bytes and wraps a Reader instead of an InputStream.
The following class, ListIt, is a small utility that displays the contents of a file or directory to standard output:
import java.io.*;
class ListIt {
public static void main ( String args[] ) throws Exception {
File file = new File( args[0] );
if ( !file.exists() || !file.canRead() ) {
System.out.println( "Can't read " + file );
return;
}
if ( file.isDirectory() ) {
String [] files = file.list();
for (int i=0; i< files.length; i++)
System.out.println( files[i] );
}
else
try {
FileReader fr = new FileReader ( file );
BufferedReader in = new BufferedReader( fr );
String line;
while ((line = in.readLine()) != null)
System.out.println(line);
}
catch ( FileNotFoundException e ) {
System.out.println( "File Disappeared" );
}
} }
ListIt constructs a File object from its first command-line argument and tests the File to see if it exists and is readable. If the File is a directory, ListIt prints the names of the files in the directory. Otherwise, ListIt reads and prints the file.
FileOutputStream is a subclass of OutputStream, so it inherits all the standard OutputStream functionality for writing to a file. Just like FileInputStream though, FileOutputStream provides only a low-level interface to writing data. You'll typically wrap another stream like a DataOutputStream or a PrintStream around the FileOutputStream to provide higher-level functionality. You can create a FileOutputStream from a String pathname or a File object. Unlike FileInputStream however, the FileOutputStream constructors don't throw a FileNotFoundException. If the specified file doesn't exist, the FileOutputStream creates the file. The FileOutputStream constructors can throw an IOException if some other I/O error occurs, so you still need to handle this exception.
If the specified file does exist, the FileOutputStream opens it for writing. When you actually call a write() method, the new data overwrites the current contents of the file. If you need to append data to an existing file, you should use a different constructor that accepts an append flag, as shown here:
FileInputStream foois = new FileInputStream( fooFile ); FileInputStream passwdis = new FileInputStream( "/etc/passwd" );
Antoher alternative for appending files is to use a RandomAccessFile, as I'll discuss shortly.
To write characters (instead of bytes) to a file, you can wrap an OutputStreamWriter around a FileOutputStream. If you want to use the default character encoding scheme, you can use the FileWriter class instead, which is provided as a convenience. FileWriter works just like FileOutputStream, except that it writes characters instead of bytes and wraps a Writer instead of an OutputStream.
The following example reads a line of data from standard input and writes it to the file /tmp/foo.txt:
String s = new BufferedReader( new InputStreamReader( System.in ) ).readLine(); File out = new File( "/tmp/foo.txt" ); FileWriter fw = new FileWriter ( out ); PrintWriter pw = new PrintWriter( fw, true ) pw.println( s );
Notice how we have wrapped a PrintWriter around the FileWriter to facilitate writing the data. To be a good filesystem citizen, you need to call the close() method when you are done with the FileWriter.
The java.io.RandomAccessFile class provides the ability to read and write data from or to any specified location in a file. RandomAccessFile implements both the DataInput and DataOutput interfaces, so you can use it to read and write strings and Java primitive types. In other words, RandomAccessFile defines the same methods for reading and writing data as DataInputStream and DataOutputStream. However, because the class provides random, rather than sequential, access to file data, it's not a subclass of either InputStream or OutputStream.
You can create a RandomAccessFile from a String pathname or a File object. The constructor also takes a second String argument that specifies the mode of the file. Use "r" for a read-only file or "rw" for a read-write file. Here's how to create a simple database to keep track of user information:
try {
RandomAccessFile users = new RandomAccessFile( "Users", "rw" );
...
}
catch (IOException e) {
}
When you create a RandomAccessFile in read-only mode, Java tries to open the specified file. If the file doesn't exist, RandomAccessFile throws an IOException. If, however, you are creating a RandomAccessFile in read-write mode, the object creates the file if it doesn't exist. The constructor can still throw an IOException if some other I/O error occurs, so you still need to handle this exception.
After you have created a RandomAccessFile, call any of the normal reading and writing methods, just as you would with a DataInputStream or DataOutputStream. If you try to write to a read-only file, the write method throws an IOException.
What makes a RandomAccessFile special is the seek() method. This method takes a long value and uses it to set the location for reading and writing in the file. You can use the getFilePointer() method to get the current location. If you need to append data on the end of the file, use length() to determine that location. You can write or seek beyond the end of a file, but you can't read beyond the end of a file. The read methods throws a EOFException if you try to do this.
Here's an example of writing some data to our user database:
users.seek( userNum * RECORDSIZE ); users.writeUTF( userName ); users.writeInt( userID );
One caveat to notice with this example is that we need to be sure that the String length for userName, along with any data that comes after it, fits within the boundaries of the record size.
For security reasons, applets are not permitted to read and write to arbitrary places in the filesystem. The ability of an applet to read and write files, as with any kind of system resource, is under the control of a SecurityManager object. A SecurityManager is installed by the application that is running the applet, such as an applet viewer or Java-enabled Web browser. All filesystem access must first pass the scrutiny of the SecurityManager. With that in mind, applet-viewer applications are free to implement their own schemes for what, if any, access an applet may have.
For example, Sun's HotJava Web browser allows applets to have access to specific files designated by the user in an access-control list. Netscape Navigator, on the other hand, currently doesn't allow applets any access to the filesystem.
It isn't unusual to want an applet to maintain some kind of state information on the system where it's running. But for a Java applet that is restricted from access to the local filesystem, the only option is to store data over the network on its server. Although, at the moment, the Web is a relatively static, read-only environment, applets have at their disposal powerful, general means for communicating data over networks, as you'll see in Chapter 9, Network Programming. The only limitation is that, by convention, an applet's network communication is restricted to the server that launched it. This limits the options for where the data will reside.
The only means of writing data to a server in Java is through a network socket. In Chapter 9, Network Programming we'll look at building networked applications with sockets in detail. With the tools of that chapter it's possible to build powerful client/server applications.
Unless otherwise restricted, a Java application can read and write to the host filesystem with the same level of access as the user who runs the Java interpreter. Java applets and other kinds of networked applications can, of course, be restricted by the SecurityManager and cut off from these services. We'll discuss applet access at the end of this section. First, let's take a look at the tools for basic file access.
Working with files in Java is still somewhat problematic. The host filesystem lies outside of Java's virtual environment, in the real world, and can therefore still suffer from architecture and implementation differences. Java tries to mask some of these differences by providing information to help an application tailor itself to the local environment; I'll mention these areas as they occur.
The java.io.File class encapsulates access to information about a file or directory entry in the filesystem. It gets attribute information about a file, lists the entries in a directory, and performs basic filesystem operations like removing a file or making a directory. While the File object handles these tasks, it doesn't provide direct access for reading and writing file data; there are specialized streams for that purpose.
You can create an instance of File from a String pathname as follows:
File fooFile = new File( "/tmp/foo.txt" ); File barDir = new File( "/tmp/bar" );
You can also create a file with a relative path like:
File f = new File( "foo" );
In this case, Java works relative to the current directory of the Java interpreter. You can determine the current directory by checking the user.dir property in the System Properties list (System.getProperty('user.dir')).
An overloaded version of the File constructor lets you specify the directory path and filename as separate String objects:
File fooFile = new File( "/tmp", "foo.txt" );
With yet another variation, you can specify the directory with a File object and the filename with a String:
File tmpDir = new File( "/tmp" ); File fooFile = new File ( tmpDir, "foo.txt" );
None of the File constructors throw any exceptions. This means the object is created whether or not the file or directory actually exists; it isn't an error to create a File object for an nonexistent file. You can use the exists() method to find out whether the file or directory exists.
One of the reasons that working with files in Java is problematic is that pathnames are expected to follow the conventions of the local filesystem. Java's designers intend to provide an abstraction that deals with most system-dependent filename features, such as the file separator, path separator, device specifier, and root directory. Unfortunately, not all of these features are implemented in the current version.
On some systems, Java can compensate for differences such as the direction of the file separator slashes in the above string. For example, in the current implementation on Windows platforms, Java accepts paths with either forward slashes or backslashes. However, under Solaris, Java accepts only paths with forward slashes.
Your best bet is to make sure you follow the filename conventions of the host filesystem. If your application is just opening and saving files at the user's request, you should be able to handle that functionality with the java.awt.FileDialog class. This class encapsulates a graphical file-selection dialog box. The methods of the FileDialog take care of system-dependent filename features for you.
If your application needs to deal with files on its own behalf, however, things get a little more complicated. The File class contains a few static variables to make this task easier. File.separator defines a String that specifies the file separator on the local host (e.g., "/" on UNIX and Macintosh systems and "\" on Windows systems), while File.separatorChar provides the same information in character form. File.pathSeparator defines a String that separates items in a path (e.g., ":" on UNIX systems; ";" on Macintosh and Windows systems); File.pathSeparatorChar provides the information in character form.
You can use this system-dependent information in several ways. Probably the simplest way to localize pathnames is to pick a convention you use internally, say "/", and do a String replace to substitute for the localized separator character:
// We'll use forward slash as our standard
String path = "mail/1995/june/merle";
path = path.replace('/', File.separatorChar);
File mailbox = new File( path );
Alternately, you could work with the components of a pathname and built the local pathname when you need it:
String [] path = { "mail", "1995", "june", "merle" };
StringBuffer sb = new StringBuffer(path[0]);
for (int i=1; i< path.length; i++)
sb.append( File.separator + path[i] );
File mailbox = new File( sb.toString() );
One thing to remember is that Java interprets the backslash character (\) as an escape character when used in a String. To get a backslash in a String, you have to use " \\".
Once we have a valid File object, we can use it to ask for information about the file itself and to perform standard operations on it. A number of methods lets us ask certain questions about the File. For example, isFile() returns true if the File represents a file, while isDirectory() returns true if it's a directory. isAbsolute() indicates whether the File has an absolute or relative path specification.
The components of the File pathname are available through the following methods: getName(), getPath(), getAbsolutePath(), and getParent(). getName() returns a String for the filename without any directory information; getPath() returns the directory information without the filename. If the File has an absolute path specification, getAbsolutePath() returns that path. Otherwise it returns the relative path appended to the current working directory. getParent() returns the parent directory of the File.
Interestingly, the string returned by getPath() or getAbsolutePath() may not be the same, case-wise, as the actual name of the file. You can retrieve the case-correct version of the file's path using getCanonicalPath(). In Windows 95, for example, you can create a File object whose getAbsolutePath() is C:\Autoexec.bat but whose getCanonicalPath() is C:\AUTOEXEC.BAT.
We can get the modification time of a file or directory with lastModified(). This time value is not useful as an absolute time; you should use it only to compare two modification times. We can also get the size of the file in bytes with length(). Here's a fragment of code that prints some information about a file:
File fooFile = new File( "/tmp/boofa" ); String type = fooFile.isFile() ? "File " : "Directory "; String name = fooFile.getName(); long len = fooFile.length(); System.out.println(type + name + ", " + len + " bytes " );
If the File object corresponds to a directory, we can list the files in the directory with the list() method:
String [] files = fooFile.list();
list() returns an array of String objects that contains filenames. (You might expect that list() would return an Enumeration instead of an array, but it doesn't.)
If the File refers to a nonexistent directory, we can create the directory with mkdir() or mkdirs(). mkdir() creates a single directory; mkdirs() creates all of the directories in a File specification. Use renameTo() to rename a file or directory and delete() to delete a file or directory. Note that File doesn't provide a method to create a file; creation is handled with a FileOutputStream as we'll discuss in a moment.
Table 8.1 summarizes the methods provided by the File class.
| Method | Return type | Description |
|---|---|---|
| canRead() | boolean | Is the file (or directory) readable? |
| canWrite() | boolean | Is the file (or directory) writable? |
| delete() | boolean | Deletes the file (or directory) |
| exists() | boolean | Does the file (or directory) exist? |
| getAbsolutePath() | String | Returns the absolute path of the file (or directory) |
| getCanonicalPath() | String | Returns the absolute, case-correct path of the file (or directory) |
| getName() | String | Returns the name of the file (or directory) |
| getParent() | String | Returns the name of the parent directory of the file (or directory) |
| getPath() | String | Returns the path of the file (or directory) |
| isAbsolute() | boolean | Is the filename (or directory name) absolute? |
| isDirectory() | boolean | Is the item a directory? |
| isFile() | boolean | Is the item a file? |
| lastModified() | long | Returns the last modification time of the file (or directory) |
| length() | long | Returns the length of the file |
| list() | String [] | Returns a list of files in the directory |
| mkdir() | boolean | Creates the directory |
| mkdirs() | boolean | Creates all directories in the path |
| renameTo(File dest) | boolean | Renames the file (or directory) |
Java provides two specialized streams for reading and writing files in the filesystem: FileInputStream and FileOutputStream. These streams provide the basic InputStream and OutputStream functionality applied to reading and writing the contents of files. They can be combined with the filtered streams described earlier to work with files in the same way we do other stream communications.
Because FileInputStream is a subclass of InputStream, it inherits all standard InputStream functionality for reading the contents of a file. FileInputStream provides only a low-level interface to reading data, however, so you'll typically wrap another stream like a DataInputStream around the FileInputStream.
You can create a FileInputStream from a String pathname or a File object:
FileInputStream foois = new FileInputStream( fooFile ); FileInputStream passwdis = new FileInputStream( "/etc/passwd" );
When you create a FileInputStream, Java attempts to open the specified file. Thus, the FileInputStream constructors can throw a FileNotFoundException if the specified file doesn't exist, or an IOException if some other I/O error occurs. You should be sure to catch and handle these exceptions in your code. When the stream is first created, its available() method and the File object's length() method should return the same value. Be sure to call the close() method when you are done with the file.
To read characters from a file, you can wrap an InputStreamReader around a FileInputStream. If you want to use the default character encoding scheme, you can use the FileReader class instead, which is provided as a convenience. FileReader works just like FileInputStream, except that it reads characters instead of bytes and wraps a Reader instead of an InputStream.
The following class, ListIt, is a small utility that displays the contents of a file or directory to standard output:
import java.io.*;
class ListIt {
public static void main ( String args[] ) throws Exception {
File file = new File( args[0] );
if ( !file.exists() || !file.canRead() ) {
System.out.println( "Can't read " + file );
return;
}
if ( file.isDirectory() ) {
String [] files = file.list();
for (int i=0; i< files.length; i++)
System.out.println( files[i] );
}
else
try {
FileReader fr = new FileReader ( file );
BufferedReader in = new BufferedReader( fr );
String line;
while ((line = in.readLine()) != null)
System.out.println(line);
}
catch ( FileNotFoundException e ) {
System.out.println( "File Disappeared" );
}
} }
ListIt constructs a File object from its first command-line argument and tests the File to see if it exists and is readable. If the File is a directory, ListIt prints the names of the files in the directory. Otherwise, ListIt reads and prints the file.
FileOutputStream is a subclass of OutputStream, so it inherits all the standard OutputStream functionality for writing to a file. Just like FileInputStream though, FileOutputStream provides only a low-level interface to writing data. You'll typically wrap another stream like a DataOutputStream or a PrintStream around the FileOutputStream to provide higher-level functionality. You can create a FileOutputStream from a String pathname or a File object. Unlike FileInputStream however, the FileOutputStream constructors don't throw a FileNotFoundException. If the specified file doesn't exist, the FileOutputStream creates the file. The FileOutputStream constructors can throw an IOException if some other I/O error occurs, so you still need to handle this exception.
If the specified file does exist, the FileOutputStream opens it for writing. When you actually call a write() method, the new data overwrites the current contents of the file. If you need to append data to an existing file, you should use a different constructor that accepts an append flag, as shown here:
FileInputStream foois = new FileInputStream( fooFile ); FileInputStream passwdis = new FileInputStream( "/etc/passwd" );
Antoher alternative for appending files is to use a RandomAccessFile, as I'll discuss shortly.
To write characters (instead of bytes) to a file, you can wrap an OutputStreamWriter around a FileOutputStream. If you want to use the default character encoding scheme, you can use the FileWriter class instead, which is provided as a convenience. FileWriter works just like FileOutputStream, except that it writes characters instead of bytes and wraps a Writer instead of an OutputStream.
The following example reads a line of data from standard input and writes it to the file /tmp/foo.txt:
String s = new BufferedReader( new InputStreamReader( System.in ) ).readLine(); File out = new File( "/tmp/foo.txt" ); FileWriter fw = new FileWriter ( out ); PrintWriter pw = new PrintWriter( fw, true ) pw.println( s );
Notice how we have wrapped a PrintWriter around the FileWriter to facilitate writing the data. To be a good filesystem citizen, you need to call the close() method when you are done with the FileWriter.
The java.io.RandomAccessFile class provides the ability to read and write data from or to any specified location in a file. RandomAccessFile implements both the DataInput and DataOutput interfaces, so you can use it to read and write strings and Java primitive types. In other words, RandomAccessFile defines the same methods for reading and writing data as DataInputStream and DataOutputStream. However, because the class provides random, rather than sequential, access to file data, it's not a subclass of either InputStream or OutputStream.
You can create a RandomAccessFile from a String pathname or a File object. The constructor also takes a second String argument that specifies the mode of the file. Use "r" for a read-only file or "rw" for a read-write file. Here's how to create a simple database to keep track of user information:
try {
RandomAccessFile users = new RandomAccessFile( "Users", "rw" );
...
}
catch (IOException e) {
}
When you create a RandomAccessFile in read-only mode, Java tries to open the specified file. If the file doesn't exist, RandomAccessFile throws an IOException. If, however, you are creating a RandomAccessFile in read-write mode, the object creates the file if it doesn't exist. The constructor can still throw an IOException if some other I/O error occurs, so you still need to handle this exception.
After you have created a RandomAccessFile, call any of the normal reading and writing methods, just as you would with a DataInputStream or DataOutputStream. If you try to write to a read-only file, the write method throws an IOException.
What makes a RandomAccessFile special is the seek() method. This method takes a long value and uses it to set the location for reading and writing in the file. You can use the getFilePointer() method to get the current location. If you need to append data on the end of the file, use length() to determine that location. You can write or seek beyond the end of a file, but you can't read beyond the end of a file. The read methods throws a EOFException if you try to do this.
Here's an example of writing some data to our user database:
users.seek( userNum * RECORDSIZE ); users.writeUTF( userName ); users.writeInt( userID );
One caveat to notice with this example is that we need to be sure that the String length for userName, along with any data that comes after it, fits within the boundaries of the record size.
For security reasons, applets are not permitted to read and write to arbitrary places in the filesystem. The ability of an applet to read and write files, as with any kind of system resource, is under the control of a SecurityManager object. A SecurityManager is installed by the application that is running the applet, such as an applet viewer or Java-enabled Web browser. All filesystem access must first pass the scrutiny of the SecurityManager. With that in mind, applet-viewer applications are free to implement their own schemes for what, if any, access an applet may have.
For example, Sun's HotJava Web browser allows applets to have access to specific files designated by the user in an access-control list. Netscape Navigator, on the other hand, currently doesn't allow applets any access to the filesystem.
It isn't unusual to want an applet to maintain some kind of state information on the system where it's running. But for a Java applet that is restricted from access to the local filesystem, the only option is to store data over the network on its server. Although, at the moment, the Web is a relatively static, read-only environment, applets have at their disposal powerful, general means for communicating data over networks, as you'll see in Chapter 9, Network Programming. The only limitation is that, by convention, an applet's network communication is restricted to the server that launched it. This limits the options for where the data will reside.
The only means of writing data to a server in Java is through a network socket. In Chapter 9, Network Programming we'll look at building networked applications with sockets in detail. With the tools of that chapter it's possible to build powerful client/server applications.
Using streams and files, you can write an application that saves and loads its data to a disk drive. Java 1.1 provides an even more powerful mechanism called object serialization that does a lot of the work for you. In its simplest form, object serialization is an automatic way to save and load an object. However, object serialization has depths that we cannot plumb within the scope of this book, including complete control over the serialization process and interesting conundrums like class versioning.
Basically, any class that implements the Serializable interface can be saved and restored from a stream. Special stream subclasses, ObjectInputStream and ObjectOutputStream, are used to serialize primitive types and objects. Subclasses of Serializable classes are also serializable. The default serialization mechanism saves the value of an object's nonstatic and nonvolatile member variables.
One of the tricky things about serialization is that when an object is serialized, any object references it contains should also be serialized. We'll see this in an upcoming example. The implication is that any object we serialize must only contain references to Serializable objects. There are ways around this problem, like marking nonserializable members as volatile or overriding the default serialization mechanisms.
In the following example, we create a Hashtable and write it to a disk file called h.ser.
import java.io.*;
import java.util.*;
public class Save {
public static void main(String[] args) {
Hashtable h = new Hashtable();
h.put("string", "Gabriel Garcia Marquez");
h.put("int", new Integer(26));
h.put("double", new Double(Math.PI));
try {
FileOutputStream fileOut = new FileOutputStream("h.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(h);
}
catch (Exception e) {
System.out.println(e);
}
}
}
First we construct a Hashtable with a few elements in it. Then, in the three lines of code inside the try block, we write the Hashtable to a file called h.ser, using the writeObject() method of ObjectOutputStream. The ObjectOutputStream class is a lot like the DataOutputStream class, except that it includes the powerful writeObject() method. The Hashtable object is serializable because it implements the Serializable interface.
The Hashtable we created has internal references to the items it contains. Thus, these components are automatically serialized along with the Hashtable. We'll see this in the next example when we deserialize the Hashtable.
import java.io.*;
import java.util.*;
public class Load {
public static void main(String[] args) {
try {
FileInputStream fileIn = new FileInputStream("h.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
Hashtable h = (Hashtable)in.readObject();
System.out.println(h.toString());
}
catch (Exception e) {
System.out.println(e);
}
}
}
In this example, we read the Hashtable from the h.ser file, using the readObject() method of ObjectInputStream. The ObjectInputStream class is a lot like DataInputStream, except it includes the readObject() method. The return type of readObject() is Object, so we need to cast it to a Hashtable. Finally, we print out the contents of the Hashtable using its toString() method.
Java 1.1 includes a new package, java.util.zip, that contains classes you can use for data compression. In this section we'll talk about how to use the classes. We'll also present two useful example programs that build on what you have just learned about streams and files.
The classes in the java.util.zip package support two widespread compression formats: GZIP and ZIP. Both of these are based on the ZLIB compression algorithm, which is discussed in RFC 1950, RFC 1951, and RFC 1952. These documents are available at ftp://ds.internic.net/rfc/. I don't recommend reading these documents unless you want to implement your own compression algorithm or otherwise extend the functionality of the java.util.zip package.
The java.util.zip class provides two FilterOutputStream subclasses to write compressed data to a stream. To write compressed data in the GZIP format, simply wrap a GZIPOutputStream around an underlying stream and write to it. The following is a complete example that shows how to compress a file using the GZIP format.
import java.io.*;
import java.util.zip.*;
public class GZip {
public static int sChunk = 8192;
public static void main(String[] args) {
if (args.length != 1) {
System.out.println("Usage: GZip source");
return;
}
// Create output stream.
String zipname = args[0] + ".gz";
GZIPOutputStream zipout;
try {
FileOutputStream out = new FileOutputStream(zipname);
zipout = new GZIPOutputStream(out);
}
catch (IOException e) {
System.out.println("Couldn't create " + zipname + ".");
return;
}
byte[] buffer = new byte[sChunk];
// Compress the file.
try {
FileInputStream in = new FileInputStream(args[0]);
int length;
while ((length = in.read(buffer, 0, sChunk)) != -1)
zipout.write(buffer, 0, length);
in.close();
}
catch (IOException e) {
System.out.println("Couldn't compress " + args[0] + ".");
}
try { zipout.close(); }
catch (IOException e) {}
}
}
First we check to make sure we have a command-line argument representing a file name. Then we construct a GZIPOutputStream wrapped around a FileOutputStream representing the given file name with the .gz suffix appended. With this in place, we open the source file. We read chunks of data from it and write them into the GZIPOutputStream. Finally, we clean up by closing our open streams.
Writing data to a ZIP file is a little more involved, but still quite manageable. While a GZIP file contains only one compressed file, a ZIP file is actually an archive of files, some (or all) of which may be compressed. Each item in the ZIP file is represented by a ZipEntry object. When writing to a ZipOutputStream, you'll need to call putNextEntry() before writing the data for each item. The following example shows how to create a ZipOutputStream. You'll notice it's just like creating a GZIPOutputStream.
ZipOutputStream zipout;
try {
FileOutputStream out = new FileOutputStream("archive.zip");
zipout = new ZipOutputStream(out);
}
catch (IOException e) {}
Let's say we have two files we want to write into this archive. Before we begin writing we need to call putNextEntry(). We'll create a simple entry with just a name. There are other fields in ZipEntry that you can set, but most of the time you won't need to bother with them.
try {
ZipEntry entry = new ZipEntry("First");
zipout.putNextEntry(entry);
}
catch (IOException e) {}
At this point you can write the contents of the first file into the archive. When you're ready to write the second file into the archive, you simply call putNextEntry() again:
try {
ZipEntry entry = new ZipEntry("Second");
zipout.putNextEntry(entry);
}
catch (IOException e) {}
To decompress data, you can use one of the two FilterInputStream subclasses provided in java.util.zip. To decompress data in the GZIP format, simply wrap a GZIPInputStream around an underlying stream and read from it. The following is a complete example that shows how to decompress a GZIP file.
import java.io.*;
import java.util.zip.*;
public class GUnzip {
public static int sChunk = 8192;
public static void main(String[] args) {
if (args.length != 1) {
System.out.println("Usage: GUnzip source");
return;
}
// Create input stream.
String zipname, source;
if (args[0].endsWith(".gz")) {
zipname = args[0];
source = args[0].substring(0, args[0].length() - 3);
}
else {
zipname = args[0] + ".gz";
source = args[0];
}
GZIPInputStream zipin;
try {
FileInputStream in = new FileInputStream(zipname);
zipin = new GZIPInputStream(in);
}
catch (IOException e) {
System.out.println("Couldn't open " + zipname + ".");
return;
}
byte[] buffer = new byte[sChunk];
// Decompress the file.
try {
FileOutputStream out = new FileOutputStream(source);
int length;
while ((length = zipin.read(buffer, 0, sChunk)) != -1)
out.write(buffer, 0, length);
out.close();
}
catch (IOException e) {
System.out.println("Couldn't decompress " + args[0] + ".");
}
try { zipin.close(); }
catch (IOException e) {}
}
}
First we check to make sure we have a command-line argument representing a file name. If the argument ends with .gz, we figure out what the file name for the uncompressed file should be. Otherwise we just use the given argument and assume the compressed file has the .gz suffix. Then we construct a GZIPInputStream wrapped around a FileInputStream representing the compressed file. With this in place, we open the target file. We read chunks of data from the GZIPInputStream and write them into the target file. Finally, we clean up by closing our open streams.
Again, the ZIP archive presents a little more complexity than the GZIP file. When reading from a ZipInputStream, you should call getNextEntry() before reading each item. When getNextEntry() returns null, there are no more items to read. The following example shows how to create a ZipInputStream. You'll notice it's just like creating a GZIPInputStream.
ZipInputStream zipin;
try {
FileInputStream in = new FileInputStream("archive.zip");
zipin = new ZipInputStream(in);
}
catch (IOException e) {}
Suppose we want to read two files from this archive. Before we begin reading we need to call getNextEntry(). At the least, the entry will give us a name of the item we are reading from the archive.
try {
ZipEntry first = zipin.getNextEntry();
}
catch (IOException e) {}
At this point you can read the contents of the first item in the archive. When you come to the end of the item, the read() method will return -1. Now you can call getNextEntry() again to read the second item from the archive.
try {
ZipEntry second = zipin.getNextEntry();
}
catch (IOException e) {}
If you call getNextEntry() and it returns null, then there are no more items and you have reached the end of the archive.
|
|
|