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

4

我需要将数值转换成字节数组。例如,将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
@G_H 我应该研究一下并进行测试。实际上,这两种方法都不是我自己编写的,测试用例也比较缺乏。感谢你指出这一点。 - Thomas Owens
3个回答

6
在Java中,一个byte以8位的二进制补码格式表示。如果你有一个在128-255范围内的int并将其转换为一个byte,那么它将成为一个具有负值(介于-1和-128之间)的byte
读取字节后,在将其转换为byte之前,必须检查它是否为-1。方法返回一个int而不是byte的原因是允许您在将其转换为byte之前检查流的结尾。 还有一件事:为什么在bytesToLong方法中要复制aBytes数组?你可以简化该方法并节省不必要的复制:
public static Long bytesToLong(byte[] aBytes, int start) {
    return ByteBuffer.wrap(aBytes, start, 8).order(ByteOrder.BIG_ENDIAN).getLong();
}

1

你的发送和接收端点都是用Java实现的。可以想象,你在发送端使用了OutputStream,在接收端使用了InputStream。假设我们暂时可以信任底层套接字实现细节,那么我们认为通过套接字发送的任何字节都会准确地到达其目的地。

那么当将某些内容转储到OutputStream时,在Java级别上实际发生了什么呢?当查看写入字节数组的方法的JavaDoc时,我们只看到正在通过流发送字节。没有什么大不了的。但是当你检查以int作为参数的方法的文档时,你会看到它详细说明了如何实际写出这个int:低位8位作为一个字节发送到流中,而高位24位(int在Java中具有32位表示)则被简单地忽略。

回到接收端。你有一个InputStream。除非你使用直接读入字节数组的方法,否则会得到一个int。就像文档所述的那样,这个int要么是0到255之间(包括0和255)的值,要么是-1,表示已经到达流的末尾。这是重要的一点。一方面,我们希望从InputStream中可以读取单个字节的所有可能位模式。但是我们必须还有一些方法来检测何时读取不再返回有意义的值。这就是为什么该方法返回int而不是byte的原因…… -1值是表示已到达流的末尾的标志。如果你得到的是-1之外的任何东西,唯一感兴趣的是其中的低8位。由于它们可以是任何位模式,它们的十进制值将范围从-128到127(包括在内)。当你直接读入字节数组而不是逐个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整型字面量),将会有所启发:

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规范),绝对确定如何解释这些字节。


0
如果你的所有数据都是大端字节序,那么你可以省去所有这些麻烦,直接使用DataOutputStream。它拥有你所需要的一切。

不幸的是,这并不全是大端序。 - Thomas Owens

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