ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Java in a Nutshell, 4th Edition

Top Ten Cool New Features of Java 2SE 1.4

by David Flanagan, author of Java in a Nutshell, 4th Edition
03/06/2002

Java 1.4 has just been released. This is a major new release, with 62 percent more classes and interfaces than Java 1.3; needless to say, there are lots of new features. This article describes my personal favorite top ten new features, ordered from cool to extremely cool. You can learn more about all of these features in the 4th edition of Java in a Nutshell. Note that the new features I've selected for this article are drawn from those documented in that book; thus I do not include any new graphics and GUI features or new enterprise features. Those are the subjects of Java Foundation Classes in a Nutshell and Java Enterprise in a Nutshell.

The format of this article does not allow me to include complete working examples of each of the ten new features I describe. I do include extensive code fragments to illustrate how to use each of the new features, but don't expect to be able to compile and run the code as shown.

10. Parsing XML

The ability to parse XML in Java is hugely important, and the only reason that this new feature isn't ranked higher on my list is that the JAXP API for parsing XML has been available as a standard extension for a while. What is new in Java 1.4, however, is that JAXP has been added to the core platform, which is important because it makes XML parsing ubiquitous. The JAXP parsing API is defined in javax.xml.parsers and includes classes for SAX and DOM parsing, which are used with the org.xml.sax packages (and its sub-packages) and with the org.w3c.dom package; these have also been added to Java 1.4. Here's how you could use the JAXP API to parse an XML document into a DOM tree, and then use the DOM API to extract information from that document.

import java.io.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;

File f;  // The file to parse.  Assume this is initialized elsewhere

// Create a factory object for creating DOM parsers
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// Now use the factory to create a DOM parser (a.k.a. a DocumentBuilder)
DocumentBuilder parser = factory.newDocumentBuilder();
// Parse the file and build a Document tree to represent its content
Document document = parser.parse(f);
// Ask the document for a list of all <sect1> tags it contains
NodeList sections = document.getElementsByTagName("sect1");
// Loop through those <sect1> elements one at a time, and extract the
// content of their <h3> tags.
int numSections = sections.getLength();
for(int i = 0; i < numSections; i++) {
    Element section = (Element)sections.item(i);  // A <sect1>
    // Find the first element child of this section (a <h3> element)
    // and print the text it contains.
    Node title = section.getFirstChild();
    while(title != null && title.getNodeType() != Node.ELEMENT_NODE) 
	title = title.getNextSibling();
    // Print the text contained in the Text node child of this element
    if (title!=null) System.out.println(title.getFirstChild().getNodeValue());
}

9. Transforming XML

The JAXP API includes classes for XML transformations as well as XML parsing. javax.xml.transform and its sub-packages allow you to change an XML document from one representation (as a stream of XML tags, as a stream of SAX events, or as a DOM tree) to another representation, and it allows you to apply an XSLT transformation to the document content at the same time. It is very easy to use, and like the XML parsing feature listed above, this would be higher up on the list if it hadn't been available as a standard extension before Java 1.4 was released.

The code below shows how you could apply an XSLT transformation to a DOM document tree and output the transformed document as XML text to standard output.

import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import org.w3c.dom.*;

File xsltfile;      // An XSLT file; initialized elsewhere
Document document;  // Assume we've already read or created this DOM document

Source xsltSource = new StreamSource(xsltfile); // Source for transformation
Source source = new DOMSource(document);        // Document to be transformed
Result result = new StreamResult(System.out);   // Where to put result document

// Start off with a factory object
TransformerFactory tf = TransformerFactory.newInstance();
// Use the factory to read the XSLT file into a Templates object
Templates transformation = tf.newTemplates(xsltSource);
// Create a Transformer object from the Templates object
Transformer transformer = transformation.newTransformer();
// Finally, perform the transformation
transformer.transform(source, result);

8. Preferences

Many of the important new features of Java 1.4 are new utilities in or beneath the java.util package. java.util.prefs is one such utility: a facility for persistently storing and querying user-specific preferences and system-wide application configuration information. The key class is Preferences, which represents a persistent set of named preferences for a package (all classes in a package typically share the same user-specific and system-wide Preferences object). Preferences defines static methods for obtaining the user and system Preferences objects for a package, and defines instance methods for setting and querying named values of various types. The following is typical code that an application might use to query preferences when initializing itself. The cool thing about the Preferences API is that it is just so easy to use -- all of the hard work of managing configuration files just goes away.

