@Test public void test() { String str1=new String("abc"); String str2=new String("abc"); boolean equals = str1.equals(str2); System.out.println(equals);//true }
hashCode方法是经过必定的算法获得一个hash值,通常配合散列集合一块儿使用,如HashMap、HashSet都是不能够存放重复元素的,那么当容器中元素个数不少时,你要添加一个元素时,难道一个一个去equals比较?固然这是能够的,可是不免效率很低,而HashMap和HashSet的底层都是使用数组+链表的方式实现的,这样有什么好处呢,当一个对象要加入集合,直接用hashCode进行一些运算获得保存的数组下标,再去数组下标对应的链表中一个一个元素比较(equals),这样显然减小了比较次数,提升了效率java
那Object的hashCode方法的默认实现是怎样的呢?node
public native int hashCode();
能够看到它是一个本地方法,实际上Object的hashCode方法返回是元素的地址(不一样的虚拟机可能不同,但Hotspot的是)程序员
class Emp{ String idCord; String name; int age; public Emp(String idCord, String name, int age) { super(); this.idCord = idCord; this.name = name; this.age = age; } } @Test public void test2() { Emp e=new Emp("0101001","zhangsan",20); System.out.println(e.hashCode());//1717159510 System.out.println(e);//com.moyuduo.test.Emp@6659c656 }
6659c656转换成十进制也就是1717159510算法
咱们不少时候这样使用HashMap数组
@Test public void test3() { HashMap<String,Emp> map=new HashMap<>(); map.put(new String("zhangsan"), new Emp("01001","zhangsan",20)); map.put(new String("lisi"), new Emp("01002","lisi",22)); map.put(new String("zhangsan"), new Emp("01003","zhangsan",23)); Emp emp = map.get("zhangsan"); System.out.println(emp);//Emp [idCord=01003, name=zhangsan, age=23] }
额?不对呀,编号为01001的张三呢?并且咱们知道new出来的String的hashCode是地址必定是不相同的,那么为何后一个张三仍是把前一个张三覆盖了呢?app
是由于String重写了hashCode方法和equals方法this
public int hashCode() { //默认为0 int h = hash; if (h == 0 && value.length > 0) { char val[] = value; //一个一个遍历String的char[] for (int i = 0; i < value.length; i++) { //hash值等于当前字符前字符的hash*31+当前字符的Unicode h = 31 * h + val[i]; } hash = h; } return h; } public boolean equals(Object anObject) { //判断两个对象的地址是否相同 if (this == anObject) { return true; } //判断传入的对象是否是String类型 if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; //判断两个String的char[]的长度是否一致 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; }
那么当咱们使用本身的对象做为键时code
@Test public void test4() { HashMap<Emp,Integer> map=new HashMap<>(); map.put(new Emp("01001","zhangsan",20),6000); map.put(new Emp("01002","lisi",22),8000); Integer integer = map.get(new Emp("01001","zhangsan",20)); System.out.println(integer);//null }
能够看到输出的是null,这是为何呢,就是由于咱们自定义的类没有从新写hashCode方法,get的时候新new出来的Emp对象的hashCode(也就是地址)确定和存的时候的hashCode不同,因此拿不到,因此当咱们自定义的类要使用散列集合存储时,必定要重写equals方法和hashCode方法对象
为何当咱们要使用自定义对象做为key存放在HashMap中时,必定要重写equals和hashCode呢?get
咱们去看看HashMap底层是怎么存键值对和获得值的
HashMap的put方法
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } //计算键的hash static final int hash(Object key) { int h; //若是键为null那么hash为0,这也是为何HashMap只能存放一个键为null的元素,不然hash为hashCode与上hashCode无符号右移16位 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //若是当前的Node数组还未初始化或长度为0 if ((tab = table) == null || (n = tab.length) == 0) //进行扩容 n = (tab = resize()).length; //节点存放的下标是数组长度-1与上键的hash if ((p = tab[i = (n - 1) & hash]) == null) //运算获得的下标的位置没有存放元素,那么直接保存 tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //若是下标位置元素的hash和键的hash相等而且下标元素的key和键的地址相同或equals那么直接覆盖 e = p; else if (p instanceof TreeNode) //若是下标元素位置存放的元素原本就是红黑色节点,那么按照红黑树的规则插入 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //下标位置有元素并且还没转化为红黑树,说明是链表存储 for (int binCount = 0; ; ++binCount) { //让e指向链表下一个节点 if ((e = p.next) == null) {//当找到最后e等于null了说明链表中没有元素的key和当前插入的key相同 //直接把节点挂到链表尾 p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //若是找到已存入元素的key和插入key的hash相同而且两key地址相等或equals,那么e就是要替换的元素 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) //替换旧值 e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) //插入后元素大小超过阈值进行扩容 resize(); afterNodeInsertion(evict); return null; }
HashMap的get方法
public V get(Object key) { Node<K,V> e; //若是经过key拿到的键值对节点为null就返回null,不然返回节点的value return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //Node[]是否已经初始化而且长度>0而且经过hash运算获得的下标已经有元素 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //判断下标第一个位置节点的hash和查询key的hash一致而且两key地址同样或equals if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; //下标节点还有next if ((e = first.next) != null) { //节点是红黑树,那么按照红黑树的查找规则进行 if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { //是链表,那么依次遍历 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
能够看到存的时候是经过hashCode获得hash再用hash获得存放下标,而后存入键值对
取的时候是经过hashCode获得hash再获得下标元素,下标元素再根据hash&&(地址相等||equals)获得键值对
说了这些再来讲说Object规范