【问题标题】:How do I generate an (almost) unique hash ID for objects?如何为对象生成(几乎)唯一的哈希 ID?
【发布时间】:2018-03-22 17:32:46
【问题描述】:

如何获取我的对象的 ID,以便于将其与其他对象区分开来?

class MyClass {
    private String s;
    private MySecondClass c;
    private Collection<someInterface> coll;
    // ..many more

    public Result calculate() {
        /* use all field values recursively to calculate the result */
        /* takes considerable amount of time. Implemented */
        return result;
    }

    public String hash() {
        /* use all field values recursively to generate a unique identifier */
        // ?????
}

calculate() 通常需要大约 40 秒才能完成。因此,我不想多次调用它。

MyClass 对象非常大(~60 MB)。 Result 的计算值只有 ~100 KB。

每当我要对一个对象运行计算时,我的程序应该查找是否已经在早些时候执行过,并且具有完全相同的值,递归。如果是这样,它将在(例如)HashMap 中查找结果。基本上,MyClass 对象本身可以用作键,但 HashMap 将包含 30-200 个元素 - 我显然不想以全尺寸存储所有这些元素。这就是为什么我想存储 30-200 个 Hash/result 值。

所以,我想我会为 MyClass 对象中的所有值生成一个 ID(哈希)。我该怎么做?这样,我可以使用那个哈希来查找结果。 我知道像 MD5 这样的哈希码不能保证 100% 的唯一性,因为多个对象可能具有相同的哈希。但是,如果我通过 MD5 存储(最多)200 个元素,我认为两次使用哈希的机会将是可以忽略的。 16^32=3.4e38 可能有不同的哈希码。我很高兴听到任何人的意见,或者看到其他方法。

生成哈希后,我不再需要该对象,只需要其各自的 result 值。

具有完全相同值的两个单独对象必须返回相同的哈希码。很像原始的 hashCode(),只是我试图保持唯一性。两个对象具有相同哈希码的概率绝对可以忽略不计。

我已经不知道如何用其他语言来描述这个问题了。如果需要进一步澄清,请询问。

那么如何生成我的MyClass.hash()

问题不在于如何或在何处存储散列,因为我什至不知道如何为整个对象生成一个(几乎)唯一的散列,对于相同的值,它总是相同的。


澄清:

说到大小,我指的是硬盘上的序列化大小。

我不认为将对象放入 HashMap 会减小它们的大小。这就是我想存储一些哈希字符串的原因。 HashMap&lt;hashStringOfMyClassObject, resultValue&gt;

当您将对象放入 HashMap(作为键或值)时,您不会创建它的副本。因此,在 HashMap 中存储 200 个大对象所消耗的内存比 200 个对象本身多一点。

我自己不存储 200 个大型对象。我只保留了 200 个很小的不同结果(作为值),以及 MyClass 对象的 200 个各自的 hashCode,它们也非常小。 “散列”对象的目的是能够使用散列而不是对象值本身。

【问题讨论】:

  • 这里似乎有各种各样的问题。为什么您的班级大小为 60MB?你说的大小是什么意思?保留大小?为什么你认为这些对象中的HashMap 会增加呢?这听起来很像 X-Y 问题。
  • 你的类是可变的吗?可以使用interning 来减少内存使用吗?
  • 当您将对象放入HashMap 中(作为键或值)时,您不会创建它的副本。因此,在 HashMap 中存储 200 个大对象所消耗的内存比 200 个对象本身要多。
  • 编辑了我的问题,请再次阅读结尾

标签: java hash


【解决方案1】:

您正在对一个对象调用 hash(),并且您的目标是记住结果,因为计算成本很高,并且除非某些状态发生变化,否则结果是不变的?

那么为什么不将结果保存在对象的实例变量中。有一些类似的逻辑

  calculate() {
      if ( m_cachedResult == null ){
          m_cachedResult = origincalCaclulate(); // refactored original
      }
      return m_cachedResult;
  }

那么,如果你能确保所有相关状态都通过这个类的setter被修改,需要重新计算时清除缓存

  setThing(newValues) {
        m_cachedResult = null;
        //process new state values
  }    

【讨论】:

