用作哈希键的java对象是否需要“完全”不可变?

时间:2017-08-30 17:27:09

标签: java hashmap hashcode

如果 hashCode() 计算使用不可变字段并且equals()使用所有字段,那么当该类用作哈希值时会出现问题键? E.g。

import java.util.Objects;

public class Car {
    protected final long vin;
    protected String state;
    protected String plateNumber;

    public Car( long v, String s, String p ) {
        vin = v; state = s; plateNumber = p;
    }


    public void move( String s, String p ) {
        state = s; plateNumber = p;
    }


    public int hashCode() {
        return (int)( vin % Integer.MAX_VALUE );
    }


    public boolean equals( Object other ) {
        if (this == other) return true;
        else if (!(other instanceof Car)) return false;
        Car otherCar = (Car) other;
        return vin == otherCar.vin
                && Objects.equals( state, otherCar.state )
                && Objects.equals( plateNumber, otherCar.plateNumber );
    }
}

在car对象插入到hashset之后调用move(),可以通过保存在别处的引用来实现。

我不是在追问性能问题。只有正确性。

我已经阅读了java hashCode contact,很少有答案,包括this由尊敬的Jon Skeet和this来自大蓝。我觉得最后一个链接给出了最好的解释,并暗示上面的代码是正确的。

修改

结论:
该类满足java中'equals()'和'hashCode()'的约束。但是,当用作集合中的键,散列或不散列时,它违反了对'equals()'的额外要求的限制 附加要求是,只要对象是键,'equals()'就必须保持一致 请参阅路易斯·沃瑟曼的反例和下面道格拉斯提供的参考资料。

很少澄清:

A)此类满足java对象级别约束:

  1. (carA == carB)暗示(carA.hashCode()== carB.hashCode())
  2. (carA.hashCode()!= carB.hashCode())暗示(carA!= carB)
  3. equals()需要具有反身性,对称性,传递性。
  4. hashCode()需要保持一致。即在一生中无法改变物体。
  5. equals()需要保持一致,只要两个对象都没有被修改
  6. 请注意,“1”和“2”的反向不是必需的。上述课程满足所有条件 此外,java文档提到“equals()...实现对象上最具辨别力的可能等价关系”,但不确定这是否是强制性的。

    B)就性能而言,碰撞避免概率的增量随着我们组合的每个连续成员变量而减小。通常很少有精心挑选的成员变量就足够了。

7 个答案:

答案 0 :(得分:2)

仅考虑hashCodeequals合同时,您认为此实施符合其要求是正确的。 hashCode使用equals使用的严格字段子集足以保证a.equals(b)根据需要隐含a.hashCode() == b.hashCode()

但是,当您引入Map时,情况会发生变化。从Map javadoc,&#34;如果对象的值以影响等于比较的方式更改,而对象是地图中的键,则不指定地图的行为。&#34; < / p>

move Car上调用Map之后,Map的行为现在未指定。Map的行为现在未指定。在许多情况下,它在实践中仍将以您希望的方式工作,但奇怪的事情可能以难以预测的方式发生。虽然从技术上讲 Car car1 = ... Car car2 = ... // a copy of car1 Map<Car, String> map1 = ... map1.put(car1, "value"); assert map1.get(car2).equals("value"); // true car1.move(...); assert map1.get(car2).equals("value"); // NullPointerException on the equals call, car2 is no longer found 自发清空或切换所有查找以使用随机数生成器在技术上是有效的,但更可能的情况可能是这样:

car2

请注意,Mapcar2都没有以任何方式改变自己,但Map的映射无论如何都改变了(或者更确切地说,消失了)。这种行为没有正式指定,但我猜大多数dplyr实现都会这样做。

答案 1 :(得分:1)

如果您从未在move位于地图后致电Car,那是正确的。否则就错了。密钥在地图中后,hashCode equals都必须保持一致。

答案 2 :(得分:1)

您可以根据需要改变您的关键候选人,在他们被用作键之前或之后(而不是在期间)。 实际上,执行这条规则非常困难。如果您改变了对象,那么如果有人将它们用作关键字,则您没有控件。

密钥的不变性更容易,消除了微妙的,难以发现的错误的来源,只是更好地为密钥工作。

在您的情况下,我发现没有正确性问题。但是为什么你不打算在哈希码中包含所有字段?

答案 3 :(得分:1)

哈希通过将项目放入“桶”来工作。每个桶都由hashcode计算。找到存储桶后,搜索会继续使用equals逐个比较每个项目。

例如:

  • 在插入期间:id为100的对象放在桶5中(计算的哈希码为5)。
  • 在检索过程中:您要求hashmap找到项目100.如果哈希值现在计算7,那么算法将在桶7中搜索您的对象,但是您的对象将永远不会被找到,因为它居住在桶5中。

