Почему Java String.length несовместим на платформах с символами Юникода? - PullRequest
8 голосов
/ 21 мая 2019

Согласно документации Java для String.length :

public int length ()

Возвращает длину этой строки.

Длина равна количеству единиц кода Unicode в строке.

Указано:

длина в интерфейсе CharSequence

Возвращает:

длина последовательности символов, представленных этим объектом.

Но тогда я не понимаю, почему следующая программа HelloUnicode.java выдает разные результаты на разных платформах. Насколько я понимаю, количество единиц кода Unicode должно быть одинаковым, поскольку Java предположительно всегда представляет строки в UTF-16 :

public class HelloWorld {

    public static void main(String[] args) {
        String myString = "I have a ? in my string";
        System.out.println("String: " + myString);
        System.out.println("Bytes: " + bytesToHex(myString.getBytes()));
        System.out.println("String Length: " + myString.length());
        System.out.println("Byte Length: " + myString.getBytes().length);
        System.out.println("Substring 9 - 13: " + myString.substring(9, 13));
        System.out.println("Substring Bytes: " + bytesToHex(myString.substring(9, 13).getBytes()));
    }

    // Code from https://stackoverflow.com/a/9855338/4019986
    private final static char[] hexArray = "0123456789ABCDEF".toCharArray();
    public static String bytesToHex(byte[] bytes) {
        char[] hexChars = new char[bytes.length * 2];
        for ( int j = 0; j < bytes.length; j++ ) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = hexArray[v >>> 4];
            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
        }
        return new String(hexChars);
    }

}

Вывод этой программы на моем компьютере с Windows:

String: I have a ? in my string
Bytes: 492068617665206120F09F998220696E206D7920737472696E67
String Length: 26
Byte Length: 26
Substring 9 - 13: ?
Substring Bytes: F09F9982

Вывод на моем компьютере CentOS 7:

String: I have a ? in my string
Bytes: 492068617665206120F09F998220696E206D7920737472696E67
String Length: 24
Byte Length: 26
Substring 9 - 13: ? i
Substring Bytes: F09F99822069

Я запускал оба с Java 1.8. Одинаковая длина байта, другая длина строки. Почему?

UPDATE

Заменив "?" в строке на "\ uD83D \ uDE42", я получу следующие результаты:

Windows

String: I have a ? in my string
Bytes: 4920686176652061203F20696E206D7920737472696E67
String Length: 24
Byte Length: 23
Substring 9 - 13: ? i
Substring Bytes: 3F2069

CentOS:

String: I have a ? in my string
Bytes: 492068617665206120F09F998220696E206D7920737472696E67
String Length: 24
Byte Length: 26
Substring 9 - 13: ? i
Substring Bytes: F09F99822069

Почему «\ uD83D \ uDE42» в конечном итоге кодируется как 0x3F на машине с Windows, мне не понятно ...

Версии Java:

Windows

java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)

CentOS:

openjdk version "1.8.0_201"
OpenJDK Runtime Environment (build 1.8.0_201-b09)
OpenJDK 64-Bit Server VM (build 25.201-b09, mixed mode)

Обновление 2

Используя .getBytes("utf-8"), с "?", встроенным в строковый литерал, вот выходные данные.

Windows

String: I have a ? in my string
Bytes: 492068617665206120C3B0C5B8E284A2E2809A20696E206D7920737472696E67
String Length: 26
Byte Length: 32
Substring 9 - 13: ?
Substring Bytes: C3B0C5B8E284A2E2809A

CentOS:

String: I have a ? in my string
Bytes: 492068617665206120F09F998220696E206D7920737472696E67
String Length: 24
Byte Length: 26
Substring 9 - 13: ? i
Substring Bytes: F09F99822069

Так что да, похоже, разница в кодировке системы. Но тогда это означает, что строковые литералы кодируются по-разному на разных платформах? Похоже, это может быть проблематично в определенных ситуациях.