  • 请注意,这要求对象是不可变的
  • 嗯,是的。阅读更仔细。我会修改的。
  • 不,我不想记住结果,我想记住结果。您的解决方案仅适用于一种可能的记忆结果,对吗?这就是我想使用 hashMap 的原因。
  • OP 说计算后对象不会被保留。问题是在某个时候可能会构造另一个具有相同值的对象。所以计算的结果需要根据对象中的值而不是对象本身来调用
  • 好的。我误解了这个问题 - 我的回答在这里无济于事。我不会删除它,因为我认为它有助于理解真正的问题。
【解决方案2】:

如果您想创建所有数据的哈希值,您需要确保可以从中获取所有字节格式的值。

要做到这一点,最好是你可以控制所有的类(也许除了 Java 内置的类),这样你就可以为它们添加一个方法来做到这一点。

鉴于您的对象非常大,将其递归地收集到一个大字节数组中然后计算摘要可能不是一个好主意。最好创建MessageDigest 对象,并添加如下方法:

void updateDigest( MessageDigest md );

他们每个人。如果您愿意,可以为此声明一个接口。每个这样的方法都会收集类自己参与“大计算”的数据,并用该数据更新md 对象。更新完所有自己的数据后,它应该递归调用其中定义了该方法的任何类的updateDigest 方法。

例如,如果您有一个带有字段的类:

int myNumber;
String myString;
MyClass myObj;  // MyClass has the updateDigest method
Set<MyClass> otherObjects;

那么它的updateDigest 方法应该是这样的:

// Update the "plain" values that are in the current object
byte[] myStringBytes = myString.getBytes(StandardCharsets.UTF_8);
ByteBuffer buff = ByteBuffer.allocate(
                        Integer.SIZE / 8    // For myNumber
                        + Integer.SIZE / 8  // For myString's length
                        + myStringBytes.length
                  );
buff.putInt( myNumber );
buff.putInt( myStringBytes.length );
buff.put( myStringBytes );
buff.flip();
md.update(buff);

// Recurse
myObj.updateDigest(md);

for ( MyClass obj : otherObjects ) {
    obj.updateDigest(md);
}

我将字符串的长度(实际上是它的字节表示的长度)添加到摘要中的原因是为了避免出现两个String 字段的情况:

String field1 = "ABCD";
String field2 = "EF";

如果你只是将它们的字节一个接一个地直接放入摘要中,它将对摘要产生相同的效果:

String field1 = "ABC";
String field2 = "DEF";

这可能会导致为两组不同的数据生成相同的摘要。所以添加长度会消除歧义。

我使用了ByteBuffer,因为添加intdouble 之类的东西相对比较方便。

如果您有无法控制且无法添加方法的类,则您必须要有创意。毕竟,您确实从每个此类类中获取了用于计算的值,因此您可以调用相同的方法并消化它们的结果。或者,如果它们是可序列化的,您可以消化它们的序列化形式。

因此,在您的 head 类中,您将使用 MessageDigest.getInstance("SHA") 或您希望使用的任何摘要创建 md 对象。

MessageDigest md = null;
try {
    md = MessageDigest.getInstance("SHA");
} catch (NoSuchAlgorithmException e) {
    // Handle properly
}

// Call md.update with class's own data and recurse using
// updateDigest methods of internal objects

// Compute the digest
byte [] result = md.digest();

// Convert to string to be able to use in a hash map
BigInteger mediator = new BigInteger(1,result);
String key = String.format("%040x", mediator);

(您实际上可以使用BigInteger 本身作为密钥)。

【讨论】:

  • 为什么要使用Integer.SIZE / 8为String对象分配空间? afaik 字符串 obj 大小是可变的..?
  • @Blauhirn 我在答案中解释说,我在每个字符串对象之前添加字符串的 length 以避免“ABCD”/“EF”与“ABC”/” DEF”问题(这可能应该对任何可变长度对象(例如数组和列表)进行,除非您确定在您的特定情况下不会出现这种情况)。
  • 啊现在我明白了:“字符串长度”你确实不是是指myStringBytes.length,而是你的意思是Integer.SIZE / 8。抱歉,逻辑上有点难以理解
  • 无论如何,谢谢,你帮了我很大的忙,实际上解决了我的问题:)
【解决方案3】:

其实你有一个对象叫UUID

表示不可变的通用唯一标识符 (UUID) 的类。 UUID 代表一个 128 位的值。

你可以找到some ideas here,例如:

import java.util.UUID;

public class GenerateUUID {
   public static UUID generate() {
        UUID idOne = UUID.randomUUID();
        return idOne;
   }
}

然后只需检查创建的对象中是否存在(这几乎是不可能的)并在必要时再次调用。

【讨论】:

  • 两个具有完全相同值的单独对象必须返回相同的哈希码。很像原始哈希码,只是我试图保持唯一性,就好像我在 List 中使用 equals()
【解决方案4】:

一般来说,计算一些类似哈希的标识符并不是一个好方法。发生冲突的可能性极低,但仍有可能发生。请记住,哈希不是 100% 随机数,在大多数情况下,它以某种方式与输入数据相关联,因此,根据您的哈希方法,某些哈希可能无法访问,或者 - 在更糟糕的情况下 - 一些它们对于相当大的输入对象集很常见。它可以精确计算,但这是在计算机科学和概率论方面。

使用一些 distest 函数(MD5、SHA 等)会有很大帮助,但仍不能完全解决问题。

我更喜欢的解决方案类似于 Jordi 的解决方案。用一些标识符增强你的类。根据您的项目 - 我将设置,例如创建日期和/或此类任务的名称。 String 名称或任务描述可以使调试更容易。

如果这些不够独特,您总是可以添加唯一的数字计数器(或UUID 实例)。

【讨论】:

    猜你喜欢
    • 2019-11-30
    • 2013-07-29
    • 1970-01-01
    • 2011-07-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多