|
|||||
//
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
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
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
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
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
}
///:~
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
]
>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
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
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
}
}
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
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
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
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
}
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
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
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
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
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
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
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
}
}
}
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
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
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
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
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
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
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
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
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
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:
|
|||||