Статьи

Сериализация Java: создание постоянной телефонной книги

Сериализация является мощным инструментом. Это то, что позволяет нам хранить объекты на диске и восстанавливать их в памяти при необходимости — возможно, при перезапуске программы. Для тех, кто хочет выйти за рамки программ типа «забей и забудь», сериализация необходима. Мы можем сериализовать объекты, чтобы создать ощущение постоянства в наших программах, чтобы информация, которую мы собираем, не терялась и не забывалась при завершении нашей программы. Давайте посмотрим, как реализовать сериализацию в Java

Люди в Oracle определяют сериализуемые объекты следующим образом:

Сериализация объекта означает преобразование его состояния в поток байтов, чтобы поток байтов мог быть возвращен обратно в копию объекта.

У программиста может быть много причин сериализовать объект, но мы сосредоточимся на причине его сохранения в файле, к которому мы будем обращаться позже.

В этом случае десериализация — это процесс преобразования сериализованной формы объекта в копию объекта для использования, как ожидается. Вы можете сериализовать любой объект, который является экземпляром класса, который либо реализует интерфейс java.io.Serializable или его подинтерфейс, сам java.io.Externalizable , либо является подклассом класса, который это делает.

Приложение

Мы собираемся создать простое приложение телефонной книги в консоли. Программа позволит пользователю добавлять контакты в свою телефонную книгу, просматривать их контакты, удалять контакты и сохранять свои контакты в файл для последующего использования. Когда пользователь завершает работу программы и запускает ее позже, ему будет предложено указать имя файла, из которого он хочет загрузить контакты своей телефонной книги. Если существует файл, содержащий сериализованную форму контактов телефонной книги, телефонная книга будет загружена с диска и предоставлена ​​пользователю для манипулирования.

Код, связанный с этой статьей, можно найти здесь .

фокус

Следуя принципу единой ответственности , в этом проекте есть шесть классов — PhoneBookApp (основной класс), PhoneBook , PhoneBookEntry и PhoneBookFileManager — и два пакета. Однако ради времени мы углубимся только в наш основной класс и пакет, содержащий классы, которые инкапсулируют функциональность нашей телефонной книги, и сосредоточимся только на частях каждого класса, которые относятся к сериализации.

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{...} } 

Наш класс PhoneBookApp обрабатывает нашу программу для взаимодействия с пользователем. Программа запускается, сообщая пользователю, что мы загружаем телефонную книгу. Наш loadPhoneBook() вызывается для выполнения этой работы, запрашивая у пользователя имя файла, из которого он хочет загрузить свою телефонную книгу. Сначала файл не должен существовать, поэтому мы сообщим пользователю, что собираемся создать новую телефонную книгу для хранения своих контактов. Эта новая телефонная книга представлена ​​переменной phoneBook и ее записи будут сохранены в файле с предоставленное имя, когда придет время. Если телефонная книга загружена с диска, она будет возвращена и сохранена в 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); } } 

Основной метод завершается показом меню возможных действий пользователю навсегда или до тех пор, пока пользователь не выйдет. Пользователь может просматривать контакты, хранящиеся в телефонной книге, добавлять новые контакты, удалять контакты, сохранять контакты в файл телефонной книги и выходить из системы, чтобы завершить работу программы. Эти действия представлены нашим перечислением UserOption и обрабатываются нашим handleUserMenuSelection() . Он просто принимает пользовательский ввод и сопоставляет его с UserOption с switch для завершения действия.

Телефонная книга

 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 {...} } 

Наш класс PhoneBook будет моделировать телефонные книги. Он будет обрабатывать все манипуляции с контактами пользователя в его телефонных книгах. Чтобы создать новую PhoneBook , нам просто нужно предоставить ее конструктору имя файла, в который она будет сохранена. Записи контактов телефонной книги хранятся в HashMap где имя контакта является ключом, гарантирующим, что каждое имя сопоставляется только одному PhoneBookEntry . Если вы хотите дублировать имена, не стесняйтесь переключать это на List .

Класс PhoneBook предоставляет методы экземпляров для добавления и удаления контактов из телефонной книги и сохранения их контактов в файл на диске. Класс также предоставляет метод static загрузки, который принимает имя файла для загрузки и возврата PhoneBook . Методы save() и load() этого класса обращаются к своим соответствующим методам в классе PhoneBookFileManager пакета:

 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

Экземпляр класса PhoneBookEntry отвечает за хранение информации о контакте телефонной книги. Он должен удерживать и отображать имя и номер телефона контакта. Вот так, верно? Ну, почти. Этот класс важнее, чем может показаться на первый взгляд. Позволь мне объяснить.

Этот класс способствует сериализации нашей телефонной книги. Если это еще не ясно, важно понимать, что телефонная книга — это абстрактное понятие. Что является физическим, это контакты, которые составляют телефонную книгу. Таким образом, наиболее важной частью нашей программы является PhoneBookEntry поскольку она моделирует контакты телефонной книги. Если у нас есть контакты, у нас автоматически есть телефонная книга, потому что мы можем легко создать и заполнить телефонную книгу с ними. Поэтому нам нужно только хранить контакты в файле на диске.

Отлично, так как мы это сделаем?

 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() {...} } 

Обратите внимание, что этот класс реализует Serializable . Как я объяснил выше, это означает, что его экземпляр сериализуем: может быть преобразован в поток байтов. Как поток байтов мы можем записать объект в файл. Подождите, вот и все? Просто, но есть еще несколько вещей, которые вы должны знать.

