Friday, February 18, 2011

Chapter 34: File Navigation and I/O

I/O has had a strange history with the SCJP certification. It was included in all the versions of the exam up to and including 1.2, then removed from the 1.4 exam, and then re-introduced for Java 5 and extended for Java 6.

I/O is a huge topic in general, and the Java APIs that deal with I/O in one fashion or another are correspondingly huge. A general discussion of I/O could include topics such as file I/O, console I/O, thread I/O, high-performance I/O, byte-oriented I/O, character-oriented I/O, I/O filtering and wrapping, serialization, and more. Luckily for us, the I/O topics included in the Java 5 exam are fairly well restricted to file I/O for characters, and serialization.

So, lets get started!!!

Exam Tip:
Stream classes are used to read and write bytes, and Readers and Writers are used to read and write characters. Since all of the file I/O on the exam is related to characters, if you see API class names containing the word “Stream”, for instance DataOutputStream, then the question is probably about serialization, or something unrelated to the actual I/O objective.

Creating Files Using Class File

Objects of type File are used to represent the actual files (but not the data in the files) or directories that exist on a computer’s physical disk. Just to make sure we’re clear, when we talk about an object of type File, we’ll say File, with a capital F. When we’re talking about what exists on a hard drive, we’ll call it a file with a lowercase f (unless it’s a variable name in some code). Let’s start with a few basic examples of creating files, writing to them, and reading from them. First, let’s create a new file and write a few lines of data to it:

import java.io.*;

class Writer1 {
public static void main(String [] args) {
File file = new File("myFirstFile.txt"); // There's no file yet!
}
}

If you compile and run this program, when you look at the contents of your current directory, you’ll discover absolutely no indication of a file called myFirstFile.txt. When you make a new instance of the class File, you’re not yet making an actual file, you’re just creating a filename. Once you have a File object, there are several ways to make an actual file. Let’s see what we can do with the File object we just made:

import java.io.*;

class Writer1 {
public static void main(String [] args) {
try { // warning: exceptions possible
boolean newFile = false;
File file = new File // it's only an object
("myFirstFile.txt");
System.out.println(file.exists()); // look for a real file
newFile = file.createNewFile(); // maybe create a file!
System.out.println(newFile); // already there?
System.out.println(file.exists()); // look again
} catch(IOException e) { }
}
}

This produces the output
false
true
true

And also produces an empty file in your current directory. If you run the code a second time you get the output
true
false
true

Let’s examine these sets of output:
• First execution The first call to exists() returned false, which we expected...remember new File() doesn’t create a file on the disk! The createNewFile() method created an actual file, and returned true, indicating that a new file was created, and that one didn’t already exist. Finally, we called exists() again, and this time it returned true, indicating that the file existed on the disk.
• Second execution The first call to exists() returns true because we built the file during the first run. Then the call to createNewFile() returns false since the method didn’t create a file this time through. Of course, the last call to exists() returns true.

A couple of other new things happened in this code. First, notice that we had to put our file creation code in a try/catch. This is true for almost all of the file I/O code you’ll ever write. I/O is one of those inherently risky things. We’re keeping it simple for now, and ignoring the exceptions, but we still need to follow the handle-or-declare rule since most I/O methods declare checked exceptions. We’ll talk more about I/O exceptions later. We used a couple of File’s methods in this code:
• boolean exists() This method returns true if it can find the actual file.
• boolean createNewFile() This method creates a new file if it doesn’t already exist.

Using FileWriter and FileReader

In practice, you probably won’t use the FileWriter and FileReader classes without wrapping them. That said, let’s go ahead and do a little “naked” file I/O: (Don’t worry about Wrapping. You’ll see it shortly in this very chapter)

import java.io.*;

class TestFileWriter {
public static void main(String [] args) {
char[] in = new char[50]; // to store input
int size = 0;
try {
File file = new File("MySecondTestFile.txt");
FileWriter fw = new FileWriter(file);
// create an actual file & a FileWriter obj
fw.write("hey\npeople\n");
// write characters to the file
fw.flush(); // flush before closing
fw.close(); // close file when done
FileReader fr = new FileReader(file);
// create a FileReader object
size = fr.read(in); // read the whole file!
System.out.print(size + " "); // how many bytes read
for(char c : in) // print the array
System.out.print(c);
fr.close(); // again close
} catch(IOException e) { }
}
}

which produces the output:
12 hey
people

Here’s what just happened:

1. FileWriter fw = new FileWriter(file) did three things:
a. It created a FileWriter reference variable, fw.
b. It created a FileWriter object, and assigned it to fw.
c. It created an actual empty file out on the disk (and you can prove it).
2. We wrote 12 characters to the file with the write() method, and we did a flush() and a close().
3. We made a new FileReader object, which also opened the file on disk for reading.
4. The read() method read the whole file, a character at a time, and put it into the char[] in.
5. We printed out the number of characters we read size, and we looped through the in array printing out each character we read, then we closed the file.
Before we go any further let’s talk about flush() and close(). When you write data out to a stream, some amount of buffering will occur, and you never know for sure exactly when the last of the data will actually be sent. You might perform many write operations on a stream before closing it, and invoking the flush() method guarantees that the last of the data you thought you had already written actually gets out to the file. Whenever you’re done using a file, either reading it or writing to it, you should invoke the close() method. When you are doing file I/O you’re using expensive and limited operating system resources, and so when you’re done, invoking close() will free up those resources.

This program that we just saw certainly works, but it’s painful in a couple of different ways:
1. When we were writing data to the file, we manually inserted line separators (in this case \n), into our data.
2. When we were reading data back in, we put it into a character array. Since it was an array, we had to declare its size beforehand, so we’d have been in trouble if we hadn’t made it big enough! We could have read the data in one character at a time, looking for the end of file after each read(), but that’s pretty painful too.
Because of these limitations, we’ll typically want to use higher-level I/O classes like BufferedWriter or BufferedReader in combination with FileWriter or FileReader.

Combining I/O Classes

Java’s entire I/O system was designed around the idea of using several classes in combination. Combining I/O classes is sometimes called wrapping and sometimes called chaining. The java.io package contains about 50 classes, 10 interfaces, and 15 exceptions. Each class in the package has a very specific purpose, and the classes are designed to be combined with each other in many ways, in order to handle a wide variety of situations.

When it’s time to do some I/O in real life, you’ll find yourself going over the java.io API, trying to figure out which classes you’ll need, and how to hook them together. For the exam, however, we need not go through all of them. It is enough if we just concentrate on the below classes.
a. File
b. FileWriter
c. BufferedWriter
d. PrintWriter
e. FileReader
f. BufferedReader

Details of the above classes can be found in table format below:

Class Parent Class Constructors Important Methods
File Object File(File, String);
File(String);
File(String, String)
createNewFile()
delete()
exists()
isDirector()
isFile()
list()
mkdir()
renameTo()
FileWriter Writer FileWriter(File);
FileWriter(String);
close()
flush()
write()
BufferedWriter Writer Writer(Writer); close()
flush()
newLine()
write()
PrintWriter Writer PrintWriter(File);
PrintWriter(String);
PrintWriter(OutputStream);
PrintWriter(Writer)
close()
flush()
format()
printf()
print()
println()
write()
FileReader Reader FileReader(File);
FileReader(String);
read()
BufferedReader Reader BufferedReader(File);
BufferedReader(String);
read()
readLine()
Now let’s say that we want to find a less painful way to write data to a file and read the file’s contents back into memory. Starting with the task of writing data to a file, here’s a process for determining what classes we’ll need, and how we’ll hook them together:
1. We know that ultimately we want to hook to a File object. So whatever other class or classes we use, one of them must have a constructor that takes an object of type File.
2. Find a method that sounds like the most powerful, easiest way to accomplish the task. When we look at the table above, we can see that BufferedWriter has a newLine() method. That sounds a little better than having to manually embed a separator after each line, but if we look further we see that PrintWriter has a method called println(). That sounds like the easiest approach of all, so we’ll go with it.
3. When we look at PrintWriter’s constructors, we see that we can build a PrintWriter object if we have an object of type File, so all we need to do to create a PrintWriter object is the following:

File file = new File("MySecondTestFile.txt");
// create a File
PrintWriter pw = new PrintWriter(file);
// pass file to the PrintWriter constructor


At this point it should be fairly easy to put together the code to more easily read data from the file back into memory. Again, looking through the table, we see a method called readLine() that sounds like a much better way to read data. Going through a similar process we get the following code:

File file = new File("MySecondTestFile.txt");
// create a File object AND open "MySecondTestFile.txt"

FileReader fr = new FileReader(file);
// create a FileReader to get data from 'file'

BufferedReader br = new BufferedReader(fr);
// create a BufferReader to get its data from a Reader

String data = br.readLine(); // read some data

