【问题标题】:HashSet is not sensitive to content of array?HashSet 对数组的内容不敏感?
【发布时间】:2016-11-19 06:41:00
【问题描述】:

我怎样才能使这段代码工作,以便当我更改数组的内容时,HashSet 会为它考虑不同的哈希码?现在它打印“真”。当且仅当元素完全相同时,我希望它打印为 true。

HashSet<int[][]> x = new HashSet<int[][]>();
int a[][] = {{1, 2}, {3, 4}};
x.add(a);
a[0][0] = 2;
a[0][1] = 1;
System.out.println(x.contains(a));

【问题讨论】:

  • 不要对数组类型执行此操作。您对其哈希码或等于实现的控制为零。
  • 对于@SotiriosDelimanolis 的观察,我要补充一点,将可变对象作为键放在映射或集合中总是存在风险的。如果它们在插入后发生突变,您将遇到麻烦。您问题的最佳答案是不要那样做
  • @SotiriosDelimanolis 您还可以零控制Integer hashcode 或 equals 实现,这些都可以用作集合/映射的键,所以 that 非常不好的论点。如果您的意思是说应该非常小心地使用可变对象作为键,那将是一个很好的论点。或者如果你的意思是说数组不实现哈希码或等于,所以你得到“身份”映射,这也是一个很好的论点。不幸的是,你两个都没有说。
  • 顺便说一句:add(a) 确实 复制数组,因此当您像将 a 添加到集合后一样修改它时,您实际上是在修改数组那是集合中。
  • @Andreas 是的,是的,所有的好东西。

标签: java hash hashmap hashset


【解决方案1】:

不幸的是,我们需要遵守指令,在使用 Java 编程时尽量不要混合数组和集合。但在这种特殊情况下,我不确定您为什么不喜欢默认行为。你实际上在做的是

  1. 将对象a 添加到集合中(不管a 是一个数组)
  2. 检查集合中是否有相同的对象(具有相同的引用a)。

在这种情况下,contains 支票不可能是false。更重要的是,你不希望这是true吗?

作为一名 Java 新手(并不暗示你是),我会对以下内容感到更加惊讶:

    HashSet<int[][]> x = new HashSet<>();
    int a[][] = {{1, 2}, {3, 4}};
    x.add(a);
    int[][] b = {{1, 2}, {3, 4}};
    System.out.println(x.contains(a));
    System.out.println(x.contains(b));

打印出来的

true
false

这就是hashCode 在 Java 中实现数组的复杂性变得显而易见的地方。您试图推断 显然 ba 相同,但该集合说它包含 a 而不包含 b。 p>

【讨论】:

  • 我可以看到很多问题的答案。 1)它暗示了一个不存在的“指令”。 (谁会/可以发出这样的指令?!?) 2)它谈论数组的“默认”行为,这实际上是内在行为。 3) 它遗漏了(或者至少没有明确解释)HashSet&lt;int[][]&gt; 存在问题的根本原因。
  • 其实我想要的是哈希码只依赖于数组的内容(也是它的维度);所以,在你的情况下,我希望 x.contains(b) 也是如此。对于“数组 a”,如果我更改了它的内容,那么 x.contains(a) 会给我错误的。但是,我认为这是不可能的,因为哈希码不仅取决于内容。对吗?
【解决方案2】:

问题的根本原因是 Java 语言规范指定的数组对象的 equals(Object)hashCode() 的语义,以及 java.lang.Object 的 javadocs。

基本上:

  • 数组类型的equals(Object) 方法指定== 运算符具有相同的语义。
  • 指定数组类型的hashCode() 以返回对象的标识哈希码值。

这意味着根据equals 对象,两个不同的数组永远不会相等。这也意味着分配一个数组的元素永远不会使数组等于另一个。

HashSet 的语义是根据equals(Object) 定义的。这意味着您的问题的答案:

HashSet对数组内容不敏感?

...是正确的:HashSet 对数组的内容不敏感。


你的例子

现在让我们看看你的例子:

HashSet<int[][]> x = new HashSet<int[][]>();
int a[][] = {{1, 2}, {3, 4}};
x.add(a);
a[0][0] = 2;
a[0][1] = 1;
System.out.println(x.contains(a));

