0

how can I make this code work such that when I change the content of the array, HashSet considers a different hashcode for it? Right now it prints "true". I want it to print true if and only if the elements are exactly the same.

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));
A. Mashreghi
  • 1,729
  • 2
  • 20
  • 33
  • 6
    Don't do this with array types. You have zero control over their hashcode or equals implementation. – Sotirios Delimanolis Jul 16 '16 at 00:14
  • 4
    To @SotiriosDelimanolis's observation, I would add that there is always a risk at putting mutable objects as keys in a map or in a set. If they are mutated after their insertion, you'll be in trouble. The best answer to your question is **don't do that** – Dici Jul 16 '16 at 00:16
  • 1
    @SotiriosDelimanolis You also have zero control over `Integer` hashcode or equals implementation, and those are fine to use as keys of a set/map, so *that* is a very bad argument. If you meant to say that use of mutable objects as a key should be done with great care, that would be a fine argument. Or if you meant to say that arrays don't implement hashcode or equals, so you get "identity" mapping, that too would be a fine argument. Unfortunately, you said neither of those. – Andreas Jul 16 '16 at 00:32
  • 1
    BTW: `add(a)` does **not** copy the array, so when you modify `a` like you do after adding it to the set, you're actually modifying the array that is *in* the set. – Andreas Jul 16 '16 at 00:40
  • 1
    @Andreas Yeah, yeah, all that good stuff. – Sotirios Delimanolis Jul 16 '16 at 00:59
  • You are violating the contract by modifying the item after putting it into the collection. An array implementation that satisfied your definition would work even worse than this code. – user207421 Jul 16 '16 at 01:30
  • I think the easiest way to get what you want is to convert the array to string and store it in HashSet. Look at my answer or full code here in Codiva online compiler https://www.codiva.io/p/2cfe7411-652c-4829-b435-c700ba4630f0 – JackDaniels Jul 16 '16 at 04:34

5 Answers5

4

Unfortunately, we need to comply with the directive not to mix arrays and collections as far as possible while programming in Java. But in this particular case, I am not sure why you don't like the default behavior. What you are effectively doing is

  1. add an object a to a set (no matter a is an array)
  2. check if that same object (which has the same reference a) is in the set.

There is no way the contains check is going to be false in this case. And more importantly, don't you want this to be true?

As a Java novice (not implying that you are), I would have been more surprised with the following:

    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));

which prints

true
false

This is where the complication with respect to hashCode implementation of arrays in Java becomes evident. You try to reason that clearly b is same as a, and yet the set says it contains a and does not contain b.

Kedar Mhaswade
  • 4,535
  • 2
  • 25
  • 34
  • I can see lots of things wrong with the answer. 1) It alludes to a "directive" which doesn't exist. (Who would / could issue such a directive?!?) 2) It talks about "default" behavior of arrays which is really **intrinsic** behavior. 3) It misses (or at least, doesn't clearly explain) the fundamental reasons why `HashSet` is problematic. – Stephen C Jul 16 '16 at 03:43
  • Actually what I wanted is that the hashcode is only dependent on the content of array (also its dimension); so, in your case I want x.contains(b) to be true as well. And also for "array a" if I changed its content then x.contains(a) gives me false. However, I suppose it is not possible, since the hashcode is not only dependent on the content. right? – A. Mashreghi Jul 16 '16 at 04:11
2

The root cause of your problem is the semantics of equals(Object) and hashCode() for array objects as specified by the Java Language Specification, and the javadocs for java.lang.Object.

Basically:

  • The equals(Object) method for an array type is specified to have the same semantics as the == operator.
  • The hashCode() for an array type is specified to return the identity hashcode value for the object.

This means that two distinct arrays are never equal according to the equals object. It also means that assigning one of the elements of an array will never make the array equal to another.

The semantics of HashSet are defined in terms of equals(Object). That means that the answer to your question:

HashSet is not sensitive to content of array?

... is, correct: HashSet is not sensitive to the content of an array.


Your example

Now lets look at your example:

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));

This returns true because the array that you put into the HashSet is the same array that you tested for. As explained above, the content of the array is irrelevant. HashSet relies on the equals(Object) method, and for an array type that tests object identity; i.e. if the array objects are the same object.


"The contract" ...

But suppose that you did this:

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));

what is going to happen now?

Answer: BAD THINGS!

The problem is that equals(Object) and hashCode() for ArrayList are sensitive to the content of the array. But what we have done here is to "violate the contract" of how you are supposed to deal with objects in a HashSet. You are not supposed to modify an object that is a member of a hash set in such a way that its hashCode value changes.

If you violate the contract for equals / hashcode while an object is in a HashSet (or is the key of a HashMap or Hashtable), then the object is liable to get lost in the data structure.

As a general rule, it is a bad idea to use mutable objects as hash keys.

This is the point that various comments have made been making. It is a very important point ... though it is not actually the fundamental problem with your example, as you wrote it.


Fixing your example

So how can we make your example work; i.e. do what (I think) you are really trying to do here?

This is for a simplified version with a 1-D array:

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

But note that this only works for "constant arrays", and I have deliberately encapsulated them to ensure that our lists are constant.