// Get Preferences objects for user and system preferences for the package
// that contains the TextEditor class.
Preferences userprefs = Preferences.userNodeForPackage(TextEditor.class);
Preferences sysprefs = Preferences.systemNodeForPackage(TextEditor.class);

// Look up a user preference value as an integer.
// Note that we always pass a default value
int width = userprefs.getInt("width", 80);
// Look up a user preference using a system preference as the default
String dictionary = userprefs.get("dictionary"
                                  sysprefs.get("dictionary",
                                               "default_dictionary"));

7. Logging

Related Reading

Java In a Nutshell
By David Flanagan

Logging is another important utility, defined by the java.util.logging package. It is particularly useful for servers and other applications that run unattended or do not have a user interface. In typical usage, the application developer uses a Logger object with a name that corresponds to the class or package name of the application to generate log messages -- at any of seven severity levels (defined by the Level class).

These messages might report errors and warnings, or provide informational messages about interesting events in the application's lifecycle. They can include debugging information, or even trace the execution of important methods within the program. It is not intended that all of these messages will actually be generated and logged all of the time; there is a default logging configuration file that specifies where log messages are directed to (the console, a file, a network socket, or a combination of these), how they are formatted (as plain text or XML documents), and at what severity threshold they are logged (log messages with a severity below the specified threshold are discarded with very little overhead and should not significantly impact the performance of the application.) The system administrator or end user of the application can modify this default configuration to suit their needs.

The java.util.logging package is very flexible, and can be used in a number of ways. For most applications, however, use of the Logging API is quite simple. Obtain a named Logger object whenever necessary by calling the static Logger.getLogger() method, passing the class or package name of the application as the logger name. Then use one of the many Logger instance methods to generate log messages. The easiest methods to use have names that correspond to severity levels, such as severe(), warning(), info(), and debug():

import java.util.logging.*;

// Get a Logger object named after the current package.
Logger logger = Logger.getLogger("com.davidflanagan.servers.pop");
logger.info("Starting server.");       // Log an informational message
ServerSocket ss;
try { ss = new ServerSocket(110); }
catch(Exception e) {                   // Log exceptions
  logger.log(Level.SEVERE, "Can't bind port 110", e);  // complex log message
  logger.warning("Exiting");                           // simple warning 
  return;
}
logger.fine("got server socket");  // low severity (fine detail) debug message

6. Secure Sockets and HTTPS

One of Java's greatest strengths, ever since Java 1.0, has been the ability to perform powerful networking with very little code. Java 1.4 adds support for secure sockets (using the SSL and TLS protocols) so that you can now easily write powerful and secure networking code. SSL support is provided by the javax.net.ssl package. (Note: javax not java.) As with all security-related packages in Java, this package is complex and highly configurable. Fortunately, however, the most common uses of SSL are quite easy to implement. The following code sets up an SSL socket to securely communicate with a Web server, using the HTTPS protocol:

import javax.net.ssl.*;
import java.io.*;
  
// Get a SocketFactory object for creating SSL sockets
SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();

// Create a secure socket connected to the HTTPS port (port 443) of a server
SSLSocket sslsock = (SSLSocket) factory.createSocket(hostname, 443);

// Get the certificate presented by the web server.  This may throw an 
// exception if the server didn't supply a certificate.  Look at the
// issuer of the certificate and decide if it is trusted.
SSLSession session = sslsock.getSession();
X509Certificate cert = (X509Certificate)session.getPeerCertificates()[0];
String issuer = cert.getIssuerDN().getName(); 

// Assuming we trust the certificate, we now use the socket just like a normal
// java.net.Socket object.  So send a HTTP request and read the response
PrintWriter out = new PrintWriter(sslsock.getOutputStream());
out.print("GET " + args[1] + " HTTP/1.0\r\n\r\n");
out.flush();

