Хранение данных в бинарном файле

от
Java

На повестке дня у нас тема хранение данных в бинарных файлах.
У многих начинающих Java-программистов, и не только Java, возникает такой вопрос: как же нам сохранить данные быстро, легко, да и еще зашифровать? Начинается поиск по сети, возникает куча вопросов, появляется куча костылей с сложным построчным парсингом. Да и еще некоторые мастера начинают прибегать к регулярным выражениям. И что в итоге? Тормоза, непонятные ошибки, проблемы короче. Непонятные ошибки — в основном ошибки логики, забыл что на Windows вместо просто переноса\n комбинация из \n\r, функция разбиения начинает отсчет с нуля, а не единицы, да и на MacOS чего-то другое поведение. Мы от всего этого уйдем и напишем примеры сохранения карты в одном файле, хранение пар Имя=Значение и просто придумаем какой-нибудь формат хранения графики с сжатием.

Внимание!!! Я не буду делать примеров приложений, все чисто на теории и моих прошлых работах, которые мне приходилось делать. И описания методов стандартных классов не будут даны, смотри в документации. Особенно умные еще проявят смекалку и дадут ответы на вопросы)

Начнем с того, что файл — этот набор байт (так Pro100%CoolКодеры, попрошу здесь ваши мега-огурментации оставить при себе). Ну байт, это число которое может быть от нуля и до 256. Да вы знаете что все что вы сейчас видите — это набор байт в файлах, который обработали ваши программы и компьютер! Все байты в файл пишутся подряд, без каких либо разделителей и специальных вставок, просто как ты на доске, в тетраде пишешь цифры, так и в файл пишешь.

Аналогии к хранению данных в файле.
Как мы записываем распорядок дня? Как мы записываем план работы нашей программы/функции? Вы поняли к чему я веду? А если и не поняли, то я напишу. Всегда при сохранении чего либо в файле мы должны записать изначально, что нам надо сделать с этими данными, сколько их, размер. И, это важно, сначала же их надо как-то записать. Далее у нас статья пойдет по ветке описания работы на конкретном примере (по другому никак, понять никак).

Нам надо сохранить массив байт (а может и int или других значений) в файл. Характеристиками нашего массива являются количество элементов и тип. Для хранения типа мы создадим несколько констант, чтоб потом при чтении нам легко было понять что мы храним в файле. Код:
  1. public static final byte
  2.     TYPE_BYTE = 0,
  3.     TYPE_INT = 1,
  4.     TYPE_STRING = 2;
  5.  
  6. public void writeByteData(byte[] data, OutputStream os) throws IOException {
  7. // чтоб понятно было
  8.     int dataLength = data.length;
  9.     byte currentType = TYPE_BYTE;
  10.  
  11.     DataOutputStream dos = new DataOutputStream(os); // нужен чтоб писать int, String и другие типы
  12. // а вот собственно запись, понятно?)
  13.     dos.write(currentType);
  14.     dos.writeInt(dataLength);
  15.     dos.write(data, 0, dataLength);
  16. }
  17.  
  18. //Далее все по аналогии, только запись будет чуть другими функциями записываться
  19. public void writeIntData(int[] data, OutputStream os) throws IOException {
  20.     int dataLength = data.length;
  21.     byte currentType = TYPE_INT;
  22.  
  23.     DataOutputStream dos = new DataOutputStream(os);
  24.     dos.write(currentType);
  25.     dos.writeInt(dataLength);
  26.     for (int i = 0; i < dataLength; i++) {
  27.          dos.writeInt(data[i]);
  28.     }
  29. }
  30.  
  31. public void writeStringData(String[] data, OutputStream os) throws IOException {
  32.     int dataLength = data.length;
  33.     byte currentType = TYPE_STRING;
  34.  
  35.     DataOutputStream dos = new DataOutputStream(os);
  36.     dos.write(currentType);
  37.     dos.writeInt(dataLength);
  38.     for (int i = 0; i < dataLength; i++) {
  39.          dos.writeUTF(data[i]);
  40.     }
  41. }

