Java的集合容器
文章目录
整体集合架构
-
面向接口编程
-
Collection
- List
- ArrayList
- LinkedList
- Set
- HashSet
- TreeSet
- List
-
Map
- HashMap
- HashTable
- LinkedHashMap
- TreeMap
ArrayList
API分类
- 增
- add
- addAll
- 删
- remove
- removeAll
- clear
- removeIf
- 改
- set
- sort
- replace
- replaceAll
- 查
- retainAll
- listIterator
- iterator
- subList
- lastIndexOf
- isEmpty
- size
- indexOf
- get
要点
- 默认初始化长度是 10
- ArrayList和Vector集合之间的区别
- ArrayList是线程不安全的,Vector是线程安全的
- ArrayList扩容默认扩容1.5倍,Vector扩容的时候是2倍
- Iterator接口
- 所有集合都实现了这个接口,实现了了这个接口都有着增强for的能力
- 增强for的底层实现还是通过iterator
- Iterator使用的时出现的并发异常ConcurrentModificationException
- 异常产生原因和迭代器里面的游标属性相关,以remove举例
- 如果在使用迭代器的时候调用了外部的remove方法,那么不会更新迭代器中的游标属性值,就会抛出异常
- 如果迭代器使用了内部的remove方法,那么会对游标进行更新,那么就不会抛出异常
- 异常产生原因和迭代器里面的游标属性相关,以remove举例
源码解读
默认容器初始容量
注释里面就说ArrayList的默认初始化长度就是 10 。
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
并发容器异常
ConcurrentModificationException,产生异常的原因就是外面的list中的数组的结构发生了改变,但是迭代器里面的指针没有改变和更新,导致最终错误的发生。
public class CollectionDemo {
public static void main(String[] args) {
ArrayList<Object> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Object> iterator = list.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
if (next.equals(1)) {
list.remove(next);
}
}
}
}
解决方案
迭代器使用 listIterator 。
public class CollectionDemo {
public static void main(String[] args) {
ArrayList<Object> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Object> iterator = list.listIterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
if (next.equals(1)) {
iterator.remove(next);
}
}
}
}
迭代器 remove 源码讲解
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
总结:如果使用iterator进行迭代的过程中如果删除其中的某个元素会报错,并发操作异常,因此如果遍历的同事需要修改元素,建议使用listIterator()。
HashSet
API分类
- 增
- add
- 删
- remove
- clear
- 改
- set
- sort
- replace
- replaceAll
- 查
- iterator
- size
- isEmpty
- contains
要点
- HashSet的底层还是通过HashMap实现的,具体可以看其中的add方法的源码,里面添加元素的容器就是一个Map
源码解读
HashSet的底层容器实现
HashSet的本质还是HashMap
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
HashMap
API分类
- 增
- put
- putAll
- putIfAbsent
- 删
- remove
- removeAll
- clear
- 改
- replace
- merge
- replaceAll
- 查
- size
- isEmpty
- containsKey
- containsValue
- get
- getOrDefault
- keySet
- entrySet
要点
-
HashMap和HashTable之间的区别
- HashMap
- 效率高,但是线程不安全
- 其中的Value允许为空
- HashTable
- 线程安全,但是效率低
- HashTable中的value不允许为空
- HashMap
-
HashMap在JDK版本1.7和1.8的实现有一些改进
- 1.7
- 数据结构实现:数组+链表
- 1.8
- 数据结构实现:数组+链表+红黑树
- 增加了链表转红黑树的属性值字段(值为8)
- 1.7
-
HashMap的初始容量是2的N次幂
- 方便进行&操作,提高效率,&要比取模运算效率要高
- 在扩容之后涉及到元素的迁移过程,迁移的时候只需要判断二进制的前一位是0或者是1即可
- 如果是 0 ,表示数组和旧数组的下标的位置不变
- 如果是 1 ,只需要将索引位置加上旧的数组的长度值即可为新数组的下标
-
HashMap扩容的倍数默认是2倍
- 方便进行位运算,效率比较高,比较细节的操作
源码解读
JDK1.7源码
- DEFAULT_INITIAL_CAPACITY,默认初始化大小: 16
- DEFAULT_LOAD_FACTOR,扩容的因子: 0.75 当前容量到达容器最大容量的一定比例的时候,就进行扩容
put方法流程图
HashMap的初始容量是2的N次幂
- 方便进行&操作,提高效率,&要比取模运算效率要高
- 在扩容之后涉及到元素的迁移过程,迁移的时候只需要判断二进制的前一位是0或者是1即可
- 如果是 0 ,表示数组和旧数组的下标的位置不变
- 如果是 1 ,只需要将索引位置加上旧的数组的长度值即可为新数组的下标
HashMap扩容的倍数默认是2倍
- 方便进行位运算,效率比较高,比较细节的操作
put()方法
判断是否需要进行扩容
让后放入新的值
resize()方法
进行扩容和数据的移动
indexFor()方法
判断新插入的值应该插入Hash表的什么位置
JDK1.8源码
put方法流程图
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 数据是否为空
if ((tab = table) == null || (n = tab.length) == 0)
// 为空,resize创建数组并且设置阈值
n = (tab = resize()).length;
// 计算插入值在数组中的位置
// 判断目标位置是否为空
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))))
// 如果目标位置和table[i]指向的是同一个节点,并且hash和key值相同,那么修改原值,返回原值
e = p;
else if (p instanceof TreeNode)
// 如果table[i]是红黑树,遍历树
// 树里已经有了目标值,修改原值,返回原值
// 树里没有原值,那么红黑树插入新的值
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// table[i]是链表
// 遍历链表
for (int binCount = 0; ; ++binCount) {
// 链表里面没有值
if ((e = p.next) == null) {
// 链表里面插入值
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 如果链表长度大于8,将链表转成红黑树
treeifyBin(tab, hash);
break;
}
// 链表里面有相同的key,那么替换原值,返回原值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
// 如果已经存在之前的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;
}
相对于1.7来说新增了红黑树
TREEIFY_THRESHOLD = 8
- 表示HashMap中的数组+链表中的链表的长度如果超过 8 的话,就将链表转换为红黑树的数据结构
- Entry的名称改为了Node