|
9. Streams and Exceptions
To be able to read and write files
To understand the concepts of text and binary streams
To learn how to throw and catch exceptions
To be able to process the command line
To be able to read and write objects using serialization
Streams, Readers, and Writers
There are two different ways to store data: text or binary formats.
In text format, data items are represented in human-readable form, as a sequence of characters. For example, the interger 12345 is stored as the sequence of five characters:
‘1’ ‘2’ ‘3’ ‘4’ ‘5’
In binary form, data items are represented in bytes. For example, the integer 12345 is stored as a sequence of four bytes:
0 0 48 57 (because 12345 = 48*256 + 57)
We use the Reader and Writer classes and their subclasses to process text files:
FileReader reader = new FileReader(“input.txt”);
int next = reader.read(); // read() will return int
char c;
if (next != -1) // returns -1 if the end of input
c = (char) next;
FileWriter writer = new FileWriter(“output.txt”);
Text input and output are more convenient for humans.
To read text data from a disk file, we create a FileReader object:
FileReader reader = new FileReader(“input.txt”);
To write text data to a disk file, we use FileWriter:
FileWriter writer = new FileWriter(“Output.txt”);
The Reader class has a method read to read a single character at a time. However, the read method returns an int, thus we have to cast it to a char.
Reader reader = …;
int next = reader.read();
char c;
if (next != -1)
c = (char)next;
We use the InputStream and OutputStream classes and their subclasses to process binary files:
FileInputStream in = new FileInputStream(“input.dat”);
int next = in.read();
byte b;
if (next != -1) // returns -1 if the end of input
b = (byte) next;
FileOutputStream output = new FileOutputStream(“output.dat”);
Binary storage is more compact and more efficient.
To read binary data from a disk file, we create a FileInputStream object:
FileInputStream inputStream = new FileInputStream(“input.dat”);
To write binary data to a disk file, we use FileOutputStream:
FileOutputStream outputStream = new FileOutputStream(“output.dat”);
The InputStream class has a method read to read a single byte at a time. The method also returns an int, thus we have to cast it to a byte.
InputStream in = …;
int next = in.read();
byte b;
if (next != -1)
b = (byte)next;
The Writer and FileOutputStream classes have a write method to write a single character or byte.
Reading and Writing Text Files
We construct a FileWriter object from the file name:
FileWriter writer = new FileWriter(“output.txt”);
We then send a character at a time to the file by calling the write method.
However, the output we have is in the form of numbers and strings so we need another class whose task is to break up numbers and strings into individual characters and send them to a writer.
This class is called PrintWriter.
PrintWriter out = new PrintWriter(writer);
Next, we can use the print and println methods to print numbers, objects, and strings:
out.print(29.95);
out.println(new Rectangle(5, 10, 15, 25));
out.println(“Hello.”);
Reading text files is less convenient, we have to use the BufferedReader class, which has a readLine method .
After reading a line of input, we can convert the strings to integer or double as needed.
Reading and Writing Text Files
To write:
FileWriter writer = new FileWriter(“output.txt”);
PrintWriter out = new PrintWriter(writer);
out.print(29.95);
out.println(new Rectangle(5, 10, 15, 25));
out.println(“Hello, World!”);
To read:
FileReader reader = new FileReader(“input.txt”);
BufferedReader in = new BufferedReader(reader);
String inputLine = in.readLine();
double x = Double.parseDouble(inputLine);
When we are done reading/writing from a file, we should close the files:
reader.close();
writer.close();
Before we can put these input/output classes to work, we need to know about exception handling.
All classes for reading and writing are defined in the java.io package.
Exception Handling
An exception is an indication that a problem occurred during the program’s execution.
When a method detects a problematic situation, what should it do?
The methods should return an indicator whether it succeeded or failed.
The exception-handling mechanism has been designed to solve these problems.
When we detect an error, we only need to throw an appropriate exception object.
The programmer encloses in a try block the code that may generate an exception.
The try block is followed by zero or more catch blocks.
Each catch block specifies the type of exception it can catch and contains an exception handler.
An optional finally block provides code that always executes regardless of whether or not an exception occurs.
If there is no catch blocks following a try block, the finally block is required.
When an exception is thrown, program control leaves the try block and the catch blocks are searched.
If the type of the thrown exception matches the parameter type in one of the catch blocks, the code for that catch block is executed.
If a finally block appears after the last catch block, it is executed regardless of whether or not an exception is thrown.
An exception can be thrown from statements in the method, or from a method called directly or indirectly from the try block.
The point at which the throw is executed is called the throw point.
The throw statement is executed to indicate that an exception has occurred (i.e., a method could not complete successfully).
This is called throwing an exception.
Throwing an exception is simple, the Java library provides many classes to signal all sorts of exceptional conditions.
For example, to signal an unexpected end of file, you can use the EOFException, a subclass of the IOException class, which denotes input/output errors.
We make an object of the exception class, and then throw it.
For example, suppose we write a method that reads a product in from a reader:
public class Product
{ private String name;
private double price;
private int score;
public void read(BufferedReader in)
{ // read product name
name = in.readLine();
// read product price
String inputLine = in.readLine();
price = Double.parseDouble(inputLine);
// read product score
inputLine = in.readLine();
score = Integer.parseInt(inputLine);
}
}
What happens if we reach the end of input?
String inputLine = in.readLine();
if (inputline == null)
// do something
We can throw an exception as follows:
String inputLine = in.readLine();
if (inputline == null)
{ EOFException exception = new EOFException(“End of
file when reading price.”);
throw exception;
}
Or we can just throw the object that the new operator returns:
String inputLine = in.readLine();
if (inputline == null) throw new
EOFException(“End of file when reading price.”);
Throwing a new exception like an example above is used when you want to generate an exception under your control rather than letting Java generate it for you.
Throwing Exceptions
Throwing Exceptions
throw exceptionObject;
throw new IllegalArgumentException();
...
String input = in.readLine();
if (input == null)
throw new EOFException(“EOF when reading”);
When we throw an exception, the method exits immediately. Execution does not continue with the method’s caller but with an exception handler.
Catching Exceptions
When an exception is thrown, an exception handler needs to catch it.
We must install exception handler for all exceptions that our program might throw.
We install an exception handler with the try statement.
Each try block contains one or more method calls that may cause an exception, and catch clauses for all exception types.
Catching Exceptions
try
{ statement
}
catch(ExceptionClass exceptionObject)
{ statement
}
catch(ExceptionClass exceptionObject)
{ statement
}
finally
{ ….
}
if use "try block" you must have whichever "catch" or "finally"
If a try block has a corresponding finally block, the finally block will be executed even if the try block is exited with return, break or continue; then the effect of the return, break or continue will occur.
When the catch(IOException e) block is executed, then some method in the try block has failed with an IOException, and that exception object is stored in the variable e.
try
{ BufferedReader in = new BufferedReader(
new InputStreamReader(System.in));
System.out.println(“How old are you?”);
String input = in.readLine();
int age = Integer.parseInt(input);
age++;
System.out.println(“Next year, you’ll be ” + age);
}
catch(IOException e)
{ System.out.println(“Input/Output error ” + e);
}
catch(NumberFormatException e)
{ System.out.println(“Input was not a number”);
}
The try block contains six statements with two potential exceptions that can be thrown in this code.
The readLine method can throw an IOException, and Integer.parseInt can throw a NumberFormatException.
If either of these exceptions is thrown, the rest of the instructions in the try block are skipped.
Finally
Suppose a method open a file, calls one or more methods, and then close the file:
BufferedReader in;
in = new BufferedReader(new FileReader(“input.txt”));
readProducts(in);
in.close();
Next, suppose that one of the methods throws an exception, then the call to close is never executed.
We solve this problem by placing the call to close inside a finally clause:
BufferedReader in = null;
try
{ in = new BufferedReader(
new FileReader(“input.txt”));
readProducts(in);
}
// optional catch clauses here
finally
{ if (in != null)
in.close();
}
Checked Exception
Exceptions fall into two categories, called checked and unchecked exceptions.
When you call a method that throws a checked exception, you must tell the compiler what we are going to do about the exception if it is thrown.
We do not need to keep track of unchecked exceptions.
Exceptions that belong to subclasses of RuntimeException are unchecked.
Checked Exceptions:
For example: All subclasses of IOException; InterruptedException; invent yourself by subclassing Exception.
Unchecked Exceptions:
For example: RuntimeException and all subclasses; ArithmeticException; IllegalArgumentException; NumberFormatExcetpion; IndexOutOfBoundsException; NullPointerException; invent yourself by subclassing RuntimeException
Checked exceptions must be listed in method's throws clause, if we do not provide catch clause.
Exception Class Hierarchy
Suppose we want to throw an IOException in a method that call BufferedReader.readLine.
The IOException is a checked exception, so we need to tell the compiler what we are going to do about it.
We have two choices:
We can place the call to the readLine inside a try block and supply a catch clause.
We can simply tell the compiler by tagging the method with a throws clause.
Throws Clauses
If we want the exception to go uncaught and rely on the propagation mechanisms to catch the exception elsewhere, we use a throws clause in the method header.
Use a throws clause only if a method will throw one of the checked exceptions.
public class Product
{ public void read(BufferedReader in) throws IOException
{ ... }
}
The throws clause signals the caller that it may encounter an IOException.
If a method can throw multiple checked exceptions, we separate them by commas:
public void read(BufferedReader in) throws IOException, FileNotFoundException
Error do not need to be listed, nor do RuntimeException
Examples: an out-of-bounds array subscript, arithmetic overflow, division by zero, invalid method parameters, memory exhaustion.
Example
In the next example, we pass along all exceptions of type IOException that readLine may throw.
When we encounter an unexpected end of file, we report it as an EOFException.
For an expected end of file, if the end of file has been reached before the start of a record, the method simply returns false.
If the file ends in the middle of a record (an unexpected end of file), then we throw an exception.
public class Product
{ public boolean read(BufferedReader in)
throws IOException
{ // read product name
name = in.readLine();
if (name == null) return false;
String inputLine = in.readLine();
if (inputLine == null)
throw new EOFException (“EOF when reading price.”);
price = Double.parseDouble(inputLine);
inputLIne = in.readLine();
if (inputLine == null)
throw new EOFException (“EOF when reading score.”);
score = Integer.parseInt(inputLine);
return;
}
private String name;
private double price;
private int score;
}
Example
The following method reads product records and puts them into a panel.
It is unconcerned with any exceptions.
If there is a problem with the input file, it simply passes the exception to its caller.
public void readProducts(BufferedReader in)
throws IOException
{ boolean done = false;
while (!done)
{
Product p = new Product();
if (p.read(in))
panel.addProduct(p);
else
done = true;
}
}
Next, we implement the user interaction.
We ask the user to enter a file name and read the product file.
If there is a problem, we report the problem.
The program is not terminated, and the user has a chance to open a different file.
public void openFile()
{ BufferedReader in = null;
try
{ // select file name
. . .
in = new BufferedReader(new FileReader(selectFile));
readProducts(in);
}
catch(FileNotFoundException e)
{ JOptionPane.showMessageDialog
(null, “Bad filename. Try again.”);
}
catch(IOException e)
{ JOptionPane.showMessageDialog
(null, “Corrupted file. Try again.”);
}
finally
{ if (in != null)
try
{ in.close();
}
catch(IOException e)
{ JOptionPane.showMessageDialog
(null, “Error closing file.”);
}
}
}
Example
import java.io.*;
public class StringEx2 {
public static void main(String args[]) throws IOException {
FileWriter writer = null;
try {
String input2;
InputStreamReader reader = new InputStreamReader(System.in);
BufferedReader console = new BufferedReader(reader);
System.out.print("Enter filename ");
input2 = console.readLine();
writer = new FileWriter(input2);
PrintWriter out = new PrintWriter(writer);
double gpa;
System.out.print("Enter Your GPA ");
input2 = console.readLine();
gpa = Double.parseDouble(input2);
System.out.println("Your GPA is " + gpa);
out.println(gpa);
}
catch(NumberFormatException e)
{ System.out.println("Input was not a number.");
}
catch(FileNotFoundException e)
{ System.out.println("Bad filename. Try again.");
}
finally {
writer.close();
}
}
}
Example: Reading Input Data in a Graphical Program
Recall the BestProduct program that reads product data from the keyboard and then marks the best buys.
In this example, we will use a text editor to write the data values into a file and then specify the name of the file when the data are to be used.
This program will display its result in a graph.
Prices grow in the x-direction, performance grows in the y-direction.
// PlotProducts.java
/**
Reads products and adds them to the product panel.
@param in the buffered reader to read from
*/
public void readProducts(BufferedReader in) throws IOException
{ ...
boolean done = false;
while (!done)
{ Product p = new Product();
if (p.read(in))
panel.addProduct(p);
else // last product read
done = true;
}
}
// PlotProducts.java
/**
Handles the open file menu. Prompts user for file
name and reads products from file.
*/
public void openFile()
{ BufferedReader in = null;
try
{ // show file chooser dialog
JFileChooser chooser = new JFileChooser();
if (chooser.showOpenDialog(null)
== JFileChooser.APPROVE_OPTION)
{ // construct reader and read products
File selectedFile = chooser.getSelectedFile();
in = new BufferedReader(new FileReader(selectedFile));
readProducts(in);
}
}
catch(FileNotFoundException e)
{ JOptionPane.showMessageDialog(null, "Bad filename. Try again.");
}
catch(IOException e)
{ JOptionPane.showMessageDialog(null, "Corrupted file. Try again.");
}
finally
{ if (in != null)
try
{ in.close();
}
catch(IOException e)
{ JOptionPane.showMessageDialog(null, "Error closing file.");
}
}
}
Command Line Arguments
There are several methods of starting a program:
By selecting “Run” in the compilation environment
By clicking on an icon
By typing the name of the program at a prompt in a terminal or shell window
The latter method is called “invoking the program from the command line”.
For the latter method, we can also type in additional information that the program can use.
These additional strings are called command line arguments.
For example, if we start a program with the command line
java Prog –v input.txt
then the program receives two command line arguments: the strings “-v” and “input.txt”.
Java usually interprets strings with a – as options and other strings as file names.
Command line arguments are placed in the args parameter of the main method:
public static void main(String[] args)
{ …
}
Thus args contains two strings
args[0] = “-v”
args[1] = “input.txt”
Encryption
Encryption is a method used for scrambling a file so that it is unreadable except to those who know the decryption method and the secret keyword.
The person performing any encryption chooses an encryption key; here we use a number between 1 and 25 as the key.
This key indicates the shift to be used in encrypting each letter.
For example, if the key is 3, replace A with a D, B with an E, and so on.
Encryption
To encrypt/decrpt data
java Crypt input.txt encrypt.txt
java Crypt -d -k11 encrypt.txt output.txt
The program takes to following command line arguments:
- An optional -d flag to indicate decryption
- An optional encryption key, -k flag
- input file name
- output file name
If no key is specified, then 3 is used.
import java.io.*;
public class Crypt
{ public static void main(String[] args)
{ boolean decrypt = false;
int key = DEFAULT_KEY;
FileReader infile = null;
FileWriter outfile = null;
if (args.length < 2 || args.length > 4) usage();
// gather command line arguments and open files
try
{ for (int i = 0; i < args.length; i++)
{ if (args[i].substring(0, 1).equals("-"))
// it is a command line option
{ String option = args[i].substring(1, 2);
if (option.equals("d"))
decrypt = true;
else if (option.equals("k"))
{ key = Integer.parseInt
(args[i].substring(2));
if (key < 1 || key >= NLETTERS)
usage();
}
}
else
{ if (infile == null)
infile = new FileReader(args[i]);
else if (outfile == null)
outfile = new FileWriter(args[i]);
}
}
}
catch(IOException e)
{ System.out.println("Error opening file");
System.exit(0);
}
if(infile == null || outfile == null) usage();
// encrypt or decrypt the input
if (decrypt) key = NLETTERS - key;
try
{ encryptFile(infile, outfile, key);
infile.close();
outfile.close();
}
catch(IOException e)
{ System.out.println("Error processing file");
System.exit(0);
}
}
public static void usage()
{ System.out.println("Usage: java Crypt [-d] [-kn] infile outfile");
System.exit(1);
}
public static char encrypt(char c, int k)
{ if ('a' <= c && c <= 'z')
return (char)('a' + (c - 'a' + k) % NLETTERS);
if ('A' <= c && c <= 'Z')
return (char)('A' + (c - 'A' + k) % NLETTERS);
return c;
}
public static void encryptFile(FileReader in, FileWriter out, int k) throws IOException
{ while (true)
{ int next = in.read();
if (next == -1) return; // end of file
char c = (char)next;
out.write(encrypt(c, k));
}
}
public static final int DEFAULT_KEY = 3;
public static final int NLETTERS = 'z' - 'a' + 1;
}
Random Access
Suppose we want to change the prices of some products that we stored in a file, the simplest technique is:
read data into an array
Update data
Save data back to a file
What happens if the data is very large?
There will be a lot of much reading and writing.
Up til now, we read to a file and write to a file one item at a time, this access pattern is called sequential access.
Next, we will learn how to access specific locations in a file and change just those locations. This access pattern is called random access.
Only disk files support random access. Each disk file has a special file pointer position.
Normally, the file pointer is at the end of the file, and any output is appended to the end.
If we move the file pointer to the middle of the file, the output overwrite what is already there.
Then the next read command starts reading at the file point location.
In Java, we use a RandomAccessFile object a access a file and move a file pointer.
We can open a file either for reading only (“r”) or for reading and writing (“rw”).
To open a random-access file, we supply a file name and a string to specify the open mode.
Random Access File
To open the file product “product.dat” for both reading and writing, we write:
RandomAccessFile f = new RandomAccessFile(“product.dat”, "rw");
To move file pointer to byte n from the beginning of the file:
f.seek(n);
To find out the current position of the file pointer (counted from the beginning of the file):
n = f.getFilePointer();
To find out the number of bytes in a file:
long filelength = f.length();
When the new data is longer than the current data, the update will overwrite the old data.
For example, if the price is increased by 50, the new price has more digits so it overwrites the newline symbol that separates the fields.
We must give each field a fixed size that is sufficiently large.
This also makes it easy to access the nth data in a file, without having to read in the first n-1 data.
When storing numbers in a file with fixed record sizes, it is easier to store them in binary format, not in text format.
The readInt and writeInt methods read and write integers as four-byte quantities.
The readDouble and writeDouble methods process double-precision floating-point numbers as eight-byte quantities.
To read:
int n = f.readInt();
double x = f.readDouble();
char c = f.readChar();
To write:
f.writeInt(n);
f.writeDouble(x);
f.writeChar(c);
Example: See Database.java
- Name: 30 characters at two bytes each (60 bytes)
- Price: one double (8 bytes)
- Score: one int (4 bytes)
import java.io.IOException;
import java.io.RandomAccessFile;
public class Database
{ public static void main(String[] args)
{ ConsoleReader console = new ConsoleReader(System.in);
System.out.println("Please enter the data file name:");
String filename = console.readLine();
try
{ RandomAccessFile file
= new RandomAccessFile(filename, "rw");
long nrecord = file.length() / RECORD_SIZE;
boolean done = false;
while (!done)
{ System.out.println
("Please enter the record to update (1 - "
+ nrecord + "), new record (0), quit (-1)");
int pos = console.readInt();
if (1 <= pos && pos <= nrecord) // update record
{ file.seek((pos - 1) * RECORD_SIZE);
Product p = readProduct(file);
System.out.println("Found " + p.getName()
+ " " + p.getPrice() + " " + p.getScore());
System.out.println("Enter the new price:");
double newPrice = console.readDouble();
p.setPrice(newPrice);
file.seek((pos - 1) * RECORD_SIZE);
writeProduct(file, p);
}
else if (pos == 0) // add record
{ System.out.println("Enter new product:");
String name = console.readLine();
System.out.println("Enter price:");
double price = console.readDouble();
System.out.println("Enter score:");
int score = console.readInt();
Product p = new Product(name, price, score);
file.seek(nrecord * RECORD_SIZE);
writeProduct(file, p);
nrecord++;
}
else if (pos == -1)
done = true;
}
file.close();
}
catch(IOException e)
{ System.out.println("Input/Output Exception " + e); }
}
public static String readFixedString(RandomAccessFile f,
int size) throws IOException
{ String b = "";
for (int i = 0; i < size; i++)
b += f.readChar();
return b.trim();
}
public static void writeFixedString(RandomAccessFile f,
String s, int size) throws IOException
{ if (s.length() <= size)
{ f.writeChars(s);
for (int i = s.length(); i < size; i++)
f.writeChar(' ');
}
else
f.writeChars(s.substring(0, size));
}
public static Product readProduct(RandomAccessFile f)
throws IOException
{ String name = readFixedString(f, NAME_SIZE);
double price = f.readDouble();
int score = f.readInt();
return new Product(name, price, score);
}
public static void writeProduct(RandomAccessFile f,
Product p) throws IOException
{ writeFixedString(f, p.getName(), NAME_SIZE);
f.writeDouble(p.getPrice());
f.writeInt(p.getScore());
}
public static final int NAME_SIZE = 30;
public static final int CHAR_SIZE = 2;
public static final int INT_SIZE = 4;
public static final int DOUBLE_SIZE = 8;
public static final int RECORD_SIZE
= CHAR_SIZE * NAME_SIZE + DOUBLE_SIZE + INT_SIZE;
}
Object Streams
We can read and write complete objects with the ObjectInputStream and ObjectOutputStream
Objects are saved in binary format; hence we use streams, not writers.
For example, we write a Product object to a file as follows:
Product p = … ;
ObjectOutputStream out = new ObjectOutputStream
(new FileOutputStream(“products.dat”));
out.writeObject(p);
To read objects:
ObjectInputStream in = new ObjectInputStream
(new FileInputStream(“products.dat”));
Product p = (Product)in.readObject( );
We can store a whole bunch of objects in an array or vector, or inside another object, and then save that object:
Vector v = new Vector();
// now add many Product objects into v
out.writeObject(v);
We can read all of objects:
Vector v = (Vector)in.readObject( );
The objects must belong to classes that implement Serializable interface:
class Product implements Serializable
{ … }
To save data to disk, it is best to put them all into one object and save that object.
class Catalog implements Serializable
{ public void addProduct(Product p) {… }
private Vector products;
}
class ProductEditor
{ public void saveFile()
{ ObjectOutputStream out = …;
out.writeObject(productCatalog);
out.close();
}
public void loadFile()
{ ObjectInputStream in = … ;
productCatalog = (Catalog)in.readObject();
in.close();
}
private Catalog productCatalog;
} |
|