0

I'm currently working on a TD game with a map editor. Now obviously, you can save and load these maps (or should be able to, at least). Problem is: at some point I'm calling .get() on a HashMap. Unfortunately, the keys that should be the same (logic-wise) are not the same object (in terms of reference), and, according to my previous google research, overriding their .equals method isn't sufficient, since they still return different hashes with .hashCode() (I verified that, they DO return different hashes, while .equals does return true).
(On a side note, that's rather confusing, since the javadoc of HashMap.get(key) only states that they have to be equal)

More specifically, the HashMap contains instances of a class Path of mine as keys, and should return the corresponding list of enemies (= value).

short version of Path (without getters etc.):

public class Path
{
    private List<Tile> tiles = new ArrayList<>();
    @Override
    public boolean equals(Object obj) {
        //code comparing the two paths
    }
    @Override
    public int hashCode() {
        //what I still need to implement. ATM, it returns super.hashCode()
    }
}

public class Tile
{
    private int x;
    private int y;
    //constructor
    //overrides equals
    //getters & some convenience methods
}

Now if two Paths are equal, I'd like them to return the same hash code, so that the HashMap returns the correct list of enemies. (I'll make sure not two identical paths can be added).

Now my question:

Do you suggest

  1. using some external library to generate a hash
  2. that I write my own implementation of calculating a hash, or
  3. something else

?

Note that I'd prefer to avoid changing the HashMap to some other type of map, if that would even help solve the problem.

ROMANIA_engineer
  • 54,432
  • 29
  • 203
  • 199
PixelMaster
  • 895
  • 10
  • 28
  • 1
    your `3)` could probably be using your `IDE` or an `IDE` in generell to make it generate the `hashcode` – SomeJavaGuy Dec 14 '16 at 13:54
  • 1
    you can make your hashcode as simple or complex as you like. it is meant as a quick "are these two vaguely simliar" check - to eliminate as many wildly different matches as possible before then diving into your `equals` method for the ones that survive the hashcode check. So, depending on your system, `return tiles.size()` might even be appropriate! – David Lavender Dec 14 '16 at 13:59
  • 2
    worth adding: one mistake people make is to make `hashcode` overly complex. there's no point having it if it's going to just be as complicated as the actual `equals` method. like I said: it's meant as a "quick check". – David Lavender Dec 14 '16 at 14:00
  • 1
    You should give the `hashCode` contract a read https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#hashCode-- – Florian Link Dec 14 '16 at 14:00
  • 1
    @MrSpoon *"you can make your hashcode as simple or complex as you like"* - you have to respect the [`hashCode` contract](https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#hashCode--). – lexicore Dec 14 '16 at 14:05
  • 1
    @MrSpoon *"it is meant as a quick "are these two vaguely simliar" check"* - not only. Hash codes are used in hash maps and co. (which the question is about) to build hash tables. So it's not only for a quick check. – lexicore Dec 14 '16 at 14:06
  • 1
    @MrSpoon *"So, depending on your system, return tiles.size() might even be appropriate!"* - this is hardly a a good hash function as it is very likely to produce collisions. – lexicore Dec 14 '16 at 14:07
  • 1
    @lexicore it's the same principal: you're making quick look-up buckets based on the hashcode. if `tiles.size()` is used as the hashcode, you might get 10 buckets rather than the 100 buckets you'd get if you wrote a more complex hashcode. But overall it would probably be quicker for most systems. – David Lavender Dec 14 '16 at 14:08
  • @MrSpoon thanks for the answer - I wasn't aware that the main purpose of hashes was just for a vague comparison, i.e. for quicker hashmaps / -tables. – PixelMaster Dec 14 '16 at 14:29
  • @lexicore @MrSpoon So if I were to give a f*ck about performance and fast hashmaps, i could just `return 0` for any instance of path? That does meet the requirements for hashes that are given in the javadoc, after all. – PixelMaster Dec 14 '16 at 14:37
  • 1
    @Misteradi1 yep, that approach is discussed here http://stackoverflow.com/a/14505443/1777072. you can normally do something slightly better than that though to at least give yourself a few buckets, rather than just that one giant "0" bucket. but, personally, i don't resort to complicated (or autogenerated) hashcode functions until there's a real need for that extra bit of performance: it falls under "premature optimisation" in my book. – David Lavender Dec 14 '16 at 15:14
  • @MrSpoon I'll probably just use 0. Considering that my Path class ONLY has one list of tiles, and that list can change, i can't really calculate anything. A hash value set upon initializing it, say, the ms since 1st Jan 1970, won't work either, since the path will be copied at some point, but should still work. Thanks though! :) – PixelMaster Dec 14 '16 at 15:27

2 Answers2

2

You definitely do need to implement your hashCode consistent with equals. IDEs often do decent job generating hashCode and equals. Also consider Objects.equals(...) and Objects.hash(...).

One warning about using Path as keys in the HashMap. You will have to make the class immutable to make it work reliably. Or at least make sure that hashCode of the key does not change. Otherwise you may not able to get you data back even with the same or equal key.

lexicore
  • 42,748
  • 17
  • 132
  • 221
  • concerning the immutability of `Path`: I hope I won't need to do that, since usually, i'll be calling `.get` with a path retrieved via a reference. This error just occurs after loading a previously created map. – PixelMaster Dec 14 '16 at 14:20
  • 1
    @Misteradi1 If hash code of the path changes you will not be able to retrieve data from the hash map even with *the very same* instance of the key. Hash map will once again get the hash code of the key and use it to locate bucket with the value. If hash code changes, it won't find the right bucket. Even if the key is the very same instance. Careful with that. – lexicore Dec 14 '16 at 14:28
1

The List has a useful method which conveniently is also named list.hashCode(). This will compute the hashCode of all the elements inside the list. So you also have to implement the hashCode for Tile which probably consist of some primitive fields or such.

e.g.

 @Override
    public int hashCode() {
         return tiles != null ? tiles.hashCode() : 0;
    }

See the docs here

int hashCode()
Returns the hash code value for this list. The hash code of a list is defined to be the result of the following calculation:

  int hashCode = 1;
  for (E e : list)
      hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());

This ensures that list1.equals(list2) implies that list1.hashCode()==list2.hashCode() for any two lists, list1 and list2, as required by the general contract of Object.hashCode().

Murat Karagöz
  • 35,401
  • 16
  • 78
  • 107
  • `Tile` is in fact pretty primitive, it just saves 2 integer values (as seen above). Thanks for the tip about list.hashCode()! – PixelMaster Dec 14 '16 at 14:14
  • Your first block of code can be shortened to [Objects.hashCode(tiles)](http://docs.oracle.com/javase/8/docs/api/java/util/Objects.html#hashCode-java.lang.Object-). – VGR Dec 14 '16 at 14:46