JDK-4214818 : equals() symmetry and equals()/hashCode() compatibility should be enforced
  • Type: Enhancement
  • Component: tools
  • Sub-Component: javac
  • Affected Version: 1.2.0
  • Priority: P5
  • Status: Closed
  • Resolution: Won't Fix
  • OS: generic
  • CPU: generic
  • Submitted: 1999-02-25
  • Updated: 2019-08-06
  • Resolved: 1999-12-22
Related Reports
Relates :  
Description

Name: igT44549			Date: 02/24/99


Hello.  This isn't actually a bug report -- it's more of a 
feature request.  However, I noticed that many of the "bugs" 
submitted here are actually feature requests, so I am assuming 
that this is OK to do.  

The Language Specification states:  

"The general contract of 'equals' is that it implements an 
equivalence realation: [....] It is *symmetric*: for any 
reference values 'x' and 'y', 'x.equals(y)' should return 
'true' if and only if "y.equals(x)' returns 'true'.

The Language Specification also states:

"If two objects are equal according to the 'equals' method, then 
calling the 'hashCode' method on each of the two objects must 
produce the same result."

I strongly agree with both of these specifications.  
Unfortunately, there is nothing in the compiler to enforce these 
rules.  As a result, these rules are very often violated (see the 
example below from the "official" Java tutorial).  This leads to 
annoying bugs.   

I don't remember enough discrete math to know if it is possible 
for the compiler to check if a given 'equals' method is 
inherently symmetric.  Since you have some sharp people there at 
Sun, I thought that you might be able to look into this.  If it 
is possible to check whether or not an 'equals' method is 
symmetric, please build this functionality into the compiler.

If it is not possible to make this check, please consider the 
following:  I have noticed that, in most cases, non-symmetric 
'equals' methods contain the keyword 'instanceof', and, 
conversely, every 'equals' method I have ever seen with the 
keyword 'instanceof' in it has been unsymmetric (see example 
below).  Would it be possible to have the compiler flag a 
warning if it detects the keyword 'instanceof' in an 'equals' 
method?

Similarly, it would be nice if the compiler would check for 
hashCode/equals compatibility.  Once again, I don't know if this 
is possible, but, if it is possible, please include this feature.

If it is not possible to do anything to coerce people to follow 
these rules, it might be a good idea to take them out of the 
Language Specification.  Although, I do believe that they are 
good rules, the simple fact is that most people don't pay 
attention to them.

EXAMPLE:

Consider the class 'Name' from the "official" Java tutorial:

http://java.sun.com/docs/books/tutorial/collections/interfaces/example-1dot2/Name.java

or Campione, et al, _The Java Tutorial Continued_, pages 55 and 721:

import java.util.*;

public class Name implements Comparable {
    private String  firstName, lastName;

// <snip>

    public boolean equals(Object o) {
        if (!(o instanceof Name))
            return false;
        Name n = (Name)o;
        return n.firstName.equals(firstName) &&
               n.lastName.equals(lastName);
    }

    public int hashCode() {
        return 31*firstName.hashCode() + lastName.hashCode();
    }

// <snip>

} 


Note that this class is not final, so the author of class 'Name' 
should expect that a user might create a subclass of 'Name'.  
Using the same style of implementing 'equals', we end up with:

import java.io.PrintWriter;

public class NameWithTitle extends Name {

    private String title;

    public NameWithTitle( String title, String first, String last ) {
        super( first, last );
        this.title = title;
    }

    public String title() {
        return title;
    }

    public boolean equals( Object o ) {
        if ( !( o instanceof NameWithTitle ) )
            return false;
        NameWithTitle n = (NameWithTitle) o;
        return n.firstName().equals( this.firstName() ) &&
               n.lastName().equals( this.lastName() ) &&
               n.title().equals( this.title() );
    }

    public int hashCode() {
        return super.hashCode() + 17*title.hashCode();
    }

    public String toString() {
        return title + " " + super.toString();
    }

    public int compareTo( Object o ) {
        NameWithTitle n = (NameWithTitle) o;
        int comp = lastName().compareTo( n.lastName() );
        if ( 0 == comp ) {
            comp = firstName().compareTo( n.firstName() );
            if ( 0 == comp ) {
                comp = title().compareTo( n.title() );
            }
        }
        return comp;
    }

    public static void main( String [] argv ) {
        PrintWriter outwriter = new PrintWriter( System.out, true );
        
        Name gosling = new Name( "James", "Gosling" );
        Name drG = new NameWithTitle( "Dr.", "James", "Gosling" );
         
        outwriter.println();
        outwriter.println( "*** our instances:" );
        outwriter.println();
        outwriter.print( "gosling:  " );
        outwriter.println( gosling );
        outwriter.print( "drG:  " );
        outwriter.println( drG );
        
        outwriter.println();
        outwriter.println( "*** Test to see if equals is symmetric:" );
        outwriter.println();
        outwriter.print( "gosling.equals( drG ):  " );
        outwriter.println( gosling.equals( drG ) );
        outwriter.print( "drG.equals( gosling ):  " );
        outwriter.println( drG.equals( gosling ) );

        outwriter.println();
        outwriter.println( "*** Test hashCode/equals compatibility:" );
        outwriter.println();
        outwriter.print( "gosling.equals( drG ):  " );
        outwriter.println( gosling.equals( drG ) );
        outwriter.print( "gosling.hashCode() == drG.hashCode():  " );
        outwriter.println( gosling.hashCode() == drG.hashCode() ); 
        outwriter.println();
        outwriter.print( "drG.equals( gosling ):  " );
        outwriter.println( drG.equals( gosling ) );
        outwriter.print( "drG.hashCode() == gosling.hashCode():  " );
        outwriter.println( drG.hashCode() == gosling.hashCode() );

    }

}

If we run the main in this class we get:

>java NameWithTitle

*** our instances:

gosling:  James Gosling
drG:  Dr. James Gosling

*** Test to see if equals is symmetric:

gosling.equals( drG ):  true
drG.equals( gosling ):  false

*** Test hashCode/equals compatibility:

gosling.equals( drG ):  true
gosling.hashCode() == drG.hashCode():  false

drG.equals( gosling ):  false
drG.hashCode() == gosling.hashCode():  false

>

Note that 'hashCode' and 'equals' are incompatible and 'equals' 
is not symmetric.  Both of these problems result from the 
inappropriate use of 'instanceof' in the 'equals' method.

-- Dave Jones
(Review ID: 54699)
======================================================================

Comments
Note that the compile-time checks in question were added to javac's -Xlint facility under JDK-6563143 and its follow-up refinements.
06-08-2019

EVALUATION The properties that the user would like the compiler to verify are, unfortunately, undecidable in a language as expressive as Java (or any other reasonable general-purpose programming language). For this reason, the compiler cannot check them statically at compile-time. A heuristic approach, suggested as an alternative, might be appropriate in an optional style-checking tool, but these have no place in the compiler itself -- certainly not in the reference implementation. A proposal for adding an 'assert' facility is presently being considered. It would be possible to catch many errors of this kind using *runtime* checks, which would be made more manageable via that facility. The properties required of equality and hashCode are reasonable expectations on the part of users of these methods, and it would be inappropriate to drop the requirements simply because it is possible (or common) to violate them. In particular, the requirement on hashCode allows the definition of generic hash table classes that work for reference type. william.maddox@Eng 1999-12-21
21-12-1999