Строка из byte[] с UTF-8 дает разные результаты на Android, чем на Windows JVM

Я пытаюсь преобразовать массив байтов в строку на Java со следующим кодом:

byte[] myArray = {25, -50, -86, 81, 47, 44, 97, -5, 69, -4, 87, -114, -47, 62, -113, -64, 58, -32, -121, -102, 53, -89, -122, 12, -2, -23, -127, 111, -100, 53, -87, -23, -44, -28, 4, -21, -42, 75, 87, -112, -38, 118, 54, 92, -116, 4, -118, 110, -87, 7, -13, 3, -72, -63, -69, 123, 92, 94, 56, 61, 120, -52, 98, -17, 5, 41, 101, -3, 121, 81, -90, 12, -35, -21, -24, 112, -94, 123, 62, 8, 27, 54, 107, -77, 64, 8, -102, -99, -1, 119, 127, 43, 12, -31, -1, 51, -15, 83, -4, -68, -30, 91, -104, 84, 18, -122, -120, 66, 116, -17, -101, -24, 105, -112, -116, -64, -108, 112, -35, 61, 66, 100, 5, -24, -26, -44, 81, -84}; // Bytes from Byte.MIN_VALUE to Byte.MAX_VALUE
String result = new String(myArray, StandardCharsets.UTF_8);

Проблема в том, что я получаю другой результат, если запускаю код в Windows (JVM 1.8.0_112), чем если запускаю его на своем устройстве Android (проверено на Android 5.1 и 6.0). Я тестирую массив байтов длиной 128, в Android я получаю строку длиной 120, а в Windows я получаю строку длиной 125. Я предполагаю, что это как-то связано с тем, что некоторые байты недействительны utf -8 символов, но все же странно, что я получаю разные результаты в зависимости от платформы.

Если я изменю кодировку на US-ASCII, я получу тот же результат на обеих платформах, как и ожидалось:

String result = new String(myArray, StandardCharsets.US_ASCII);

Изменить: извините за путаницу. Я не генерирую его наугад каждый раз. Я просто имею в виду, что байты не имеют значимого значения UTF-8. Это массив байтов, который я использую для тестирования:

System.out.println(Arrays.toString(myArray)): [25, -50, -86, 81, 47, 44, 97, -5, 69, -4, 87, -114, -47, 62, -113, -64, 58, -32, -121, -102, 53, -89, -122, 12, -2, -23, -127, 111, -100, 53, -87, -23, -44, -28, 4, -21, -42, 75, 87, -112, -38, 118, 54, 92, -116, 4, -118, 110, -87, 7, -13, 3, -72, -63, -69, 123, 92, 94, 56, 61, 120, -52, 98, -17, 5, 41, 101, -3, 121, 81, -90, 12, -35, -21, -24, 112, -94, 123, 62, 8, 27, 54, 107, -77, 64, 8, -102, -99, -1, 119, 127, 43, 12, -31, -1, 51, -15, 83, -4, -68, -30, 91, -104, 84, 18, -122, -120, 66, 116, -17, -101, -24, 105, -112, -116, -64, -108, 112, -35, 61, 66, 100, 5, -24, -26, -44, 81, -84]

Редактировать 2: Результат окон:

System.out.println(String(myArray, StandardCharsets.UTF_8)).length: 125
System.out.println(String(myArray, StandardCharsets.UTF_8)): ΪQ/,a�E�W��>��:���5����o�5������KW��v6\��n�����{\^8=x�b�)e�yQ����p�{6k����w+��3�S���[�T��Bt��i����p�=Bd���Q�
System.out.println(toUnicode(String(myArray, StandardCharsets.UTF_8))): \u0019\u03aa\u0051\u002f\u002c\u0061\ufffd\u0045\ufffd\u0057\ufffd\ufffd\u003e\ufffd\ufffd\u003a\ufffd\ufffd\ufffd\u0035\ufffd\ufffd\u000c\ufffd\ufffd\u006f\ufffd\u0035\ufffd\ufffd\ufffd\ufffd\u0004\ufffd\ufffd\u004b\u0057\ufffd\ufffd\u0076\u0036\u005c\ufffd\u0004\ufffd\u006e\ufffd\u0007\ufffd\u0003\ufffd\ufffd\ufffd\u007b\u005c\u005e\u0038\u003d\u0078\ufffd\u0062\ufffd\u0005\u0029\u0065\ufffd\u0079\u0051\ufffd\u000c\ufffd\ufffd\ufffd\u0070\ufffd\u007b\u003e\u0008\u001b\u0036\u006b\ufffd\u0040\u0008\ufffd\ufffd\ufffd\u0077\u007f\u002b\u000c\ufffd\ufffd\u0033\ufffd\u0053\ufffd\ufffd\ufffd\u005b\ufffd\u0054\u0012\ufffd\ufffd\u0042\u0074\ufffd\ufffd\u0069\ufffd\ufffd\ufffd\ufffd\u0070\ufffd\u003d\u0042\u0064\u0005\ufffd\ufffd\ufffd\u0051\ufffd

Результат андроида:

System.out.println(String(myArray, StandardCharsets.UTF_8)).length: 120
System.out.println(String(myArray, StandardCharsets.UTF_8)): ΪQ/,a�E�W��>��:ǚ5����o�5������KW��v6\��n���{{\^8=x�b�)e�yQ����p�{>6k�@���w+�
System.out.println(toUnicode(String(myArray, StandardCharsets.UTF_8))): \u0019\u03aa\u0051\u002f\u002c\u0061\ufffd\u0045\ufffd\u0057\ufffd\ufffd\u003e\ufffd\ufffd\u003a\u01da\u0035\ufffd\ufffd\u000c\ufffd\ufffd\u006f\ufffd\u0035\ufffd\ufffd\ufffd\ufffd\u0004\ufffd\ufffd\u004b\u0057\ufffd\ufffd\u0076\u0036\u005c\ufffd\u0004\ufffd\u006e\ufffd\u0007\ufffd\u0003\ufffd\u007b\u007b\u005c\u005e\u0038\u003d\u0078\ufffd\u0062\ufffd\u0005\u0029\u0065\ufffd\u0079\u0051\ufffd\u000c\ufffd\ufffd\ufffd\u0070\ufffd\u007b\u003e\u0008\u001b\u0036\u006b\ufffd\u0040\u0008\ufffd\ufffd\ufffd\u0077\u007f\u002b\u000c\ufffd\ufffd\u0033\ufffd\u0053\ufffd\ufffd\u005b\ufffd\u0054\u0012\ufffd\ufffd\u0042\u0074\ufffd\ufffd\u0069\ufffd\ufffd\u0014\u0070\ufffd\u003d\u0042\u0064\u0005\ufffd\ufffd\ufffd\u0051\ufffd

Редактировать 3: добавлены правильные строки UTF-16.

Редактировать 4: изменен код на рабочий пример


person jesm00    schedule 24.03.2017    source источник
comment
Вы используете один и тот же myArray на обеих платформах?   -  person Steve Smith    schedule 24.03.2017
comment
Да, я распечатал его на обеих платформах, и он абсолютно одинаков.   -  person jesm00    schedule 24.03.2017
comment
Посмотрим, правильно ли я понял: вы создаете случайный массив байтов и удивляетесь, почему результаты меняются при каждом запуске кода? Хммм..... Теперь, если бы вы имели в виду произвольный, но четко определенный/фиксированный массив байтов, используемый повторно, это было бы по-другому. Если это так, покажите нам массив байтов или, что еще лучше, дайте нам минимальный, полный и проверяемый пример   -  person Andreas    schedule 24.03.2017
comment
Может быть, одна из платформ не справляется с преобразованием некоторых значений байта в символ? Строка result должна выглядеть довольно странно.   -  person Steve Smith    schedule 24.03.2017
comment
@Andreas ОП также задается вопросом, почему вместо этого US-ASCII не дает разные результаты, поэтому, если это правда, возможно, он / она не генерирует случайные значения при каждом запуске.   -  person SantiBailors    schedule 24.03.2017
comment
Да, я неправильно сформулировал вопрос. Я думаю, теперь яснее, что я делаю   -  person jesm00    schedule 24.03.2017
comment
Charset.forName("UTF-8") каким-то образом стал StandardCharsets.UTF_16. Если ваши тесты настолько последовательны, вас не должны удивлять разные результаты…   -  person Holger    schedule 24.03.2017
comment
@Holger Кто-то попросил меня распечатать его в UTF-16 (вместе с массивом байтов, который я использую)   -  person jesm00    schedule 24.03.2017
comment
Строка Java представляет собой последовательность кодовых единиц UTF-16. Чтобы увидеть, что они из себя представляют однозначно, их можно записать в буквальном виде так: ΪQ/,a�E�…   -  person Tom Blodget    schedule 24.03.2017


Ответы (2)


Кажется, Android немного неаккуратно интерпретирует последовательности UTF-8. Соответствующая часть стандарта находится в D92 в главе 3, "Соответствие":

До стандарта Unicode версии 3.1 проблематичными последовательностями байтов «не самой короткой формы» в UTF-8 были те, в которых символы BMP могли быть представлены более чем одним способом. Эти последовательности имеют неверный формат, поскольку они не разрешены таблицей 3-7.

Ваш ввод имеет такую ​​последовательность «не самой короткой формы», например. -32, -121, -102 и -63, -69. В то время как Android интерпретирует каждую из этих последовательностей в один символ, Java правильно отклоняет эти последовательности и преобразует каждый байт искаженного ввода в один символ замены, что приводит к более длинной строке.

Вы можете продемонстрировать это на Java, используя парсер, интерпретирующий «модифицированную UTF-8»:

byte[][] samples = {
    { -32, -121, -102 },
    { -63, -69 }
};
for(byte[] array: samples) {
    System.out.println("source: "+Arrays.toString(array));
    String string = new String(array, StandardCharsets.UTF_8);
    System.out.println("strictly interpreted: "+string);
    System.out.println("length: "+string.length());
    ByteBuffer bb = ByteBuffer.allocate(array.length+2);
    bb.putShort((short)array.length).put(array);
    ByteArrayInputStream bis = new ByteArrayInputStream(bb.array());
    DataInputStream dis = new DataInputStream(bis);
    string = dis.readUTF();
    System.out.println("sloppily interpreted: "+string);
    System.out.println("length: "+string.length());
    byte[] actual = string.getBytes(StandardCharsets.UTF_8);
    System.out.println("correct sequence: "+Arrays.toString(actual));
    System.out.println();
}

который будет печатать

source: [-32, -121, -102]
strictly interpreted: ���
length: 3
sloppily interpreted: ǚ
length: 1
correct sequence: [-57, -102]

source: [-63, -69]
strictly interpreted: ��
length: 2
sloppily interpreted: {
length: 1
correct sequence: [123]

Он также показывает правильную «кратчайшую форму» последовательности символов.

person Holger    schedule 24.03.2017

Есть несколько отличий в выходных строках. Первый соответствует входной последовательности байтов 0xE0 0x87 0x9A. Правильным декодированием является исключение или замена символа(ов). (Должен ли это быть один, два или три символа замены? Я бы сказал, что два , что и дает декодер .NET на моей машине, но в любом случае я предпочитаю исключения в большинстве случаев.)

Ваши JVM на Andriod интерпретируют это как U+01DA. Это, вероятно, «правильно» математически в алгоритме, который выполняет недостаточные проверки недопустимых последовательностей.

person Tom Blodget    schedule 24.03.2017