Вся информация, которая идентифицирует класс, записывается в сериализованный поток. Это означает, что вся информация, хранящаяся в сериализованном объекте, будет сохранена в файле при его записи. Но что, если вы не хотите, чтобы поле было частью записанного потока байтов? Вам повезло: в Java есть удобный модификатор transient который вы можете использовать для игнорирования поля во время сериализации. Если вы посмотрите на наше определение PhoneBookEntry , то увидите, что мы игнорируем поле isNew . Следовательно, только name и number контакта будут частью его байтового потока. Как только мы охватим все это, мы можем приступить к сериализации нашего объекта.

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) {...} } 

В соответствии с принципом единой ответственности, наш класс PhoneBook не несет прямой ответственности за сохранение и загрузку телефонной книги в файл и из файла на диске; PhoneBookFileManager берет на себя эту ответственность. Последние два метода этого класса являются простыми служебными методами, которые используются наиболее важными методами save() и load() . Давайте рассмотрим последние два.

Метод save() отвечает за сериализацию контактов предоставленной телефонной книги и их запись в файл, указанный в поле экземпляра fileName телефонной книги на диске. Хотя методы хорошо документированы, позвольте мне быстро объяснить, как они работают.

 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; } 

Сериализация записей телефонной книги

Чтобы сериализовать контакты телефонной книги, мы получаем ее имя и расширение для создания экземпляра FileOutputStream . Используя наш fileOutputStream , мы создаем экземпляр ObjectOutputStream : через него мы можем записать объекты в файл. Далее мы должны получить контакты из телефонной книги.

Вызов метода экземпляра getEntries() нашей телефонной книги вернет Collection всех экземпляров PhoneBookEntry хранящихся в нашей телефонной книге. Поскольку интерфейс Collection не реализует Serializable , мы должны преобразовать его в LinkedList или ArrayList которые это делают; Я выбрал LinkedList потому что он относится ко мне (подумайте об этом).

Теперь, когда у нас есть наш сериализуемый объект, мы можем записать его в наш objectOutputStream и objectOutputStream его в fileOutputStream . И это все для сериализации контактов нашей телефонной книги и сохранения их в файл. Используя наши блоки try-catch для обнаружения любых ошибок, которые могут возникнуть, мы заканчиваем блоком finally чтобы закрыть выходные потоки перед возвратом результата операции сохранения. Если вы откроете файл, который вы предоставили для сохранения контактов, вы увидите, что он заполнен данными, хотя и нечитаемыми для людей.

Десериализация записей телефонной книги

Чтобы загрузить контакты обратно из файла:

 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; } 

Помимо нахождения в противоположном направлении, метод load() работает так же, как save() . Вместо того чтобы открывать ObjectOutputStream с помощью FileOutputStream , мы открываем ObjectInputStream с помощью FileInputStream . Как и следовало ожидать, мы вызываем метод экземпляра readObject() нашего objectInputStream для чтения содержимого файла как объекта. Мы должны решить, какой тип объекта мы читаем, и сохранить его в соответствующей переменной. Поскольку мы записали Collection в файл, мы должны прочитать ее обратно как таковую; Вот почему мы приводим прочитанный объект в Collection .

Теперь, когда у нас есть десериализованная коллекция PhoneBookEntry , мы можем восстановить объект PhoneBook из него. В конце блока try мы создаем новую телефонную addFromFile() с указанным именем файла и вызываем ее addFromFile() с коллекцией записей в качестве единственного аргумента. Мы заканчиваем этот метод, закрывая входные потоки и возвращая загруженный объект PhoneBook вызывающей стороне. Вот как выглядит PhoneBook.addFromFile() :

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

В отличие от PhoneBook.addContact() , этот метод не проверяет имя или номер контакта перед добавлением его в карту записей телефонной книги, поскольку мы уверены, что файл не был поврежден.

телефоны

Запуск программы

Чтобы протестировать и наблюдать за сериализацией и десериализацией, вам нужно запустить программу дважды. Сначала запустите его, чтобы добавить контакты и сохранить его. Вам нужно будет сохранить файл перед завершением программы, чтобы он мог сериализовать записи телефонной книги в предоставленный файл. При втором запуске укажите то же имя файла, что и при десериализации контактов телефонной книги, и загрузите его из файла.

Запишите распечатку контактов в телефонной книге при втором запуске после добавления нового контакта. Контакты, которые были загружены из файла, не печатаются с «новой» маркировкой, поскольку их transient isNew поле transient isNew не было сериализовано. Только контакты, добавленные после загрузки контактов телефонной книги с диска, имеют «новую» маркировку.

Обязательно не изменяйте поля PhoneBookEntry после сериализации телефонной книги, иначе у вас возникнет проблема несовместимости с serialVersionUID PhoneBookEntry при попытке десериализации объектов.

Вывод

Это было долго, но теперь все кончено — я рад, что вы сделали это! Нам удалось разработать функциональное приложение для телефонной книги, следуя принципу единой ответственности. Вы даже можете использовать его в качестве собственной телефонной книги на случай, если ваш смартфон выйдет из ниоткуда.

Теперь вы знаете, как сериализовать и десериализовать объекты, а также как хранить их в файле и при необходимости читать их обратно в вашу программу. Само приложение может использовать некоторые улучшения для проверки предоставленных пользователем данных и лучшего пользовательского опыта. Попробуйте настроить его и сделать его лучше. Посмотрите, что вы можете построить с ним.

Нажмите здесь и здесь для возможного вывода программы. Хотя меню печатается перед каждым действием, для краткости я показывал его только один раз при каждом запуске программы. Если у вас есть какие-либо вопросы, пожалуйста, не стесняйтесь оставлять комментарии, я сделаю все возможное, чтобы ответить.