Exam Tip:
You’re almost certain to encounter exam questions that test your knowledge of how I/O classes can be chained. If you’re not totally clear on this last section, we recommend that you use the table of details reg. these classes above as a reference, and write code to experiment with which chaining combinations are legal and which are illegal.

Working with Files and Directories

Earlier we touched on the fact that the File class is used to create files and directories. In addition, File’s methods can be used to delete files, rename files, determine whether files exist, create temporary files, change a file’s attributes, and differentiate between files and directories. A point that is often confusing is that an object of type File is used to represent either a file or a directory. We’ll talk about both cases next.

We saw earlier that the statement
File file = new File("DoSomething");

creates a File object, and then does one of two things:
1. If “DoSomething” does NOT exist, no actual file is created.
2. If “DoSomething” does exist, the new File object refers to the existing file.
Notice that File file = new File("DoSomething"); NEVER creates an actual file. There are two ways to create a file:
1. Invoke the createNewFile() method on a File object. For example:
2. File file = new File("DoSomething"); // no file yet
3. file.createNewFile();
// make a file, "DoSomething" which is assigned to 'file'
4. Create a Writer or a Stream. Specifically, create a FileWriter, a PrintWriter, or a FileOutputStream. Whenever you create an instance of one of these classes, you automatically create a file, unless one already exists, for instance
File file = new File("DoSomething"); // no file yet
PrintWriter pw = new PrintWriter(file);
// make a PrintWriter object AND make a file, "DoSomething" to which
// 'file' is assigned, AND assign 'pw' to the PrintWriter

Creating a directory is similar to creating a file. Again, we’ll use the convention of referring to an object of type File that represents an actual directory, as a Directory File object, capital D, (even though it’s of type File.) We’ll call an actual directory on a computer a directory, small d. Phew! As with creating a file, creating a directory is a two-step process; first we create a Directory (File) object, then we create an actual directory using the following mkdir() method:

File myTestDir = new File("myTestDir"); // create an object
myTestDir.mkdir(); // create an actual directory

Once you’ve got a directory, you put files into it, and work with those files:
File myFile = new File(myTestDir, "myTestFile.txt");
myFile.createNewFile();

This code is making a new file in a subdirectory. Since you provide the subdirectory to the constructor, from then on you just refer to the file by its reference variable. In this case, here’s a way that you could write some data to the file myFile:

PrintWriter pw = new PrintWriter(myFile);
pw.println("new stuff");
pw.flush();
pw.close();

Be careful when you’re creating new directories! As we’ve seen, constructing a Writer or a Stream will often create a file for you automatically if one doesn’t exist, but that’s not true for a directory:

File myTestDir = new File("myTestDir");
// myTestDir.mkdir(); // call to mkdir() commented out
File myFile = new File(myTestDir, "myTestFile.txt");
myFile.createNewFile(); // exception if no mkdir!

This will generate an exception something like
java.io.IOException: No such file or directory

You can refer a File object to an existing file or directory. For example, assume that we already have a subdirectory called myOldDir in which resides an existing file myOldDirFile.txt, which contains several lines of text. When you run the following code

File myOldDir = new File("myOldDir"); // assign a dir
System.out.println(myOldDir.isDirectory());

File myOldDirFile = new File(myOldDir, "myOldDirFile.txt"); // assign a file

System.out.println (myOldDirFile.isFile());

FileReader fr = new FileReader(myOldDirFile);
BufferedReader br = new BufferedReader(fr); // make a Reader

String s;
while( (s = br.readLine()) != null) // read data
System.out.println(s);

br.close();

the following output will be generated:
true
true
existing sub-dir data
line 2 of text
line 3 of text

Take special note of what the readLine() method returns. When there is no more data to read, readLine() returns a null—this is our signal to stop reading the file. Also, notice that we didn’t invoke a flush() method. When reading a file, no flushing is required, so you won’t even find a flush() method in a Reader kind of class.

In addition to creating files, the File class also lets you do things like renaming and deleting files. The following code demonstrates a few of the most common ins and outs of deleting files and directories (via delete()), and renaming files and directories (via renameTo()):
File dirToDelete = new File("dirToDelete"); // make a directory
dirToDelete.mkdir();

File delFile1 = new File(dirToDelete, "delFile1.txt");
// add file to directory

delFile1.createNewFile();

File delFile2 = new File(dirToDelete, "delFile2.txt");
// add file to directory
delFile2.createNewFile();
delFile1.delete(); // delete a file
System.out.println("dirToDelete is " + dirToDelete.delete());
// attempt to delete the directory

