一. 什么是集合?
首先要申明一点:集合是一个对象,在它的类里面包含了自己的属性和方法,并拥有非常丰富的功能。
其次,集合是一个容器,不同的集合实现了不同的数据结构,如栈、队列、链表、树等。利用不同集合的不同特点可以对数据进行不同形式的操作。
二。 集合出现的背景
早在 Java 2 中之前,Java 就提供了特设类。比如:Dictionary, Vector, Stack, 和 Properties 这些类用来存储和操作对象组。
虽然这些类都非常有用,但是它们缺少一个核心的,统一的主题。由于这个原因,使用 Vector 类的方式和使用 Properties 类的方式有着很大不同。 集合框架被设计成要满足以下几个目标。
- 该框架必须是高性能的。基本集合(动态数组,链表,树,哈希表)的实现也必须是高效的。
- 该框架允许不同类型的集合,以类似的方式工作,具有高度的互操作性。
- 对一个集合的扩展和适应必须是简单的。
为此,整个集合框架就围绕一组标准接口而设计。你可以直接使用这些接口的标准实现,诸如: LinkedList, HashSet, 和 TreeSet 等,除此之外你也可以通过这些接口实现自己的集合。
该内容摘自菜鸟教程
三。集合体系结构
Collection是大部分集合的顶级接口,定义了集合的一些通用方法,这样其他接口或者实现了Collection接口的对象都将使用这个通用方法,定义了集合操作的标准。
Map是key-value形式的集合顶级接口,功能和Colleciton接口一样,都是定义一个统一标准。
虽然这张图看起来有一些眼花缭乱,但是我们在实际使用中用到的不多,只有几个接口如List、Set、Queue、Map,由于接口不能够被直接实例化使用,所以我们需要了解它们每个接口下面具体的实现类构造。
四。常用集合分析
List接口
List是一个有序的,可重复的集合。它的主要实现类有两个:ArrayList、LinkedList.
ArrayList
特点及实现原理
- ArrayList集合的查询快,增删慢。 我们来看看ArrayList的属性定义:
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // 这是一个数组
ArrayList的底层用一个数组来存储数据的。大家都知道数组在被分配内存的时候分配的一段连续的内存。通过数组的首元素就可以非常快的定位到其他位置的元素,所以它的查询速度非常之快。 数组中的元素都是连续的,当向数组中增加或者删除一个元素时,为了保证数组内存的连续性,会将后续所有的元素整体的向后移动一位或者前进一位,这个代价是比较大的,所以增删比较慢。
- ArrayList长度可以自动扩容 查看源码可以知道,在ArrayList中有一个grow方法,用来进行集合的扩容,代码如下:
// 集合扩容方法
private void grow(int minCapacity) {
// 1. 获取到老数组的长度
int oldCapacity = elementData.length;
// 2. 设置新数组长度为老数组长度+老数组长度的一半。 >>1是移位操作,相当于原来1/2。
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 3. 如果新数组长度 < 最小容量
if (newCapacity - minCapacity < 0)
// 新数组长度 = 最小容量
newCapacity = minCapacity;
// 4. 如果新数组容量 > 最大长度
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 重新设置新数组容量
newCapacity = hugeCapacity(minCapacity);
// 5. 直接拷贝旧数组的内容到新数组中
elementData = Arrays.copyOf(elementData, newCapacity);
}
因此我们可以知道arraylist集合扩容的原理就是:重新创建一个长度为之前1.5倍大小的数组,然后使用Arrays.copyOf()方法将旧数组中的内容全部拷贝到新数组中来。
LinkedList
特点及实现原理
- 集合中元素增删快,查询慢 LinkedList是一个双向链表,链表的形成就是一个一个的节点,每一个节点指向自己的下一个就形成了链。源码中的节点定义如下:
private static class Node<E> {
E item; // 节点中的元素
Node<E> next; // 指向的下一个节点
Node<E> prev; // 指向的上一个节点
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
学过数据结构的同学应该都知道,节点之间的连接都是通过引用来指向下一个或者上一个的,因此要增加或者删除一个节点只需要修改一下这个引用就可以了,而不需要移动整体的节点位置,所以它的增删速度很快。要查找链表中的某一个元素必须要遍历整个链表才能够查询出来,时间复杂度是O(n),因此查询是比较慢的。
Set接口
Set是一个无序的集合,不能够存储重复的数据。
HashSet
- 无序集合如何实现? 看看源码的实现方式:
private transient HashMap<E,Object> map; // 定义了一个hashmap
private static final Object PRESENT = new Object(); // 创建了一个object对象
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
public void clear() {
map.clear();
}
........................................
在jdk源码中我们可以看到,set集合底层是通过hashmap来实现的。几乎所有方法都是直接调用了hashmap的方法。在添加元素的时候将被添加的元素作为key,固定的object对象作为value,然后进行添加。hashmap中的key是不能够重复的,因此set也不会发生重复。
Map接口
map是一个key-value形式的数据结构,查询和增删速度都非常快。
HashMap
- map的底层数据结构是什么 进入源码可以看到,在map中定义的所有属性:
//***** map中的数据存放位置 ********
transient Node<K,V>[] table;
// 默认初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子。【决定map在什么时候应该扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表转化为红黑树的临界点
static final int TREEIFY_THRESHOLD = 8;
// 红黑树退化为链表的临界点。
static final int UNTREEIFY_THRESHOLD = 6;
// 节点定义
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
// 树结构定义
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
所以说当前hashmap的数据结构是数组+链表+红黑树。 map中的所有元素存放在一个Node的数组中(数组结构),每一个数组位置是一个Node节点,节点可以组成一个链表(链表结构),当链表长度达到临界值后,链表就会转变为树结构,且该树为红黑树。
- map方法详细解析
putVal()方法
流程:
1.先判断数组是否为空,如果数组为空就初始化数组。
2.利用元素key的hash值和数组长度-1求位运算&,计算添加要添加的元素在数组上的具体位置。
3.如果该位置有值,判断key的hashcode和equals方法是否相等,相等就替换掉原有的旧值,不相等就依次遍历整个链表,判断是否相等。如果该位置是红黑树节点,那就以红黑树的方式来进行节点的添加。
// 数组中和hash取位运算&的作用 ==> 定位元素在数组中的位置
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 0. 初始条件设置
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1. 数组进行空值判断并赋值 tab = 旧数组值,n = 旧数组长度,
if ((tab = table) == null || (n = tab.length) == 0)
// 1.1 如果旧数组为空,对旧数组进行扩容。 扩容后n为新数组的长度
n = (tab = resize()).length;
// 2. table[(n-1)&hash]是计算出这个值在数组中的哪个位置并将结果赋值给p,判断该位置是否有值
if ((p = tab[i = (n - 1) & hash]) == null)
// 2.1 如果这个位置没有值,将这个新的值添加到 map中去。 设置下一个值为null
tab[i] = newNode(hash, key, value, null);
else {
// 2.2 如果要插入的这个地方有值
Node<K,V> e; K k;
// p是数组中原有的值
// 2.2.1 判断一下hashcode值是否重复,key地址是否相等==和equals同时进行判断
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 2.2.2 相等;旧值赋值给e
e = p;
// 2.2.3 如果不相等(可以插入),并且已经变成了红黑树结构,那么就变成红黑树节点插入进去
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 2.2.4 如果不相等,还是链表结构
else {
// 一个死循环
for (int binCount = 0; ; ++binCount) {
// 2.2.4.1判断该节点是否是最后一个节点
if ((e = p.next) == null) {
// 2.2.4.2 如果是,就将这个节点添加到后面去
p.next = newNode(hash, key, value, null);
// 2.2.4.3 如果标志已经到达可以形成红黑树结构了
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 2.2.4.4 转变为红黑树
treeifyBin(tabd, hash);
break;
}
// 如果该节点的hash值和插入元素hash值相同,并且key也相同,直接退出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//也就是 p = p.next
p = e;
}
}
// 如果这个位置元素不为空
if (e != null) { // existing mapping for key
// 获取到这个位置的旧值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
// 替换原有的值
e.value = value;
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 修改次数+1
++modCount;
// 如果map的长度> 扩容临界条件
if (++size > threshold)
// 扩容一下。
resize();
afterNodeInsertion(evict);
return null;
}
resize()扩容方法
1.先获取到原有数组中的数据,判断table数组是否为空,空的话对table数组进行初始化。
2.不为空的话设置新数组长度为老数组长度的两倍,扩容临界点也是老数组的两倍。
3.创建一个新的table数组,将原数组中的所有内容取出来重新进行hash散列放入到新数组中。
final Node<K,V>[] resize() {
//1. 获取到原有table的数据
Node<K,V>[] oldTab = table;
// 1.1 判断table是否为空,获取到table的长度--oldcap
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 1.2 获取到当前扩容临界点
int oldThr = threshold; // threshold是扩容的临界点:负载因子*当前容量
// 1.3 定义新map的长度,新map的扩容临界点
int newCap, newThr = 0;
// 2 如果原数组的长度大于0
if (oldCap > 0) {
// 2.1 原数组长度是否已经达到最大长度了
if (oldCap >= MAXIMUM_CAPACITY) {
// 2.2 如果是:扩容临界点= 最大值
threshold = Integer.MAX_VALUE;
// 2.3 不继续扩容了,直接返回老数组
return oldTab;
}
// 原数组长度未达到最大值,还可以继续扩容。
// 新数组长度 = 老数组长度的两倍(使用了位运算)【 扩容容量增大两倍!】
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 新扩容临界点 = 老扩容临界点的两倍
newThr = oldThr << 1; // double threshold
}
// 如果老的扩容点>0
else if (oldThr > 0) // initial capacity was placed in threshold
// 新map长度就等于老扩容临界点的值。
newCap = oldThr;
else {
// 默认第一次初始化hashmap时扩容走这里
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新的扩容点==0
if (newThr == 0) {
// 新扩容点=新数组长度*负载因子
float ft = (float)newCap * loadFactor;
// 新扩容点 = ft
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 正式设置扩容临界点
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 创建一个新的node数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 设置map中的table为该table
table = newTab;
// 如果老数组中的table不为空的话(猜测是进行赋值了)
if (oldTab != null) {
// 遍历老table
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 判断该位置的值是否为null
if ((e = oldTab[j]) != null) {
// 值不为null的话,把该处值获取出来,原值就设置为null(便于gc回收)
oldTab[j] = null;
// 如果下一个已经没有值了
if (e.next == null)
// 把老值赋值给新数组的具体位置上。扩容会重新进行hash
newTab[e.hash & (newCap - 1)] = e;
// 如果下一个有值,并且是红黑树节点
else if (e instanceof TreeNode)
// 进行红黑树相关的判定
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 该位置有值,并且是一个链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
getNode()方法
1.先判断数组是否为空,为空直接返回null
2.如果数组不为空并且该位置存在节点的话,就判断传入key的hashcode和该节点hashcode和equals是否相等,相等就直接返回,不相等就继续向下遍历,直到链表或者红黑树为null。
final Node<K,V> getNode(int hash, Object key) {
// 0. 初始化条件
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 如果数组不为空并且这个位置的元素存在,将这个值赋值给first
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 如果hash值相等,并且equals也相同的话
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
// 直接返回结果
return first;
// 如果这个位置的下一个节点存在的话
if ((e = first.next) != null) {
// 先判断是否是红黑树节点
if (first instanceof TreeNode)
// 如果是的话以红黑树节点的方式来返回。
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 如果这个节点的hash相等并且equals也相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 直接返回
return e;
} while ((e = e.next) != null);
}
}
// 没找到,返回null
return null;
}
remove() 方法
1.先判断数组不为空,为空直接返回null。
2.通过要删除key的hashcode和数组长度-1求&运算,计算到删除元素的位置。
3.该处的Node节点不为空。比较hashcode和equals方法是否相同,相同的话就删除掉这个节点,否则一直遍历,直到链表或者红黑树遍历结束为止。
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
// 0. 初始化条件 tab是数组。n是数组长度 p是要删除的节点
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 判断数组是否为空,将要删除位置的节点值赋值给p节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 如果这个位置有值,并且hashcode和equals都相等。
// 对定义的k,v赋值,它们的值就是要删除的这个节点的kv值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 将要删除被删除的节点提前赋值给node
node = p;
// 如果说下一个节点不为空(隐含了该节点已经不包含要删除的节点) e是链表中下一个节点
else if ((e = p.next) != null) {
// 判断一下节点的类型是否属于红黑树
if (p instanceof TreeNode)
// 是的话那就以红黑树的方式来进行
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 不是红黑树说明是一个链表,那么就需要进行遍历啦
do {
// 如果节点匹配,直接退出循环
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 如果该节点存在,并且互相匹配 node节点是要删除的这个节点
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 如果这个节点是红黑树
if (node instanceof TreeNode)
// 以红黑树的方式来删除该节点。
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 如果这个节点就是头节点
else if (node == p)
// index是要删除位置的索引值:将链表中下一个节点的值赋值到头位置中
tab[index] = node.next;
else
// 这个节点是普通的链表节点
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
ConcurrentHashMap
ConcurrentHashMap是一个线程安全的散列表,虽然说HashTable也是线程安全的散列表,但是ConcurrentHashMap的运行效率比HashTable要高很多,所以在多线程情况下我们应该使用ConcurrentHashMap。
- 实现的数据结构
ConcurrentHashMap实现的数据结构和HashMap相同,都是数组+链表+红黑树。
- 保证线程安全的方式
在jdk1.8之前使用segment来进行加锁,每一个segment都继承自ReetrentLock。也就是将原来的table数组分为一段一段的segment,当向map中添加元素时会进行两次hash,第一次hash定位到对应的segment,第二次hash定位到具体的table数组,理论上可以承受segment个数量的并发量。
在jdk1.8版本开始使用CAS+Synchronized来进行加锁保证线程安全。
部分源码分析:
Putval() 方法
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 存入的key和value不能为空
if (key == null || value == null) throw new NullPointerException();
// 计算key的hash值
int hash = spread(key.hashCode());
int binCount = 0;
// 将数组内容赋值给tab
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 如果数组内容为null
if (tab == null || (n = tab.length) == 0)
// 初始化数组
tab = initTable();
// case 1.数组该位置没有元素============如果 插入的key对应数组位置为null,将该位置赋值为f,也就是头节点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// ***** 使用cas方式来插入,插入成功就跳出循环,插入失败就继续循环尝试 *****
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
// 如果数组该位置不为空
// 初始化一个老值
V oldVal = null;
// 获取到头位置节点的监视器锁
// ***********************************
synchronized (f) {
if (tabAt(tab, i) == f) {
// 头节点的hash值大于0,说明是链表。为什么???
if (fh >= 0) {
binCount = 1;
// 循环,e是每次遍历的节点
for (Node<K,V> e = f;; ++binCount) {
K ek;
// case2:是链表,依次遍历比较链表内容======如果插入元素的hash值和equals方法和节点都相等
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
// 老值 = 当前数组节点的旧值
oldVal = e.val;
if (!onlyIfAbsent)
// 设置该节点的value为插入的value值
e.val = value;
// 方法结束....
break;
}
// 节点的数据不相等
Node<K,V> pred = e;
// case3:如果该节点已经是最后一个节点=================
if ((e = e.next) == null) {
// 那就创建一个新节点插入到元素后面
pred.next = new Node<K,V>(hash, key,
value, null);
// put完成,退出循环
break;
}
}
}
// 该节点是一个红黑树
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
// 以红黑树的方式来进行添加
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 看看是否需要扩容
addCount(1L, binCount);
return null;
}
红黑树简单介绍
在hashmap中使用到了红黑树这一种数据结构,到底什么是红黑树呢?为什么会使用红黑树呢?本节简单给大家介绍一下。
红黑树就是一种数据结构,一种自平衡的平衡二叉树,在平衡二叉树上进行了优化,减少了其频繁旋转造成的时间损耗,用来快速的查找数据。
最开始我们是在序列中直接遍历每一个元素来查找我们需要的元素,这样时间复杂度为O(n)。
二叉查找树可以用来很方便的进行二分查找,但是在构造二叉查找树的时候,如果序列本来就是有序的,那么该二叉查找树就会退化为一个线性列表。
二叉平衡树:保证任何一个节点的左右节点高度差不超过1。如何保持平衡?通过节点旋转的方式。节点一共有四种旋转方式。LL:右旋即可完成平衡,RR:左旋即可完成平衡,LR:先左旋,再右旋,RL:先右旋,再左旋四种方式。节点按照这种方式一一构建完成即可构成一颗二叉平衡树。它的查找、插入、删除在平均和最坏情况下时间复杂度都是O(logn)
可以看到,二叉平衡树在每次插入或者删除节点的时候都会判断构建的节点是否是平衡的,还要根据四种情况逐一的进行判断然后调整,它的要求也十分严格,左右子树的高度差不能够超过1,这种方式是比较耗时的。所以红黑树出来了,红黑树是一种自平衡的二叉平衡树。
- 红黑树的特点
- 每一个节点的颜色必须是红色或者黑色,根结点必须是黑色的。
- 红色节点的孩子必须是黑色的,不能有两个相同的红色节点连在一起。
- 每一个子节点到根结点的距离中黑色节点的数目是相同的。
- 由以上规则可以推理出:红黑树的最大高度不超过2log2(n+1)
- 对于一个有n个节点的红黑树而言,不论查找,删除,其时间复杂度都是O(logn)
五。两个常用的工具类
Arrays: 数组操作的工具类
常用方法有以下:
sort() : 可以对传入的数组进行排序。
fill(int[] array,int value):可以对数组内容进行填充。
copyof(int[] origin,int length):数组拷贝,拷贝旧数组的内容到新数组中。
copyofRange(int[] origin,int from,int to):拷贝一个指定长度的新数组。
asList(...T):返回一个arrayList。
Collection: 集合操作的工具类
- int binarySearch(List list,int key):二分查找某个值的位置。
- reverse(List list):反转集合数据。
- shuffle(List list):随机打乱集合数据。
- addAll(Collectopm co,...T):在集合中添加下列需要的数据
- swap(List list,int index1,int index2):交换list集合中index1和index2的位置。
- fill(List list,int value): 填充list中的值为value。
- copy(List list1,List list2):将list2中的值赋值到list1中来。
- replaceAll(List list,int oldValue,int newValue):替换list中的所有旧值为新值。