Java集合详解
一.了解集合
1.集合框架图
为了存储并处理大量的数据必须具备相应的存储结构,之前学习了数组,但是发现数组在使用的过程中有限制:数组长度一旦确定,就无法更改;除非采用建立新的数组,将原来的数组内容拷贝过去。数组中只能存放指定的数据类型,操作不方便。在实际的开发中,为了操作方便,JDK中提供了集合框架,即保存对象的容器。
简单来说集合就是用于存储对象的容器。
◣注意:
(1)所有的集合对象都属于java.util包。
(2)集合中如果没有指定泛型,默认的存放数据类型为Object。
集合结构图:集合容器因为内部数据结构的不同,有多种具体容器,不断的向上抽取,就形成了集合框架。框架的顶层是Collection接口。图中列出的为常用的以及面试中需要熟悉的集合。
从上图可以看出java中的集合很多,但是他们主要分为Collection和Map,Collection获取保存对象使用索引,我们可以把他看成类似于数组的类,Map则是以键-值对的方式存储对象(Map集合我们将在后面进行具体讲述)。Collection分为三种不同的形式,分别是List,Queue和Set接口,然后就是对应的不同的实现方式。
注意:文中源码均基于JDK1.8进行分析。
2.Iterable顶层接口
package java.lang;
import java.util.Iterator;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;
public interface Iterable<T> {
//Iterator方法
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}
Iterable接口中只有iterator()一个接口方法,Iterator也是一个接口,其主要有hasNext()和next()方法。
3.Collection接口
package java.util;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
//继承的是Iterable接口
public interface Collection<E> extends Iterable<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
boolean retainAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, 0);
}
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
}
Collection常用操作方法总结如下
返回类型 | 方法名称 | 说明 |
---|---|---|
boolean | add(数据); | 添加数据,返回是否添加成功 |
boolean | clear(); | 清除集合中的元素 |
boolean | contains(元素); | 判断集合中是否包含指定元素 |
containsAll(集合对象); | 判断集合中是否包含另一个集合 | |
boolean | isEmpty() | 判断集合中是否存在元素(不能用来判断null) |
Object | remove(元素); | 删除第一次出现的指定元素 |
int | size(); | 获取集合的长度 |
Object[] | toArray() | 将集合转换成数组 |
Iterator | iterator() | 迭代 |
二.List集合
1.List接口
package java.util;
import java.util.function.UnaryOperator;
public interface List<E> extends Collection<E> {
<T> T[] toArray(T[] a);
boolean addAll(Collection<? extends E> c);
boolean addAll(int index, Collection<? extends E> c);
default void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final ListIterator<E> li = this.listIterator();
while (li.hasNext()) {
li.set(operator.apply(li.next()));
}
}
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
boolean equals(Object o);
E get(int index);
E set(int index, E element);
void add(int index, E element);
int indexOf(Object o);
int lastIndexOf(Object o);
ListIterator<E> listIterator();
List<E> subList(int fromIndex, int toIndex);
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.ORDERED);
}
}
从以上源代码中可以看出Collection和List的区别:
(1)List比Collection多了get/set/indexOf等方法,支持index下标操作。
(2)Collection和List最大的区别就是Collection是无序的,不支持索引操作,而List是有序的,因此List接口支持使用sort方法。
(3) 二者的Spliterator操作方式不一样。
(4)List中Iterator为ListIterator。
2.List接口常用的三个实现类
注意:文章中的源码分析均根据JDK1.8版本来分析
2.1 ArrayList
2.1.1 ArrayList底层实现和构造函数
package java.util;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
//默认初始容量
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//该elementData是真正存放元素的容器,可见ArrayList是基于数组实现的
transient Object[] elementData; // non-private to simplify nested class access
private int size;
//ArrayList构造函数
//带初始容量参数的构造函数。(用户自己指定容量)
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {//初始容量大于0
this.elementData = new Object[initialCapacity];//创建initialCapacity大小的数组
} else if (initialCapacity == 0) {//初始容量等于0
this.elementData = EMPTY_ELEMENTDATA;//创建空数组
} else {//初始容量小于0,抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//ArrayList支持默认大小构造,和空构造,当空构造的时候存放数据的Object[] elementData是一个空数组{}
//默认构造函数,使用初始容量10构造一个空列表(无参数构造)
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//构造包含指定Collection元素的列表,这些元素利用该集合的迭代器按顺序返回,如果指定的集合为null,throws NullPointerException。
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;
}
}
}
从以上源代码中可以看出ArrayList是基于数组实现的,以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为10。
2.1.2 ArrayList扩容机制
当初始化的list是一个空ArrayList的时候,会直接扩容到DEFAULT_CAPACITY,该值大小是一个默认值10。而当添加进ArrayList中的元素超过了数组能存放的最大值就会进行扩容。下面将分步骤来详细讲解ArrayList扩容机制,以无参构造函数创建的 ArrayList 为例进行分析。
首先看一下添加元素的方法
//将指定的元素追加到此列表的末尾
public boolean add(E e) {
//添加元素之前,先调用ensureCapacityInternal方法
ensureCapacityInternal(size + 1); // Increments modCount!!
//这里看到ArrayList添加元素的实质就相当于为数组赋值
elementData[size++] = e;
return true;
}
再看add 方法 调用的ensureCapacityInternal(size + 1)方法
//得到最小扩容量
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 获取默认的容量和传入参数的较大值
// 当要add 进第1个元素时,minCapacity为1,在Math.max()方法比较后,minCapacity 为10
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
再看ensureExplicitCapacity() 方法
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
//调用grow方法进行扩容,调用此方法代表已经开始扩容了
grow(minCapacity);
}
/**
》当我们要 add 进第1个元素到 ArrayList 时,elementData.length为0(因为还是一个空的list),
因为执行了 ensureCapacityInternal() 方法 ,所以 minCapacity 此时为10。
》此时,minCapacity - elementData.length > 0 成立,所以会执行grow(minCapacity)方法。
当add第2个元素时,minCapacity 为2,此时e lementData.length(容量)在添加第一个元素后扩容成10了。
》此时,minCapacity - elementData.length > 0 不成立,所以不会执行grow(minCapacity)方法。
》当添加第3、4···到第10个元素时,依然不会执行grow方法,数组容量都为10。
》直到添加第11个元素,minCapacity(为11)比elementData.length(为10)要大,
此时进入grow方法进行扩容。
*/
再看grow(int minCapacity)方法
//要分配的最大数组大小
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// ArrayList扩容的核心方法
private void grow(int minCapacity) {
//oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
//采用右移运算,就是原来的一半,所以扩容1.5倍。比如10的二进制是1010,右移后变成101就是5
int newCapacity = oldCapacity + (oldCapacity >> 1);
//然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,就把最小需要容量作为数组的新容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量大于 MAX_ARRAY_SIZE,则执行hugeCapacity()方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
//如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
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第1个元素时,oldCapacity 为0,经比较后第一个if判断成立,newCapacity = minCapacity(为10)。但是第二个if判断不会成立,即newCapacity 不比 MAX_ARRAY_SIZE大,则不会进入 hugeCapacity 方法。数组容量为10,add方法中 return true,size增为1。
》当add第11个元素进入grow方法时,newCapacity为15,比minCapacity(为11)大,第一个if判断不成立。新容量没有大于数组最大size,不会进入hugeCapacity方法。数组容量扩为15,add方法中return true,size增为11。
》以此类推····
*/
最后看hugeCapacity() 方法
//对minCapacity和MAX_ARRAY_SIZE进行比较
//若minCapacity大,将Integer.MAX_VALUE作为新数组的大小
//若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小
//MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
最后说一下扩容机制中还有一个ensureCapacity方法
这个方法 ArrayList 内部没有被调用过,所以很显然是提供给用户调用的,这个方法的作用是:向 ArrayList 添加大量元素之前最好先使用ensureCapacity 方法,以减少增量重新分配的次数。
/**
如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。
* @param minCapacity 所需的最小容量
*/
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
2.1.3 ArrayList中arraycopy()方法实现数组复制
阅读源码的话,我们就会发现 ArrayList 中大量调用了System.arraycopy() 和 Arrays.copyOf()这两个方法。比如:我们上面讲的扩容操作以及add(int index, E element)、toArray() 等方法中都用到了该方法。
//在此列表中的指定位置插入指定的元素。
//先调用 rangeCheckForAdd 对index进行界限检查;
//然后调用 ensureCapacityInternal 方法保证capacity足够大;
//再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
//arraycopy()方法实现数组自己复制自己
//elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量;
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
java是无法自己分配空间的,是底层C和C++的实现。以C为例,我们知道C中数组是一个指向首部的指针,比如我们C语言对数组进行分配内存。Java就是通过arraycopy这个native方法实现的数组的复制。
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
copyOf() 内部实际调用了 System.arraycopy() 方法
// 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素);
//返回的数组的运行时类型是指定数组的运行时类型。
public Object[] toArray() {
//elementData:要复制的数组;size:要复制的长度
return Arrays.copyOf(elementData, size);
}
二者区别:arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 ,copyOf() 是系统自动在内部新建一个数组,并返回该数组。
2.1.4 ArrayList实现序列化安全
为什么中elementData用transient修饰?
transient Object[] elementData; // non-private to simplify nested class access
(1)transient的作用是该属性不参与序列化。
(2)ArrayList继承了标示序列化的Serializable接口。
(3)对arrayList序列化的过程中进行了读写安全控制。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
/**
* Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,deserialize it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
在序列化方法writeObject()方法中可以看到,先用默认写方法,然后将size写出,最后遍历写出elementData,因为该变量是transient修饰的,所有进行手动写出,这样它也会被序列化了。那是不是多此一举呢?
protected transient int modCount = 0;
当然不是,其中有一个关键的modCount, 该变量是记录list修改的次数的,当写入完之后如果发现修改次数和开始序列化前不一致就会抛出异常,序列化失败。这样就保证了序列化过程中是未经修改的数据,保证了序列化安全。(java集合中都是这样实现)
2.2 LinkedList
从集合框架图就可以看到,LinkedList既是List接口的实现也是Queue接口的实现(Deque),它可以根据索引来随机的访问集合中的元素,还实现了Deque接口,它还是一个队列,可以被当成双端队列来使用。故其和ArrayList相比LinkedList支持的功能更多。
在介绍LinkedList之前我们先复习一下有关链表的知识:
链表是由一系列非连续的节点组成的存储结构,简单分类的话,链表分为单向链表和双向链表,而单向和双向链表又可以分为循环链表和非循环链表。
-
单向链表就是通过每个结点的指针指向下一个结点从而链接起来的结构,最后一个节点的next指向null。
-
单向循环链表和单向列表的不同是,最后一个节点的next不是指向null,而是指向head节点,形成一个“环”。
-
双向链表是包含两个指针的,pre指向前一个节点,next指向后一个节点,但是第一个节点head的pre指向null,最后一个节点的tail指向null。
-
双向循环链表和双向链表的不同在于,第一个节点的pre指向最后一个节点,最后一个节点的next指向第一个节点,也形成一个“环”。而LinkedList就是基于双向循环链表设计的。
2.2.1 LinkedList底层实现和构造函数
package java.util;
import java.util.function.Consumer;
// LinkedList由一个头节点和一个尾节点组成,分别指向链表的头部和尾部。
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;//默认为0的size
transient Node<E> first;//头结点
transient Node<E> last;//尾节点
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
}
可见,LinkedList中节点Node的结构是由一个头节点,一个尾节点和一个默认为0的size构成,可见LinkedList的底层是基于双向链表实现的。其中,LinkedList中Node源码如下,由当前值item,和指向上一个节点prev和指向下个节点next的指针组成。并且只含有一个构造方法,按照(prev, item, next)这样的参数顺序构造。
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;
}
}
2.2.2 链表的头插法和尾插法
/**
* Links e as first element.
*/
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
/**
* Links e as last element.
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
2.2.3 LinkedList查询
//获取第index个节点
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
//node(index)方法的实现
Node<E> node(int index) {
// assert isElementIndex(index);
// 判断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;
}
}
//checkElementIndex(index)方法:检查节点是否合法
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
ArrayList随机访问比LinkedList快的原因: LinkedList要遍历找到该位置才能进行修改,而ArrayList是内部数组操作会更快。
//查询索引修改方法,先找到对应节点,将新的值替换掉旧的值。
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
2.2.4 LinkedList插入和删除
//添加元素--采用尾插法将新节点放入尾部
public boolean add(E e) {
linkLast(e);
return true;
}
// Links e as last element.
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
//删除指定节点
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) {//如果前驱为null, 说明此节点为头节点
first = next;
} else {
prev.next = next;//前驱结点的后继节点指向当前节点的后继节点
x.prev = null;//当前节点的前驱置空
}
if (next == null) {//如果当前节点的后继节点为null ,说明此节点为尾节点
last = prev;//当前节点的后继置空
} else {
next.prev = prev;//当前节点的后继节点的前驱指向当前节点的前驱节点
x.next = null;
}
x.item = null;//当前节点的元素设置为null ,等待垃圾回收
size--;
modCount++;
return element;
}
2.2.5 ArrayList和LinkedList的比较
ArrayList和LinkedList均设计为非线程安全的,ArrayList内部采用的是数组,LinkedList内部采用的是链表结构。
(1)ArrayList : 采用数组保存元素,意味着当大量添加元素,数组空间不足时,依然要通过新建数组,内存复制的方式来增加容量,效率比较低,当对数组进行插入、删除操作的时候又会进行循环移位操作,效率也比较低,只有进行按照下标进行查询的首使用数组效率很高。
(2) LinkedList : 采用链表结果保存元素,在添加元素的时候只需要进行简单的一次内存分配即可,效率较高,进行插入和删除操作,只需要对链表中相邻的元素进行修改即可,效率也很高,但是进行按照下标查询时,需要链表进行遍历,效率低下。
例如:
public static void main(String[] args) {
/**
* LinkedList和ArrayList中的操作是一样的
* 查找的时候:ArrayList效率比较高(使用频率比较高)
* 添加和删除:LinkedList效率比较高
*/
List list1=new LinkedList();
List list2=new ArrayList();
/*for (int i = 0; i < 5; i++) {
list1.add(i);
}
System.out.println(list1.toString());
*/
for (int i = 0; i < 100000; i++) {
list1.add(i);
}
for (int i = 0; i < 100000; i++) {
list2.add(i);
}
long start=System.currentTimeMillis();
System.out.println(start);
for (int i = 0; i < list2.size(); i++) {
//System.out.println(list2.get(99899));
list2.remove(999);
}
long end=System.currentTimeMillis();
System.out.println("ArrayList所用时常:"+(end-start));
long start1=System.currentTimeMillis();
for (int i = 0; i < list1.size(); i++) {
//System.out.println(list1.get(99899));
list1.remove(999);
}
long end1=System.currentTimeMillis();
System.out.println("LinkedList所用时常:"+(end1-start1));
}
2.3 Vector
Vector和Stack在集合中用的特别少,因此这里只是简单介绍一下。
2.3.1 Vector底层原理和构造方法
public class Vector<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
protected Object[] elementData; //存放元素的数组,底层使用数组来实现
protected int elementCount;//有效元素数量,小于等于数组长度
protected int capacityIncrement;//容量增加量,和扩容相关
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector() {
this(10);
}
public Vector(Collection<? extends E> c) {
elementData = c.toArray();
elementCount = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}
}
2.3.2 Vector线程安全性
vector是线程安全的,其中的操作方法都使用synchronized修饰,这里只列举了一个方法。
这里提到了线程安全性和非线程安全性,这部分内容我将在之后的博客中进行总结讲解,欢迎关注我。
public synchronized void copyInto(Object[] anArray) {
System.arraycopy(elementData, 0, anArray, 0, elementCount);
}
2.3.3 Vector扩容
看源码可知,扩容当构造没有capacityIncrement时,一次扩容数组变成原来两倍,否则每次增加capacityIncrement。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
2.3.4 Vector方法示例
数组移除某一元素并且移动后,一定要将原来末尾设为null,且有效长度减1。
//例如:移除某个元素
public synchronized E remove(int index) {
modCount++;
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
E oldValue = elementData(index);
int numMoved = elementCount - index - 1;
if (numMoved > 0)
//复制数组,假设数组移除了中间某元素,后边有效值前移1位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//引用null ,gc会处理
elementData[--elementCount] = null; // Let gc do its work
return oldValue;
}
2.3.5 ArrayList和Vector比较
(1)ArrayList :底层使用数组来实现(数组集合),(长度是由系统自动扩充),非线程安全的类(建议使用),安全性低,可共享的。
(2)Vector : 底层使用数组来实现,是线程安全的类,效率低于ArrayList。
总的来说,ArrayList与Vector的内部实现类似,Vector设计为线程安全的,是为了保证线程安全;因此Vector在性能方面逊于ArrayList。Vector使用较少。
3.List与数组的区别
List集合与数组的用途非常相似,都是用来存储大量数据的,不同点有:
(1)数组长度在使用之前必须确定,一旦确定不能改变,而List集合长度是可变的。
(2)数组中必须存放同一类型的数据,List集合中可以存放不同类型的数据,集合中不可以存储基本数据类型值,集合是用于存储对象的容器。
三.Set集合
如果说List对集合加了有序性的化,那么Set就是对集合加上了唯一性。
Set特点:
▪ 不能添加重复的数据,如果数据重复在添加的过程中会返回false。
▪ 添加的数据是无序的(通过对象的hash码值进行存放的)。
▪ Set集合中常用的类有HashSet和TreeSet。
1. Set接口
java中的Set接口和Colletion是完全一样的定义。
package java.util;
public interface Set<E> extends Collection<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean retainAll(Collection<?> c);
boolean removeAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT);
}
}
**Set中常用的方法:**与Collection中的方法一样,因为它继承于Collection接口
返回类型 | 方法名称 | 作用 |
---|---|---|
boolean | add(Object o) | 添加元素 |
void | clear() | 移除Set集合中的所有元素 |
boolean | contains(Object o) | 判断集合中是否存在指定元素 |
boolean | isEmpty() | 判断集合是否为空 |
Iterator | iterator() | 返回Set集合中对元素迭代的迭代器 |
boolean | remove(Object o) | 从集合中删除元素 |
int | size() | 返回集合中的元素数量 |
Object[] | toArray() | 将集合转换成数组 |
Set集合的遍历:
方式一:通过迭代器进行遍历
(1)通过集合的iterator()方法获取迭代器对象。
(2)调用迭代器的hasNext()方法判断是否有下一条元素。
(3)如果有下一条元素则通过迭代器的next()方法获取元素。
Iterator it=set.iterator();
//it.hasNext():判断迭代器中是否有下一个元素
while (it.hasNext()) {
//it.next();返回下一个元素
Object obj=it.next();
System.out.println(obj);
}
方式二:增强版的for循环
增强版的for循环适用于所有的集合(数组)遍历
语法:
for(数据类型 变量名 : 集合对象/数组){
//循环体
}
◣ 注意:数据类型必须是集合中存放的数据的数据类型。
for (Object obj : set) {
System.out.println(obj);
}
2. Set接口常用的实现类
注意:文章中的源码分析均根据JDK1.8版本来分析
2.1 HashSet
2.1.1 HashSet底层实现和构造函数
package java.util;
import java.io.InvalidObjectException;
public class HashSet<E> extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
private transient HashMap<E,Object> map; //基于hashmap
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
public Iterator<E> iterator() {
return map.keySet().iterator();
}
public int size() {
return map.size();
}
public boolean isEmpty() {
return map.isEmpty();
}
public boolean contains(Object o) {
return map.containsKey(o);
}
public boolean add(E e) { //添加元素基于hashmap的put方法,插入的值会作为HashMap的key
return map.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
public void clear() {
map.clear();
}
}
从HashSet源码可以看到HashSet内部其实是一个HashMap。
对于HashMap的实现,后面将会重点介绍到!!
2.1.2 HashSet存储原理
从HashSet的源码中就可以看出:
(1)HashSet是基于HashMap实现的,底层数据结构是哈希表,本质就是对哈希值的存储,默认构造函数是构建一个初始容量为16,负载因子为0.75 的HashMap。封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素(见HashSet中add方法)实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。当存储第一个元素时,内部给定一张hash表(数组),初始为16个长度,每个元素都有一个hash值,根据hash值在hash表中可以计算出位置,如果该位置为null,则直接存储;否则与该位置的链表元素一一比较,如果找到相同的,则确定唯一性。
(2)我们前面说到List是对集合加上了有序性,而Set是对集合加上了唯一性,那么hashSet为了保证元素的唯一性,必须重写元素的hashCode方法和equals方法,当hashCode值不相同(即算出的元素存储的位置目前没有任何元素存储),则直接存储了,不用再判断equals了,当hashCode值相同时(即算出的元素的存储位置目前已经存在有其他的元素了),会再调用一次equals方法,判断其返回值是否为true,如果是则视为用重复元素,不用存储;如果为false,代表两个对象不是同一个对象,该对象可以添加进去。这些哈希值相同内容不同的元素都存放在一个桶里(哈希表中有一个桶结构,每一个桶都有一个哈希值)。
① 重写equals方法
Set中的数据是无序的,并不可重复,可在存放数据的时候是根据对象的hashcode码进行存放的当两个对象的hashCode值相等的时候,还要去比较两个对象的equals方法是否相等,如果equals方法返回true,那么代表两个对象是同一个,不能添加进去,否则代表两个对象不是同一个对象,该对象可以添加进去。
② 重写hashCode()方法
Set集合中存放数据时,要尽量避免经常发生hashCode相同的冲突,这会降低程序的性能,所以要在存放的对象的类中重写hashCode()方法,用来保证hashCode是唯一的。
◣ 注意:
▪ 当两个对象的hashCode相等的时候,equals不一定相等
▪ 如果两个对象的equals相等,那么他们的hashCode肯定相等。
例如
Student类
package gaoji2;
public class Student{
private int id;
private String name;
private int age;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Student(int id, String name, int age) {
super();
this.id = id;
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student [id=" + id + ", name=" + name + ", age=" + age + "]";
}
@Override
public boolean equals(Object obj) {
System.out.println("equals被执行了");
//1.判断对象obj是否为空
if(obj==null){
return false;
}
//2.判断是否属于同一类型的对象
if(!(obj instanceof Student)){
return false;
}
//3.判断内容是否一样
Student stu=(Student) obj;
if(stu.getAge()==age&&stu.getName().equals(name)&&stu.getId()==id){
return true;
}
return false;
}
@Override
public int hashCode() {
System.out.println("hashCode被执行了");
return name.hashCode()+age+id;
}
}
2.2 TreeSet
2.2.1 TreeSet底层实现和构造函数
package java.util;
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
private transient NavigableMap<E,Object> m;// 使用 NavigableMap 的 key 来保存 Set 集合的元素
private static final Object PRESENT = new Object(); // 使用一个 PRESENT 作为 Map 集合的所有 value
// 构造器以指定的 NavigableMap 对象创建 Set 集合
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
public TreeSet() {
this(new TreeMap<E,Object>());
}
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
public TreeSet(Collection<? extends E> c) {
this();
addAll(c);
}
public TreeSet(SortedSet<E> s) {
this(s.comparator());
addAll(s);
}
public Iterator<E> iterator() {
return m.navigableKeySet().iterator();
}
public Iterator<E> descendingIterator() {
return m.descendingKeySet().iterator();
}
public NavigableSet<E> descendingSet() {
return new TreeSet<>(m.descendingMap());
}
public int size() {
return m.size();
}
public boolean isEmpty() {
return m.isEmpty();
}
public boolean contains(Object o) {
return m.containsKey(o);
}
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return m.remove(o)==PRESENT;
}
public void clear() {
m.clear();
}
}
从TreeSet源码中我们可以看到,TreeSet的底层是TreeMap,例如添加元素就是调用TreeMap的put方法,添加的数据存入了map的key的位置,而value则固定是PRESENT。TreeSet中的元素是有序且不重复的,因为TreeMap中的key是有序且不重复的。
关于TreeMap的实现原理将在后面map中介绍!!
2.2.2 TreeSet存储原理
(1)元素如何存储进去?
TreeSet底层的数据结构是红黑树(是一个自平衡的二叉树),存储第一个元素时,直接作为根节点存储;存储第二个元素时,元素与根节点进行比较,如果比根小,看左子树是否有元素,如果为null,直接存储;否则,以左子树为根继续比较,如果比根大,看右子树是否有元素,如果为null,直接存储,否则,以右子树为根继续比较,如果找到相等的,则确定唯一性。
(2)元素如何取出?
可以通过前序遍历、中序遍历和后序遍历取出元素。
(3)TreeSet底层是如何保证元素的排序和唯一性的?
根据源码,我们知道,TreeSet的底层代码可以通过两种方式来实现其元素的有序性,分别是自然排序和比较器排序,根据比较的返回值是否是0来决定是否唯一。
- 自然排序(元素具备比较性)
compareTo()这个方法是定义在 Comparable里面的,所以你要想重写该方法,就必须是先实现Comparable接口,这个接口表示的就是自然排序。因此让元素所属的类实现Comparable接口,重写其中的compareTo方法是第一种方式。
例如:自己写了一个类如下
public class Student implements Comparable{
private int id;
private String name;
private int age;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Student(int id, String name, int age) {
super();
this.id = id;
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student [id=" + id + ", name=" + name + ", age=" + age + "]";
}
@Override
public int compareTo(Object o) {
//定义比较规则:根据对象的age进行升序排列
//判断后返回1 -1 0
/**1:大于
* -1:小于
* 0:等于
*/
//1.判断对象o是否为空
if(o==null){
return 0;
}
//判断对象是否属于同一类型
if(o instanceof Student){
//强制类型转换
Student stu=(Student) o;
//根据对象的age进行判断
if(stu.getAge()>age){
return 1;
}else if(stu.getAge()<age){
return -1;
}else{
return 0;
}
}
return 0;
}
}
测试
Set set=new TreeSet();
/*set.add("5");
set.add("2");
set.add("8");
set.add("3");
set.add("6");
set.add("4");*/
set.add(new Student(1, "zhangsan1", 16));
set.add(new Student(8, "zhangsan8", 20));
set.add(new Student(2, "zhangsan2", 32));
set.add(new Student(6, "zhangsan6", 19));
set.add(new Student(4, "zhangsan4", 18));
for (Object o : set) {
System.out.println(o);
}
结果
- 比较器排序(集合具备比较性)
让集合构造方法接收Comparator的实现类对象,在实现类中重写compare()方法。这种方法通过调用集合的带参构造来实现比较,例如下面源码中的带参构造器,在addAll方法中用到了。
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
public Comparator<? super E> comparator() {
return m.comparator();
}
public boolean addAll(Collection<? extends E> c) {
// Use linear-time version if applicable
if (m.size()==0 && c.size() > 0 &&
c instanceof SortedSet &&
m instanceof TreeMap) {
SortedSet<? extends E> set = (SortedSet<? extends E>) c;
TreeMap<E,Object> map = (TreeMap<E, Object>) m;
Comparator<?> cc = set.comparator();
Comparator<? super E> mc = map.comparator();
if (cc==mc || (cc != null && cc.equals(mc))) {
map.addAllForTreeSet(set, PRESENT);
return true;
}
}
return super.addAll(c);
}
注意的是:根据对源码和底层数据结构红黑树的理解,无论是通过自然排序还是通过比较器排序,数据结构核心是通过将根节点和子节点比较来进行。
2.2.3 HashSet和TreeSet的比较
(1)底层存储的数据结构不同
HashSet:底层使用哈希表结构存储,元素允许为空;
TreeSet:使用树结构进行存储,不可重复,不能存放null数据。
(2)存储时保证数据唯一性依据不同
HashSet是通过重写hashCode()方法和equals()方法来保证;
TreeSet是通过Comparable接口的compareTo()方法来保证的。
(3)有序性不同
HashSet元素无序;
TreeSet是排序后的Set集合。
3.List和Set的区别
(1)List,Set都是继承自Collection接口;
(1)List : 存放的元素有放入的顺序,并且数据是可以重复的。
(2)Set : 存放的元素无放入的顺序,数据是不可以重复。
四.Map集合
Map特点:
Map集合用来保存具有映射关系的数据,即以键值对(key-value)的方式来存储数据。因此在Map集合的内部有两个集合,一个集合用于保存Map中的key(键),一个集合来保存Map中的value(值),其中key和value可以是任意的数据类型。
(1)Map中的key是唯一的,value没有限制。
(2)map集合中一个key只能对应一个value。
(3)如果存储数据的时候key重复了则会覆盖前面的数据。
1. Map接口
package java.util;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.io.Serializable;
public interface Map<K,V> {
// Query Operations
int size();
boolean isEmpty();
boolean containsKey(Object key);
boolean containsValue(Object value);
V get(Object key);
// Modification Operations
V put(K key, V value);
V remove(Object key);
// Bulk Operations
void putAll(Map<? extends K, ? extends V> m);
void clear();
Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K, V>> entrySet();
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getKey().compareTo(c2.getKey());
}
public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getValue().compareTo(c2.getValue());
}
public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
}
public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
}
}
// Comparison and hashing
boolean equals(Object o);
int hashCode();
default V getOrDefault(Object key, V defaultValue) {
V v;
return (((v = get(key)) != null) || containsKey(key))
? v
: defaultValue;
}
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch(IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
action.accept(k, v);
}
}
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Objects.requireNonNull(function);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch(IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
// ise thrown from function is not a cme.
v = function.apply(k, v);
try {
entry.setValue(v);
} catch(IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
}
}
default V putIfAbsent(K key, V value) {
V v = get(key);
if (v == null) {
v = put(key, value);
}
return v;
}
default boolean remove(Object key, Object value) {
Object curValue = get(key);
if (!Objects.equals(curValue, value) ||
(curValue == null && !containsKey(key))) {
return false;
}
remove(key);
return true;
}
default boolean replace(K key, V oldValue, V newValue) {
Object curValue = get(key);
if (!Objects.equals(curValue, oldValue) ||
(curValue == null && !containsKey(key))) {
return false;
}
put(key, newValue);
return true;
}
default V replace(K key, V value) {
V curValue;
if (((curValue = get(key)) != null) || containsKey(key)) {
curValue = put(key, value);
}
return curValue;
}
default V computeIfAbsent(K key,
Function<? super K, ? extends V> mappingFunction) {
Objects.requireNonNull(mappingFunction);
V v;
if ((v = get(key)) == null) {
V newValue;
if ((newValue = mappingFunction.apply(key)) != null) {
put(key, newValue);
return newValue;
}
}
return v;
}
default V computeIfPresent(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
V oldValue;
if ((oldValue = get(key)) != null) {
V newValue = remappingFunction.apply(key, oldValue);
if (newValue != null) {
put(key, newValue);
return newValue;
} else {
remove(key);
return null;
}
} else {
return null;
}
}
default V compute(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
V oldValue = get(key);
V newValue = remappingFunction.apply(key, oldValue);
if (newValue == null) {
// delete mapping
if (oldValue != null || containsKey(key)) {
// something to remove
remove(key);
return null;
} else {
// nothing to do. Leave things as they were.
return null;
}
} else {
// add or replace old mapping
put(key, newValue);
return newValue;
}
}
default V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
Objects.requireNonNull(value);
V oldValue = get(key);
V newValue = (oldValue == null) ? value :
remappingFunction.apply(oldValue, value);
if(newValue == null) {
remove(key);
} else {
put(key, newValue);
}
return newValue;
}
}
Map接口本身就是一个顶层接口,由一堆Map自身接口方法和一个Entry接口组成,Entry接口定义了主要是关于Key-Value自身的一些操作,Map接口定义的是一些属性和关于属性查找修改的一些接口方法。
Map集合常用方法:
返回类型 | 方法名称 | 作用 |
---|---|---|
Object | put(Object key,Object value) | 添加元素,返回于此有关的value |
void | clear() | 清空集合中的数据 |
boolean | containsKey(Object key) | 根据key从集合中判断key是否存在 |
boolean | containsValue(Object value) | 根据value从集合中判断value是否存在 |
Object | get(Object key) | 根据key获取对应的value值 |
Set | keySet() | 返回Map集合的key的集合 |
int | size() | 返回集合中元素的数量 |
Object | remove(Object key) | 删除集合中key对应的元素 |
Map集合的遍历:
方式一:根据Map集合的key集合进行遍历
▪ 通过集合的keySet()方法获取所有的键的集合
▪ 遍历该键的集合,使用get(key)获取值
//获取map集合中所有的key--------set集合
Set set=map.keySet();
//获取set集合的迭代器
Iterator it=set.iterator();
//循环遍历迭代器
while(it.hasNext()){
Object o=it.next();//遍历拿到key
System.out.println("key:"+o+"---->value:"+map.get(o));
}
/*for (Object o : set) {
//遍历key的集合然后根据key获取相应的值
System.out.println("key:"+o+"---->value:"+map.get(o));
}*/
方式二:根据Map集合的EntrySet(键-值对)集合进行遍历
▪ 获取整个Map集合的entry对象集合,使用entrySet()方法获取
▪ 遍历entry对象集合通过entry对象的getKey和getValue获取键和值
//获取EntrySet集合
System.out.println("-------------entry------------");
Set set1=map.entrySet();
for (Object ob : set1) {
//Entry对象代表map集合中的一条映射关系,里面封装了key和value
Entry en=(Entry) ob;
System.out.println("key:"+en.getKey()+"---->value:"+en.getValue());
}
2. Map接口常用的实现类
2.1 HashMap
HashMap是Java中最常用K-V容器,采用了哈希的方式进行实现,HashMap中存储的是一个又一个Key-Value的键值对,我们将其称作Entry,HashMap对Entry进行了扩展(称作Node),使其成为链表或者树的结构存储在HashMap的容器里(是一个数组)。
特点:
(1)key可以为空(null),由于键不可重复,因此只能有一个key为null, value可以为空(null)。
(2)HashMap中的数据是无序的,键不可重复的。
(3)非线程安全的,效率高。
2.1.1 HashMap底层实现原理
JDK1.8中hashMap的组成
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//默认初始容量,是HashMap的最小容量为16,容量就是数组的大小也就是变量,transient Node<K,V>[] table。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量,该数组最大值为2^31次方。
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的加载因子,如果构造的时候不传则为0.75,用于扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//一个位置里存放的节点转化成树的阈值,也就是8,比如数组里有一个node,这个node链表的长度达到8才会转化为红黑树。
static final int TREEIFY_THRESHOLD = 8;
//一个反树化的阈值,当这个node长度减少到该值就会从树转化成链表
static final int UNTREEIFY_THRESHOLD = 6;
//满足节点变成树的另一个条件,就是存放node的数组长度要达到64
static final int MIN_TREEIFY_CAPACITY = 64;
//具体存放数据的数组
transient Node<K,V>[] table;
//entrySet,一个存放k-v缓冲区
transient Set<Map.Entry<K,V>> entrySet;
//size是指hashMap中存放了多少个键值对
transient int size;
//对map的修改次数
transient int modCount;
//阈值 表示当前HashMap能够承受的最多的键值对数量,一旦超过这个数量HashMap就会进行扩容
int threshold;
//加载因子
final float loadFactor;
}
从以上源码中可以看出hashMap的组成是什么样的,由于文中所有源码的分析均基于JDK1.8,因此,在JDK 1.8中,HashMap的底层数据结构是“数组+链表+红黑树”,即在链表的长度超过阈值8时转化为红黑树结构。而在1.8之前,HashMap的底层数据结构只有“数组+链表”。这样做的原因是什么?接下来具体分析一下HashMap的底层实现。
首先,我们先回顾一下数组和链表。
数组的特点:它的存储区间是连续的,占用内存严重,空间复杂也很大,时间复杂为O(1)。
优点:是随机读取效率很高,因为数组是连续的(随机访问性强,查找速度快)。
缺点:插入和删除数据效率低,由于插入数据的时候插入位置后面的数据在内存中要往后移动,且大小固定不易动态扩展。链表的特点:区间离散,占用内存宽松,空间复杂度小,时间复杂度O(N)。
优点:插入删除速度快,内存利用率高,没有大小固定,扩展灵活。
缺点:不能随机查找,每次都是从第一个开始遍历(查询效率低)。
那么,有没有一种数据结构,查询效率高并且插入删除的效率也高呢?哈希表就是这样一种数据结构,哈希表是基于哈希函数建立的一种查找表,hash函数就是根据key计算出应该存储地址的位置(地址index=H(key)),它其实就是将数组和链表两种数据结构相结合,简单的说就是每个元素都是链表的数组。在JDK1.8之前,HashMap由数组和链表来实现对数据的存储,也就是哈希表,使用链表处理hash冲突,具体的在源码中,HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体,以此来解决Hash冲突的问题。下图表示了JDK1.7的HashMap数据结构。
在JDK1.8中,对HashMap的底层实现进行了优化,数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,当链表长度超过阈值(8)时,将链表转换为红黑树,当长度降到6就转成普通bin。在性能上进一步得到提升。下图表示了JDK1.8的HashMap数据结构。
接下来分析一下JDK1.8的源码中涉及到的数据结构,也解释一下为什么链表长度为8时要转换为红黑树的问题?
在HashMap的源码注释中其实已经说明其实现结构。
/*
* Implementation notes.
*
* This map usually acts as a binned (bucketed) hash table, but
* when bins get too large, they are transformed into bins of
* TreeNodes, each structured similarly to those in
* java.util.TreeMap.
* 说是map通常当做binned(存储桶)的哈希表,但是当bin太大时,它们将转换为TreeNodes的bin,每个bin的结构与java.util.TreeMap中的相似。
(1)数组
//具体存放数据的数组
transient Node<K,V>[] table;
(2)链表
//数组元素Node<K,V>实现了Entry接口,Node是单向链表,它实现了Map.Entry接口
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
//构造函数(Hash值, 键, 值, 下一个节点)
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;
}
//判断两个node是否相等,若key和value都相等,返回true。
public final boolean equals(Object o) {
if (o == this)//可以与自身比较为true
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;
}
}
Map接口中有一个Entry接口,在HashMap中对其进行了实现,Entry的实现是HashMap存放的数据的类型。其中Entry在HashMap的实现是Node,Node是一个单链表的结构,TreeNode是其子类,是一个红黑树的类型。
(3)红黑树
/**
* Entry for Tree bins.(bin是指的table中某一个位置的node,一个node可以理解成一批数据。)
* Extends LinkedHashMap.Entry (which in turn
* extends Node) so can be used as extension of either regular or
* linked node.
*/
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; //颜色属性
//构造函数包含hash,键,值和下一个节点
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
/**
* Returns root of tree containing this node.返回当前节点的根节点
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
我们都知道,链表的时间复杂度是O(n),红黑树的时间复杂度O(logn),很显然,红黑树的复杂度是优于链表的,既然这样,那为什么hashmap不直接使用红黑树呢?
从时间复杂度来分析,红黑树的平均查找长度是log(n),如果长度为8,平均查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,而log(6)=2.6,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
/*Because TreeNodes are about twice the size of regular nodes, we
* use them only when bins contain enough nodes to warrant use
* (see TREEIFY_THRESHOLD). And when they become too small (due to
* removal or resizing) they are converted back to plain bins. In
* usages with well-distributed user hashCodes, tree bins are
* rarely used. Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million
从具体源码中来分析:源码中的注释写的很清楚,因为树节点所占空间是普通节点的两倍,所以只有当节点足够多的时候,才会使用树节点。也就是说,节点少的时候,尽管时间复杂度上,红黑树比链表好一点,但是红黑树所占空间比较大,综合考虑,认为只能在节点太多的时候,红黑树占空间大这一劣势不太明显的时候,才会舍弃链表,使用红黑树,说白了就是trade-off,空间和时间的权衡。那为什么达到阈值8才会选择使用红黑树呢?当hashCode离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,而且根据统计,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。所以选择阈值是8,是根据概率统计决定的。而且此时链表的性能已经很差了。所以在这种比较罕见和极端的情况下,才会把链表转变为红黑树。因为链表转换为红黑树也是需要消耗性能的,特殊情况特殊处理,为了挽回性能,权衡之下,才使用红黑树来提高性能。也就是说在大部分情况下,hashmap还是使用的链表,如果是理想的均匀分布,节点数不到8,hashmap就会自动扩容,看下面链表转为红黑树的方法的源码就可以:
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
* 除非hash表太小(在这种情况下将调整大小),否则将替换给定哈希值的索引处bin中所有链接的节点。
*/
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);
}
}
在该方法中,有这样一个判断,数组长度小于MIN_TREEIFY_CAPACITY,就会扩容,而不是直接转变为红黑树,显然HashMap认为,虽然链表长度超过了8,但是table长度太短,只需要扩容然后重新散列一下就可以,因此可不是什么链表长度为8就变为红黑树,还有别的条件。
//满足节点变成树的另一个条件,就是存放node的数组长度要达到64
static final int MIN_TREEIFY_CAPACITY = 64;
因此在通常情况下,链表长度很难达到8,但是特殊情况下链表长度为8,哈希表容量又很大,造成链表性能很差的时候,就会采用红黑树提高性能,这就是为什么链表长度为8时要转换为红黑树的原因。那我们红黑树退化为链表的情况又是怎么样的呢?
2.1.2 HashMap构造函数
//只带有初始容量的构造函数
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//无参构造函数
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//用m的元素初始化散列映射
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
//带有初始容量和负载因子的有参构造函数
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0) //容量不能为负数
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)//当容量大于2^31就取最大值1<<31;
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))//负载因子为正
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//当前数组table的大小,一定是2的幂次方
//tableSizeFor方法保证了数组一定是是2的幂次方,是大于initialCapacity最接近的值
this.threshold = tableSizeFor(initialCapacity);//新的阈值
}
/**
* Returns a power of two size for the given target capacity.
*/
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;
}
该方法将一个二进制数第一位1后边的数字全部变成1,然后再加1,这样这个二进制数就一定是100…这样的形式。此处实现在ArrayDeque的实现中也用到了类似的方法来保证数组长度一定是2的幂次方。返回距离指定参数最近的2的整数次幂,例如7返回8, 8返回8, 9返回16等。
2.1.3 HashMap扩容机制resize()
先说下前面多次提到的负载因子:源码中有个公式为threshold = loadFactor * 容量。HashMap和HashSet都允许你指定负载因子的构造器,表示当负载情况达到负载因子水平的时候,容器会自动扩容,HashMap默认使用的负载因子值为0.75f(当容量达到四分之三进行再散列(扩容))。当负载因子越大的时候能够容纳的键值对就越多但是查找的代价也会越高。所以如果你知道将要在HashMap中存储多少数据,那么你可以创建一个具有恰当大小的初始容量这可以减少扩容时候的开销。但是大多数情况下0.75在时间跟空间代价上达到了平衡,所以不建议修改。
扩容resize() 函数会在两种情况下被调用:
(1) HashMap new 出来后还没有 put 元素进去,没有真正分配存储空间被初始化,调用 resize() 函数进行初始化;
(2) 原 table 中的元素个数达到了 capacity * loadFactor 的上限,需要扩容。此时调用 resize(),new 一个两倍长度的新 Node 数组,进行rehash,并将容器指针(table)指向新数组。
由此引申出一个问题HashMap是先插入还是先扩容:HashMap初始化后首次插入数据时,先发生resize扩容再插入数据,之后每当插入的数据个数达到threshold时就会发生resize,此时是先插入数据再resize。
/**
* 初始化或增加表大小。 如果为空,则根据字段阈值中保持的初始容量目标进行分配.
* 否则,因为我们使用的是2的幂,所以每个bin中的元素必须保持相同的索引,或者在新表中以2的幂偏移。
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//首次初始化后table为Null
int oldCap = (oldTab == null) ? 0 : oldTab.length;//获取原哈希表容量 如果哈希表为空则容量为0 ,否则为原哈希表长度
int oldThr = threshold;//获取原哈希表扩容门槛,默认构造器的情况下为0
int newCap, newThr = 0;//初始化新容量和新扩容门槛为0
//如果原容量大于 0,这个if语句中计算进行扩容后的容量及新的负载门槛
if (oldCap > 0) {
//判断原容量是否大于等于HashMap允许的容量最大值2^30
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;//满足则把当前HashMap的扩容门槛设置为Integer允许的最大值
return oldTab;//不再扩容直接返回
}
/**
* newCap = oldCap << 1 ; 类似 newCap = oldCap * 2 移位操作更加高效
* 表示把原容量的二进制位向左移动一位,扩大为原来的2倍,同样还是2^n
* 如果新的数组容量 < HashMap允许的容量最大值2^30
* 并且原数组容量 >= 默认的初始化数组容量16
*/
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; //新的扩容门槛为原来的扩容门槛的2倍,同样二进制左移操作
}
/**
* 如果原数组容量小于等于零并且原负载门槛大于0,则新数组容量为原负载门槛大小
*/
else if (oldThr > 0)
newCap = oldThr;
/**
* 在默认构造器下进行扩容:初始化默认容量和默认负载门槛
* 如果原数组容量小于等于0并且原负载门槛也小于等于0,则新数组容量为默认初始化容量16
* 新负载门槛为默认负载因子(0.75f) * 16=12;
*/
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
/**
* 如果新负载门槛为0,则开始使用新的数组容量进行计算
*/
if (newThr == 0) {
float ft = (float)newCap * loadFactor; // 新的数组容量 * 负载因子
//如果新数组容量小于HashMap允许的最大容量并且新计算的负载门槛小于HashMap允许的最大容量,则新的负载门槛为计算后的值的最大整型,否则新的负载门槛为Integer.MAX_VALUE
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;//设置全局负载门槛为计算后的新的负载门槛
/**
* 根据新的数组容量创建新的哈希桶 赋值给newTab
*/
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;//把新创建的哈希桶赋值给全局table
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// 当前index没有发生hash冲突,直接对2取模,即移位运算hash &(2^n -1)
// 扩容都是按照2的幂次方扩容,因此newCap = 2^n
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 当前index对应的节点为红黑树
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 把当前index对应的链表分成两个链表,减少扩容的迁移量
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; // help gc
newTab[j + oldCap] = hiHead; // 扩容长度为当前index位置+旧的容量
}
}
}
}
}
return newTab;
}
当对hashMap扩容时,会调用resize方法,resize会以桶(一个哈希表下标下的所有元素)为单位,把元素转移到新哈希表中, 当桶为红黑树时,会调用split方法进行红黑树的扩容移动处理,split方法中会出现红黑树退化为链表情况(整个hashMap中也只有这个方法会出现红黑树退化为链表的情况)。在split方法中,将当前红黑树分割放入lo,hi这两个变量(treeNode类型)里。可以先理解为两条链表。当lo链表中元素<=6,会将lo转换为真正的链表(Node类型),当lo链表中元素>6,则会进行树化处理, 例如对链表中节点的left,right等进行赋值,hi树同理。
/**
* Splits nodes in a tree bin into lower and upper tree bins,
* or untreeifies if now too small. Called only from resize;
* see above discussion about split bits and indices.
*
* @param map the map
* @param tab the table for recording bin heads
* @param index the index of the table being split
* @param bit the bit of hash to split on
*/
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
2.1.4 HashMap存取原理
(1)HashMap中put()
put()实现原理:第一步:将k,v封装到Node对象当中(节点)。第二步:它的底层会调用K的hashCode()方法得出hash值。第三步:通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
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;
//当hash到的位置,该位置为null的时候,存放一个新node放入,p赋值成了table该位置的node值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//该位置第一个就是查找到的值,将p赋给e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果是红黑树,调用红黑树的putTreeVal方法
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) {//注意e = p.next这个一直将下一节点赋值给e,直到尾部,注意开头是++binCount
p.next = newNode(hash, key, value, null);
//当链表长度大于等于7,插入第8位,树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
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;
}
/**
* Tree version of putVal.
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&(q = ch.find(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
(2)HashMap中get()
get()实现原理:第一步:先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。第二步:通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。重点理解如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着参数K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//先判断表不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
//找到要查询的Key在table中的位置,table是存放HashMap中每一个Node的数组
(first = tab[(n - 1) & hash]) != null) {
//Node可能是一个链表或者树,先判断根节点是否是要查询的key
if (first.hash == hash &&
((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 {
//链表查找
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);//遍历链表,当链表后续为null则退出循环
}
}
return null;
}
/**
* Calls find for root node.
*/
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
/**
* Finds the node starting at root p with the given hash and key.
* The kc argument caches comparableClassFor(key) upon first use
* comparing keys.
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}
2.2 TreeMap
特点:
(1)在hashMap的基础上,根据key进行排序;
(2)TreeMap中的key不能为null;
(3)TreeMap中的键的对象必须实现Comparable接口。
2.3 HashTable
HashTable和HashMap的操作是相同的,区别如下:
(1)HashTable是线程安全的,HashMap是非线程安全的。所以HashMap比HashTable的性能更高。
(2)HashTable不允许用于null值作为key或者value,但是HashMap是可以的。
2.3.1 HashTable底层实现原理
HashTable和HashMap的实现原理几乎一样,差别无非是以上两点,但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。
HashTable性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是JDK1.7中的ConcurrentHashMap所采用的"分段锁"思想。
下面我们主要讨论ConcurrentHashMap的实现。
2.4 ConcurrentHashMap
我们知道,哈希表是复杂度为O(1)的数据结构,在java中对于HashMap和HashTable的使用也很频繁,但是在多线程环境下,使用HashMap在并发执行put操作是会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成唤醒数据结构,Entry的next节点永远不为空,就会产生死循环。为了避免这种隐患,强烈建议使用ConcurrentHashMap代替HashMap。 ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现,ConcurrentHashMap在并发编程的场景中使用频率非常之高,接下来分析下ConcurrentHashMap的实现原理,并对其实现原理进行分析(包括JDK1.8和JDK1.7之间的对比)。
2.4.1 ConcurrentHashMap底层实现原理(JDK1.7)
在JDK1.7中,ConcurrentHashMap采用了"分段锁"策略,ConcurrentHashMap的主干是个Segment数组。一个ConcurrentHashMap维护一个Segment数组,一个Segment维护一个HashEntry数组,HashEntry是目前我们提到的最小的逻辑处理单元了。
可以看到,Segment类似于HashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于同一个Segment的操作才需考虑线程同步,不同的Segment则无需考虑。
final Segment<K,V>[] segments;
transient volatile HashEntry<K,V>[] table;
Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock),通过每次锁住一个segment来保证每个segment内的操作的线程安全性从而实现全局线程安全
static class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
final float loadFactor;
Segment(float lf) { this.loadFactor = lf; }
}
Segment构造方法
Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
this.loadFactor = lf;//负载因子
this.threshold = threshold;//阈值
this.table = tab;//主干数组即HashEntry数组
}
ConcorrenthashMap的构造方法
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
//MAX_SEGMENTS 为1<<16=65536,也就是最大并发数为65536
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
//2的sshif次方等于ssize,例:ssize=16,sshift=4;ssize=32,sshif=5
int sshift = 0;
//ssize 为segments数组长度,根据concurrentLevel计算得出
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
//segmentShift和segmentMask这两个变量在定位segment时会用到
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//计算cap的大小,即Segment中HashEntry的数组长度,cap也一定为2的n次方.
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
//创建segments数组并初始化第一个Segment,其余的Segment延迟初始化
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0);
this.segments = ss;
}
初始化方法有三个参数,如果用户不指定则会使用默认值,initialCapacity为16,loadFactor为0.75(负载因子,扩容时需要参考),concurrentLevel为16。
注意:从上面的代码可以看出来,Segment数组的大小ssize是由concurrentLevel来决定的,但是却不一定等于concurrentLevel,ssize一定是大于或等于concurrentLevel的最小的2的次幂。比如:默认情况下concurrentLevel是16,则ssize为16;若concurrentLevel为14,ssize为16;若concurrentLevel为17,则ssize为32。为什么Segment的数组大小一定是2的次幂?其实主要是便于通过按位与的散列算法来定位Segment的index。(具体可以查看前面对于HashMap中扩容的分析)
ConcorrentHashMap 中的put()方法
public V put(K key, V value) {
Segment<K,V> s;
//concurrentHashMap不允许key/value为空
if (value == null)
throw new NullPointerException();
//hash函数对key的hashCode重新散列,避免差劲的不合理的hashcode,保证散列均匀
int hash = hash(key);
//返回的hash值无符号右移segmentShift位与段掩码进行位运算,定位segment
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
从源码看出,put的主要逻辑是:首先定位segment并确保定位的Segment已初始化,然后调用Segment的put方法。
segmentShift和segmentMask这两个全局变量的主要作用是用来定位Segment,int j =(hash >>> segmentShift) & segmentMask。
(1)segmentMask:段掩码,假如segments数组长度为16,则段掩码为16-1=15;segments长度为32,段掩码为32-1=31。这样得到的所有bit位都为1,可以更好地保证散列的均匀性
(2)segmentShift:2的sshift次方等于ssize,segmentShift=32-sshift。若segments长度为16,segmentShift=32-4=28;若segments长度为32,segmentShift=32-5=27。而计算得出的hash值最大为32位,无符号右移segmentShift,则意味着只保留高几位(其余位是没用的),然后与段掩码segmentMask位运算来定位Segment。
ConcorrentHashMap 中的get()方法
public V get(Object key) {
Segment<K,V> s;
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
//先定位Segment,再定位HashEntry
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
get方法无需加锁,由于其中涉及到的共享变量都使用volatile修饰,volatile可以保证内存可见性,所以不会读取到过期数据。
2.4.2 ConcurrentHashMap底层实现原理(JDK1.8)
我们以put()方法为例来讨论ConcurrentHashMap的并法操作原理:
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());//计算hash值
int binCount = 0;//记录链表的长度
for (Node<K,V>[] tab = table;;) {//当线程出现竞争时不断进行自旋操作
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)//如果数组为空,则进行数组初始化
tab = initTable();//初始化数组的方法
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//以volatile读的方式读取table数组中的元素,通过hash值对应的数组下标得到第一个节点
if (casTabAt(tab, i, null, //如果该下标返回的节点为空,直接通过CAS算法将新的值封装成node值插入。如果CAS失败,说明存在竞争
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) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
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;
}
(1)ConcorrentHashMap相比于1.7版本,JDK 1.8做了哪些改进?
- 取消了segment分段设计,直接使用Node数组来保存数据,并且采用Node数组元素作为锁来实现每一行数据进行加锁来进一步减少并发冲突的概率。
- 将原本数组+链表的数据结构变更为了数组+链表+红黑树的结构。为什么要引入红黑树呢?在正常情况下,key hash之后如果能够很均匀的分散在数组中,那么table数组中的每个队列的长度主要为0或者1,但是实际情况下,还是会存在一些队列长度过长的情况。如果还采用单向列表方式,那么查询某个节点的时间复杂度就变为O(n); 因此对于队列长度超过8的列表,JDK1.8采用了红黑树的结构,那么查询的时间复杂度就会降低到O(logN),可以提升查找的性能。(具体分析可以查看前面关于HashMap中1.8版本红黑树的介绍)
(2)ConcurrentHashMap中变量使用final和volatile修饰有什么用呢?
- Final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。
- 使用volatile来保证某个变量内存的改变对其他线程即时可见,在配合CAS可以实现不加锁对并发操作的支持。get操作可以无锁是由于Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。
(3)我们可以使用CocurrentHashMap来代替Hashtable吗?
- 我们知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。它们都可以用于多线程的环境,但是当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。因为ConcurrentHashMap引入了分割(segmentation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。
(4)ConcurrentHashMap有什么缺陷吗?
- ConcurrentHashMap 是设计为非阻塞的。在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。坏处是严格来说读取操作不能保证反映最近的更新。例如线程A调用putAll写入大量数据,期间线程B调用get,则只能get到目前为止已经顺利插入的部分数据。