这将返回 true,因为您放入 HashSet 的数组与您测试的数组相同。如上所述,数组的内容是无关紧要的。 HashSet 依赖于equals(Object) 方法,用于测试对象身份的数组类型;即如果数组对象是同一个对象。


“合同”...

但假设你这样做了:

HashSet<List<Integer>> x = new HashSet<List<Integer>>();
List<Integer> a = new ArrayList<>();
a.append(1);
a.append(2);
x.add(a);
a.set(0, 3);
System.out.println(x.contains(a));

现在会发生什么?

答案:坏事!

问题是equals(Object)hashCode()ArrayList 对数组的内容很敏感。但是我们在这里所做的是“违反合同”,即你应该如何处理HashSet 中的对象。您不应该修改作为散列集成员的对象,使其hashCode 值发生变化。

如果您在对象位于HashSet(或者是HashMapHashtable 的键)中时违反了equals / hashcode 的约定,那么该对象很容易在数据结构中丢失。

作为一般规则,使用可变对象作为哈希键是个坏主意。

这是各种 cmets 提出的观点。这是非常重要的一点......虽然它实际上并不是你的例子的根本问题,就像你写的那样。


修正你的例子

那么我们如何才能使您的示例有效;即做(我认为)你真正想做的事情?

这是一个一维数组的简化版本:

public List<Integer> makeSealedList(Integer ... values) {
    return Collections.immutableList(Arrays.asList(values.clone()));
}

HashSet<List<Integer>> x = new HashSet<List<Integer>>();
List<Integer> a = makeSealedList(1, 2);
List<Integer> b = makeSealedList(1, 2);
List<Integer> c = makeSealedList(3, 2);
x.add(a);

System.out.println(x.contains(a));   // prints true
System.out.println(x.contains(b));   // prints true
System.out.println(x.contains(c));   // prints false

但请注意,这只适用于“常量数组”,我特意将它们封装以确保我们的列表是常量。

如果您希望能够在哈希集中更改数组并让哈希集自动注意到更改并根据其新的数组重新哈希数组...这是任何 Java 都不支持的SE 集合类型。而且我认为如果没有一大堆额外的基础设施,它是无法实现的。

(对此的实际解决方案是从集合中删除“数组”,对其进行更新,然后重新添加。)

