Java哈希码:对象 vs 组合

4

我在一些代码中发现了这种方法,其唯一目的是为 HashMap 创建一个字符串 Key (编辑:在我的情况下,X、Y和Z都将是数字,如果将它们视为坐标,则会更容易理解):

protected String createMappingKey(String x, String y, String z) {
        return x+":"+y+":"+z;
}

这段代码中有些问题,我认为最好用一个对象来替换它,代码如下(请注意,此代码已由我的IDE生成,因此您可以根据需要更改实现方式):

public static class MyKey {
        String x,y,z;

        // Constructor(s), maybe getters and setters too here

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

            MyKey myKey = (MyKey) o;

            if (x != null ? !x.equals(myKey.x) : myKey.x != null) return false;
            if (y != null ? !y.equals(myKey.y) : myKey.y != null) return false;
            if (z != null ? !z.equals(myKey.z) : myKey.z != null) return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = x != null ? x.hashCode() : 0;
            result = 31 * result + (y != null ? y.hashCode() : 0);
            result = 31 * result + (z != null ? z.hashCode() : 0);
            return result;
        }
    }

但这似乎是很多代码而没有太多的价值。我相信这两种方法之间碰撞数量的差异只会微不足道。

你更喜欢哪种方法?为什么?还有其他更好的方法吗?

如果其中一种方法的碰撞数量比另一种方法显著增加,那么我也很感兴趣,我将开设一个单独的问题来处理此事。


1
希望你的关键类不要有设置器,因为你的字段应该是不可变的。 ;) - Peter Lawrey
9个回答

3

当我想创建一个键,但不需要完整的键类时,我倾向于使用Arrays.asList创建一个List

protected List<Object> createMappingKey(String x, String y, String z) {
    return Arrays.<Object>asList(x, y, z);
}

字符串的危险在于,如果您的元素使用了您也用作分隔符的字符,则equals可能会发生冲突。列表确保不会发生这样的冲突。它还具有与任何具有正确的equals/hashCode实现的对象一起工作的好处,而不仅仅是字符串和具有equals兼容toString实现的对象。
如果您确实想创建一个键类,您可以使用Appache Commons的EqualsBuilderHashCodeBuilder来显着缩短您的hashCode和equals类。

这是一个不错的提示,我之前不知道有 HashCodeBuilder 这个工具。我得记住它。 - StevenWilkins

1
你可以尝试使用不同的分隔符,比如冒号“:”,假设你的字符串中没有空字节。冒号更有可能出现。
protected String createMappingKey(String x, String y, String z) {
    return x+'\0'+y+'\0'+z;
}

就像你所说的,为了微不足道的差异而付出了很多工作。

一个缺点是它会将null和"null"视为相同。你必须判断这是否可能成为问题。


这是一个公正的观点。在我的情况下,在我混淆代码之前,很明显所有三个字符串都是数字。我会更新问题并说明这一事实。 - StevenWilkins
@StevenWilkins,然后使用任何常规分隔符的字符串。你的数字为什么不是像int或double这样的数字,而是字符串?即使它们是数字,你也可能会做同样的事情。 - Peter Lawrey

0

你的代码将比字符串更快。

你可以通过将逻辑移动到一个公共基类中,使用Java等效的this technique来简化它。 (使用返回属性值数组的抽象方法)

它会看起来像这样

abstract class EqualityComparable {
    protected abstract Object[] keys();

    @Override
    public boolean equals(Object o) { ... }
    @Override
    public int hashCode() { ... }
}

0
创建的键将作为哈希表中的键使用,这意味着会调用hashCode()方法。因此实际上,您的键hashCode可能比使用该方法更糟糕。如果您想通过引入一个单独的键类来使您的代码更清晰,那没问题,请只是使用它。
    @Override
    public int hashCode() {
        return (x+":"+y+":"+z).hashCode();
    }

如此简单。请注意,您可能需要考虑确保所选择的分隔符(在本例中为“:”)不会出现在字符串x、y、z中,否则不同的字符串可能会创建相同的hashCode。


0

createMappingKey()方法仅创建一次字符串。由于字符串是不可变的,因此用于Maps的哈希码只计算一次。但该方法创建了一些StringBuilder来执行连接操作。仅在稍后将哈希码表示为HashMap中的哈希码时才创建字符串表示本身。因此,它是一个没有价值的一次性产品。

