这个 Java ByteBuffer 的行为有解释吗?

Posted

技术标签:

【中文标题】这个 Java ByteBuffer 的行为有解释吗?【英文标题】:Is there an explanation for the behavior of this Java ByteBuffer? 【发布时间】:2011-12-28 15:32:18 【问题描述】:

我需要将数值转换为字节数组。例如,要将 long 转换为字节数组,我有这个方法:

public static byte[] longToBytes(long l) 
  ByteBuffer buff = ByteBuffer.allocate(8);

  buff.order(ByteOrder.BIG_ENDIAN);

  buff.putLong(l);

  return buff.array();

这很简单——花很长时间,分配一个可以容纳它的数组,然后把它扔进去。不管l 的值是多少,我都会得到一个8 字节的数组,然后我可以按预期处理和使用它。就我而言,我正在创建自定义二进制格式,然后通过网络传输它。

当我使用值 773450364 调用此方法时,我得到一个数组 [0 0 0 0 46 25 -22 124] 返回。我有代码也可以将字节数组转换回它们的数值:

public static Long bytesToLong(byte[] aBytes, int start) 
  byte[] b = new byte[8];

  b[0] = aBytes[start + 0];
  b[1] = aBytes[start + 1];
  b[2] = aBytes[start + 2];
  b[3] = aBytes[start + 3];
  b[4] = aBytes[start + 4];
  b[5] = aBytes[start + 5];
  b[6] = aBytes[start + 6];
  b[7] = aBytes[start + 7];

  ByteBuffer buf = ByteBuffer.wrap(b);
 return buf.getLong();

当我将数组从另一个方法传回这个方法时,我得到 773450364,这是正确的。

现在,我通过 TCP 将此数组传输到另一个 Java 客户端。 java.io.InputStream.read() 方法的文档说它返回一个介于 0 和 255 之间的 int 值,除非到达流的末尾并返回 -1。但是,当我使用它填充字节数组时,我继续在接收端获取负值。我怀疑这与溢出有关(值 255 无法放入 Java 字节,所以当我将其放入字节数组时,它会溢出并变为负数)。

这让我想到了我的问题。负数的存在与我有关。现在,我正在开发应用程序的 Java 端,其中一个字节介于 -128 和 127 之间。另一个端点可能是 C、C++、Python、Java、C#……谁知道呢。我不确定某些字节数组中负值的存在将如何影响处理。 除了记录这种行为之外,我可以/应该做些什么来让自己和未来的开发人员在这个系统上工作更容易,尤其是在不是用 Java 编写的端点中?

【问题讨论】:

在调用getLong() 之前,您不应该在bytesToLong 方法中设置ByteBuffer 的字节序,使其与longToBytes 相同吗?与您的问题无关,只是想知道... @G_H 我应该调查一下并进行测试。实际上我自己并没有编写这些方法中的任何一个,并且测试用例......缺乏。感谢您指出这一点。 【参考方案1】:

Java 中的 byte 以 8 位 two's complement 格式表示。如果您有一个在 128 - 255 范围内的 int 并将其转换为 byte,那么它将变为具有负值(介于 -1 和 -128 之间)的 byte

读取一个字节后,必须检查它是否为-1,然后再将其转换为byte。该方法返回 int 而不是 byte 的原因是允许您在将其转换为 byte 之前检查流结束。

另一件事:为什么要在 bytesToLong 方法中复制 aBytes 数组?您可以大大简化该方法并保存不必要的副本:

public static Long bytesToLong(byte[] aBytes, int start) 
    return ByteBuffer.wrap(aBytes, start, 8).order(ByteOrder.BIG_ENDIAN).getLong();

【讨论】:

【参考方案2】:

您的发送和接收端点目前都是用 Java 实现的。可以想象,您在发送端使用OutputStream,在接收端使用InputStream。假设我们暂时可以信任底层的套接字实现细节,我们将认为通过套接字发送的任何字节到达其目的地时都是完全相同的。

那么,在将某些内容转储到 OutputStream 时,Java 级别实际上会发生什么?检查the JavaDoc for a method writing a byte array 时,我们看到所有这些都告诉我们字节正在通过流发送。那里没什么大不了的。但是,当您查看method taking an int as argument 的文档时,您会看到它详细说明了这个 int 是如何实际写出的:低阶 8 位作为一个字节通过流发送,而高阶 24 位(int在 Java 中具有 32 位表示)被简单地忽略。

到接收方。你有一个 InputStream。除非你使用one of the methods reading directly into a byte array,否则你会得到一个int。 Like the doc says,int 将是介于 0 和 255 之间的值,如果已到达流的末尾,则为 -1。这是重要的一点。一方面,我们希望单个字节的每个可能的位模式都可以从 InputStream 中读取。但是我们还必须有一些方法来检测何时读取不再可以返回有意义的值。这就是为什么该方法返回一个 int 而不是一个字节... -1 值是表示已到达流末尾的标志。如果你得到的不是 -1,唯一感兴趣的是那些低 8 位。由于这些可以是任何位模式,因此它们的十进制值范围为 -128 到 127(含)。当您直接读入字节数组而不是 int per int 时,将为您完成“修剪”。所以你会看到那些负值是有道理的。也就是说,它们只是负数,因为 Java 将字节表示为有符号十进制的方式。唯一感兴趣的是实际的位模式。对于所有你关心的,它可以代表 0 到 255 或 1000 到 1255 的值。

一次使用一个字节的典型 InputStream 读取循环如下所示:

InputStream ips = ...;
int read = 0;
while((read = ips.read()) != -1) 
    byte b = (byte)read;
    //b will now have a bit pattern ranging from 0x00 to 0xff in hex, or -128 to 127 in two-complement signed representation

运行时,以下(使用 Java 7 int 字面量)将显示:

public class Main 

    public static void main(String[] args) 

        final int i1 = Ox00_00_00_fe;
        final int i1 = Ox80_00_00_fe;

        final byte b1 = (byte)i1;
        final byte b2 = (byte)i2;

        System.out.println(i1);
        System.out.println(i2);

        System.out.println(b1);
        System.out.println(b2);

        final int what = Ox12_34_56_fe;
        final byte the_f = (byte)what;

        System.out.println(what);
        System.out.println(the_f);

    


从这里可以清楚地看出,从 int 转换为 byte 只会丢弃除最低有效 8 位之外的任何内容。因此 int 可以是正数或负数,它与字节值没有任何关系。只有最后 8 位。

长话短说:您从 InputStream 中获得了正确的字节值。这里真正的担心是,如果客户端可以用任何编程语言编写并在任何平台上运行,您需要在文档中清楚地说明接收到的字节的含义以及它们是否是long,这是如何编码的。明确编码是在 Java 中完成的,在特定的字节序中使用 ByteBufferputLong 方法。只有这样他们才能获得绝对确定如何解释这些字节的信息(结合 Java 规范)。

【讨论】:

【参考方案3】:

如果您的所有数据都是大端数据,您可以省去所有这些麻烦并使用 DataOutputStream。它有你需要的一切。

【讨论】:

不幸的是,这不全是大端。

以上是关于这个 Java ByteBuffer 的行为有解释吗?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 java.nio.channels.FileChannel 读取到 ByteBuffer 实现类似 BufferedReader#readLine() 的行为

Java:转换 ByteBuffer 多维数组

请解释这个 Java 数组参考参数传递行为

java.nio.ByteBuffer 以及flip,clear及rewind区别

Java经典23种设计模式之行为型模式

这个 ByteBuffer.allocate() 方法究竟创建了啥?