Weekly Plans and Assignments: 1. Thursday(11/12): Introduction to Ch.6 - FileIO in Java. Reading from a data file and writing to the file. Here is the DataFile for the TimeLine Program. Here is a program that reads a data file line by line, FileReaderDemo. HW: Program 6_01, the Timeline Program. 2. Friday(11/13): Class Presentations of Prog6_01 Timeline, and hand tracing of algorithmic code. Here is the DataFile for the Basketball Tryout Stats Program. HW: Program 6_02 BasketballTryoutStats. 3. Monday(11/16): Class Presentations of Prog6_02 BasketballTryoutStats, and hand tracing of algorithmic code. Here is the DataFile for the Business Accounts Program. Here is a program that reads a data file character by character, FileReaderDemo2. HW: Program 6_03 BusinessAccounts. 4. Tuesday(11/17): Presentations of Prog6_03 BusinessAccounts, and hand tracing of Java programs. Here is the DataFile for the Multiple-Choice Test Corrector Program. HW: Program6_04 TestCorrect. 3. Wednesday(11/18): Presentations of Prog6_04 and hand-tracing of Java programs. Here is the DataFile for the DVD Search Program. HW: Program6_05 DVDSearch.4. Thursday(11/19): Presentations of Prog6_05 and hand-tracing of Java programs. Here is a program that transfers data from one file to another, FileAccess_out. HW: Program6_06 FileDataTransfer. 5. Friday(11/20): Class-work Handout #1 on review of Java and related topics. Here is a program that accepts String data from the keyboard and and then writes it to a file, FileOutputDemo. HW: Program6_07 ToolInventory. 6. Monday(11/23): Class-work Handout #2 on review of Java and related topics. HW: Program 6_08 BusinessAccountsII.7. Tuesday(11/24): Test on Ch.6 - FileIo in Java. HW: Program6_09 ToolInventoryII. (Click here for assignment details.) Very Important: If you have any questions or were absent from class, see me before school (8:00 - 8:30 AM), during Lunch, or after school. Best to send an email to rpersin@fau.edu.
Website Notes: AP Comp Sci A – Java. I. File I/O Basics:Since we are relatively new to programming file operations, there are a couple of things that may not be apparent and can be a source of confusion so let's clarify them before we go any further. 1. Let's consider the nature of a file. Once you have written data to a file, you can regard it as just a linear sequence of bytes. The bytes in a file are referenced by their offset from the beginning, so the first byte is byte 0, the next byte is byte 1, the third byte is byte 2, and so on through to the end of the file. 2. If there are n bytes in a file the last byte will be at offset n-1. There is no specific information in the file about how the data originated or what it represents unless you explicitly put it there. Even if there is, you need to know that it's there and read and interpret the data accordingly. 3. For instance if you write a series of 25 binary values of type int to a file, it will contain 100 bytes. There will be nothing in the file to indicate that the data consists of four byte integers so there is nothing to prevent you from reading the data back as 50 Unicode characters or 10 long values followed by a string, or any other arbitrary collection of data items that corresponds to 100 bytes. 4. Of course, the result is unlikely to be very meaningful unless you interpret the data in the form in which it was written. This implies that to read data from a file correctly, you need to have prior knowledge of the structure and format of the data. 5. There are many ways in which the form of the data in the file may be recorded or implied. For instance, one way that the format of the data in a file can be communicated is to use an agreed file name extension for data of a particular kind, such as .java or .gif or .wav. Each type of file has a predefined structure so from the file extension you know how to interpret the data in the file. 6. You can access an existing file to read from or write to it in two different ways, described as sequential access or random access. The latter is sometimes referred to as direct access. 7. Sequential access to a file is quite straightforward and works pretty much as you would expect. Sequential read access involves reading bytes from the file starting from the beginning with byte 0. Of course, if you are only interested in the file contents starting at byte 100, you can just read and ignore the first 100 bytes. 8. Sequential write access involves writing bytes to the file either starting at the beginning if you are replacing the existing data or at the end if you are appending new data to the file. 9. The term random access is often misunderstood initially. Just like sequential access, random access is just a way of accessing data in a file and has nothing to do with how the data in the file is structured or how the physical file was originally written. 10. You can access any file randomly for reading and/or writing. When you access a file randomly, you can read one or more bytes from the file starting at any point. For instance, you could read 20 bytes starting at the thirteenth byte in the file (which will be the byte at offset 12 of course), then read 50 bytes starting at the 101st byte or any other point that you choose. 11. Similarly, you can update an existing file in random access mode by writing data starting at any point in the file. In random access mode, the choice of where to start reading or writing and how many bytes you read or write, is entirely up to you. 12. You just need to know the offset for the byte where a read or write operation should start. Of course, for these to be sensible and successful operations, you have to have a clear idea of how the data in the file is structured. II. File Input and Output: 1. The new file I/O capabilities introduced in Java 1.4 provide the potential for substantially improved performance at the cost of some slight increase in complexity over the I/O facilities of previous releases. 2. There are three kinds of objects involved in reading and writing files using the new I/O capability:
working with. You use these for files to which you want to write. We will also be using FileInputStream objects for files that we want to read.
file, or from which you get the data that has been read.
reading or writing of the data using one or more buffer objects. 3. The process for writing and reading files is basically quite simple. To write to a file, you load data into one or more buffers that you have created, and then call a method for the channel object to write the data to the file that is encapsulated by the file stream. To read from a file, you call a method for the channel object to read data from the file into one or more buffers, and then retrieve the data from the buffers. 4. There are four classes defined in the java.io package that we will be using when we are working with files. As we have said, the FileInputStream and FileOutputStream classes define objects that provide access to a file for reading or writing respectively. 5. You use an object of type RandomAccessFile when you want to access a file randomly, or when you want to use a single channel to both read from and write to a file. 6. You will see from the SDK documentation for the FileInputStream, FileOutputStream, and RandomAccessFile classes that they each provide methods for I/O operations. However, we will ignore these, as we will be using the services of a file channel to perform operations with objects of these stream classes. 7. The only method from these classes that we will be using is the close() method, that closes the file and any associated channel. III. File Channels. 1. These were introduced in the 1.4 release of Java to provide a faster capability for input and output operations with files, network sockets, and piped I/O operations between programs than the methods provided by the stream classes. We will only be discussing channels in the context of files. 2. The channel mechanism can take advantage of buffering and other capabilities of the underlying operating system and therefore is considerably more efficient than using the operations provided directly within the file stream classes. 3. As we said earlier, a channel transfers data between a file and one or more buffers. We will take a quick look at the overall relationship between the various classes that define channels and buffers, and then look into the details of how you use channels with file streams. 4. There are a considerable number of classes and interfaces defining both channels and buffers. They also have similar names such as ByteBuffer and ByteChannel. Of course, File and file stream objects are also involved in file I/O operations so you will be using at least four different types of objects working together when you read from or write to files. 5. Just to clarify what they all do, here's a summary of the essential role of each of them in file operations:
object encapsulating a file path can be used to construct a file stream object.
channel. A FileOutputstream object encapsulates a file that can be written by a channel.
and written by a channel.
be written to a file using the buffer's put() methods.
from a file.
RandomAccessFile object.
methods with a buffer or buffers as the source or destination of data. 6. The channel interfaces and classes that we will be using are in the java.nio.channels package. The classes that define buffers are defined in the java.nio package. In a program that reads or writes files we will therefore need import statements for class names from at least three packages, the two packages we have just introduced plus the java.io package. IV. File Buffers.1. All the classes that define buffers have the abstract class Buffer as a base. The Buffer class therefore defines the fundamental characteristics common to all buffers. 2. A particular buffer can store a sequence of elements of a given type, and an element can be of any primitive data type other than boolean. Thus, you can create buffers to store byte values, char values, short values, int values, long values, float values, or double values. 3. We keep repeating except for type boolean every so often, so we had better address that. The various types of buffers only provide for the numerical data types and type boolean does not fit into this category. 4. Of course, you may actually want to record some boolean values in a file. In this case, you have to devise a alternative representation. You could use integer values 0 and 1, or perhaps strings "true" and "false", or even characters 't' and 'f'. 5. You could even represent a boolean value as a single bit and pack eight of them at a time into a single byte, but this is only likely to be worthwhile if you have a lot of them. Which approach you choose will depend on what is most convenient in the context in which you are using them. 6. While we have seven different classes defining buffers, a channel only uses buffers of type ByteBuffer to read or write data. The other types of buffers are called view buffers, because they are usually created as views of an existing buffer of type ByteBuffer. 7. Each type of buffer stores elements of a specific kind – a ByteBuffer object holds bytes, a LongBuffer object holds integers of type long, and so on for the other buffer types. 8. The capacity of a buffer is the maximum number of elements it can contain, not the number of bytes – unless, of course, it stores elements of type byte. The capacity of a buffer is fixed when you create it and cannot be changed subsequently. You can obtain the capacity for a buffer object as a value of type int by calling the capacity() method that it inherits from the Buffer class. 9. Of course, for a buffer that stores bytes, the capacity will be the maximum number of bytes it can hold, but for a buffer of type DoubleBuffer for instance that stores double values, the capacity will be the maximum number of double values you can put in it. Elements in a buffer are indexed from zero so the index position for referencing elements in a buffer runs from 0 to capacity-1. 10. A buffer also has a limit and a position, both of which affect read/ write operations executed by a channel using the buffer. 11. The position is the index position of the next buffer element to be read or written. This sounds a little strange, but keep in mind that a buffer can be for file input or output. For example, with a buffer used for output, the position identifies the next element to be written to the file. For a buffer used for file input, the position identifies where the next element read from the file will be stored. 12. The limit is the index position of the first element that should not be read or written. Thus, elements can be read or written starting with the element at position, and up to and including the element at limit-1. 13. Thus if you want to fill all the elements in a buffer, the position must be at zero since this is where the first data item will go, and the limit must be equal to the capacity since the last data item has to be stored at the last element in the buffer, which is capacity-1. 14. A buffer's position and limit are used for determining what elements are involved in a read or write operation executed by a channel. How they affect I/O operations is easier to understand if we take a specific example. Let's first consider an operation that writes data from the buffer to a file. 15. When a file write operation is executed by a channel using a given buffer, elements from the buffer will be written to the file starting at the index specified by the position. Successive elements will be written to the file up to, and including, the element at index position limit-1. 16. For a read operation, data that is read from the file is stored in a buffer starting at the element index given by the buffer position. Elements will continue to be read, assuming they are available from the file, up to the index position limit-1. 17. Thus when you want to write all the data from a buffer, the limit will have to be equal to the capacity. In this case the limit will be an index value that is one beyond the index value for the last element in the buffer, so limit-1 will refer to the last element. 18. The position and limit are involved when you load data into a buffer or retrieve data from it. The position specifies where the next element should be inserted in a buffer or retrieved from it. As we shall see, you will usually have the position automatically incremented to point to the next available position when you insert or extract elements in a buffer. 19. The limit acts as a constraint to indicate where the data in a buffer ends, a bit like an end-of-file marker. You cannot insert or extract elements beyond the position specified by the limit. 20. Since a buffer's position is an index, it must be greater than or equal to zero. You can also deduce that it must also be less than or equal to the limit. Clearly, the limit cannot be greater than the capacity of a buffer. 21. Otherwise, we could be trying to write elements to positions beyond the end of the buffer. However, as we have seen, it can be equal to it. These relationships can be expressed as: 0 • position • limit • capacity V. File Buffer Methods. 1. As a general rule, if your code attempts to do things directly or indirectly that result in certain conventions being violated, an exception will be thrown. 2. When you create a new independent buffer, its capacity will be fixed at a value that you specify. It will also have a position of zero and its limit will be set to its capacity. When you create a view buffer from an existing ByteBuffer, the contents of the view buffer starts at the current position for the ByteBuffer. 3. The capacity and limit for the view buffer will be set to the limit for the original buffer, divided by the number of bytes in an element in the view buffer. The limit and position for the view buffer will subsequently be independent of the limit and position for the original buffer. 4. Of course, before you can use a channel to write the contents of a buffer to a file, you need to load the buffer with the data. Methods for loading data into a buffer are referred to as put methods. Similarly, when a channel has read data from a file into a buffer, you are likely to want to retrieve the data from the buffer. In this case you use the buffer's get methods. 5. There are two kinds of operations that transfer data elements to or from a buffer. A relative put or get operation transfers one or more elements starting at the buffer's current position. In this case the position is automatically incremented by the number of elements transferred. In an absolute put or get operation, you explicitly specify an index for the position in the buffer where the data transfer is to begin. In this case the buffer's position will not be updated so it will remain at the index value it was before the operation was executed. 6. The ByteBuffer class and all the view buffer classes have two put() methods for transferring a single element of the buffer's type to the buffer. One is a relative put() method that transfers an element to a given index position in the buffer and the other is an absolute put method that places the element at an index position that you specify as an argument. 7. They also have three relative put methods for bulk transfer of elements of the given type. Each of these methods returns a reference to the buffer for which they were called. If the buffer is read-only, any of these methods will throw an exception of type ReadOnlyBufferException. 8. We will see how a buffer can be read-only when we discuss Using View Buffers in more detail. Each Buffer class that stores elements of a given basic type – CharBuffer, DoubleBuffer, or whatever – will have put() methods analogous to these, but with arguments of a type appropriate to the type of element in the buffer. 9. The ByteBuffer class has some extra methods that enable you to transfer binary data of other primitive types to the buffer. To start with, we will be using the simplest write() method for a file channel that writes the data contained in a single ByteBuffer object to a file. 10. The number of bytes written to the file is determined by the buffer's position and limit when the write() method executes. Bytes will be written starting with the byte at the buffer's current position. The number of bytes written is limit-position, which is the number returned by the remaining() method for the buffer object. 11. The write() method returns the number of bytes written as a value type int. A write() method for a channel will only return when the write operation is complete, but this does not guarantee that the data has actually been written to the file. 12. Some of the data may still reside in the native I/O buffers. If the data you are writing is critical and you want to minimize the risk of losing it in the event of a system crash, you can force all outstanding output operations to a file that were previously executed by the channel to be completed by calling the force() method for the FileChannel object. 13. The force() method will throw a ClosedChannelException if the channel is closed, or an IOException if some other I/O error occurs. Note that the force() method only guarantees that all data will be written for a local storage device. 14. Only one write operation can be in progress for a given file channel at any time. If you call write() while a write() operation initiated by another thread is in progress, your call to the write() method will block until the write that is in progress has been completed. 15. We will just write a series of useful statements to a file called statmnts.txt, that we will create in the directory MyProjects on drive C:. If you want to write to a different drive and/or directory, just change the program accordingly. Here's the code: import java.io.*; import java.nio.*; import java.nio.channels.FileChannel; public class WriteProverbs { public static void main(String[] args) { String dirName = "c:/MyProjects"; // Directory for the output file String fileName = "statmnts.txt"; // Name of the output file String[] sayings = { "I think therefore I am.", "Only the mediocre are always at their best.", "A little knowledge is a dangerous thing.", "Calculate what you talk about.", "Come to class with the knowledge of the day before.", "Who knows most says least.", "Success is equal to preparation plus opportunity." }; File aFile = new File(dirName, fileName); FileOutputStream outputFile = null; try { outputFile = new FileOutputStream(aFile, true); } catch (FileNotFoundException e) { e.printStackTrace(System.err); System.exit(1); } FileChannel outChannel = outputFile.getChannel(); // Create a buffer to accommodate the longest string + its length value int maxLength = sayings[0].length(); for (int i = 1; i < sayings.length; i++) { if (maxLength < sayings[i].length()) maxLength = sayings[i].length (); } ByteBuffer buf = ByteBuffer.allocate(2 * maxLength + 4); // Write the file try { for (int i = 0; i < sayings.length; i++) { buf.putInt(sayings[i].length()).asCharBuffer().put(sayings[i]); buf.position(buf.position() + 2 * sayings[i].length()).flip(); outChannel.write(buf); // Write the buffer to the file channel buf.clear(); } outputFile.close(); // Close the output stream & the channel System.out.println("Statements written to file."); } catch (IOException e) { e.printStackTrace(System.err); System.exit(1); } System.exit(0); } } 16. When you execute this it should produce the rather terse output: Statements written to file. You can check the veracity of this assertion by inspecting the contents of the file with a plain text editor. The program writes the strings from the array, sayings, to the file. 17. We create a String array, sayings[], that contains statements that are written to the stream in the for loop. We put the length of each proverb in the buffer using the putInt() method for the ByteBuffer object. We then use a view buffer of type CharBuffer to transfer the string to the buffer. 19. The contents of the view buffer will start at the current position for the byte buffer. This corresponds to the byte immediately following the string length. 20. Transferring the string into the view buffer only causes the view buffer's position to be updated. The byte buffer's position is still pointing back at the byte following the string length where the first character of the string was written. We therefore have to increment the position for the byte buffer by twice the number of characters in the string before flipping it to make it ready to be written to the file. 21. The first time you run the program, the file doesn't exist, so it will be created. You can then look at the contents. If you run the program again, the same statements will be appended to the file, so there will be a second set. 22. Alternatively, you could modify the sayings[] array to contain different statements the second time around. Each time the program runs, the data will be added at the end of the existing file. 23. After writing the contents of the byte buffer to the file, we call its clear() method to reset the position to zero and the limit back to the capacity. This makes it ready for transferring the data for the next statement on the next iteration. Remember that it doesn't change the contents of the buffer though. VI. Random Access Files1. Instances of this class support both reading and writing to a random access file. A random access file behaves like a large array of bytes stored in the file system. There is a kind of cursor, or index into the implied array, called the file pointer; input operations read bytes starting at the file pointer and advance the file pointer past the bytes read. 2. If the random access file is created in read/write mode, then output operations are also available; output operations write bytes starting at the file pointer and advance the file pointer past the bytes written. 3. Output operations that write past the current end of the implied array cause the array to be extended. The file pointer can be read by the getFilePointer() method and set by the seek() method. 4. Here are some of the methods that you may need:
resources associated with the file.
beginning of the file, at which the next read or write occurs
5. The following code shows how to use the RandomAccessFile class. The random access file in this example is a binary file called accounts. import java.io.*;
public class RandomFile
{
final static int LONG_SIZE = 8;
final static int DOUBLE_SIZE = 8;
final static int RECORD_SIZE = LONG_SIZE + DOUBLE_SIZE;
static RandomAccessFile file;
static long numAccounts = 0;
public static void updateHeader ( double newBalance ) throws IOException
{
file.seek ( 0 );
file.writeLong ( numAccounts );
double tmpBal;
if ( numAccounts == 0 )
tmpBal = newBalance;
else
{
tmpBal = file.readDouble ();
tmpBal += newBalance;
}
file.seek ( LONG_SIZE );
file.writeDouble ( tmpBal );
}
public static void writeHeader ( ) throws IOException
{
file.seek ( 0 );
System.out.println ( "The number of accounts = " + file.readLong() );
System.out.println ( "The total balance = " + file.readDouble() );
}
public static void createAccount ( double newBalance ) throws IOException
{
numAccounts++;
file.seek ( numAccounts * RECORD_SIZE );
file.writeLong ( numAccounts );
file.writeDouble ( newBalance );
updateHeader ( newBalance );
}
public static int fileSize ( ) throws IOException
{
return ( int ) ( file.length() / RECORD_SIZE );
}
public static void scanFile () throws IOException
{
file.seek ( 0 );
long numRecords = file.readLong ();
double sumBalance = file.readDouble ();
long account;
double balance;
for (long i = 1; i <= numRecords; i++ )
{
account = file.readLong ();
balance = file.readDouble ();
System.out.println ("Account number: " + account + " " +
"Balance: " + balance );
}
}
public static void main ( String args[] ) throws IOException
{
// Create random access file and update header
file = new RandomAccessFile ("accounts", "rw");
double sumBalance = 0.0;
updateHeader ( sumBalance );
// Create account and update header
double newBalance = 1000.0;
createAccount ( newBalance );
// Create another account and update header
newBalance = 2000.0;
createAccount ( newBalance );
// Create another account and update header
newBalance = 3000.0;
createAccount ( newBalance );
// Write out the size of the file
System.out.println ( "The number of records in the file is " + fileSize( ) );
// Write out the header of the file
writeHeader ();
// Scan the file
scanFile ();
}
}
|
|
|