Download Files, Streams, and Serialization

Survey
yes no Was this document useful for you?
   Thank you for your participation!

* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project

Document related concepts
no text concepts found
Transcript
COMP2059 - Sun Certified Java Programmer
Overview of Java I/O
I/O in Java is built on streams. Input streams read data. Output streams write data. Different stream
classes read and write particular sources of data but all fundamental output streams have the same
basic methods to write data and all fundamental input streams have the same basic methods to read
data. Once a stream has been created, you can ignore the details of exactly what it is you are
reading or writing.
Filter streams can be chained to either an input stream or an output stream. These can modify the
data as it is read or written, eg by encrypting or compressing it, or they may provide additional
methods for converting the data that's read or written to other formats.
Readers and writers can be chained to input and output streams to allow programs to read or write
text, ie characters, rather than bytes. Readers and writers can handle a wide variety of character
encodings, not just the default Java character set Unicode.
Files in Java
(p429-433)
Files are used for persistent storage of data. The Java File class is designed to provide programs
with a mechanism for interacting with the file system of a computer. The actual reading and writing
of data though, is accomplished by streams. A class diagram for the File class is shown with the
most commonly used methods:
File
- String filename
<<constructor>>
+ File(String)
...
<<query>>
+ boolean canRead()
+ boolean canWrite()
+ boolean exists()
+ String getAbsolutePath()
+ boolean isFile()
+ boolean isDirectory()
+ boolean length()
...
<<update>>
+ boolean createNewFile()
+ boolean delete()
+ void deleteOnExit()
+ boolean mkdir()
...
Example - Displaying Information about a File
01
02
03
04
import java.io.*;
public class FileInfo {
public static void main(String[] args) {
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
String filename = "readme.txt";
if (args.length > 0) filename = args[0];
File f = new File(filename);
if (f.exists()) {
System.out.println("File name: " + f.getName());
System.out.println("File path: " + f.getAbsolutePath());
System.out.println("File length: " + f.length());
if (f.isFile()) System.out.println("File is normal");
if (f.isHidden()) System.out.println("File is hidden");
else System.out.println("File is not hidden");
if (f.isDirectory()) System.out.println("File is a directory");
if (f.canRead()) System.out.println("File is readable");
else System.out.println("File is not readable");
if (f.canWrite()) System.out.println("File is writable");
else System.out.println("File is not writable");
} else System.out.println("File does not exist");
}
}
The above code will display information about a file in the current directory. The filename is
passed as a command line argument else it will look for the file readme.txt. Note that since the file
hasn't been opened there is no need (nor is it possible) to close it.
Example - Creating a New File
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
import java.io.*;
public class CreateFile {
public static void main(String[] args) {
String filename = "readme.txt";
if (args.length > 0) filename = args[0];
File f = new File(filename);
if (f.exists()) {
System.out.println("File already exists");
} else {
try {
if (f.createNewFile()) {
System.out.println(filename + " created");
}
} catch (IOException e) {}
}
}
}
It is possible that a new file can't be created, eg the directory is read-only, in this case the
createNewFile method will throw an IOException. This is handled in the above with a
try...catch. Again the file hasn't actually been opened hence needn't be (actually can't be) closed.
Accessing Text Files
(p433-443)
A file that contains exclusively char data is said to be a text file, and can be created and modified
by text editor programs, eg Notepad, Textpad. The java.io package includes several classes to
manipulate text files, many of these use the concept of reader and writer objects to massage stream
data. In order to write to a file a program could create two objects - a writer object and an output
stream; the actual output methods are performed on the writer. Similarly, a program can input from
a file by way of a reader object that is connected to an input stream object.
Executing Program
OutputStreamWriter
FileOutputStream
File
InputStreamReader
FileInputStream
File
ProgramProgram
Executing Program
The file is opened for writing (and if necessary created) when the FileOutPutStream is
instantiated. The file is opened for reading when the FileInputStream is instantiated. The
OutputStreamWriter is a character writer for the underlying output stream. The
InputStreamReader is a character reader for the underlying input stream.
If this is all getting to be a bit complicated help is at hand in the form of the FileWriter and
FileReader classes. These classes write/read text files using the platform's default character
encoding. If all you want to do is write/read characters and strings to/from the file these classes will
do the job by themselves.
Example - Writing Characters and Strings to a Text File
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
import java.io.*;
public class TextWrite {
public static void main(String[] args) {
try {
FileWriter fW = new FileWriter("sample.txt");
BufferedWriter bW = new BufferedWriter(fW);
bW.write('Z');
bW.write("\r\n");
bW.write("Fred Flintstone");
bW.write("\r\n");
bW.flush();
bW.close();
} catch(IOException e) {
System.out.println("Error in writing");
}
}
}
When the FileWriter object fW is instantiated, the file sample.txt is opened for writing. If it
already existed all previous contents are deleted; if it didn't exist it is created. The next line chains a
BufferedWriter object bW onto fW. The application would work without the BufferedWriter
object but would be less efficient. BufferedWriter provides a character buffer that stores text
being written until the buffer fills up or is flushed, only then is all the text written to the underlying
stream. This reduces the number of separate writes to the underlying stream and hence file and so
increases efficiency. Note that it is the methods of BufferedWriter that are used to do the writing.
An abridged class diagram for BufferedWriter is shown:
BufferedWriter
<<constructor>>
+ BufferedWriter(Writer)
...
<<update>>
+ void write(int c)
+ void write(String str)
+ void newLine()
+ void flush()
+ void close()
...
Note that the newLine() method writes a platform-dependent line terminator string: \n on Unix, \r
on the MAC, and \r\n on Windows. This method should not be used when writing network code,
instead explicitly write the carriage return / line feed pair as has been done in the TextWrite class
above.
Example - Reading from a Text File
In a similar way to writing two classes are used to read from a text file. These are the FileReader
class and the BufferedReader class, though it is the methods of the BufferedReader class that are
used to do the reading.
We will look at two ways of reading a text file, either line at a time or character at a time; firstly
line at a time:01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
import java.io.*;
public class LineRead {
public static void main(String[] args) {
try {
String inStr;
FileReader fR = new FileReader("sample.txt");
BufferedReader bR = new BufferedReader(fR);
inStr = bR.readLine();
while (inStr != null) {
System.out.println(inStr);
inStr = bR.readLine();
}
bR.close();
} catch(IOException e) {
System.out.println("Error in reading");
}
}
}
Note that the String returned by readLine() does not include the end of line character ('\n'), the
end of line is consumed by the method call. When readLine() is called after reaching the end of
the file, null is returned.
Similarly reading the file character at a time can also be done:01
02
03
04
05
06
07
08
09
10
11
12
13
14
import java.io.*;
public class CharRead {
public static void main(String[] args) {
try {
int inInt;
char inChar;
FileReader fR = new FileReader("sample.txt");
BufferedReader bR = new BufferedReader(fR);
inInt = bR.read();
while (inInt != -1) {
inChar = (char)inInt;
System.out.print(inChar);
inInt = bR.read();
15
16
17
18
19
20
21
}
bR.close();
} catch(IOException e) {
System.out.println("Error in reading");
}
}
}
In order to read a file Character at a time, BufferedReader provides a method called read. The
read method is a bit unusual because it returns an int value rather than a char. A cast is required
before the read method's value can be used as a char. Another characteristic of the read method is
that it returns the value -1 to indicate any attempt to read past the end of file.
The most commonly used methods of the BufferedReader class are shown in the following class
diagram:
BufferedReader
<<constructor>>
+ BufferedReader(Reader)
...
<<query>>
+ int read()
+ String readLine()
...
<<update>>
+ void close()
Example - Writing Formatted Data to a Text File
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
import java.io.*;
public class FormatWrite {
public static void main(String[] args) {
try {
PrintWriter pW = new PrintWriter(“sample.txt”);
pW.print(12);
pW.println(true);
pW.print(98.7);
pW.println("hello World");
pW.print('Z');
pW.flush();
pW.close();
} catch(IOException e) {
System.out.println("Error in writing");
}
}
}
As it is not just characters and strings that are being written to the file here, the FileWriter class
can't be used. The code here uses PrintWriter. Use a text editor to check what is written to the
file, if the file "sample.txt" already exists, it will be overwritten.
The most commonly used methods of the PrintWriter class are shown in the following class
diagram:
PrintWriter
<<constructor>>
+ PrintWriter(OutputStream)
+ PrintWriter(String)
...
<<update>>
+ void close()
+ void flush()
+ void print(boolean)
+ void print(char)
+ void print(double)
+ void print(float)
+ void print(int)
+ void print(long)
+ void print(Object)
+ void print(String)
+ void println()
+ void println(boolean)
+ void println(char)
+ void println(double)
+ void println(float)
+ void println(int)
+ void println(long)
+ void println(Object)
+ void println(String)
...
The PrintWriter class allow objects of different types to be converted to text and written to a text
file. This is similar to the formatted output provided by the printf statement in C. The println()
method of the PrintWriter class should not be used in networking code for the line separator
problem described earlier.
Larger Example
Consider a program to read a text file and output the most used letter (or letters).
One strategy for writing this program is to build a class into which all the letters in the file are
entered and stored in a form that can easily be processed. The LetterData class defined as follows
does this with an array that has an element for each letter of the alphabet in which the number of
times that letter occurs is stored:
LetterData
- int[] data
<<constructor>>
+ LetterData()
<<update>>
+ void addLetter(int)
<<query>>
+ int getMaxNo()
+ String getMaxLetters()
The code to implement this is shown:
01
02
public class LetterData {
private int[] data;
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public LetterData() {
data = new int[26];
for (int n = 0; n <= 25; n++) data[n] = 0;
}
public void addLetter(int letter) {
if ((letter >= 'A') && (letter <= 'Z')) data[letter - 'A']++;
if ((letter >= 'a') && (letter <= 'z')) data[letter - 'a']++;
}
public int getMaxNo() {
int max = 0;
for (int n = 0; n <= 25; n++) {
if (data[n] > max) max = data[n];
}
return max;
}
public String getMaxLetters() {
int max_count;
String outString = "";
int max = getMaxNo();
if (max != 0) {
max_count = 0;
for (int n = 0; n <= 25; n++) {
if (data[n] == max) {
outString += ((char)(n + 'A')) + " ";
max_count++;
}
}
if (max_count > 1) outString += "are the most used letters";
else outString += "is the most used letter";
} else outString += "The file contained no letters";
return outString;
}
}
A LetterData object can thus be instantiated, the file read one character at a time, the characters
entered into the LetterData object using the addLetter method, and the most used letter found
with the getMaxLetters method. An application which does this (using a file called "sample.txt"
as input) and outputs the result to the console is shown:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
import java.io.*;
public class MostLetters {
public static void main(String[] args) {
LetterData store = new LetterData();
try {
FileReader fR;
BufferedReader bR;
fR = new FileReader("sample.txt");
bR = new BufferedReader(fR);
int inInt;
inInt = bR.read();
while (inInt != -1) {
store.addLetter(inInt);
inInt = bR.read();
}
} catch(IOException e) {
System.out.println("Error in reading input file");
}
System.out.println(store.getMaxLetters());
21
22
}
}
Exercises
(1)
Modify the above example to output the results to a file called results.txt rather than the
console.
(2)
Add a method to the LetterData class called getLetterList() which returns a string
which is a list of all the letters used in the file. Use this to add to what is written to the
results.txt file.
(3)
Add a method to the LetterData class called getLetterNumber(char ch) which returns
the number of instances in the file of the character passed to it. Use this to add a histogram
(constructed with '*' characters) of the instances of all the letters to what is written to the
results.txt file.
Serialization
(p433-457)
Serialization allows us to write entire objects to a file and read them back at a later time. This is
really powerful as large amounts of data can be stored with very little effort from the programmer.
Example - Serializing an Object
First we need an object to serialize. To get that object we need a class. We will use the simple
Person class shown here:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
import java.io.*;
class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
Note that the class implements the Serializable interface. This is just a marker interface so it doesn't
require any methods to be implemented. Now we need a class that writes a Person object to a file:
01
02
03
04
05
import java.io.*;
public class Write {
public static void main(String[] args) {
try {
06
07
08
09
10
11
12
13
Person p = new Person("Doris", 32);
FileOutputStream fos = new FileOutputStream("serialized.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(p);
oos.close();
} catch (Exception e) {}
}
}
A Person object is created in line 6. A FileOutputStream and a file called "serialized.obj" are
created in line 7. The FileOutputStream is wrapped in a ObjectOutput stream in line 8.
Line 9 is where the big action happens! We call the writeObject method on the
ObjectOutputStream and pass in our Person object. This passes our Person object through the
ObjectOutputStream, then through the FileOutputStream, and writes it to the file.
Line 10 closes the ObjectOutputStream, which closes the FileOutputStream, which closes the
file.
All the code is in a try block because working with streams can throw up a variety of exceptions.
Running the Write application will create the "serialized.obj" file. For interest open this in a text
editor. It is not a text file but you should recognise the odd string in there. Now we need a Read
application to get our object back out of the file:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
import java.io.*;
class Read {
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("serialized.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Person newP = (Person)ois.readObject();
ois.close();
System.out.println(newP.getName()
+ " is "
+ newP.getAge()
+ " years old");
} catch (Exception e) {
System.out.println("problem");
}
}
}
Line 6 creates a FileInputStream and connects it to the "serialized.obj" file. Line 7 wraps the
FileInputStream in an ObjectInputStream.
Line 8 is where the big action happens in this class. An object is read from the
ObjectInputStream. However this comes out as a reference to a type Object, thus we do the cast
to a type Person.
Line 9 closes the streams and file. Lines 10 - 13 output the results.
As an exercise, see what happens when you mark the age instance variable of the Person class as
transient. Remember to recompile the Person class before using it again.
Example - Manual Serialization (p449-452)
Say, for example, the age variable in the Person class above was declared transient but you didn't
want it to comeback as a default value on de-serialization. There is a way of manually saving the
state of transient variables. This involves making the otherwise serializable class implement
writeObject and readObject methods. In our case the Person class becomes:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.io.*;
class Person implements Serializable {
private String name;
transient private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
private void writeObject(ObjectOutputStream os) {
try {
os.defaultWriteObject();
os.writeInt(age);
} catch (Exception e) {}
}
private void readObject(ObjectInputStream is) {
try {
is.defaultReadObject();
age = is.readInt();
} catch (Exception e) {}
}
}
We don't need to make any changes to the Read and Write classes. In true object-oriented fashion
the Person class now knows how to manually serialize its unserializable (transient) bits.
The writeObject method of lines 20 - 25 specifies the serialization process. First the serializable
parts of the object are serialized by calling the defaultWriteObject method, then the age is
written to the stream. ObjectOutputStream has a whole set of methods for writing primitive
values (see JavaDocs).
Similarly the readObject method in lines 27 - 32 specifies the de-serialization process.