您可以使其具有更多的价值并提供其他目的。您的MyKey类也可以是不可变的,因此您可以预先计算自己的hashCode并缓存它。这有可能在HashMap非常频繁地访问或修改时提高性能,因为equals()/hashCode()调用会更快。

一个专门的键类听起来更具面向对象的优点,但当然会引入额外的源代码和混乱。但它可能更好地表达了键实际上是什么:它只是一些字符串的连接吗?我怀疑。它可能也是客户的标识符之类的东西,那么为什么不把它称为那样的名称呢?

public class CustomerIdentifier 
{

 public CustomerIdentifier(String tenantId,
                           String customerName,
                           String location) 
 {
  // Validate tenantId, customerName, location
  // e.g. non-null, minimum length, uppercase/lowercase
  // pre-calculate hashCode
  // and throw away values if we don't need them 
 }

 // equals() and hashCode() go here
 public long hashCode() { return precalc; };
}



map.put(new CustomerIdentifier("a","b","c), customer);

这种方法让你在项目中定义一个客户对象的标识方式。它封装了键值生成-你不必关心是否必须使用 : 作为分隔符或其他什么。其它客户端(例如,如果 HashMap 被其他不受你控制的代码访问)可能更容易使用,因为他们只需使用 CustomerIdentifier 而不是重复键值生成代码(虽然没多少,但问题通常会在稍后出现,当有人更改分隔符而其他代码却没有...)

此外,这种方法还能让你将 x/y/z 值的验证放在一个类中。它们可以为 null 吗?它们可以是冒号吗?你如何区分它们?

x=':' y=''  z=''    is       ::::
x=''  y=':' z=''    is       ::::
x=''  y=''  z=':'   is       ::::

缺点是它可能更难调试。在这种情况下,您还需要在该类中实现toString()方法,以便您可以更快地浏览调试器视图中的哈希映射,并找到您要查找的键。

第二个缺点是,您可能经常需要为访问HashMap的代码创建新的键对象,但只有x/y/z值可用。


0

我不想说,但是你的答案更糟糕 - 它将使用4倍的对象引用(一个用于键,加上每个字符串一个 = 4),另外还必须单独计算3个哈希码以及每次查找的额外处理。


你所说的部分内容在Java 1.2版本中是正确的,但现在即使你没有明确声明,连接字符串也会自动使用StringBuffer(Java 1.4+)。此外,字符串中的哈希码不是在实例化时计算的,而是在首次请求时进行惰性计算。 - Edwin Buck
我认为你没有理解我的观点。使用字符串更好 - 它将使用一个字符串缓冲区进行追加,然后仅使用1个对象引用。他的类将保留对每个字符串的引用(共3个),再加上对对象本身的引用。 - Kylar
然后对于哈希码-字符串只查找一次并缓存,但是他必须计算3个分别针对字符串x、y和z的哈希码,然后进行其他处理以创建自己的哈希码。(是的,在第一次查找后,他将获得缓存的好处)。 - Kylar

0
如果你的字符串比较短,那么我会更倾向于使用组合字符串键的方法。原因是这样不会增加代码复杂度。你真的需要编写/调试/维护你的组合键类及其equals/hashCode方法吗?另一方面,如果字符串不短,则基于类的方法更好,因为它将产生更低的内存占用。

0

我不认为您上面的MyKey方法会与使用createMappingKey生成的键相比,导致HashMap的行为有明显不同。 MyKeyString都具有正确的equalshashCode行为,因此从HashMap的角度来看,两者之间没有任何区别。

我认为,将MyKey用作您的HashMap键而不是普通的String,可以更清楚地告诉其他程序员有关什么构成了正确的键的严格规则,以便他们不会做一些愚蠢的事情,例如在HashMap中插入""键(无论是故意还是错误操作)。


0

我感觉你代码示例中缺失的部分会比你发布的内容更有帮助。你为什么需要一个字符串键?如果这些 x、y、z 数字是相关的,最好将它们放在一些有意义的 bean 中,你可以将其用作映射键(还可以提供一些额外的功能)。

如果 X、Y、Z 真的是坐标,那么这意味着你只需要一个 Point3D 作为键。将其设置为不可变的,然后直接使用它,为什么要费心创建字符串模式来作为映射键呢?如果你需要 {x}:{y}:{z} 表示形式,可以从 Point3D.toString() 返回它。并且你也可以通过一些静态的 valueOf(String) 工厂方法从字符串表示中重构出一个点。


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