Java Hashcode提供整数溢出

时间:2014-05-13 14:21:21

标签: java hash hashcode integer-overflow integer-hashing

背景资料:

在我的项目中,我正在将强化学习(RL)应用于Mario域。对于我的状态表示,我选择使用带有自定义对象的哈希表作为键。我的自定义对象是不可变的,并且已经覆盖了.equals()和.hashcode()(由IntelliJ IDE生成)。

这是生成的.hashcode(),我在评论中添加了可能的值作为额外信息:

@Override
public int hashCode() {
    int result = (stuck ? 1 : 0);                // 2 possible values: 0, 1
    result = 31 * result + (facing ? 1 : 0);     // 2 possible values: 0, 1 
    result = 31 * result + marioMode;            // 3 possible values: 0, 1, 2
    result = 31 * result + (onGround ? 1 : 0);   // 2 possible values: 0, 1 
    result = 31 * result + (canJump ? 1 : 0);    // 2 possible values: 0, 1 
    result = 31 * result + (wallNear ? 1 : 0);   // 2 possible values: 0, 1 
    result = 31 * result + nearestEnemyX;        // 33 possible values: - 16 to 16
    result = 31 * result + nearestEnemyY;        // 33 possible values: - 16 to 16

    return result;
}

问题:

此处的问题是上述代码中的结果可能超过Integer.MAX_VALUE。我在网上看过这不一定是个问题,但就我而言。这部分是由于使用的算法是Q-Learning(RL方法)并且取决于存储在哈希表内的正确Q值。基本上我在检索值时不会有冲突。在运行我的实验时,我发现结果并不好,我95%肯定问题在于从哈希表中检索Q值。 (如果需要,我可以扩展为什么我对此有所了解,但这需要一些与该问题无关的项目额外信息。)

问题:

有没有办法避免整数溢出,也许我在这里忽略了什么?或者是否有另一种方式(可能是另一种数据结构)可以合理地快速获得给定我的自定义键的值?

注:

在阅读了一些评论之后,我意识到我选择使用HashTable可能不是最好的,因为我想要导致冲突的唯一键。如果我仍然想使用HashTable,我可能需要一个正确的编码。

4 个答案:

答案 0 :(得分:8)

您需要专用的密钥字段来保证唯一性

.hashCode()并非专为您使用它而设计的

.hashCode()旨在为分组算法提供良好的一般结果,可以容忍轻微的冲突。它不是为提供唯一密钥而设计的。默认算法是时间和空间以及轻微碰撞的折衷,它不应该保证唯一性。

完美哈希

您需要实现的是基于对象内容的perfect hash或其他一些唯一键。这可以在int的范围内实现,但我不会使用.hashCode()来表示此问题。我会在对象上使用显式关键字段。

Unique Hashing

使用内置于标准库中的使用SHA1散列的一种方法,它对小数据集的冲突几率极低。您发布到SHA1的值不会发生巨大的组合爆炸。

您应该能够使用您在问题中显示的有限值来计算生成minimal perfect hash的方法。

  

最小完美散列函数是映射n的完美散列函数   n个连续整数的键 - 通常为[0..n-1]或[1..n]。更多   表达这一点的正式方式是:让j和k成为某些元素   有限集K.F是最小完美散列函数iff F(j)= F(k)   暗示j = k(注入性)并且存在一个整数a,使得   F的范围是a..a + | K | -1。它已被证明是一般目的   最小完美散列方案至少需要1.44位/密钥。2   最好的当前已知的最小完美哈希方案使用大约2.6   比特/键。[3]

     

最小完美散列函数F是按键保持的顺序   以某种顺序给出a1,a2,...,an和任何键aj和ak,j      

最小完美散列函数F是单调的,如果它保留了   按键的词典顺序。在这种情况下,函数值是   只是每个键在所有的排序顺序中的位置   键。如果要散列的键本身存储在已排序的中   对于数组,可以存储少量的附加位   键入可用于计算哈希值的数据结构   快。[6]

解决方案

请注意,在谈到URL时,它可以是您根据对象计算的任何byte[]的任意String代表。

我通常会覆盖toString()方法,使其生成独特的内容,然后将其提供给UUID.nameUUIDFromBytes()方法。

Type 3 UUID can be just as useful as well UUID.nameUUIDFromBytes()

  

版本3 UUID使用从URL通过MD5派生UUID的方案,a   完全限定的域名,对象标识符,区分   name(轻量级目录访问协议中使用的DN)或on   未指定名称空间中的名称。版本3 UUID具有该表单   xxxxxxxx-xxxx-3xxx-yxxx-xxxxxxxxxxxx其中x是任何十六进制数字   y是8,9,A或B中的一个。

     