// Next, read the server's response and print it to the console.
BufferedReader in =
  new BufferedReader(new InputStreamReader(sslsock.getInputStream()));
String line;
while((line = in.readLine()) != null) System.out.println(line);

// Finally, close the socket.
sslsock.close(); 

5. LinkedHashMap

Halfway through my list of cool features of Java 1.4 is the java.util.LinkedHashMap class. This new addition to the Collections API is a Map implementation that is based on a hashtable, just as HashMap is. LinkedHashMap differs in that it also maintains a linked list running through the map entries, and so can iterate through those entries in a predictable order. Usually, this order is the order in which entries are inserted into the map; you may occasionally encounter situations in which this is a very useful feature. Java 1.4 also includes a LinkedHashSet which is very similar.

If the idea of merging a HashMap with a LinkedList is not cool enough for you, consider this: LinkedHashMap can be configured (pass true as the third argument to the constructor) to order its entries from most-recently-accessed (i.e. queried or set) to least-recently-accessed. That's a nifty feature, but there's more! LinkedHashMap has a protected method named removeEldestEntry() that is called every time a new entry is added to the map. If this method returns true, then the "eldest" entry in the map (i.e. the one that was least-recently inserted or least-recently used) is automatically deleted. The default implementation of removeEldestEntry() always returns false, but you can override it to return true when the size of the map reaches a certain maximum size. Presto: you've got an LRU (least-recently-used) cache!

public class LRUCache extends java.util.LinkedHashMap {
    public LRUCache(int maxsize) {
	super(maxsize*4/3 + 1, 0.75f, true);
	this.maxsize = maxsize;
    }
    protected int maxsize;
    protected boolean removeEldestEntry() { return size() > maxsize; }
}

Editor's Update: The signature for removeEldestEntry() does not match that of the superclass and therefore this method does not override the corresponding method in the superclass. A fix that also includes the J2SE 1.5 feature metadata has been posted by David Flanagan as a blog entry Failure to override .

4. FileChannel

The most important new feature of Java 1.4 is probably the New I/O API. This new API is defined in the java.nio package and its sub-packages, and provides high-performance input and output using an architecture that is mostly unrelated to the existing java.io API. One of the new classes, java.nio.channels.FileChannel, defines a new way to read and write files. Reading and writing files isn't very cool in and of itself, since we could already do that with existing java.io classes. What is cool about FileChannel, however, is that it provides facilities for memory mapping and locking files. Memory mapping allows you to view the contents of a file as if they were stored in an array directly in memory. The FileChannel API doesn't use arrays directly, but instead uses a java.nio.ByteBuffer. Here is code that uses memory mapping:



import java.io.*;              // For FileInputStream
import java.nio.*;             // For ByteBuffer
import java.nio.channels.*;    // For FileChannel

// Create a FileChannel, get the file size and map the file to a ByteBuffer
FileChannel in = new FileInputStream("test.data").getChannel();
int filesize = (int)in.size();
ByteBuffer mappedfile = in.map(FileChannel.MapMode.READ_ONLY, 0, filesize);
// Assume the file contains fixed-size records of binary data.
static final int RECORDSIZE = 80; // The size of each record
int recordNumber = 1;             // This is the record we want to read
byte[] recorddata = new byte[RECORDSIZE];     // An array to hold record data
mappedfile.position(recordNumber*RECORDSIZE); // Start of desired record
mappedfile.get(recorddata);                   // Fill the array from the buffer

Memory mapping can be lots of fun, but there is an overhead associated with it. In typical Java implementations, it is usually more efficient to just read small (< 100Kb, perhaps) files than it is to use memory mapping.

The other cool feature of FileChannel that we'll consider here is its ability to lock a file or a portion of a file. Locks can be used to prevent all concurrent access to the file (an exclusive lock) or to prevent concurrent writes (a shared lock). (Note that some operating systems strictly enforce all locks. Others only provide an advisory locking facility that requires programs to cooperate and to attempt to acquire a lock before reading or writing portions of a shared file.) Here is code you might use to obtain a shared lock on a region of a file in order to prevent any other (cooperating) programs from writing to that region while you were reading from it:


