Persistence
Many applications are only of limited use if they are not able to save their state each time they run. Persistence enables you to store information to disk and to read it back at a later time.
In this section you will learn:
- How to save data to a disk file
- How to read saved data back from a disk file
- An overview of different types of persistence
Saving data to disk
Thus far with the zoo application, every time you end the application any new, modified or deleted information about animals and zookeepers is lost. Persistence refers to the action of storing data permanently, typically to a disk file or a database.
The use of a relational database is beyond the scope of this course and is in any case better suited to larger scale applications with more sophisticated needs. It is worth mentioning, however, that Java is quite capable of connecting to many types of relational databases on various platforms.
The more straightforward approach used in this course will be to use object serialization; that is, writing the data of the appropriate objects to a binary file whenever the application is ended by the user, and then restoring that data from the file back into the application whenever it starts up again. To make use of object serialization you need to code each class that needs to be saved as implementing the Serializable interface. This interface, which is in the java.io package[1], is unusual in that it has no methods which you need to provide code for. It is simply a marker to Java to give it permission to save the values of its instance variables. A very useful feature that is built-in is that where a saved instance variable stores a reference to another object, which in turn may reference another, they will all be saved in one fell swoop, provided they are all marked as Serializable.
In the zoo application it is the ZooAdministrator object that holds references to the collections of ZooKeeper, Visitor and Pen objects. The Pen objects in turn hold references to collections of Animal objects. You only therefore need to serialize the ZooAdministrator object for them all to be saved. Before doing so, however, you need to change its class header to implement the the Serializable interface:
import java.io.*;
public class ZooAdministrator implements Serializable {
You should now also change the following classes to implement Serializable:
ZooKeeper,Visitor,PenandAnimal(note that you don't need to changeLion,MonkeyandPenguinsince they inherit fromAnimal).
public class ZooKeeper implements Emailable, Comparable<ZooKeeper>, Serializable {
public class Visitor implements Emailable, Comparable<Visitor>, Serializable {
public class Pen implements Comparable<Pen>, Serializable {
public abstract class Animal implements Comparable<Animal>, Serializable {
There are also references to the classes Email and Person that are in the com.example.util package, so these also need to be amended in the same way:
public final class Email implements Comparable<Email>, Serializable {
public class Person implements Serializable {
With the above in place, you can now start the process of coding the serialization section of the application. You may recall from an earlier section the client-server approach that the application has adopted:
In the above figure, the virtualzoo.ui package is the client and the virtualzoo.core package is the server. Because this separation involves two levels the client-server approach is also known as 2-tier. When you need to include a database of some description it is common to extend this into a 3-tier structure:
Even though you will not be using a relational database it is still useful to separate out the persistence mechanism into its own layer, as indicated in the above figure. Create a sub-package under virtualzoo call db, and then create a class in virtualzoo.db called ZooSerialization, which will be a singleton:
package virtualzoo.db;
import java.io.*;
import virtualzoo.core.*;
public class ZooSerialization {
public static final String ZOO_FILENAME = "zoo.ser";
private static ZooSerialization instance;
public static ZooSerialization getInstance() {
if (instance == null) {
instance = new ZooSerialization();
}
return instance;
}
private ZooSerialization() {
}
public void save(ZooAdministrator admin) throws IOException {
// code will go here...
}
public ZooAdministrator restore() throws ClassNotFoundException, IOException {
// code will go here...
}
}
The above class is comprised of the two methods save() and restore() which will be used to save and restore the zoo application's data respectively. Because reading and writing to disk files may not always work (for example, you might try to read a non-existent or corrupted file) then exceptions could get thrown. These will be propagated back to the caller, as shown in the method signatures above.
For the save() method code the following:
public void save(ZooAdministrator admin) throws IOException {
File f = new File(ZOO_FILENAME); // 1
FileOutputStream fos = new FileOutputStream(f); // 2
BufferedOutputStream bos = new BufferedOutputStream(fos); // 3
ObjectOutputStream oos = new ObjectOutputStream(bos); // 4
oos.writeObject(admin); // 5
oos.close(); // 6
System.out.println("Data has beeen saved to " + // 7
f.getAbsolutePath());
}
- Statement 1 instantiates a
Fileobject using theStringconstant as its file path[1] and name. The instantiatedFileobject does not have to exist on the disk and won't actually be created, so the object is just a pointer to either a file or directory that may or may not exist - Statement 2 instantiates a
FileOutputStreamobject pointing to the aboveFileobject. Objects of typeFileOutputStreamcan be used to save a series of bytes to a file - Statement 3 instantiates a
BufferedOutputStreamobject which contains the aboveFileOutputStream. Creating aBufferedOutputStreamis optional but recommended, since it uses a memory buffer to speed up the process of writing the data to the disk. You can think of it as a "wrapper" over theFileOutputStreamthat adds the additional capability of being buffered - Statement 4 instantiates an
ObjectOutputStreamobject consisting of the aboveBufferedOutputStream. Objects of typeObjectOutputStreamare used to for the actual serialization process. You can think of it as a "wrapper" over theBufferedOutputStreamthat adds the additional capability of allowing serialization of objects - Statement 5 invokes the
writeObject()method on theObjectOutputStreamto save theZooAdministratorobject passed as the argument to thesave()method. ThewriteObject()method is what actually performs the serialization of the object to the disk file - Statement 6 closes the
ObjectOutputStream(which will result in the other stream objects being closed since they are wrapped inside of it). It is important to remember to close your streams in order to ensure the data is actually written to the disk and to release its resources - Statement 7 writes the absolute path and file name to the Output window
The code for the restore() method is similar except that it uses input streams instead of output streams and returns the restored object:
public ZooAdministrator restore() throws ClassNotFoundException,
IOException {
File f = new File(ZOO_FILENAME);
FileInputStream fis = new FileInputStream(f);
BufferedInputStream bis = new BufferedInputStream(fis);
ObjectInputStream ois = new ObjectInputStream(bis);
ZooAdministrator admin = (ZooAdministrator) ois.readObject();
ois.close();
System.out.println("Data has beeen loaded from " +
f.getAbsolutePath());
return admin;
}
- You use the
readObject()method of theObjectInputStreamclass to read the serialized data from the disk back into the application. Because this methods returns anObjectyou need to cast it back into aZooAdministrator
As mentioned earlier, the act of serializing an object causes that object's instance (but not static) variables to be saved, so in your case it will save all instance variables in the ZooAdministrator class, plus all referenced objects, etc. There is a slight issue, however, since two of the instance variables are used by the UI as listeners, namely animalListeners and zooKeeperListeners. The upshot of this is that UI listeners don't get serialized, so special handling is required to cater for this, as follows:
- Mark the listener instance variables as
transient. Variables markedtransientwill be ignored by the serialization process - As part of the restoration process, because the listener variables were not saved you need to instantiate them again
In ZooAdministrator add the transient keyword to the declaration of the two listener instance variables:
private transient Collection<ZooKeeperListener> zooKeeperListeners; private transient Collection<AnimalListener> animalListeners;
Now add a method to ZooAdministrator called readObject():
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
zooKeeperListeners = new ArrayList<ZooKeeperListener>();
animalListeners = new ArrayList<AnimalListener>();
}
- The de-serialization process looks for a method with the above signature and if found executes it. As coded above, its first task is to call
defaultReadObject()(which performs the normal de-serialization) followed by simply instantiating empty listener collections
Because the UI should only converse with the core system you need to add another method to ZooAdministrator for that purpose:
public static void save() throws Exception {
ZooSerialization zser = ZooSerialization.getInstance();
zser.save(getInstance());
}
- Note that the method is
staticand that you need to importvirtualzoo.db - The method simply invokes the
save()method ofZooSerialization
The restoration of the serialized data is most readily done in the static getInstance() method, so modify it as follows:
public static ZooAdministrator getInstance() {
if (instance == null) {
ZooSerialization zser = ZooSerialization.getInstance();
try {
instance = zser.restore();
} catch (FileNotFoundException ex) {
instance = new ZooAdministrator();
} catch (Exception ex) {
ex.printStackTrace();
}
}
return instance;
}
- The above attempts to de-serialize the file, but if it wasn't found (as would be the case the very first time you run the application) then the constructor is called instead. If any other error occurs, the
printStackTrace()method is invoked to send the details to the Output window
Finally, you need to modify the confirmClose() method in AdministratorFrame to invoke the save() method of the ZooAdministrator object:
public void confirmClose() {
// Prompt for confirmation
int response = JOptionPane.showConfirmDialog(this,
"Are you sure you want to exit?",
"Exit Person Manager",
JOptionPane.YES_NO_OPTION);
// See what response is
if (response == JOptionPane.YES_OPTION) {
try {
// End the application
ZooAdministrator.save();
} catch (Exception ex) {
ex.printStackTrace();
JOptionPane.showMessageDialog(this,
ex.getMessage(),
"Unable to save data.",
JOptionPane.ERROR_MESSAGE);
} finally {
dispose();
}
}
}
- You need to import
virtualzoo.core - If an exception is thrown, then a dialog will be displayed
- Note the use of the
finallyblock so that the frame'sdispose()method is called whether or not an exception occurred
Comments