After a program ends, it gets erased from the memory. When you run it again, its content is loaded into the memory and all Java objects are created from scratch. If you’d like to save some results of the program execution, they must be stored in files on a hard disk, a solid-state drive, a memory stick, or any other device that can store the data for a long time.
In this chapter you’ll learn how to save data on disks and read them back into memory using Java Input/Output(I/O) streams.A program can open an output stream with a file on disk and write the important data there. Similarly, you can open an input stream and read the data from a file.
For example, if a player wins a game and you want to save the score, you can save it in a file called scores.txt using an output stream. If you want to save the player’s preferences (e.g. colors or fonts) you can save them in a file, and read and apply them when the user runs your program again.
A program reads or writes data from/to a stream serially byte after byte, character after character, etc. Since your program may use different data types like String
, int
, double
, you should use an appropriate Java stream, for example a byte stream, a character stream, or a data stream.
In this chapter I’ll show you how to use I/O streams with files located in your computer. In the next chapter you’ll learn about I/O streams that can work with remote data located in other computers connected into a network.
All classes that can work with file streams are located in packages java.io
and java.nio
. Classes from the java.io
implement blocking I/O: When bytes are being read/written, they become unavailable for other threads of execution. This may not be an issue with small files like a history of game scores, but may become an issue with large files that contain a gigabyte or more. The package java.nio
offers a set of classes with improved performance as well as some useful utility classes.
I’ll start with explaining the basics of working with streams of bytes using classes from the java.io
package, and then give you some examples of working with classes from java.nio
.
Think of a stream as a pipe. If you just need to read the data from some source, imagine that the data flows through a pipe into your program. If you need to write the data into storage, it’s another pipe where the data flow from your program to this storage. As with any pipe, it has to be opened to start the flow and closed to stop it.
Java supports several types of streams, but no matter what type you are going to use, the following three steps should be done in your program:
-
Open a stream that points at some data store e.g. a local or remote file.
-
Read or write data from/to this stream.
-
Close the stream (Java can do it automatically).
In the real world people use different types of pipes for carrying different content (e.g. gas, oil, milkshakes). Similarly, Java programmers use different classes depending on the type of data they need to carry over a stream. Some streams are used for carrying text, some for bytes, et al.
To create a program that reads a file, interprets and displays its content on the screen, you need to know what type of data is stored in this file. On the other hand, a program that just copies files from one place to another does not even need to know if this file represents an image, a text, or a file with music. Such a copy program reads the original file into computer’s memory as a set of bytes, and then writes them into another file located in the destination folder.
The java.io
package includes the classes FileInputStream
and FileOutputStream
that allow you to read and write files one byte at a time. If your program has to work only with text files, you could use the classes Reader
and Writer
, which are specifically meant for reading character streams.
After selecting, say FileInputStream
you’d be invoking one of its methods to read the file data. For example the online documentation of the class FileInputStream
includes the method read
having the following signature:
public int read() throws IOException
This should tell you that the method read
returns an integer value and may throw the IOException
, which is an error that may happen if the file is corrupted or deleted, a device failure happened et al. Hence, you must enclose the invocation of the method read
into a try-catch block as explained in Chapter 9. To get a specific reason for this exception you’ll call the method getMessage
on the exception object.
Each byte in a file is represented by a code, which is a positive integer number. The method read
returns the value of this code.
The next example shows how to use the class FileInputStream
to read the bytes from a file named abc.dat. This little program won’t interpret the meaning of each byte - it’ll just print their code values.
I’ll start with creating a new Java project Chapter11 in IDEA. Then I’ll create a new empty file abc.dat in the root folder of the project (right-click on the project name and select the menu New | File). Finally, I’ll type the following text in abc.dat:
This is a test file
Then I’ll create a new Java class MyByteReader
that will read and print the content of the file abc.dat. But let’s pretend that we don’t know that the content of this file is text - I want to read it byte by byte and print each byte’s code (not a letter) separated by a space character. The class MyByteReader
will look like this:
import java.io.FileInputStream;
import java.io.IOException;
public class MyByteReader {
public static void main(String[] args) {
try (FileInputStream myFile = // (1)
new FileInputStream("abc.dat")){
int byteValue;
while ((byteValue = myFile.read()) != -1) { // (2)
System.out.print(byteValue + " "); // (3)
}
} catch (IOException ioe) { // (4)
System.out.println("Could not read file: " +
ioe.getMessage());
}
}
}
-
First we open the file for reading (using a default character set) by creating an instance of the class
FileInputStream
passing the file nameabc.dat
as a parameter. In Chapter 9 you’ve learned the syntax of the try-catch blocks, but here I used a special syntax called try-with-resources. When there are some program resources that have to be opened and closed (e.g.FileInputStream
), just instantiate them in parentheses right after thetry
keyword and Java will close them for you automatically. This spares you from writing afinally
block containing the code for closing resources. -
This line starts a
while
loop that calls the methodread()
, which reads one byte, assigns its value to the variablebyteCode
, and compares it with-1
(the end of file indicator). -
If the value in the current byte is not equal to
-1
, we print it followed by a space character. The loop ends when it reads the-1
byte. -
If an
IOException
occurs, catch it and print the reason for this error.
Running this program prints the following numbers that represent encoding of the text "This is a test file":
84 104 105 115 32 105 115 32 97 32 116 101 115 116 32 102 105 108 101
Writing bytes into a file works similarly, but you’d use the class FileOutputStream
and its method write()
as shown in the program MyByteWriter
below.
import java.io.FileOutputStream;
import java.io.IOException;
public class MyByteWriter {
public static void main(String[] args) {
// Some byte values represented by integer codes
int someData[]= {56,230,123,43,11,37}; // (1)
try (FileOutputStream myFile = new FileOutputStream("xyz.dat")){ // (2)
int arrayLength = someData.length;
for (int i = 0; i < arrayLength; i++){
myFile.write(someData[i]); // (3)
}
} catch (IOException ioe) {
System.out.println("Could not write into the file: " + ioe.getMessage()); // (4)
}
}
}
-
The program
MyByteWriter
populates an arraysomeData
with integer codes that may represent some characters -
Then the program opens the file xyz.dat.
-
Then it proceeds to writes each of the integers into the file.
-
If an error occurs, we catch it and print the reason.
The code examples in the Byte Streams section were reading or writing into a file one byte at a time. One invocation of read
would read one byte, and one invocation of write
would write one byte. In general, disk access is much slower than the processing performed in memory; that’s why it’s not a good idea to access the disk a thousand times to read a file of 1,000 bytes. To minimize the number of times the disk is accessed, Java provides buffers, which serve as reservoirs of data.
The class BufferedInputStream
works as a middleman between FileInputStream
and the file itself. It reads a big chunk of bytes from a file into memory (a buffer) in one shot, and the FileInputStream
object then reads single bytes from there, which are fast memory-to-memory operations. BufferedOutputStream
works similarly with the class FileOutputStream
.
The main idea here is to minimize disk access. Buffered streams are not changing the type of the original streams — they just make reading more efficient. A program performs stream chaining (or stream piping) to connect streams, just as pipes are connected in plumbing.
The next code listing shows a class MyBufferedByteReader
, which is a slightly modified version of MyByteReader
. I just attached "another fragment to the pipe" - the BufferedInputStream
from the java.io
package.
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class MyBufferedByteReader {
public static void main(String[] args) {
try (FileInputStream myFile = new
FileInputStream("abc.dat"); // (1)
BufferedInputStream buff = new
BufferedInputStream(myFile);){
int byteValue;
while ((byteValue = buff.read()) != -1) { // (2)
System.out.print(byteValue + " ");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
-
Here we use the try-with-resources syntax again. This time we create an instance of
FileInputReader
and then and instance ofBufferedInputReader
providing theFileInputReader
as an argument. This is how we connect to fragments of the pipe that uses the file abc.dat as the source of data. -
Under the hood the
BufferedInputReader
reads bytes from the disk in chunks into a memory buffer, and then the methodread
reads one byte at time from memory.
The program MyBufferedByteReader
produces the same output as MyByteReader
, but will work just a little bit faster.
Text in Java is represented as a set of char
values, which are encoded based on the character sets capable to represent alphabet or other symbols used in a particular human language. As you can imagine, the text in English, Ukrainian, and Japanese should use different character encodings. The names of some of the standard character sets for English-speaking people are US-ASCII, UTF-8, and UTF-16.
Each JVM has a default character set, which could be changed during the JVM startup. You can also find out what’s the default character in your Java installation by invoking the method defaultCharset
on the class Charset
, and I’ll show you how to do it in the section "Reading Text Files".
The Java classes FileReader
and FileWriter
from the package java.io
were specifically created to work with text files, but they work only with default character encoding and don’t handle localization properly.
For efficiency, the reading can be piped with the BufferedReader
or BufferedWriter
, which read or write from/to the stream buffering characters. I’ll show you examples of working with text files in the next section.
The java.nio
package contains classes that offer a simplified and more efficient way of working with files, and you’ll see some useful examples here. But first I’d like to introduce the classes Path
and Paths
that will represent a file you’re planning to work with.
So far we worked with files located in the root directory of our IDEA project. But files are grouped in directories (folders), which in turn can include nested directories and files. More over, files are represented differently in Windows, Mac OS and Linux operational systems. For example, in Windows the pull path to a file may look like this: c:\mystuff\games\tictactoeScores.txt
In Mac OS or Linux, disks are not named with letters like c: or d:. For example, the path to user’s files starts from the Users directory. Besides a backslash is not used for a separator symbol there. A full path to a file tictactoeScores.txt of the user mary may look like this:
/Users/mary/mystuff/games/tictactoeScores.txt
So before even opening a file you can create an instance of the object Path
, which doesn’t open or read a file, but represents the location of the file on the disk.
There is a special class Paths
(yes, in plural) that includes a number of static methods, and in particular, can build the Path
object for you. For example, you can invoke the method get
passing the name of the file as the argument:
Path path = Paths.get("abc.dat");
Since we specified just the file name without a directory, the program will look for the file in the current directory, which in IDEA is the directory of your project (e.g. Chapter11). Outside of the IDE, the current directory is the one where the program was started from.
But if we were to create a directory data at the root of our project and move the file abc.dat there, then the relative path to the file starting from the project root would look as "data/abc.dat"
on Mac OS or "data\\abc.dat"
on Windows. Two backslashes in a Java string correspond to one backslash in the Windows operational system.
But your IDEA project is also located in some directory on disk, e.g. chapter11, which is a subdirectory of practicalJava. The file can be represented by the absolute path, which may look like this:
c:\practicalJava\chapter11\data\abc.dat
or
/Users/mary/practicalJava/chapter11/data/abc.dat
The class Files
has several methods that will help you with file manipulations. It can create, delete, rename and copy files. There are methods to read and write files too.
The simplest way to read a file is by calling the method readAllBytes
on the class Files
. The following line reads the file abc.dat into an array of bytes:
byte[] myFileBytes = Files.readAllBytes(Paths.get("abc.dat");
If the file contains text, you can read the file into a String
variable like this:
String myFileText = new String(Files.readAllBytes(Paths.get("abc.dat")));
If you want to read a text file into a collection, where each element contains one line from the file use the method readAllLines
.
List<String> myFileLines = Files.readAllLines(Paths.get("abc.dat"));
Both readAllBytes
and readAllLines
do not use buffers, but for small files is not important. For more efficient reading you can ask the class Files
to create a buffered stream. For example, the program MyTextFileReader
uses the method newBufferedReader
for more efficient reading. Here I used the same file abc.dat located in the root directory of our IDEA project Chapter11.
package niosamples;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class MyTextFileReader {
public static void main(String[] args){
Path path = Paths.get("abc.dat"); // (1)
System.out.println("The absolute path is " +
path.toAbsolutePath());
try {
if ( Files.exists(path)){ // (2)
System.out.println("The file size is " + Files.size(path));
}
BufferedReader bufferedReader= // (3)
Files.newBufferedReader(path, StandardCharsets.UTF_8);
String currentLine;
while ((currentLine = // (4)
bufferedReader.readLine()) != null){
System.out.println(currentLine);
}
} catch (IOException ioe) {
System.out.println("Can't read file: " +
ioe.getMessage());
}
System.out.println( // (5)
"Your default character encoding is " + Charset.defaultCharset());
}
}
-
The program starts with creating a
Path
object from abc.dat and printing its absolute path. -
Then it checks if the file represented by the path exists and prints its size in bytes.
-
Here we’re opening the buffered reader capable of reading text encoded with a character set
UTF-8
. -
Reading the text lines from the buffer.
-
Print the default character set being used for those who are interested.
When I ran the program MyTextFileReader
it printed the following:
The absolute path is /Users/yfain11/IdeaProjects/jfk/Chapter11/abc.dat
The file size is 19
This is a test file
Your default character encoding is UTF-8
Now experiment to see if the IOException
is thrown if the file is not found where it is expected to be. Just move the file abc.dat into a different directory and re-run MyTextFileReader
. Now the console output looks different:
Can't read file: abc.dat
Your default character encoding is UTF-8
This output was produced by the catch
section from MyTextFileReader
where the code invoked the getMessage
method on the IOException
object.
Writing into a file is as reading one. Start with creating create an instance of the Path
object. If you want to write bytes, create a byte array, populate it with data and call the method write
on the class Files
. If you want to write some text into a file, just convert the text from a String
into a byte array and then call the method write
, for example:
Path path = Paths.get("data/scores.txt");
Files.write(path, myScore.getBytes(),
StandardOpenOption.APPEND);
When your program opens a file for writing, you need to consider different options. For example, you may want to append text to an existing file, hence the above code snippet passes the argument StandardOpenOption.APPEND
to method write
. If you want to create a file from scratch every time you run the program, use the option StandardOpenOption.CREATE
.
The following program MyTextFileWriter
writes the text into a file scores.txt
in the directory data
.
package niosamples;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;
public class MyTextFileWriter {
public static void main(String[] args){
System.out.println("The current directory is "+
System.getProperty("user.dir")); // (1)
String myScore = // (2)
"My game score is 28000 " +
LocalDateTime.now() + "\n";
Path path = Paths.get("data/scores.txt"); // (3)
try {
if ( Files.exists(path)){ // (4)
Files.write(path, myScore.getBytes(),
StandardOpenOption.APPEND);
} else {
Files.write(path, myScore.getBytes(),
StandardOpenOption.CREATE);
}
System.out.println("The game score was saved at " + path.toAbsolutePath());
} catch (IOException ioe) {
System.out.println("Can't write file: "
+ ioe.getMessage());
}
}
}
-
For illustration purposes I want the program to print its current directory, which is stored in a special Java variable
user.dir
. -
Then I populate a string with the content "My game score is 28000" and concatenate the current date and time followed by
\n
, which is the end of line marker. -
Creating a
Path
object pointing at the filescore.txt
. -
Check if the file
score.txt
already exists, append the new content to it. If the file doesn’t exists – create it and write the content there.
The program MyTextFileWriter
printed the following on my console:
The current directory is /Users/yfain11/IdeaProjects/jfk/Chapter11
The game score was saved at /Users/yfain11/IdeaProjects/jfk/Chapter11/data/scores.txt
After running MyTextFileWriter
twice I opened the file scores.txt. Here’s what I found there:
My game score is 28000 2015-01-11T09:07:49.352
My game score is 28000 2015-01-11T09:10:11.049
Each time the program saved the same score of 28000
attaching the system date and time. Of course, you could calculate and write the real score of a game. This is a simple way of printing the local date and time. The time portion goes after the letter "T" and shows hours, minutes, seconds, and nanoseconds. In the Java package java.time
you can find multiple classes and methods providing formatting date and time for pretty printing. Go through Oracle’s tutorial on working with Date and Time API if you’re interested.
For writing small files like scores.txt you don’t need to use buffered streams. For large files though, use BufferedWriter, for example:
String myScore = "My game score is 28000 " +
LocalDateTime.now() + "\n";
BufferedWrited writer = Files.newBufferedWriter(path, StandardOpenOption.APPEND);
writer.write(myScore)
The class MyTextFileBufferedFileWriter
shows how to write the game score using BufferedWriter
.
package niosamples;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;
public class MyTextFileBufferedFileWriter {
public static void main(String[] args) {
String myScore = "My game score is 28000 " + LocalDateTime.now() + "\n";
Path path = Paths.get("data/scores.txt");
try (BufferedWriter writer =
getBufferedWriter(path)) {
writer.write(myScore);
System.out.println("The game score was saved at " + path.toAbsolutePath());
} catch (IOException ioe) {
System.out.println("Can't write file: " +
ioe.getMessage());
}
}
// The factory of BufferedWriter objects
private static BufferedWriter getBufferedWriter(Path path) throws IOException{
if (Files.exists(path)) {
return Files.newBufferedWriter(path,
StandardOpenOption.APPEND);
} else {
return Files.newBufferedWriter(path,
StandardOpenOption.CREATE);
}
}
}
In this class I’ve moved the code that checks if the file exists into a separate method getBufferedWriter
. I did it to illustrate so-called factory design pattern. A factory can build different objects, right? The method getBufferedWriter
also builds different instances of a BufferedReader
depending on the existence of the file referred by path
. In programmers jargon, the methods that create and return different object instances, based on some parameter, are called factories.
If you run the program MyTextFileBufferedFileWriter
it’ll produce the same results as MyTextFileFileWriter
. If you use these programs for writing short text, you won’t notice the difference in speed of execution. But when you need to write large amounts of information MyTextFileBufferedFileWriter
will work faster.
So far all of the code examples had the file names stored right in the code, or as programmers say, hard-coded in the program. This means that to create a program that reads a file with a different name you’d have to modify the code and recompile the program, which is not nice. You should create universal programs that can take parameters (e.g.the name of the file) from a command line during the program launch.
In the first lesson of this book you’ve been running the program HelloWorld from a command window:
java HelloWorld
But you’re allowed to add command-line arguments right after the name of the program, for example,
java HelloWorld Mary Smith
In this case, the program HelloWorld
would get two command line parameters: Mary and Smith. If you remember, we always write the method main with a String
array as an argument:
public static void main(String[] args) {}
The JVM passes the String
array to the main method, and if you start a program without any command line arguments, this array remains empty. Otherwise, this array will have exactly as many elements as the number of command-line arguments passed to the program.
Let’s see how we can use these command line arguments in a very simple class that will just print them:
public class PrintArguments {
public static void main(String[] args) {
// How many arguments we've got?
int numberOfArgs = args.length;
for (int i=0; i<numberOfArgs; i++){
System.out.println("I've got " + args[i]);
}
}
}
This program loops through the array args
and prints the arguments received from the command line, if any. Run this program from the Command (or Terminal) Window as follows:
java PrintArguments Mary Smith 123.5
The program will print the following:
I've got Mary
I've got Smith
I've got 123
The JVM placed Mary
into the array element args[0]
, Smith
into args[1]
, and 123
into args[2]
.
Command-line arguments are always being passed to a program as strings. It’s the responsibility of a program to convert the data to the appropriate data type, for example:
int myScore = Integer.parseInt(args[2]);
It’s always a good idea to check if the command line contains the correct number of arguments. Check this right in the beginning of the method main
. If the program doesn’t receive expected arguments, it should print a brief message about it and immediately stop by using a special method exit
of the class System
:
public static void main(String[] args) {
if (args.length != 3){
System.out.println(
"Please provide arguments, for example:");
System.out.println("java PrintArguments Mary Smith 123");
// Exit the program
System.exit(0);
}
}
You can test your programs that take command-line arguments without leaving IntelliJ IDEA. Just open the menu Run, select Edit Configurations and enter the values in the field Program arguments as shown below:
Now let’s write a program to copy files. To make this program working with any files, the names of the original and destination files have to be passed to this program as command-line arguments.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FileCopy {
public static void main(String[] args) {
if (args.length != 2) {
System.out.println(
"Please provide source and destination file names, for example:");
System.out.println("java FIleCopy abc.datcopyOfabc.dat");
// Exit the program
System.exit(0);
}
Path sourcePath = Paths.get(args[0]);
Path destinationPath = Paths.get(args[1]);
try {
Files.copy(sourcePath, destinationPath);
System.out.println("The file " + args[0] + " is copied to " + args[1]);
} catch (IOException ioe) {
System.out.println("Can't copy file: " +
ioe.getMessage());
}
}
}
The file copy is done by calling the method copy
on the class Files
. This program will work fine as long as the destination file doesn’t exist. You can check for the file’s existence in the program by using the method exists
of the class Files
as it was done in the class MyTextFileReader
earlier.
Imagine a building that, with a push of a button, can be turned into a pile of construction materials. Load all these materials on the truck and drive to a different city. On arrival push another button, and the building is magically re-created in its original form. This is what Java serialization is about, but instead of a building we’ll use a Java object. By “clicking the serialize button” JVM turns an instance of an object into a pile of bytes, and “clicking the deserialize button” re-creates the object.
Why would you need such functionality? Say you are working on a board game and want to be able to save the current state of the game so the player can continue playing it tomorrow even if the computer will need to be rebooted. The program needs to save the state of the game in a file, and when the player launches the game again, the program should load the saved state and recreate the situation on the board. Creating a Java class with fields like player name, level, score, and lives can be a good way to represent a state of the game.
class GameState {
String playerName;
int level;
int score;
int remainingLives;
// other fields go here
}
When the user selects the menu option Save Game State, the program has to create an instance of the object GameState
, assign the values to its fields and save these values in a file. But which format to save these values in? Create a String
variable, and concatenate all these values into it separating the values with commas? It’s a lot of work. Also, you’d need to remember the order of these concatenated values so that when you need to read them you know which fields of the object GameState
to assign them to.
Luckily, Java greatly simplifies this process. You can create an instance of the Java object, populate it with the values, and then serialize this instance into a bunch of bytes in a special format that remembers the structure of the class GameState
. Accordingly, when you need to re-create the instance of the GameState
for these bytes, you can deserialize the object in one shot with all its fields' values.
A Java object can be serialized if it implements Serializabe
interface. It’s a very simple interface to implement, as it doesn’t declare any abstract methods. Just add implements Serializable
to the class declaration to make a class serializable. The following class GameState
will represent the state of the game.
import java.io.Serializable;
public class GameState implements Serializable {
String playerName; // (1)
int level;
int score;
int remainingLives;
GameState(String playerName, int Level, // (2)
int score, int remainingLives){
this.playerName = playerName;
this.level=level;
this.score=score;
this.remainingLives = remainingLives;
}
public String toString(){ // (3)
return "PlayerName: " + playerName + ", level:" + level + ", score: " + score +
", remainingLives: " + remainingLives;
}
}
-
The
GameState
class has four fields that describe the current state of the game. -
The class constructor populates these fields with the values passed as arguments.
-
We override the method
toString
to be able to print the content of the fields. The methodtoString
is declared in the classObject
- the ancestor of all Java classes. For example, if you writeSystem.out.println(myObject)
, Java finds and invokes thetoString`method on the object `myObject
. You’ll see this in action in the classGameStateManager
below.
Java objects are serialized into an I/O stream of type ObjectOutputStream
. You’ll need to create an instance of OutputStream
, pipe it with ObjectOutputStream
, and invoke a method writeObject
.
To deserialize (to read back) a Java object you need to create an instance of InputStream
, pipe it with ObjectInputStream
, and invoke a method readObject
.
Let’s create a class GameStateManager
that will perform the following actions:
-
Create an instance of the class
GameState
and populate its fields with some data. -
Serialize this instance of
GameState
into the file namedgamestate.ser
. -
Deserialize the instance of
GameState
from the filegamestate.ser
into a new object.
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class GameStateManager {
public static void main(String[] args) {
// Create and populate the GameState object
GameState gameState =
new GameState("Mary", 45,28000,3); // (1)
// The file for serialization/deserialization
Path path = Paths.get("gamestate.ser"); // (2)
saveGameState(path, gameState); // (3)
System.out.println("The GameStateObject is serialized");
GameState deserializedGameState = // (4)
loadGameState(path);
System.out.println("Deserialized game state; " +
deserializedGameState);
}
// Serialize the gameState into a file
private static void saveGameState(Path path, // (5)
GameState gameState) {
try (ObjectOutputStream whereToWrite = // (6)
new ObjectOutputStream(Files.newOutputStream(
path, StandardOpenOption.CREATE))){
whereToWrite.writeObject(gameState); // (7)
} catch (IOException ioe) {
System.out.println("Can't serialize file: " + ioe.getMessage());
}
}
// Deserialize the GameState from a file
private static GameState loadGameState(Path path){
GameState loadedGameState = null;
try (ObjectInputStream whereToReadFrom = // (8)
new ObjectInputStream(Files.newInputStream(path))){
loadedGameState= (GameState) whereToReadFrom.readObject(); // (9)
} catch (ClassNotFoundException cnfe) {
System.out.println("Can't find the declaration of GameState: " + cnfe.getMessage());
} catch (IOException ioe) {
System.out.println("Can't deserialize file: " + ioe.getMessage());
}
return loadedGameState;
}
}
-
The program starts with creating and populating the game state object. In this example I used hard-coded values, but in a real game they should be taken from the variables that reflect the actual game status.
-
The serialized file’s name is
gamestate.ser
and it’ll be represented by thePath
object. -
Then we call the method
saveGameState
that will serialize the objectGameState
into a file. -
After serialization is complete, we deserialize it back into another instance of the game state object. The variable
deserializedGameState
points at this new instance. -
The method
saveGameState
is responsible for serializing the object. It takes two parameters - the path to the file and the reference to the object that has to be serialized. -
Now we’re opening the file’s
OutputStream
and pipe it up with theObjectOutputStream
. -
It takes just one line of code to serialize the
GameState
object into the file using the methodwrite
. -
The method
loadGameState
deserializes the object from a file, and we’re opening theInputStream
and pipe it up with theObjectInputStream
.The data from the file will be read in the same format and order as it was written. So if you change the declaration of the classGameState
between serialization and deserialization, Java will throwInvalidClassException
. -
It takes just one line of code to deserialize the file into a
GameState
object using the methodread
. In this example the same program serializes and deserializes the game state, but this may not be the case. You can send the serialized object to another computer on the network, and another program may deserialize theGameState
. Hence it’s important that the declaration of theGameState
is available on both computers. The exceptionClassNotFoundException
will be thrown if the declaration of the classGameState
is not available.
Run the program GameStateManager
, and you’ll see that it creates a new file gamestate.ser
. Open this file in a text editor, and you’ll see some gibberish. Java serializes objects in its internal format, which is not meant to be read by people. The console output will look like this:
The GameStateObject is serialized
Deserialized the game state
Deserialized game state object; PlayerName: Mary, level:0, score: 28000, remainingLives: 3
If you place the breakpoint right after the invokation of the loadGameState
method, you’ll be able to see two different instances of the GameState
object as shown below:
Note how IDEA conveniently shows the values of the variables not only in the debugger’s view at the bottom but in the source code of the program in grayed out font.
In this project you’ll write a program that can keep the history of the game scores in a file by using Java serialization. This program should be able to deserialize and sort the saved scores showing the highest scores on top.
To complete this project you need to understand how to compare objects. While comparing two numbers is easy, Java objects may include multiple fields and you need to decide which fields of the object should be compared to place objects in a certain order. In this assignment you’ll be ordering objects Score
so the objects with larger value of the field score
should come first. In other words, you’ll need to sort the objects Score
in the descending order of the field score
.
-
Create a serializable class
Score
to represent a game score.import java.io.Serializable; import java.time.LocalDateTime; class Score implements Serializable { String name; int score; LocalDateTime dateTime; Score(String name, int score, LocalDateTime dateTime){ this.name=name; this.score=score; this.dateTime=dateTime; } public String toString(){ return name + " scored " + score + " on " + dateTime; } }
-
Create a class
ScoreManager
with the methodmain
. Insidemain
declare and instantiate a collection of scores:List<Score> scores
. Create thePath
object to point at the filescores.ser
.List<Score> scores = new ArrayList<>(); Path path = Paths.get("scores.ser");
The class
ArrayList
is one of the Java collections that implements theList
interface, so declaring this variable of typeList
is valid. -
Add the methods
saveScores
andloadScores
to the classScoreManager
:private static void saveScores(Path path, List<Score> gameScores) {} private static List<Score> loadScores(Path path){}
-
Write the code in the method
saveScore
to serialize the collectionscores
into a filescores.ser
. Use the code from the classGameStateManager
as an example, but this time you’ll need to serialize not one object instance, but a collection of objects, for example:try (ObjectOutputStream whereToWrite = new ObjectOutputStream( Files.newOutputStream(path, StandardOpenOption.CREATE))){ whereToWrite.writeObject(gameScores); } catch (IOException ioe) { System.out.println("Can't serialize scores: " + ioe.getMessage()); }
-
Write the code in the method
loadScores
to deserialize the data from the filescores.ser
:List<Score> loadedScores= null; try (ObjectInputStream whereToReadFrom = new ObjectInputStream(Files.newInputStream(path))){ loadedScores= (List<Score>) whereToReadFrom.readObject(); } catch (ClassNotFoundException cnfe) { System.out.println("Can't find the declaration of Score: " + cnfe.getMessage()); } catch (IOException ioe) { System.out.println("Can't deserialize file: " + ioe.getMessage()); } return loadedScores;
-
Starting from this step all coding should be done in the
main
method ofScoreManager
. If the file scores.ser already exists, load the collection of score. If scores.ser doesn’t exist, create a new collection.if (Files.exists(path)) { scores = loadScores(path); } else { scores = new ArrayList<>(); }
-
Create an instance of the
Score
class, populate its fieldsname
,score
, anddateTime
. Assign a random number generated by the classjava.util.Random
to thescore
field. Assign the current date and time to the fielddateTime
Use the methodnextInt
to generate a number between 0 and 50000. Add the hew score to thescores
collection.Random numberGenerator = new Random(); Score myScore = new Score("Mary", numberGenerator.nextInt(50000), LocalDateTime.now()); scores.add(myScore);
-
Print all the scores and invoke the method
saveScore
to serialize allscores
into the file.System.out.println("All scores:"); scores.forEach(s -> System.out.println(s)); saveScores(path, scores);
-
Sort and print the collection to show the highest score on top. Use the class
Comarator
to specify that sorting should be done by the fieldscore
. For sorting in the descendant order invoke the methodreverseOrder
. Use the Stream API.System.out.println("Sorted scores (highest on top):"); Comparator<Score> byScoreDescending = Collections.reverseOrder(Comparator.comparing(s -> s.score)); scores.stream() .sorted(byScoreDescending) .forEach(s -> System.out.println(s));
-
Run the program
ScoreManager
and observe that all scores are printed in descending order.