Эффективный способ вычисления длины байта символа, в зависимости от кодирования

Каков наиболее эффективный способ вычисления длины байта символа, учитывающего кодировку символов? Кодирование будет известно только во время выполнения. Например, в 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(); Может быть немного более эффективным, а может и нет.