Contents Previous Next
import java.io.*;
public class Copy1 {
public static void main(String[] args) throws IOException {
if (args.length == 2) {
BufferedReader input = new BufferedReader(new FileReader(args[0]));
BufferedWriter output = new BufferedWriter(new FileWriter(args[1]));
while (input.ready()) {
output.write(input.read());
}
input.close();
output.close();
}
else {
System.out.println("usage: java Copy1 source-file dest-file");
}
}
}
This program uses two command-line arguments; hence, in order to run
the program, you will need to issue the DOS command
java Copy1 source-file dest-filewhere source-file is an existing file, and dest-file is the name of the new copy.
Notice that this program uses the package java.io; you might want to refer to the documentation for that package.
In the method header for main, there is a clause throws IOException. We will discuss this clause later. In the meantime, notice how the parameter for main is used in this example. Two command-line arguments are required, so args.length is compared for identity with the int value 2. Every array in Java has, in addition to the fields inherited from Object, a final int length field, which tells how many elements belong to the array. The elements are indexed from 0 to length - 1 using square brackets, as in the first two statements in the body of the if statement above. Thus, the if statement is executed if there are exactly two command-line arguments, and these arguments are found in args[0] and args[1], respectively; otherwise, the else statement is executed, printing an error message to standard output.
Consider the first statement of the if block:
BufferedReader input = new BufferedReader(new FileReader(args[0]));
This statement declares and constructs a BufferedReader, which is a
class in the package java.io. This class provides for
efficient reading from a character input stream. Because the input is
considered to be a stream of characters, Java will read from it a
character at a time; however, we don't want to do a disk or network
access each time we read a character, because this would be very
inefficient. The BufferedReader class causes the input to be
buffered; i.e., a fixed-sized block of characters is first read
into a memory buffer. Then the BufferedReader provides access to one
character at a time until the buffer is empty, at which time it reads
a new block of characters into the buffer.
The argument to the constructor is a FileReader, whose class also belongs to java.io. This class provides the most convenient way of associating a file of characters (note that any file can be considered to be a file of characters) with an object in Java. This is accomplished with one of the FileReader constructors. The constructor used in the above example simply takes the name of the file as a String.
Take a look at the documentation for the constructor FileReader(String). Notice the clause, throws FileNotFoundException, which has a similar syntax to the clause, throws IOException in the definition of our main method. A throws clause in a method header declares that the execution of that method can result in the occurrence of a particular kind of error. Suppose, for example, that the String passed to the constructor for FileReader is not the name of an existing file. It is then impossible for the constructor to complete its task, becase there is no file to associate with the object it is constructing. The mechanism that Java provides for handling such error conditions is that of throwing either exceptions or errors. Java provides a class Throwable (in java.lang) whose subclasses consist of all of the errors or exceptions that might be thrown. Unless the method that encounters the error or exception provides a means of handling it (we will see how to do that shortly), that method immediately terminates, and the error or exception is thrown in its calling method. The subclass Exception contains all of the exceptions that a Java application might want to handle on its own - we will see how to do that shortly. The subclass Error of Throwable contains more serious errors that Java applications should not try to handle. The subclass RuntimeException of Exception contains those exceptions that Java might want to handle in some cases, but which normally can be avoided by careful programming (in fact, the normal cause of these exceptions is an error in the program). An example of a RuntimeException is NullPointerException, which is thrown when a program attempts to access a field or method of a reference variable whose value is null; in a well-written program, such an exception should not occur. The remaining subclasses of Exception are exceptions which ordinarily should be handled somewhere in the program, so any method that can cause one of these exceptions to be thrown must alert the calling program to this fact by declaring that exception in a throws clause. If the calling program does not handle this exception, and does not declare it in a throws clause, a compile-time error will result.
Look at the documentation for FileNotFoundException. It is a subclass of Exception, but not RuntimeException; therefore, it must either be handled by main or declared in the throws clause of main. Notice that that FileNotFoundException is a subclass of IOException, so the clause throws IOException in the header of main includes FileNotFoundException.
The second line of the if block declares and constructs a BufferedWriter, which is associated with the file given as the second command-line argument.
The third line of the if block begins a while statement. The while statement has two possible forms:
while (boolean-expression) statement;or
while (boolean-expression) {statement-list};
where boolean-expression is an expression whose result
is of type boolean, statement is any
statement that can be included in a method body, and
statement-list is any sequence of statements that can
be included in a method body. If boolean-expression
evaluates to true, statement or
statement-list is executed, and the while
statement is executed again; otherwise, the remainder of the
while statement is skipped.
The boolean expression in this while statement is
input.ready()ready is a method of BufferedReader that returns true if there are more characters to be read; therefore, the loop iterates while there remain characters in the input file that have not been read. Notice that ready throws IOException. As we are discovering, several of the methods called within the main method throw IOException or one of its subclasses. By declaring that main throws IOException, we have covered all of these possibilities.
The body of the loop consists of a single statement:
output.write(input.read());
write is a method of BufferedWriter; this method
writes a single character. What is odd about this method is that its
parameter is of type int. The write method writes
the low-order 16 bits of its parameter (Java uses Unicode characters,
which are 16 bits, although the subset which corresponds to ASCII
characters is typically written as a single byte). Why would this
method take an int parameter, if it is going to treat is as a
character? It does this so that the write method is
consistent with the read method of BufferedReader.
Take a look at the documentation for this method. It reads a single
character, but returns an int value. The reason for this is
that this method was designed to handle an attempt to read past the
end of the file by returning a value that will not be confused with a
valid Unicode character, namely -1 (this should be included
in the documentation, but is not). Conversions between char
and int usually are done automatically in Java; however, care
must be taken when invoking a method that is defined differently for
the two different types of paramters. We will consider such an
example later.
To summarize, the while loop repeatedly reads a character from the input file and writes it to the output file until the entire input has been consumed. Finally, both the input file and output file are closed. This is particularly important for a BufferedWriter, because closing the file causes its buffer to be flushed (i.e., all characters remaining in the buffer are written to the file), and updates the directory containing the the file.
Try running this program with a source file that does not exists. Your response will be something like
java.io.FileNotFoundException: badname
at java.io.FileInputStream.(FileInputStream.java)
at java.io.FileReader.(FileReader.java)
at Copy1.main(Copy1.java:6)
where badname is the first command-line argument. This
output contains a stack trace, which usually indicates a bug in
the program. In the next section, we will show how to handle
exceptions so that the stack trace doesn't appear.
import java.io.*;
public class Copy2 {
public static void main(String[] args) {
if (args.length == 2) {
try {
BufferedReader input = new BufferedReader(new FileReader(args[0]));
BufferedWriter output = new BufferedWriter(new FileWriter(args[1]));
while (input.ready()) {
output.write(input.read());
}
input.close();
output.close();
}
catch (IOException e) {
System.out.println(e);
}
}
else {
System.out.println("usage: Copy2 source-file dest-file");
}
}
}
The only differences between Copy1 and Copy2 are:
try {statement-list} catch-clauses
where catch-clauses is one or more clauses of the form
catch (throwable-type identifier) {statement-list}
and statement-list is any sequence of
statements that may be included in a method body. In the
catch clauses, throwable-type is the name of
any subclass of Throwable, and identifier is
a new variable name. The statements in the try block are
executed normally, but if an error or exception is thrown by any of
these statements, control exits the try block. At this
point, execution proceeds as if the following method call were made:
catch (throwable);where throwable is the Throwable object that was thrown. Thus, each of the catch clauses is treated like the definition of a method with return type void. If more than one of the catch clauses has parameter of a type to which the thrown object belongs, the first of these is called, then, assuming the catch clause returns normally, control resumes at the statement following the last catch clause. If none of the parameters of the catch clauses match the type of the thrown object, the try statement throws this object.
Let's see how this works in Copy2. The try block contains several statements that can throw an IOException object (or an object belonging to a subclass of IOException). If one of these exceptions occurs, the catch clause is called, with the exception thrown as its formal parameter e. This IOException object is then printed to standard output, and the main method finishes normally. This is not a very elegant exception handler, but it does handle all IOExceptions so that a stack trace is not generated when they occur (try copying a nonexistent file with program to see the result). Note also that only IOExceptions are handled by this exception handler.
import java.io.*;
public class Copy3 {
public static void main(String[] args) {
if (args.length == 2) {
try {
BufferedReader input = new BufferedReader(new FileReader(args[0]));
BufferedWriter output = new BufferedWriter(new FileWriter(args[1]));
StringBuffer buf = new StringBuffer();
while (input.ready()) {
buf.append((char) input.read());
}
output.write(buf.toString());
input.close();
output.close();
}
catch (IOException e) {
System.out.println(e);
}
}
else {
System.out.println("usage: Copy3 source-file dest-file");
}
}
}
The above program is included only as an illustration, not as an
improvement over Copy2; Copy2 is, in fact a better solution to the
file copying problem. However, this example is included because most
programs that do I/O on text files need to store that text. This
example shows how to do that.
A StringBuffer is similar to a String, except that the contents of a StringBuffer may change. For efficiency reasons, it is best to use a StringBuffer to accumulate the text. Instead of immediately writing each character as it is read, each character is now appended to a StringBuffer. Notice the statement that actually does the appending:
buf.append((char) input.read());
This statement contains an explicit type-cast:
(char). A type-cast causes the expression immediately
following it to be treated as the given type. In this case, the
read method returns and int, but our type-cast
explicitly converts it to type char. We stated earlier that
this conversion is normally done automatically when and int
is passed to a method that requires a char; however, if we
look at the StringBuffer documentation (in the package
java.lang), we see that there are append methods
that take single parameters of both of these types. Thus, if the
type-cast is omitted, the append(int) method will be called,
rather than the append(char) method. The problem with this
is that these two methods have different results. The
append(char) method appends its parameter as a single
character to the StringBuffer. However, the append(int)
method first converts its int parameter to a String giving
the decimal representation of the int, then appends this
String to the StringBuffer. Thus, if the character 'a' were read from
the file and passed as an int to append, the value
appended would be the String "97".
The other statement to notice is
output.write(buf.toString());
The write method of BufferedWriter will take a String parameter, so
there is no need to write the StringBuffer a character at a time. All
we need to do is convert the StringBuffer to a String using its
toString method, which is very efficient as long as we don't make any
subsequent changes to the StringBuffer. The reason for this is that
when converting a StringBuffer to a String, Java allows the two
structures to share the storage containing their character contents
until the contents of the StringBuffer are changed. If and when such
a change occurs, the contents are copied to a new location.
Contents Previous Next
Last modified 8/24/98 - Rod Howell, howell@cis.ksu.edu