Estoy escribiendo un decodificador que recibirá una secuencia de búferes de bytes y decodificará el contenido en una sola String . Puede haber cualquier número de búferes de bytes, cada uno de los cuales contiene cualquier número de bytes. Los búferes no se dividen necesariamente en los límites de los caracteres, por lo que, dependiendo de la codificación, pueden contener caracteres parciales al principio o al final. Esto es lo que quiero poder hacer, donde StringByteStreamDecoder es la nueva clase que necesito escribir.
suspend fun decode(data: Flow<ByteBuffer>, charset: Charset): String { val decoder = StringByteStreamDecoder(charset) data.collect { bytes -> decoder.feed(bytes) } decoder.endOfInput() return decoder.toString() } El enfoque más simple es recopilar todos los búferes de bytes en una matriz de un solo byte. Rechacé este enfoque porque tiene una sobrecarga de memoria significativa. Requiere asignar espacio para el mensaje completo al menos dos veces: una para los bytes sin procesar y otra para los caracteres decodificados. Aquí está mi implementación simple, usando un ByteArrayOutputStream como un búfer de bytes en expansión.
class StringByteStreamDecoder(private val charset: Charset) { private val buffer = ByteArrayOutputStream() fun feed(data: ByteBuffer) { if (data.hasArray()) { buffer.write(data.array(), data.position() + data.arrayOffset(), data.remaining()) } else { val array = ByteArray(data.remaining()) data.get(array) buffer.write(array, 0, array.size) } } fun endOfInput() { buffer.flush() } override fun toString(): String { return buffer.toString(charset) } }Para evitar almacenar en búfer todo el flujo de bytes en la memoria, me gustaría decodificar los caracteres sobre la marcha. No es posible decodificar cada búfer de bytes directamente en datos de caracteres, ya que puede contener caracteres parciales al principio y al final. El decodificador de caracteres (hasta donde tengo entendido) no tiene la capacidad de almacenar en búfer caracteres parciales y solo consumirá caracteres completos. Entonces, para cada búfer de bytes entrante, mi enfoque es:
Una vez que se han recibido todos los datos, se pueden vaciar los bytes restantes en el búfer de bytes temporal. Esto resuelve el problema con caracteres parciales, siempre que el búfer de bytes temporal sea al menos tan grande como el carácter más ancho del conjunto de caracteres.
class StringByteStreamDecoder(charset: Charset, bufferSize: Int = 1024) { private val decoder = charset.newDecoder() private val tmpBytes = ByteBuffer.allocate(bufferSize) private val tmpChars = CharBuffer.allocate((tmpBytes.capacity() * decoder.maxCharsPerByte()).toInt() + 1) private val stringBuilder = StringBuilder() fun feed(data: ByteBuffer) { do { tmpBytes.put(data.nextSlice(maxSize = tmpBytes.remaining())) flushBytes() } while (data.hasRemaining()) } fun endOfInput() { flushBytes(endOfInput = true) } override fun toString(): String = stringBuilder.toString() private fun ByteBuffer.nextSlice(maxSize: Int): ByteBuffer { val size = minOf(maxSize, remaining()) val slice = slice(position(), size) position(position() + size) return slice } private fun flushBytes(endOfInput: Boolean = false) { tmpBytes.flip() decoder.decode(tmpBytes, tmpChars, endOfInput) tmpBytes.compact() flushChars() } private fun flushChars() { tmpChars.flip() stringBuilder.append(tmpChars) tmpChars.clear() } }Todavía no estoy completamente satisfecho con este enfoque, debido a los búferes temporales adicionales. Me gustaría poder hacer que el búfer de bytes temporal contenga un máximo de un carácter (parcial). Sin embargo, si hiciera eso, tendría que anteponerlo de alguna manera al siguiente fragmento de datos entrante. Eso significaría asignar un nuevo búfer de bytes para contener el carácter parcial almacenado en el búfer más los nuevos datos entrantes. Copiar todos los datos del búfer entrante al búfer concatenado no es más eficiente que simplemente usar un búfer temporal más grande en primer lugar.
Sin embargo, para cadenas pequeñas, el búfer de bytes temporal representa una sobrecarga significativa. Podría hacer que el búfer temporal fuera más pequeño, pero eso podría perjudicar el rendimiento al decodificar cadenas más grandes.
También sé que StringBuilder cambiará de tamaño dinámicamente según la entrada, y podría no ser la forma más eficiente de asignar espacio para el String resultante.
Creo que podría evitar parte de la asignación de memoria adicional si tuviera acceso a algo como el búfer de cadena descrito en esta respuesta . Eso me permitiría crear una vista en ventana concatenada de los búferes de bytes entrantes. El decodificador de caracteres podría entonces consumir la vista concatenada directamente, en lugar de necesitar el búfer temporal adicional. Sin embargo, no puedo encontrar nada en la biblioteca estándar que ofrezca ese tipo de funcionalidad.
¿Es posible resolver este problema sin asignar memoria adicional más allá de los datos entrantes y la propia String resultante? Si no, ¿cuál es la cantidad mínima de memoria adicional que se necesita y cuál es el enfoque que logrará ese mínimo?
Dudo que haya alguna posibilidad de crear una cadena en Java sin copiar los datos. Ya sea que lo creemos desde byte[] , desde char[] , concatenamos otras cadenas, usamos StringBuilder / StringBuffer - siempre tenemos que copiar los datos. Parece que asumiste erróneamente que StringBuilder alguna manera crea cadenas directamente. No, copia los datos en toString() . Potencialmente, substring() podría evitar la creación de copias de datos en algunas JVM, pero no sé si se implementa así en la práctica.
Lo más probable es que esto se deba al hecho de que se garantiza que las cadenas son inmutables, pero las fuentes de los datos suelen ser mutables, por lo que es necesario copiar los datos.
Si conoce el tamaño de los datos de antemano o sospecha de su tamaño, creo que lo más eficiente es asignar una matriz de bytes, escribir todo en ella y luego convertir. Así que tu intento inicial.
Si la memoria es realmente una gran preocupación para usted, puede buscar JVM que le den acceso a algunas cosas avanzadas y tal vez le permitan crear una cadena de byte[] / char[] sin copiar. Pero antes que nada, deberías replantearte si realmente deberías preocuparte por esto. O tal vez esto es solo una optimización prematura.