Java Serialization: Building a Persistent Phone Book

Share this article

Java Serialization: Building a Persistent Phone Book

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.

Frequently Asked Questions (FAQs) about Java Serialization and Building a Persistent Phone Book

What is Java Serialization and why is it important in building a persistent phone book?

Java Serialization is a mechanism that converts an object’s state into a byte stream, which can then be persisted into a file or sent over a network. It’s crucial in building a persistent phone book because it allows the phone book data to be stored and retrieved even after the application has been closed or restarted. This means that the data in your phone book won’t be lost when you close the application, making it “persistent”.

How can I serialize an object in Java?

To serialize an object in Java, you need to implement the Serializable interface in the class of the object you want to serialize. Then, you can use the ObjectOutputStream class to write the object to an output stream. Here’s a simple example:

public class Contact implements Serializable {
private String name;
private String phoneNumber;
// getters and setters
}

FileOutputStream fileOut = new FileOutputStream("contact.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(contact);
out.close();
fileOut.close();

How can I deserialize an object in Java?

Deserialization is the process of converting the byte stream back into an object. You can use the ObjectInputStream class to read the object from an input stream. Here’s an example:

FileInputStream fileIn = new FileInputStream("contact.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
Contact contact = (Contact) in.readObject();
in.close();
fileIn.close();

What is the purpose of the serialVersionUID in Java Serialization?

The serialVersionUID is a unique identifier for each Serializable class. It’s used during deserialization to verify that the sender and receiver of a serialized object have loaded classes for that object that are compatible in terms of serialization. If the receiver has loaded a class for the object that has a different serialVersionUID than that of the corresponding sender’s class, then deserialization will result in an InvalidClassException.

How can I handle changes to a Serializable class?

If you make changes to a Serializable class, you should update the serialVersionUID. If you don’t specify a serialVersionUID, the Java compiler will automatically generate one, which might change when you modify the class and cause an InvalidClassException during deserialization. To avoid this, you can declare your own serialVersionUID like this:

public class Contact implements Serializable {
private static final long serialVersionUID = 1L;
// ...
}

How can I store multiple objects in a file?

You can store multiple objects in a file by writing them to the ObjectOutputStream one after the other. When you read them back with an ObjectInputStream, you should read them in the same order.

How can I search for a contact in the phone book?

You can search for a contact in the phone book by reading all the contacts from the file and comparing each one with the search criteria. For example, you can compare the name of each contact with the search name.

How can I delete a contact from the phone book?

To delete a contact from the phone book, you can read all the contacts from the file, write the ones you want to keep to a temporary file, and then rename the temporary file to the original file.

How can I sort the contacts in the phone book?

You can sort the contacts in the phone book by reading all the contacts into a list, sorting the list using the Collections.sort() method, and then writing the sorted list back to the file.

How can I handle exceptions during serialization and deserialization?

You should handle exceptions during serialization and deserialization by using a try-catch block. For example, you should catch the IOException that might be thrown when you read or write an object, and the ClassNotFoundException that might be thrown when you read an object.

Lincoln DanielLincoln Daniel
View Author

Lincoln W Daniel is a software engineer who has worked at companies big and small. Receiving internship offers from companies like NASA JPL, he has worked at IBM and Medium.com. Lincoln also teaches programming concepts on his ModernNerd YouTube channel by which he emphasizes relating coding concepts to real human experiences.

java
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week