Эффективный способ вычисления длины байта символа, в зависимости от кодирования
Каков наиболее эффективный способ вычисления длины байта символа, учитывающего кодировку символов? Кодирование будет известно только во время выполнения. Например, в UTF-8 символы имеют длину байта переменной, поэтому каждый символ должен определяться индивидуально. До сих пор я пришел к следующему:
char c = getCharSomehow();
String encoding = getEncodingSomehow();
// ...
int length = new String(new char[] { c }).getBytes(encoding).length;
Но это неудобно и неэффективно в цикле, так как new String
нужно создавать каждый раз. Я не могу найти другие и более эффективные способы в Java API. Там String#valueOf(char)
, но, согласно его источнику, он в основном такой же, как и выше. Я предполагаю, что это можно сделать с помощью побитовых операций, таких как смещение битов, но это моя слабость, и я не уверен, как это сделать при учете здесь:)
Если вы сомневаетесь в необходимости этого, отметьте этот раздел.
Обновление: ответ от @Bkkbrad технически наиболее эффективен:
char c = getCharSomehow();
String encoding = getEncodingSomehow();
CharsetEncoder encoder = Charset.forName(encoding).newEncoder();
// ...
int length = encoder.encode(CharBuffer.wrap(new char[] { c })).limit();
Однако, как отметил @Stephen C, с этим возникли проблемы. Могут быть, например, комбинированные/суррогатные символы, которые также необходимо учитывать. Но это еще одна проблема, которая должна быть решена на шаге до этого шага.
Ответы
Ответ 1
Используйте CharsetEncoder и повторно используйте CharBuffer в качестве входа и ByteBuffer в качестве вывода.
В моей системе следующий код занимает 25 секунд для кодирования 100 000 одиночных символов:
Charset utf8 = Charset.forName("UTF-8");
char[] array = new char[1];
for (int reps = 0; reps < 10000; reps++) {
for (array[0] = 0; array[0] < 10000; array[0]++) {
int len = new String(array).getBytes(utf8).length;
}
}
Однако следующий код делает то же самое менее чем за 4 секунды:
Charset utf8 = Charset.forName("UTF-8");
CharsetEncoder encoder = utf8.newEncoder();
char[] array = new char[1];
CharBuffer input = CharBuffer.wrap(array);
ByteBuffer output = ByteBuffer.allocate(10);
for (int reps = 0; reps < 10000; reps++) {
for (array[0] = 0; array[0] < 10000; array[0]++) {
output.clear();
input.clear();
encoder.encode(input, output, false);
int len = output.position();
}
}
Изменить: почему ненавистникам нужно ненавидеть?
Здесь решение, которое читает из CharBuffer и отслеживает суррогатные пары:
Charset utf8 = Charset.forName("UTF-8");
CharsetEncoder encoder = utf8.newEncoder();
CharBuffer input = //allocate in some way, or pass as parameter
ByteBuffer output = ByteBuffer.allocate(10);
int limit = input.limit();
while(input.position() < limit) {
output.clear();
input.mark();
input.limit(Math.max(input.position() + 2, input.capacity()));
if (Character.isHighSurrogate(input.get()) && !Character.isLowSurrogate(input.get())) {
//Malformed surrogate pair; do something!
}
input.limit(input.position());
input.reset();
encoder.encode(input, output, false);
int encodedLen = output.position();
}
Ответ 2
Возможно, что схема кодирования может кодировать заданный символ как переменное количество байтов, в зависимости от того, что происходит до и после него в последовательности символов. Следовательно, длина байта, которую вы получаете от кодирования одного символа String, не является полным ответом.
(Например, теоретически вы можете получить символы baudot/teletype, закодированные как 4 символа каждые 3 байта, или теоретически можно рассматривать UTF-16 + потоковый компрессор в качестве схемы кодирования. Да, все это немного неправдоподобно, но...)
Ответ 3
Если вы можете гарантировать, что вход хорошо сформирован UTF-8, то нет причин для поиска кодовых точек вообще. Одной из сильных сторон UTF-8 является то, что вы можете обнаружить начало кодовой точки из любой позиции в строке. Просто выполните поиск назад, пока не найдете байта таким образом, чтобы (b и 0xc0)!= 0x80, и вы нашли другого символа. Поскольку кодированная кодовая точка UTF-8 всегда равна 6 байтам, вы можете скопировать промежуточные байты в буфер фиксированной длины.
Изменить: я забыл упомянуть, даже если вы не идете с этой стратегией, недостаточно использовать Java "char" для хранения произвольных кодовых точек, поскольку значения кодовой точки могут превышать 0xffff. Вам нужно сохранить кодовые точки в "int".
Ответ 4
Попробуйте Charset.forName("UTF-8").encode("string").limit();
Может быть немного более эффективным, а может и нет.