FileChannel in;  // Initialized elsewhere
FileLock lock = in.lock(recordNumber*RECORDSIZE, // Start of locked region
                        RECORDSIZE,              // Length of locked region
                        true);   // Shared lock: prevent concurrent updates
// Now read the desired record, then release the lock when done
lock.release()

3. Non-Blocking I/O

For programmers writing high-performance network servers, the most important feature of the New I/O API is the ability to perform non-blocking I/O. The SocketChannel class of java.nio.channels allows you to read and write from a network connection; it is the New I/O equivalent of java.net.Socket.

SocketChannel and other SelectableChannel subclasses can be put into non-blocking mode and registered with a Selector object. The Selector class defines a method named select() that does the block; this method monitors one or more channels, and returns when one (or more) of them is ready for I/O. With a single Selector object, a server can monitor any number of SocketChannel client connections. Prior to the introduction of this new API, each client connection had to be handled with its own thread and blocking Socket object. This was a heavy-weight solution, and did not scale well for large servers. The code below shows the basic loop used in servers that perform non-blocking I/O:


import java.nio.channels.*;
import java.util.*;

// Create a selector object to monitor all open client connections.
Selector selector = Selector.open();

// Create a new non-blocking ServerSocketChannel, bind it to port 8000, and
// register it with the Selector
ServerSocketChannel server = ServerSocketChannel.open();    // Open channel
server.configureBlocking(false);                            // Non-blocking
server.socket().bind(new java.net.InetSocketAddress(8000)); // Bind to port
SelectionKey serverkey = server.register(selector, SelectionKey.OP_ACCEPT);

for(;;) {  // The main server loop.  The server runs forever.
    // This call blocks until there is activity on one of the 
    // registered channels. This is the key method in non-blocking I/O.
    selector.select();

    // Get a java.util.Set containing the SelectionKey objects for
    // all channels that are ready for I/O.
    Set keys = selector.selectedKeys();

    // Use a java.util.Iterator to loop through the selected keys
    for(Iterator i = keys.iterator(); i.hasNext(); ) {
	SelectionKey key = (SelectionKey) i.next();
	i.remove();  // Remove the key from the set of selected keys

	// Check whether this key is the SelectionKey we got when
	// we registered the ServerSocketChannel.
	if (key == serverkey) {
	    // Activity on the ServerSocketChannel means a client
	    // is trying to connect to the server.
	    if (key.isAcceptable()) {
		// Accept the client connection
		SocketChannel client = server.accept();
		// Put the client channel in non-blocking mode.
		client.configureBlocking(false);
		// Now register the client channel with the Selector object
		SelectionKey clientkey =
                    client.register(selector, SelectionKey.OP_READ);
	    }
	}
	else {  // Otherwise, there must be activity on a client channel
	    // Double-check that there is data to read
	    if (!key.isReadable()) continue;
            // Get the client channel that has data to read
	    SocketChannel client = (SocketChannel) key.channel();

	    // Now read bytes from the client into a buffer allocated elsewhere
	    int bytesread = client.read(buffer);

	    // If read() returns -1, it indicates end-of-stream, which
	    // means the client has disconnected, so de-register the
	    // selection key and close the channel.
	    if (bytesread == -1) {  
		key.cancel();
		client.close();
		continue;
	    }
	}
    }
}

2. Regular Expressions

Number two on the top ten list of cool new features in Java 1.4 is regular expressions. A regular expression is a way of describing a pattern of text. The java.util.regex package allows you to compare strings to regular expressions to determine if they match, and, if so, to determine exactly what parts of the string matched the pattern (and possibly its sub-patterns). Regular expressions also provide a powerful search-and-replace capability.

