0

I have a package private interface that's able to identify equality based on captured synthetic argument references.

interface SynthEqual {
    default Object synthParamAt(int paramIndex) {
        try {
            return getClass().getDeclaredFields()[paramIndex].get(this);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
   default<S> boolean equalTo(int paramIndex, S other) {
       return synthParamAt(paramIndex).equals(other);
   }

   //Edit:
   // I realized this is a better option, instead of exposing the synthetic param:
   default boolean equalTo(int at, SynthEqual that) {
      return that != null && that.synthParamAt(at).equal(synthParamAt(at));
   }
   // and making synthParamAt private with Java9
}

This interface is used for when lambda captures more than 2 arguemnts, but I am interested only on the equality of one of them...

If you .hash this synthetic arguments they are different... EVEN IF they come from the same reference source.

But if you == Or .equal() the result comes as true.

This is why I rather NOT @Override equals or hashcode, instead I always use my plain custom equalizer methods..., BUT if you wish/need... you could... in theory do this:

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        AClass<?> that = (AClass<?>) o;
        return that.lambdaFieldA.equalTo(0, this.lambdaFieldA.synthParamAt(0)) && Objects.equals(fieldB, that.fieldB);
    }

As you can see this composed class is properly overriding equals, but my lambda is using it's custom synthetic equality test.

as for hashCode... well I don't know how would that be overridden... I believe it is not possible to override hash in such a way that synthetic arguments can be inferred. Up to now, this has never been an issue...

The real problem comes when dealing with Maps.

When we look at the putVal() method of ConcurrenthashMap we can see that they are using the hash function to search for a match in the Node table.

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        //Here
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh; K fk; V fv;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            // Here the fetch
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// .......
// A few lines bellow:
else if (onlyIfAbsent // check first node without acquiring lock
                     //Here compares
                     && fh == hash
                     && ((fk = f.key) == key || (fk != null && key.equals(fk))) // FINALLY!!!
                     && (fv = f.val) != null)
                return fv;
.....
static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }

I can see, that .equals is performed ONLY After a hash search has found a match... So I believe I need to fix my hash override for my equals to be able to test.

How could I override the K key in such a way that it properly deals with synthetic argument equalities under the rules of a ConcurrentHashMap?

Delark
  • 1,141
  • 2
  • 9
  • 15

1 Answers1

0

Ok this is a partial answer, I realized my question was kind of lazy, so I decided to test things with more depth. The only thing missing was this:

default int hashAt(int at) {
            return paramAt(at).hashCode();
        }

So that the resulting interface:

interface SynthEqual {
    private Object paramAt(int at) {
        try {
            return getClass().getDeclaredFields()[at].get(this);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    default <S> boolean equalTo(int at, S arg) {
        return paramAt(at).equals(arg);
    }

    default<S> boolean equalTo(int at, SynthEqual that) {
        return that != null && paramAt(at).equals(that.paramAt(at));
    }

    default int hashAt(int at) {
        return paramAt(at).hashCode();
    }
}

The only obligatory requirement for a K key then is to NOT be a lambda instance in itself.

The key object could then, by overriding equals() and hasCode(), make use of our lambda default tests and infer their inner synthetic arguments equality.

static class LambdaWrapper {
    final HeyThere lambda;

    LambdaWrapper(HeyThere lambda) {
        this.lambda = lambda;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        LambdaWrapper that = (LambdaWrapper) o;
        return lambda.equalTo(0, that.lambda);
    }

    @Override
    public int hashCode() {
        return lambda.hashAt(0);
    }
}
Delark
  • 1,141
  • 2
  • 9
  • 15