Reputation: 2843
I am using boolean arrays as keys for a HashMap. But the problem is HashMap fails to get the keys when a different array is passed as key, although the elements are same. (As they are different objects).
How can I make it work with arrays as keys ? Here is the code :
public class main {
public static HashMap<boolean[], Integer> h;
public static void main(String[] args){
boolean[] a = {false, false};
h = new HashMap<boolean[], Integer>();
h.put(a, 1);
if(h.containsKey(a)) System.out.println("Found a");
boolean[] t = {false, false};
if(h.containsKey(t)) System.out.println("Found t");
else System.out.println("Couldn't find t");
}
}
Both the arrays a
and t
contain the same elements, but HashMap doesn't return anything for t
.
How do I make it work ?
Upvotes: 41
Views: 51449
Reputation: 5206
Problems
As others have said, Java arrays inherit .hashcode()
and .equals()
from Object, which uses a hash of the address of the array or object, completely ignoring its contents. The only way to fix this is to wrap the array in an object that implements these methods based on the contents of the array. This is one reason why Joshua Bloch wrote Item 25: "Prefer lists to arrays." Java provides several classes that do this or you can write your own using Arrays.hashCode()
and Arrays.equals()
which contain correct and efficient implementations of those methods. Too bad they aren't the default implementations!
Whenever practical, use a deeply unmodifiable (or immutable) class for the keys to any hash-based collection. If you modify an array (or other mutable object) after storing it as a key in a hashtable, it will almost certainly fail future .get()
or .contains()
tests in that hashtable. See also Are mutable hashmap keys a dangerous practice?
Specific Solution
// Also works with primitive: (boolean... items)
public static List<Boolean> bList(Boolean... items) {
List<Boolean> mutableList = new ArrayList<>();
for (Boolean item : items) {
mutableList.add(item);
}
return Collections.unmodifiableList(mutableList);
}
ArrayList implements .equals()
and .hashCode()
(correctly and efficiently) based on its contents, so that every bList(false, false)
has the same hashcode as, and will be equal to every other bList(false, false)
.
Wrapping it in Collections.unmodifiableList()
prevents modification.
Modifying your example to use bList() requires changing just a few declarations and type signatures. It is as clear as, and almost as brief as your original:
public class main {
public static HashMap<List<Boolean>, Integer> h;
public static void main(String[] args){
List<Boolean> a = bList(false, false);
h = new HashMap<>();
h.put(a, 1);
if(h.containsKey(a)) System.out.println("Found a");
List<Boolean> t = bList(false, false);
if(h.containsKey(t)) System.out.println("Found t");
else System.out.println("Couldn't find t");
}
}
Generic Solution
public <T> List<T> bList(T... items) {
List<T> mutableList = new ArrayList<>();
for (T item : items) {
mutableList.add(item);
}
return Collections.unmodifiableList(mutableList);
}
The rest of the above solution is unchanged, but this will leverage Java's built-in type inference to work with any primitive or Object (though I recommend using only with immutable classes).
Library Solution
Instead of bList()
, use Google Guava's ImmutableList.of()
, or my own Paguro's vec()
, or other libraries that provide pre-tested methods like these (plus immutable/unmodifiable collections and more).
Inferior Solution
This was my original answer in 2017. I'm leaving it here because someone found it interesting, but I think it's second-rate because Java already contains ArrayList and Collections.unmodifiableList() which work around the problem. Writing your own collection wrapper with .equals() and .hashCode() methods is more work, more error-prone, harder to verify, and therefore harder to read than using what's built-in.
This should work for arrays of any type:
class ArrayHolder<T> {
private final T[] array;
@SafeVarargs
ArrayHolder(T... ts) { array = ts; }
@Override public int hashCode() { return Arrays.hashCode(array); }
@Override public boolean equals(Object other) {
if (array == other) { return true; }
if (! (other instanceof ArrayHolder) ) {
return false;
}
//noinspection unchecked
return Arrays.equals(array, ((ArrayHolder) other).array);
}
}
Here is your specific example converted to use ArrayHolder:
// boolean[] a = {false, false};
ArrayHolder<Boolean> a = new ArrayHolder<>(false, false);
// h = new HashMap<boolean[], Integer>();
Map<ArrayHolder<Boolean>, Integer> h = new HashMap<>();
h.put(a, 1);
// if(h.containsKey(a)) System.out.println("Found a");
assertTrue(h.containsKey(a));
// boolean[] t = {false, false};
ArrayHolder<Boolean> t = new ArrayHolder<>(false, false);
// if(h.containsKey(t)) System.out.println("Found t");
assertTrue(h.containsKey(t));
assertFalse(h.containsKey(new ArrayHolder<>(true, false)));
I used Java 8, but I think Java 7 has everything you need for this. I tested hashCode and equals using TestUtils.
Upvotes: 5
Reputation: 2991
Map
implementations relies on key's equals
and hashCode
methods. Arrays in java are directly extends from Object
, they use default equals
and hashCode
of Object
which only compares identity
.
If I were you, I would create a class Key
class Key {
private final boolean flag1;
private final boolean flag2;
public Key(boolean flag1, boolean flag2) {
this.flag1 = flag1;
this.flag2 = flag2;
}
@Override
public boolean equals(Object object) {
if (!(object instanceof Key)) {
return false;
}
Key otherKey = (Key) object;
return this.flag1 == otherKey.flag1 && this.flag2 == otherKey.flag2;
}
@Override
public int hashCode() {
int result = 17; // any prime number
result = 31 * result + Boolean.valueOf(this.flag1).hashCode();
result = 31 * result + Boolean.valueOf(this.flag2).hashCode();
return result;
}
}
After that, you can use your key with Map
:
Map<Key, Integer> map = new HashMap<>();
Key firstKey = new Key(false, false);
map.put(firstKey, 1);
Key secondKey = new Key(false, false) // same key, different instance
int result = map.get(secondKey); // --> result will be 1
Reference: Java hash code from one field
Upvotes: 6
Reputation: 4487
You could use a library that accepts an external hashing and comparing strategy (trove).
class MyHashingStrategy implements HashingStrategy<boolean[]> {
@Override
public int computeHashCode(boolean[] pTableau) {
return Arrays.hashCode(pTableau);
}
@Override
public boolean equals(boolean[] o1, boolean[] o2) {
return Arrays.equals(o1, o2);
}
}
Map<boolean[], T> map = new TCustomHashMap<boolean[],T>(new MyHashingStrategy());
Upvotes: 1
Reputation: 61128
Map uses equals()
to test if your keys are the same.
The default implementation of that method in Object
tests ==
, i.e. reference equality. So, as your two arrays are not the same array, equals
always returns false.
You need to make the map call Arrays.equals
on the two arrays to check for equality.
You can create an array wrapper class that uses Arrays.equals
and then this will work as expected:
public static final class ArrayHolder<T> {
private final T[] t;
public ArrayHolder(T[] t) {
this.t = t;
}
@Override
public int hashCode() {
int hash = 7;
hash = 23 * hash + Arrays.hashCode(this.t);
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final ArrayHolder<T> other = (ArrayHolder<T>) obj;
if (!Arrays.equals(this.t, other.t)) {
return false;
}
return true;
}
}
public static void main(String[] args) {
final Map<ArrayHolder<Boolean>, Integer> myMap = new HashMap<>();
myMap.put(new ArrayHolder<>(new Boolean[]{true, true}), 7);
System.out.println(myMap.get(new ArrayHolder<>(new Boolean[]{true, true})));
}
Upvotes: 1
Reputation: 1251
You could create a class that contains the array. Implements the hashCode() and equals() methods for that class, based on values:
public class boolarray {
boolean array[];
public boolarray( boolean b[] ) {
array = b;
}
public int hashCode() {
int hash = 0;
for (int i = 0; i < array.length; i++)
if (array[i])
hash += Math.pow(2, i);
return hash;
}
public boolean equals( Object b ) {
if (!(b instanceof boolarray))
return false;
if ( array.length != ((boolarray)b).array.length )
return false;
for (int i = 0; i < array.length; i++ )
if (array[i] != ((boolarray)b).array[i])
return false;
return true;
}
}
You can then use:
boolarray a = new boolarray( new boolean[]{ true, true } );
boolarray b = new boolarray( new boolean[]{ true, true } );
HashMap<boolarray, Integer> map = new HashMap<boolarray, Integer>();
map.put(a, 2);
int c = map.get(b);
System.out.println(c);
Upvotes: 2
Reputation: 15278
It is not possible to do this with arrays, as any two different arrays don't compare equals
, even if they have the same elements.
You need to map from container class, for example ArrayList<Boolean>
(or simply List<Boolean>
. Perhaps BitSet
would be even more appropriate.
Upvotes: 13
Reputation: 95498
You cannot do it this way. Both t
and a
will have different hashCode()
values because the the java.lang.Array.hashCode()
method is inherited from Object
, which uses the reference to compute the hash-code (default implementation). Hence the hash code for arrays is reference-dependent, which means that you will get a different hash-code value for t
and a
. Furthermore, equals
will not work for the two arrays because that is also based on the reference.
The only way you can do this is to create a custom class that keeps the boolean
array as an internal member. Then you need to override equals
and hashCode
in such a way that ensures that instances that contain arrays with identical values are equal and also have the same hash-code.
An easier option might be to use List<Boolean>
as the key. Per the documentation the hashCode()
implementation for List
is defined as:
int hashCode = 1;
Iterator<E> i = list.iterator();
while (i.hasNext()) {
E obj = i.next();
hashCode = 31*hashCode + (obj==null ? 0 : obj.hashCode());
}
As you can see, it depends on the values inside your list and not the reference, and so this should work for you.
Upvotes: 35
Reputation: 45060
boolean[] t;
t = a;
If you give this, instead of boolean[] t = {false, false};
, then you'll get the desired output.
This is because the Map
stores the reference
as the key
, and in your case, though t
has the same values, it doesn't have the same reference as a
.
Hence, when you give t=a
, it'll work.
Its very similar to this:-
String a = "ab";
String b = new String("ab");
System.out.println(a==b); // This will give false.
Both a
& b
hold the same value, but have different references. Hence, when you try to compare the reference using ==
, it gives false
.
But if you give, a = b;
and then try to compare the reference
, you'll get true
.
Upvotes: 1
Reputation: 4956
Probably it is because equals() method for Array returns acts different then you expect. You should think about implementing your own collecting and override equals() and hashCode().
Upvotes: 1