File newName = new File(dirToDelete, "newName.txt"); // a new object
delFile2.renameTo(newName); // rename file

File newDir = new File("newDir"); // rename directory
dirToDelete.renameTo(newDir);

This outputs

dirToDelete is false

and leaves us with a directory called newDir that contains a file called newName.txt. Here are some rules that we can deduce from this result:
• delete() You can’t delete a directory if it’s not empty, which is why the invocation dirToDelete.delete() failed.
• renameTo() You must give the existing File object a valid new File object with the new name that you want. (If newName had been null we would have gotten a NullPointerException.)
• renameTo() It’s okay to rename a directory, even if it isn’t empty.
There’s a lot more to learn about using the java.io package, but as far as the exam goes we only have one more thing to discuss, and that is how to search for a file. Assuming that we have a directory named SearchTest that we want to search through, the following code uses the File.list() method to create a String array of files and directories, which we then use the enhanced for loop to iterate through and print:

String[] files = new String[100];
File search = new File("SearchTest");
files = search.list(); // create the list

for(String fn : files) // iterate through it
System.out.println("found " + fn);

On our system, we got the following output:
found dir1
found dir2
found dir3
found file1.txt
found file2.txt
Your results will almost certainly vary :)

In this section we’ve scratched the surface of what’s available in the java.io package. Entire books have been written about this package, so we’re obviously covering only a very small (but frequently used) portion of the API. On the other hand, if you understand everything we’ve covered in this section, you will be in great shape to handle any java.io questions you encounter on the exam

The java.io.Console Class

New to Java 6 is the java.io.Console class. In this context, the console is the physical device with a keyboard and a display. If you’re running Java SE 6 from the command line, you’ll typically have access to a console object, to which you can get a reference by invoking System.console(). Keep in mind that it’s possible for your Java program to be running in an environment that doesn’t have access to a console object, so be sure that your invocation of System.console() actually returns a valid console reference and not null.

The Console class makes it easy to accept input from the command line, both echoed and nonechoed (such as a password), and makes it easy to write formatted output to the command line. It’s a handy way to write test engines for unit testing or if you want to support a simple but secure user interaction and you don’t need a GUI.

On the input side, the methods you’ll have to understand are readLine and readPassword. The readLine method returns a string containing whatever the user keyed in—that’s pretty intuitive. However, the readPassword method doesn’t return a string: it returns a character array. Here’s the reason for this: Once you’ve got the password, you can verify it and then absolutely remove it from memory. If a string was returned, it could exist in a pool somewhere in memory and perhaps some nefarious hacker could find it.

Let’s take a look at a small program that uses a console to support testing another class:
import java.io.Console;

public class TestJavaConsole {
public static void main(String[] args) {
Console c = System.console(); // #1: get a Console
char[] pw;
pw = c.readPassword("%s", "pw: "); // #2: return a char[]
for(char ch: pw)
c.format("%c ", ch); // #3: format output
c.format("\n");

MyUtilClass mu = new MyUtilClass();
while(true) {
name = c.readLine("%s", "input?: "); // #4: return a String

c.format("output: %s \n", mu.doSomething(name));
}
}
}

class MyUtilClass { // #5: class to test
String doSomething(String arg1) {
// stub code
return "result is " + arg1;
}
}

Let’s review this code:
• At line 1, we get a new console object. Remember that we can’t say this:
Console c = new Console();
• At line 2, we invoke readPassword, which returns a char[], not a string. You’ll notice when you test this code that the password you enter isn’t echoed on the screen.
• At line 3, we’re just manually displaying the password you keyed in, separating each character with a space. Later on in this chapter, you’ll read about the format method, so stay tuned.
• At line 4, we invoke readLine, which returns a string.
• At line 5 is the class that we want to test. Later in this chapter, when you’re studying regex and formatting, we recommend that you use something like TestJavaConsole to test the concepts that you’re learning.
The Console class has more capabilities than are covered here, but if you understand everything discussed so far, you’ll be in good shape for the exam.

Previous Chapter: Chapter 33 - StringBuffer and StringBuilder

Next Chapter: Chapter 34 - Serialization

No comments:

Post a Comment

© 2013 by www.inheritingjava.blogspot.com. All rights reserved. No part of this blog or its contents may be reproduced or transmitted in any form or by any means, electronic, mechanical, photocopying, recording, or otherwise, without prior written permission of the Author.

ShareThis

Google+ Followers

Followers