【讨论】:

    【解决方案3】:

    Java 数组继承自Object,并且 覆盖equals()hashCode(),这意味着数组的HashSetHashMap 是真正的“身份”集/地图,即与IdentityHashMap 类似。

    没有IdentityHashSet,因为它没有什么意义,尽管可以使用IdentityHashMap 进行模拟。

    由于数组不会覆盖 equals()hashCode(),因此为方便起见,他们将 equals()hashCode() 辅助方法添加到 Arrays 类中。

    【讨论】:

    • 虽然这回答了 OP 的具体问题,但同样重要的是要指出,如果数组确实将其哈希码基于其内容,它也会导致 OP 的用例失败,就像 @987654341 @ 不会自动重新哈希数组并重新计算其哈希桶。
    【解决方案4】:

    解决问题的最简单(但不是最好的方法)是将数组转换为字符串并将字符串存储在 HashSet 中。

    HashSet<String> x = new HashSet<>();
    int a[][] = {{1, 2}, {3, 4}};
    x.add(Arrays.deepToString(a));
    a[0][0] = 2;
    a[0][1] = 1;
    System.out.println(x.contains(Arrays.deepToString(a)));
    

    Codiva.io online compiler IDE 中的完整工作代码。

    以正确的方式解决这个问题实际上更难,也更复杂。这也将解释为什么您还没有一个像样的工作解决方案。

    您需要了解 Pass By ValuePass By Reference 和 Java 之间的区别,除了对于原语来说,一切都是通过引用传递的。因此,您添加到集合中的是 2D 数组的引用。因此,当值发生变化时,Set 中存在的元素(对数组的引用)的值也会发生变化。第二个问题是数组是可变的,当你将它用作 HashMap 或 HashSet 中的键时,这会导致更多的错误。

    我在 10 年前发布了这个博客,当时我尝试explain this the beginners。我认为这仍然是相关的。

    TL;DR 版本是,

    永远不要在 Set 中使用可变元素,也永远不要作为 Map 中的 Key

    在您的情况下,您的逻辑中有一个额外的错误。 如果你没看错,

    现在它打印“true”。当且仅当元素完全相同时,我希望它打印为 true。

    在您的情况下,当您将数组“a”插入地图时,地图有一些价值,即,

    a[][] = {{1, 2}, {3, 4}}
    

    Set 的内容也是 {{1, 2}, {3, 4}}

    现在,您正在更改 a 的值。新值为 a[][] = {{2, 1},{3, 4}} 同时,您更改了集合中存在的值。目前,该集合还包含{{2, 1},{3, 4}}。所以它返回true。 不幸的是,你这不是你想要的。解释您的代码的最短方法是,

    int a[] = {1, 2}
    int b[] = a;
    a[0] = 3;
    System.out.println(Arrays.equals(a, b));
    

    你期望答案是什么?

    【讨论】:

    • 答案不应包含指向粘贴框或类似站点的链接。当粘贴框超时时,您的答案将不完整。
    • 我意识到了这一点,所以我也在代码中添加了相关代码sn-p。我刚刚注意到,我只包含了代码的前半部分,并包含了完整的代码。感谢您指出。我希望我能得到一个支持
    • Java 中没有传递引用。一切都是按值传递的,但这些值可以是引用(这是不同的)。见stackoverflow.com/questions/40480/…
    • 另外,这句话是错误的So when the value changes, the value of the element (the reference to the array) present in the Set also changes。改变一个数组不会改变它的引用
    • 我刚才说的是引用中存在的值,由元素中的内容。我同意您对 Java 的评论只有“按值传递”,其中值是参考。但是对于初学者来说理解它的最好方法是知道什么是Pass by Reference
    【解决方案5】:

    我同意@Dici 从不使用 可变 值,如集合和映射中的数组。这对每个程序员来说都是一个很好的实践。下面的回答只是给那些正在学习Java并且想知道Sets和Maps如何保持其独特性的人提供一个例子。

    由于 HashSet 使用 HashMap 来保持条目的唯一性。 HashMap 使用 hashCode()equals() 来比较条目。所以你必须覆盖它们。

    如果我是你,我将创建一个名为 Matrix 的新类作为条目。并将它们放入HashSet。代码如下:

    public class Matrix {
    
        @Override
        public int hashCode(){
            int[] hash=new int[2];
            for(int i=0;i<2;i++){
                hash[i]=Arrays.hashCode(matrix[i]);
            }
            return Arrays.hashCode(hash);
        }
    
        @Override
        public boolean equals(Object o){
            Matrix inM=(Matrix)o;
            for(int i=0;i<2;i++){
                if(!Arrays.equals(inM.matrix[i],this.matrix[i])){
                    return false;
                }
            }
            return true;
        }
    
        //constructor
        public Matrix(int a,int b,int c,int d){
            matrix=new int[2][2];
            matrix[0][0]=a;
            matrix[0][1]=b;
            matrix[1][0]=c;
            matrix[1][1]=d;
        };
    
        //fields
        private int[][] matrix;
    }
    

    现在我们将 Matrix 插入 HashSet。

    /**
     *  MAIN
     */
    public static void main(String[] args){
        HashSet<Matrix> hs = new HashSet<Matrix>();
        Matrix m1 = new Matrix(1,2,3,4);
        Matrix m2 = new Matrix(5,6,7,8);
    
        hs.add(m1);
        System.out.println(hs.contains(m1));
        System.out.println(hs.contains(m2));
    }
    

    然后它运行良好。

    //OutPut:
    true
    false
    

    【讨论】:

    • 使用Arrays.equalsArrays.hashCode 有更简单的方法。这可能是一个有效的答案,但仍然存在可变性问题
    • 它并没有解决问题,因为OP违反了我在将项目放入集合后修改项目的合同。
    • 感谢您的回答,但我想要的只是不要经历定义新类的麻烦。
    • @A.Mashreghi 我不会把定义一个类称为“麻烦”。实际上,我认为在绝大多数用例中,在 Java 中使用数组并不是一个好主意。
    猜你喜欢
    • 1970-01-01
    • 2021-03-09
    • 2015-01-30
    • 2015-12-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-12
    • 2015-12-31
    相关资源
    最近更新 更多