Search code examples
javalambdajava-8equals-operator

Java 8, equals, == and Lambdas


(actual question at the end)

Hello World! I've got some disturbing thing happening here that I cannot wrap my head around...

Laszy as I am, I hate re-implementing the stupid euqals() emthods over and over again; also the hashCode() methods, but still focus the "equality" on selected member variables. I could also reduce the implementation by using a default interface that checks class instances, or using reflection, but I thought I'd got with this static utility method and a labmda:

public class StackOverflow_TestEq {

    static public interface JcLambda_pG1_pG1_rBool<T> {
        boolean test(T p1, T p2);
    }

    static public <T> boolean equals_internal(final T pThisObject, final Object pOtherObject, final JcLambda_pG1_pG1_rBool<T> pEqualizer) {
        if (pThisObject == null) return pOtherObject == null;
        if (pOtherObject == null) return false;

        final Class<? extends Object> thisClass = pThisObject.getClass();
        if (!thisClass.isInstance(pOtherObject)) return false;

        @SuppressWarnings("unchecked") final T other = (T) pOtherObject;
        final boolean res = pEqualizer.test(pThisObject, other);
        //      System.out.println("Testing " + pThisObject + " vs " + other + " => " + res);
        return res;
    }

    /*
     * TEST DEFINES
     */

    static private interface CNameable {
        String getName();
    }
    static private class CA implements CNameable {
        String  name    = "Bier";
        @Override public String toString() {
            return getClass().getSimpleName() + "." + name;
        }
        @Override public String getName() {
            return name;
        }
    }
    static private class CB implements CNameable {
        String  name    = "Schnaps";
        @Override public String toString() {
            return getClass().getSimpleName() + "." + name;
        }
        @Override public String getName() {
            return name;
        }
    }
    @SuppressWarnings("synthetic-access") static private class CC extends CA {
        @SuppressWarnings({ "hiding" }) String  name    = "Wein";
        @Override public String toString() {
            return getClass().getSimpleName() + "." + name;
        }
        @Override public String getName() {
            return name;
        }
    }
    @SuppressWarnings("synthetic-access") static private class CD extends CA {
        @SuppressWarnings({ "hiding" }) String  name    = "Bier";
        @Override public String toString() {
            return getClass().getSimpleName() + "." + name;
        }
        @Override public String getName() {
            return name;
        }
    }

    /*
     * TEST
     */

    @SuppressWarnings("synthetic-access") public static void main(final String[] args) {
        final CA a = new CA();
        final CB b = new CB();
        final CC c = new CC();
        final CD d = new CD();
        final JcLambda_pG1_pG1_rBool<CNameable> res = new JcLambda_pG1_pG1_rBool<CNameable>() {
            @Override public boolean test(final CNameable pP1, final CNameable pP2) {
                //              return pP1.getName().equals(pP2.getName());
                return pP1.getName() == pP2.getName();
            }
        };

        System.out.println("\nExplicit eq");
        System.out.println("a=a:" + equals_internal(a, a, (p1, p2) -> p1.getName().equals(p2.getName())));
        System.out.println("a=b:" + equals_internal(a, b, (p1, p2) -> p1.getName().equals(p2.getName())));
        System.out.println("a=c:" + equals_internal(a, c, (p1, p2) -> p1.getName().equals(p2.getName())));
        System.out.println("a=d:" + equals_internal(a, d, (p1, p2) -> p1.getName().equals(p2.getName())));

        System.out.println("\nExplicit ==");
        System.out.println("a=a:" + equals_internal(a, a, (p1, p2) -> p1.getName() == p2.getName()));
        System.out.println("a=b:" + equals_internal(a, b, (p1, p2) -> p1.getName() == p2.getName()));
        System.out.println("a=c:" + equals_internal(a, c, (p1, p2) -> p1.getName() == p2.getName()));
        System.out.println("a=d:" + equals_internal(a, d, (p1, p2) -> p1.getName() == p2.getName()));

        System.out.println("\nImplicit");
        System.out.println("a=a:" + equals_internal(a, a, res));
        System.out.println("a=b:" + equals_internal(a, b, res));
        System.out.println("a=c:" + equals_internal(a, c, res));
        System.out.println("a=d:" + equals_internal(a, d, res));

        System.out.println("\nExplicit eqName");
        System.out.println("a=a:" + equals_internal(a, a, (p1, p2) -> p1.name.equals(p2.name)));
        System.out.println("a=b:" + equals_internal(a, b, (p1, p2) -> p1.name.equals(p2.name)));
        System.out.println("a=c:" + equals_internal(a, c, (p1, p2) -> p1.name.equals(p2.name)));
        System.out.println("a=d:" + equals_internal(a, d, (p1, p2) -> p1.name.equals(p2.name)));

        System.out.println("\nExplicit ==name");
        System.out.println("a=a:" + equals_internal(a, a, (p1, p2) -> p1.name == p2.name));
        System.out.println("a=b:" + equals_internal(a, b, (p1, p2) -> p1.name == p2.name));
        System.out.println("a=c:" + equals_internal(a, c, (p1, p2) -> p1.name == p2.name));
        System.out.println("a=d:" + equals_internal(a, d, (p1, p2) -> p1.name == p2.name));
    }



}

Output:

Explicit eq
a=a:true
a=b:false
a=c:false
a=d:true

Explicit ==
a=a:true
a=b:false
a=c:false
a=d:true

Implicit
a=a:true
a=b:false
a=c:false
a=d:true

Explicit eqName
a=a:true
a=b:false
a=c:true
a=d:true

Explicit ==name
a=a:true
a=b:false
a=c:true
a=d:true

So... I was expecting all 5 variants (eq, ==, Implicit, eqName, ==name) to give me back the same results (true, false, false, true)...

But eqName and ==name do not. Why do they return a=c as true instead of false?

Does it have to do with the name shadowing that is happening there? So instead of accessing CC.name the comparison lambda is always accessing CA.name?


Solution

  • To analyze why the result of the following is true

    equals_internal(a, c, (p1, p2) -> p1.name == p2.name)
    

    you have to look at the signature of equals_internal:

    equals_internal(final T pThisObject, final Object pOtherObject, final JcLambda_pG1_pG1_rBool<T> pEqualizer)
    

    So in this case, T is the type of a, i.e. CA.

    Therefore the JcLambda_pG1_pG1_rBool<T> you are passing to the static method is a JcLambda_pG1_pG1_rBool<CA>, which means p1 and p2 are of type CA, so p1.name == p2.name compares the name property of the base class CA, and "Bier" == "Bier" is true due to the String pool.

    The result of equals_internal(a, c, (p1, p2) -> p1.name.equals(p2.name)) is true for the same reason (only here you don't have to rely on the String pool returning the same String instance for both "Bier", since you are using String.equals).

    In the first 3 cases the result is different, because you are using the getName() method, which is overridden in the sub-class, and returns the name property of the sub-class.

    To summarize, the different behavior is caused by that fact that, unlike methods, instance variables cannot be overridden.