Java Serialization: Building a Persistent Phone Book

    Lincoln Daniel
    Share

    Serialization is a powerful tool. It’s what allows us to store objects on disk and reconstruct them in memory when necessary — perhaps when the program restarts. For anyone who wants to move beyond the run-and-forget type of programs, serialization is essential. We can serialize objects to create a sense of persistence in our programs so that the information we gather is not lost, or forgotten, when our program ends. Let’s take a look at how to implement serialization in Java

    The folks over at Oracle define serializable objects like so:

    To serialize an object means to convert its state to a byte stream so that the byte stream can be reverted back into a copy of the object.

    There may be many reasons for a programmer to serialize an object, but we will focus on the reason of storing it in a file to be accessed later.

    In this case, deserialization is then the process of reverting the serialized form of an object into a copy of the object to use as expected. You can serialize any object that is an instance of a class that either implements java.io.Serializable interface or its subinterface, java.io.Externalizable itself or is the subclass of a class that does.

    The Application

    We’re going to build a simple phone book app in the console. The program will allow a user to add contacts to their phone book, view their contacts, delete contacts, and save their contacts to a file for later. When the user ends the program and relaunches it later, they will be asked for the name of the file they want to load their phone book contacts from. If a file containing the serialized form of the phone book’s contacts exists, the phone book will be loaded from disk and made available to the user for manipulation.

    The code related to this article can be found here.

    Focus

    Following the single responsibility principle, there are six classes — PhoneBookApp (the main class), PhoneBook, PhoneBookEntry, and PhoneBookFileManager — and two packages in this project. However, for the sake of time, we’ll only dive into our main class and the package containing classes that encapsulate our phone book’s functionality and we’ll only focus on the parts of each class that pertain to serialization.

    PhoneBookApp

    public class PhoneBookApp {
        //the name of the file to save and or load a phone book to or from
        private static String phoneBookFileName = "default-phone-book";
        //the phone book to store all the user's contacts
        private static PhoneBook phoneBook;
        //initialize a Scanner to capture user input
        private static Scanner userInputScanner = new Scanner(System.in);
        public static void main(String[] args) {
            Logger.message("Starting Phone Book App!");
            loadPhoneBook();
    
            //forever
            for(;;) {
                //show the menu
                showMenu();
            }
        }
    
        private static void loadPhoneBook() {...}
        private static void showMenu() {...}
        private static void handleUserMenuSelection(){...}
        private enum UserOption{...} 
    }
    

    Our PhoneBookApp class handles running our program to interact with the user. The program starts off by letting the user know we are loading the phone book. Our loadPhoneBook() method is called to do this work by asking the user for the name of the file they want to load their phone book from. At first, the file should not exist, so we will tell the user that we are going to create a new PhoneBook to store their contacts in. This new phone book is represented by the phoneBook variable and its entries will be saved in a file with the provided name when the time comes. If a phone book is loaded from disk, it will be returned and stored in phoneBook:

    private static void loadPhoneBook() {
            Logger.prompt("Where do you want to load your phone book from? File name: ");
            if(userInputScanner.hasNextLine()) {
                phoneBookFileName = userInputScanner.nextLine();
            }
    
            //try to load the user's phone book with the file name
            phoneBook = PhoneBook.load(phoneBookFileName);
            if(phoneBook != null) {
                //great, the phone book was loaded
                Logger.message(format("Loaded your %s phone book of %d contacts.", 
                        phoneBookFileName, phoneBook.getSize())
                );
            } else {
                //no phone book loaded. create new one
                phoneBook = new PhoneBook(phoneBookFileName);
                Logger.message("Opened a new phone book at " + phoneBookFileName);
            }
        }
    

    The main method finishes by showing the menu of possible actions to the user forever, or until the user quits. The user can view contacts stored in the phone book, add new contacts, delete contacts, save the contacts to the phone book’s file, and quit to end the program. Those actions are represented by our UserOption enum and are handled in our handleUserMenuSelection() method. It simply takes in the user input and matches it to a UserOption with a switch to complete the action.

    PhoneBook

    public class PhoneBook {
        //the name of the file to store this phone book in disk. Unchangeable
        private final String fileName;
        /*Stores entries for this phone book. 
        The entries of this map may be referred to as contacts*/
        private final HashMap<String,PhoneBookEntry> entriesMap = new HashMap<>();
        //the number of unsaved changes, such as new or removed contacts, to this phone book.
        private int numUnsavedChanges = 0;
    
        public PhoneBook(String fileName) {}
        public Collection<PhoneBookEntry> getEntries() {}
        public String getFileName() {...}
        public int getSize() {...}
        public int getNumUnsavedChanges() {...}
    
        public AddContactResult addContact(String name, String number) {...}
    
        public void addFromDisk(Collection<PhoneBookEntry> phoneBookEntries) {...}
    
        public boolean deleteContact(String name) {...}
        public void display() {...}
        public boolean isValidName(String name) {...}
        private boolean isValidPhoneNumber(String number) {}
        public boolean save() {...}
        public static PhoneBook load(String fileName) {...}
        public enum AddContactResult {...}
    }
    

    Our PhoneBook class will model phone books. It will handle all manipulations to the user’s contacts in their phone books. To instantiate a new PhoneBook, we simply need to provide its constructor the name of the file to save it to. A phone book’s contact entries are stored in a HashMap where the name of a contact is the key to ensure that each name only maps to one PhoneBookEntry. If you want duplicate names, feel free to switch this to a List.

    The PhoneBook class provides instance methods to add and delete contacts from a phone book and save its contacts to its file on disk. The class also provides a static load method that accepts a file name to load and return a PhoneBook from. The save() and load() methods of this class consult their corresponding methods in the package-local PhoneBookFileManager class:

    public boolean save() {
        boolean success = PhoneBookFileManager.save(this);
        if(success) numUnsavedChanges = 0;
        return success;
    }
    
    public static PhoneBook load(String fileName) {
        return PhoneBookFileManager.load((fileName));
    }
    

    PhoneBookEntry

    An instance of the PhoneBookEntry class is responsible for holding information about a phone book contact. It must hold and display the name and phone number of the contact. That’s it, right? Well, almost. This class is more important than may meet the eyes. Let me explain.

    This class is instrumental in the serialization of our phone book. If it hasn’t been clear yet, it is important to realize that a phone book is an abstract concept. What is physical is the contacts that make up the phone book. Thus, the most important part of our program is PhoneBookEntry because it models phone book contacts. If we have the contacts, we automatically have a phone book because we can easily construct and populate a phone book with them. Therefore, we only need to store the contacts in the file on disk.

    Great, so how do we do that?

    class PhoneBookEntry implements Serializable {
        //the name of this contact.
        private final String name;
        //the number of this contact.
        private String number;
        /*whether or not this contact is new and unsaved. 
        This won't be serialized.*/
        private transient boolean isNew;
    
        public PhoneBookEntry(String name, String number) {
            this.name = name;
            this.number = number;
            this.isNew = true;
        }
    
        public void setNumber(String number) {...}
        public String getName() {...}
    
        public void setIsNew(boolean isNew) {...}
    
        @Override
        public String toString() {...}
    }
    

    Observe that this class implements Serializable. As I explained above, this means an instance of it is serializable: able to be converted to a byte stream. As a byte stream, we can write out the object to a file. Wait, so that’s it? Just about, but there are a few more things you should know.

    All the information that identifies the class is recorded in the serialized stream. That means that all the information stored in a serialized object will be stored in the file when we write it out. But what if you don’t want a field to be part of the byte stream that is written out? You’re in luck: Java provides a handy modifier called transient that you can use to ignore a field during serialization. If you take a look at our PhoneBookEntry definition, you’ll see we’re ignoring the isNew field. Consequently, only the name and number of a contact will be part of its byte stream. Once we’ve covered all of that, we can proceed to serialize our object.

    PhoneBookManager

    class PhoneBookFileManager {
    
        protected static boolean save(PhoneBook phoneBook) {...}
    
        protected static PhoneBook load(String fileName) {...}
    
        private static String getFileNameWithExtension(String fileName) {...}
    
        private static void closeCloseable(Closeable closeable) {...}
    }
    

    In line with the single responsibility principle, our PhoneBook class isn’t directly responsible for saving and loading a phone book to and from a file on disk; the PhoneBookFileManager takes on that responsibility. The last two methods of this class are simple utility methods that are used by the more vital save() and load() methods. Let’s explore the latter two.

    The save() method is responsible for serializing the provided PhoneBook‘s contacts and writing them out to a file identified by the phone book’s fileName instance field on disk. While the methods are well-documented, let me quickly explain how they work.

    protected static boolean save(PhoneBook phoneBook) {
        if(phoneBook != null) {
            String fileName = phoneBook.getFileName();
    
            //make sure the file is a txt file
            String fileNameAndExt = getFileNameWithExtension(fileName);
    
            FileOutputStream fileOutputStream = null;
            ObjectOutputStream objectOutputStream = null;
            try {
                //create a file output stream to write the objects to
                fileOutputStream = new FileOutputStream(fileNameAndExt);
                /*create an object output stream 
                to write out the objects to the file*/
                objectOutputStream = new ObjectOutputStream(fileOutputStream);
    
                /*convert the collection of phone book entries into a LinkedList
                because LinkedLists implement Serializable*/
                LinkedList<PhoneBookEntry> serializableList = 
                    new LinkedList<>(phoneBook.getEntries());
                //write the serializable list to the object output stream
                objectOutputStream.writeObject(serializableList);
                //flush the object output stream
                objectOutputStream.flush();
    
                /*set each entry's isNew value to false 
                because they are saved now.*/
                for(PhoneBookEntry entry: serializableList) {
                    entry.setIsNew(false);
                }
    
                //SUCCESS!
                return true;
            } catch (IOException e) {
                //fail
                e.printStackTrace();
            } finally {
                /*before proceeding, 
                close output streams if they were opened*/
                closeCloseable(fileOutputStream);
                closeCloseable(objectOutputStream);
            }
        }
    
        //fail
        return false;
    }
    

    Serializing the Phone Book Entries

    To serialize the phone book’s contacts, we get its file name and extension to instantiate a FileOutputStream with. Using our fileOutputStream, we instantiate an ObjectOutputStream: we can write out the objects to the file through this. Next, we must get the contacts from the phone book.

    Calling our phoneBook‘s getEntries() instance method will return a Collection of all of the PhoneBookEntry instances stored in our phone book. Because the Collection interface does not implement Serializable, we must convert it to a LinkedList or ArrayList which do; I chose LinkedList because it relates to me (think about it).

    Now that we have our serializable object, we can write it to our objectOutputStream and flush that to the fileOutputStream. And that’s it for serializing our phone book contacts and saving them to a file. Using our try-catch blocks to catch any errors that may arise, we end with a finally block to close the output streams before returning the result of the save operation. If you open up the file you provided to save the contacts to, you’ll see it’s filled with the data, albeit unreadable by humans.

    Deserializing the Phone Book Entries

    On to loading the contacts back in from the file:

    protected static PhoneBook load(String fileName) {
        if(fileName != null && !fileName.trim().isEmpty()) {
            //make sure the file is a txt file
            String fileNameWithExt = getFileNameWithExtension(fileName);
    
            FileInputStream fileInputStream = null;
            ObjectInputStream objectinputstream = null;
            try {
    
                /*create the file input stream with 
                the fileNameWithExt to read the objects from*/
                fileInputStream = new FileInputStream(fileNameWithExt);
                /*create an object input stream on the file input stream 
                to read in the objects from the file*/
                objectinputstream = new ObjectInputStream(fileInputStream);
    
                /*read the deserialized object from the object input stream 
                and cast it to a collection of PhoneBookEntry*/
                Collection<PhoneBookEntry> deserializedPhoneBookEntries = 
                    (Collection<PhoneBookEntry>) objectinputstream.readObject();
    
                //create a new PhoneBook to load the deserialized entries into
                PhoneBook phoneBook = new PhoneBook(fileName);
                //add the collection of phone book entries to the phone book
                phoneBook.addFromFile(deserializedPhoneBookEntries);
    
                //SUCCESSS! Rreturn the loaded phone book
                return phoneBook;
            } catch (FileNotFoundException e) {
                //fail
                Logger.debug(format("Loading phone book from %s failed." 
                    + " No phone book found at that directory.",
                        fileNameWithExt));
            } catch (IOException e) {
                //fail
                e.printStackTrace();
                Logger.debug(format("Loading phone book from %s failed. %s.",
                        fileNameWithExt, e.getMessage())
                );
            } catch (ClassNotFoundException e) {
                //fail
                e.printStackTrace();
                Logger.debug(format("Loading phone book from %s failed. " +
                    "Error deserializing data and converting to proper object.",
                     fileNameWithExt)
                );
            } finally {
                //before proceeding, close input streams if they were opened
                closeCloseable(fileInputStream);
                closeCloseable(objectinputstream);
            }
        }
    
        //fail
        return null;
    }
    

    Besides being in the exact opposite direction, the load() method works much like save(). Instead of opening a ObjectOutputStream with a FileOutputStream, we open an ObjectInputStream with a FileInputStream. As you would expect, we call the readObject() instance method of our objectInputStream to read the contents of the file in as an object. We must decide what type of object we are reading in and store it in the appropriate variable. Because we wrote out a Collection to the file, we should read it back in as such; that’s why we cast the read object to a Collection.

    Now that we have our deserialized collection of PhoneBookEntry‘s we can reconstruct a PhoneBook object from it. Towards the end of the try block, we instantiate a new PhoneBook with the provided file name and call its addFromFile() method with the collection of entries as the lone argument. We finish this method by closing the input streams and returning the loaded PhoneBook object to the caller. This is how PhoneBook.addFromFile() looks:

    public void addFromFile(Collection<PhoneBookEntry> phoneBookEntries) {
        if(phoneBookEntries != null) {
            for (PhoneBookEntry entry : phoneBookEntries) {
                if (entry != null) {
                    entriesMap.put(entry.getName(), entry);
                }
            }
        }
    }
    

    Unlike PhoneBook.addContact(), this method does not validate the contact’s name or number before adding it to the phone book’s entries map because we trust that the file was not corrupted.

    phones

    Running the Program

    To test and observe the serialization and deserialization functionality, you’ll need to run the program twice. First, run it to add contacts and save it. You’ll need to save the file before ending the program in order for it to serialize the phone book’s entries into the provided file. The second time you run it, provide the same file name as the first time to deserialize the phone book’s contacts and load it in from the file.

    Take note of the print out of the contacts in the phone book on the second run after adding a new contact. The contacts that were loaded from the file are not printed with a “new” marking because their transient isNew field was not serialized. Only the contacts added after loading the phone book’s contacts from disk have the “new” marking.

    Be sure not to change the fields of PhoneBookEntry after serializing the phone book or you’ll have an incompatibility problem with the serialVersionUID of PhoneBookEntry when you attempt to deserialize the objects.

    Conclusion

    That was a long one, but it’s over now – I’m glad you made it! We managed to develop a functional phone book application while following the single responsibility principle. You could even use it as your own phone book in case your smartphone quits on you out of nowhere.

    You now know how to serialize and deserialize objects as well as how to store them in a file and read them back into your program when necessary. The application itself could use some improvements for validation of user provided data and better user experience. Try to tweak it and make it better. See what you can build with it.

    Click here and here for a possible output of the program. While the menu is printed before each action, I’ve only shown it once for each run of the program for brevity. If you have any questions, please feel free to leave a comment, I’ll do my best to answer.

    CSS Master, 3rd Edition