ZeePedia

Run-time Type Identification:The need for RTTI, A class method extractor

<< The Java I/O System:The File class, Compression, Object serialization, Tokenizing input
Creating Windows & Applets:Applet restrictions, Running applets from the command line >>
img
// Now get them back:
ObjectInputStream in1 =
new ObjectInputStream(
new ByteArrayInputStream(
buf1.toByteArray()));
ObjectInputStream in2 =
new ObjectInputStream(
new ByteArrayInputStream(
buf2.toByteArray()));
ArrayList animals1 =
(ArrayList)in1.readObject();
ArrayList animals2 =
(ArrayList)in1.readObject();
ArrayList animals3 =
(ArrayList)in2.readObject();
System.out.println("animals1: " + animals1);
System.out.println("animals2: " + animals2);
System.out.println("animals3: " + animals3);
}
} ///:~
One thing that's interesting here is that it's possible to use object
serialization to and from a byte array as a way of doing a "deep copy" of
any object that's Serializable. (A deep copy means that you're
duplicating the entire web of objects, rather than just the basic object and
its references.) Copying is covered in depth in Appendix A.
Animal objects contain fields of type House. In main( ), an ArrayList
of these Animals is created and it is serialized twice to one stream and
then again to a separate stream. When these are deserialized and printed,
you see the following results for one run (the objects will be in different
memory locations each run):
animals: [Bosco the dog[Animal@1cc76c], House@1cc769
, Ralph the hamster[Animal@1cc76d], House@1cc769
, Fronk the cat[Animal@1cc76e], House@1cc769
]
animals1: [Bosco the dog[Animal@1cca0c], House@1cca16
, Ralph the hamster[Animal@1cca17], House@1cca16
, Fronk the cat[Animal@1cca1b], House@1cca16
]
632
Thinking in Java
img
animals2: [Bosco the dog[Animal@1cca0c], House@1cca16
, Ralph the hamster[Animal@1cca17], House@1cca16
, Fronk the cat[Animal@1cca1b], House@1cca16
]
animals3: [Bosco the dog[Animal@1cca52], House@1cca5c
, Ralph the hamster[Animal@1cca5d], House@1cca5c
, Fronk the cat[Animal@1cca61], House@1cca5c
]
Of course you expect that the deserialized objects have different addresses
from their originals. But notice that in animals1 and animals2 the same
addresses appear, including the references to the House object that both
share. On the other hand, when animals3 is recovered the system has no
way of knowing that the objects in this other stream are aliases of the
objects in the first stream, so it makes a completely different web of
objects.
As long as you're serializing everything to a single stream, you'll be able to
recover the same web of objects that you wrote, with no accidental
duplication of objects. Of course, you can change the state of your objects
in between the time you write the first and the last, but that's your
responsibility--the objects will be written in whatever state they are in
(and with whatever connections they have to other objects) at the time
you serialize them.
The safest thing to do if you want to save the state of a system is to
serialize as an "atomic" operation. If you serialize some things, do some
other work, and serialize some more, etc., then you will not be storing the
system safely. Instead, put all the objects that comprise the state of your
system in a single container and simply write that container out in one
operation. Then you can restore it with a single method call as well.
The following example is an imaginary computer-aided design (CAD)
system that demonstrates the approach. In addition, it throws in the issue
of static fields--if you look at the documentation you'll see that Class is
Serializable, so it should be easy to store the static fields by simply
serializing the Class object. That seems like a sensible approach, anyway.
//: c11:CADState.java
// Saving and restoring the state of a
// pretend CAD system.
Chapter 11: The Java I/O System
633
img
import java.io.*;
import java.util.*;
abstract class Shape implements Serializable {
public static final int
RED = 1, BLUE = 2, GREEN = 3;
private int xPos, yPos, dimension;
private static Random r = new Random();
private static int counter = 0;
abstract public void setColor(int newColor);
abstract public int getColor();
public Shape(int xVal, int yVal, int dim) {
xPos = xVal;
yPos = yVal;
dimension = dim;
}
public String toString() {
return getClass() +
" color[" + getColor() +
"] xPos[" + xPos +
"] yPos[" + yPos +
"] dim[" + dimension + "]\n";
}
public static Shape randomFactory() {
int xVal = r.nextInt() % 100;
int yVal = r.nextInt() % 100;
int dim = r.nextInt() % 100;
switch(counter++ % 3) {
default:
case 0: return new Circle(xVal, yVal, dim);
case 1: return new Square(xVal, yVal, dim);
case 2: return new Line(xVal, yVal, dim);
}
}
}
class Circle extends Shape {
private static int color = RED;
public Circle(int xVal, int yVal, int dim) {
super(xVal, yVal, dim);
}
634
Thinking in Java
img
public void setColor(int newColor) {
color = newColor;
}
public int getColor() {
return color;
}
}
class Square extends Shape {
private static int color;
public Square(int xVal, int yVal, int dim) {
super(xVal, yVal, dim);
color = RED;
}
public void setColor(int newColor) {
color = newColor;
}
public int getColor() {
return color;
}
}
class Line extends Shape {
private static int color = RED;
public static void
serializeStaticState(ObjectOutputStream os)
throws IOException {
os.writeInt(color);
}
public static void
deserializeStaticState(ObjectInputStream os)
throws IOException {
color = os.readInt();
}
public Line(int xVal, int yVal, int dim) {
super(xVal, yVal, dim);
}
public void setColor(int newColor) {
color = newColor;
}
public int getColor() {
Chapter 11: The Java I/O System
635
img
return color;
}
}
public class CADState {
public static void main(String[] args)
throws Exception {
ArrayList shapeTypes, shapes;
if(args.length == 0) {
shapeTypes = new ArrayList();
shapes = new ArrayList();
// Add references to the class objects:
shapeTypes.add(Circle.class);
shapeTypes.add(Square.class);
shapeTypes.add(Line.class);
// Make some shapes:
for(int i = 0; i < 10; i++)
shapes.add(Shape.randomFactory());
// Set all the static colors to GREEN:
for(int i = 0; i < 10; i++)
((Shape)shapes.get(i))
.setColor(Shape.GREEN);
// Save the state vector:
ObjectOutputStream out =
new ObjectOutputStream(
new FileOutputStream("CADState.out"));
out.writeObject(shapeTypes);
Line.serializeStaticState(out);
out.writeObject(shapes);
} else { // There's a command-line argument
ObjectInputStream in =
new ObjectInputStream(
new FileInputStream(args[0]));
// Read in the same order they were written:
shapeTypes = (ArrayList)in.readObject();
Line.deserializeStaticState(in);
shapes = (ArrayList)in.readObject();
}
// Display the shapes:
System.out.println(shapes);
}
636
Thinking in Java
img
} ///:~
The Shape class implements Serializable, so anything that is
inherited from Shape is automatically Serializable as well. Each Shape
contains data, and each derived Shape class contains a static field that
determines the color of all of those types of Shapes. (Placing a static
field in the base class would result in only one field, since static fields are
not duplicated in derived classes.) Methods in the base class can be
overridden to set the color for the various types (static methods are not
dynamically bound, so these are normal methods). The
randomFactory( ) method creates a different Shape each time you call
it, using random values for the Shape data.
Circle and Square are straightforward extensions of Shape; the only
difference is that Circle initializes color at the point of definition and
Square initializes it in the constructor. We'll leave the discussion of Line
for later.
In main( ), one ArrayList is used to hold the Class objects and the
other to hold the shapes. If you don't provide a command line argument
the shapeTypes ArrayList is created and the Class objects are added,
and then the shapes ArrayList is created and Shape objects are added.
Next, all the static color values are set to GREEN, and everything is
serialized to the file CADState.out.
If you provide a command line argument (presumably CADState.out),
that file is opened and used to restore the state of the program. In both
situations, the resulting ArrayList of Shapes is printed. The results
from one run are:
>java CADState
[class Circle color[3] xPos[-51] yPos[-99] dim[38]
, class Square color[3] xPos[2] yPos[61] dim[-46]
, class Line color[3] xPos[51] yPos[73] dim[64]
, class Circle color[3] xPos[-70] yPos[1] dim[16]
, class Square color[3] xPos[3] yPos[94] dim[-36]
, class Line color[3] xPos[-84] yPos[-21] dim[-35]
, class Circle color[3] xPos[-75] yPos[-43] dim[22]
, class Square color[3] xPos[81] yPos[30] dim[-45]
, class Line color[3] xPos[-29] yPos[92] dim[17]
, class Circle color[3] xPos[17] yPos[90] dim[-76]
Chapter 11: The Java I/O System
637
img
]
>java CADState CADState.out
[class Circle color[1] xPos[-51] yPos[-99] dim[38]
, class Square color[0] xPos[2] yPos[61] dim[-46]
, class Line color[3] xPos[51] yPos[73] dim[64]
, class Circle color[1] xPos[-70] yPos[1] dim[16]
, class Square color[0] xPos[3] yPos[94] dim[-36]
, class Line color[3] xPos[-84] yPos[-21] dim[-35]
, class Circle color[1] xPos[-75] yPos[-43] dim[22]
, class Square color[0] xPos[81] yPos[30] dim[-45]
, class Line color[3] xPos[-29] yPos[92] dim[17]
, class Circle color[1] xPos[17] yPos[90] dim[-76]
]
You can see that the values of xPos, yPos, and dim were all stored and
recovered successfully, but there's something wrong with the retrieval of
the static information. It's all "3" going in, but it doesn't come out that
way. Circles have a value of 1 (RED, which is the definition), and
Squares have a value of 0 (remember, they are initialized in the
constructor). It's as if the statics didn't get serialized at all! That's right--
even though class Class is Serializable, it doesn't do what you expect.
So if you want to serialize statics, you must do it yourself.
This is what the serializeStaticState( ) and deserializeStaticState( )
static methods in Line are for. You can see that they are explicitly called
as part of the storage and retrieval process. (Note that the order of writing
to the serialize file and reading back from it must be maintained.) Thus to
make CADState.java run correctly you must:
1.
Add a serializeStaticState( ) and deserializeStaticState( ) to
the shapes.
2.
Remove the ArrayList shapeTypes and all code related to it.
3.
Add calls to the new serialize and deserialize static methods in the
shapes.
Another issue you might have to think about is security, since serialization
also saves private data. If you have a security issue, those fields should
be marked as transient. But then you have to design a secure way to
638
Thinking in Java
img
store that information so that when you do a restore you can reset those
private variables.
Tokenizing input
Tokenizing is the process of breaking a sequence of characters into a
sequence of "tokens," which are bits of text delimited by whatever you
choose. For example, your tokens could be words, and then they would be
delimited by white space and punctuation. There are two classes provided
in the standard Java library that can be used for tokenization:
StreamTokenizer and StringTokenizer.
StreamTokenizer
Although StreamTokenizer is not derived from InputStream or
OutputStream, it works only with InputStream objects, so it rightfully
belongs in the I/O portion of the library.
Consider a program to count the occurrence of words in a text file:
//: c11:WordCount.java
// Counts words from a file, outputs
// results in sorted form.
import java.io.*;
import java.util.*;
class Counter {
private int i = 1;
int read() { return i; }
void increment() { i++; }
}
public class WordCount {
private FileReader file;
private StreamTokenizer st;
// A TreeMap keeps keys in sorted order:
private TreeMap counts = new TreeMap();
WordCount(String filename)
throws FileNotFoundException {
try {
Chapter 11: The Java I/O System
639
img
file = new FileReader(filename);
st = new StreamTokenizer(
new BufferedReader(file));
st.ordinaryChar('.');
st.ordinaryChar('-');
} catch(FileNotFoundException e) {
System.err.println(
"Could not open " + filename);
throw e;
}
}
void cleanup() {
try {
file.close();
} catch(IOException e) {
System.err.println(
"file.close() unsuccessful");
}
}
void countWords() {
try {
while(st.nextToken() !=
StreamTokenizer.TT_EOF) {
String s;
switch(st.ttype) {
case StreamTokenizer.TT_EOL:
s = new String("EOL");
break;
case StreamTokenizer.TT_NUMBER:
s = Double.toString(st.nval);
break;
case StreamTokenizer.TT_WORD:
s = st.sval; // Already a String
break;
default: // single character in ttype
s = String.valueOf((char)st.ttype);
}
if(counts.containsKey(s))
((Counter)counts.get(s)).increment();
else
counts.put(s, new Counter());
640
Thinking in Java
img
}
} catch(IOException e) {
System.err.println(
"st.nextToken() unsuccessful");
}
}
Collection values() {
return counts.values();
}
Set keySet() { return counts.keySet(); }
Counter getCounter(String s) {
return (Counter)counts.get(s);
}
public static void main(String[] args)
throws FileNotFoundException {
WordCount wc =
new WordCount(args[0]);
wc.countWords();
Iterator keys = wc.keySet().iterator();
while(keys.hasNext()) {
String key = (String)keys.next();
System.out.println(key + ": "
+ wc.getCounter(key).read());
}
wc.cleanup();
}
} ///:~
Presenting the words in sorted form is easy to do by storing the data in a
TreeMap, which automatically organizes its keys in sorted order (see
Chapter 9). When you get a set of keys using keySet( ), they will also be
in sorted order.
To open the file, a FileReader is used, and to turn the file into words a
StreamTokenizer is created from the FileReader wrapped in a
BufferedReader. In StreamTokenizer, there is a default list of
separators, and you can add more with a set of methods. Here,
ordinaryChar( ) is used to say "This character has no significance that
I'm interested in," so the parser doesn't include it as part of any of the
words that it creates. For example, saying st.ordinaryChar('.') means
that periods will not be included as parts of the words that are parsed. You
Chapter 11: The Java I/O System
641
img
can find more information in the JDK HTML documentation from
java.sun.com.
In countWords( ), the tokens are pulled one at a time from the stream,
and the ttype information is used to determine what to do with each
token, since a token can be an end-of-line, a number, a string, or a single
character.
Once a token is found, the TreeMap counts is queried to see if it already
contains the token as a key. If it does, the corresponding Counter object
is incremented to indicate that another instance of this word has been
found. If not, a new Counter is created--since the Counter constructor
initializes its value to one, this also acts to count the word.
WordCount is not a type of TreeMap, so it wasn't inherited. It
performs a specific type of functionality, so even though the keys( ) and
values( ) methods must be reexposed, that still doesn't mean that
inheritance should be used since a number of TreeMap methods are
inappropriate here. In addition, other methods like getCounter( ),
which get the Counter for a particular String, and sortedKeys( ),
which produces an Iterator, finish the change in the shape of
WordCount's interface.
In main( ) you can see the use of a WordCount to open and count the
words in a file--it just takes two lines of code. Then an Iterator to a sorted
list of keys (words) is extracted, and this is used to pull out each key and
associated Count. The call to cleanup( ) is necessary to ensure that the
file is closed.
StringTokenizer
Although it isn't part of the I/O library, the StringTokenizer has
sufficiently similar functionality to StreamTokenizer that it will be
described here.
The StringTokenizer returns the tokens within a string one at a time.
These tokens are consecutive characters delimited by tabs, spaces, and
newlines. Thus, the tokens of the string "Where is my cat?" are "Where",
"is", "my", and "cat?" Like the StreamTokenizer, you can tell the
StringTokenizer to break up the input in any way that you want, but
642
Thinking in Java
img
with StringTokenizer you do this by passing a second argument to the
constructor, which is a String of the delimiters you wish to use. In
general, if you need more sophistication, use a StreamTokenizer.
You ask a StringTokenizer object for the next token in the string using
the nextToken( ) method, which either returns the token or an empty
string to indicate that no tokens remain.
As an example, the following program performs a limited analysis of a
sentence, looking for key phrase sequences to indicate whether happiness
or sadness is implied.
//: c11:AnalyzeSentence.java
// Look for particular sequences in sentences.
import java.util.*;
public class AnalyzeSentence {
public static void main(String[] args) {
analyze("I am happy about this");
analyze("I am not happy about this");
analyze("I am not! I am happy");
analyze("I am sad about this");
analyze("I am not sad about this");
analyze("I am not! I am sad");
analyze("Are you happy about this?");
analyze("Are you sad about this?");
analyze("It's you! I am happy");
analyze("It's you! I am sad");
}
static StringTokenizer st;
static void analyze(String s) {
prt("\nnew sentence >> " + s);
boolean sad = false;
st = new StringTokenizer(s);
while (st.hasMoreTokens()) {
String token = next();
// Look until you find one of the
// two starting tokens:
if(!token.equals("I") &&
!token.equals("Are"))
continue; // Top of while loop
Chapter 11: The Java I/O System
643
img
if(token.equals("I")) {
String tk2 = next();
if(!tk2.equals("am")) // Must be after I
break; // Out of while loop
else {
String tk3 = next();
if(tk3.equals("sad")) {
sad = true;
break; // Out of while loop
}
if (tk3.equals("not")) {
String tk4 = next();
if(tk4.equals("sad"))
break; // Leave sad false
if(tk4.equals("happy")) {
sad = true;
break;
}
}
}
}
if(token.equals("Are")) {
String tk2 = next();
if(!tk2.equals("you"))
break; // Must be after Are
String tk3 = next();
if(tk3.equals("sad"))
sad = true;
break; // Out of while loop
}
}
if(sad) prt("Sad detected");
}
static String next() {
if(st.hasMoreTokens()) {
String s = st.nextToken();
prt(s);
return s;
}
else
return "";
644
Thinking in Java
img
}
static void prt(String s) {
System.out.println(s);
}
} ///:~
For each string being analyzed, a while loop is entered and tokens are
pulled off the string. Notice the first if statement, which says to continue
(go back to the beginning of the loop and start again) if the token is
neither an "I" nor an "Are." This means that it will get tokens until an "I"
or an "Are" is found. You might think to use the == instead of the
equals( ) method, but that won't work correctly, since == compares
reference values while equals( ) compares contents.
The logic of the rest of the analyze( ) method is that the pattern that's
being searched for is "I am sad," "I am not happy," or "Are you sad?"
Without the break statement, the code for this would be even messier
than it is. You should be aware that a typical parser (this is a primitive
example of one) normally has a table of these tokens and a piece of code
that moves through the states in the table as new tokens are read.
You should think of the StringTokenizer only as shorthand for a simple
and specific kind of StreamTokenizer. However, if you have a String
that you want to tokenize and StringTokenizer is too limited, all you
have to do is turn it into a stream with StringBufferInputStream and
then use that to create a much more powerful StreamTokenizer.
Checking capitalization style
In this section we'll look at a more complete example of the use of Java
I/O, which also uses tokenization. This project is directly useful because it
performs a style check to make sure that your capitalization conforms to
the Java style as found at java.sun.com/docs/codeconv/index.html. It
opens each .java file in the current directory and extracts all the class
names and identifiers, then shows you if any of them don't meet the Java
style.
For the program to operate correctly, you must first build a class name
repository to hold all the class names in the standard Java library. You do
this by moving into all the source code subdirectories for the standard
Chapter 11: The Java I/O System
645
img
Java library and running ClassScanner in each subdirectory. Provide as
arguments the name of the repository file (using the same path and name
each time) and the -a command-line option to indicate that the class
names should be added to the repository.
To use the program to check your code, hand it the path and name of the
repository to use. It will check all the classes and identifiers in the current
directory and tell you which ones don't follow the typical Java
capitalization style.
You should be aware that the program isn't perfect; there are a few times
when it will point out what it thinks is a problem but on looking at the
code you'll see that nothing needs to be changed. This is a little annoying,
but it's still much easier than trying to find all these cases by staring at
your code.
//: c11:ClassScanner.java
// Scans all files in directory for classes
// and identifiers, to check capitalization.
// Assumes properly compiling code listings.
// Doesn't do everything right, but is a
// useful aid.
import java.io.*;
import java.util.*;
class MultiStringMap extends HashMap {
public void add(String key, String value) {
if(!containsKey(key))
put(key, new ArrayList());
((ArrayList)get(key)).add(value);
}
public ArrayList getArrayList(String key) {
if(!containsKey(key)) {
System.err.println(
"ERROR: can't find key: " + key);
System.exit(1);
}
return (ArrayList)get(key);
}
public void printValues(PrintStream p) {
Iterator k = keySet().iterator();
646
Thinking in Java
img
while(k.hasNext()) {
String oneKey = (String)k.next();
ArrayList val = getArrayList(oneKey);
for(int i = 0; i < val.size(); i++)
p.println((String)val.get(i));
}
}
}
public class ClassScanner {
private File path;
private String[] fileList;
private Properties classes = new Properties();
private MultiStringMap
classMap = new MultiStringMap(),
identMap = new MultiStringMap();
private StreamTokenizer in;
public ClassScanner() throws IOException {
path = new File(".");
fileList = path.list(new JavaFilter());
for(int i = 0; i < fileList.length; i++) {
System.out.println(fileList[i]);
try {
scanListing(fileList[i]);
} catch(FileNotFoundException e) {
System.err.println("Could not open " +
fileList[i]);
}
}
}
void scanListing(String fname)
throws IOException {
in = new StreamTokenizer(
new BufferedReader(
new FileReader(fname)));
// Doesn't seem to work:
// in.slashStarComments(true);
// in.slashSlashComments(true);
in.ordinaryChar('/');
in.ordinaryChar('.');
in.wordChars('_', '_');
Chapter 11: The Java I/O System
647
img
in.eolIsSignificant(true);
while(in.nextToken() !=
StreamTokenizer.TT_EOF) {
if(in.ttype == '/')
eatComments();
else if(in.ttype ==
StreamTokenizer.TT_WORD) {
if(in.sval.equals("class") ||
in.sval.equals("interface")) {
// Get class name:
while(in.nextToken() !=
StreamTokenizer.TT_EOF
&& in.ttype !=
StreamTokenizer.TT_WORD)
;
classes.put(in.sval, in.sval);
classMap.add(fname, in.sval);
}
if(in.sval.equals("import") ||
in.sval.equals("package"))
discardLine();
else // It's an identifier or keyword
identMap.add(fname, in.sval);
}
}
}
void discardLine() throws IOException {
while(in.nextToken() !=
StreamTokenizer.TT_EOF
&& in.ttype !=
StreamTokenizer.TT_EOL)
; // Throw away tokens to end of line
}
// StreamTokenizer's comment removal seemed
// to be broken. This extracts them:
void eatComments() throws IOException {
if(in.nextToken() !=
StreamTokenizer.TT_EOF) {
if(in.ttype == '/')
discardLine();
else if(in.ttype != '*')
648
Thinking in Java
img
in.pushBack();
else
while(true) {
if(in.nextToken() ==
StreamTokenizer.TT_EOF)
break;
if(in.ttype == '*')
if(in.nextToken() !=
StreamTokenizer.TT_EOF
&& in.ttype == '/')
break;
}
}
}
public String[] classNames() {
String[] result = new String[classes.size()];
Iterator e = classes.keySet().iterator();
int i = 0;
while(e.hasNext())
result[i++] = (String)e.next();
return result;
}
public void checkClassNames() {
Iterator files = classMap.keySet().iterator();
while(files.hasNext()) {
String file = (String)files.next();
ArrayList cls = classMap.getArrayList(file);
for(int i = 0; i < cls.size(); i++) {
String className = (String)cls.get(i);
if(Character.isLowerCase(
className.charAt(0)))
System.out.println(
"class capitalization error, file: "
+ file + ", class: "
+ className);
}
}
}
public void checkIdentNames() {
Iterator files = identMap.keySet().iterator();
ArrayList reportSet = new ArrayList();
Chapter 11: The Java I/O System
649
img
while(files.hasNext()) {
String file = (String)files.next();
ArrayList ids = identMap.getArrayList(file);
for(int i = 0; i < ids.size(); i++) {
String id = (String)ids.get(i);
if(!classes.contains(id)) {
// Ignore identifiers of length 3 or
// longer that are all uppercase
// (probably static final values):
if(id.length() >= 3 &&
id.equals(
id.toUpperCase()))
continue;
// Check to see if first char is upper:
if(Character.isUpperCase(id.charAt(0))){
if(reportSet.indexOf(file + id)
== -1){ // Not reported yet
reportSet.add(file + id);
System.out.println(
"Ident capitalization error in:"
+ file + ", ident: " + id);
}
}
}
}
}
}
static final String usage =
"Usage: \n" +
"ClassScanner classnames -a\n" +
"\tAdds all the class names in this \n" +
"\tdirectory to the repository file \n" +
"\tcalled 'classnames'\n" +
"ClassScanner classnames\n" +
"\tChecks all the java files in this \n" +
"\tdirectory for capitalization errors, \n" +
"\tusing the repository file 'classnames'";
private static void usage() {
System.err.println(usage);
System.exit(1);
}
650
Thinking in Java
img
public static void main(String[] args)
throws IOException {
if(args.length < 1 || args.length > 2)
usage();
ClassScanner c = new ClassScanner();
File old = new File(args[0]);
if(old.exists()) {
try {
// Try to open an existing
// properties file:
InputStream oldlist =
new BufferedInputStream(
new FileInputStream(old));
c.classes.load(oldlist);
oldlist.close();
} catch(IOException e) {
System.err.println("Could not open "
+ old + " for reading");
System.exit(1);
}
}
if(args.length == 1) {
c.checkClassNames();
c.checkIdentNames();
}
// Write the class names to a repository:
if(args.length == 2) {
if(!args[1].equals("-a"))
usage();
try {
BufferedOutputStream out =
new BufferedOutputStream(
new FileOutputStream(args[0]));
c.classes.store(out,
"Classes found by ClassScanner.java");
out.close();
} catch(IOException e) {
System.err.println(
"Could not write " + args[0]);
System.exit(1);
}
Chapter 11: The Java I/O System
651
img
}
}
}
class JavaFilter implements FilenameFilter {
public boolean accept(File dir, String name) {
// Strip path information:
String f = new File(name).getName();
return f.trim().endsWith(".java");
}
} ///:~
The class MultiStringMap is a tool that allows you to map a group of
strings onto each key entry. It uses a HashMap (this time with
inheritance) with the key as the single string that's mapped onto the
ArrayList value. The add( ) method simply checks to see if there's a key
already in the HashMap, and if not it puts one there. The
getArrayList( ) method produces an ArrayList for a particular key,
and printValues( ), which is primarily useful for debugging, prints out
all the values ArrayList by ArrayList.
To keep life simple, the class names from the standard Java libraries are
all put into a Properties object (from the standard Java library).
Remember that a Properties object is a HashMap that holds only
String objects for both the key and value entries. However, it can be
saved to disk and restored from disk in one method call, so it's ideal for
the repository of names. Actually, we need only a list of names, and a
HashMap can't accept null for either its key or its value entry. So the
same object will be used for both the key and the value.
For the classes and identifiers that are discovered for the files in a
particular directory, two MultiStringMaps are used: classMap and
identMap. Also, when the program starts up it loads the standard class
name repository into the Properties object called classes, and when a
new class name is found in the local directory that is also added to
classes as well as to classMap. This way, classMap can be used to step
through all the classes in the local directory, and classes can be used to
see if the current token is a class name (which indicates a definition of an
object or method is beginning, so grab the next tokens--until a
semicolon--and put them into identMap).
652
Thinking in Java
img
The default constructor for ClassScanner creates a list of file names,
using the JavaFilter implementation of FilenameFilter, shown at the
end of the file. Then it calls scanListing( ) for each file name.
Inside scanListing( ) the source code file is opened and turned into a
StreamTokenizer. In the documentation, passing true to
slashStarComments( ) and slashSlashComments( ) is supposed to
strip those comments out, but this seems to be a bit flawed, as it doesn't
quite work. Instead, those lines are commented out and the comments are
extracted by another method. To do this, the "/" must be captured as an
ordinary character rather than letting the StreamTokenizer absorb it as
part of a comment, and the ordinaryChar( ) method tells the
StreamTokenizer to do this. This is also true for dots ("."), since we
want to have the method calls pulled apart into individual identifiers.
However, the underscore, which is ordinarily treated by
StreamTokenizer as an individual character, should be left as part of
identifiers since it appears in such static final values as TT_EOF, etc.,
used in this very program. The wordChars( ) method takes a range of
characters you want to add to those that are left inside a token that is
being parsed as a word. Finally, when parsing for one-line comments or
discarding a line we need to know when an end-of-line occurs, so by
calling eolIsSignificant(true) the EOL will show up rather than being
absorbed by the StreamTokenizer.
The rest of scanListing( ) reads and reacts to tokens until the end of the
file, signified when nextToken( ) returns the final static value
StreamTokenizer.TT_EOF.
If the token is a "/" it is potentially a comment, so eatComments( ) is
called to deal with it. The only other situation we're interested in here is if
it's a word, of which there are some special cases.
If the word is class or interface then the next token represents a class or
interface name, and it is put into classes and classMap. If the word is
import or package, then we don't want the rest of the line. Anything
else must be an identifier (which we're interested in) or a keyword (which
we're not, but they're all lowercase anyway so it won't spoil things to put
those in). These are added to identMap.
Chapter 11: The Java I/O System
653
img
The discardLine( ) method is a simple tool that looks for the end of a
line. Note that any time you get a new token, you must check for the end
of the file.
The eatComments( ) method is called whenever a forward slash is
encountered in the main parsing loop. However, that doesn't necessarily
mean a comment has been found, so the next token must be extracted to
see if it's another forward slash (in which case the line is discarded) or an
asterisk. But if it's neither of those, it means the token you've just pulled
out is needed back in the main parsing loop! Fortunately, the
pushBack( ) method allows you to "push back" the current token onto
the input stream so that when the main parsing loop calls nextToken( )
it will get the one you just pushed back.
For convenience, the classNames( ) method produces an array of all the
names in the classes container. This method is not used in the program
but is helpful for debugging.
The next two methods are the ones in which the actual checking takes
place. In checkClassNames( ), the class names are extracted from the
classMap (which, remember, contains only the names in this directory,
organized by file name so the file name can be printed along with the
errant class name). This is accomplished by pulling each associated
ArrayList and stepping through that, looking to see if the first character
is lowercase. If so, the appropriate error message is printed.
In checkIdentNames( ), a similar approach is taken: each identifier
name is extracted from identMap. If the name is not in the classes list,
it's assumed to be an identifier or keyword. A special case is checked: if
the identifier length is three or more and all the characters are uppercase,
this identifier is ignored because it's probably a static final value such as
TT_EOF. Of course, this is not a perfect algorithm, but it assumes that
you'll eventually notice any all-uppercase identifiers that are out of place.
Instead of reporting every identifier that starts with an uppercase
character, this method keeps track of which ones have already been
reported in an ArrayList called reportSet( ). This treats the ArrayList
as a "set" that tells you whether an item is already in the set. The item is
produced by concatenating the file name and identifier. If the element
isn't in the set, it's added and then the report is made.
654
Thinking in Java
img
The rest of the listing is comprised of main( ), which busies itself by
handling the command line arguments and figuring out whether you're
building a repository of class names from the standard Java library or
checking the validity of code you've written. In both cases it makes a
ClassScanner object.
Whether you're building a repository or using one, you must try to open
the existing repository. By making a File object and testing for existence,
you can decide whether to open the file and load( ) the Properties list
classes inside ClassScanner. (The classes from the repository add to,
rather than overwrite, the classes found by the ClassScanner
constructor.) If you provide only one command-line argument it means
that you want to perform a check of the class names and identifier names,
but if you provide two arguments (the second being "-a") you're building a
class name repository. In this case, an output file is opened and the
method Properties.save( ) is used to write the list into a file, along with
a string that provides header file information.
Summary
The Java I/O stream library does satisfy the basic requirements: you can
perform reading and writing with the console, a file, a block of memory,
or even across the Internet (as you will see in Chapter 15). With
inheritance, you can create new types of input and output objects. And
you can even add a simple extensibility to the kinds of objects a stream
will accept by redefining the toString( ) method that's automatically
called when you pass an object to a method that's expecting a String
(Java's limited "automatic type conversion").
There are questions left unanswered by the documentation and design of
the I/O stream library. For example, it would have been nice if you could
say that you want an exception thrown if you try to overwrite a file when
opening it for output--some programming systems allow you to specify
that you want to open an output file, but only if it doesn't already exist. In
Java, it appears that you are supposed to use a File object to determine
whether a file exists, because if you open it as a FileOutputStream or
FileWriter it will always get overwritten.
Chapter 11: The Java I/O System
655
img
The I/O stream library brings up mixed feelings; it does much of the job
and it's portable. But if you don't already understand the decorator
pattern, the design is nonintuitive, so there's extra overhead in learning
and teaching it. It's also incomplete: there's no support for the kind of
output formatting that almost every other language's I/O package
supports.
However, once you do understand the decorator pattern and begin using
the library in situations that require its flexibility, you can begin to benefit
from this design, at which point its cost in extra lines of code may not
bother you as much.
If you do not find what you're looking for in this chapter (which has only
been an introduction, and is not meant to be comprehensive), you can
find in-depth coverage in Java I/O, by Elliotte Rusty Harold (O'Reilly,
1999).
Exercises
Solutions to selected exercises can be found in the electronic document The Thinking in Java
Annotated Solution Guide, available for a small fee from .
1.
Open a text file so that you can read the file one line at a time.
Read each line as a String and place that String object into a
LinkedList. Print all of the lines in the LinkedList in reverse
order.
2.
Modify Exercise 1 so that the name of the file you read is provided
as a command-line argument.
3.
Modify Exercise 2 to also open a text file so you can write text into
it. Write the lines in the ArrayList, along with line numbers (do
not attempt to use the "LineNumber" classes), out to the file.
4.
Modify Exercise 2 to force all the lines in the ArrayList to upper
case and send the results to System.out.
5.
Modify Exercise 2 to take additional command-line arguments of
words to find in the file. Print any lines in which the words match.
656
Thinking in Java
img
6.
Modify DirList.java so that the FilenameFilter actually opens
each file and accepts the file based on whether any of the trailing
arguments on the command line exist in that file.
7.
Create a class called SortedDirList with a constructor that takes
file path information and builds a sorted directory list from the
files at that path. Create two overloaded list( ) methods that will
either produce the whole list or a subset of the list based on an
argument. Add a size( ) method that takes a file name and
produces the size of that file.
8.
Modify WordCount.java so that it produces an alphabetic sort
instead, using the tool from Chapter 9.
9.
Modify WordCount.java so that it uses a class containing a
String and a count value to store each different word, and a Set
of these objects to maintain the list of words.
10.
Modify IOStreamDemo.java so that it uses
LineNumberInputStream to keep track of the line count. Note
that it's much easier to just keep track programmatically.
11.
Starting with section 4 of IOStreamDemo.java, write a program
that compares the performance of writing to a file when using
buffered and unbuffered I/O.
12.
Modify section 5 of IOStreamDemo.java to eliminate the spaces
in the line produced by the first call to in5br.readLine( ). Do
this using a while loop and readChar( ).
13.
Repair the program CADState.java as described in the text.
14.
In Blips.java, copy the file and rename it to BlipCheck.java
and rename the class Blip2 to BlipCheck (making it public and
removing the public scope from the class Blips in the process).
Remove the //! marks in the file and execute the program
including the offending lines. Next, comment out the default
constructor for BlipCheck. Run it and explain why it works. Note
that after compiling, you must execute the program with "java
Blips" because the main( ) method is still in class Blips.
Chapter 11: The Java I/O System
657
img
15.
In Blip3.java, comment out the two lines after the phrases "You
must do this:" and run the program. Explain the result and why it
differs from when the two lines are in the program.
16.
(Intermediate) In Chapter 8, locate the
GreenhouseControls.java example, which consists of three
files. In GreenhouseControls.java, the Restart( ) inner class
has a hard-coded set of events. Change the program so that it
reads the events and their relative times from a text file.
(Challenging: Use a design patterns factory method to build the
events--see Thinking in Patterns with Java, downloadable at
.)
658
Thinking in Java
img
12: Run-time Type
Identification
The idea of run-time type identification (RTTI) seems
fairly simple at first: it lets you find the exact type of an
object when you only have a reference to the base type.
However, the need for RTTI uncovers a whole plethora of interesting (and
often perplexing) OO design issues, and raises fundamental questions of
how you should structure your programs.
This chapter looks at the ways that Java allows you to discover
information about objects and classes at run-time. This takes two forms:
"traditional" RTTI, which assumes that you have all the types available at
compile-time and run-time, and the "reflection" mechanism, which allows
you to discover class information solely at run-time. The "traditional"
RTTI will be covered first, followed by a discussion of reflection.
The need for RTTI
Consider the now familiar example of a class hierarchy that uses
polymorphism. The generic type is the base class Shape, and the specific
derived types are Circle, Square, and Triangle:
Shape
draw()
Circle
Square
Triangle
659
img
This is a typical class hierarchy diagram, with the base class at the top and
the derived classes growing downward. The normal goal in object-
oriented programming is for the bulk of your code to manipulate
references to the base type (Shape, in this case), so if you decide to
extend the program by adding a new class (Rhomboid, derived from
Shape, for example), the bulk of the code is not affected. In this example,
the dynamically bound method in the Shape interface is draw( ), so the
intent is for the client programmer to call draw( ) through a generic
Shape reference. draw( ) is overridden in all of the derived classes, and
because it is a dynamically bound method, the proper behavior will occur
even though it is called through a generic Shape reference. That's
polymorphism.
Thus, you generally create a specific object (Circle, Square, or
Triangle), upcast it to a Shape (forgetting the specific type of the
object), and use that anonymous Shape reference in the rest of the
program.
As a brief review of polymorphism and upcasting, you might code the
above example as follows:
//: c12:Shapes.java
import java.util.*;
class Shape {
void draw() {
System.out.println(this + ".draw()");
}
}
class Circle extends Shape {
public String toString() { return "Circle"; }
}
class Square extends Shape {
public String toString() { return "Square"; }
}
class Triangle extends Shape {
public String toString() { return "Triangle"; }
}
660
Thinking in Java
img
public class Shapes {
public static void main(String[] args) {
ArrayList s = new ArrayList();
s.add(new Circle());
s.add(new Square());
s.add(new Triangle());
Iterator e = s.iterator();
while(e.hasNext())
((Shape)e.next()).draw();
}
} ///:~
The base class contains a draw( ) method that indirectly uses
toString( ) to print an identifier for the class by passing this to
System.out.println( ). If that function sees an object, it automatically
calls the toString( ) method to produce a String representation.
Each of the derived classes overrides the toString( ) method (from
Object) so that draw( ) ends up printing something different in each
case. In main( ), specific types of Shape are created and then added to
an ArrayList. This is the point at which the upcast occurs because the
ArrayList holds only Objects. Since everything in Java (with the
exception of primitives) is an Object, an ArrayList can also hold Shape
objects. But during an upcast to Object, it also loses any specific
information, including the fact that the objects are Shapes. To the
ArrayList, they are just Objects.
At the point you fetch an element out of the ArrayList with next( ),
things get a little busy. Since ArrayList holds only Objects, next( )
naturally produces an Object reference. But we know it's really a
Shape reference, and we want to send Shape messages to that object. So
a cast to Shape is necessary using the traditional "(Shape)" cast. This is
the most basic form of RTTI, since in Java all casts are checked at run-
time for correctness. That's exactly what RTTI means: at run-time, the
type of an object is identified.
In this case, the RTTI cast is only partial: the Object is cast to a Shape,
and not all the way to a Circle, Square, or Triangle. That's because the
only thing we know at this point is that the ArrayList is full of Shapes.
Chapter 12: Run time Type Identification
661
Table of Contents:
  1. Introduction to Objects:The progress of abstraction, An object has an interface
  2. Everything is an Object:You manipulate objects with references, Your first Java program
  3. Controlling Program Flow:Using Java operators, Execution control, true and false
  4. Initialization & Cleanup:Method overloading, Member initialization
  5. Hiding the Implementation:the library unit, Java access specifiers, Interface and implementation
  6. Reusing Classes:Composition syntax, Combining composition and inheritance
  7. Polymorphism:Upcasting revisited, The twist, Designing with inheritance
  8. Interfaces & Inner Classes:Extending an interface with inheritance, Inner class identifiers
  9. Holding Your Objects:Container disadvantage, List functionality, Map functionality
  10. Error Handling with Exceptions:Basic exceptions, Catching an exception
  11. The Java I/O System:The File class, Compression, Object serialization, Tokenizing input
  12. Run-time Type Identification:The need for RTTI, A class method extractor
  13. Creating Windows & Applets:Applet restrictions, Running applets from the command line
  14. Multiple Threads:Responsive user interfaces, Sharing limited resources, Runnable revisited
  15. Distributed Computing:Network programming, Servlets, CORBA, Enterprise JavaBeans
  16. A: Passing & Returning Objects:Aliasing, Making local copies, Cloning objects
  17. B: The Java Native Interface (JNI):Calling a native method, the JNIEnv argument
  18. Java Programming Guidelines:Design, Implementation
  19. Resources:Software, Books, My own list of books
  20. Index