Получить размер строки w/encoding в байтах без преобразования в байт []

У меня есть ситуация, когда мне нужно знать размер пары String/encoding, в байтах, но не может использовать метод getBytes(), потому что 1) String очень большой и дублирует String в массиве byte[] будет использоваться большой объем памяти, но больше к точке 2) getBytes() выделяет массив byte[] на основе длины String * максимально возможных байтов на символ. Поэтому, если у меня есть String с символами 1.5B и кодировкой UTF-16, getBytes() будет пытаться выделить массив 3 ГБ и выйти из строя, поскольку массивы ограничены 2 ^ 32 - X байтами (X - спецификация Java).

Итак - есть ли способ вычислить размер байта пары String/encoding непосредственно из объекта String?

UPDATE:

Здесь рабочая реализация jtahlborn отвечает:

private class CountingOutputStream extends OutputStream {
    int total;

    @Override
    public void write(int i) {
        throw new RuntimeException("don't use");
    }
    @Override
    public void write(byte[] b) {
        total += b.length;
    }

    @Override public void write(byte[] b, int offset, int len) {
        total += len;
    }
}

Ответ 1

Простой, просто напишите его в фиктивный выходной поток:

class CountingOutputStream extends OutputStream {
  private int _total;

  @Override public void write(int b) {
    ++_total;
  }

  @Override public void write(byte[] b) {
    _total += b.length;
  }

  @Override public void write(byte[] b, int offset, int len) {
    _total += len;
  }

  public int getTotalSize(){
     _total;
  }
}

CountingOutputStream cos = new CountingOutputStream();
Writer writer = new OutputStreamWriter(cos, "my_encoding");
//writer.write(myString);

// UPDATE: OutputStreamWriter does a simple copy of the _entire_ input string, to avoid that use:
for(int i = 0; i < myString.length(); i+=8096) {
  int end = Math.min(myString.length(), i+8096);
  writer.write(myString, i, end - i);
}

writer.flush();

System.out.println("Total bytes: " + cos.getTotalSize());

это не только просто, но, вероятно, так же быстро, как и другие "сложные" ответы.

Ответ 2

То же самое с использованием библиотек apache-commons:

public static long stringLength(String string, Charset charset) {

    try (NullOutputStream nul = new NullOutputStream();
         CountingOutputStream count = new CountingOutputStream(nul)) {

        IOUtils.write(string, count, charset.name());
        count.flush();
        return count.getCount();
    } catch (IOException e) {
        throw new IllegalStateException("Unexpected I/O.", e);
    }
}

Ответ 3

Хорошо, это очень грубо. Я признаю это, но этот материал скрыт от JVM, поэтому нам нужно немного копать. И немного пота.

Во-первых, нам нужен фактический char [], который поддерживает строку, не создавая копию. Для этого нам нужно использовать отражение, чтобы перейти в поле "значение":

char[] chars = null;
for (Field field : String.class.getDeclaredFields()) {
    if ("value".equals(field.getName())) {
        field.setAccessible(true);
        chars = (char[]) field.get(string); // <--- got it!
        break;
    }
}

Затем вам нужно реализовать подкласс java.nio.ByteBuffer. Что-то вроде:

class MyByteBuffer extends ByteBuffer {
    int length;            
    // Your implementation here
};

Игнорировать все геттеры, реализовать все методы put, такие как put(byte) и putChar(char) и т.д. Внутри что-то вроде put(byte), увеличивайте длину на 1, внутри put(byte[]) увеличивайте длину по длине массива. Возьми? Все, что ставится, вы добавляете размер того, что нужно для длины. Но вы ничего не храните в своем ByteBuffer, вы просто подсчитываете и выбрасываете, поэтому места не требуется. Если вы остановите методы put, вы, вероятно, сможете выяснить, какие из них вам действительно нужны. putFloat(float), вероятно, не используется, например.

Теперь для грандиозного финала, соединяя все это:

MyByteBuffer bbuf = new MyByteBuffer();         // your "counting" buffer
CharBuffer cbuf = CharBuffer.wrap(chars);       // wrap your char array
Charset charset = Charset.forName("UTF-8");     // your charset goes here
CharsetEncoder encoder = charset.newEncoder();  // make a new encoder
encoder.encode(cbuf, bbuf, true);               // do it!
System.out.printf("Length: %d\n", bbuf.length); // pay me US$1,000,000

Ответ 4

Здесь, по-видимому, рабочая реализация:

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

public class TestUnicode {

    private final static int ENCODE_CHUNK = 100;

    public static long bytesRequiredToEncode(final String s,
            final Charset encoding) {
        long count = 0;
        for (int i = 0; i < s.length(); ) {
            int end = i + ENCODE_CHUNK;
            if (end >= s.length()) {
                end = s.length();
            } else if (Character.isHighSurrogate(s.charAt(end))) {
                end++;
            }
            count += encoding.encode(s.substring(i, end)).remaining() + 1;
            i = end;
        }
        return count;
    }

    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 100; i++) {
            sb.appendCodePoint(11614);
            sb.appendCodePoint(1061122);
            sb.appendCodePoint(2065);
            sb.appendCodePoint(1064124);
        }
        Charset cs = StandardCharsets.UTF_8;

        System.out.println(bytesRequiredToEncode(new String(sb), cs));
        System.out.println(new String(sb).getBytes(cs).length);
    }
}

Вывод:

1400
1400

На практике я бы увеличил ENCODE_CHUNK до 10MChars или около того.

Вероятно, немного менее эффективный, чем ответ brettw, но проще реализовать.