If you want to be able to be able to change an array while it is in the hashset and have the hashset automatically notice the change and rehash the array based on its new ... that is not supported by any of the Java SE collection types. And I don't think it is implementable without a whole bunch of extra infrastructure.

(The practical solution to that would be to remove the "array" from set, update it, and then add it back again.)

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
0

Java arrays inherit from Object, and do not override equals() and hashCode(), which means that a HashSet or HashMap of arrays are really "identity" sets/maps, i.e. works similarly to IdentityHashMap.

There is no IdentityHashSet, because it would make little sense, though it can be simulated using IdentityHashMap.

Since arrays don't override equals() and hashCode(), they added equals() and hashCode() helper methods to the Arrays class, to your convenience.

Dici
  • 25,226
  • 7
  • 41
  • 82
Andreas
  • 154,647
  • 11
  • 152
  • 247
  • 5
    While this answers the OP's specific question, it is also important to point out that if arrays did base their hash code on their contents it would cause the OP's use case to fail as well, as the `HashMap` would not automatically rehash the array and recompute its hash bucket. – Jim Garrison Jul 16 '16 at 01:10
0

Easiest (but not the best way) to fix your problem is, convert the array to string and store the string the 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)));

The full working code in Codiva.io online compiler IDE.

Solving this issue the right way is actually more difficult and more involved. This will also explain why you haven't got a decent working solution yet.

You need to understand that the different between Pass By Value and Pass By Reference and in Java except for primitives everything is Pass By Reference. So, what you have added to the set is, a reference the the 2D array. So when the value changes, the value of the element (the reference to the array) present in the Set also changes. The second issue is the fact that array is mutable, that would call for more bugs when you use it as the key in HashMap or HashSet.

I posted this blog 10 years ago, when I tried to explain this the beginners. I think this is still relevant.

The TL;DR version is,

Never use a mutable element in a Set, and never as a Key in Map

In your case there is an additional error in your logic. If you see correctly,

Right now it prints "true". I want it to print true if and only if the elements are exactly the same.

In your case, when you insert the array 'a' into the Map, the map has some value, which is,

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

Contents of the Set is also {{1, 2}, {3, 4}}

Now, you are changing the value of a. The new value is a[][] = {{2, 1},{3, 4}} Simultaneously, you have changed the value that is present in the set. At present, the set also contains {{2, 1},{3, 4}}. So it returns true. Unfortunately, you this is not want you want. The shortest way to explain your code is,

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

What do you expect the answer to be?

JackDaniels
  • 985
  • 9
  • 26
  • An answer should not include a link to a paste-box or similar site. When the paste-box times out, your answer will be incomplete. – Stephen C Jul 16 '16 at 04:46
  • I was aware of it, and thats why I also added the relevant code snippet also in the code. I just noticed, I included only the first half of the code, and included the complete code. Thanks for pointing it out. I hope I get back an upvote – JackDaniels Jul 16 '16 at 05:01
  • There is no pass-by-reference in Java. Everything is passed by value, but these values can be references (which is different). See http://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value – Dici Jul 16 '16 at 05:06
  • Also, this sentence is wrong `So when the value changes, the value of the element (the reference to the array) present in the Set also changes`. Mutating an array does not change its reference – Dici Jul 16 '16 at 05:08
  • I just said the value present in the the reference pointed to, by what is in the element. I agree with your comment on Java only has 'Pass By Value' where the value is the reference. But the best way to understand it for beginners is to know what is **Pass by Reference**. – JackDaniels Jul 16 '16 at 05:09
  • You said `in Java except for primitives everything is Pass By Reference`, which is wrong. There is no pass-by-reference in Java – Dici Jul 16 '16 at 05:10
  • You are technically correct, but try explaining that to a beginner. – JackDaniels Jul 16 '16 at 05:12
  • Well, the best thing to do then is not to give them erroneous information to confuse them further – Dici Jul 16 '16 at 05:45
  • Generally, I might have agreed, but in this case, I disagree. But edits are welcome. – JackDaniels Jul 16 '16 at 06:04
-1

I am agree with @Dici that never use mutable value like arrays in a set and map. This is a nice practice for every programmer. The following answer is just to give an example for those who is learning Java and who want to know how Sets and Maps keep their uniqueness.

Since the HashSet use the HashMap to keep the uniqueness of the entry. And HashMap use hashCode() and equals() to compare the entries. So you have to override them.

If I was you, I will create a new class called Matrix to be the entry. And put them into the HashSet. Here is the code:

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;
}

Now we insert the Matrix into the 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));
}

Then it works well.

//OutPut:
true
false
shen
  • 933
  • 10
  • 19
  • 1
    There are easier ways to do this using `Arrays.equals` and `Arrays.hashCode`. This could be a valid answer but there's still the problem of mutability – Dici Jul 16 '16 at 01:22
  • 1
    And it doesn't solve the problem, because the OP is violating the contract my modifying the item after putting it into the collection. – user207421 Jul 16 '16 at 01:28
  • Thanks for the answer but all i wanted was not to go through the trouble of defining a new class. – A. Mashreghi Jul 16 '16 at 04:12
  • @A.Mashreghi I would not call defining a class "trouble". Actually, I think using arrays in Java is a bad idea in the vast majority of the use-cases. – Dici Jul 16 '16 at 05:51