Through source code analysis of HashMap and HashSet, the Hash storage mechanism set and reference are just like the reference type array. When we put Java objects into an array, not really put a Java object into an array, but put the object reference into an array. Each array element is a reference variable. Actually, HashSet and HashMap
Through source code analysis of HashMap and HashSet, the Hash storage mechanism set and reference are just like the reference type array. When we put Java objects into an array, not really put a Java object into an array, but put the object reference into an array. Each array element is a reference variable. Actually, HashSet and HashMap
Analysis of Its Hash Storage Mechanism through source code of HashMap and HashSet
Set and reference
Just like an array of reference types, when we put a Java object into an array, we do not actually put a Java object into an array, but just put the object reference into an array, each array element is a reference variable.
In fact, there are many similarities between HashSet and HashMap. For HashSet, the system uses the Hash algorithm to determine the storage location of the collection elements. This ensures that the collection elements can be saved and retrieved quickly; for HashMap, the system key-value is processed as a whole. The system always calculates the storage location of key-value based on the Hash algorithm, this ensures that the key-value pairs of Map can be saved and retrieved quickly.
Before introducing the Set storage, we need to point out that although the Set claims to store Java objects, it does not actually put Java objects into the Set, it is just a reference to keep these objects in the Set. That is to say, the Java set is actually a collection composed of multiple referenced variables, which point to the actual Java object.
Storage Implementation of HashMap
When the program tries to put multiple key-values into HashMap, take the following code snippet as an example:
HashMap
Map = new HashMap
(); Map. put ("language", 80.0); map. put ("Mathematics", 89.0); map. put ("English", 78.2 );
HashMap uses a so-called "Hash algorithm" to determine the storage location of each element.
When the program executes map. put ("", 80.0); The system calls the hashCode () method of "" to obtain its hashCode value-each Java object has a hashCode () method, you can obtain its hashCode value through this method. After obtaining the hashCode value of this object, the system determines the storage location of the element based on the hashCode value.
Let's look at the source code of the put (K key, V value) method of the HashMap class:
Public V put (K key, V value) {// if the key is null, call the putForNullKey method to process if (key = null) return putForNullKey (value ); // calculate the Hash value int hash = hash (key. hashCode (); // search for the index int I = indexFor (hash, table. length); // If the Entry at the I index is not null, The next element of the e element for (Entry
E = table [I]; e! = Null; e = e. next) {Object k; // find that the specified key is equal to the key to be put in (the hash value is the same // use equals to compare true) if (e. hash = hash & (k = e. key) = key | key. equals (k) {V oldValue = e. value; e. value = value; e. recordAccess (this); return oldValue ;}// if the Entry at the I index is null, EntrymodCount ++ is not found here; // Add key and value to the I index addEntry (hash, key, value, I); return null ;}
JDK source code
You can find a src.zip compressed file under the JDK installation directory, which contains all the source files of the Java base class library. As long as you are interested in learning, you can open this compressed file at any time to read the source code of the Java class library, which is very helpful to improve your programming ability. It should be pointed out that the source code contained in src.zip does not contain Chinese comments like the above. The comments are added by the author.
The above program uses an important internal interface: Map. Entry. Each Map. Entry is actually a key-value pair. It can be seen from the above program that when the system decides to store the key-value Pair in HashMap, the value in the Entry is not considered at all. It only calculates and determines the storage location of each Entry based on the key. This also illustrates the previous conclusion: we can regard the value in the Map set as a subsidiary of the key. When the system determines the storage location of the key, the value will be saved there.
The above method provides a method to calculate the Hash code based on the return value of hashCode (): hash (). This method is purely mathematical computation. The method is as follows:
Static int hash (int h) {h ^ = (h >>>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4);} for any given object, as long as its hashCode () returns the same value, the program calls hash (int h) the Hash value calculated by the method is always the same. Next, the program will call the indexFor (int h, int length) method to calculate which index the object should be stored in the table array. The code for indexFor (int h, int length) is as follows:
static int indexFor(int h, int length){return h & (length-1);}
This method is very clever. It always uses h & (table. length-1) to obtain the storage location of the object -- and the length of the underlying array of HashMap is always 2 to the power of n. For details, see the introduction of the HashMap constructor.
When length is always a multiple of 2, h & (length-1) will be a very clever design: Suppose h = 5, length = 16, then h & length-1 will get 5; If h = 6, length = 16, then h & length-1 will get 6 ...... If h = 15, length = 16, h & length-1 will get 15; but when h = 16, length = 16, then h & length-1 will get 0; when h = 17, length = 16, then h & length-1 will get 1 ...... This ensures that the calculated index value is always within the index of the table array.
According to the source code of the put method, when the program tries to put a key-value pair into HashMap, the program first determines the storage location of the Entry based on the return value of the key hashCode: if the hashCode () values of the keys of the two entries are the same, they are stored in the same location. If the keys of the two entries return true through equals comparison, the value of the newly added Entry will overwrite the value of the original Entry in the set, but the key will not overwrite. If the keys of these two entries are compared by equals, false is returned. The newly added Entry forms an Entry chain with the original Entry in the set, the newly added Entry is in the header of the Entry chain. For more information, see the description of the addEntry () method.
When a key-value pair is added to a HashMap, the return value of its key hashCode () determines the storage location of the key-value Pair (that is, the Entry object. When the hashCode () return values of the keys of the two Entry objects are the same, the overwrite behavior is determined by the key comparison value through eqauls () (true is returned ), or generate an Entry chain (return false ).
The above program also calls addEntry (hash, key, value, I); Code, where addEntry is a package access permission method provided by HashMap, this method is only used to add a key-value pair. The code for this method is as follows:
Void addEntry (int hash, K key, V value, int bucketIndex) {// obtain the EntryEntry at the specified bucketIndex
E = table [bucketIndex]; // ① // place the newly created Entry to the bucketIndex index, and point the new Entry to the original Entrytable [bucketIndex] = new Entry
(Hash, key, value, e); // if the number of key-value pairs in Map exceeds the limit if (size ++> = threshold) // extend the table object length to 2 times. Resize (2 * table. length); // ②}
The code for the above method is very simple, but it contains a very elegant design: the system always places the newly added Entry object to the bucketIndex index of the table array. If an Entry object already exists at the bucketIndex, the newly added Entry object points to the original Entry object (which generates an Entry chain). If there is no Entry object in the bucketIndex index, that is, the e variable of code ① above is null, that is, the newly added Entry object points to null, that is, no Entry chain is generated.
Hash algorithm performance options
According to the code above, we can see that when the Entry chain is stored in the same bucket, the newly added Entry is always in the bucket, the Entry that is first placed in the bucket is at the end of the Entry chain.
There are two variables in the above program:
Size: this variable stores the number of key-value pairs contained in the HashMap.
Threshold: this variable contains the limit of the key-value pair that HashMap can accommodate. Its value is equal to the capacity of HashMap multiplied by the load factor ).
Code ② In the above program shows that when size ++> = threshold, HashMap automatically calls the resize method to expand the capacity of HashMap. The capacity of HashMap is doubled every time it is expanded.
The table used in the above program is actually a normal array, each array has a fixed length, the length of this array is the capacity of HashMap. HashMap contains the following constructor:
HashMap (): Construct a HashMap with an initial capacity of 16 and a load factor of 0.75.
HashMap (int initialCapacity): constructs a HashMap with an initial capacity of initialCapacity and a load factor of 0.75.
HashMap (int initialCapacity, float loadFactor): Creates a HashMap with the specified initial capacity and load factor.
When creating a HashMap, the system automatically creates a table array to save the entries in the HashMap. The following is the code of a constructor in the HashMap:
// Create HashMappublic HashMap (int initialCapacity, float loadFactor) with the specified initialization capacity and load factor {// The initial capacity cannot be negative if (initialCapacity <0) throw new capacity ("Illegal initial capacity:" + initialCapacity); // if the initial capacity is greater than the maximum capacity, show the capacity if (initialCapacity> MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; // The load factor must be greater than 0. if (loadFactor <= 0 | Float. isNaN (loadFactor) throw new IllegalArgumentException (loadFacto R); // calculate the nth power value greater than 2 of initialCapacity. Int capacity = 1; while (capacity <initialCapacity) capacity <= 1; this. loadFactor = loadFactor; // set the capacity limit to * capacity load factor threshold = (int) (capacity * loadFactor); // initialize the table array table = new Entry [capacity]; // ① init ();}
The bold code in the above Code contains a concise code implementation: Find the nth power value greater than initialCapacity and the smallest 2, and use it as the actual capacity of HashMap (saved by the capacity variable ). For example, if initialCapacity is set to 10, the actual capacity of the HashMap is 16.
Capacity of initialCapacity and HashTable
The initialCapacity specified during HashMap creation is not equal to the actual capacity of HashMap. Generally, the actual capacity of HashMap is larger than initialCapacity, unless the value of initialCapacity is equal to the n power of 2. Of course, after mastering the knowledge of HashMap capacity allocation, you should specify the initialCapacity parameter value to the nth power of 2 when creating a HashMap, which can reduce the computing overhead of the system.
At code 1, we can see that the essence of table is an array, an array of capacity length.
For HashMap and its Child classes, they use the Hash algorithm to determine the storage location of elements in the collection. When the system starts to initialize HashMap, the system will create an Entry array with a capacity length. The location of elements stored in this array is called "bucket )", each bucket has its specified index, and the system can quickly access the elements stored in the bucket based on its index.
At any time, each "Bucket" of HashMap stores only one element (that is, one Entry), because the Entry object can contain a reference variable (that is, the last parameter of the Entry constructor) it is used to point to the next Entry, so it may occur that the bucket of HashMap has only one Entry, but this Entry points to another Entry -- this forms an Entry chain. 1:
Figure 1. Storage diagram of HashMap
--------------------------------------------------------------------------------
Read Implementation of HashMap
When the Entry stored in each bucket of HashMap is only a single Entry -- that is, the Entry chain is not generated through the pointer, HashMap has the best performance: when the program extracts the corresponding value through the key, the system only needs to calculate the return value of the hashCode () of the key, find the index of the key in the table Array Based on the return value of the hashCode, and then retrieve the Entry at the index, finally, return the value corresponding to the key. Check the get (K key) method code of the HashMap class:
Public V get (Object key) {// if the key is null, call getForNullKey to retrieve the corresponding valueif (key = null) return getForNullKey (); // calculate its hash code int hash = hash (key. hashCode (); // directly retrieve the value of the specified index in the table array, for (Entry
E = table [indexFor (hash, table. length)]; e! = Null; // search for the next Entre of the Entry chain = e. next) // ① {Object k; // if the key of the Entry is the same as the searched key if (e. hash = hash & (k = e. key) = key | key. equals (k) return e. value;} return null ;}
From the code above, we can see that if each bucket of HashMap has only one Entry, HashMap can quickly retrieve the Entry in the bucket according to the index; in the case of a "Hash Conflict", an Entry chain is not stored in a single bucket. The system can only traverse each Entry in order, until the Entry you want to search for is found. If the Entry that you want to search for is located at the end of the Entry chain (the Entry is first placed in the bucket ), then the system must loop to the end to find this element.
In summary, HashMap treats key-value as a whole at the underlying layer, which is an Entry object. At the underlying layer of HashMap, an Entry [] array is used to store all key-value pairs. When an Entry object needs to be stored, its storage location is determined based on the Hash algorithm; when an Entry needs to be retrieved, it will also find its storage location based on the Hash algorithm and retrieve it directly. It can be seen that the reason why HashMap can quickly store and retrieve the entries it contains is similar to what our mother taught us in real life: different things should be placed in different places, you can quickly find it as needed.
When creating a HashMap, there is a default load factor. The default value is 0.75, which is a compromise between time and space costs: increasing the load factor can reduce the memory space occupied by the Hash table (that is, the Entry array), but it will increase the time overhead of data query, query is the most frequent operation (query is required for both the get () and put () Methods of HashMap). Reducing the load factor will improve the performance of data query, but it will increase the memory space occupied by the Hash table.
After mastering the above knowledge, we can adjust the load factor value as needed when creating a HashMap. If the program is concerned about space overhead and memory shortage, you can increase the load factor appropriately. If the program is more concerned about the time overhead, the load factor can be appropriately reduced if the memory is relatively wide. Generally, programmers do not need to change the value of the load factor.
If you know at the beginning that HashMap will save multiple key-value pairs, you can use a large initialization capacity during creation, if the number of entries in HashMap never exceeds the capacity limit (capacity * load factor), HashMap does not need to call the resize () method to reassign the table array to ensure better performance. Of course, setting the initial capacity too high at the beginning may waste space (the system needs to create an Entry array with a capacity length). Therefore, you must be careful when initializing the capacity settings when creating a HashMap.
Implementation of HashSet
For a HashSet, it is implemented based on HashMap. The underlying HashSet uses HashMap to store all elements. Therefore, the implementation of HashSet is relatively simple. You can view the source code of HashSet and see the following code:
Public class HashSet
Extends AbstractSet
Implements Set
, Cloneable, java. io. Serializable {// use the HashMap key to save all the elements in the HashSet private transient HashMap
Map; // define a virtual Object as the valueprivate static final Object PRESENT of HashMap = new Object ();... // initialize the HashSet. A HashMappublic HashSet () {map = new HashMap is initialized at the underlying layer.
() ;}// Create a HashSet with the specified initialCapacity and loadFactor // In fact, it is to create a HashMappublic HashSet (int initialCapacity, float loadFactor) {map = new HashMap
(InitialCapacity, loadFactor);} public HashSet (int initialCapacity) {map = new HashMap
(InitialCapacity);} HashSet (int initialCapacity, float loadFactor, boolean dummy) {map = new LinkedHashMap
(InitialCapacity, loadFactor);} // call map's keySet to return all keypublic Iterator
Iterator () {return map. keySet (). iterator () ;}// call the size () method of HashMap to return the number of entries, and the number of elements in the Set is obtained. public int size () {return map. size () ;}// call the isEmpty () of HashMap to determine whether the HashSet is empty. // when the HashMap is empty, the corresponding HashSet is also empty. public boolean isEmpty () {return map. isEmpty ();} // call the hashinskey of HashMap to determine whether to include all elements of the specified key // HashSet. It is the public boolean contains (Object o) Saved by the key of HashMap) {return map. containsKey (o) ;}// put the specified element into the HashSet, that is, put the element as the key into the HashMappublic boolean add (E e) {return map. put (e, PRESENT) = null;} // call the remove Method of HashMap to delete the specified Entry. In this way, the public boolean remove (Object o) element of HashSet is deleted) {return map. remove (o) = PRESENT;} // call the clear method of Map to clear all entries, so that all elements in the HashSet are cleared. public void clear () {map. clear ();}...}
From the source code above, we can see that the implementation of HashSet is actually very simple. It only encapsulates a HashMap object to store all the set elements, all set elements in a HashSet are actually saved by the HashMap key, while the HashMap value stores a PRESENT, which is a static Object.
Most HashSet methods are implemented by calling the HashMap method. Therefore, the implementation of HashSet and HashMap is essentially the same.
Put of HashMap and add of HashSet
Because the add () method of HashSet actually changes to calling the put () method of HashMap to add a key-value pair when adding a set element, when the key in the newly added HashMap Entry is the same as the key in the set with the original Entry (the return value of hashCode () is the same, true is also returned through equals comparison ), the value of the newly added Entry will overwrite the value of the original Entry, but the key will not change. Therefore, if you add an existing element to the HashSet, newly Added collection elements (stored by the HashMap key at the underlying layer) do not overwrite existing collection elements.
After mastering the above theoretical knowledge, let's take a look at a sample program to test whether you have mastered the functions of HashMap and HashSet.
class Name{private String first;private String last;public Name(String first, String last){this.first = first;this.last = last;}public boolean equals(Object o){if (this == o){return true;}if (o.getClass() == Name.class){Name n = (Name)o;return n.first.equals(first)&& n.last.equals(last);}return false;}}public class HashSetTest{public static void main(String[] args){Set
s = new HashSet
();s.add(new Name("abc", "123"));System.out.println(s.contains(new Name("abc", "123")));}}
After adding a new Name ("abc", "123") object to the HashSet, the program immediately checks whether the HashSet contains a new Name ("abc ", "123") object. Rough look, it is easy to think that the program will output true.
In actual running, the above program will see that the program outputs false, because the HashSet criteria for determining the equality between two objects require that the hashCode () of the two objects be required, in addition to comparing and returning true through the equals () method () return values are equal. The above program did not override the hashCode () method of the Name class. The values of hashCode () returned by two Name objects are different. Therefore, HashSet treats them as two objects, so the program returns false.
It can be seen that when we try to treat the Object of a class as the key of HashMap, or try to save the Object of this class into HashSet, We will overwrite the equals (Object obj) of this class) the methods and hashCode () methods are very important, and the return values of these two methods must be consistent: when the two hashCode () return values of the class are the same, they use equals () if the method is compared, return true. Generally, all key attributes involved in calculating the return value of hashCode () should be used as the criteria for comparison of equals.
HashCode () and equals ()
For details about how to correctly override the hashCode () method and equals () method of a class, refer to the crazy Java handout in crazy Java.
The following program correctly overrides the hashCode () and equals () Methods of the Name class. The program is as follows:
Class Name {private String first; private String last; public Name (String first, String last) {this. first = first; this. last = last;} // judge whether two names are equal according to first: public boolean equals (Object o) {if (this = o) {return true;} if (o. getClass () = Name. class) {Name n = (Name) o; return n. first. equals (first) ;}return false ;}// return the public int hashCode () {return first value based on the hashCode () value of the first object. hashCode ();} public String toString () {return "Name [first =" + first + ", last =" + last + "]" ;}} public class HashSetTest2 {public static void main (String [] args) {HashSet
Set = new HashSet
(); Set. add (new Name ("abc", "123"); set. add (new Name ("abc", "456"); System. out. println (set );}}
The above program provides a Name class, which overrides the equals () and toString () methods. Both methods are determined based on the first instance variable of the Name class, when the first instance variables of the two Name objects are the same, the hashCode () return values of the two Name objects are the same, and true is returned for comparison through equals.
The main program method first adds the first Name object to the HashSet. The first instance variable value of this Name object is "abc ", next, the program tries to add a Name object named first "abc" to the HashSet. Obviously, the new Name object cannot be added to the HashSet, because the first of the Name object to be added here is "abc", HashSet judges that the new Name object is the same as the original Name object, so it cannot be added, when the program outputs the set in code ①, it will see that the set contains only one Name object, which is the first Name object whose last is "123.