Regular expressions are heavily used in the Perl programming language, and Java 1.4 has adopted the regular expression pattern syntax of Perl 5. This means that Perl programmers can easily learn to use regular expressions in Java. In addition to the java.util.regex package, the String class has had regular-expression utility methods added that are simpler to use, in some cases. The following code shows some of the cool things you can do in Java with regular expressions. (Unfortunately, there is no room here to explain regular expression syntax, so if you are not already familiar with how regular expressions work in Perl or a similar language, this code won't make much sense to you.)


// Use a String convenience method to replace all occurrences of the word
// "java" (with any capitalization) with the correctly capitalized "Java"
s = s.replaceAll("(?i)\\bjava\\b", // The pattern: "java", case-insensitive
                 "Java");          // The replacement string

// Here's a more complex use of regular expressions, using the Pattern and
// Matcher classes from java.util.regex.  
// Create a pattern that describes any word beginning with "Java", and
// which also "captures" whatever suffix follows the "Java" prefix.  It
// uses the CASE_INSENSITIVE flag, so it matches any capitalization
Pattern p = Pattern.compile("\\bJava(\\w*)", Pattern.CASE_INSENSITIVE);
// This is a sentence we want to compare the pattern to.
String text = "Java is fun; JavaScript is funny.";
// Create a Matcher object to compare the pattern to the text
Matcher m = p.matcher(text);
// Now loop to find all matches of the pattern in the text
while(m.find()) {
  // For each match, print the text that matched the pattern, and its
  // position within the string
  System.out.println("Found '" + m.group(0) + "' at position " + m.start(0));
  // Also, if there was a suffix, print the suffix.
  if (m.start(1) < m.end(1)) System.out.println("Suffix is " + m.group(1));
}

1. Assertions

And the number one cool feature of Java 1.4 is... the assert statement!

An assert statement is used to document and verify design assumptions in Java code. An assertion consists of the assert keyword, followed by a boolean expression that the programmer believes should always evaluate to true. By default, assertions are not enabled, and the assert statement does not actually do anything. It is possible to enable assertions as a debugging and testing tool, however, and when this is done, the assert statement evaluates the expression. If it is indeed true, then assert does nothing. On the other hand, if the expression evaluates to false, then the assertion fails, and the assert statement throws a java.lang.AssertionError.

The assert statement may optionally include a second expression, separated from the first by a colon. When assertions are enabled, and the first expression evaluates to false, the value of the second expression is taken as an error code or error message of some sort, and is passed to the AssertionError() constructor. The full syntax of the statement is:

assert assertion ;

or:

assert assertion : errorcode ;

It is important to remember that the assertion expression must be a boolean expression, which typically means that it contains a comparison operator or invokes a boolean-valued method. The errorcode expression may have any value.

Now let's consider how assert can be used in real code. Suppose you are writing a method in which you believe that the variable x will always have a value of 0 or 1. You can state this belief explicitly with an assertion:


if (x == 0) {
  ...
}
else {  
  assert x == 1 : x;  // x must be 0 or 1.
  ...
}

A similar technique works with switch statements. If you ever find yourself writing a switch that does not have a default case, use an assertion to encode your assumption that all the possible cases are explicitly enumerated. For example:

switch(x) {
case -1: return LESS;
case 0: return EQUALS;
case 1: return GREATER;
default: assert false:x; // throw AssertionError if x is not -1, 0, or 1.
}

Note that assert false; always fails. This form of the statement is a useful "dead end" statement when you believe that the statement can never be reached.

The examples above are only two of the common idioms in which assertions are useful. There are other important ones as well, such as testing class invariants and testing preconditions for private methods. Learning to use assertions well takes practice, and you can read more about the theory and practical applications of assertions in Chapter 2 of Java in a Nutshell, 4th Edition.

Java In a Nutshell

Related Reading

Java In a Nutshell
By David Flanagan

Note that assert was not a reserved word prior to Java 1.4, so the javac compiler does not recognize the assert statement by default. To compile code that contains assertions, you must pass the argument -source 1.4 to javac. This means that existing Java code that happens to use assert as an identifier will still compile.

As noted above, assertions must be enabled in order to be tested at run time. You do this with the -ea argument to the java interpreter. This argument by itself enables assertions in all non-system classes. You can also enable on a per-class or per-package basis:

java -ea:com.example.sorters.MergeSort    // Enable assertions for one class
java -ea:com.example.sorters...           // Enable assertions for a package

You can also use the -da argument to selectively disable assertions in classes or packages.

David Flanagan is the author of a number of O'Reilly books, including Java in a Nutshell, Java Examples in a Nutshell, Java Foundation Classes in a Nutshell, JavaScript: The Definitive Guide, and JavaScript Pocket Reference.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.