一、Arraylist类
1.1 默认初始容量
默认初始容量是10
private static final int DEFAULT_CAPACITY = 10;
1.2 数据序列化
用transient关键字修饰,意味着不使用默认的序列化方法,他使用writeObject方法来序列化,使用readObject来反序列化。
transient Object[] elementData;
1.3 构造方法
- 带参数构造方法
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
如果参数大于0,则直接新建对应长度的Object数组返回。
如果参数等于0.则指向空数组的指针
参数小于0,抛出异常
- 参数是Collection接口的实例
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
将Collection实例转换为数组,记录下实例的长度
长度小于0.指向空数组
长度大于0,elementData 数组指向该数组
1.4 添加
- 判断集合是不是已经满了
- 集合满的话判断集合的容量是不是小默认容量的一半,是的话,集合的长度增加12,否则增加当前集合长度的一半
- 将旧的集合数据放到新的集合中
1.5 删除(index的删除)
- 判断index是否大于集合的长度,大于,则抛出数组越界异常
- 小于,则将index后面的元素向前复制,返回删除的元素
二、HashMap
参考:一文读懂HashMap
2.1 避免哈希冲突
HashMap使用链表法避免哈希冲突(相同hash值),当链表长度大于TREEIFY_THRESHOLD(默认为8)时,将链表转换为红黑树,当然小于UNTREEIFY_THRESHOLD(默认为6)时,又会转回链表以达到性能均衡。
2.2 HashMap结果图
2.3 默认容量
16,而且hashmap的容量只能是2的幂数
2.4 hash值的计算
- 首先调用hashcode方法计算出key对应的hash值,记为hash1
- 将hash1右移16位记为hash2,与hash1进行异或记为hash3
- 用hash3与n(集合的长度减1)进行与运算,相当于做了取余运算,只不过这种运算更加高效和节省计算机资源
2.4.1 equals方法
未重写的形式:和==一样的功能,判断的是比较对象的地址是否相同。
public boolean equals(Object obj) {
return (this == obj);
}
2.4.2 string类重写equals方法
- 地址相同返回true
- 地址不同转换为string类,逐个比较字符
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;
}
2.4.3 为什么重写equals方法需要重写hashcode方法
equals方法重写后,比较的是对象的值。比如:stu stu1 = new stu(1,“张三”),stu stu2 = new stu(1,“张三”),使用重写后的equals方法,stu1.equals(stu2)返回true,使用未重写的hashcode,未重写的hashcode是根据对象的地址通过hash算法算来的,可见,使用未重写的hashcode比较,返回false,违反了equals方法相等,hashcode也必须相等的规定,就会产生,同样的key,返回的hashcode值不同,就会出错。
2.5 put元素的过程
- 判断table是否为空,空的话先构造table
- 判断key是否为空,是的话放在table为0的位置
- 计算出key的hash值
- 根据hash值找到元素要放的桶的位置,遍历桶中的链表,有相同的key值,就将对应的value替换,并返回oldvalu
- 如果没有相同的key值,就进行新增节点的的操作
- 新增节点需要判断新增节点后(当前节点不存在)节点的数量是否等于阈值和当前节点是否存在
- 大于的话就进行扩容
注意:
- 桶中链表的长度大于8,转为红黑树,小于6,转换为链表
- 如果桶满了(容量*加载因子(0.5)),就resize,2倍
2.6 get元素的过程
- 判断key是不是为null,是的话去0号位置的桶去找key为null的value然后返回
- key不为null,根据key计算出对应的hash值
- 找到hash值对应的通桶,根据key的值找到对应的value值
2.7 主要参数
1)桶(capacity)容量,即数组长度:DEFAULT_INITIAL_CAPACITY=1<<4;默认值为16
即在不提供有参构造的时候,声明的hashmap的桶容量;
2)MAXIMUM_CAPACITY = 1 << 30;
极限容量,表示hashmap能承受的最大桶容量为2的30次方,超过这个容量将不再扩容,让hash碰撞起来吧!
3)static final float DEFAULT_LOAD_FACTOR = 0.75f;
负载因子(loadfactor,默认0.75),负载因子有个奇特的效果,表示当当前容量大于(size/)时,将进行hashmap的扩容,扩容一般为扩容为原来的两倍。
4)int threshold;阈值
阈值算法为capacity*loadfactory,大致当map中entry数量大于此阈值时进行扩容(1.8)
5)transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;(默认为空{})
核心的数据结构,即所谓的数组+链表的部分。
2.8 节点的类型
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
2.9 扩容和碰撞
扩容的思路:
- 判断是否到达最大的容量
- 达到最大容量,设置新的容量为原容量的2倍
- 将原来table里面的值放到新的table里面
- 扩容完之后需要重新根据key值计算在table上的hash位置
2.10 先插入?先扩容?
JDK1.8:先插入后扩容
JDK1.7:先扩容后插入
三、HashTable
3.1 基础参数
- 默认加载因子:0.75
- 初始容量:11
- 扩容:2倍加1
- 容量:hashtable的容量可以为任意值
- 继承实现:继承自Dictioary类,实现map接口
3.2 重要的成员变量
table, count, threshold, loadFactor, modCount。
3.3 hash值的计算
- 直接使用了hashcode计算出来的值,然后进行一次与运算,将负的哈希值转换为正的哈希值,再对数组的长度进行模运算。
index = (hash & 0x7FFFFFFF) % tab.length;
3.4 hashmap和hashtable的区别
3.5 hashtable的遍历
- 枚举遍历
public class list {
public static void main(String[] args) {
Hashtable<Object, Object> map = new Hashtable<>();
map.put("1","12");
map.put("4","22");
map.put("3","32");
map.put("5","42");
Enumeration<Object> elements = map.elements();
while (elements.hasMoreElements()){
System.out.print(elements.nextElement() + " ");
}
}
}
- entrySet遍历
public class list {
public static void main(String[] args) {
Hashtable<Object, Object> map = new Hashtable<>();
map.put("1","12");
map.put("4","22");
map.put("3","32");
map.put("5","42");
Iterator<Map.Entry<Object, Object>> iterator = map.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<Object, Object> next = iterator.next();
System.out.println((String)next.getValue() + " " + next.getKey() + " ");
}
}
}
- keyset遍历
public class list {
public static void main(String[] args) {
Hashtable<Object, Object> map = new Hashtable<>();
map.put("1","12");
map.put("4","22");
map.put("3","32");
map.put("5","42");
Iterator<Object> iterator = map.keySet().iterator();
while (iterator.hasNext()){
Object next = iterator.next();
System.out.println(map.get(next) + " " + next);
}
}
}
四、ConcurrentHashMap(JDK1.6和JDK1.7)
4.1 参考
4.2 put方法 JDK1.6和JDK1.7的区别
在获得segment锁之前,会通过trylock方法尝试获得锁,在尝试获得锁的过程中,会遍历对应的位置的链表,如果找不到相同的key值,则会提前创建好一个节点,为后续的put操作做准备,trylock多次,仍然无法获得锁,就会请求lock去获得锁。事先遍历的好处是,将已经遍历的结果进行缓存,等到真正put的时候就不需要缓存了。插入节点时,使用的是头结点插入的方法,再将链表插入到对应的数组位置中。
4.3 rehash
对之前的rehash进行了优化,在扩容之后,选择一个节点,这个节点之后的所有节点的索引都不改变,这个节点之前的索引变为Index + capacity。
4.4 remove
和put方法一样,在尝试获得锁时会进行遍历,已提高缓存命中率。
4.5 get和containskey
使用的是Unsafe的getObjectVolatile方法提供的原子读语义来获取对应的段上的链表,未使用锁,在读数据的过程中,其他线程可能对数据已经做了改变,返回的可能是过时的数据,如果要求强一致性,那么必须使用Collections.synchronizedMap()方法。
4.6 特性
- 不允许key和value为null
- 使用段锁来实现高并发
4.7 结构图
4.9 JDK1.8 实现
cas方式实现
4.10 扩容
2倍的容量进行扩容,只是对一个段进行扩容
4.11 size和containsKey方法
可能需要锁住整张表,按顺序上锁,再按顺序解锁,不然会产生死锁。