Lecture : File I/O & Object Serialization
In the previous lecture:
In this lecture:
Scanner : a simple way to read data from the keyboard and a file.
import java.util.Scanner; import java.io.*; public class myReader { public static void main(String[] args) throws IOException { String fileName, theText; Scanner inputKeyScanner; Scanner inputFileScanner; File inputFile; System.out.println("\nPlease enter the name of a file to be read: "); inputKeyScanner = new Scanner(System.in); // Make a Scanner object to read from System.in fileName = inputKeyScanner.nextLine(); // Read the next line of text entered from the keyboard inputFile = new File(fileName); // Make a file object with the file name entered from the keyboard inputScanner = new Scanner(inputFile); // Make a Scanner object to read from the input file while(inputScanner.hasNext()) // As long as the input Scanner has a next thing to read... { theText = inputScanner.nextLine(); // Read a line of text from the file System.out.println("Line: " + theText); } } } |
The Scanner object allows us to read data from System.in (the keyboard) or from a File object that we create.
Scanner .nextline() reads a line of text from the Scanner up until the next new-line/carriage-return character and returns it as a String.
In this example we open a file (the name for which is entered by the user). This operation might throw an IOException (e.g. if the file doesn't exist) so the method states: throws IOException. We should use the File class methods (listed in a table below) to determine if the file exists before we attempt to read from it.
Homework: | A better way to get the user to enter a file that constrains them against entering an invalid file name is to use the Java GUI element JFileChooser. Look this up and re-implement the code above to employ it. |
Selected Scanner methods.
next() | Finds and returns the next complete token from this Scanner. Tokens are delimited by white-space characters (space, tab, new-line etc.) by default. |
nextInt() | Finds and returns the next integer from this Scanner. (Also nextLong(), nextShort(), nextFloat() ...) |
hasNextInt() | Returns true if the next token in this Scanner's input can be interpretted as an int using the nextInt() method. |
useDelimiter() | Set the delimiter between tokens. |
delimiter() | Returns the pattern the Scanner is currently using to delimit tokens. |
close() | Closes this Scanner. |
Selected File methods.
The File class actually represents a file or directory name, not a file or directory. The File class is not for manipulating the contents of a file, it is for working with the file as a single entitity.
getName() | Returns the file name omitting the directory path to the file. |
getPath() | Returns the file name including the directory path to the file. |
getParent() | Returns the path of the directory that holds the file (as a String). |
exists() | Returns a boolean value stating if the file exists. |
canRead() | Returns a boolean value stating if the file can be read. |
canWrite() | Returns a boolean value stating if the file can be written. |
lastModified() | Returns a time stamp of when the file was last modified. (This can be compared against other modification times to see which file is older for example). |
delete() | Deletes a file or directory. The File class is not for creating files. We'll look at that below. |
Streams
For sequential I/O Java employs streams.
A stream is an object to which data can be written, and from which data can be read, sequentially.
Examples of streams include System.in (an InputStream), System.out and System.err (PrintStreams: one for standard output, one for output of error messages.)
Selected stream classes from package java.io
InputStream | For reading streams of bytes. This is the super-class for all byte input streams |
BufferedInputStream | Reads a buffer of bytes from an InputStream and returns bytes from the buffer to optimise small reads. |
FileInputStream | Reads bytes sequentially from a file. |
ObjectInputStream | Reads binary representations of Java objects and primitive values from a byte stream. |
OutputStream | For writing streams of bytes. This is the super-class for all byte output streams |
BufferedOutputStream | Writes a buffer of bytes to an OutputStream only once the buffer is full for optimum efficiency. |
FileOutputStream | Writes bytes sequentially to a file. |
ObjectOutputStream | Writes binary representations of Java objects and primitive values to a byte stream. |
Reader | For reading streams of unicode characters. This is the super-class of all character input streams |
BufferedReader | Reads a buffer of characters from a Reader and returns characters from the buffer to optimise small reads. |
FileReader | Reads characters sequentially from a file. |
StringReader | Reads characters sequentially from a String. |
Writer | For writing streams of unicode characters. This is the super-class of all character output streams |
BufferedWriter | Writes a buffer of characters to a Writer only once the buffer is full for optimum efficiency. |
FileWriter | Writes characters sequentially to a file. |
StringWriter | Writes characters sequentially to a String. |
Reading from the command line and copying a file (of bytes).
import java.io.*; public class FileCopy { public static void main (String[] args) { // Check that we have the correct number of command-line arguments if (args.length!=2) { System.err.println("\nUsage: java FileCopy <source> <destination>"); } else { // Pass the command-line arguments to the copy method try { copy(args[0], args[1]); } catch (IOException e) { System.err.println(e.getMessage()); } } } public static void copy (String outFileName, String inFileName) throws IOException { File inFile = new File(inFileName); File outFile = new File(outFileName); // Test to see if the file exists and is readable if (!inFile.exists()) { abort("No such file: " + inFileName); } if (!inFile.canRead()) { abort("Source file is unreadable: " + inFileName); } // We should also test to check the inFile is not a directory. // Should also test to see that outFile is not already a file (or it // might be overwritten etc. etc.) System.out.print("This program will copy the contents of " + inFileName + " into "); System.out.print("a file called " + outFileName + ". Proceed (y/n)? "); System.out.flush(); // Make a buffered reader that will be reading from System.in (the keyboard) BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); String response = in.readLine(); if (!response.equals("y")) { abort("Copy has not been executed."); } FileInputStream inStream = null; FileOutputStream outStream = null; try { inStream = new FileInputStream(inFile); // Make an input File stream to read from the input file outStream = new FileOutputStream(outFile); // Make an output File stream to write to the output file byte[] buffer = new byte [4096]; // Make buffer to store what is read int bytesRead; // Make a variable to hold the number of bytes read // While there are bytes to be read: // (i) get them from the file; (ii) put them in the buffer; // (iii) count them; (iv) write them out again while ((bytesRead = inStream.read(buffer)) != -1) { outStream.write(buffer, 0, bytesRead); } } finally // Always close the files when we're finished { if (inStream!=null) try { inStream.close(); } catch (IOException e) { ; } if (outStream!=null) try { outStream.close(); } catch (IOException e) { ; } } } // A method to throw an exception private static void abort(String msg) throws IOException { throw new IOException("File Copy: " + msg); } } |
The above program uses FileInputStream and FileOutputStream objects to do the reading and writing of the file contents. If we wanted to read and write characters (instead of bytes) we would use a FileReader and a FileWriter object instead.
If we do this we must make sure the buffer is a char[] (not a byte[]). We can convert the buffer's contents (chars) into a String and print it out. Try the code below by inserting it into the example above.
Reading and printing a file of characters.
FileReader inStream = null; inStream = new FileReader(inFile); String textRead; char[] buffer = new char [4096]; |
Object Serialization
Serialization refers to the writing of an object's state to a byte stream so that the object can be reconstructed later.
The entire object (including all of its private data members and all of its components (even if these are also objects) is serialized.
Serialization works on complex data structures (like graphs and trees), even on an application's entire state to allow us to write them to disk or to send them across a network. I.e. serializing makes deep-clones of these structures.
Beware: serialization depends on the exact structure of your application program and its data-structures. If you save the state of some data-structures or a program, quit the program, then modify the application source code, you won't be able to re-load the saved version of the state because the old serialzed state will not be compatible with the new application code.
Object serialization example
import java.io.*; public class Serializer { // A data structure we'll use to test serialization public static class TestDataStructure implements Serializable { String message; int [] data; TestDataStructure other; public String toString() { String s = message; for (int i=0; i<data.length; i++) { s += " " + data[i]; } if (other!=null) { s+= "\n\t" + other.toString(); } return s; } } // A method to serialize a data structure static void store (Serializable o, File f) throws IOException { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f)); out.writeObject(o); out.close(); } // A method to de-serialize a data structure static Object load (File f) throws IOException, ClassNotFoundException { ObjectInputStream in = new ObjectInputStream(new FileInputStream(f)); return in.readObject(); } public static void main (String[] args) throws IOException, ClassNotFoundException { TestDataStructure ds = new TestDataStructure(); ds.message = "I'm the original data structure"; ds.data = new int[] { 1,2,3,4 }; ds.other = new TestDataStructure(); ds.other.message = "I'm the nested data structure"; ds.other.data = new int[] { 50,60,70 }; System.out.println(ds); File f = new File ("dataStruct.sef"); Serializer.store(ds, f); ds = null; ds = (TestDataStructure) Serializer.load(f); System.out.println("Read from file: " + ds); } } |
$> java Serializer I'm the original data structure 1 2 3 4 I'm the nested data structure 50 60 70 Read from file: I'm the original data structure 1 2 3 4 I'm the nested data structure 50 60 70 |
At left is the output generated by the program above. As you can see, the data-structure and its contents (including the array contents and the nested data-structure) can all be serialized easily. |