|
Tech Tips archive
May 07, 2002
WELCOME to the Java Developer Connection (JDC) Tech Tips, May 07, 2002. This issue covers:
These tips were developed using Java 2 SDK, Standard Edition, v 1.4.
FILE CHANNELS
Channels are a new Java library feature, part of the new I/O
package (java.nio). The documentation for channels defines a channel like this:
A channel represents an open connection to an entity such
as a hardware device, a file, a network socket, or a program
component that is capable of performing one or more distinct
I/O operations, for example reading or writing.
The java.nio package has many important features in it, including
buffers, file channels, other kinds of channels such as socket
channels, and extensible interfaces. In this tip, the focus is on
file channels only. An important benefit of using file channels
is additional I/O functionality in your applications. This tip
presents several examples of what this benefit means in practice.
Suppose that you have some legacy data in binary files, and this
data includes some 16-bit numbers written by the following
C program:
/* cd1.c */
#include <stdio.h>
#include <assert.h>
short data[] = {1234, 2345, 3456, 4567, 5678};
#define SIZESHORT 2
int main()
{
int i, j;
/* open data file for writing */
FILE* fp = fopen("data", "wb");
assert(fp);
/* write out each short value, low byte first */
for (i = 0; i < sizeof(data) / sizeshort; i++)
{
short item = data[i];
for (j = 0; j < sizeshort; j++) {
char c = (char)(item & 0xff);
fputc(c, fp);
item >>= 8;
}
}
fclose(fp);
return 0;
}
|
This program writes five 16-bit values to a file. Each value is
written as two bytes, with the low byte written first
(this ordering of low byte first is referred to as
"little-endian"). Compile, link, and run the C program to produce
the data file.
How do you read this data using a Java program? Here's one way:
import java.io.*;
public class ChannelDemo0 {
public static void main(String args[])
throws IOException {
FileInputStream fis =
new FileInputStream("data");
DataInputStream dis =
new DataInputStream(fis);
short s = dis.readShort();
System.out.println(s);
dis.close();
}
}
|
This program uses the DataInputStream class and the readShort method of that class. Unfortunately, however, if you run the ChannelDemo0 program, the result is:
-11772
This does not correspond to the first value (1234) written into
the data file. The problem is that the C program writes the short
values as low byte / high byte, and the readShort method expects
high byte / low byte.
How do you solve this problem? Here's another approach, one that
reads the values from the data file and computes their sum:
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
public class ChannelDemo1 {
// sum the values of short data items in a file
static short sumFileContents(String fn)
throws IOException {
// open input stream and get channel
FileInputStream fis =
new FileInputStream(fn);
FileChannel fc = fis.getChannel();
// map the file into a byte buffer
MappedByteBuffer mbb = fc.map(
FileChannel.MapMode.READ_ONLY, 0,
fc.size());
// set byte order to be little-endian
mbb.order(ByteOrder.LITTLE_ENDIAN);
// get short view buffer of byte buffer
ShortBuffer sb = mbb.asShortBuffer();
// sum up the values
short sum = 0;
while (sb.hasRemaining()) {
sum += sb.get();
}
// finish up
fc.close();
fis.close();
return sum;
}
public static void main(String args[])
throws IOException {
short sum = sumFileContents("data");
System.out.println(sum);
}
}
|
The sumFileContents method first creates a FileInputStream, and
then gets a file channel based on this stream. A similar approach
is used for output streams (FileOutputStream) or for files that
are open for both reading and writing (RandomAccessFile).
After the method gets the channel, it maps the data file into a
MappedByteBuffer. This means that the buffer's content is exactly
the file's content, so reading the buffer fetches bytes from the
file and writing the buffer stores bytes into the file.
The byte buffer is then specified to be little-endian, changing
the default order from big-endian. The method then creates a
short "view buffer" on the byte buffer. The view buffer presents
a view of the mapped byte buffer as a sequence of short (16-bit)
values. The short values are each composed of two bytes, and the
low-order byte is assumed to come first, so as to match what the
C program writes. The view buffer is backed by the byte buffer.
So what the method is doing is creating a view of the data file
as a sequence of little-endian short values. The final step in
the method is to sum up the values.
Run the ChannelDemo1 program. It should produce the result:
17280
Let's go on and look at another example of mapping files. Suppose
you want to reverse the bytes in a file. How can you do this? One
simple way uses the following approach:
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
public class ChannelDemo2 {
public static void main(String args[])
throws IOException {
// check command-line argument
if (args.length != 1) {
System.err.println(
"missing file argument");
System.exit(1);
}
// get channel
RandomAccessFile raf =
new RandomAccessFile(args[0], "rw");
FileChannel fc = raf.getChannel();
// map file to buffer
MappedByteBuffer mbb = fc.map(
FileChannel.MapMode.READ_WRITE, 0,
fc.size());
// reverse bytes of file
int len = (int)fc.size();
for (
int i = 0, j = len - 1; i < j; i++, j--)
{
byte b = mbb.get(i);
mbb.put(i, mbb.get(j));
mbb.put(j, b);
}
// finish up
fc.close();
raf.close();
}
}
|
This program opens a channel based on a RandomAccessFile object,
and maps the file for reading and writing. It then sets up a loop
that exchanges bytes starting at the two ends of the buffer, and
works toward the middle. Because the MappedByteBuffer is mapped
to the file on disk, changes to the buffer are reflected in the
file. If you enter the commands:
javac ChannelDemo2.java
java ChannelDemo2 ChannelDemo2.java
and then look at the text of ChannelDemo2.java, you will find
that all of the bytes in ChannelDemo2.java are reversed. In other
words, the first line of the file will be:
}
and the last line of the file will be:
;*.oin.avaj tropmi
To restore the ChannelDemo2.java file to its original byte order,
you need to enter the command:
java ChannelDemo2 ChannelDemo2.java
Mapping a file can be very useful, but it doesn't necessarily
free you from doing additional work. For example, suppose that
you map a file to get convenient access to the last few bytes of
the file. There is a certain amount of intrinsic work that you
need to do in this case, such as doing a disk seek to the end of
the file, and reading a file block into memory. There's no
getting around this work. But mapping is certainly convenient,
and sometimes it can be faster than the alternatives. For example,
it can help you avoid system call overhead or buffer copies.
Let's look at several ways of copying one file to another using
file channel features. The first approach is another example of
mapped files:
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
public class ChannelDemo3 {
public static void main(String args[])
throws IOException {
// check command-line arguments
if (args.length != 2) {
System.err.println("missing filenames");
System.exit(1);
}
// get channels
FileInputStream fis =
new FileInputStream(args[0]);
FileOutputStream fos =
new FileOutputStream(args[1]);
FileChannel fcin = fis.getChannel();
FileChannel fcout = fos.getChannel();
// map input file
MappedByteBuffer mbb = fcin.map(
FileChannel.MapMode.READ_ONLY, 0,
fcin.size());
// do the file copy
fcout.write(mbb);
// finish up
fcin.close();
fcout.close();
fis.close();
fos.close();
}
}
|
In this example, the input file channel is mapped to a buffer,
and then that buffer is written to the output channel. Because
the buffer represents the whole file, writing the buffer is
equivalent to copying the file. So if you enter the commands:
javac ChannelDemo3.java
java ChannelDemo3 ChannelDemo3.java ChannelCopy3.java
It copies the ChannelDemo3 file to ChannelCopy3.java.
Another way of copying a file looks like this:
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
public class ChannelDemo4 {
public static void main(String args[])
throws IOException {
// check command-line arguments
if (args.length != 2) {
System.err.println("missing filenames");
System.exit(1);
}
// get channels
FileInputStream fis =
new FileInputStream(args[0]);
FileOutputStream fos =
new FileOutputStream(args[1]);
FileChannel fcin = fis.getChannel();
FileChannel fcout = fos.getChannel();
// do the file copy
fcin.transferTo(0, fcin.size(), fcout);
// finish up
fcin.close();
fcout.close();
fis.close();
fos.close();
}
}
|
The transferTo method transfers bytes from the source channel
(fcin) to the specified target channel (fcout). The transfer is
typically done without explicit user-level reads and writes of
the channel. The documentation for the transferTo method says:
This method is potentially much more efficient than a simple
loop that reads from this channel and writes to the target
channel. Many operating systems can transfer bytes directly
from the filesystem cache to the target channel without
actually copying them.
In other words, transferTo may rely on special operating system
features that support very fast file transfers.
What does a "regular" file copy look like using file channels?
Here's an example:
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
public class ChannelDemo5 {
public static void main(String args[])
throws IOException {
// check command-line arguments
if (args.length != 2) {
System.err.println("missing filenames");
System.exit(1);
}
// get channels
FileInputStream fis =
new FileInputStream(args[0]);
FileOutputStream fos =
new FileOutputStream(args[1]);
FileChannel fcin = fis.getChannel();
FileChannel fcout = fos.getChannel();
// allocate buffer
ByteBuffer buf =
ByteBuffer.allocateDirect(8192);
// do copy
long size = fcin.size();
long n = 0;
while (n < size) {
buf.clear();
if (fcin.read(buf) < 0) {
break;
}
buf.flip();
n += fcout.write(buf);
}
// finish up
fcin.close();
fcout.close();
fis.close();
fos.close();
}
}
|
This program copies one file to another, one buffer at a time.
Before it reads each chunk of the input file into the buffer, the
program "clears" the buffer. This makes the buffer ready for
reading by setting the position to 0 and the limit to the
capacity. Then, after each read, the program "flips" the buffer.
This makes the buffer ready for writing by setting the limit to
the current position and the current position to 0.
At this point, you've seen some of the new features offered by
file channels. There are other features, such as locking, that
are also important to learn about. Beyond the new features, file
channels offer significant I/O performance gains. For more
information about file channels, see
New I/O APIs.
STACK TRACE ELEMENTS
The standard Java library has long had a mechanism for displaying
stack tracebacks using the Throwable.printStackTrace method. This
method is used to dump the program context for uncaught exceptions.
The trace is printed on the System.err stream or on a specified
PrintStream or PrintWriter.
A new library feature gives you programmatic access to stack
tracebacks. You can retrieve an array of StackTraceElement
objects, with each object representing a single stack frame in
a trace. Let's look at an example to see how this works:
class A {
B bref;
void f() {
bref.g();
}
}
class B {
void g() {}
}
class C {
String str;
int len = str.length();
}
public class TraceDemo1 {
// dump a single stack trace element
static void dumpTraceElement(
StackTraceElement ste) {
System.err.println("filename = " +
ste.getFileName());
System.err.println("line number = " +
ste.getLineNumber());
System.err.println("class name = " +
ste.getClassName());
System.err.println("method name = " +
ste.getMethodName());
System.err.println("is native method = " +
ste.isNativeMethod());
}
// dump an array of stack trace elements,
// most recent first
static void dumpTrace(Throwable e) {
// display exception
System.err.println("Exception: " + e);
System.err.println();
// display traceback
StackTraceElement ste[] = e.getStackTrace();
for (int i = 0; i < ste.length; i++) {
dumptraceelement(ste[i]);
system.err.println();
}
}
public static void main(string args[]) {
// call a.f() and trigger an exception
try {
a aref = new a();
aref.f();
}
catch (throwable e) {
// display regular stack trace
e.printstacktrace();
system.err.println();
// dump stack trace in custom format
dumptrace(e);
}
system.err.println();
system.err.println(
"==============================");
system.err.println();
// trigger an exception in initialization
try {
new c();
}
catch (throwable e) {
dumptrace(e);
}
}
}
|
In this example, the TraceDemo1 program first calls the A.f
method. That method, in turn, calls B.g. Unfortunately, when B.g is called, the B object reference is null. This triggers an exception.
The first thing the program prints is the regular stack trace,
which looks like this:
java.lang.NullPointerException
at A.f(TraceDemo1.java:5)
at TraceDemo1.main(TraceDemo1.java:61)
Then it displays a customized stack traceback, with the exception
name and a sequence of StackTraceElements:
Exception: java.lang.NullPointerException
filename = TraceDemo1.java
line number = 5
class name = A
method name = f
is native method = false
filename = TraceDemo1.java
line number = 61
class name = TraceDemo1
method name = main
is native method = false
|
Notice that the filename, class name, and method name of the
first StackTraceElement refer to the point of the exception. The
exception occurred at line 5 of TraceDemo1.java, within method
A.f.
The second part of the example shows what happens when an
exception is thrown during instance initialization. In this
example, the TraceDemo1 program creates a C object. When the C
object is instantiated, it attempts to take the length of the str
string. However, because str is never explicitely initialized,
the reference to str is a null reference. Here's the output for
that part of the example:
Exception: java.lang.NullPointerException
filename = TraceDemo1.java
line number = 15
class name = C
method name = <init>
is native method = false
filename = TraceDemo1.java
line number = 83
class name = TraceDemo1
method name = main
is native method = false
|
The point of the exception is line 15 of TraceDemo1.java, in the
C class, in a method called <init>, which is a special name for instance initialization methods within the Java virtual machine*.
You can use the StackTraceElement feature to implement customized
exception reporting and logging formats. An example of
a customized logging format is one that limits the stack trace to
a manageable depth. For example, if there are 500 stack frames,
you might want to preserve the first ten and the last ten. The
StackTraceElement information is part of the serialized
representation of Throwable class instances, so it's available
for deserialized objects.
For more information about stack trace elements, see the
description of the StackTraceElement class.
Also, see section 10.12, Threads and Exceptions, in "The Java
Programming Language Third Edition" by Arnold, Gosling, and Holmes.
IMPORTANT: Please read our Terms of Use and Privacy policies:
http://www.sun.com/share/text/termsofuse.html
http://www.sun.com/privacy/
http://developer.java.sun.com/berkeley_license.html
FEEDBACK
Comments? Send your feedback on the JDC Tech Tips to:
jdc-webmaster@sun.com
SUBSCRIBE/UNSUBSCRIBE
- To subscribe, go to the subscriptions page, choose the newsletters you want to subscribe to and click "Update".
- To unsubscribe, go to the subscriptions page, uncheck the appropriate checkbox, and click "Update".
- ARCHIVES
You'll find the JDC Tech Tips archives at:
http://java.sun.com/jdc/TechTips/index.html
- COPYRIGHT
Copyright 2002 Sun Microsystems, Inc. All rights reserved.
901 San Antonio Road, Palo Alto, California 94303 USA.
This document is protected by copyright. For more information, see:
http://java.sun.com/jdc/copyright.html
This issue of the JDC Tech Tips is written by Glen McCluskey.
JDC Tech Tips
May 07, 2002
Sun, Sun Microsystems, Java, and Java Developer Connection are
trademarks or registered trademarks of Sun Microsystems, Inc.
in the United States and other countries.
* As used in this document, the terms "Java
virtual machine" or "JVM" mean a virtual machine for the Java platform.
|