0

So I'm solving Day 24 of Advent of Code and I run into a strange problem with Java and/or Eclipse (most likely Eclipse). My algorithm is correct but it doesn't return the correct result (I know this because I've, since, submitted a correct solution). It appears that my HashMap isn't working properly--I know this is a problem with hashCode() (because I've since, quite accidentally, fixed it).

Here is the original (eclipse generated) hashCode() method:

@Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + (evenRow ? 1231 : 1237);
        result = prime * result + x;
        result = prime * result + y;
        return result;
    }

So here is my thought: x and y need to be "bigger" so I try long's. I do this and get wildly different (incorrect) results (with an updated, generated by eclipse, hashCode() method). I do a bunch of other stuff, including changing the values to BigInteger...none of which makes sense because there's no way I should need that much precision (for this particular problem). Having said that, the BigInteger implementation gives the same result as the int implementation.

Further confusion arises when I try to manually create a hashCode() method, like so:

public int hashCode(){
    return x + y;
}

Not great but it should (theoretically) work, right? If the two objects are equal, then x + y should be equal then it will check the equals(...) method for equality (which I'm virtually certain is correctly implemented via eclipse). This returns yet even different results.

Changing the hashCode() method changes the results I get. There are two reasons I can think of for getting these disparate effects:

  1. hashCode() is clearly returning collisions--I don't see why this would be a problem as long as hashCode() is consistent with equals(...) which I think eclipse's implementation satisfies.
  2. I'm using a mutable object as a key (I suspect this is the culprit). So instead of creating multiple new object keys, I have a single object that I change as needed for lookup.

I don't see why the 2nd should be a problem because the contract of hashCode() is:

Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.

But provided no information used in equals comparisons on the object is modified.? If I change any field member, it has been modified so should not be expected to return the same result!

So what I don't understand: is why is the hashCode() method interfering with the proper function of a HashMap. I mean don't get me wrong--if the two hashCodes for the same object aren't equal, that's going to be a problem, but I don't understand why two same hashCode for differing objects (via equals(...)) should create a problem.

If it helps, this is the equals(...) method generated by eclipse:

public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        HexCoord other = (HexCoord) obj;
        if (evenRow != other.evenRow)
            return false;
        if (x != other.x)
            return false;
        if (y != other.y)
            return false;
        return true;
    }

Edit: For what it's worth, changing the hashCode() function to this (which I did quite randomly) "solves" the problem:

public int hashCode() {
        final int prime = 341; // <-- changed 31 to 341
        int result = 1;
        result = prime * result + (evenRow ? 1231 : 1237);
        result = prime * result + x;
        result = prime * result + y;
        return result;
}

Further Edit: added code example: (I really think this is going to introduce un-useful questions...I've questioned whether or not my methods here are correct--and they appear to be so because I've gotten a correct result--it's the HashMap getting and putting of objects that seems to be the problem)

Some Explanation:

Here is coord.set (so it doesn't just copy the actual object--it recreates it):

public void set(final HexCoord coord) {
        this.x = coord.x; // <-- these are int's
        this.y = coord.y;
        this.evenRow = coord.evenRow;// <-- this is a boolean
    }

Here is the getHexTile(...) method:

private static HexTile getHexTile(Map<HexCoord, HexTile> tiles,
            HexCoord coord) {
        HexTile tile = tiles.get(coord);
        if (tile == null) {
            tile = new HexTile(coord);
            tiles.put(coord, tile);
        }

        return tile;
    }

with new HexTile(coord):

    public HexTile(final HexCoord coord) {
        this.coord = coord.copy();
    }

and copy:

    public HexCoord copy() {
        final HexCoord copy = new HexCoord();
        copy.set(this); // <-- rights the values to the new copy

        return copy;
    }
            // set black count
            tiles.values().stream()
                    .filter(tile -> tile.getColor() == HexTile.Color.black)
                    .collect(Collectors.toList()).stream().forEach(tile -> {
                        coord.set(tile.coord);

                        coord.moveE();
                        HexTile neighbor = getHexTile(tiles, coord);
                        neighbor.addBlackNeighbor();
                        coord.moveW();

                        coord.moveW();
                        neighbor = getHexTile(tiles, coord);
                        neighbor.addBlackNeighbor();
                        coord.moveE();

                        coord.moveNE();
                        neighbor = getHexTile(tiles, coord);
                        neighbor.addBlackNeighbor();
                        coord.moveSW();

                        coord.moveSW();
                        neighbor = getHexTile(tiles, coord);
                        neighbor.addBlackNeighbor();
                        coord.moveNE();

                        coord.moveNW();
                        neighbor = getHexTile(tiles, coord);
                        neighbor.addBlackNeighbor();
                        coord.moveSE();

                        coord.moveSE();
                        neighbor = getHexTile(tiles, coord);
                        neighbor.addBlackNeighbor();
                    });

            tiles.values().parallelStream().forEach(tile -> {
                final int blackCount = tile.getBlackCount();
                if (tile.getColor() == HexTile.Color.black) {
                    if (blackCount == 0 || blackCount > 2) {
                        tile.flip();
                    }
                } else if (blackCount == 2) {
                    tile.flip();
                }

                tile.resetCount();
            });

Resolution: For anyone still interested the fix was simple: I was already cloning the HexCoord object when I created the HexTile. The "key" (get it, key...as in key-value pair), was making sure that the Map held distinct keys by making sure that the map's keys came from the newly created HexTile:

private static HexTile getHexTile(Map<HexCoord, HexTile> tiles,
            HexCoord coord) {
        HexTile tile = tiles.get(coord);
        if (tile == null) {
            tile = new HexTile(coord);
            tiles.put(tile.coord, tile);
        }

        return tile;
    }
Jared
  • 946
  • 5
  • 8
  • @dreamcrash I expect that if I have a key object, o1, to give me a value v1, then I expect that if I _then_ modify that _same_ object, o1, such that that modified object should return value 2, v2, that my HashMap does indeed return the v2, given the modified o1. – Jared Dec 24 '20 at 09:21
  • I would further add, that I expect if I have two keys: k1 and k2 (such the k1 != k2 via equals method) that even if k1 and k2 have the same hashCode, that they will be seen as different keys via the HashMap. – Jared Dec 24 '20 at 09:31
  • Yes, and if I understand correctly you should not have issues with that. For instance https://www.codepile.net/pile/APQ0AXx7 works fine – dreamcrash Dec 24 '20 at 09:48
  • 1
    "*I don't see why the 2nd should be a problem*". You are looking at the wrong contract: ["behavior of a map is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is a key in the map"](https://docs.oracle.com/javase/8/docs/api/java/util/Map.html) – Michael Dec 24 '20 at 10:17
  • @Michael Ah! Thank you--that answers my question! Weird that I was able to even solve the problem, lol. I thought about making things immutable but really did not want to...I guess I should have...stilly not sure if that's a fault of mine or of Java 8's... – Jared Dec 24 '20 at 10:32
  • @Michael I would point out that I also tried this by _explicitly_ defining all of the maps as [HashMaps](https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html) and that also didn't work (don't know why that would make a difference and it didn't). – Jared Dec 24 '20 at 10:41
  • @Michael Am I the only one that thinks it's weird to create a hash table that doesn't rely on the key's actual hash value??? I mean seriously, I'm sitting here thinking that Java 8's hash functionality relies on the hashCode....but it seems like it doesn't. – Jared Dec 24 '20 at 10:45
  • 1
    @Jared It does rely on the hash value. The problem for you is that the hash value is computed at the time of insertion, to determine what bucket to put the item into. If the item's hash changes, it's potentially (and very likely) going to exist in the wrong bucket when you check whether the key is present - it will check the wrong bucket, and so may return false negatives (i.e. item is present in the map, but the map purports that it isn't). The map has no way to know when a mutable object changes, so it has no way to know to rehash it. – Michael Dec 24 '20 at 11:00
  • @Jared You can see my answer on this question for a high-level explanation of how HashMap works: https://stackoverflow.com/a/43911638/1898563 – Michael Dec 24 '20 at 11:00
  • @Michael but the key never changes (in any way--that's what you say as the "item"), Example: `coord.moveE()`, so `coord` has changed its value now and therefore key (and value) has changed--it gets a different `HexTile`. Now after that I call `coord.moveW()` which "undo's" the "move east", now the get of _that_ coord should be the original (root). – Jared Dec 24 '20 at 12:08
  • @Michael Thanks for responding. I realize now the problem is with the equality check (because HashMap cannot faithfully check for equality if it doesn't "remember" the value of the initial key that was mapped). So basically the answer to my question (which seems kind of obvious--even from the start) is that my "fix" _was_ indeed pure luck and I just so happened to create a hash function that generated unique hash values for the given input. – Jared Dec 24 '20 at 20:12

0 Answers0