Recently, I read about equals() and hashcode() in Chapter 3 of Effective Java. These two methods are easily confused by many Java programmers, so this paper introduces the usage and implementation of these two methods.
What are the uses of equals() and hashcode()?
We usually use equals() to compare whether the logical meaning of two objects is the same. For instance:
class Person { String name; int age; long id; }
We now have two Person objects, person1 and person2, so when are these two equal? For two people, we think that if their names, ages and ID s are exactly the same, then they are the same person. That is to say, if
person1.name = person2.name person1.age = person2.age person1.id = person2.id
So we think of person1.equals(person2)=true. This means that equals means that they are logically equal.
hashcode() is a hash value calculated by hash on an object. It has the following characteristics:
- Objects X and y have the same hashcode, which does not mean that the two objects are the same (x.equals(y)=true), and may have hash collisions; however, if hashcode is not the same, it must be two different objects.
- If equals() of two objects is equal, then hashcode must be equal.
So we can generally use hashcode to quickly compare the differences between two objects, because if X. hashcode ()!= y. hashcode (), then x.equals(y)=false.
Characteristics of equals()
Many times we want to rewrite the equals() method of a custom object, so remember that your equals() method must satisfy the following four conditions:
- Reflexivity: For non-null objects x, there must be x.equals(x)=true;
- Symmetry: If x.equals(y)=true, then y.equals(x) must also be true;
- Transitivity: If x.equals(y)=true and y.equals(z)=true, then x.equals(z) must be true;
- For non-null objects x, there must be x.equals(null)=false
How to overload the equals() method?
Generally speaking, if you want to overload the equals() method, you can refer to the following set of template codes:
- First, use=== to determine whether two objects refer to the same.
- Use instanceof to determine whether two objects are of the same type.
- If the types are the same, the parameters to be compared are transformed.
- Compare whether each logical value in two objects is equal, and return true or false only if all are equal.
- Test whether this method can satisfy the above features.
Implementation of equals() and hashcode() in Java source String
After looking at the above features and overloading methods, you may be a bit overwhelmed. Now let's look at how String in Java is implemented and whether it meets the above features.
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
As you can see, the above method performs the following steps in turn:
- This = anObject is referenced for comparison.
- Judge type anObject instance of String;
- Transition String anotherString = String anObject;
- To compare logical values for String, first length is equal n == anotherString.value.length; then each character is equal, see the code, and finally the result is returned.
Next, I write a test code to verify that it meets the above features:
private static void testStringEquals() { String x = "First"; String y = "First"; String z = new String("First"); System.out.println(x.equals(x)); System.out.println((x.equals(y) && y.equals(x))); if (x.equals(y) && y.equals(x)) { System.out.println(x.equals(z)); } System.out.println(x.equals(null)); }
The printing results are as follows:
true true true false
The instructions are in line.
Then let's look at the source code implementation of hashcode(). We know that hashcode means calculating hash hash hash value. In fact, it means calculating a hash value quickly for an object to differentiate usage: as long as hashcode() is different, the two objects must be different. Let's see how String calculates its hash value.
private final char value[]; /** The value is used for character storage. */ private int hash; /** Cache the hash code for the string Default to 0 */ public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
The main code used to calculate hashcode is this code.
for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; }
value is an array of characters that store string values internally. The way to calculate hashcode is to traverse each character in turn, multiply it by 31, and then add the next character. For example, the hashcode of "a" is 97; the hashcode of "aa" is 31*97+97=3104. Therefore, it can be seen that two String objects with different hashcodes must not be the same object.
Remember: when overloading equals(), make sure that two equals objects must have the same hashcode
Many people overload equals() without guaranteeing that two equals objects have the same hashcode, which leads to strange errors.
For example, let me start by overloading the equals() method of PhoneNumberWithout Hashcode:
class PhoneNumberWithoutHashcode { final short countryCode; final short number; public PhoneNumberWithoutHashcode(int countryCode, int number) { this.countryCode = (short) countryCode; this.number = (short) number; } @Override public boolean equals(Object obj) { // 1. check == reference if(obj == this) { return true; } // 2. check obj instance if (!(obj instanceof PhoneNumberWithoutHashcode)) return false; // 3. compare logic value PhoneNumberWithoutHashcode anObj = (PhoneNumberWithoutHashcode) obj; return anObj.countryCode == this.countryCode && anObj.number == this.number; } }
Let's create two identical objects and see how their equals() hashcode() returns.
private static void test() { PhoneNumberWithoutHashcode p1 = new PhoneNumberWithoutHashcode(86, 123123); PhoneNumberWithoutHashcode p2 = new PhoneNumberWithoutHashcode(86, 123123); System.out.println("p1.equals(p2)=" + p1.equals(p2)); System.out.println("p1.hashcode()=" + p1.hashCode()); System.out.println("p2.hashcode()=" + p2.hashCode()); }
The results are as follows:
p1.equals(p2)=true p1.hashcode()=1846274136 p2.hashcode()=1639705018
As you can see, they are equals, but hashcode is different. This violates the Java guidelines, what results will it lead to?
private static void test() { PhoneNumberWithoutHashcode p1 = new PhoneNumberWithoutHashcode(86, 123123); PhoneNumberWithoutHashcode p2 = new PhoneNumberWithoutHashcode(86, 123123); System.out.println("p1.equals(p2)=" + p1.equals(p2)); HashMap<PhoneNumberWithoutHashcode, String> map = new HashMap<>(); map.put(p1, "TheValue"); System.out.println("Result: " + map.get(p2)); }
What do readers think they will print? Result: The Value? Let's look at the results of the operation:
p1.equals(p2)=true Result: null
The question is, p1 and p2 are equal, but they are not the same key, at least for HashMap, they are not the same key. Why?
Let's see how HashMap put s and get s.
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
As you can see from this code, when p1 and p2 are stored, hash(key) is calculated once, as follows:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
In fact, we call the key.hashCode() method, and we know that although p1.equals(p2)=true, but p1. hashCode ()!= p2. hashCode (), so P1 and P2 for HashMap are basically two keys, of course, each other can not get the value of the other.
So how to improve this class? Let's implement its hashcode method again.
class PhoneNumber { protected final short countryCode; protected final short number; public PhoneNumber(int countryCode, int number) { this.countryCode = (short) countryCode; this.number = (short) number; } @Override public boolean equals(Object obj) { // 1. check == reference if (this == obj) return true; // 2. check obj instance if (!(obj instanceof PhoneNumber)) return false; // 3. compare logic value PhoneNumber target = (PhoneNumber) obj; return target.number == this.number && target.countryCode == this.countryCode; } @Override public int hashCode() { return (31 * this.countryCode) + this.number; } }
At this point, our test code:
private static void test() { PhoneNumber p1 = new PhoneNumber(86, 12); PhoneNumber p2 = new PhoneNumber(86, 12); System.out.println("p1.equals(p2)=" + p1.equals(p2)); System.out.println("p1.hashcode()=" + p1.hashCode()); System.out.println("p2.hashcode()=" + p2.hashCode()); HashMap<PhoneNumber, String> map = new HashMap<>(2); map.put(p1, "TheValue"); System.out.println("Result: " + map.get(p2)); }
The printing results are as follows:
p1.equals(p2)=true p1.hashcode()=88076 p2.hashcode()=88076 Result: TheValue
It means that after overloading hashcode, PhoneNumber can run normally in HashMap, after all, HashMap HashSet like this is based on object hash value.
Summary
If there are omissions and errors, readers are welcome to submit them. Thank you.