Неправильная двоичная сериализация HashMap<String, Double>

Я написал код для сериализации HashMap<String,Double> путем итерации записей и сериализации каждого из них вместо использования ObjectOutputStream.readObject(), Причина заключается только в эффективности: результирующий файл намного меньше и намного быстрее записывается и читается (например, 23 МБ за 0,6 секунды против 29 МБ за 9,9 секунды).

Вот что я сделал для сериализации:

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.bin"));
oos.writeInt(map.size()); // write size of the map
for (Map.Entry<String, Double> entry : map.entrySet()) { // iterate entries
    System.out.println("writing ("+ entry.getKey() +","+ entry.getValue() +")");
    byte[] bytes = entry.getKey().getBytes();
    oos.writeInt(bytes.length); // length of key string
    oos.write(bytes); // key string bytes
    oos.writeDouble(entry.getValue()); // value
}
oos.close();

Как видите, я получаю byte массив для каждого ключа String, сериализовать его длину, а затем сам массив. Вот что я сделал для десериализации:

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.bin"));
int size = ois.readInt(); // read size of the map
HashMap<String, Double> newMap = new HashMap<>(size);
for (int i = 0; i < size; i++) { // iterate entries
    int length = ois.readInt(); // length of key string
    byte[] bytes = new byte[length];
    ois.read(bytes); // key string bytes
    String key = new String(bytes);
    double value = ois.readDouble(); // value
    newMap.put(key, value);
    System.out.println("read ("+ key +","+ value +")");
}

Проблема в том, что в какой-то момент ключ неправильно сериализован. Я отлаживал до такой степени, что я мог видеть, что ois.read(bytes) читать 8 байтов вместо 16, как это и предполагалось, поэтому ключ String не был правильно сформирован и double значение было прочитано с использованием последних 8 байтов ключа, который еще не был прочитан. В конце концов, исключения везде.

Используя приведенные ниже примеры данных, результат будет примерно таким:

read (2010-00-056.html,12154.250518054876)
read (2010-00-        ,1.4007397428546247E-76)
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at ti.Test.main(Test.java:82)

Проблема может быть замечена в сериализованном файле (это следует прочитать 2010-00-008.html ):

два байта добавляются между String ключ. См . Ответ MxyL для получения дополнительной информации об этом. Итак, все сводится к следующему: почему эти два байта добавлены и почему readFully работает нормально?

Почему не String правильно (де) сериализовано? Это может быть какой-то отступ до фиксированного размера блока или что-то в этом роде? Есть ли лучший способ сериализации вручную String при поиске эффективности? Я ожидал какого-то writeString а также readString, но, похоже, в Java такого нет ObjectStream,

Я пытался использовать буферизованные потоки на случай, если там что-то не так, явно указав, сколько байтов нужно записать и прочитать, используя разные кодировки, но не повезло.

Вот некоторые примеры данных для воспроизведения проблемы:

HashMap<String, Double> map = new HashMap<String, Double>();
map.put("2010-00-027.html",21732.994621513037); map.put("2010-00-020.html",3466.5169348296736); map.put("2010-00-051.html",12528.648992702407); map.put("2010-00-062.html",3354.8950010256385);
map.put("2010-00-024.html",10295.095511718278); map.put("2010-00-052.html",5381.513344679818);  map.put("2010-00-007.html",16466.33813960735);  map.put("2010-00-017.html",9484.969198176652);
map.put("2010-00-054.html",15423.873112634772); map.put("2010-00-022.html",8123.842752870753);  map.put("2010-00-033.html",21238.496665104063); map.put("2010-00-028.html",7578.792651786424);
map.put("2010-00-048.html",3566.4118233046393); map.put("2010-00-040.html",2681.0799941861724); map.put("2010-00-049.html",14308.090890746222); map.put("2010-00-058.html",5911.342406606804);
map.put("2010-00-045.html",2284.118716145881);  map.put("2010-00-031.html",2859.565771680721);  map.put("2010-00-046.html",4555.187022907964);  map.put("2010-00-036.html",8479.709295569426);
map.put("2010-00-061.html",846.8292195815125);  map.put("2010-00-023.html",14108.644025417952); map.put("2010-00-041.html",22686.232732684934); map.put("2010-00-025.html",9513.539663409734);
map.put("2010-00-012.html",459.6427911376829);  map.put("2010-00-005.html",0.0);    map.put("2010-00-013.html",2646.403220496738);  map.put("2010-00-065.html",5808.86423609936);
map.put("2010-00-056.html",12154.250518054876); map.put("2010-00-008.html",10811.15198506469);  map.put("2010-00-042.html",9271.006516004005);  map.put("2010-00-000.html",4387.4162586468965);
map.put("2010-00-059.html",4456.211623469774);  map.put("2010-00-055.html",3534.7511584735325); map.put("2010-00-057.html",8745.640098512009);  map.put("2010-00-032.html",4993.295735075575);
map.put("2010-00-021.html",3852.5805998017922); map.put("2010-00-043.html",4108.020033536286);  map.put("2010-00-053.html",2.2446400279239946); map.put("2010-00-030.html",17853.541210836203);

