This chapter includes a
set of projects that build on the material presented in this book or otherwise
didn’t fit in earlier chapters.
Most of these projects are
significantly more complex than the examples in the rest of the book, and they
often demonstrate new techniques and uses of class
libraries.
If you come from a C or C++
background, you might be skeptical at first of Java’s power when it comes
to handling text. Indeed, one drawback is that execution speed is slower and
that could hinder some of your efforts. However, the tools (in particular the
String class) are quite powerful, as the examples
in this section show (and performance improvements have been promised for
Java).
As you’ll see, these examples
were created to solve problems that arose in the creation of this book. However,
they are not restricted to that and the solutions they offer can easily be
adapted to other situations. In addition, they show the power of Java in an area
that has not previously been emphasized in this
book.
You’ve no doubt noticed that
each complete code listing (not code fragment) in this book begins and ends with
special comment tag marks ‘//:’ and
‘///:~’. This meta-information is included so that the code
can be automatically extracted from the book into compilable source-code files.
In my previous book, I had a system that allowed me to automatically incorporate
tested code files into the book. In this book, however, I discovered that it was
often easier to paste the code into the book once it was initially tested and,
since it’s hard to get right the first time, to perform edits to the code
within the book. But how to extract it and test the code? This program is the
answer, and it could come in handy when you set out to solve a text processing
problem. It also demonstrates many of the String class
features.
I first save the entire book in
ASCII text format into a separate file. The CodePackager program has two
modes (which you can see described in usageString): if you use the
-p flag, it expects to see an input file containing the ASCII text from
the book. It will go through this file and use the comment tag marks to extract
the code, and it uses the file name on the first line to determine the name of
the file. In addition, it looks for the package statement in case it
needs to put the file into a special directory (chosen via the path indicated by
the package statement).
But that’s not all. It also
watches for the change in chapters by keeping track of the package names. Since
all packages for each chapter begin with c02, c03, c04,
etc. to indicate the chapter where they belong (except for those
beginning with com, which are ignored for the purpose of keeping track of
chapters), as long as the first listing in each chapter contains a
package statement with the chapter number, the CodePackager
program can keep track of when the chapter changed and put all the subsequent
files in the new chapter subdirectory.
As each file is extracted, it is
placed into a SourceCodeFile object that is then placed into a
collection. (This process will be more thoroughly described later.) These
SourceCodeFile objects could simply be stored in files, but that brings
us to the second use for this project. If you invoke CodePackager
without the -p flag it expects a “packed” file as
input, which it will then extract into separate files. So the -p flag
means that the extracted files will be found “packed” into this
single file.
Why bother with the packed file?
Because different computer platforms have different ways of storing text
information in files. A big issue is the end-of-line character or characters,
but other issues can also exist. However, Java has a special type of IO stream
– the DataOutputStream
– which promises that, regardless of what machine the data is coming
from, the storage of that data will be in a form that can be correctly retrieved
by any other machine by using a
DataInputStream. That is,
Java handles all of the platform-specific details, which
is a large part of the promise of Java. So the -p flag stores everything
into a single file in a universal format. You download this file and the Java
program from the Web, and when you run CodePackager on this file
without the -p flag the files will all be extracted to appropriate
places on your system. (You can specify an alternate subdirectory; otherwise the
subdirectories will just be created in the current directory.) To ensure that no
system-specific formats remain, File objects are used everywhere a path
or a file is described. In addition, there’s a sanity check: an empty file
is placed in each subdirectory; the name of that file indicates how many files
you should find in that subdirectory.
Here is the code, which will be
described in detail at the end of the listing:
//: CodePackager.java // "Packs" and "unpacks" the code in "Thinking // in Java" for cross-platform distribution. /* Commented so CodePackager sees it and starts a new chapter directory, but so you don't have to worry about the directory where this program lives: package c17; */ import java.util.*; import java.io.*; class Pr { static void error(String e) { System.err.println("ERROR: " + e); System.exit(1); } } class IO { static BufferedReader disOpen(File f) { BufferedReader in = null; try { in = new BufferedReader( new FileReader(f)); } catch(IOException e) { Pr.error("could not open " + f); } return in; } static BufferedReader disOpen(String fname) { return disOpen(new File(fname)); } static DataOutputStream dosOpen(File f) { DataOutputStream in = null; try { in = new DataOutputStream( new BufferedOutputStream( new FileOutputStream(f))); } catch(IOException e) { Pr.error("could not open " + f); } return in; } static DataOutputStream dosOpen(String fname) { return dosOpen(new File(fname)); } static PrintWriter psOpen(File f) { PrintWriter in = null; try { in = new PrintWriter( new BufferedWriter( new FileWriter(f))); } catch(IOException e) { Pr.error("could not open " + f); } return in; } static PrintWriter psOpen(String fname) { return psOpen(new File(fname)); } static void close(Writer os) { try { os.close(); } catch(IOException e) { Pr.error("closing " + os); } } static void close(DataOutputStream os) { try { os.close(); } catch(IOException e) { Pr.error("closing " + os); } } static void close(Reader os) { try { os.close(); } catch(IOException e) { Pr.error("closing " + os); } } } class SourceCodeFile { public static final String startMarker = "//:", // Start of source file endMarker = "} ///:~", // End of source endMarker2 = "}; ///:~", // C++ file end beginContinue = "} ///:Continued", endContinue = "///:Continuing", packMarker = "###", // Packed file header tag eol = // Line separator on current system System.getProperty("line.separator"), filesep = // System's file path separator System.getProperty("file.separator"); public static String copyright = ""; static { try { BufferedReader cr = new BufferedReader( new FileReader("Copyright.txt")); String crin; while((crin = cr.readLine()) != null) copyright += crin + "\n"; cr.close(); } catch(Exception e) { copyright = ""; } } private String filename, dirname, contents = new String(); private static String chapter = "c02"; // The file name separator from the old system: public static String oldsep; public String toString() { return dirname + filesep + filename; } // Constructor for parsing from document file: public SourceCodeFile(String firstLine, BufferedReader in) { dirname = chapter; // Skip past marker: filename = firstLine.substring( startMarker.length()).trim(); // Find space that terminates file name: if(filename.indexOf(' ') != -1) filename = filename.substring( 0, filename.indexOf(' ')); System.out.println("found: " + filename); contents = firstLine + eol; if(copyright.length() != 0) contents += copyright + eol; String s; boolean foundEndMarker = false; try { while((s = in.readLine()) != null) { if(s.startsWith(startMarker)) Pr.error("No end of file marker for " + filename); // For this program, no spaces before // the "package" keyword are allowed // in the input source code: else if(s.startsWith("package")) { // Extract package name: String pdir = s.substring( s.indexOf(' ')).trim(); pdir = pdir.substring( 0, pdir.indexOf(';')).trim(); // Capture the chapter from the package // ignoring the 'com' subdirectories: if(!pdir.startsWith("com")) { int firstDot = pdir.indexOf('.'); if(firstDot != -1) chapter = pdir.substring(0,firstDot); else chapter = pdir; } // Convert package name to path name: pdir = pdir.replace( '.', filesep.charAt(0)); System.out.println("package " + pdir); dirname = pdir; } contents += s + eol; // Move past continuations: if(s.startsWith(beginContinue)) while((s = in.readLine()) != null) if(s.startsWith(endContinue)) { contents += s + eol; break; } // Watch for end of code listing: if(s.startsWith(endMarker) || s.startsWith(endMarker2)) { foundEndMarker = true; break; } } if(!foundEndMarker) Pr.error( "End marker not found before EOF"); System.out.println("Chapter: " + chapter); } catch(IOException e) { Pr.error("Error reading line"); } } // For recovering from a packed file: public SourceCodeFile(BufferedReader pFile) { try { String s = pFile.readLine(); if(s == null) return; if(!s.startsWith(packMarker)) Pr.error("Can't find " + packMarker + " in " + s); s = s.substring( packMarker.length()).trim(); dirname = s.substring(0, s.indexOf('#')); filename = s.substring(s.indexOf('#') + 1); dirname = dirname.replace( oldsep.charAt(0), filesep.charAt(0)); filename = filename.replace( oldsep.charAt(0), filesep.charAt(0)); System.out.println("listing: " + dirname + filesep + filename); while((s = pFile.readLine()) != null) { // Watch for end of code listing: if(s.startsWith(endMarker) || s.startsWith(endMarker2)) { contents += s; break; } contents += s + eol; } } catch(IOException e) { System.err.println("Error reading line"); } } public boolean hasFile() { return filename != null; } public String directory() { return dirname; } public String filename() { return filename; } public String contents() { return contents; } // To write to a packed file: public void writePacked(DataOutputStream out) { try { out.writeBytes( packMarker + dirname + "#" + filename + eol); out.writeBytes(contents); } catch(IOException e) { Pr.error("writing " + dirname + filesep + filename); } } // To generate the actual file: public void writeFile(String rootpath) { File path = new File(rootpath, dirname); path.mkdirs(); PrintWriter p = IO.psOpen(new File(path, filename)); p.print(contents); IO.close(p); } } class DirMap { private Hashtable t = new Hashtable(); private String rootpath; DirMap() { rootpath = System.getProperty("user.dir"); } DirMap(String alternateDir) { rootpath = alternateDir; } public void add(SourceCodeFile f){ String path = f.directory(); if(!t.containsKey(path)) t.put(path, new Vector()); ((Vector)t.get(path)).addElement(f); } public void writePackedFile(String fname) { DataOutputStream packed = IO.dosOpen(fname); try { packed.writeBytes("###Old Separator:" + SourceCodeFile.filesep + "###\n"); } catch(IOException e) { Pr.error("Writing separator to " + fname); } Enumeration e = t.keys(); while(e.hasMoreElements()) { String dir = (String)e.nextElement(); System.out.println( "Writing directory " + dir); Vector v = (Vector)t.get(dir); for(int i = 0; i < v.size(); i++) { SourceCodeFile f = (SourceCodeFile)v.elementAt(i); f.writePacked(packed); } } IO.close(packed); } // Write all the files in their directories: public void write() { Enumeration e = t.keys(); while(e.hasMoreElements()) { String dir = (String)e.nextElement(); Vector v = (Vector)t.get(dir); for(int i = 0; i < v.size(); i++) { SourceCodeFile f = (SourceCodeFile)v.elementAt(i); f.writeFile(rootpath); } // Add file indicating file quantity // written to this directory as a check: IO.close(IO.dosOpen( new File(new File(rootpath, dir), Integer.toString(v.size())+".files"))); } } } public class CodePackager { private static final String usageString = "usage: java CodePackager packedFileName" + "\nExtracts source code files from packed \n" + "version of Tjava.doc sources into " + "directories off current directory\n" + "java CodePackager packedFileName newDir\n" + "Extracts into directories off newDir\n" + "java CodePackager -p source.txt packedFile" + "\nCreates packed version of source files" + "\nfrom text version of Tjava.doc"; private static void usage() { System.err.println(usageString); System.exit(1); } public static void main(String[] args) { if(args.length == 0) usage(); if(args[0].equals("-p")) { if(args.length != 3) usage(); createPackedFile(args); } else { if(args.length > 2) usage(); extractPackedFile(args); } } private static String currentLine; private static BufferedReader in; private static DirMap dm; private static void createPackedFile(String[] args) { dm = new DirMap(); in = IO.disOpen(args[1]); try { while((currentLine = in.readLine()) != null) { if(currentLine.startsWith( SourceCodeFile.startMarker)) { dm.add(new SourceCodeFile( currentLine, in)); } else if(currentLine.startsWith( SourceCodeFile.endMarker)) Pr.error("file has no start marker"); // Else ignore the input line } } catch(IOException e) { Pr.error("Error reading " + args[1]); } IO.close(in); dm.writePackedFile(args[2]); } private static void extractPackedFile(String[] args) { if(args.length == 2) // Alternate directory dm = new DirMap(args[1]); else // Current directory dm = new DirMap(); in = IO.disOpen(args[0]); String s = null; try { s = in.readLine(); } catch(IOException e) { Pr.error("Cannot read from " + in); } // Capture the separator used in the system // that packed the file: if(s.indexOf("###Old Separator:") != -1 ) { String oldsep = s.substring( "###Old Separator:".length()); oldsep = oldsep.substring( 0, oldsep. indexOf('#')); SourceCodeFile.oldsep = oldsep; } SourceCodeFile sf = new SourceCodeFile(in); while(sf.hasFile()) { dm.add(sf); sf = new SourceCodeFile(in); } dm.write(); } } ///:~
You’ll first notice the
package statement that is commented out. Since this is the first program
in the chapter, the package statement is necessary to tell
CodePackager that the chapter has changed, but putting it in a package
would be a problem. When you create a package, you tie the resulting
program to a particular directory structure, which is fine for most of the
examples in this book. Here, however, the CodePackager program must be
compiled and run from an arbitrary directory, so the package statement is
commented out. It will still look like an ordinary package
statement to CodePackager, though, since the program isn’t
sophisticated enough to detect multi-line comments. (It has no need for such
sophistication, a fact that comes in handy here.)
The first two classes are
support/utility classes designed to make the rest of the program more consistent
to write and easier to read. The first, Pr, is similar to the ANSI C
library perror, since it prints an error message (but also exits the
program). The second class encapsulates the creation of files, a process that
was shown in Chapter 10 as one that rapidly becomes verbose and annoying. In
Chapter 10, the proposed solution created new classes, but here static
method calls are used. Within those methods the appropriate exceptions are
caught and dealt with. These methods make the rest of the code much cleaner to
read.
The first class that helps solve
the problem is SourceCodeFile, which represents all the information
(including the contents, file name, and directory) for one source code file in
the book. It also contains a set of String constants representing the
markers that start and end a file, a marker used inside the packed file, the
current system’s end-of-line separator and file path separator (notice the
use of System.getProperty( ) to get the local version), and a
copyright notice, which is extracted from the following file
Copyright.txt.
////////////////////////////////////////////////// // Copyright (c) Bruce Eckel, 1998 // Source code file from the book "Thinking in Java" // All rights reserved EXCEPT as allowed by the // following statements: You may freely use this file // for your own work (personal or commercial), // including modifications and distribution in // executable form only. Permission is granted to use // this file in classroom situations, including its // use in presentation materials, as long as the book // "Thinking in Java" is cited as the source. // Except in classroom situations, you may not copy // and distribute this code; instead, the sole // distribution point is http://www.BruceEckel.com // (and official mirror sites) where it is // freely available. You may not remove this // copyright and notice. You may not distribute // modified versions of the source code in this // package. You may not use this file in printed // media without the express permission of the // author. Bruce Eckel makes no representation about // the suitability of this software for any purpose. // It is provided "as is" without express or implied // warranty of any kind, including any implied // warranty of merchantability, fitness for a // particular purpose or non-infringement. The entire // risk as to the quality and performance of the // software is with you. Bruce Eckel and the // publisher shall not be liable for any damages // suffered by you or any third party as a result of // using or distributing software. In no event will // Bruce Eckel or the publisher be liable for any // lost revenue, profit, or data, or for direct, // indirect, special, consequential, incidental, or // punitive damages, however caused and regardless of // the theory of liability, arising out of the use of // or inability to use software, even if Bruce Eckel // and the publisher have been advised of the // possibility of such damages. Should the software // prove defective, you assume the cost of all // necessary servicing, repair, or correction. If you // think you've found an error, please email all // modified files with clearly commented changes to: // Bruce@EckelObjects.com. (please use the same // address for non-code errors found in the book). //////////////////////////////////////////////////
When extracting files from a packed
file, the file separator of the system that packed the file is also noted, so it
can be replaced with the correct one for the local system.
The subdirectory name for the
current chapter is kept in the field chapter, which is initialized to
c02. (You’ll notice that the listing in Chapter 2 doesn’t
contain a package statement.) The only time that the chapter field
changes is when a package statement is discovered in the current
file.
The first constructor is used to
extract a file from the ASCII text version of this book. The calling code (which
appears further down in the listing) reads each line in until it finds one that
matches the beginning of a listing. At that point, it creates a new
SourceCodeFile object, passing it the first line (which has already been
read by the calling code) and the
BufferedReader object
from which to extract the rest of the source code listing.
At this point, you begin to see
heavy use of the String methods. To extract the file name, the overloaded
version of
substring( ) is
called that takes the starting offset and goes to the end of the String.
This starting index is produced by finding the
length( ) of the
startMarker.
trim( ) removes white
space from both ends of the String. The first line can also have words
after the name of the file; these are detected using
indexOf( ), which
returns -1 if it cannot find the character you’re looking for and the
value where the first instance of that character is found if it does. Notice
there is also an overloaded version of indexOf( ) that takes a
String instead of a character.
Once the file name is parsed and
stored, the first line is placed into the contents String (which
is used to hold the entire text of the source code listing). At this point, the
rest of the lines are read and concatenated into the contents
String. It’s not quite that simple, since certain situations
require special handling. One case is error checking: if you run into a
startMarker, it means that no end marker was placed at the end of the
listing that’s currently being collected. This is an error condition that
aborts the program.
The second special case is the
package keyword. Although Java is a free-form language, this program
requires that the package keyword be at the beginning of the line. When
the package keyword is seen, the package name is extracted by looking for
the space at the beginning and the semicolon at the end. (Note that this could
also have been performed in a single operation by using the overloaded
substring( ) that takes both the starting and ending indexes.) Then
the dots in the package name are replaced by the file separator, although an
assumption is made here that the file separator is only one character long. This
is probably true on all systems, but it’s a place to look if there are
problems.
The default behavior is to
concatenate each line to contents, along with the end-of-line string,
until the endMarker is discovered, which indicates that the constructor
should terminate. If the end of the file is encountered before the
endMarker is seen, that’s an error.
The second constructor is used to
recover the source code files from a packed file. Here, the calling method
doesn’t have to worry about skipping over the intermediate text. The file
contains all the source-code files, placed end-to-end. All you need to hand to
this constructor is the BufferedReader where the information is coming
from, and the constructor takes it from there. There is some meta-information,
however, at the beginning of each listing, and this is denoted by the
packMarker. If the packMarker isn’t there, it means the
caller is mistakenly trying to use this constructor where it isn’t
appropriate.
Once the packMarker is
found, it is stripped off and the directory name (terminated by a
‘#’) and the file name (which goes to the end of the line)
are extracted. In both cases, the old separator character is replaced by the one
that is current to this machine using the String
replace( ) method. The
old separator is placed at the beginning of the packed file, and you’ll
see how that is extracted later in the listing.
The rest of the constructor is
quite simple. It reads and concatenates each line to the contents until
the endMarker is found.
The next set of methods are simple
accessors: directory( ), filename( ) (notice the method
can have the same spelling and capitalization as the field) and
contents( ), and hasFile( ) to indicate whether this
object contains a file or not. (The need for this will be seen
later.)
The final three methods are
concerned with writing this code listing into a file, either a packed file via
writePacked( ) or a Java source file via writeFile( ).
All writePacked( ) needs is the DataOutputStream, which was
opened elsewhere, and represents the file that’s being written. It puts
the header information on the first line and then calls
writeBytes( ) to write contents in a “universal”
format.
When writing the Java source file,
the file must be created. This is done via IO.psOpen( ), handing it
a File object that
contains not only the file name but also the path. But the question now is: does
this path exist? The user has the option of placing all the source code
directories into a completely different subdirectory, which might not even
exist. So before each file is written,
File.mkdirs( )
is called with the path that you want to write the file into. This will make the
entire path all at once.
It’s convenient to organize
the listings as subdirectories while the whole collection is being built in
memory. One reason is another sanity check: as each subdirectory of listings is
created, an additional file is added whose name contains the number of files in
that directory.
The DirMap class produces
this effect and demonstrates the concept of a “multimap.” This is
implemented using a
Hashtable whose keys are
the subdirectories being created and whose values are
Vector objects containing the
SourceCodeFile objects in that particular directory. Thus, instead of
mapping a key to a single value, the “multimap” maps a key to a set
of values via the associated Vector. Although this sounds complex,
it’s remarkably straightforward to implement. You’ll see that most
of the size of the DirMap class is due to the portions that write to
files, not to the “multimap” implementation.
There are two ways you can make a
DirMap: the default constructor assumes that you want the directories to
branch off of the current one, and the second constructor lets you specify an
alternate absolute path for the starting directory.
The add( ) method is
where quite a bit of dense action occurs. First, the directory( ) is
extracted from the SourceCodeFile you want to add, and then the
Hashtable is examined to see if it contains that key already. If not, a
new Vector is added to the Hashtable and associated with that key.
At this point, the Vector is there, one way or another, and it is
extracted so the SourceCodeFile can be added. Because
Vectors can be easily
combined with Hashtables like this, the power of both is
amplified.
Writing a packed file involves
opening the file to write (as a
DataOutputStream so the
data is universally recoverable) and writing the header information about the
old separator on the first line. Next, an Enumeration of the
Hashtable keys is produced and stepped through to select each directory
and to fetch the Vector associated with that directory so each
SourceCodeFile in that Vector can be written to the packed
file.
Writing the Java source files to
their directories in write( ) is almost identical to
writePackedFile( ) since both methods simply call the appropriate
method in SourceCodeFile. Here, however, the root path is passed into
SourceCodeFile.writeFile( ) and when all the files have been written
the additional file with the name containing the number of files is also
written.
The previously described classes
are used within CodePackager. First you see the usage string that gets
printed whenever the end user invokes the program incorrectly, along with the
usage( ) method that calls it and exits the program. All
main( ) does is determine whether you want to create a packed file
or extract from one, then it ensures the arguments are correct and calls the
appropriate method.
When a packed file is created,
it’s assumed to be made in the current directory, so the DirMap is
created using the default constructor. After the file is opened each line is
read and examined for particular conditions:
When
extracting a packed file, the extraction can be into the current directory or
into an alternate directory, so the DirMap object is created accordingly.
The file is opened and the first line is read. The old file path separator
information is extracted from this line. Then the input is used to create the
first SourceCodeFile object, which is added to the DirMap. New
SourceCodeFile objects are created and added as long as they contain a
file. (The last one created will simply return when it runs out of input and
then hasFile( ) will return
false.)
Although the previous example can
come in handy as a guide for some project of your own that involves text
processing, this project will be directly useful because it performs a style
check to make sure that your capitalization conforms to the de-facto Java style.
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 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, run it and 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 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.
The explanation immediately follows
the listing:
//: 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 very // useful aid. import java.io.*; import java.util.*; class MultiStringMap extends Hashtable { public void add(String key, String value) { if(!containsKey(key)) put(key, new Vector()); ((Vector)get(key)).addElement(value); } public Vector getVector(String key) { if(!containsKey(key)) { System.err.println( "ERROR: can't find key: " + key); System.exit(1); } return (Vector)get(key); } public void printValues(PrintStream p) { Enumeration k = keys(); while(k.hasMoreElements()) { String oneKey = (String)k.nextElement(); Vector val = getVector(oneKey); for(int i = 0; i < val.size(); i++) p.println((String)val.elementAt(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() { path = new File("."); fileList = path.list(new JavaFilter()); for(int i = 0; i < fileList.length; i++) { System.out.println(fileList[i]); scanListing(fileList[i]); } } void scanListing(String fname) { try { 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('_', '_'); 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); } } } catch(IOException e) { e.printStackTrace(); } } void discardLine() { try { while(in.nextToken() != StreamTokenizer.TT_EOF && in.ttype != StreamTokenizer.TT_EOL) ; // Throw away tokens to end of line } catch(IOException e) { e.printStackTrace(); } } // StreamTokenizer's comment removal seemed // to be broken. This extracts them: void eatComments() { try { if(in.nextToken() != StreamTokenizer.TT_EOF) { if(in.ttype == '/') discardLine(); else if(in.ttype != '*') in.pushBack(); else while(true) { if(in.nextToken() == StreamTokenizer.TT_EOF) break; if(in.ttype == '*') if(in.nextToken() != StreamTokenizer.TT_EOF && in.ttype == '/') break; } } } catch(IOException e) { e.printStackTrace(); } } public String[] classNames() { String[] result = new String[classes.size()]; Enumeration e = classes.keys(); int i = 0; while(e.hasMoreElements()) result[i++] = (String)e.nextElement(); return result; } public void checkClassNames() { Enumeration files = classMap.keys(); while(files.hasMoreElements()) { String file = (String)files.nextElement(); Vector cls = classMap.getVector(file); for(int i = 0; i < cls.size(); i++) { String className = (String)cls.elementAt(i); if(Character.isLowerCase( className.charAt(0))) System.out.println( "class capitalization error, file: " + file + ", class: " + className); } } } public void checkIdentNames() { Enumeration files = identMap.keys(); Vector reportSet = new Vector(); while(files.hasMoreElements()) { String file = (String)files.nextElement(); Vector ids = identMap.getVector(file); for(int i = 0; i < ids.size(); i++) { String id = (String)ids.elementAt(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.addElement(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); } public static void main(String[] args) { 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.save(out, "Classes found by ClassScanner.java"); out.close(); } catch(IOException e) { System.err.println( "Could not write " + args[0]); System.exit(1); } } } } 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. As in the
previous example, it uses a
Hashtable (this time with
inheritance) with the key as the single string that’s mapped onto the
Vector value. The add( ) method simply checks to see if
there’s a key already in the Hashtable, and if not it puts one
there. The getVector( ) method produces a Vector for a
particular key, and printValues( ), which is primarily useful for
debugging, prints out all the values Vector by
Vector.
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 Hashtable 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
Hashtable 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).
The default constructor for
ClassScanner creates a list of file names (using the JavaFilter
implementation of
FilenameFilter, as
described in Chapter 10). 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 (it doesn’t quite work in Java
1.0). 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.
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 collection. 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 Vector and stepping through that, looking to see if the first
character is lower case. 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 3 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 a Vector called
reportSet( ). This treats the Vector 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.
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.
Chapter 11 introduced the Java 1.1
concept of reflection and
used that feature to look up methods for a particular class – either the
entire list of methods or a subset of those whose names match a keyword you
provide. The magic of this is that it can automatically show you all the
methods for a class without forcing you to walk up the inheritance hierarchy
examining the base classes at each level. Thus, it provides a valuable
timesaving tool for programming: because the names of most Java method names are
made nicely verbose and descriptive, you can search for the method names that
contain a particular word of interest. When you find what you think you’re
looking for, check the online documentation.
However, by Chapter 11 you
hadn’t seen the AWT, so that tool was developed as a command-line
application. Here is the more useful GUI version, which dynamically updates the
output as you type and also allows you to cut and paste from the
output:
//: DisplayMethods.java // Display the methods of any class inside // a window. Dynamically narrows your search. import java.awt.*; import java.awt.event.*; import java.applet.*; import java.lang.reflect.*; import java.io.*; public class DisplayMethods extends Applet { Class cl; Method[] m; Constructor[] ctor; String[] n = new String[0]; TextField name = new TextField(40), searchFor = new TextField(30); Checkbox strip = new Checkbox("Strip Qualifiers"); TextArea results = new TextArea(40, 65); public void init() { strip.setState(true); name.addTextListener(new NameL()); searchFor.addTextListener(new SearchForL()); strip.addItemListener(new StripL()); Panel top = new Panel(), lower = new Panel(), p = new Panel(); top.add(new Label("Qualified class name:")); top.add(name); lower.add( new Label("String to search for:")); lower.add(searchFor); lower.add(strip); p.setLayout(new BorderLayout()); p.add(top, BorderLayout.NORTH); p.add(lower, BorderLayout.SOUTH); setLayout(new BorderLayout()); add(p, BorderLayout.NORTH); add(results, BorderLayout.CENTER); } class NameL implements TextListener { public void textValueChanged(TextEvent e) { String nm = name.getText().trim(); if(nm.length() == 0) { results.setText("No match"); n = new String[0]; return; } try { cl = Class.forName(nm); } catch (ClassNotFoundException ex) { results.setText("No match"); return; } m = cl.getMethods(); ctor = cl.getConstructors(); // Convert to an array of Strings: n = new String[m.length + ctor.length]; for(int i = 0; i < m.length; i++) n[i] = m[i].toString(); for(int i = 0; i < ctor.length; i++) n[i + m.length] = ctor[i].toString(); reDisplay(); } } void reDisplay() { // Create the result set: String[] rs = new String[n.length]; String find = searchFor.getText(); int j = 0; // Select from the list if find exists: for (int i = 0; i < n.length; i++) { if(find == null) rs[j++] = n[i]; else if(n[i].indexOf(find) != -1) rs[j++] = n[i]; } results.setText(""); if(strip.getState() == true) for (int i = 0; i < j; i++) results.append( StripQualifiers.strip(rs[i]) + "\n"); else // Leave qualifiers on for (int i = 0; i < j; i++) results.append(rs[i] + "\n"); } class StripL implements ItemListener { public void itemStateChanged(ItemEvent e) { reDisplay(); } } class SearchForL implements TextListener { public void textValueChanged(TextEvent e) { reDisplay(); } } public static void main(String[] args) { DisplayMethods applet = new DisplayMethods(); Frame aFrame = new Frame("Display Methods"); aFrame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); aFrame.add(applet, BorderLayout.CENTER); aFrame.setSize(500,750); applet.init(); applet.start(); aFrame.setVisible(true); } } class StripQualifiers { private StreamTokenizer st; public StripQualifiers(String qualified) { st = new StreamTokenizer( new StringReader(qualified)); st.ordinaryChar(' '); } public String getNext() { String s = null; try { if(st.nextToken() != StreamTokenizer.TT_EOF) { switch(st.ttype) { case StreamTokenizer.TT_EOL: s = null; break; case StreamTokenizer.TT_NUMBER: s = Double.toString(st.nval); break; case StreamTokenizer.TT_WORD: s = new String(st.sval); break; default: // single character in ttype s = String.valueOf((char)st.ttype); } } } catch(IOException e) { System.out.println(e); } return s; } public static String strip(String qualified) { StripQualifiers sq = new StripQualifiers(qualified); String s = "", si; while((si = sq.getNext()) != null) { int lastDot = si.lastIndexOf('.'); if(lastDot != -1) si = si.substring(lastDot + 1); s += si; } return s; } } ///:~
Some things you’ve seen
before. As with many of the GUI programs in this book, this is created to
perform both as an application and as an applet. Also, the
StripQualifiers class is exactly the same as it was in Chapter
11.
The GUI contains a TextField
name in which you can enter the fully-qualified class name you want to
look up, and another one, searchFor, in which you can enter the optional
text to search for within the list of methods. The Checkbox allows you to
say whether you want to use the fully-qualified names in the output or if you
want the qualification stripped off. Finally, the results are displayed in a
TextArea.
You’ll notice that there are
no buttons or other components by which to indicate that you want the search to
start. That’s because both of the TextFields and the
Checkbox are monitored by their listener objects. Whenever you make a
change, the list is immediately updated. If you change the text within the
name field, the new text is captured in class NameL. If the text
isn’t empty, it is used inside
Class.forName( ) to
try to look up the class. As you’re typing, of course, the name will be
incomplete and Class.forName( ) will fail, which means that it
throws an exception. This is trapped and the TextArea is set to “No
match”. But as soon as you type in a correct name (capitalization counts),
Class.forName( ) is successful and getMethods( ) and
getConstructors( ) will return arrays of Method and
Constructor objects, respectively. Each of the objects in these arrays is
turned into a String via toString( ) (this produces the
complete method or constructor signature) and both lists are combined into
n, a single String array. The array n is a member of
class DisplayMethods and is used in updating the display whenever
reDisplay( ) is called.
If you change the Checkbox
or searchFor components, their listeners simply call
reDisplay( ). reDisplay( ) creates a temporary array of
String called rs (for “result set”). The result set is
either copied directly from n if there is no find word, or
conditionally copied from the Strings in n that contain the
find word. Finally, the strip Checkbox is interrogated to
see if the user wants the names to be stripped (the default is
“yes”). If so, StripQualifiers.strip( ) does the job; if
not, the list is simply displayed.
In init( ), you might
think that there’s a lot of busy work involved in setting up the layout.
In fact, it is possible to lay out the components with less work, but the
advantage of using BorderLayouts this way is that it allows the user to
resize the window and make – in particular – the TextArea
larger, which means you can resize to allow you to see longer names without
scrolling.
You might find that you’ll
keep this tool running while you’re programming, since it provides one of
the best “first lines of attack” when you’re trying to figure
out what method to call.
This program was modified from code
originally created by Larry O’Brien, and is based on the
“Boids” program created by Craig Reynolds in 1986 to demonstrate an
aspect of complexity theory called “emergence.”
The goal here is to produce a
reasonably lifelike reproduction of flocking or
herding behavior in animals by
establishing a small set of simple rules for each animal. Each animal can look
at the entire scene and all the other animals in the scene, but it reacts only
to a set of nearby “flockmates.” The animal moves according to three
simple steering behaviors:
More elaborate models
can include obstacles and the ability for the animals to predict collisions and
avoid them, so the animals can flow around fixed objects in the environment. In
addition, the animals might also be given a goal, which can cause the herd to
follow a desired path. For simplicity, obstacle avoidance and goal-seeking is
not included in the model presented here.
Emergence
means that, despite the limited nature of computers and the simplicity of the
steering rules, the result seems realistic. That is, remarkably lifelike
behavior “emerges” from this simple model.
The program is presented as a
combined application/applet:
//: FieldOBeasts.java // Demonstration of complexity theory; simulates // herding behavior in animals. Adapted from // a program by Larry O'Brien lobrien@msn.com import java.awt.*; import java.awt.event.*; import java.applet.*; import java.util.*; class Beast { int x, y, // Screen position currentSpeed; // Pixels per second float currentDirection; // Radians Color color; // Fill color FieldOBeasts field; // Where the Beast roams static final int GSIZE = 10; // Graphic size public Beast(FieldOBeasts f, int x, int y, float cD, int cS, Color c) { field = f; this.x = x; this.y = y; currentDirection = cD; currentSpeed = cS; color = c; } public void step() { // You move based on those within your sight: Vector seen = field.beastListInSector(this); // If you're not out in front if(seen.size() > 0) { // Gather data on those you see int totalSpeed = 0; float totalBearing = 0.0f; float distanceToNearest = 100000.0f; Beast nearestBeast = (Beast)seen.elementAt(0); Enumeration e = seen.elements(); while(e.hasMoreElements()) { Beast aBeast = (Beast) e.nextElement(); totalSpeed += aBeast.currentSpeed; float bearing = aBeast.bearingFromPointAlongAxis( x, y, currentDirection); totalBearing += bearing; float distanceToBeast = aBeast.distanceFromPoint(x, y); if(distanceToBeast < distanceToNearest) { nearestBeast = aBeast; distanceToNearest = distanceToBeast; } } // Rule 1: Match average speed of those // in the list: currentSpeed = totalSpeed / seen.size(); // Rule 2: Move towards the perceived // center of gravity of the herd: currentDirection = totalBearing / seen.size(); // Rule 3: Maintain a minimum distance // from those around you: if(distanceToNearest <= field.minimumDistance) { currentDirection = nearestBeast.currentDirection; currentSpeed = nearestBeast.currentSpeed; if(currentSpeed > field.maxSpeed) { currentSpeed = field.maxSpeed; } } } else { // You are in front, so slow down currentSpeed = (int)(currentSpeed * field.decayRate); } // Make the beast move: x += (int)(Math.cos(currentDirection) * currentSpeed); y += (int)(Math.sin(currentDirection) * currentSpeed); x %= field.xExtent; y %= field.yExtent; if(x < 0) x += field.xExtent; if(y < 0) y += field.yExtent; } public float bearingFromPointAlongAxis ( int originX, int originY, float axis) { // Returns bearing angle of the current Beast // in the world coordiante system try { double bearingInRadians = Math.atan( (this.y - originY) / (this.x - originX)); // Inverse tan has two solutions, so you // have to correct for other quarters: if(x < originX) { if(y < originY) { bearingInRadians += - (float)Math.PI; } else { bearingInRadians = (float)Math.PI - bearingInRadians; } } // Just subtract the axis (in radians): return (float) (axis - bearingInRadians); } catch(ArithmeticException aE) { // Divide by 0 error possible on this if(x > originX) { return 0; } else return (float) Math.PI; } } public float distanceFromPoint(int x1, int y1){ return (float) Math.sqrt( Math.pow(x1 - x, 2) + Math.pow(y1 - y, 2)); } public Point position() { return new Point(x, y); } // Beasts know how to draw themselves: public void draw(Graphics g) { g.setColor(color); int directionInDegrees = (int)( (currentDirection * 360) / (2 * Math.PI)); int startAngle = directionInDegrees - FieldOBeasts.halfFieldOfView; int endAngle = 90; g.fillArc(x, y, GSIZE, GSIZE, startAngle, endAngle); } } public class FieldOBeasts extends Applet implements Runnable { private Vector beasts; static float fieldOfView = (float) (Math.PI / 4), // In radians // Deceleration % per second: decayRate = 1.0f, minimumDistance = 10f; // In pixels static int halfFieldOfView = (int)( (fieldOfView * 360) / (2 * Math.PI)), xExtent = 0, yExtent = 0, numBeasts = 50, maxSpeed = 20; // Pixels/second boolean uniqueColors = true; Thread thisThread; int delay = 25; public void init() { if (xExtent == 0 && yExtent == 0) { xExtent = Integer.parseInt( getParameter("xExtent")); yExtent = Integer.parseInt( getParameter("yExtent")); } beasts = makeBeastVector(numBeasts, uniqueColors); // Now start the beasts a-rovin': thisThread = new Thread(this); thisThread.start(); } public void run() { while(true) { for(int i = 0; i < beasts.size(); i++){ Beast b = (Beast) beasts.elementAt(i); b.step(); } try { thisThread.sleep(delay); } catch(InterruptedException ex){} repaint(); // Otherwise it won't update } } Vector makeBeastVector( int quantity, boolean uniqueColors) { Vector newBeasts = new Vector(); Random generator = new Random(); // Used only if uniqueColors is on: double cubeRootOfBeastNumber = Math.pow((double)numBeasts, 1.0 / 3.0); float colorCubeStepSize = (float) (1.0 / cubeRootOfBeastNumber); float r = 0.0f; float g = 0.0f; float b = 0.0f; for(int i = 0; i < quantity; i++) { int x = (int) (generator.nextFloat() * xExtent); if(x > xExtent - Beast.GSIZE) x -= Beast.GSIZE; int y = (int) (generator.nextFloat() * yExtent); if(y > yExtent - Beast.GSIZE) y -= Beast.GSIZE; float direction = (float)( generator.nextFloat() * 2 * Math.PI); int speed = (int)( generator.nextFloat() * (float)maxSpeed); if(uniqueColors) { r += colorCubeStepSize; if(r > 1.0) { r -= 1.0f; g += colorCubeStepSize; if( g > 1.0) { g -= 1.0f; b += colorCubeStepSize; if(b > 1.0) b -= 1.0f; } } } newBeasts.addElement( new Beast(this, x, y, direction, speed, new Color(r,g,b))); } return newBeasts; } public Vector beastListInSector(Beast viewer) { Vector output = new Vector(); Enumeration e = beasts.elements(); Beast aBeast = (Beast)beasts.elementAt(0); int counter = 0; while(e.hasMoreElements()) { aBeast = (Beast) e.nextElement(); if(aBeast != viewer) { Point p = aBeast.position(); Point v = viewer.position(); float bearing = aBeast.bearingFromPointAlongAxis( v.x, v.y, viewer.currentDirection); if(Math.abs(bearing) < fieldOfView / 2) output.addElement(aBeast); } } return output; } public void paint(Graphics g) { Enumeration e = beasts.elements(); while(e.hasMoreElements()) { ((Beast)e.nextElement()).draw(g); } } public static void main(String[] args) { FieldOBeasts field = new FieldOBeasts(); field.xExtent = 640; field.yExtent = 480; Frame frame = new Frame("Field 'O Beasts"); // Optionally use a command-line argument // for the sleep time: if(args.length >= 1) field.delay = Integer.parseInt(args[0]); frame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); frame.add(field, BorderLayout.CENTER); frame.setSize(640,480); field.init(); field.start(); frame.setVisible(true); } } ///:~
Although this isn’t a perfect
reproduction of the behavior in Craig Reynold’s “Boids”
example, it exhibits its own fascinating characteristics, which you can modify
by adjusting the numbers. You can find out more about the modeling of flocking
behavior and see a spectacular 3-D version of Boids at Craig Reynold’s
page http://www.hmt.com/cwr/boids.html.
To run this program as an applet,
put the following applet tag in an HTML file:
<applet code=FieldOBeasts width=640 height=480> <param name=xExtent value = "640"> <param name=yExtent value = "480"> </applet>
This chapter shows some of the more
sophisticated things that you can do with Java. It also makes the point that
while Java must certainly have its limits, those limits are primarily relegated
to performance. (When the text-processing programs were written, for example,
C++ versions were much faster – this might be due partly to an inefficient
implementation of the IO library, which should change in time.) The limits of
Java do not seem to be in the area of expressiveness. Not only does it
seem possible to express just about everything you can imagine, but Java seems
oriented toward making that expression easy to write and read. Therefore you
don’t run into the wall of complexity that often occurs with languages
that are more trivial to use than Java (at least they seem that way, at first).
And with Java 1.2’s JFC/Swing library, even the
expressiveness and ease of use of the AWT is improving
dramatically.