Также ... откуда берется последовательность байтов C3B0C5B8E284A2E2809A для представления смайлика в Windows? Это не имеет смысла для меня.

Для полноты, используя .getBytes("utf-16"), с "?", встроенным в строковый литерал, вот выходные данные.

Windows

String: I have a ? in my string
Bytes: FEFF00490020006800610076006500200061002000F001782122201A00200069006E0020006D007900200073007400720069006E0067
String Length: 26
Byte Length: 54
Substring 9 - 13: ?
Substring Bytes: FEFF00F001782122201A

CentOS:

String: I have a ? in my string
Bytes: FEFF004900200068006100760065002000610020D83DDE4200200069006E0020006D007900200073007400720069006E0067
String Length: 24
Byte Length: 50
Substring 9 - 13: ? i
Substring Bytes: FEFFD83DDE4200200069

Ответы [ 2 ]

4 голосов
/ 21 мая 2019

Вы должны быть осторожны при указании кодировок:

  • когда вы компилируете файл Java, он использует некоторую кодировку для исходного файла. Я предполагаю, что это уже сломало ваш оригинальный строковый литерал при компиляции. Это можно исправить с помощью escape-последовательности.
  • после использования escape-последовательности значение String.length одинаково. Байты внутри строки также одинаковы, но то, что вы печатаете, не показывает этого.
  • напечатанные байты отличаются, потому что вы назвали getBytes(), и это снова использует кодировку среды или платформы. Так что это тоже было сломано (замена не кодируемых смайликов на вопросительный знак). Вам нужно позвонить getBytes("UTF-8"), чтобы быть независимым от платформы.

Итак, чтобы ответить на конкретные поставленные вопросы:

Одинаковая длина байта, другая длина строки. Почему?

Поскольку строковый литерал кодируется компилятором java, и компилятор java часто использует разные кодировки в разных системах по умолчанию. Это может привести к разному количеству символьных единиц на символ Unicode, что приведет к разной длине строки. Передача параметра командной строки -encoding с одним и тем же параметром на разных платформах приведет к их последовательному кодированию.

Почему «\ uD83D \ uDE42» в конечном итоге кодируется как 0x3F на машине с Windows, мне не понятно ...

Он не закодирован как 0x3F в строке. 0x3f - знак вопроса. Java добавляет это, когда запрашивается вывод недопустимых символов с помощью System.out.println или getBytes, что имело место, когда вы кодировали литеральные представления UTF-16 в строке с другой кодировкой, а затем пытались распечатать его на консоли и getBytes от него.

Но тогда это означает, что строковые литералы кодируются по-разному на разных платформах?

По умолчанию да.

Также ... откуда берется последовательность байтов C3B0C5B8E284A2E2809A для представления смайлика в Windows?

Это довольно запутанно. Символ «?» (кодовая точка Unicode U + 1F642) сохраняется в исходном файле Java с кодировкой UTF-8 с использованием последовательности байтов F0 9F 99 82. Компилятор Java затем считывает исходный файл с использованием кодировки по умолчанию платформы, Cp1252 ( Windows-1252), поэтому он обрабатывает эти байты UTF-8, как если бы они были символами Cp1252, создавая строку из 4 символов, переводя каждый байт из Cp1252 в Unicode, что приводит к U + 00F0 U + 0178 U + 2122 U + 201A. Затем вызов getBytes("utf-8") преобразует эту 4-символьную строку в байты, кодируя их как utf-8. Поскольку каждый символ строки больше, чем шестнадцатеричный 7F, каждый символ преобразуется в 2 или более байтов UTF-8; следовательно, полученная строка будет такой длинной. Значение этой строки не имеет значения; это просто результат использования неправильной кодировки.

1 голос
/ 21 мая 2019

Вы не учли, что getBytes () возвращает байты в кодировке платформы по умолчанию. Это отличается от Windows и CentOS.

См. Также Как найти кодировку / кодировку по умолчанию в Java? и документацию API по String.getBytes () .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...