4 ответа

Решение

ObjectOutputStream сначала записывает STREAM_MAGIC(0xaced), затем записывает STREAM_VERSION(5), затем записывает TC_BLOCKDATALONG (0x7A), затем размер блока (1024), а для последнего блока, если длина меньше 255, это wirte TC_BLOCKDATA (0x77) и размер блока (длина последний блок)

поэтому, когда ObjectOutputStream использует readFully, он сначала читает данные в буфер, который пропускает STREAM_MAGIC, STREAM_VERSION, затем для каждого блока читает размер блока, чтобы получить размер, а затем читает все данные размера в буфер.

ois.read(bytes); // key string bytes

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

Есть ли лучший способ сериализации вручную при поиске эффективности?

Существует пара writeUTF() и readUTF().

Вы должны отметить, что, вызывая getBytes(), вы вводите зависимость от платформы. Вы должны указать кодировку как здесь, так и при восстановлении String.

Здесь следует отметить две интересные вещи

Во-первых, если вы удалили последние 4 записи в данных образца, ошибка не возникает. То есть два байта ошибочно не добавляются. Weird.

Во-вторых, если вы откроете свой файл в шестнадцатеричном редакторе и прокрутите вниз до записи, где встречаются два дополнительных байта, вы увидите, что он начинается с 4-байтового целого числа, которое правильно равно 16 (имейте в виду, что это большой обратный порядок байт). Затем вы видите вашу строку с двумя дополнительными байтами, за которыми следует двойной, связанный с ней.

Теперь странно то, как Java читает эти байты. Во-первых, он читает длину строки, как вы указали. Затем он пытается прочитать 16 байтов... но здесь, похоже, не удалось прочитать 16 байтов, так как ваши операторы печати показывают

read (2010-00-,1.3980409401811577E-76))

Теперь поместите курсор прямо после этих двух странных байтов, и вы увидите это

IMG

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

Кроме того, когда я пытался скопировать эту строку из консоли моего IDE, он только вставил

read (2010-00-

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

img2

Итак, похоже, что Java удалось прочитать только 10 байтов и двигаться дальше, что объясняет строку и число после.

Так что, казалось бы, когда вы read и передать в буфер, он не заполняется полностью. Есть даже рекомендация от самой подсказки, которая говорит мне использовать readFully!

img4

Проведя небольшое тестирование, я пошел дальше и изменил

ois.read(bytes); // key string bytes

в

ois.readFully(bytes, 0, length); // key string bytes

И по какой-то причине это работает.

read (2010-00-013.html,2646.403220496738)
read (2010-00-005.html,0.0)
read (2010-00-056.html,12154.250518054876)
read (2010-00-008.html,10811.15198506469)
read (2010-00-042.html,9271.006516004005)
read (2010-00-000.html,4387.4162586468965)  // where it was failing before
read (2010-00-059.html,4456.211623469774)

проблема

Теперь, факт, что это действительно работало, является проблемой. ПОЧЕМУ это работает? Совершенно очевидно, что между вашей строкой есть два дополнительных байта (поэтому длина строки должна быть 18, а не 16). Это не так, как файл изменился или что-нибудь.

Действительно, когда я вручную отредактировал файл так, чтобы в нем было только три записи, и я указал, что их всего две, я получаю следующий вывод:

img3

read (2010-00-056.html,12154.250518054876)
read (2010-00-wd008.ht,1.2466701288348126E219)

Это то, что я ожидаю от строки с 18 байтами (ну, может быть, не так wd, Я ожидал w,), но вы указали, что их всего 16. Вы должны согласиться с тем, что использование readFully на самом деле работал, странно.

Так что есть несколько загадок

  1. Почему эти два дополнительных байта добавлены
  2. Почему они НЕ добавляются, когда вы удаляете последние 4 записи (или больше, если хотите)
  3. Почему используется readFully работа, все остальное постоянное?

К сожалению, этот ответ не отвечает на ваши вопросы, и сейчас я также довольно озадачен не только проблемами, которые вы подняли, но и поведением, которое я наблюдаю.

ObjectInputStream#read не гарантирует, что он прочитает число в buffer.length() байтов. Когда чтение происходит на границе текущего блока буфера чтения, он возвращает только количество байтов, оставшихся в буфере. Это должно быть написано так.

        int offset=0;
        while(offset<length) {
            int cnt=ois.read(bytes,offset, length-offset); // key string bytes
            offset+=cnt;
        }
Другие вопросы по тегам