HashSet对数组内容不敏感?

时间:2016-07-16 00:12:13

标签: java hash hashmap hashset

如何让这段代码工作,这样当我改变数组的内容时,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));

5 个答案:

答案 0 :(得分:3)

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

  1. 将对象a添加到集合中(无论a是否为数组)
  2. 检查相同的对象(具有相同的引用a)是否在集合中。
  3. 在这种情况下,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
    

    这就是Java中hashCode数组实现的复杂性变得明显的地方。您尝试将{em>明确 b与<{1>} 相同,并且该集合表明它包含a且不包含a

答案 1 :(得分:2)

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

基本上:

  • 数组类型的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)方法,以及测试对象标识的数组类型;即如果数组对象是同一个对象。

&#34;合同&#34; ...

但是假设你这样做了:

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 对数组内容敏感。但我们在这里所做的是违反合同&#34;你应该如何处理HashSet中的对象。您不应该以其hashCode值更改的方式修改作为哈希集成员的对象。

如果您在对象位于HashSet(或者是HashMapHashtable的关键字)时违反了equals / hashcode的合同,则该对象可能会被取消丢失在数据结构中。

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

这是各种评论所提出的观点。这是非常重要的一点......虽然它实际上并不是你的例子中的基本问题,但正如你所写的那样。

修复您的示例

那么我们怎样才能使你的榜样有效;即做那些(我想)你真的想在这里做什么?

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

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

但请注意,这仅适用于&#34;常量数组&#34;,我故意将它们封装到确保我们的列表是常量。

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

(实际的解决方案是从集合中移除&#34;数组&#34;更新它,然后再将其添加回来。)

答案 2 :(得分:0)

Java数组继承自Object,并且覆盖equals()hashCode(),这意味着HashSet或{{3}数组真的是&#34; identity&#34;集合/映射,即与HashMap类似的工作。

没有IdentityHashSet,因为它可以毫无意义,但可以使用IdentityHashMap进行模拟。

由于数组不会覆盖equals()hashCode(),因此他们将equals()hashCode()辅助方法添加到IdentityHashMap类,以方便您使用

答案 3 :(得分:0)

解决问题的最简单(但不是最好的方法)是,将数组转换为字符串并将字符串存储为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中的完整工作代码。

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

您需要了解 传递价值 传递参考 与Java之间的区别对于原语,一切都是通过参考。那么,你添加到集合中的是二维数组的引用。因此,当值更改时,Set中存在的元素值(对数组的引用)也会更改。第二个问题是数组是可变的,当你将它用作HashMap或HashSet中的键时会调用更多错误。

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

TL; DR版本是,

永远不要在集合中使用可变元素,永远不要在地图中使用键

在您的情况下,您的逻辑中会出现其他错误。 如果你看得对,

  

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

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

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

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

现在,您正在更改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));

您期望答案是什么?

答案 4 :(得分:-1)

  

我同意@Dici,它永远不会像集合和地图中的数组那样使用可变值。这对每个程序员来说都是一个很好的做法。下面的答案只是为那些正在学习Java并想知道集合和地图如何保持其独特性的人提供一个例子。

由于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