1.前提
本系列基于jdk1.8主要介绍HashMap的概念以及源码分析,会对比常见集合与HashMap之间的区别,以及面试遇到的问题。
2.HashMap简介
在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
- HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
- HashMap 继承于AbstractMap,实现了Map,Cloneable,java.io.Serializable接口。
- HashMap的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
- HashMap有两个参数影响其性能:初始容量和加载因子。默认初始容量是16,加载因子是0.75。容量是哈希表中桶(Entry数组)的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,通过调用 rehash 方法将容量翻倍。
3.Hash实现原理
在介绍hashMap源码之前先讲几个基本概念以及Object类的两个方法hashCode和equals
3.1.Java中==号与equals()方法的区别
==号和equals()方法都是比较是否相等的方法,那它们有什么区别和联系呢?
首先,==号在比较基本数据类型时比较的是值,而用==号比较两个对象时比较的是两个对象的地址值:
package hello;
/**
* 描述:
* 作者:袁伟倩
* 创建日期:2018-05-10 11:47.
*/
public class Test {
public static void main(String[] args) {
int x = 10;
int y = 10;
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(x == y); // 输出true
System.out.println(str1 == str2); // 输出false
}
在比较基本类型的值的时候也会引申一个自动装箱和拆箱的问题,比如下面的例子,具体可以看我写的一篇博客
package hello;
/**
* 描述:
* 作者:袁伟倩
* 创建日期:2018-05-10 11:47.
*/
public class Test {
public static void main(String[] args) {
int x = 10;
int y = 10;
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(x == y); // 输出true
System.out.println(str1 == str2); // 输出false
Integer a = 1444;
int b = 1444;
Integer x1 = 10;
Integer y1 = 10;
Integer x2 = 1444;
Integer y2 = 1444;
System.out.println(a == b); // 输出true
System.out.println(x1 == y1); // 输出true
System.out.println(x2 == y2); // 输出false
}
}
我们可以通过查看源码知道,equals()方法存在于Object类中,因为Object类是所有类的直接或间接父类,也就是说所有的类中的equals()方法都继承自Object类,而通过源码我们发现,Object类中equals()方法底层依赖的是==号,那么,在所有没有重写equals()方法的类中,调用equals()方法其实和使用==号的效果一样,也是比较的地址值,然而,Java提供的所有类中,绝大多数类都重写了equals()方法,重写后的equals()方法一般都是比较两个对象的值:下面看一下String类中重写了equals方法源码:
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;
}
比如下面的实例:
package hello;
/**
* 描述:
* 作者:袁伟倩
* 创建日期:2018-05-10 11:47.
*/
public class Test {
public static void main(String[] args) {
String str1 = new String("abc");
String str2 = new String("abc");
System.out.println(str1.equals(str2));// 输出true
System.out.println(str1 == str2);// 输出false
}
}
这里又引申一个问题,java 常量池问题,具体介绍详细看java的自动装箱和拆箱这篇文章
package hello;
/**
* 描述:
* 作者:袁伟倩
* 创建日期:2018-05-10 11:47.
*/
public class Test {
public static void main(String[] args) {
String str1 ="abc";
String str2 ="abc";
System.out.println(str1.equals(str2));// 输出true
System.out.println(str1 == str2);// 输出true
}
}
综上所叙,如果想让两个对象相等,需要重写equals方法。
3.2.hashCode方法
hashCode方法,这个方法我们平时通常是用不到的,它是为哈希家族的集合类框架(HashMap、HashSet、HashTable)提供服务,hashCode 的常规协定是:
在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。
以下情况不 是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。
当我们看到实现这两个方法有这么多要求时,立刻凌乱了,幸好有IDE来帮助我们,Eclipse中可以通过快捷键alt+shift+s调出快捷菜单,选择Generate hashCode() and equals(),根据业务需求,勾选需要生成的属性,确定之后,这两个方法就生成好了,我们通常需要在JavaBean对象中重写这两个方法。
实例:当我比较两个对象时,如果只是比对某几个属性,可重新生成equals和hashcode方法,比如:
package hello;
/**
* 描述:
* 作者:袁伟倩
* 创建日期:2018-05-09 09:50.
*/
public class Model {
public Integer id;
public String name;
public String code;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Model model = (Model) o;
if (!id.equals(model.id)) return false;
return name.equals(model.name);
}
@Override
public int hashCode() {
int result = id.hashCode();
result = 31 * result + name.hashCode();
return result;
}
}
即使属性code不一样,而id和name一样,也一样认为两个对象相等。
3.2.HashMap中定义的成员变量
/**
* The default initial capacity - MUST be a power of two.
* 默认初始容量为16,必须为2的幂
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
* 最大容量为2的30次方
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
* 默认加载因子0.75
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
* 由链表转换成树的阈值TREEIFY_THRESHOLD
* 一个桶中bin(箱子)的存储方式由链表转换成树的阈值。即当桶中bin的数量超过TREEIFY_THRESHOLD时使用树来代替链表。默认值是8
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
* 由树转换成链表的阈值UNTREEIFY_THRESHOLD
* 当执行resize操作时,当桶中bin的数量少于UNTREEIFY_THRESHOLD时使用链表来代替树。默认值是6
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
* 当桶中的bin被树化时最小的hash表容量。(如果没有达到这个阈值,即hash表容量小于MIN_TREEIFY_CAPACITY,当桶中bin的数量太多时会执行resize扩容操作)这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。
*/
static final int MIN_TREEIFY_CAPACITY = 64;