С записью закончили, теперь чтение. Здесь я сделаю очень и очень просто, просто три метода
  1. // добавим массив строк с названиями типов. Для облегчения улавливания наших ошибок
  2. // метка №1 к  вопросу
  3. String[] typeNames = new String[] {"Байт", "Целое", "Строка" };
  4.  
  5. public byte[] readByteData(InputStream is) throws IOException, Exception{
  6.     DataInputStream dis = new DataInputStream(dis);
  7.  
  8.     byte currentType = dis.read();
  9.     if (curreanType != TYPE_BYTE) {
  10.         throw new Exception("Ошибочка! Мы читаем не тот тип! В файле: " + typeNames[currentType]);
  11.     }
  12.  
  13.     int dataLength = dis.readInt();
  14.     byte[] data = new Byte[dataLength];
  15.     if (dis.readFully(data) != dataLength) { // надо проверить, чтоб все было хорошо
  16.         throw new Excetion("Ошибочка! Что-то пошло не так...");
  17.     }
  18.     return data;
  19. }
  20.  
  21. public int[] readIntData(InputStream is) throws IOException, Exception{
  22.     DataInputStream dis = new DataInputStream(dis);
  23.  
  24.     byte currentType = dis.read();
  25.     if (curreanType != TYPE_INT) {
  26.         throw new Exception("Ошибочка! Мы читаем не тот тип! В файле: " + typeNames[currentType]);
  27.     }
  28.  
  29.     int dataLength = dis.readInt();
  30.     int[] data = new int[dataLength];
  31.     int i = 0;
  32.     for (; i < dataLength; i++) {
  33.         data[i] = dis.readInt();
  34.     }
  35.     if (i != dataLength) { // надо проверить, чтоб все было хорошо
  36.         throw new Excetion("Ошибочка! Что-то пошло не так...");
  37.     }
  38.     return data;
  39. }
  40.  
  41. public String[] readStringData(InputStream is) throws IOException, Exception{
  42.     DataInputStream dis = new DataInputStream(dis);
  43.  
  44.     byte currentType = dis.read();
  45.     if (curreanType != TYPE_STRING) {
  46.         throw new Exception("Ошибочка! Мы читаем не тот тип! В файле: " + typeNames[currentType]);
  47.     }
  48.  
  49.     int dataLength = dis.readInt();
  50.     String[] data = new String[dataLength];
  51.     int i = 0;
  52.     for (; i < dataLength; i++) {
  53.         data[i] = dis.readUTF();
  54.     }
  55.     if (i != dataLength) { // надо проверить, чтоб все было хорошо
  56.         throw new Excetion("Ошибочка! Что-то пошло не так...");
  57.     }
  58.     return data;
  59. }

Вот это примитивненько сделали. Можно вполне пользоваться и соблюдать определенную строгость с порядком чтения и записи — обязательно читаем все типы так как записали иначе будем ловить ошибки. Но это только начало наш дебют в шахматном понимании. Итак, переходим к миттельшпиле, будем улучшать процесс загрузки данных из файла. Например у нас имеются какие-то массивы с данными игрока для многопользовательской игры. Пусть это будет массив байт, не играет большой роли. Число игроков у нас каждый день растет и стает неудобно проходит по базе каждый раз для сохранения разных характеристик игрока. На этот случай мы можем ввести новый тип данных TYPE_PLAYER и с легкостью считывать его из файла способом выше. А что если мы хотим чтобы оно само читало из файла? Тогда сделаем следующим образом:
  1. public void loadFile(InputStream is) {
  2.     DataInputStream dis = new DataInputStream(is);
  3.     int type = (int)dis.readByte(); // загружаем тип
  4. // метка №2
  5.     switch(type) {
  6.         case TYPE_PLAYER:
  7.             byte[] data = new byte[dis.readInt()];
  8.             dis.readFully(data); // загружаем массив игрока
  9.             players.add(new Player(data)); // а это для примера, players это вектор или хеш-таблица, Player класс игрока
  10.         break;
  11.         case TYPE_WORLD_CHUNK: // а это тип куска карты. Согласитесь, если у нас мир создается во время игры и растет во все стороны, то лучше бы было его загружать циклом
  12.             byte[] data = new byte[dis.readInt()];
  13.             dis.readFully(data);
  14.             worldChunks.add(new WorldChunk(data));
  15.         break;
  16.     }
  17. }

А теперь подключаем воображение и начинаем рисовать картиночки в головке как можно компактно хранить данные в файлах. И напоследок:
1) в начале в файл можно записывать заголовок, чтобы распознавать "ваш ли это файл";
2) можно прибежать к методам шифрования (криптографии) и защитить ваш файл от вскрытия злоумышленниками;
3) есть множество алгоритмов сжатия данных, используйте их, экономьте место на диске пользователя;
4) файл можно условно разделить на две области: основной заголовок где хранится общая информация о файле и тело файла, где хранятся ваши данные;
5) так же можете добавить в заголовок поле с хешем данных в вашем файле для проверки его целостности;
6) и самое главное опыты и фантазия.

Вопросы
1. Скажите, почему я вправе был сделать так typeNames[currentType] (смотри метка №1)? Да и чего-то там там не хватает (без "не хватает" оно будет работать).
2. Что произойдет, если мы подсунем не наш файл, а скажем песню? (все возможные ошибки которые могут встретится по коду).
3. Почему код на метке №2 не будет работать динамически?
4. Изначально я хотел написать больше примеров, но потом понял, как много мне придется писать вручную — теперь это ваше задание, по возможности могу помочь с отлавливанием ошибок.

Если уж так захочется узнать правильно ли Вы ответили, можете написать мне письмо на сайте с ответом.
+2   3   1
2205