确定给定名称的版本3 UUID,即UUID   命名空间(例如,域的6ba7b810-9dad-11d1-80b4-00c04fd430c8)是   转换为与其十六进制对应的字节串   数字,与输入名称连接,用MD5进行散列,产生128   位。六个位由固定值替换,其中四个位   表示版本3的版本,0011。最后,固定哈希是   转换回十六进制形式,用连字符分隔   与其他UUID版本相关的部分。

我首选的解决方案是Type 5 UUID(Type 3的SHA版本)

  

版本5 UUID使用带有SHA-1散列的方案;否则就是了   与版本3中的想法相同.RFC 4122声明版本5是首选   超过版本3基于名称的UUID,因为MD5的安全性   损害。请注意,160位SHA-1哈希被截断为128位   使长度发挥作用。一个错误解决了这个例子   RFC 4122的附录B.

关键对象应该是不可变的

通过这种方式,您可以计算toString().hashCode()并在Constructor内生成一个唯一的主键并设置一次,而不是一遍又一遍地计算它们。

这是一个习惯性不可变对象的吸管示例,并根据对象的内容计算唯一键。

package com.stackoverflow;

import javax.annotation.Nonnull;
import java.util.Date;
import java.util.UUID;

public class Q23633894
{

    public static class Person
    {
        private final String firstName;
        private final String lastName;
        private final Date birthday;
        private final UUID key;
        private final String strRep;

        public Person(@Nonnull final String firstName, @Nonnull final String lastName, @Nonnull final Date birthday)
        {
            this.firstName = firstName;
            this.lastName = lastName;
            this.birthday = birthday;
            this.strRep = String.format("%s%s%d", firstName, lastName, birthday.getTime());
            this.key = UUID.nameUUIDFromBytes(this.strRep.getBytes());
        }

        @Nonnull
        public UUID getKey()
        {
            return this.key;
        }

        // Other getter/setters omitted for brevity

        @Override
        @Nonnull
        public String toString()
        {
            return this.strRep;
        }

        @Override
        public boolean equals(final Object o)
        {
            if (this == o) { return true; }
            if (o == null || getClass() != o.getClass()) { return false; }
            final Person person = (Person) o;
            return key.equals(person.key);
        }

        @Override
        public int hashCode()
        {
            return key.hashCode();
        }
    }
}

答案 1 :(得分:6)

对于对象状态的唯一表示,总共需要19位。因此,可以通过完美的散列来表示它"整数值(最多可包含32位):

@Override
public int hashCode() {
    int result = (stuck ? 1 : 0); // needs 1 bit (2 possible values)
    result += (facing ? 1 : 0) << 1; // needs 1 bit (2 possible values)
    result += marioMode << 2; // needs 2 bits (3 possible values)
    result += (onGround ? 1 : 0) << 4; // needs 1 bit (2 possible values)
    result += (canJump ? 1 : 0) << 5; // needs 1 bit (2 possible values)
    result += (wallNear ? 1 : 0) << 6; // needs 1 bit (2 possible values)
    result += (nearestEnemyX + 16) << 7; // needs 6 bits (33 possible values)
    result += (nearestEnemyY + 16) << 13; // needs 6 bits (33 possible values)
}

答案 2 :(得分:2)

不是使用31作为您的幻数,而是需要使用可能性的数量(标准化为0)

@Override
public int hashCode() {
    int result = (stuck ? 1 : 0);                // 2 possible values: 0, 1
    result = 2 * result + (facing ? 1 : 0);      // 2 possible values: 0, 1 
    result = 3 * result + marioMode;             // 3 possible values: 0, 1, 2
    result = 2 * result + (onGround ? 1 : 0);    // 2 possible values: 0, 1 
    result = 2 * result + (canJump ? 1 : 0);     // 2 possible values: 0, 1 
    result = 2 * result + (wallNear ? 1 : 0);    // 2 possible values: 0, 1 
    result = 33 * result + (16 + nearestEnemyX); // 33 possible values: - 16 to 16
    result = 33 * result + (16 + nearestEnemyY); // 33 possible values: - 16 to 16

    return result;
}

这将为您提供104544个可能的hashCodes()顺便说一句,您可以通过使用一系列/%

来反转此过程以从代码中获取原始值

答案 3 :(得分:-1)

尝试Guava的hashCode()方法或JDK7的Objects.hash()。这比编写自己的方式更好。不要自己重复代码(以及任何其他人可以使用开箱即用解决方案):