- Привет, Амиго! Сегодня мы познакомимся с еще одной интересной темой. А именно: сохранением и загрузкой (восстановлением) объектов. Допустим у нас есть класс Cat:
Код |
---|
class Cat { public String name; public int age; public int weight; } |
И мы хотим добавить в него удобный механизм сохранения в файл и загрузки из файла.
Вот как это можно сделать:
Код |
---|
class Cat { public String name; public int age; public int weight; public void save(OutputStream outputStream) throws Exception { PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(name); printWriter.println(age); printWriter.println(weight); } public void load(InputStream inputStream) throws Exception { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); name = reader.readLine(); age = Integer.parseInt(reader.readLine()); weight = Integer.parseInt(reader.readLine()); } } |
- О! Это же очень просто. Мы просто печатаем значения всех аргументов, по одному в каждой строчке. А при загрузке читаем их в том же порядке. Отличное решение.
- Спасибо, Амиго. А можешь написать, как будут выглядеть методы save и load у такой группы классов:
Код |
---|
class Cat { public String name; public int age; public int weight; } class Dog { public String name; public int age; } class Human { public Cat cat; public Dog dog; } |
У тебя есть объект человек, и он может иметь одну собаку и одного кота.
- У меня есть решение:
Код |
---|
class Cat { public String name; public int age; public int weight; public void save(OutputStream outputStream) throws Exception { PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(name); printWriter.println(age); printWriter.println(weight); } public void load(InputStream inputStream) throws Exception { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); name = reader.readLine(); age = Integer.parseInt(reader.readLine()); weight = Integer.parseInt(reader.readLine()); } } |
class Dog { public String name; public int age; public void save(OutputStream outputStream) throws Exception { PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(name); printWriter.println(age); } public void load(InputStream inputStream) throws Exception { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); name = reader.readLine(); age = Integer.parseInt(reader.readLine()); } } |
class Human { public Cat cat; public Dog dog; public void save(OutputStream outputStream) throws Exception { cat.save(outputStream); dog.save(outputStream); } public void load(InputStream inputStream) throws Exception { cat.load(inputStream); dog.load(inputStream); } } |
- Очень хорошее решение. Но что будет, если у человека нет кота, а есть только собака?
Где проверки на null?
- Сейчас исправлю:
Код |
---|
class Human { public Cat cat; public Dog dog; public void save(OutputStream outputStream) throws Exception { if (cat != null) cat.save(outputStream); if (dog != null) dog.save(outputStream); } public void load(InputStream inputStream) throws Exception { cat = new Cat(); cat.load(inputStream); dog = new Dog(); dog.load(inputStream); } } |
- Все равно не очень верно. У тебя две ошибки:
1) Если у человека нет ни кота, ни собаки, они все равно будут созданы, при вызове метода load
2) Если мы сохраним только собаку, то ее данные будут прочитаны котом при загрузке.
- А что же делать?
- Мы не можем пропускать запись переменных, иначе это вызовет сбой при чтении. Но нужно сделать так, чтобы переменные, чье значение было null при сохранении и после загрузки, получали null. Вот мой вариант:
Код |
---|
class Human { public Cat cat; public Dog dog; public void save(OutputStream outputStream) throws Exception { PrintWriter writer = new PrintWriter(outputStream); String isCatPresent = cat != null ? "yes" : "no"; writer.print(isCatPresent); if (cat!=null) cat.save(outputStream); String isDogPresent = dog != null ? "yes" : "no"; writer.print(isDogPresent); if (dog != null) dog.save(outputStream); } public void load(InputStream inputStream) throws Exception { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); String isCatPresent = reader.readLine(); if (isCatPresent.equals("yes")) { cat = new Cat(); cat.load(inputStream); } String isDogPresent = reader.readLine(); if (isDogPresent.equals("yes")) { dog = new Dog(); dog.load(inputStream); } } } |
- Да, мне нравится такое решение.
- Да, что-то в нем есть.
- Привет, Амиго!
Задачи |
---|
1. Читаем и пишем в файл: Human Реализуйте логику записи в файл и чтения из файла для класса Human Поле name в классе Human не может быть пустым В файле your_file_name.tmp может быть несколько объектов Human Метод main реализован только для вас и не участвует в тестировании |
2. Читаем и пишем в файл: JavaRush Реализуйте логику записи в файл и чтения из файла для класса JavaRush В файле your_file_name.tmp может быть несколько объектов JavaRush Метод main реализован только для вас и не участвует в тестировании |
3. Знакомство с properties В методе fillInPropertiesMap считайте имя файла с консоли и заполните карту properties данными из файла. Про .properties почитать тут - http://ru.wikipedia.org/wiki/.properties Реализуйте логику записи в файл и чтения из файла для карты properties. |
4. Читаем и пишем в файл статики Реализуйте логику записи в файл и чтения из файла для класса ClassWithStatic Метод load должен инициализировать объект включая статические поля данными из файла Метод main реализован только для вас и не участвует в тестировании |
5. И еще раз о синхронизации Разберитесь почему не работает метод main() Реализуйте логику записи в файл и чтения из файла для класса Object Метод load должен инициализировать объект данными из файла Метод main реализован только для вас и не участвует в тестировании |
- Помнишь, мы сегодня разбирали сохранение объектов в файл и чтение из файла?
- Да, только мы сохраняли в поток вывода, а читали из потока ввода.
- Молодец, Амиго. Приятно слышать, что ты замечаешь такие мелочи. А ты бы смог дописать код, чтобы было сохранение в файл и чтение из файла?
- А что там писать?! Объявил FileInputStream и FileOutputStream и передавай их в методы save & load. Тут уже ничего не перепутаешь - все просто.
- Рада за тебя. Итак, новая тема – сериализация.
Сериализация – это практически то же самое, что мы с тобой только что делали, только гораздо круче и встроено прямо в Java-машину. Java-машина умеет сохранять и загружать свои объекты. Для этого ей даже не требуются методы save & load: все объекты хранятся внутри Java-машины, и она имеет к ним полный доступ.
Мы просто берем объект и сохраняем его в поток/читаем из потока:
Код |
---|
public static void main(String[] args) throws Exception { Cat cat = new Cat(); //save cat to file FileOutputStream fileOutput = new FileOutputStream("cat.dat"); ObjectOutputStream outputStream = new ObjectOutputStream(fileOutput); outputStream.writeObject(cat); fileOutput.close(); outputStream.close(); //load cat from file FileInputStream fiStream = new FileInputStream("cat.dat"); ObjectInputStream objectStream = new ObjectInputStream(fiStream); Object object = objectStream.readObject(); fiStream.close(); objectStream.close(); Cat newCat = (Cat)object; } |
- И все?
- Да. Там очень большой и сложный механизм сериализации, который поддерживает сохранение в поток и чтение из потока почти всех типов данных.
- Почти всех, это значит не всех?
- Да, дело в том, что не все объекты по своей сути можно сохранить. Некоторые объекты не хранят все свои данные в себе, а лишь ссылаются на другие объекты и/или источники данных. Например, консоль (System.in), поток ввода (InputStream), или что-нибудь еще.
Поэтому разработчики Java придумали специальный интерфейс-маркер – Serializable. Его называют маркером, т.к. он не содержит никаких данных и методов. Он используется только для того, чтобы «помечать» (маркировать) классы. Если мы считаем, что наш класс хранит в себе все свои данные, тогда мы можем пометить его этим маркером – написать implements Serializable.
Пример «кота» с поддержкой сериализации:
Код |
---|
class Cat implements Serializable { public String name; public int age; public int weight; } |
Когда мы пытаемся сериализовать (сохранить) какой-нибудь объект, Java-машина проверяет – поддерживает ли он сериализацию: реализует ли он интерфейс Serializable? Если да, то сохраняет объект, если нет – выкидывает исключение о невозможности сериализации.
Тут нужно понимать, что сериализуемый объект должен состоять тоже только из сериализуемых объектов.
- Ну, этого и следовало ожидать. Нельзя же сохранить целое, не сохранив какие-то его части.
- Именно так.
- А как же типы int, String, ArrayList?
- Они все поддерживают сериализацию, на этот счет разработчики Java специально позаботились. Тут проблем быть не должно.
Более того, при сериализации объекта сохраняется его тип. Теперь ты можешь в переменную класса с типом Object сохранить ссылку на объект Cat, и все это отлично сериализуется и десериализуется.
- Десериализуется?
- Десериализация – так называют процесс, обратный сериализации – чтение и восстановление объекта из потока/файла.
- Тогда вопросов больше нет.
- Привет, Амиго!
Задачи |
---|
1. Как сериализовать? Сделайте так, чтобы сериализация класса Human была возможной |
2. Как сериализовать JavaRush? Сделайте так, чтобы сериализация класса JavaRush была возможной |
3. Как сериализовать Singleton? Два десериализованных объекта singleton и singleton1 имеют разные ссылки в памяти, а должны иметь одинаковые. В класс Singleton добавьте один метод (погуглите), чтобы после десериализации ссылки на объекты были равны. Метод main не участвует в тестировании. |
4. Как сериализовать static? Сделайте так, чтобы сериализация класса ClassWithStatic была возможной |
5. Как сериализовать что-то свое? Сделайте так, чтобы сериализация класса Object была возможной |
- Привет, Амиго! Хотела тебе порассказывать одно маленькое дополнение к сериализации.
Допустим наш класс содержит ссылку на какой-нибудь InputStream, тогда его нельзя сериализовать, ведь так?
- Да. Ты же сама говорила, что потоки сериализовать нельзя. А сериализовать объект, у которого есть несериализуемые данные – тоже нельзя.
- Да. Именно так. Но что, если класс хранит данные, которые не играют значащей роли в его состоянии, но мешают считаться ему сериализуемым классом? Мало ли что класс может у себя хранить ненужного. Возможно, он может выбросить эти данные в любой момент или даже так и делает постоянно.
Для таких случаев разработчики Java придумали специальное слово – transient. Его можно написать перед переменной класса и она будет не учитываться при сериализации. Ее состояние не будет ни сохраняться, ни восстанавливаться. Как будто и нет ее вовсе. Как раз для таких ситуаций, как мы только что рассмотрели.
Помнишь кеширование и модификатор volatile? Нет правил без исключений.
Вот тебе один примерчик такого счастья:
Пример «кота» с невидимой для сериализации переменной - in:
Код |
---|
class Cat implements Serializable { public String name; public int age; public int weight; transient public InputStream in = System.in; } |
- Привет, Амиго! Хотел бы немного дополнить то, что рассказала тебе Элли.
Иногда бывает нужно управлять процессом сериализации. Вот одни из причин:
1) Объект не готов к сериализации: его нынешнее внутреннее состояние в процессе изменения.
2) Объект содержит несериализуемые объекты, но может перевести их в удобную для сериализации форму: сохранить в массив байт или что-нибудь еще.
3) Объект хочет десериализовать все свои данные, как одно целое и/или зашифровать их перед сериализацией.
Причин выполнить сериализацию в ручном режиме может быть множество. Но не хотелось бы лишаться всех тех преимуществ, которые дает стандартная сериализация. Ведь наш объект могут использовать другие объекты. И они тоже не смогут сериализоваться, если наш объект откажется от поддержки сериализации.
На этот случай тоже есть решение – интерфейс Externalizable. Спасибо дальновидным разработчикам Java. Достаточно заменить интерфейс Serializable на интерфейс Externalizable, и ваш класс сможет управлять процессом сериализации в ручном режиме.
Дело в том, что интерфейс Externalizable, в отличие от Serializable, содержит два метода, которые вызываются Java-машиной при сериализации объекта. Вот как это выглядит:
Код |
---|
class Cat implements Externalizable { public String name; public int age; public int weight; public void writeExternal(ObjectOutput out) { out.writeObject(name); out.writeInt(age); out.writeInt(weight); } public void readExternal(ObjectInput in) { name = (String) in.readObject(); age = in.readInt(); weight = in.readInt(); } } |
Ничего не напоминает?
- Ух ты! Именно так мы пробовали сохранять объекты до «изобретения» сериализации.
- Да, теперь все просто: если устраивает стандартная сериализация – просто наследуем наш класс от интерфейса Serializable. Если не устраивает – наследуем от Externalizable и пишем код для сохранения/загрузки объекта нашего класса.
- А класс, помеченный Externalizable, считается сериализуемым? Мы можем «безопасно» хранить на него ссылки в наших сериализуемых классах?
- Да. Если класс реализует интерфейс Serializable или Externalizable, он считается сериализуемым.
- Отличное решение. Мне нравится.
- Рад это слышать. Но это еще не всё… Спроси лучше профессора Ханса о всяких нюансах. Они тут точно есть. Он хотел тебе дать что-то почитать.
- Привет, Амиго!
Задачи |
---|
1. Externalizable для апартаментов Реализуйте интерфейс Externalizable для класса Apartment Подумайте, какие поля не нужно сериализовать. |
2. OutputToConsole Класс OutputToConsole должен сериализоваться с помощью интерфейса Externalizable. Подумайте, какие поля не нужно сериализовать. Исправьте ошибку. Сигнатуры методов менять нельзя. |
3. Externalizable Person Класс Person должен сериализоваться с помощью интерфейса Externalizable. Подумайте, какие поля не нужно сериализовать. Исправьте ошибку сериализации. Сигнатуры методов менять нельзя. |
4. Serializable Solution Сериализуйте класс Solution. Подумайте, какие поля не нужно сериализовать, пометить ненужные поля — transient. Объект всегда должен содержать актуальные на сегодняшний день данные. Метод main не участвует в тестировании. Написать код проверки самостоятельно в методе main: 1) создать файл, открыть поток на чтение (input stream) и на запись(output stream) 2) создать экземпляр класса Solution - savedObject 3) записать в поток на запись savedObject (убедитесь, что они там действительно есть) 4) создать другой экземпляр класса Solution с другим параметром 5) загрузить из потока на чтение объект - loadedObject 6) проверить, что savedObject.string равна loadedObject.string 7) обработать исключения |
5. Переопределение сериализации Сделайте так, чтобы после десериализации нить runner продолжила работать. Ключевые слова объекта runner менять нельзя. Hint/Подсказка: Конструктор не вызывается при сериализации, только инициализируются все поля. |
- А если ты хочешь расширить или углубить свои знания по сериализации. Или, не дай бог, что-нибудь не понял, то для тебя у меня есть замечательные лекции.
- Привет, Амиго! Устал? Ничего, тяжело в учении – легко в бою. Давай посмотрим хорошее видео.
- Привет, Амиго! Что-то ты расслабился! Вот твои задания. Если вдруг возникнут трудности, то обращайся к коллегам, они точно помогут.
Дополнительные задания для выполнения в Intellij Idea |
---|
1. Минимум изменений Используя минимум изменений кода сделайте так, чтобы сериализация класса C стала возможной. |
2. Десериализация На вход подается поток, в который записан сериализованный объект класса A либо класса B. Десериализуйте объект в методе getOriginalObject предварительно определив, какого именно типа там объект. Реализуйте интерфейс Serializable где необходимо. |
3. Найти ошибки Почему-то при сериализации/десериализации объекта класса B возникают ошибки. Найдите проблему и исправьте ее. Класс A не должен реализовывать интерфейсы Serializable и Externalizable. Сигнатура класса В не содержит ошибку :) Метод main не участвует в тестировании. |
4. Исправить ошибку После десериализации объекта класса Solution обнаружили, что данных в словаре [m] нет :( Исправить 1 ошибку. Метод main в тестировании не участвует. |
5. Сериализуйте Person Сериализуйте класс Person стандартным способом. При необходимости поставьте полям модификатор transient. |
6. Запрет сериализации Запретите сериализацию класса SubSolution используя NotSerializableException. Сигнатуры классов менять нельзя |
7. Переопределение сериализации в потоке Сериализация/десериализация Solution не работает. Исправьте ошибки не меняя сигнатуры методов и класса. Метод main не участвует в тестировании. Написать код проверки самостоятельно в методе main: 1) создать экземпляр класса Solution 2) записать в него данные - writeObject 3) сериализовать класс Solution - writeObject(ObjectOutputStream out) 4) десериализовать, получаем новый объект 5) записать в новый объект данные - writeObject 6) проверить, что в файле есть данные из п.2 и п.5 |
8. Правильный вывод Расставить обращение к методам суперкласса и модификаторы доступа так, чтобы вывод на экран был следующим: C class, method2 A class, method2 A class, method1 B class, method1 Количество изменений модификаторов доступа и вызовов методов должно быть минимально 1. Из одного метода можно вызвать только один метод суперкласса. 2. Из одного метода можно вызвать только один метод класса. 3. Можно менять модификаторы доступа к методам. |
9. Знакомство с графами Прочитать в дополнительных материалах о сериализации графов. Дан ориентированный плоский граф Solution, содержащий циклы и петли. Пример, ![]() Все данные должны сохранить порядок следования. |
- Ты уже всё сделал? Вот тебе ещё и бонусные задания повышенной сложности:
1. Алгоритмы-числа Задача: Число S состоит из M чисел, например, S=370 и M(количество цифр)=3 Реализовать логику метода getNumbers, который должен среди натуральных чисел меньше N (long) находить все числа, удовлетворяющие следующему критерию: число S равно сумме его цифр, возведенных в M степень getNumbers должен возвращать все такие числа в порядке возрастания Пример искомого числа: 370 = 3*3*3 + 7*7*7 + 0*0*0 8208 = 8*8*8*8 + 2*2*2*2 + 0*0*0*0 + 8*8*8*8 На выполнение дается 10 секунд и 50 МБ памяти. |
2. Алгоритмы-прямоугольники Задача: 1. Дан двумерный массив N*N, который содержит несколько прямоугольников. 2. Различные прямоугольники не соприкасаются и не накладываются. 3. Внутри прямоугольник весь заполнен 1. 4. В массиве: 4.1) a[i, j] = 1, если элемент (i, j) принадлежит какому-либо прямоугольнику 4.2) a[i, j] = 0, в противном случае 5. getRectangleCount должен возвращать количество прямоугольников. 6. Метод main не участвует в тестировании |
3. Кроссворд Задача: 1. Дан двумерный массив, который содержит буквы английского алфавита в нижнем регистре. 2. Метод detectAllWords должен найти все слова из words в массиве crossword. 3. Элемент(startX, startY) должен соответствовать первой букве слова, элемент(endX, endY) - последней. text - это само слово, располагается между начальным и конечным элементами 4. Все слова есть в массиве. 5. Слова могут быть расположены горизонтально, вертикально и по диагонали как в нормальном, так и в обратном порядке. 6. Метод main не участвует в тестировании |
4. Свой список Задача: Посмотреть, как реализован LinkedList. Элементы следуют так: 1->2->3->4 и так 4->3->2->1 По образу и подобию создать Solution. Элементы должны следовать так: 1->3->7->15 ->8... ->4->9 ->10 2->5->11 ->12 ->6->13 ->14Во внутренней реализации элементы должны добавляться по 2 на каждый уровень Метод getParent должен возвращать элемент, который на него ссылается. Например, 3 ссылается на 7 и на 8, т.е. getParent("8")=="3", а getParent("13")=="6" Строки могут быть любыми. При удалении элемента должна удаляться вся ветка. Например, list.remove("5") должен удалить "5", "11", "12" Итерироваться элементы должны в порядке добавления Доступ по индексу запрещен, воспользуйтесь при необходимости UnsupportedOperationException Должно быть наследование AbstractList<String>, List<String>, Cloneable, Serializable Метод main в тестировании не участвует |