一、HashMap
1.HashMap中元素的特点
①HashMap中的所有的Key彼此之间是不可重复的,无序的。所有的Key就构成一个Set集合。—>key所在的类要重写hashCode()方法和equals()
②HashMap中的所有的value彼此之间是可重复的,无序的。所有的value就构成一个Collection集合。——>value所在的类要重写equals()
③HashMap中的key-value,就构成了一个entry。
④HashMap中的所有的entry彼此之间是不可重复的,无序的。所有的entry就构成了一个Set集合。
2.HashMap源码解析:
2.1 jdk7中创建对象和添加数据过程(以JDK1.7.0_07为例说明):
//创建对象的过程中,底层会初始化数组Entry[] table = new Entry[16];
HashMap<String,Integer> map = new HashMap<>();
源码:
public HashMap(int initialCapacity, float loadFactor) {
//...略...
//通过此循环,得到capacity的最终值,此最终值决定了Entry数组的长度。此时的capacity一定是2的整数倍
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor; //确定了加载因子的值0.75
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); //确定了临界值
table = new Entry[capacity]; //初始化数组,长度为capacity
//..略..
}
map.put("AA",78); //"AA"和78封装到一个Entry对象中,考虑将此对象添加到table数组中。
添加/修改的过程:
将(key,value1)添加到当前的map中:
首先,需要调用key1所在类的hashCode()方法,计算key1对应的哈希值1,此哈希值1经过某种算法(hash())之后,得到哈希值2.
哈希值2在经过某种算法(indexFor())之后,就确定了(key1,value1)在数组table中的索引位置i。
1.1如果次索引位置i的数组上没有元素,则(key1,value1)添加成功。---->情况1
1.2如果次索引位置i的数组上有元素(key2,value2),则需要比较key1和key2的哈希值2—>哈希冲突
2.1如果key1的哈希值2与key2的哈希值2不相同,则(key1,value1)添加成功------>情况2
2.2如果key1的哈希值2与key2的哈希值2相同,则需要继续比较key1和key2的equals().要调用key1所在类的equals(),将key2作为参数传递进去。
3.1调用equals(),返回false:则(key1,value1)添加成功。---->情况3
3.2调用equals(),返回true:则认为key1和key2是相同的,默认情况下替换原有的value2.
说明:情况1:将(key1,value1)存放到数组的索引i的位置
情况2,情况3:(key1,value1)元素与现有的(key2,value2)构成单向链表结构, (key1,value1)指向(key2,value2)
随着不断地添加元素,在满足如下的条件的情况下,会考虑扩容:
(size >= threshold) && (null != table[i])
当元素的个数达到临界值(->数组的长度 * 加载因子)时,就会考虑扩容。默认的临界值=16*0.75---->12
2.2jdk8与jdk7的不同之处(以jdk1.8.0_271为例):
①在jdk8中,的那个我们创建了HashMap实例以后,底层并没有初始化table数组。当首次添加(key,value)时,进行判断,
如果发现table尚未初始化,则对数组进行初始化。
②在jdk8中,HashMap底层定义了Node内部类,替换jdk7中的Entry内部类,意味着,我们创建的数组是Node[]
③在jdk8中,如果当前的(key,value)经过一系列判断之后,可以添加到当前的数组角标i中。如果此时角标i位置上有元素。在jdk7中是将新的(key.value)指向已有的旧的元素(头插法),而在jdk8中是旧的元素指向新的(key,value)元素(尾插法)。“七上八下”
④jdk7:数组+单向链表
jdk8:数组+单向链表+红黑树
什么时候会使用单向链表变为红黑树:如果数组索引i位置上的个数达到8,并且数组的长度打到64时,我们就将此索引i位置上的多个元素改为使用红黑树的结构进行存储。(为什么修改呢?红黑树进行put()、get()/remove()操作的时间复杂度O(logn),比单向链表的时间复杂度O(n)的好,性能更高)
什么时候会使用红黑树变为单线链表:当使用红黑树的索引i位置上的元素的个数低于6的时候,就会将红黑树结构退化为单向链表。
2.3 属性/字段:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的初始容量 16
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量 1 << 30
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认加载因子
static final int TREEIFY_THRESHOLD = 8; //默认树化阈值8,当链表的长度达到这个值后,要考虑树化
static final int UNTREEIFY_THRESHOLD = 6;//默认反树化阈值6,当树中结点的个数达到此阈值后,要考虑变为链表
public class MapExer {
public static void main(String[] args) {
HashMap<Integer, String> map = new HashMap<>();
map.put(31, "张三");
map.put(31, "李四");
map.put(47, "王五");
map.put(45, "赵六");
}
}
在类中重写了hashcode()的方法,通过hash值索引位置,张三跟李四的索引位置相同,而且hash值也相同,key也相同,也就替换了。
王五与李四的hash值索引的位置相同,当时当比较hash值时却不同,通过尾插法插入链表当中。
public class Person {
int id;
String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
//其中Person类中重写了hashCode()和equal()方法
@Override
public boolean equals(Object o) {
System.out.println("Person equals()....");
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
if (id != person.id) return false;
return name != null ? name.equals(person.name) : person.name == null;
}
@Override
public int hashCode() {
int result = id;
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
public class HashSetDemo {
public static void main(String[] args) {
HashSet set = new HashSet();
Person p1 = new Person(1001,"AA");//新建两个实例的Person对象
Person p2 = new Person(1002,"BB");
set.add(p1);//添加到HashSet当中
set.add(p2);
System.out.println(set);//打印set表
p1.name = "CC";//修改p1中的name的值
set.remove(p1);//移除p1,因为p1所在的位置是名字为"AA"的索引位置,当改为"CC"时,通过计算Hash值时与之前的不一样,从而并没有真正移除p1
System.out.println(set);
set.add(new Person(1001,"CC"));//添加一个对象,如下图1,通过hash计算的索引位置为7,但是由于hash值不同,尾插法插到后面去
System.out.println(set);
set.add(new Person(1001,"AA"));//虽然索引位置与hash值与索引位置相同,但是通过equals比值发现不同,直接尾插法
System.out.println(set);
}
}
图1