目录
一、集合体系图
二、ArrayList
ArrayList
ArrayList 是由数组实现的,ArrayList是有序的,可以插入重复数据的,ArrayList可以插入多个null,ArrayList是线程不安全的
ArrayList底层维护的是一个Object数组elementData
ArrayList扩容规则:
使用无参构造初始化,初始长度为0;
使用带有int类型参数构造初始化,初始长度为int参数值;
使用Collection类型参数进行初始化,初始长度为Collection.size();
1、add方法扩容规则
无参构造创建ArrayList后扩容规则:
当数组元素超过现有长度最大值,进行第一次扩容,原有长度+10;
以后每次扩容长度(length)为,length右移一位+length,即length*1.5后向下取整;
初始长度为0;那么经过10次扩容后的长度分别为:
0---》【10,15,22,33,49,73,109,163,244,366】
带参构造创建ArrayList后扩容规则:
当数组元素超过现有长度最大值,扩容长度(length)为,length右移一位+length,即length*1.5后向下取整;
ArrayList扩容源码
最后一层一层的返回执行elementData[size++]=e,进行赋值
2、addAll方法扩容规则
当数组中没有时,扩容的长度为(10和实际元素个数的较大值);
有元素时扩容的长度(length)为,length右移一位+length
三、Vector
Vector也是由数组实现的,是线程安全的,效率低
通过无参构造创建vector默认初始10个容量,以后每次进行2倍扩容
通过带参构造创建vector每次进行2倍扩容
下面了解一下vector的构造器
//这个就是无参构造,通过构造器一层一层的调用最后会生成一个初始10容量的数组
public Vector() {
this(10);
}
//带一个参数的构造,传进来的参数就是数组的初始容量
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
前两个都还好吧,主要看一下这个构造器:
//这个构造器的第一个参数,还是和数组容量相关,不多赘述
//第二个参数,以后每次增加多少个容量
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
其余的源码和ArrayList相似度99%,只有扩容机制略有不同
四、LinkedList
底层是双向链表,可以添加任意元素包括null,元素可以重复,
线程不安全
LinkedList源码解读:
先看下成员属性,和内部类信息
public class LinkedList<E> {
//容量
transient int size = 0;
//头节点
transient java.util.LinkedList.Node<E> first;
//尾节点
transient java.util.LinkedList.Node<E> last;
//每个节点的信息
private static class Node<E> {
//数据内容
E item;
//当前节点的后一个节点
LinkedList.Node<E> next;
//当前节点的前一个节点
LinkedList.Node<E> prev;
Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
}
构造器
当LinkedList执行crud操作时,其实就是对Node中的信息进行操作
add方法:
remove(index);通过索引删除操作
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
方法太多了,不一个一个写了,搞清双向链表结构,在看源码so easy!!!
五、LinkedList vs ArrayList 性能比较
ArrayList:
1、基于数组,需要连续内存
2、查询速度快
3、尾部插入、尾部删除性能可以,其他部分插入删除速度慢(涉及到数据移动所以慢)
4、可以利用cpu缓存,局部性原理
LinkedList:
1、基于双向链表,无需连续内存
2、查询速度慢(要沿着链表进行遍历)
3、头尾插入、删除性能高
4、占用内存多
六、HashSet
HashSet实现了Set接口,HashSet底层实际上是HashMap,元素的值作为map的key,创建一个虚拟的new Object对象作为map的value,进行存储
可以存放null值,HashSet元素顺序根据Hash值决定,不能有重复的元素
构造器:
// 构造一个新的空集;支持的HashMap实例具有默认的初始容量 (16) 和加载因子 (0.75)。
public HashSet() {
map = new HashMap<>();
}
// 构造一个包含指定集合中元素的新集合。 HashMap是使用默认加载因子 (0.75) 和足以包含指定集合中的元素的初始容量创建的。
// 参形:
// c – 其元素要放入此集合的集合
// 抛出:
// NullPointerException – 如果指定的集合为空
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
// 构造一个新的空集;后备HashMap实例具有指定的初始容量和指定的负载因子。
// 参形:
// initialCapacity - 哈希映射的初始容量
// loadFactor - 哈希映射的负载因子
// 抛出:
// IllegalArgumentException – 如果初始容量小于零,或者负载因子为非正数
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
// 构造一个新的空集;支持HashMap实例具有指定的初始容量和默认加载因子 (0.75)。
// 参形:
// initialCapacity - 哈希表的初始容量
// 抛出:
// IllegalArgumentException – 如果初始容量小于零
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
// 构造一个新的空链接哈希集。 (此包私有构造函数仅由 LinkedHashSet 使用。)支持 HashMap 实例是具有指定初始容量和指定负载因子的 LinkedHashMap。
// 参形:
// initialCapacity - 哈希映射的初始容量
// loadFactor - 哈希映射的负载因子
// dummy – 忽略(将此构造函数与其他 int、float 构造函数区分开来。)
// 抛出:
// IllegalArgumentException – 如果初始容量小于零,或者负载因子为非正数
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
请参考hashmap!!!
七、TreeSet
底层有TreeMap实现,可以通过构造器传入一个比较器,实现有序
八、LinkenHashSet
底层有LinkedHashMap实现,底层维护的是一个数组+双向链表
九、HashMap
1、数据的特点
Map中的key不允许重复,value可以重复。
key可以为null,当时只能有一个。
value也可以为null,可以有多个。
2、底层数据结构,1.7/1.8有什么不同?
1.7:数组+链表
1.8:数组+(链表/红黑树)
HashMap中的静态属性:
//默认初始容量 - 必须是 2 的幂。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量。必须是 2 <= 1<<30 的幂
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子,当数组容量达到指定负载因子倍数的时候,会进行扩容
// 例16 * 0.75 = 12 16容量的数组put第13个元素的时候会进行扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树化阈值(条件一):
static final int TREEIFY_THRESHOLD = 8;
//树化阈值(条件二):
//这两个条件同时满足,链表转换红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
//反树化阈值(条件之一):达到该值,红黑树转换链表
static final int UNTREEIFY_THRESHOLD = 6;
HashMap中的成员属性:
//HashMap中的数组部分,用于存放链表node节点
transient HashMap.Node<K,V>[] table;
//保存缓存的 entrySet()。
transient Set<Map.Entry<K,V>> entrySet;
//键值映射的数量。
transient int size;
//记录HashMap的修改次数
transient int modCount;
//扩容的阈值(当前容量 * 负载因子)
int threshold;
//负载因子。
final float loadFactor;
hashMap:数组+链表转换数组+红黑树图示。
条件1:当某一桶内元素个数超过阈值8,并且数组长度小于64时,优先进行扩容;
条件2:当扩容后的长度达到64,并且某一桶内的元素个数仍然大于8的时候,进行转换
3、为什么要用红黑树,为什么不直接用红黑树,树化阈值为什么是8,什么时候树化,什么时候退化为链表?
为什么要用红黑树:当链表过长的时候影响hashmap性能,进行引用红黑树;
为什么不直接使用红黑树:当链表短的时候性能基本等价于红黑树,并且红黑树更占用内存,所以没必要;
为什么树化阈值为8:红黑树主要用来防止dos攻击,hash值如果足够随机,在负载因子0.75(元素个数与数组长度的比例大于负载因子时,进行扩容)的情况下,长度超过8的链表出现概率极低,选择8是为了树化几率更小
何时会树化:链表长度大于8,数组长度大于64的时候树化
何时退化为链表:
1、在扩容后进行树拆分时,树元素个数<=6则会退化为链表
2、remove树节点时,若root、root.left、root.right、root.left.left有一个为null,也会退化成链表
4、HashMap源码跟进
Map map = new HashMap();
map.put(“key”,“value”);源码跟进
注:源码中的代码太长了,我把执行该方法走过的代码留下其余全删掉了,简单记录了一下执行流程!!!
//判断当前数组是否为空,数组长度是否为0,如果某一条件满足,执行resize()方法
//resize()方法,就是一个扩容的方法
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
final HashMap.Node<K,V>[] resize() {
// 将table对象赋给局部变量 oldTab,当前为NULL;
HashMap.Node<K,V>[] oldTab = table;
// 将当前键值对的数量赋值给oldCap局部变量,当前为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 将上一次的扩容阈值赋给oldThr局部变量,这个值用于判断是否扩容的标准
// 当前值为0
int oldThr = threshold;
int newCap, newThr = 0;
// false
if (oldCap > 0) {}
// false
else if (oldThr > 0){}
//此处作者给了一段注释,意思前两个条件不满足,代表就是第一次进行容量初始化默认值为16
else { // zero initial threshold signifies using defaults
// 将默认数组初始容量值赋给newCap,newCap=16;
newCap = DEFAULT_INITIAL_CAPACITY;
// 设置扩容阈值,newThr=12;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 将扩容阈值,交给成员变量保存
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 重新创建了一个长度为为16的数组,上边给newCap赋了一个初始容量的值
HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];
// 将创建好的新数组交给成员变量table保存
table = newTab;
//当前方法执行完毕后
//成员变量threshold(扩容的阈值)=12,
//table = new HashMap.Node[16];
return newTab;//返回newTab是一个16容量的空数组
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//上边if执行完毕后,tab = 16容量的空数组,p=null,n=16,i=0;
//(n - 1) & hash 执行一个n-1后的值 和 hash 值的一个按位与运算重新得到一个hash值(也就是对应的桶下标),赋值给i
//tab[对应的桶下标]赋值给p
//如果p为null,代表当前位置没有数据,创建一个node节点,并添加到当前位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//集合操作次数+1
++modCount;
//添加完元素后进行一个 ++size 操作
// 在判断当前键值数量是否超过12(上一次执行resize()方法,保存的成员变量threshold值为12,忘记了请看上一张图)
// 如果超过12,执行resize()方法,在进行扩容操作
if (++size > threshold)
resize();
// Callbacks to allow LinkedHashMap post-actions
// void afterNodeAccess(Node<K,V> p) { }
// void afterNodeInsertion(boolean evict) { }
// void afterNodeRemoval(Node<K,V> p) { }
//这三个方法是HashMap留给LinkedHashMap用的,所以没有对其进行任何操作处理
afterNodeInsertion(evict);
// 当该方法执行完毕后,记录一下成员变量的值
// threshold=12;
// table = new HashMap.Node[16];
// modCunt = 1;
// size=1;
return null;
}
至此,第一次的put操作源码结束
执行第二次put先添加一个不同的键值对,看下源码的执行流程
Map map = new HashMap();
map.put(“key”,“value”);
map.put(“2”,“2”);
System.out.println(map);
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
//走到这个if判断的时候,还是对key的值根据算法进行一个桶下标的计算
//计算完以后,如果p的值等于null,证明当前桶内没有元素,执行一次添加元素的操作
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);//走完if以后else当然不进去直接到底下
++modCount;
//这块注意了哈,虽然if判断的结果为false,但是有个++size别忘了
if (++size > threshold)
resize();
afterNodeInsertion(evict);
// 再记录一下成员变量的值
// threshold=12;
// table = new HashMap.Node[16];
// modCunt = 2;
// size=2;
return null;
}
执行第三次put,添加一个相同的键值对
Map map = new HashMap();
map.put(“key”,“value”);
map.put(“2”,“2”);
map.put(“2”,“3”);
System.out.println(map);
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
HashMap.Node<K, V>[] tab;
HashMap.Node<K, V> p;
int n, i;
//这一步,比较简单,就是判断一下当前的数组是不是null,数组里边键值对数量 是否等于0
//除了第一次可以直接跳过这步
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//当再次执行这个if判断时,这次计算出来的p=tab[桶下标]里边明显是有值的就是上一次添加进来的2-2那个键值对
//所以这次不为null跳过这步,执行else
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//先记录一下当前的局部变量的值
/*
tab 为 16长度的Node类型数组
p 为上次添加进来的2-2键值对
n=16,i=2
*/
HashMap.Node<K, V> e;
K k;
//条件1、计算一下p的hash值,是否等于方法参数传进来的hash值
/*
说明一下p的hash值是怎么来的呢,是执行
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
这段代码,通过newNode传进去的,Node为HashMap的局部内部类,newNode为HashMap的成员方法
可以通过ctrl+鼠标左键点进去看一下
*/
//条件2、
//这段代码要注意括号哈,这里会优先进行p.key 赋值 k的操作,后面才能进行key.equals(k)的比较
//这段if主要用于判断后来put这个值的key和hash值是否等于现在桶内已有元素的key和hash值,下边会详细说明一下这个判断
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//当然if结束后不走这段哈可以跳过,这里简单说明一下这段代码
//这里就是判断一下当前这个p是否是TreeNode类型,TreeNode英文翻译一下为树节点
// 也就是判断一下当前桶内元素数据类型是否为红黑树,如果是,执行红黑树对应的方法
else if (p instanceof HashMap.TreeNode)
e = ((HashMap.TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
//如果key和hash值都不同,也不是树节点,说明当前桶内数据依然是链表形式
else {//当然这个也可以跳过
for (int binCount = 0; ; ++binCount) {
//这个p = tab[i = (n - 1) & hash],p是从这来的哈
//首先判断一下p后边是否还有元素,如果没有p后边没有元素了,进去
if ((e = p.next) == null) {
//把方法传进来这个新的键值对参数利用上,添加到p的后边
p.next = newNode(hash, key, value, null);
//这里相当于一个循环的退出条件
//当把新元素添加到链表后,立即判断原来的链表长度是否大于等于7
//因为树化的阈值为8,新添加的元素不在binCOunt的记数范围内所以判断条件为7
//如果条件满足,说明满足树化的第一个条件,执行对应方法再进行第二个条件的判断是否树化,并退出循环
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//这里依然是一个key和hash的判断,这里e的值,是p.next给的
//也就是说这里的循环是在遍历这个链表
//看一下链表上的每一个元素的key和hash是否等于要put的这个值得key和hash
//如果有一个是相同的,则退出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//如果都没有条件满足e赋值给p退出else
p = e;
}
}
//最后这段代码,其实就是将当前put传进来的value值做一个返回
//他这代码里边将方法参数的value传递给了一个方法,供linkedList使用
if (e != null) { // existing mapping for key
V oldValue = e.value;
//这个onlyIfAbsent是参数传进来的值,默认为false取反则为true 第二条件不做判断
if (!onlyIfAbsent || oldValue == null)
//注当前的e,是上边的p给的值
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
//对这段代码做一个简单的说明
//这段代码用于判断put进来的元素和当前的桶内元素做一个比较
/*
这里比较的两个元素:一个是p,不做再多的解释了回顾一下源码的p是怎么来的
另一个就是咱们putVal方法传进来的一系列的参数值
首先比较p的hash和方法参数的hash是否相等,因为咱们传的key都是2所以当前的hash值是相等的p.hash == hash --- true
然后再判断key是否相等,和hash同理(k = p.key) == key --- true
这里最最重要的是这个equals方法,这个方法的返回值取决于传入的参数类型key的类型
如果说参数key是一个String类型,那么是比较两个字符串的值是否相等
如果是其他类型(比如自己写的类,作为key)那么你就要重写equals方法,来自定义这个比较,进行返回
如果没有重写,默认调用object的equals比较的是两个对象的地址
当然他这里用的是 短路或 的操作如果 (k = p.key) == key 已然为true 后面不做判断
*/
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
hashmap先写到这把,以后有精力会对数组扩容和红黑树的部分进行补充
十、HashTable
特点:
1、键和值不能为null,否则抛空指针
2、HashTable是线程安全的,HashMap是线程不安全的
3、使用方法基本和HashMap相同
4、底层有数组HashTable$Entry[],初始化大小11
5、扩容值为2倍+1
6、负载因子 0.75
7、效率比HashMap低
J、Properties
1、继承HashTable实现Map接口,
2、使用特点和HashTable类似
3、该类通常用于加载 以properties结尾的文件(文件内容以键值对形式存储)
Q、TreeMap
TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
TreeMap基于红黑树实现。根据其键的自然顺序排序,或者由创建时提供的Comparator比较器排序,具体取决于使用的构造函数。
TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。
另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。
TreeMap不是线程安全的
通过Comparator构造TreeMap,会将第一个put进来的元素作为root保存
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
}
以后每次put根据比较器的排序规则选择left或right节点保存
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
//有Comparator执行的代码
if (cpr != null) {
do {//遍历所有的key,给当前key找到合适位置
parent = t;
cmp = cpr.compare(key, t.key);//动态绑定到我们实现的Comparator
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
//如果遍历过程中,发现准备添加key 和当前已有的key相等(由我们自己写的Comparator实现方式来决定),就不添加
else
return t.setValue(value);
} while (t != null);
}
//没有Comparator执行的代码
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
K、LinkedHashMap
不想自己画了,从网上借了一个图
Map接口的哈希表和链表实现,具有可预测的迭代顺序。此实现与HashMap的不同之处在于它维护一个双向链表,该列表贯穿其所有条目。这个链表定义了迭代顺序,通常是键插入映射的顺序(插入顺序)。请注意,如果将键重新插入,则插入顺序不会受到影响。 (如果在调用m.containsKey(k)将在调用之前立即返回true时调用m.put(k, v),则将键k重新插入到映射m中。)
王、Iterator 迭代机制
1、FailFast
不允许遍历的同时进行修改,如果修改抛出并发修改异常;
2、FailSafe
允许遍历的同时进行修改,牺牲一致性完成遍历