ByteBuffer - compareTo 方法可能会发散

Posted

技术标签:

【中文标题】ByteBuffer - compareTo 方法可能会发散【英文标题】:ByteBuffer - compareTo method might diverge 【发布时间】:2014-03-07 15:55:41 【问题描述】:

根据文章here,ByteBuffers 上的 compareTo 方法在处理负数时可能无法正常工作

bytes in Java are signed, contrary to what one typically expects. What is easy to miss
though, is the fact that this affects ByteBuffer.compareTo() as well. The Java API
documentation for that method reads:

"Two byte buffers are compared by comparing their sequences of remaining elements 
lexicographically, without regard to the starting position of each sequence within its
corresponding buffer."

A quick reading might lead one to believe the result is what you would typically expect, 
but of course given the definition of a byte in Java, this is not the case. The result 
is that the order of byte buffers that contains values with the highest order bit set,   
will diverge from what you may be expecting.

我尝试了几个将负值放入缓冲区的示例,并与正值进行比较,结果总是可以的。文章是在谈论我们例如的情况吗?读取二进制数据,当整数-1 存储为100000...001 时会导致问题吗?

【问题讨论】:

我想说的是,您可能期望的是,包含负字节的字节缓冲区会比包含正字节的字节缓冲区“更小”(在 compareTo 的上下文中),但由于签名则相反,即 -1 > 1 因为11111111111111111111111111111111 > 00000000000000000000000000000001 这并不神秘。 ByteBuffer.compareTo() 的 Javadoc 指出,字节的比较就像通过 Byte.compareTo() 一样,而后者又指定了有符号的比较。 【参考方案1】:

文章声称ByteBuffer 包含设置了高位的字节(即,值在0x800xFF 范围内的字节)将被视为值与另一个缓冲区中的相应字节相比。换句话说,每个字节都被视为一个 8 位有符号整数。因此,您应该期望0x90 的字节值将小于0x30 的字节值。

至少,考虑到 Java 中 byte 值的标准行为,这是理论上的。在实践中,我希望字节将被比较为 8 位 unsigned 整数,因此 0x90 的字节将比较 大于 0x30 的字节.

这完全取决于如何解释“字典顺序”一词。例如,如果每个字节代表一个 8 位字符代码,那么它在逻辑上应该被视为无符号值(不管 Java 通常如何处理 byte 对象)。

所以归结为两个ByteBuffers的比较实际上是如何实现的。一种方法是将字节视为 Java 有符号整数,如下所示:

// Note: Code has be simplified for brevity
int compare(byte[] buf1, byte[] buf2)

    ...
    for (int i = 0;  i < buf1.length  &&  i < buf2.length;  i++)
    
        int cmp = buf1[i] - buf2[i];        // Signed arithmetic
        if (cmp != 0)
            return cmp;
    
    return (buf1.length - buf2.length);

另一种方法是将字节视为无符号整数,它使用稍微不同的算法进行比较:

        int cmp = (buf1[i] & 0xFF) - (buf2[i] & 0xFF);  // Unsigned arithmetic

正如我上面所说,我希望大多数实现都使用第二种方法,而没有给出“字典顺序”的任何具体定义。

【讨论】:

我投了反对票,因为它不处理 Java SE 的默认情况,这很容易测试。令人讨厌的是,Java API 还不够。如果还没有,我会提交错误报告。 @Maarten Bodewes,但答案正确地解释了引用文章中的意思 - 字典顺序如何根据天气字节被视为有符号或无符号。赞成。 @uvsmtid 根据之前的评论,我没有与您争论,而是 created another answer。我希望你明白我想说什么 - 我在发布这篇文章时没有时间写一个广泛的答案或更深入地研究,但我知道这是错误的。 谢谢,这是我的问题的关键,我正在对 EBCDIC 数字进行比较,这些数字表示为 F0、F1、F2 ..... 以及 java 在执行时默认将字节扩大为 int Byte.compare(x, y) 在我的情况下使用“return x-y”失败了,因为它应该是 0x000000F1 而不是 0xFFFFFFF1,对于我的解决方案,我必须使用 Byte.compareUnsigned(x, y) 这解决了我的问题。 【参考方案2】:

作为问题下方的mentioned in the comments,public int compareTo(ByteBuffer that)的描述:

... 通过按字典顺序比较它们的剩余元素序列来比较两个字节缓冲区,而不考虑每个序列在其相应缓冲区中的起始位置。就像调用Byte.compare(byte,byte) 一样比较字节元素对。 ...

导致public int compareTo(Byte anotherByte)

... 如果此Byte 等于参数Byte返回0;如果此Byte 在数值上小于参数Byte,则值小于0;如果此Byte 在数值上大于参数Byte有符号比较[强调我的]),则值大于0。 ...

因此,在 JavaDoc 描述中,区别非常明确。实际上,大多数高级 Java 开发人员会预料到这里会出现问题,并且会理解 compareTo 中发生的事情。所以从这个意义上说,没有混淆。

这会使this other answer 无效,因为Java 中没有实现差异的空间。


问题当然是在大多数其他编程语言中,字节被视为无符号。此外,字节数组的字典比较通常会假设字节是无符号的(较大的结构/数字不太可能由可能具有负值的无符号字节创建)。

因此,一个习惯于其他语言的毫无戒心的程序员或一个只是漫不经心地假设无符号字节(并直接将描述中的“比较字节”翻译为ByteBuffer.compareTo(ByteBuffer that))的程序员会大吃一惊。

第二部分是文章所暗示的。在 Java 中处理字节可以说是一个错误,因为您通常使用这些构造不是为了节省内存而是为了 IO,并且字节通常被视为无符号。

【讨论】:

以上是关于ByteBuffer - compareTo 方法可能会发散的主要内容,如果未能解决你的问题,请参考以下文章

Java中String的compareTo方法

C#中short的CompareTo()方法

ByteBuffer,啥是检测是不是需要翻转的干净方法

CompareTo方法详解

Java 中 compareTo方法问题

面试题: TreeSet里面放对象,如果同时放入了父类和子类的实例对象,那比较时使用的是父类的compareTo方法,还是使用的子类的compareTo方法,还是抛异常!