javaequalseffective-javacanonical-form

Canonical form of field


I'm studying Effective Java, Item 8 (Obey the general contract when overriding equals). It has been explained quite clearly by the author, but still some parts are not that much elaborated.

For this example, he considers a class CaseInsensitiveString defined as :

public final class CaseInsensitiveString {
    private final String s;

    public CaseInsensitiveString(String s) {
        if (s == null)
            throw new NullPointerException();
        this.s = s;
    }

    // Broken - violates symmetry!
    @Override
    public boolean equals(Object o) {
        if (o instanceof CaseInsensitiveString)
            return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
        if (o instanceof String) // One-way interoperability!
            return s.equalsIgnoreCase((String) o);
        return false;
    }
    // ... // Remainder omitted
}

In the end of the article, he says :

For some classes, such as CaseInsensitiveString above, field comparisons are more complex than simple equality tests. If this is the case, you may want to store a canonical form of the field, so the equals method can do cheap exact comparisons on these canonical forms rather than more costly inexact compar- isons. This technique is most appropriate for immutable classes (Item 15); if the object can change, you must keep the canonical form up to date.

I searched for this term and found that it basically means a standard representation of something, like absolute path without any symbolic links for a file in a directory. But I'm unable to understand the use of 'canonical' form for this class, which would help here. Any suggestions?


Solution

  • I think in this particular example, a canonical form might be storing the lowercase or uppercase version of the String and doing comparisons on that.

    private final String s;
    
    public CaseInsensitiveString(String s) {
        //for real code probably use locale version
        this.s = s.toLowerCase();
    }
    

    This makes the equality comparison cheaper because we can do exact string comparisons instead of the more expensive equalsIgnoreCase

    // Broken - violates symmetry!
    @Override
    public boolean equals(Object o) {
        if (o instanceof CaseInsensitiveString)
            return s.equals(((CaseInsensitiveString) o).s);
        if (o instanceof String) // One-way interoperability!
            return s.equals((String) o);
        return false;
    }