总结:哈希码和实际键一起工作。前者用于知道项目应该在哪个桶中。后者由equals比较使用,寻找实际项目返回。

答案 4 :(得分:1)

Short answer: it should be OK, but prepare for bizarre behavior.

Longer answer: when you change fields that participate in equals() on a key, the value keyed by that key will no longer be found.

Still longer answer: this looks as X/Y problem: you're asking about X, but you really need X to accomplish Y. Maybe you should ask about Y?

The car in your case is uniquely identified by body { font-family: "Lato", sans-serif; padding-top: 30px; } .toggleNav { position: fixed; left: 5px; top: 5px; font-size: 30px; cursor: pointer; z-index: 10; opacity: 1; transition: transform .5s ease, color .5s, opacity .5s; } .nav-is-open .toggleNav { transform: translateX( calc( 50vw - 40px ) ); color: #fff; opacity: .5; } .nav-is-open .toggleNav::before { content: '\000D7'; } .nav-is-open .toggleNav > span { display: none; } .nav-is-open .toggleNav:hover { opacity: 1; } /* Combine same styles of both sides in one class */ .Sidenav { position: fixed; top: 0; height: 100%; width: 0; z-index: 1; padding-top: 60px; background-color: #111; overflow-x: hidden; transition: width .5s ease; } .leftSidenav { left: 0; } .rightSidenav { right: 0; /* Instead of having width: 0 we transform it to the side This prevents text from wrapping */ width: 50%; transform: translateX( 100% ); transition: transform .5s ease; } /* Combine link styles into one */ .Sidenav a { display: block; padding: 8px 8px 8px 32px; text-decoration: none; font-size: 25px; color: #818181; transition: 0.3s; } .Sidenav a:hover { color: #f1f1f1; } .nav-is-open .Sidenav { width: 50%; transform: translateX( 0 ); } @media screen and (max-height: 450px) { .Sidenav { padding-top: 15px; } .Sidenav a { font-size: 18px; } }. A car equals to itself. But, a car can be registered in different states. Maybe the answer is to have a Registration object (or a few of them) attached to the car? And then you can separate <div class="Sidenav leftSidenav"> <a href="#">About</a> <a href="#">Services</a> <a href="#">Clients</a> <a href="#">Contact</a> </div> <div class="Sidenav rightSidenav"> <a href="#">About 2</a> <a href="#">Services 2</a> <a href="#">Clients 2</a> <a href="#">Contact 2</a> </div> <a class="toggleNav" onclick="toggleNav()"><span>&#9776;</span></a> from .nav-is-open.

答案 5 :(得分:0)

当您的hashCode()实施仅使用有限数量的字段(vs equals)时,您几乎会降低使用散列的所有算法/数据结构的效果:HashMap,{ {1}}等等。您正在增加碰撞概率 - 当两个不同的对象(HashSet返回false)具有相同的哈希值时,情况就会出现。

答案 6 :(得分:0)

简短的回答是:否。

答案很长:

完全不变性不是必需的。但是:

等于必须仅依赖于不可变值。 Hashcode必须依赖于不可变值,或者是equals中使用的值的常量或子集,或者是equals中使用的所有值。 equals中未提及的值不得是哈希码的一部分。

如果你改变值等于和hashcode依赖,很可能你没有在基于散列的数据结构中再次找到你的对象。看看这个:

public class Test {


    private static class TestObject {
        private String s;

        public TestObject(String s) {
            super();
            this.s = s;
        }

        public void setS(String s) {
            this.s = s;
        }

        @Override
        public boolean equals(Object obj) {
            boolean equals = false;
            if (obj instanceof TestObject) {
                TestObject that = (TestObject) obj;
                equals = this.s.equals(that.s);
            }
            return equals;
        }

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

    public static void main(String[] args) {

        TestObject a1 = new TestObject("A");
        TestObject a2 = new TestObject("A");

        System.out.println(a1.equals(a2)); // true

        HashMap<TestObject, Object> hashSet = new HashMap<>(); // hash based datastructure

        hashSet.put(a1, new Object());

        System.out.println(hashSet.containsKey(a1)); // true

        a1.setS("A*");

        System.out.println(hashSet.containsKey(a1)); // false !!! // Object is not found as the hashcode used initially before mutation was used to determine the hash bucket

        a2.setS("A*");

        System.out.println(hashSet.containsKey(a2)); // false !!! Because a1 is in wrong hash bucket ...

        System.out.println(a1.equals(a2)); // ... even if the objects are equals

    }

}