一、集合框架图
Java集合框架主要包含两种类型的容器,一是集合(Collection),存储元素集合,二是图(Map),存储键(key)-值(value)对.Collection接口下面有两个重要的子接口List和Set,再下面是一些抽象类,最后是实现类,常用的实现类有HashSet、ArrayList、HashMap等等。
二、Set和List的区别
Set接口实例存储的是一组唯一,无序的对象。List实例存储的是一组不唯一,有序的对象。
Set遍历效率低,删除和插入效率高,删除和插入不会引起对象位置改变。
List遍历效率高,删除和插入效率低,因为删除和插入会引起对象位置改变。
三、集合实现类
HashMap
继承AbstractMap,是基于哈希表的Map接口的非同步实现,存储的对象是Node(包含key和value),key和value都可以为null,最多存在一个键值对的key为null。不保证有序(比如插入顺序),并且不保证顺序不随时间发生变化。
JDK1.7以前由数组+链表组成,JDK1.8由数组+链表+红黑树组成,使用链表是为了解决哈希冲突(两个对象的hashCode方法计算的哈希码值一致导致计算出的数组下标相同),当链表长度大于阈值(默认8)并且当前数组长度大于64时,此时此下标位置上的所有元素使用红黑树存储,注意如果仅仅是某个链表长度大于阈值,选择进行扩容。
底层数据结构:
1,new HashMap<>()
jdk1.7之前,创建了一个长度16的Entry数组,jdk1.8是在首次调用put方法时,创建一个长度16的Node数组。
2.put方法
对key做hash操作(先调用key的hashCode方法得到哈希码值,然后哈希码值跟它无符号右移16位的值做异或运算),再调用putVal方法
先判断Node数组是否为null或者长度等于0,如果是那么resize方法进行扩容
hash操作得到的值与数组长度-1做与运算得到数组下标
得到下标后,判断数组当前位置是否有元素,如果没有,将键值对放到数组当前位置
如果已有元素,那么调用新key的equals方法跟原有key进行比较,如果相同替换原有value,如果不相同将新的键值对放到链表的末尾或者放到红黑树中
上面流程完成后,如果size>threshold,那么调用resize方法扩容
3.resize方法
由于每次扩容都是翻倍,与原来的n-1&hash相比,只是多了一个二进制位,所以节点要么是原来位置,要么是原来位置+原容量这个位置。因此只需要判断原来的hash值新增的bit位是1还是0
问题:
1,为什么HashMap的容量是2的幂次方?容量是2的幂次方有什么好处?如果创建HashMap对象时实参不是2的幂次方会怎么样?
jdk1.8putVal方法计算数组下标的时候使用(数组长度 - 1) & hash,数组长度是2的幂次方的时候这个表达式的结果与hash%数组长度相同,并且能够让数据均匀分布减少哈希碰撞
tableSizeFor方法会计算出一个大于且最接近的2的幂次方数,通过一系列的无符号右移运算和与运算得出(思想是将这个数的最高位下面的bit位全部变成1,第一次运算保证最高位至少有两个连续的1,第二次运算保证最高位至少有四个连续的1。。。,最后一次运算保证最高位有十六个连续的1)
2.loadFactor负载因子为什么是0.75
当size>capacity*loadFactor时哈希表会扩容,如果太大会导致查询效率低,过小会导致数组的利用率低,存放的数据会很分散。0.75是官方经过大量测试得出的最优的结果。
JDK1.8HashMap的源码
重要属性
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的初始化容量16,HashMap容器的容量必须是2的次方
static final int MAXIMUM_CAPACITY = 1 << 30;//最大的容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认的负载因子,构造函数没有指定时使用
static final int TREEIFY_THRESHOLD = 8;//树化的阈值,哈希桶元素的数量至少为8时才会转化成红黑树
static final int UNTREEIFY_THRESHOLD = 6;//链表化的阈值,当进行resize操作时,哈希桶元素的数量不超过6时才会转化成链表
static final int MIN_TREEIFY_CAPACITY = 64;//树化最小容量,容量至少为64时才会转化成红黑树
transient Node<K,V>[] table;//HashMap容器实现存储功能的数组,必须是2的次方
transient Set<Map.Entry<K,V>> entrySet;//保存缓存的entrySet
transient int size;//HashMap容器包含的键值对数量
transient int modCount;//HashMap结构被修改的次数(是指键值对数量改变或者rehash)
int threshold;//进行resize操作的大小,=当前最大容量*负载因子
final float loadFactor;//负载因子
重要的内部类
Node:基本的哈希桶节点
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
TreeNode:继承LinkedHashMap的内部类Entry,而LinkedHashMap.Entry又继承Node,所以TreeNode算是Node的孙子类
TreeNode的方法太多,这里只贴出它的属性
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;
重要方法
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
//计算hash
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//先计算hashCode,然后将得到的值与它的高16位做异或运算,得到hash
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)//计算index,数组长度减一并与hash做与运算
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))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) //如果链表的长度大于等于树化阈值8,将链表转换成红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for 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;
}
//此方法只是将链表转换成双向链表,真正树化操作在treeify方法
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
//将双向链表转换成平衡二叉查找树
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
final Node<K,V>[] resize() {
//把没插入之前的哈希数组做我诶oldTal
Node<K,V>[] oldTab = table;
//old的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//old的临界值
int oldThr = threshold;
//初始化new的长度和临界值
int newCap, newThr = 0;
//oldCap > 0也就是说不是首次初始化,因为hashMap用的是懒加载
if (oldCap > 0) {
//大于最大值
if (oldCap >= MAXIMUM_CAPACITY) {
//临界值为整数的最大值
threshold = Integer.MAX_VALUE;
return oldTab;
}
//标记##,其它情况,扩容两倍,并且扩容后的长度要小于最大值,old长度也要大于16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//临界值也扩容为old的临界值2倍
newThr = oldThr << 1;
}
/**如果oldCap<0,但是已经初始化了,像把元素删除完之后的情况,那么它的临界值肯定还存在,
如果是首次初始化,它的临界值则为0
**/
else if (oldThr > 0)
newCap = oldThr;
//首次初始化,给与默认的值
else {
newCap = DEFAULT_INITIAL_CAPACITY;
//临界值等于容量*加载因子
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//此处的if为上面标记##的补充,也就是初始化时容量小于默认值16的,此时newThr没有赋值
if (newThr == 0) {
//new的临界值
float ft = (float)newCap * loadFactor;
//判断是否new容量是否大于最大值,临界值是否大于最大值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//把上面各种情况分析出的临界值,在此处真正进行改变,也就是容量和临界值都改变了。
threshold = newThr;
//表示忽略该警告
@SuppressWarnings({"rawtypes","unchecked"})
//初始化
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//赋予当前的table
table = newTab;
//此处自然是把old中的元素,遍历到new中
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
//临时变量
Node<K,V> e;
//当前哈希桶的位置值不为null,也就是数组下标处有值,因为有值表示可能会发生冲突
if ((e = oldTab[j]) != null) {
//把已经赋值之后的变量置位null,当然是为了好回收,释放内存
oldTab[j] = null;
//如果下标处的节点没有下一个元素
if (e.next == null)
//把该变量的值存入newCap中,e.hash & (newCap - 1)并不等于j
newTab[e.hash & (newCap - 1)] = e;
//该节点为红黑树结构,也就是存在哈希冲突,该哈希桶中有多个元素
else if (e instanceof TreeNode)
//把此树进行转移到newCap中
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { /**此处表示为链表结构,同样把链表转移到newCap中,就是把链表遍历后,把值转过去,在置位null**/
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;
}
}
}
}
}
//返回扩容后的hashMap
return newTab;
}
关于更详细的HashMap红黑树知识,请参考【Java入门提高篇】Day25 史上最详细的HashMap红黑树解析 - 弗兰克的猫 - 博客园
遍历HashMap
Map<String,String> map = new HashMap<String,String>();
map.put("郭如飞", "yz");
map.put("余豪", "lazy");
for (Entry<String, String> entry : map.entrySet())
{
System.out.println("键:"+entry.getKey()+",值:"+entry.getValue());
}
LinkedHashMap
TreeMap
Properties
HashSet
HashSet继承AbstractSet,实现Set接口,内部存储是HashMap。当HashSet中存储元素时,直接将这个元素作为key,默认的Object常量作为value,存储在map中。
TreeSet
LinkedHashSet
Hashtable和HashMap的区别
1、 hashmap中key和value均可以为null,但是hashtable中key和value均不能为null。
2、 hashmap采用的是数组(桶位)+链表+红黑树结构实现(jdk1.8之后),而hashtable中采用的是数组(桶位)+链表实现。
3、 hashmap中出现hash冲突时,如果链表节点数小于8时是将新元素加入到链表的末尾,而hashtable中出现hash冲突时采用的是将新元素加入到链表的开头。
4、 hashmap中数组容量的大小要求是2的n次方,如果初始化时不符合要求会进行调整,必须为2的n次方,而hashtable中数组容量的大小可以为任意正整数。
5、 hashmap中的寻址方法采用的是位运算按位与,而hashtable中寻址方式采用的是求余数。
6、 hashmap不是线程安全的,而hashtable是线程安全的,hashtable中的get和put方法均采用了synchronized关键字进行了方法同步。
7、 hashmap中默认容量的大小是16,而hashtable中默认数组容量是11。
ArrayList
继承AbstractList,实现了List接口。底层基于数组实现容量动态变化,允许元素为null,下面我们来看JDK1.8版本的ArrayList源码。
重要属性
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;
- DEFAULT_CAPACITY:默认容量大小
- EMPTY_ELEMENTDATA :空数组
- DEFAULTCAPACITY_EMPTY_ELEMENTDATA :默认容量的空数组
- elementData:ArrayList的内部结构,是一个Object[]类型的数组
- size:ArrayList包含的元素的数量
构造方法
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);
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
this.elementData = EMPTY_ELEMENTDATA;
}
}
- ArrayList(int initialCapacity):实例化ArrayList对象时指定容量大小。
- ArrayList():无参构造方法,通过此构造器创建的ArrayList,第一次使用add方法添加元素时,容量为10。
- ArrayList(Collection<? extends E> c):接收一个Collection的实例,将该实例转化成ArrayList对象。
add(E e)方法
先看add(E e)方法相关的源码:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
我们可以看到add(E e)方法首先会调用ensureCapacityInternal方法来检查容量大小,如果发现elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,即ArrayList通过无参构造器来实例化并且是第一次调用add方法,那么会将容量大小设置为默认容量大小10。
ensureExplicitCapacity方法判断如果所需的最小容量大于数组当前长度,那么需要扩容
grow方法来进行实际的扩容操作,新的数组长度是原来的3/2倍,并且新数组长度不能大于Integer.MAX_VALUE - 8,然后使用Arrays.copyOf方法将原来的数据复制到新数组中
使用ArrayList遇到的坑
如下代码中,在遍历List时,调用了remove方法,删除元素a
//arrayList中的值为 [a,a,c,a,a]
for (int i = 0; i < arrayList.size(); i++) {
if (arrayList.get(i) == "a") {
arrayList.remove(i);
}
}
System.out.println(arrayList);
这段代码看似解决了删除列表中所有的a元素,但是删除后得出List的结果为[a, c, a],为什么这种方式没有达到想要的效果,其实仔细分析后会发现,在调用remove()方法时List的长度会发生变化而且元素的位置会发生移动,从而在遍历时list实际上是变化的,例如
- 当i=0时,此时list中的元素为[a,a,c,a,a],
- 但当i=1时,此时List中的元素为[a,c,a,a],元素的位置发生了移动,从而导致在遍历的过程中不能达到删除的效果
解决方案
- 逆向遍历
- 使用迭代器遍历(推荐用此方法)
Iterator<String> ite = arrayList.listIterator();
while (ite.hasNext()){
if(ite.next() == "a")
ite.remove();
}
System.out.println(arrayList);
其他注意点
ArryList进行扩容时,需要将原有数组的元素拷贝到一个新数组中,非常耗时,所以建议在确定ArrayList元素数量的时候再使用它。
LinkedList
LinkedList继承了AbstractSequentialList,实现了List和Deque接口。本质上是一个双向链表。
由于实现了Deque接口,可以将LinkedList当做一个队列,去处理其中的元素。
AbstractSequentialList类实现连续访问数据(链表)所需的工作,如果是随机访问(数组)建议用AbstractList,而不是AbstractSequentialList
重要属性
transient int size = 0;//集合大小
transient Node<E> first;//指向头结点
transient Node<E> last;//指向尾结点
构造方法
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
内部类
Node用来表示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;
}
}
重要方法
add(E e)
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);//创建一个Node
last = newNode;//尾结点指向当前元素
if (l == null)
first = newNode;//如果last==null说明链表没有任何元素,此时添加的元素即为头结点
else
l.next = newNode;//将链表的最后一个元素指向新的元素
size++;
modCount++;
}
remove(Object o)、remove(int index)
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
public E remove(int index) {
checkElementIndex(index);
return unlink(node(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;
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
Vector
继承了AbstractList,实现了List接口,是一个线程安全的动态数组。
同ArrayList相比,除了线程安全(很多方法加了synchronized)外,其他地方基本一致。
Stack
继承了Vector,基于动态数组实现的一个线程安全的栈
集合类中常用的方法
Java集合框架的实现源码,经常看到 Arrays.copy()、System.arraycopy() 方法,以下稍作整理。
Arrays.copyOf()
作用:基于原数组复制一个新的数组并返回,不影响原有数组
我们这里看到copyOf方法有很多重载的方法,这里我只看public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType),因为其他方法同这个方法类似。
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
从源码可以看出来,copyOf方法是通过System.arraycopy方法来复制数组的。下面我们看一下
System.arraycopy
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
src:源数组
srcPos:源数组开始复制的索引
dest:目标数组
destPos:目标数组开始复制的索引
length:复制的长度
注意:System.arraycopy是浅复制,如果数组元素的类型是引用类型,复制的是引用类型对象的地址值,不是真的将原有对象复制一份。
Object[] toArray();<T> T[] toArray(T[] a);
Collection接口定义的两个抽象方法,集合实现类必须实现的两个方法,将集合转换成数组,区别是后者泛型方法。