Java:将字符串转换为ByteBuffer和从ByteBuffer转换回字符串以及相关问题

96

我正在使用Java NIO进行套接字连接,并且我的协议是基于文本的,因此在将它们写入SocketChannel之前,我需要能够将字符串转换为ByteBuffer,并将传入的ByteBuffer转换回字符串。目前,我正在使用以下代码:

public static Charset charset = Charset.forName("UTF-8");
public static CharsetEncoder encoder = charset.newEncoder();
public static CharsetDecoder decoder = charset.newDecoder();

public static ByteBuffer str_to_bb(String msg){
  try{
    return encoder.encode(CharBuffer.wrap(msg));
  }catch(Exception e){e.printStackTrace();}
  return null;
}

public static String bb_to_str(ByteBuffer buffer){
  String data = "";
  try{
    int old_position = buffer.position();
    data = decoder.decode(buffer).toString();
    // reset buffer's position to its original so it is not altered:
    buffer.position(old_position);  
  }catch (Exception e){
    e.printStackTrace();
    return "";
  }
  return data;
}

这个方法大多数情况下都有效,但我想知道这是否是进行双向转换的首选(或最简单)方法,还是是否有其他尝试的方式。偶尔会在调用encode()decode()时,似乎随机地抛出一个java.lang.IllegalStateException: Current state = FLUSHED, new state = CODING_END或类似的异常,即使每次进行转换时我都使用一个新的ByteBuffer对象。我需要同步这些方法吗?有更好的方法在字符串和字节缓冲区之间进行转换吗?谢谢!


看到异常的完整堆栈跟踪会有所帮助。 - Michael Borgwardt
3个回答

55

请查看CharsetEncoderCharsetDecoder的API描述。为避免问题,请按照一定的方法调用顺序进行操作。例如,对于CharsetEncoder

  1. 通过reset方法重置编码器,除非它以前没有使用过;
  2. 调用encode方法零次或多次,只要可能有更多输入可用,传递false作为endOfInput参数,并在每次调用之间填充输入缓冲区并清空输出缓冲区;
  3. 最后再调用一次encode方法,将endOfInput参数设置为true; 然后
  4. 调用flush方法,以便编码器可以将任何内部状态刷新到输出缓冲区。

顺便说一下,这是我在NIO中使用的相同方法,尽管我的一些同事正在将每个char直接转换为byte,因为他们只使用ASCII,我可以想象这可能更快。


3
非常感谢,这非常有帮助!我发现我的转换函数被多个线程同时调用,尽管我没有设计它允许那样做。为了避免并发问题或不必要地在这些对象上同步,我通过调用charset.newEncoder().encode()和charset.newDecoder().decode()来确保每次都使用新的编码器/解码器。我还运行了一些测试,并没有发现每次使用newEncoder()/newDecoder()会有明显的性能差异! - DivideByHero
3
没问题。您可以通过使用ThreadLocal并根据需要惰性地为每个线程创建专用的编码器/解码器来避免每次创建新的编码器/解码器,同时仍然保持线程安全(这就是我所做的)。 - Adamski
1
这个能行吗?new String(bb.array(), 0, bb.array().length, "UTF-8") - bentech

51

除非情况已经发生了改变,否则你最好选择

public static ByteBuffer str_to_bb(String msg, Charset charset){
    return ByteBuffer.wrap(msg.getBytes(charset));
}

public static String bb_to_str(ByteBuffer buffer, Charset charset){
    byte[] bytes;
    if(buffer.hasArray()) {
        bytes = buffer.array();
    } else {
        bytes = new byte[buffer.remaining()];
        buffer.get(bytes);
    }
    return new String(bytes, charset);
}

通常情况下,buffer.hasArray()的结果取决于您的用例,可能始终为true或始终为false。实际上,除非您真正希望它在任何情况下都能正常工作,否则安全起见,可以优化掉您不需要的分支。


14

Adamski提供的答案很好,并描述了使用一般编码方法(其中一个输入是字节缓冲区)进行编码操作时的步骤。

然而,本讨论中涉及的方法是encode的变体 - encode(CharBuffer in)。这是一个方便方法,实现了整个编码操作。(请参见P.S.中的Java文档引用)

根据文档,因此不应在已经进行编码操作的情况下调用此方法(这就是ZenBlender代码中发生的事情-在多线程环境中使用静态编码器/解码器)。

个人而言,我喜欢使用方便方法(而不是更通用的编码/解码方法),因为它们通过在封面下执行所有步骤来减轻负担。

ZenBlender和Adamski已经在他们的评论中建议了多种安全执行此操作的方法。在此列出它们:

  • 每次需要时创建新的编码器/解码器对象(效率不高,可能会导致大量对象)。要么
  • 使用ThreadLocal避免为每个操作创建新的编码器/解码器。要么
  • 同步整个编码/解码操作(除非牺牲一些并发性对程序无所谓)

P.S.

Java文档引用:

  1. 编码(方便)方法:http://docs.oracle.com/javase/6/docs/api/java/nio/charset/CharsetEncoder.html#encode%28java.nio.CharBuffer%29
  2. 通用编码方法:http://docs.oracle.com/javase/6/docs/api/java/nio/charset/CharsetEncoder.html#encode%28java.nio.CharBuffer,%20java.nio.ByteBuffer,%20boolean%29
  3. 该链接提供了Java NIO中CharsetEncoder类的encode()方法,可将字符序列编码为字节序列。方法参数包括CharBuffer(包含输入字符序列)、ByteBuffer(接收输出字节序列)和一个布尔值,指示是否强制进行编码。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接