1.集合的分类
我们可以从一张类图来了解集合整个的情况,图中虚线框为接口,实线框为类,加重的实线框为比较重要的类。
2.集合相关概念
2.1集合和数组的区别
集合:集合类存放于java.util包中,集合中存放的是对象的引用,长度可以发生改变,可在多数情况下使用。
数组:可以存储有限个类型相同的变量的集合,把具有相同类型的若干元素按无序的形式组织起来的一种形式,数组的长度是固定的,不适合在元素的数量不确定的情况下使用。
2.2集合中各个集合的简介
从图中可以看出集合中重要且常用的集合就那几种,List、Set、Map是这个集合体系中最主要的三个接口。 List和Set继承自Collection接口。 Map也属于集合系统,但和Collection接口不同,他和Collection是依赖关系。
接口名称 | 用法 | 功能特点 |
---|---|---|
List | ArrayList、LinkedList和Vector是三个主要的实现类,使用时可直接创建ArrayList、LinkedList和Vector的对象。 | 有序且允许元素重复 |
Set | HashSet和TreeSet是两个主要的实现类,使用方法同上 | Set 只能通过游标来取值,并且集合中的元素无序但是不能重复的。 |
Map | HashMap、TreeMap和Hashtable是Map的三个主要的实现类,使用方法同上 | Map 是键值对集合。其中key列就是一个集合,key不能重复,但是value可以重复 |
实现List接口的实体类说明
1. ArrayList
- ArrayList底层的数据结构
ArrayList的继承关系
java.lang.Object
↳ java.util.AbstractCollection<E>
↳ java.util.AbstractList<E>
↳ java.util.ArrayList<E>
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
ArrayList 是一个数组队列,相当于 动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口.
RandomAccess:是一个空接口,提供了随机访问的功能,当我们的List实现了此接口后,就会为该List提供提供快速访问功能,我们可以通过元素的序号快速获得元素对象
Cloneable:实现了Cloneable接口后,即覆盖了clone这个函数,能被克隆
Serializable:实现了Serializable这些接口后即表示ArrayList支持序列化,能够通过序列化操作去传输。
ArrayList相信大家都使用过,ArrayList中的操作是线程不安全的,线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。所以建议在单线程中使用ArrayList,多线程中我们可以使用Vector。看一下ArrayList的构造函数
/**
initialCapacity是ArrayList的默认容量大小。
初始化elementData数组大小
*/
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}
/**
默认构造函数
*/
public ArrayList() {
super();
//未指定数组大小,则elementData为空数组EMPTY_ELEMENTDATA
this.elementData = EMPTY_ELEMENTDATA;
}
/**
建一个包含collection的ArrayList
*/
public ArrayList(Collection<? extends E> c) {
//将collection转化为数组,赋值给elementData
elementData = c.toArray();
//将elementData数组的长度赋值给size
size = elementData.length;
//返回若不是Object[]将调用Arrays.copyOf方法将其转为Object[]
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
从ArrayList的源码中可以看出,ArrayList包含了两个重要的对象:elementData 和 size。也就是说
(1) elementData 是”Object[]类型的数组”,它保存了添加到ArrayList中的元素。实际上,elementData是个动态数组,我们能通过构造函数 ArrayList(int initialCapacity)来执行它的初始容量为initialCapacity;如果通过不含参数的构造函数ArrayList()来创建ArrayList,则elementData默认创建一个空数组。elementData数组的大小会根据ArrayList容量的增长而动态的增长,具体的增长方式,请参考源码分析中的ensureCapacity()函数。
(2) size 则是动态数组的实际大小。
也就是说ArrayList的底层数据结构是动态数组,他是先确定ArrayList的容量,若当前容量不足以容纳当前的元素个数时,然后通过Arrays.copyOf()重新创建一个数组,将原来的数组copy进去,设置新的容量,然后赋值给elementData。
- ArrayList内部主要源码分析
首先来看类中的声明
// 序列版本号
private static final long serialVersionUID = 8683452581122892189L;
//默认集合的长度
private static final int DEFAULT_CAPACITY = 10;
// 默认空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 保存ArrayList中数据的数组
transient Object[] elementData;
// ArrayList中实际元素的数量
private int size;
接着看add的方法:
public boolean add(E e) {
// 扩容检查
ensureCapacityInternal(size + 1);
//添加单个元素
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
//倘若指定的index大于数组的长度,报出数组下标越界的异常
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
// 扩容检查
ensureCapacityInternal(size + 1);
//将index位置后面的数组元素统一后移一位,把index位置空出来
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
//扩容
private void ensureCapacityInternal(int minCapacity) {
//判断elementData是否为默认空数组
if (elementData == EMPTY_ELEMENTDATA) {
//倘若elementData为空数组,找出默认集合的长度和minCapacity中最大的一个赋值给minCapacity
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//判断是否要扩容
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
//记录修改数组的次数
modCount++;
//判断是否需要扩容,并扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
//记录下原来的数组容量
int oldCapacity = elementData.length;
//获得新的扩容后的容量:原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//当数组的长度过大后会调用hugeCapacity
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//拷贝到新的数组,指定大小,返回后赋值给elementData,完成扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
ArrayList类中还有很多其他方法,比较简单,这里就不赘述了。
- ArrayList的应用场景
类型 | 内部结构 | 顺序遍历速度 | 随机遍历速度 | 追加代价 | 插入代价 | 删除代价 | 占用内存 |
---|---|---|---|---|---|---|---|
ArrayList | 动态数组 | 高 | 高 | 中 | 高 | 高 | 低 |
所以很明显,当我们的实际需求中需要查找或者遍历的时候,使用ArrayList最好,如果有大量的插入删除操作尽量避免使用它。
2. LinkedList
- LinkedList底层的数据结构
LinkedList的继承关系
java.lang.Object
↳ java.util.AbstractCollection<E>
↳ java.util.AbstractList<E>
↳ java.util.AbstractSequentialList<E>
↳ java.util.LinkedList<E>
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable{}
从继承关系上我们可以看到不实现RandomAccess接口,不支持随机访问。LinkedList继承自AbstractSequentialList,这个抽象类实现了最基本的顺序访问功能
Deque接口:可以充当一般的双端队列或者栈
自jdk1.7之后,LinkedList底层使用的是不带头结点的普通的双向链表,增加了两个节点指针first和last分别指向首尾节点。如图:
注意:倘若size=0,则first和last指向同一空元素
- LinkedList内部源码分析
/**
* 头部添加
*/
private void linkFirst(E e) {
//获取头结点
final Node<E> f = first;
//新建一个节点,尾部指向之前的头元素的first
final Node<E> newNode = new Node<>(null, e, f);
//first指向新建的节点
first = newNode;
//如果之前链表为null、新建的节点也就是最后一个节点
if (f == null)
last = newNode;
else
//如果不为null,原来的头节点的头部指向现在新建的头节点
f.prev = newNode;
//链表的长度++
size++;
//链表修改的次数++
modCount++;
}
/**
*尾部添加
*/
void linkLast(E e) {
//获取尾节点
final Node<E> l = last;
//新建一个节点,头部指向之前的尾节点last
final Node<E> newNode = new Node<>(l, e, null);
//last指向新建的节点
last = newNode;
//假如之前的last指向null,新建的节点也是头节点
if (l == null)
first = newNode;
else
//如果不为null,原来的尾节点的尾部指向新建的尾节点
l.next = newNode;
//链表的长度++
size++;
//链表修改次数++
modCount++;
}
/**
* 在指定节点之前插入某个元素,这里假定指定节点不为null
*/
void linkBefore(E e, Node<E> succ) {
//获取指定节点 succ 前面的一个节点
final Node<E> pred = succ.prev;
//新建一个节点,头部指向succ节点前面的节点,尾部指向succ,数据为e
final Node<E> newNode = new Node<>(pred, e, succ);
//succ的节点头部指向新建节点
succ.prev = newNode;
//假如succ的前一个节点为null,让first指向新建的节点
if (pred == null)
first = newNode;
else
//否则让原先succ前面的节点的尾部指向新建节点
pred.next = newNode;
size++;
modCount++;
}
/**
* 删除头结点,返回头结点上的数据,既定first不为null
*/
private E unlinkFirst(Node<E> f) {
// 获取头结点的数据
final E element = f.item;
//获取头结点的下一个节点
final Node<E> next = f.next;
//将头结点置null
f.item = null;
//尾部也指向null
f.next = null; // help GC
//让first指向头结点的下一个节点
first = next;
//头节点后面的节点为 null,说明移除这个节点后,链表里没节点了
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
/**
* 删除尾部节点并返回数据,假设不为空
*/
private E unlinkLast(Node<E> l) {
//获取尾节点的数据
final E element = l.item;
//获取尾节点的前一个节点
final Node<E> prev = l.prev;
//值置null
l.item = null;
l.prev = null; // help GC
//让last指向尾节点的前一个节点
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
/**
*删除某个指定节点
*/
E unlink(Node<E> x) {
//假设 x 不为空
final E element = x.item;
//获取指定节点前面、后面的节点
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//如果前面没有节点,说明 x 是第一个
if (prev == null) {
first = next;
} else {
//前面有节点,让前面节点跨过 x 直接指向 x 后面的节点
prev.next = next;
x.prev = null;
}
//如果后面没有节点,说 x 是最后一个节点
if (next == null) {
last = prev;
} else {
//后面有节点,让后面的节点指向 x 前面的
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
展示一下添加的过程,红色代表已经做出修改,是无效的
展示一下删除的过程,红色代表已经做出修改,是无效的
ListedList还有很多方法,但是都较为简单,只要理解了上面的几个重要的方法,其他的你都可以融会贯通。
- LinkedList的应用场景
类型 | 内部结构 | 顺序遍历速度 | 随机遍历速度 | 追加代价 | 插入代价 | 删除代价 | 占用内存 |
---|---|---|---|---|---|---|---|
LikedList | 双端链表 | 中 | 不支持 | 高 | 高 | 高 | 中 |
所以很明显,ListedList基于双端链表,添加/删除元素只会影响周围的两个节点,开销很低;只能顺序遍历,无法按照索引获得元素,因此查询效率不高;没有固定容量,不需要扩容;需要更多的内存, 每个节点中需要多存储前后节点的信息,占用空间更多些。
注意: linkedList 和 ArrayList 一样,不是同步容器。所以需要外部做同步操作,或者直接用 Collections.synchronizedList 方法包一下,最好在创建时就包一下:
List l = Collections.synchronizedList(new LinkedList(…));
LinkedList的迭代器都是 fail-fast 的: 如果在并发环境下,其他线程使用迭代器以外的方法修改数据,会导致 ConcurrentModificationException异常,所以遍历是最好使用迭代器进行。
3. Vector
- Vectort底层的数据结构
Vector的继承关系
java.lang.Object
↳ java.util.AbstractCollection<E>
↳ java.util.AbstractList<E>
↳ java.util.Vector<E>
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
看过前面的ArrayList和LinkedList之后,相信大家已经对RandomAccess,和Cloneable,还有Serializable接口很熟悉了,这里不再赘述。
接着来看构造方法和声明。
//存储元素的数组
protected Object[] elementData;
//元素的个数
protected int elementCount;
//扩容增量
protected int capacityIncrement;
//序列化标识
private static final long serialVersionUID = -2767605614048989439L;
//capacityIncrement这个变量,需要在构造器中指定这个值(默认为0,可以手动指定)
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
//手动指定Vector的大小
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
//默认大小为10
public Vector() {
this(10);
}
//初始化一个集合进来
public Vector(Collection<? extends E> c) {
//将集合转化为数组赋值给elementData
elementData = c.toArray();
//获取数组的长度赋值给elementCount
elementCount = elementData.length;
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}
通过常量和构造方法我们可以看出Vector的底层也是个动态数组,并且它的结构代码和ArrayList相似。我们这里主要看扩容和保证线程同步的源码。
- Vector内部源码分析
//为了保证同步,只有一个线程操作,方法前面加了synchronized来修饰
public synchronized void ensureCapacity(int minCapacity) {
if (minCapacity > 0) {
//修改数组的次数
modCount++;
//判断是否扩容
ensureCapacityHelper(minCapacity);
}
}
/**
*
*/
private void ensureCapacityHelper(int minCapacity) {
// 当传进来的长度减去原先elementData大于0时,开始扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
*
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//扩容的方法
private void grow(int minCapacity) {
// 将elementData原先的长度赋值给oldCapacity
int oldCapacity = elementData.length;
//新的长度等于原先的长度加上(扩容增量>0的话就是扩容增量,否则原先的长度)
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//当数组的长度过大后会调用hugeCapacity
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//拷贝到新的数组,指定大小,返回后赋值给elementData,完成扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
从方法命名上来看,Vector跟ArrayList还是很类似的,但是两者的grow方法有点小区别:
Vector的扩容是基于capacityIncrement的,也就是所谓的扩容增量,如果该值不为0,那么每次扩容后的大小就是在原始容量加上扩容增量。如果未设置capacityIncrement,那么直接扩容为原来的两倍。
当然,前提是扩容后大小得大于等于所需要的最小容量minCapacity且不能超过MAX_ARRAY_SIZE,同时还要防止溢出(会抛出异常)
接下来看一下几个方法
public synchronized void trimToSize() {
modCount++;
int oldCapacity = elementData.length;
if (elementCount < oldCapacity) {
elementData = Arrays.copyOf(elementData, elementCount);
}
}
//获得数组的长度
public synchronized int capacity() {
return elementData.length;
}
//获得集合的长度
public synchronized int size() {
return elementCount;
}
//判null的方法
public synchronized boolean isEmpty() {
return elementCount == 0;
}
可以发现,这些方法都加上了synchronized关键字,也就是说Vector是一个线程安全的类。
Vector除了ListIterator和iterator两种迭代方式之外,还有独特的迭代方式,那就是elements方法,这个方法通过匿名内部类的方式构造一个Enumeration对象,并实现了hasMoreElements和nextElement方法,类似迭代器的hasNext和next方法
public Enumeration<E> elements() {
//匿名内部类方式构造一个Enumeration对象
return new Enumeration<E>() {
int count = 0;
public boolean hasMoreElements() {
return count < elementCount;
}
public E nextElement() {
synchronized (Vector.this) {
if (count < elementCount) {
return elementData(count++);
}
}
throw new NoSuchElementException("Vector Enumeration");
}
};
}
- Vectort的应用场景
Vector在实际的开发中使用较少,Vector所有方法都是同步,有性能损失。并且Vector会在你不需要进行线程安全的时候,强制给你加锁,导致了额外开销,所以慢慢被弃用了。
- #### 实现Set接口的实现类说明
1. HashSet
- HashSet 底层的数据结构
首先来看HashSet的继承关系
java.lang.Object
↳ java.util.AbstractCollection<E>
↳ java.util.AbstractSet<E>
↳ java.util.HashSet<E>
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
可以看到HashSet实现了Set接口和Cloneable,Serializable接口,关于Cloneable,Serializable接口不再多说,这里HashSet继承AbstactSet这个中间抽象类,并且这个抽象类又继承自AbstractCollection,AbstractCollection其实更像是实现List,Set的共同的方法,而AbstactSet和AbstactList更像是提供给Set、List各自特有方法的实现。接着来看:
//序列化标识
static final long serialVersionUID = -5024744406713321676L;
//底层使用HashMap来保存HashSet中所有元素。
private transient HashMap<E,Object> map;
//定义一个虚拟的Object对象作为HashMap的value
private static final Object PRESENT = new Object();
可以看到HashSet的底层实现是基于HasMap的,它不保证set 的迭代顺序,特别是它不保证该顺序恒久不变。且允许使用null元素,HashSet的实现较为的简单,其相关的操作都是通过直接调用底层HashMap的相关方法来完成
- HashSet 内部源码分析
-
1.构造函数
/*
默认构造函数,实际底层会初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。
*/
public HashSet() {
map = new HashMap<>();
}
/**
构造一个包含指定collection中的元素的新set。
* 实际底层使用默认的加载因子0.75和足以包含指定
* collection中所有元素的初始容量来创建一个HashMap。
*/
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
/**
以指定的initialCapacity和loadFactor构造一个空的HashSet。
*/
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
/**
以指定的initialCapacity构造一个空的HashSet。
*/
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
/**
以指定的initialCapacity和loadFactor构造一个新的空链接哈希集合。
此构造函数为包访问权限,不对外公开,实际只是是对LinkedHashSet的支持
*/
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
-
从构造函数中可以看到,基本都是为了构造一个HashMap来存储数据
- 2.常用方法
//如果set中尚未包含指定元素,则调用map的put方法,其中value是一个静态的Object对象
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//如果指定元素存在于此 set 中,则将其移除
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
//从此 set 中移除所有元素
public void clear() {
map.clear();
}
//判断set中是否含有指定元素,如果有,返回true
public boolean contains(Object o) {
return map.containsKey(o);
}
//实际调用HashMap的clone()方法,获取HashMap的浅表副本,并设置到 HashSet中
public Object clone() {
try {
HashSet<E> newSet = (HashSet<E>) super.clone();
newSet.map = (HashMap<E, Object>) map.clone();
return newSet;
} catch (CloneNotSupportedException e) {
throw new InternalError();
}
}
//返回对此set中元素进行迭代的迭代器。返回元素的顺序并不是特定的
public Iterator<E> iterator() {
return map.keySet().iterator();
}
-
可见HashSet中的元素,只是存放在了底层HashMap的key上, value使用一个static final的Object对象标识。
-
3.保证存储对象的唯一性
Set是一个不包含重复对象的集合,且最多只有null元素,如何保证其唯一且不重复呢,看一下构造函数的addAll(c); 追源码发现最后进入AbstractCollection中addAll中
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
从上面代码中可以看到,源码中也是通过循环一个一个add进去的,那我们看一下HashSet的add方法。
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
发现调用了map的put方法,进入HashMap的put方法看看:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
int i = indexFor(hash, table.length);
for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
可以看到for循环中,遍历table中的元素,如果hash码值不相同,说明是一个新元素,存;如果没有元素和传入对象(也就是add的元素)的hash值相等,那么就认为这个元素在table中不存在,将其添加进table;如果hash码值相同,且equles判断相等,说明元素已经存在,不存;如果hash码值相同,且equles判断不相等,说明元素不存在,存;如果有元素和传入对象的hash值相等,那么,继续进行equles()判断,如果仍然相等,那么就认为传入元素已经存在,不再添加,结束,否则仍然添加;
从上面我们可以看到通过哈希算法,对key产生哈希码,通过哈希码和equals方法保证其唯一,也就是说,要想保证对象在Set中的唯一,需要重写hashCode和equals方法。
- HashSet 小结
HashSet是Set接口典型实现,它按照Hash算法来存储集合中的元素,具有很好的存取和查找性能。且主要具有以下特点:
(1)不保证set的迭代顺序
(2)HashSet不是同步的,如果多个线程同时访问一个HashSet,要通过代码来保证其同步
(3)集合元素值可以是null,且只能有一个
(4)当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据该值确定对象在HashSet中的存储位置
(5)在Hash集合中,不能同时存放两个相等的元素,而判断两个元素相等的标准是两个对象通过equals方法比较相等并且两个对象的HashCode方法返回值也相等。
2. TreeSet
- TreeSet底层的数据结构
首先来看TreeSet的继承关系
java.lang.Object
↳ java.util.AbstractCollection<E>
↳ java.util.AbstractSet<E>
↳ java.util.TreeSet<E>
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
TreeSet继承自AbstractSet,所以他是一个Set的集合,具有Set的属性和方法
TreeSet实现了NavigaableSet接口,意味着它支持一系列的导航方法,比如查找与指定目标最匹配项
//NavigableMap对象,TreeMap实现了NavigableMap接口
private transient NavigableMap<E,Object> m;
//静态的PRESENT对象,代表TreeMap中的value
private static final Object PRESENT = new Object();
从上述继承关系以及常量声明中看到,TreeSet的底层是基于TreeMap的key来存储的,而Value值全部为默认值PRESENT。
- TreeSet内部源码分析
首先来看TreeSet的构造函数
//内部私有构造函数不对外公开,初始化NavigableMap对象m
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
/**
默认构造函数,创建空的TreeMap的对象
*/
public TreeSet() {
this(new TreeMap<E,Object>());
}
/**
带比较器的构造函数。
*/
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
/**
创建TreeSet
*/
public TreeSet(Collection<? extends E> c) {
// 调用默认构造器创建一个TreeSet,底层以 TreeMap 保存集合元素
this();
//将集合c中的全部元素都添加到TreeSet中
addAll(c);
}
/**
创建TreeSet,并将s中的全部元素都添加到TreeSet中
*/
public TreeSet(SortedSet<E> s) {
this(s.comparator());
addAll(s);
}
从源码中也可以看到TreeSet中的其他方法也比较简单,和HashSet中的有点类似,我们主要来看重点的方法:
public boolean addAll(Collection<? extends E> c) {
// 判断是否传入的集合参数c是否为SortedSet或其子类且c不为空(c.size()>0),
//如果是则会调用addAllForTreeSet方法,否则会直接返回addAll方法的结果
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);
}
//从TreeMap中找到了该方法
void addAllForTreeSet(SortedSet<? extends K> set, V defaultVal) {
try {
buildFromSorted(set.size(), set.iterator(), null, defaultVal);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
//该方法的作用即是在线性时间内对数据进行排序
private void buildFromSorted(int size, Iterator<?> it,
java.io.ObjectInputStream str,
V defaultVal)
throws java.io.IOException, ClassNotFoundException {
this.size = size;
root = buildFromSorted(0, 0, size-1, computeRedLevel(size),
it, str, defaultVal);
}
当使用一个TreeMap集合作为参数构造一个TreeSet的时候,TreeSet会将Map中的元素先排序,然后将排序后的元素add到TreeSet中。也就是说TreeSet中的元素都是排过序的,另外正因为存在排序过程,所以TreeSet不允许插入null值,因为null值不能排序
- TreeSet小结
1、TreeSet不能有重复的元素;
2、TreeSet具有排序功能;
3、TreeSet中的元素必须实现Comparable接口并重写compareTo()方法,TreeSet判断元素是否重复 、以及确定元素的顺序 靠的都是这个方法;
对于java类库中定义的类,TreeSet可以直接对其进行存储,如String,Integer等,因为这些类已经实现了Comparable接口);
对于自定义类,如果不做适当的处理,TreeSet中只能存储一个该类型的对象实例,否则无法判断是否重复。
4、TreeSet依赖TreeMap。
5、TreeSet相对HashSet,TreeSet的优势是有序,劣势是相对读取慢。根据不同的场景选择不同的集合。
3. LinkedHashSet
- LinkedHashSet底层的数据结构
LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起 来像是以插入顺 序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。
java.lang.Object
↳ java.util.AbstractCollection<E>
↳ java.util.AbstractSet<E>
↳ java.util.HashSet<E>
↳ java.util.LinkedHashSet<E>
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable
可以看到LinkedHashSet继承自HashSet,那么HashSet的属性和方法他都有,他比较简单,我们主要看一下他的构造函数
- LinkedHashSet内部源码分析
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
public LinkedHashSet() {
super(16, .75f, true);
}
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
从上面的构造函数看到,都是调用HashSet的构造器,而HashSet中有一个重要的方法:
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
也就是说在父类 HashSet 中,专为 LinkedHashSet 提供的构造方法如下,该方法为包访问权限,并未对外公开。由上述源代码可见,LinkedHashSet 通过继承HashSet,底层使用LinkedHashMap,以很简单明了的方式来实现了其自身的所有功能。
实现Map接口的实现类说明
1. TreeMap
- TreeMap底层的数据结构
首先来看TreeMap的继承关系
java.lang.Object
↳ java.util.AbstractMap<E>
↳ java.util.AbstractMap<E>
↳ java.util.TreeMap<E>
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
- TreeMap继承自AbstractMap,所以它是一个Map,即是一个key-value的集合
- TreeMap实现了NavigableMap,表示其支持一系列的导航方法,比如返回有序的key集合
- TreeMap实现了Cloneable和Serializable接口,即表示它能被克隆也支持序列化
//比较器,通过comparator接口我们可以对TreeMap的内部排序进行精密的控制
private final Comparator<? super K> comparator;
//TreeMap红-黑节点,为TreeMap的内部类
private transient TreeMapEntry<K,V> root = null;
/**
* 树中的条目数
*/
private transient int size = 0;
/**
*对树进行结构修改的次数
*/
private transient int modCount = 0;
//TreeMap的静态内部类,“红黑树的节点”对应的类。
static final class TreeMapEntry<K,V> implements Map.Entry<K,V> {
K key;//键
V value;//值
TreeMapEntry<K,V> left = null;//左子节点
TreeMapEntry<K,V> right = null;//右子节点
TreeMapEntry<K,V> parent;//父节点
boolean color = BLACK;//节点的颜色
/**
* TreeMapEntry的构造器,初始化key和value还有父节点
*/
TreeMapEntry(K key, V value, TreeMapEntry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
}
上面主要看了TreeMap的几个比较重要的变和内部类,可以得出TreeMap的底层由实现是红黑树算法的实现,所以要了解TreeMap就必须对红黑树有一定的了解。接下来看看红黑树的实现。
(1)红黑树概念理解
红黑树又称红-黑二叉树,它首先是一颗二叉树,它具体二叉树所有的特性。同时红黑树更是一颗自平衡的排序二叉树。我们知道一颗基本的二叉树他们都需要满足一个基本性质–即树中的任何节点的值大于它的左子节点,且小于它的右子节点,按照这个基本性质使得树的检索效率大大提高,这里又说道平衡二叉树,它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。也就是说该二叉树的任何一个等等子节点,其左右子树的高度都相近。红黑树顾名思义就是节点是红色或者黑色的平衡二叉树,它通过颜色的约束来维持着二叉树的平衡。对于一棵有效的红黑二叉树有以下特点:
1、每个节点都只能是红色或者黑色
2、根节点是黑色
3、每个叶节点(NIL节点,空节点)是黑色的。
4、如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。
5、从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
(2)经典红黑二叉树
这里看一个典型的红黑树
从上面可以看出红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这棵树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。所以红黑树它是复杂而高效的,其检索效率O(log n)。
(3)相关操作
左旋:
private void rotateLeft(TreeMapEntry<K,V> p) {
if (p != null) {
//定义一个新的TreeMapEntry保存p(需要左旋的节点)的右子节点
TreeMapEntry<K,V> r = p.right;
//将p的左子节点赋值给右子节点
p.right = r.left;
//如果新的节点r的左子节点不为null
if (r.left != null)
//将p赋值给新的节点r的左子节点的父节点
r.left.parent = p;
//将p的父节点赋值给r的父节点
r.parent = p.parent;
//如果p的父节点为null
if (p.parent == null)
//将r作为根节点
root = r;
//如果p节点为他的父亲节点的左子节点
else if (p.parent.left == p)
//将r赋值给p
p.parent.left = r;
else
//否则将r赋值给p父亲节点的右子节点
p.parent.right = r;
//将p节点赋值给r的左子节点
r.left = p;
//将r赋值给p的父子节点
p.parent = r;
}
}
引用一张图来方便我们理解:
右旋:
/** 右旋的过程为左旋逆过程 */
private void rotateRight(TreeMapEntry<K,V> p) {
if (p != null) {
TreeMapEntry<K,V> l = p.left;
p.left = l.right;
if (l.right != null) l.right.parent = p;
l.parent = p.parent;
if (p.parent == null)
root = l;
else if (p.parent.right == p)
p.parent.right = l;
else p.parent.left = l;
l.right = p;
p.parent = l;
}
}
图片引用自http://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html
着色:
/** 插入之后的修正操作。目的是保证:红黑树插入节点之后,仍然是一颗红黑树 */
private void fixAfterInsertion(TreeMapEntry<K,V> x) {
x.color = RED;
while (x != null && x != root && x.parent.color == RED) {
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
TreeMapEntry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
TreeMapEntry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
root.color = BLACK;
}
红黑树是一种比较高效的平衡查找树,应用非常广泛,很多编程语言的内部实现都或多或少的采用了红黑树。
- TreeMap重要的源码方法分析
//默认构造方法
public TreeMap() {
comparator = null;
}
/**
带比较器的构造方法
*/
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
/**
带Map的构造函数,Map会成为TreeMap的子集
*/
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;
putAll(m);
}
/**
*带SortedMap的构造函数,SortedMap会成为TreeMap的子集
*/
public TreeMap(SortedMap<K, ? extends V> m) {
//使用SortedMap的比较器
comparator = m.comparator();
try {
//通过递归将SortedMap中的元素逐个关联
buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
}
...
//插入节点的过程
public V put(K key, V value) {
TreeMapEntry<K,V> t = root;
if (t == null) {
//如果红黑树为null,且比较器不为null
if (comparator != null) {
if (key == null) {
//type check
comparator.compare(key, key);
}
} else {
if (key == null) {
throw new NullPointerException("key == null");
} else if (!(key instanceof Comparable)) {
throw new ClassCastException(
"Cannot cast" + key.getClass().getName() + " to Comparable.");
}
}
//需要插入的节点为根节点
root = new TreeMapEntry<>(key, value, null);
//TreeMap的长度为1
size = 1;
//修改次数++
modCount++;
return null;
}
//定义一个父结点
int cmp;
TreeMapEntry<K,V> parent;
//优先通过比较器比较两个结点的大小
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
//待插入结点小于当前结点
if (cmp < 0)
//进入左子树
t = t.left;
//待插入结点大于当前结点
else if (cmp > 0)
//进入右子树
t = t.right;
else
//当前结点等于待插入结点,覆盖原值
return t.setValue(value);
} while (t != null);
}else {
//比较器为null的情况,如果没有定义比较器,那么key必须实现Comparable接口
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
//找到插入点之后,创建新结点,插入之
TreeMapEntry<K,V> e = new TreeMapEntry<>(key, value, parent);
//判断是挂到左边还是右边
if (cmp < 0)
parent.left = e;
else
parent.right = e;
//进行着色和旋转等操作修复红黑树,之前看过了
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
...
//删除节点的过程
private void deleteEntry(TreeMapEntry<K,V> p) {
//修改的次数++,长度--
modCount++;
size--;
//如果p有子节点
if (p.left != null && p.right != null) {
//那么就用 p节点的中序后继节点代替 p 节点
//successor(P)方法为寻找P的替代节点。规则是右分支最左边,或者 左分支最右边的节点
TreeMapEntry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
}
// replacement为替代节点,如果P的左子树存在那么就用左子树替代,否则用右子树替代
TreeMapEntry<K,V> replacement = (p.left != null ? p.left : p.right);
//如果替代节点不为空
if (replacement != null) {
// Link replacement to parent
replacement.parent = p.parent;
if (p.parent == null)
root = replacement;
else if (p == p.parent.left)
p.parent.left = replacement;
else
p.parent.right = replacement;
// 将P节点从这棵树中剔除掉
p.left = p.right = p.parent = null;
// 若P为红色直接删除,红黑树保持平衡 但是若P为黑色,则需要调整红黑树使其保持平衡
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) {
//p没有父节点,表示为P根节点,直接删除即可
root = null;
} else {
//P节点不存在子节点,直接删除即可
//如果P节点的颜色为黑色,对红黑树进行调整
if (p.color == BLACK)
fixAfterDeletion(p);
//删除P节点
if (p.parent != null) {
if (p == p.parent.left)
p.parent.left = null;
else if (p == p.parent.right)
p.parent.right = null;
p.parent = null;
}
}
}
关于TreeMap的插入和删除过程:
http://www.jb51.net/article/84833.htm 这篇文章有图,可以帮助理解整个过程
- TreeMap小结
TreeMap在一个“红-黑”树的基础上实现,适用于按自然顺序或自定义顺序遍历键(key)。 查看键或者“键-值”对时,它们会按固定的顺序排列(取决于Comparable或Comparator,稍后即会讲到)。TreeMap最大的好处就是我们得到的是已排好序的结果。TreeMap是含有subMap()方法的唯一一种Map,利用它可以返回树的一部分。
2. HashMap
- HashMap底层的数据结构
-
哈希表:哈希表hashtable(key,value) 就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。(或者:把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。)
而当使用哈希表进行查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位 - 初始容量(initialCapacity):是哈希表中桶的数量,初始容量只是哈希表在创建时的容量,HashMap中的
- 加载因子(loadFactor): 是哈希表在其容量自动增加之前可以达到多满的一种尺度。加载因子默认值为0.75
接着来看HashMap的继承关系:
java.lang.Object
↳ java.util.AbstractMap<E>
↳ java.util.HashMap<E>
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
很明显,我们可以通过继承关系和图来很清楚的了解HashMap继承和实现接口的关系,而相应的接口和父类前面均已提到,这里不再赘述。接着来看源码中定义声明部分。
//默认初始容量
static final int DEFAULT_INITIAL_CAPACITY = 4;
//最大的容量值
static final int MAXIMUM_CAPACITY = 1 << 30;
//加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// HashMapEntry<?, ?>[]实现bucket数组,HashMapEntry对象又实现了链表。整体实现了hashMap底层存储结构
static final HashMapEntry<?,?>[] EMPTY_TABLE = {};
transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;
//k/v隐射关系的数量
transient int size;
// 阈值,当HashMap的下一个元素size>=threshold时,rehash
int threshold;
//
final float loadFactor = DEFAULT_LOAD_FACTOR;
//修改次数
transient int modCount;
这里可以看到HashMap中底层最重要的类HashMapEntry,HashMapEntry是HashMap的基础接着来看他是怎么实现HashMap底层存储结构的。
当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。 每个bucket中存储一个元素,即一个HashEntry对象,但每一个HashEntry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。 HashEntry是HashMap的基本组成单元,每一个HashEntry包含一个key-value键值对。 HashEntry是HashMap中的一个静态内部类
可以看到其中有key,value
还有next:存储指向下一个Entry的引用,单链表结构,具体表现如下图
图片引用自百度百科
- HashMap内部源码分析
首先来看构造方法:
//初始化保证初始容量和记载因子有效
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY) {
initialCapacity = MAXIMUM_CAPACITY;
} else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) {
initialCapacity = DEFAULT_INITIAL_CAPACITY;
}
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
threshold = initialCapacity;
//init方法在HashMap中没有实际实现,不过在其子类中就会有对应实现
init();
}
/**
通过扩容因子构造HashMap,容量取默认值,即4
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
//装载因子取0.75,容量取4,构造HashMap
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/**
通过其他Map来初始化HashMap,容量通过其他Map的size来计算,加载因子取0.75
*/
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
//初始化HashMap底层的数组结构
inflateTable(threshold);
//将m中的元素全部加到HashMap中
putAllForCreate(m);
}
解读几个重要的方法:
//确保capacity为大于或等于toSize的最接近toSize的二次幂
private static int roundUpToPowerOf2(int number) {
// number肯定不能为负数
int rounded = number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (rounded = Integer.highestOneBit(number)) != 0
? (Integer.bitCount(number) > 1) ? rounded << 1 : rounded
: 1;//Integer.highestOneBit是用来获取最左边的bit(其他bit位为0)所代表的数值
return rounded;
}
//为主干数组table在内存中分配存储空间
private void inflateTable(int toSize) {
//此处为threshold赋值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,
//capaticy一定不会超过MAXIMUM_CAPACITY,除非loadFactor大于1
int capacity = roundUpToPowerOf2(toSize);
float thresholdFloat = capacity * loadFactor;
if (thresholdFloat > MAXIMUM_CAPACITY + 1) {
thresholdFloat = MAXIMUM_CAPACITY + 1;
}
threshold = (int) thresholdFloat;
//实例化bucket数组,容量为capacity
table = new HashMapEntry[capacity];
}
//put操作
public V put(K key, V value) {
//如果bucket数组是空的,去实例化它
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//key为null的情况,下面会看
if (key == null)
return putForNullKey(value);
//计算key的hash值
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
//计算该hash值在bucket数组中的下标
int i = indexFor(hash, table.length);
//通过for循环,遍历table[i]中的链表
for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//判断链表中是否有hash值相同的key
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
//若相同,新的value覆盖旧的,返回旧value值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//修改次数++
modCount++;
//把当前key,value添加到table[i]的链表中
addEntry(hash, key, value, i);
return null;
}
private V putForNullKey(V value) {
//查找链表中是否有null键
for (HashMapEntry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
//直接覆盖旧的value,返回旧的value
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//将该null key插入
addEntry(0, null, value, 0);
return null;
}
对于hash操作,我们可以看到
(1)求得key的hash值:singleWordWangJenkinsHash(key),
(2)然后计算该hash值在table中的下标,主要是indexFor方法:
//HashMap通过&运算符(按位与操作)来保证table元素分布均匀和充分利用空间
static int indexFor(int h, int length) {
return h & (length-1);
}
完成hash操作后如何把该key-value插入到该索引的链表中呢
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果size大于极限容量,将要进行重建内部数据结构操作
//之后的容量是原来的两倍,并且重新设置hash值和hash值在table中的索引值
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容操作,新的capacity为原来的两倍
resize(2 * table.length);
//key为null,hash值为0,插入到哈希表的表头table[0]的位置
hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
/**
创建新的HashMapEntry节点的操作
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry<K,V> e = table[bucketIndex];
table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
size++;
}
可以看到,具体操作是首先取得bucketIndex位置的HashMapEntry头结点,并创建新节点,把该新节点插入到链表中的头部,该新节点的next指针指向原来的头结点。也就是说系统总是将新的HashMapEntry对象添加到bucketIndex处。如果bucketIndex处已经有了对象,那么新添加的HashMapEntry对象将指向原有的HashMapEntry对象,形成一条HashMapEntry链,但是若bucketIndex处没有HashMapEntry对象,也就是e==null,那么新添加的HashMapEntry对象指向null,也就不会产生HashMapEntry链了。
接着来看 HashMap是如何扩容的:HashMap中有这样一个的变量threshold,这是容器的容量极限,还有一个变量size,这是指HashMap中键值对的数量,也就是node的数量,当不断添加key-value,size大于了容量极限threshold时,会发生扩容,扩容发生在resize方法中,也就是扩大数组(桶)的数量,如下:
//扩容操作,传入新的容量
void resize(int newCapacity) {
引用扩容前的HashMapEntry数组
HashMapEntry[] oldTable = table;
//将扩容前的HashMapEntry的数组长度赋值给oldCapacity
int oldCapacity = oldTable.length;
//如果扩容前的数组大小如果已经达到最大(2^30)了
if (oldCapacity == MAXIMUM_CAPACITY) {
//修改阈值为int的最大值,这样以后就不会扩容了
threshold = Integer.MAX_VALUE;
return;
}
//重新创建一个HashMapEntry数组
HashMapEntry[] newTable = new HashMapEntry[newCapacity];
//将数据迁移到新的newTable中
transfer(newTable);
//HashMap的table属性引用新的HashMapEntry数组
table = newTable;
//修改阙值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/**
* 使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有HashMapEntry
* 数组的元素拷贝到新的HashMapEntry数组里
*/
void transfer(HashMapEntry[] newTable) {
int newCapacity = newTable.length;
for (HashMapEntry<K,V> e : table) {
while(null != e) {
HashMapEntry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
读取的步骤比较简单,调用singleWordWangJenkinsHash(key)求得key的hash值,然后调用indexFor(hash)求得hash值对应的table的索引位置,然后遍历索引位置的链表,如果存在key,则把key对应的Entry返回,否则返回null
//get操作
public V get(Object key) {
if (key == null)//如果key为null,求null键
return getForNullKey();
//根据key获得Entry
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
private V getForNullKey() {
if (size == 0) {
return null;
}
for (HashMapEntry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
- HashMap小结
(1) 扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。
(2) 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊。
(3) HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap。
(4) JDK1.8引入红黑树大程度优化了HashMap的性能。
3. Hashtable
- Hashtable底层的数据结构
Hashtable 是一个散列表,它存储的内容是键值对(key-value)映射。
Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。
Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。此外,Hashtable中的映射不是有序的。
Hashtable的继承关系:
java.lang.Object
↳ java.util.Dictionary<K, V>
↳ java.util.Hashtable<K, V>
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable
图片引用自“https://blog.youkuaiyun.com/zheng0518/article/details/42199477”
- Hashtable内部源码分析
首先来看构造函数
//指定容量和加载因子的构造函数
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
//实例化table,给loadFactor 和threshold 赋值
this.loadFactor = loadFactor;
table = new HashtableEntry[initialCapacity];
threshold = (initialCapacity <= MAX_ARRAY_SIZE + 1) ? initialCapacity : MAX_ARRAY_SIZE + 1;
}
/**
指定“容量大小”的构造函数
*/
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
/**
默认构造函数,指定的容量大小是11;加载因子是0.75
*/
public Hashtable() {
this(11, 0.75f);
}
/**
包含“子Map”的构造函数, 将“子Map”的全部元素都添加到Hashtable中
*/
public Hashtable(Map<? extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
这里Hashtable的内部源码和HashMap很相似,如果你之前理解了HashMap的源码,看起来应该不难,这里就不再赘述。接着主要是分析一下HashMap和Hashtable的区别
- Hashtable小结
- 1.HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。
- 2.HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
- 3.另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。
- 4.由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。
- 5.HashMap不能保证随着时间的推移Map中的元素次序是不变的。
集合名称 | 继承的父类 | 线程安全 | 查找速度 | null键和null值 |
---|---|---|---|---|
HashMap | AbstractMap | 不安全 | 快 | 支持 |
Hashtable | Dictionary | 安全 | 慢 | 不支持 |
3.面试中通常会问到的关于集合的面试题
集合中的面试千奇百怪,正所谓万变不离其宗,熟悉了底层源码的你基本没什么难度,简单的这里就不赘述,主要总结几个面试过程中遇到的:
1.集合框架中大量使用了泛型,说说这样用的好处
Java1.5引入了泛型,所有的集合接口和实现都大量地使用它。泛型允许我们为集合提供一个可以容纳的对象类型,因此,如果你添加其它类型的任何元素,它会在编译时报错。这避免了在运行时出现ClassCastException,因为你将会在编译时得到报错信息。泛型也使得代码整洁,我们不需要使用显式转换和instanceOf操作符。它也给运行时带来好处,因为不会产生类型检查的字节码指令。
2.为何Collection不从Cloneable和Serializable接口继承
Collection接口指定一组对象,对象即为它的元素。如何维护这些元素由Collection的具体实现决定。例如,一些如List的Collection实现允许重复的元素,而其它的如Set就不允许。很多Collection实现有一个公有的clone方法。然而,把它放到集合的所有实现中也是没有意义的。这是因为Collection是一个抽象表现。重要的是实现。
当与具体实现打交道的时候,克隆或序列化的语义和含义才发挥作用。所以,具体实现应该决定如何对它进行克隆或序列化,或它是否可以被克隆或序列化。
在所有的实现中授权克隆和序列化,最终导致更少的灵活性和更多的限制。特定的实现应该决定它是否可以被克隆和序列化。
3.为何Map接口不继承Collection接口
尽管Map接口和它的实现也是集合框架的一部分,但Map不是集合,集合也不是Map。因此,Map继承Collection毫无意义,反之亦然。
如果Map继承Collection接口,那么元素去哪儿?Map包含key-value对,它提供抽取key或value列表集合的方法,但是它不适合“一组对象”规范
4.Iterator是啥?Iterater和ListIterator之间有什么区别
Iterator接口提供遍历任何Collection的接口。我们可以从一个Collection中使用迭代器方法来获取迭代器实例。迭代器取代了Java集合框架中的Enumeration。迭代器允许调用者在迭代过程中移除元素
(1)我们可以使用Iterator来遍历Set和List集合,而ListIterator只能遍历List。
(2)Iterator只可以向前遍历,而LIstIterator可以双向遍历。
(3)ListIterator从Iterator接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个
5.在迭代一个集合的时候,如何避免ConcurrentModificationException?
在遍历一个集合的时候,我们可以使用并发集合类来避免ConcurrentModificationException,比如使用CopyOnWriteArrayList,而不是ArrayList。
6.我们能否使用任何类作为Map的key
我们可以使用任何类作为Map的key,然而在使用它们之前,需要考虑以下几点:
(1)如果类重写了equals()方法,它也应该重写hashCode()方法
(2)类的所有实例需要遵循与equals()和hashCode()相关的规则。请参考之前提到的这些规则。
(3)如果一个类没有使用equals(),你不应该在hashCode()中使用它。
(4)用户自定义key类的最佳实践是使之为不可变的,这样,hashCode()值可以被缓存起来,拥有更好的性能。不可变的类也可以确保hashCode()和equals()在未来不会改变,这样就会解决与可变相关的问题了。
7.concurrent有了解过吗? BlockingQueue是什么?
Java1.5并发包(java.util.concurrent)包含线程安全集合类,允许在迭代时修改集合。迭代器被设计为fail-fast的,会抛出ConcurrentModificationException。一部分类为:CopyOnWriteArrayList、 ConcurrentHashMap、CopyOnWriteArraySet。
Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。
8.HashMap与Hashtable的区别:
HashMap可以接受null键值和值,而Hashtable则不能。
Hashtable是线程安全的,通过synchronized实现线程同步。而HashMap是非线程安全的,但是速度比Hashtable快。
9.HashMap的原理
参见上文中的HashMap的具体实现
10.当两个对象的hashcode相同怎么办
当哈希地址冲突时,HashMap采用了链地址法的解决方式,将所有哈希地址冲突的记录存储在同一个线性链表中。具体来说就是根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。
11.如果两个键的hashcode相同,你如何获取值对象
HashMap在链表中存储的是键值对,找到哈希地址位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象
12.如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办
HashMap默认的负载因子大小为0.75,也就是说,当一个map填满了75%的空间的时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的数组,来重新调整map的大小,并将原来的对象放入新的数组中。
13.为什么String, Interger这样的wrapper类适合作为键?
String, Interger这样的wrapper类是final类型的,具有不可变性,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。
14.ConcurrentHashMap和Hashtable的区别
Hashtable和ConcurrentHashMap有什么分别呢?它们都可以用于多线程的环境,但是当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。因为ConcurrentHashMap引入了分割(segmentation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。
15.有了解过哈希碰撞(hash冲突)的几种处理方式吗
在计算hash地址的过程中会出现对于不同的关键字出现相同的哈希地址的情况,即key1 ≠ key2,但是f(key1) = f(key2),这种情况就是Hash 冲突。具有相同关键字的key1和key2称之为同义词。
通过优化哈希函数可以减少这种冲突的情况(如:均衡哈希函数),但是在通用条件下,考虑到于表格的长度有限及关键值(数据)的无限,这种冲突是不可避免的,所以就需要处理冲突,冲突处理分为以下四种方式:
开放地址又分为:
线性探测再散列
二次探测再散列
伪随机探测再散列
处理冲突的基本原则就是出现冲突后按照一定算法查找一个空位置存放
再哈希:再哈希法,就是出现冲突后采用其他的哈希函数计算,直到不再冲突为止。
链地址:链接地址法不同与前两种方法,他是在出现冲突的地方存储一个链表,所有的同义词记录都存在其中。形象点说就行像是在出现冲突的地方直接把后续的值摞上去
建立公共溢出区:建立公共溢出区的基本思想是:假设哈希函数的值域是[1,m-1],则设向量HashTable[0…m-1]为基本表,每个分量存放一个记录,另外设向量OverTable[0…v]为溢出表,所有关键字和基本表中关键字为同义词的记录,不管它们由哈希函数得到的哈希地址是什么,一旦发生冲突,都填入溢出表。
10.二叉树查找算法,遍历树的三种方式,前序,中序,后序,通过伪代码实现。
4.总结
关于这次复习,自己觉得收货还是蛮大的,不仅复习了集合相关概念,还顺便复习了相关数据结构,感受颇深。如果有哪些点你觉得我的理解不对。欢迎留言指正,最近开通了自己的微信公众号,偶尔更新文章,生活感悟,好笑的段子,欢迎订阅