JDK在1.2版本把名字改为了J2SE,所以在6版本之前都叫这个名字,J2SE 1.2。
1.2版本中最重要的一大看点是引入了集合框架 Collections framework。这是最早的集合包,里面只定义了一些简单的集合,源码实现也非常简单,源码位置在java.util包下。
Collection
集合框架的顶层接口,定义了集合的一些基本操作,比如size、isEmpty、contains、iterator、toArray、add、remove、clear。抽象类AbstractCollection
实现了Collection
集合,简化了集合实现步骤。实现了一些基本的方法,比如contains、toArray、remove、clear、toString方法,当然子类也可以重载这些方法,实现自己的定义。
本文主要介绍基于List接口实现的的ArrayList、LinkedList集合,基于Map接口实现的的HashMap、TreeMap和WeakHashMap集合,基于Set接口实现的的HashSet、TreeSet集合。
List
AbstractList
列表抽象类AbstractList
定义一个支持随机访问数据的列表实现方式。抽象类里面最重要的是实现Iterator
接口和新增了一个listIterator方法:
- iterator方法,创建并返回了一个Itr对象。 内部类
Itr
实现了Iterator
接口,重写了hasNext,next,remove,checkForComodification方法,支持单向遍历集合。 - listIterator方法,创建并返回了一个从下标0开始的ListItr对象。内部类
ListItr
继承Itr
并实现了ListIterator
集合的迭代器接口。ListIterator
接口继承了Iterator
接口并新增了基于列表操作的方法hasPrevious,previous,nextIndex,previousIndex等方法,支持双向遍历集合。
最后一个内部类是SubList
子列表类,基于当前列表构建出的子列表。但是注意,这个子列表是通过对当前列表的引用来实现的,所以会一直持有当前集合的对象引用。而且只要当前列表一被修改,对其子列表的任何操作都是报错抛出异常。
iterator() 迭代方法,创建一个Itr对象返回
创建一个Itr对象返回,Itr对象实现了Iterator接口 Iterator迭代器定义了三个方法,Itr类实现了这三个方法
AbstractList.Itr
Itr.hasNext() 判断是否还有元素
通过cursor当前游标来判断List是否还有元素 每调用一次next方法,cursor++,只要cursor不等于size就说明还有元素
public boolean hasNext() {
return cursor != size();
}
Itr.next() 返回下一个元素
- 迭代器是直接访问实现类的元素,根据当前游标位置获取指定坐标的元素
- 使用迭代器是不允许修改List的,所以会检查modCount是否修改过;如果修改过,直接抛异常;
- 最后游标++,返回获取到的元素。
public Object next() {
try {
// 调用实现类的get方法,获取指定坐标的元素
Object next = get(cursor);
// 检查迭代过程中,List是否有被修改过
checkForComodification();
// 游标++,记录最新一次next方法返回的元素坐标
lastRet = cursor++;
return next;
} catch(IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
final void checkForComodification() {
// 检查基础ArrayList里的modCount是否被修改过
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
Itr.remove() 删除上一次next方法返回的元素,要先调用next才能调用remove。
- 迭代器删除元素也是直接调用实现类的remove方法,将上一次next方法返回的元素删除;
public void remove() {
if (lastRet == -1)
throw new IllegalStateException();
try {
// 调用实现类的remove方法
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
int newModCount = modCount;
if (newModCount - expectedModCount > 1)
throw new ConcurrentModificationException();
// 重置modCount值
expectedModCount = newModCount;
} catch(IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
注意:Iterator的next和remove方法在代码执行时都会对modCount值进行检查,防止基础List数组被操作过,导致不一致。
在使用迭代器遍历集合的过程中,要删除元素,必须使用迭代器的remove方法,不能直接使用集合提供的remove方法
因为iterator每次next方法时,都会检查modCount是否被修改过。ArrayList的remove方法会修改modCount的值。
Iterator接口只提供了在遍历列表的过程中删除元素,不能新增元素,如果要新增可以使用ListItr类,ListItr继承了Itr,提供了新增方法。
AbstractList.ListItr
ListItr继承了Itr类并实现了ListIterator接口。ListIterator接口继承了Iterator接口,在Iterator接口的基础上又新增了几个方法。ListIterator接口提供了倒序遍历List的功能,并且实现了可以在迭代过程中新增或修改元素。
- hasPrevious 双向遍历,判断是否有上一个元素,该功能可以实现List的反转,即倒序遍历
- previous 返回上一个元素
- set 修改当前元素值
- add 新增元素
ListItr.hasPrevious() 判断上一个是否还有元素,即是否是List头
只要cursor不等于0即可
public boolean hasPrevious() {
return cursor != 0;
}
ListItr.previous() 返回上一个元素
- –cursor即可获取上一个元素的坐标,然后通过实现类的get方法即可获取到上一个元素
public Object previous() {
try {
Object previous = get(--cursor);
checkForComodification();
lastRet = cursor;
return previous;
} catch(IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
ListItr.set() 修改当前值,即上一次调用next方法返回的元素值
- 调用实现类的set方法,修改lastRet坐标的值即可
- 因为对List进行了操作,所以set方法也要记得更新modCount值
public void set(Object o) {
if (lastRet == -1)
throw new IllegalStateException();
try {
// 通过lastRet值来更新元素
AbstractList.this.set(lastRet, o);
int newModCount = modCount;
if (newModCount - expectedModCount > 1)
throw new ConcurrentModificationException();
// 记得要更新modCount值
expectedModCount = newModCount;
} catch(IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
ListItr.add() 新增元素,这里要注意新增的位置,即上一次调用next方法返回的元素值的位置后面
- 调用实现类的add方法,将新元素添加到cursor++位置即可
- 因为对List进行了操作,所以add方法也要记得更新modCount值
public void add(Object o) {
try {
AbstractList.this.add(cursor++, o);
lastRet = -1;
int newModCount = modCount;
if (newModCount - expectedModCount > 1)
throw new ConcurrentModificationException();
expectedModCount = newModCount;
} catch(IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
}
AbstractList.SubList
AbstractList.subList
方法会返回一个子列表对象SubList
,SubList
集成了AbstractList
,SubList
根据当前ArrayList列表构建一个子列表。父集合如果有被操作修改,SubList会报错。SubList内部也提供了set/add/remove等操作集合的方法。
SubList构造函数
- SubList并没有创建一个新的集合,而是持有对父集合的引用,内部通过坐标来实现
- SubList维护了modCount值,父集合如果有被修改,SubList操作会报错
- SubList可以修改父集合,因为SubList每次修改后会更新modCount值
// 持有当前ArrayList集合对象的引用,即父集合的引用
private AbstractList l;
// fromIndex,ArrayList的开始位置
private int offset;
// 子集合的长度
private int size;
// 用于检查父集合是否被修改过
private int expectedModCount;
// 通过构造函数可以看出,SubList并没有新创建一个集合,而是持有对父集合的引用,内部通过坐标来实现
SubList(AbstractList list, int fromIndex, int toIndex) {
if (fromIndex < 0)
throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
if (toIndex > list.size())
throw new IndexOutOfBoundsException("toIndex = " + toIndex);
if (fromIndex > toIndex)
throw new IllegalArgumentException("fromIndex(" + fromIndex +
") > toIndex(" + toIndex + ")");
l = list;
offset = fromIndex;
size = toIndex - fromIndex;
expectedModCount = l.modCount;
}
ArrayList
基于数组实现的动态列表,继承了AbstractList
并实现了List
接口。size,get,set操作的时间复杂度都是常量时间,add操作的时间复杂度接近常量。ArrayList的所有操作的时间复杂度几乎都是接近常量,所以是一个非常高效的列表。每个ArrayList都有一个容量capacity,指定数组的大小,该容量值会随着数组元素的增加自动调整。
ArrayList是线程不安全的,非同步的,所以对于多线程操作会出现同步问题。Vector是线程安全的列表,也是基于数组的动态列表,所有操作的方法都加上了synchronized修饰符。
ArrayList的set方法不会修改modCount值,add/remove方法会修改modCount值。modCount值是针对List的结构做统计的,所以新增和删除操作被记录,修改操作不会改变结构所以不会被记录。
核心方法介绍
构造函数
ArrayList内部是使用数组作为存储结构的,在构造函数里实例化了一个Object[]数组
private transient Object elementData[]; // 元素实际存储在这个数组中
public ArrayList() {
this(10); // 默认数组容量值为10
}
public ArrayList(int initialCapacity) {
super(); // AbstractList构造方法为空
this.elementData = new Object[initialCapacity]; // 创建ArrayList对象的时候就会立即初始化一个数组
}
toArray() 将ArrayList以数组形式返回输出
toArray()返回的是新数组,并不是直接把内部的
elementData[]
数组返回。使用System.arraycopy方法将内部数组数据复制到新的数组,然后把新创建的数组返回。这样对新数组的修改操作不会影响到现有数组。
// 将集合以数组形式返回 这里会创建一个新的数组返回,不会直接将列表集合里的elementData数组返回。
public Object[] toArray() {
Object[] result = new Object[size]; // 创建一个新的数组,容量为ArrayList的容量
System.arraycopy(elementData, 0, result, 0, size); // 使用System.arraycopy将当前列表复制到新的数组。
return result;
}
add(Object o) 将对象添加到List中
- add方法首先对数组的容量大小进行检查,看是否需要扩容;如果不需要直接将对象添加到数组末尾,结束返回true;
- 如果需要扩容,则先计算新的容量值,默认扩大1.5倍;然后判断新计算出来的值是否满足条件;如果还是不满足,则直接使用新增容量值;最后将现有数组内容全部复制到新数组上,通过System.arraycopy方法复制数组;
- 容量扩容完后,将对象添加到数组末尾,结束返回true;
public boolean add(Object o) {
ensureCapacity(size + 1); // 动态调整数组容量大小,确认容量空间够
elementData[size++] = o; // 将新元素添加到数组末尾
return true;
}
// ensureCapacity会自动扩容,所以只有调用了这个方法,一定会确保数组的容量是够用的
public void ensureCapacity(int minCapacity) {
modCount++; // 记录对集合的每次操作,在迭代器中就根据这个遍历值来判断集合是否被修改过。
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
int newCapacity = (oldCapacity * 3)/2 + 1; // 如果集合容量不够了,就进行扩容
if (newCapacity < minCapacity)
newCapacity = minCapacity; // 如果容量还是不够,就直接用指定的容量minCapacity
elementData = new Object[newCapacity]; // 创建一个使用新容量的新的数组
System.arraycopy(oldData, 0, elementData, 0, size); // 使用System.arraycopy将旧数组拷贝到新的数组
}
// 如果当前容量够,就直接结束返回
}
add(int index, Object element) 将元素添加到指定位置
- 首先对index位置进行范围检查;
- 然后对数组容量进行检查;调用ensureCapacity方法,确保数组容量够,如果不够会自动完成扩容;
- 其次将在index后的所有元素往后移动一位;通过System.arraycopy方法实现元素后移;
- 最后将值添加到index位置,size++
// 将新元素添加到指定位置
public void add(int index, Object element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: "+size);
ensureCapacity(size+1); // Increments modCount!! 确保容量够
System.arraycopy(elementData, index, elementData, index + 1,
size - index); // 将当前位置以及后面的所有元素都往后移动一格
elementData[index] = element; // 将新元素添加到当前位置
size++;
}
remove(int index) 删除指定位置的元素,删除完后要将后面的元素都往前移动一位。
- 首先检查index范围;
- 然后将index后的元素都往前移动一位;通过System.arraycopy方法实现;
- 其次将最后一位元素位置null,size–;
// 删除指定位置的元素
public Object remove(int index) {
RangeCheck(index);
modCount++; // 只要有对集合操作,modCount都会增加
Object oldValue = elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved); // 将当前位置后面的所有元素都往前移动一格
elementData[--size] = null; // Let gc do its work 最后一格元素置为null,方便垃圾收集器回收
return oldValue;
}
clear() clear方法并不会缩减数组容量,而是把数组所有元素设置为null,size设置为0.
// 清空列表集合
public void clear() {
modCount++; // 清空操作也会记录
// Let gc do its work
for (int i = 0; i < size; i++)
elementData[i] = null; // 这里是通过遍历方式,将数组里的所有元素都置为null。
size = 0;
}
public Object get(int index) {
RangeCheck(index);
return elementData[index];
}
iterator() 迭代方法,是直接调用父类的实现
- iterator()内部创建了一个Itr对象,Itr对象实现了Iterator接口
除了以上几个基本的方法,ArrayList还提供了一些实现:
- int indexOf(Object elem); 返回第一个匹配到的元素坐标,需要遍历数组获取对应元素下标
- int lastIndexOf(Object elem); 返回最后一个匹配到的元素坐标,需要倒序遍历数组获取对应元素下标
- Object clone(); 克隆当前ArrayList实例,实例化一个新数组,然后将元素全部拷贝到新数组
- Object[] toArray(Object a[]); toArray可以输出到指定数组里,这里需要对指定数组的容量值进行判断是否需要扩容,然后再将元素复制到指定数组
- addAll(Collection c); 直接添加一个集合到数组末尾,这里操作与添加单个元素类似,先检查数组容量是否需要扩容,扩容完后通过迭代方式将集合添加到数组中
- removeRange(int fromIndex, int toIndex); 删除指定范围内的元素,将toIndex后的元素移动到fromIndex的位置
- writeObject/readObject; 用于序列化操作,这里注意Object elementData[]数组是被一个transient变量修饰,序列化时不会将transient变量进行序列化
遍历集合的三种方式
有三种方式遍历ArrayList:
- for 使用ArrayList自身属性和方法完成遍历
- Iterator 使用AbstractList.Itr迭代器完成遍历
- forEach 内部实现就是用Iterator,所以本质也是Iterator方式迭代
遍历集合并删除元素的两种方式
- for 倒序遍历删除
public void remove(ArrayList<String> list) {
for(int i = list.size() - 1; i > 0; i++) {
if("StringA".equals(list.get(i))) {
list.remove(i);
}
}
}
- Iterator 遍历删除
public void remove(ArrayList<String> list) {
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
if("StringA".equals(iterator.next())) {
iterator.remove();
}
}
}
AbstractSequentialList
支持顺序访问数据的抽象顺序列表集合,继承AbstractList
抽象类,并重写了操作方法,所有的操作都基于public abstract ListIterator listIterator(int index);
来实现。iterator方法内部是调用AbstractList
实现的ListItr内部对象。
LinkedList
双向链表,可以用于实现栈、队列或双端队列等数据结构。链表也是非线程安全的,非同步的。
!!!这里后续要改进下,不要直接贴和解读代码,要先把思路讲解一下,在看代码。比如链表提供了什么功能,每个功能做什么用的,然后再看具体代码是怎么实现的!!!
LinkedList内部是使用双端链表进行保存的,addFirst将元素添加到链表头,addLast将元素添加到链表尾部,add方法默认将元素添加到链表尾部,remove方法将指定元素从链表中删除,clear清空链表。
LinkedList.Entry
链路节点类定义,任何Entry都是一个节点。
Entry第一个参数为节点值,第二个参数为下一个节点引用,第三个参数为上一个节点引用。
// 链表节点,链表的组成部分
private static class Entry {
Object element; // 节点值
Entry next; // 下一个节点
Entry previous; // 上一个节点
Entry(Object element, Entry next, Entry previous) {
this.element = element;
this.next = next;
this.previous = previous;
}
}
LinkedList.ListItr
LinkedList内部实现了ListInterator迭代器。
LinkedList
LinkedList初始化默认有一个空节点Entry header,Entry的previous/next指针都指向自己。
private transient Entry header = new Entry(null, null, null); // 初始会创建一个空的Entry节点,链表节点都是用Entry来表示的。记住该版本的链表只有header,当next指针指向header的时候,表示链表尾。
private transient int size = 0; // 链表长度
public LinkedList() {
header.next = header.previous = header; // 初始化的时候会构建一个空的循环链表
}
LinkedList.add(Object o)
LinkedList.add方法默认将元素添加到链表头。
add方法内部调用addBefore方法添加元素,添加到header后面
public boolean add(Object o) {
// header
// header<->entryA
addBefore(o, header);
return true;
}
private Entry addBefore(Object o, Entry e) {
// 这不是头插法吗?
// header<->newEntry
// header<->entryA
Entry newEntry = new Entry(o, e, e.previous);
newEntry.previous.next = newEntry;
newEntry.next.previous = newEntry;
size++;
modCount++;
return newEntry;
}
LinkedList.remove(Object o)
删除第一个遇到的元素,从header链表头开始遍历判断。
- 先循环遍历链表,找到要删除的Entry节点;
- 删除Entry节点,通过移动previous和next指针实现删除操作。
public boolean remove(Object o) {
if (o==null) {
for (Entry e = header.next; e != header; e = e.next) {
if (e.element==null) {
remove(e);
return true;
}
}
} else {
// 从header.next第一个节点开始遍历,通过equals方法比较元素是否相等
for (Entry e = header.next; e != header; e = e.next) {
if (o.equals(e.element)) {
// 找到了,删除Entry节点
remove(e);
return true;
}
}
}
return false;
}
// 移除当前节点
// head <--> Entry A <--> Entry B <--> Entry C <--> head(the one)
private void remove(Entry e) {
if (e == header)
throw new NoSuchElementException();
e.previous.next = e.next; // 将当前元素的上一个元素的next指针指向当前元素的next对象
e.next.previous = e.previous; // 将当前元素的下一个元素的previous指针指向当前元素的previous对象
size--;
modCount++;
// 这样操作完之后,当前元素的next指针还会指向next对象,previous指针还会指针previous对象。这里只改变了上一个和下一个元素的指针指向。并不会改变自己的。所以1.2中即使remove元素,元素对象还是不会被回收。
// 2018/10/17 jdk1.5优化了这个现象,将e.next = e.previous = null 以及 e.element = null
}
LinkedList.get(int index)
通过下标值获取链表元素,链表遍历只看线性遍历。但是LinkedList是双向链表,所以可以正序遍历也可以倒序遍历。
- 首先判断index是在上半区还是下半区;
- 如果是在上半区,就从0开始正序遍历;如果是在下半区,就从size开始倒序遍历;
- 遍历的时候只要取得节点的next值即可。
// 根据坐标获取元素
public Object get(int index) {
return entry(index).element;
}
private Entry entry(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+size);
Entry e = header;
if (index < size/2) { // 一次折半查找,从前半部分或者后半部分开始查找
for (int i = 0; i <= index; i++)
e = e.next;
} else {
for (int i = size; i > index; i--)
e = e.previous;
}
return e;
}
Map
键值对映射集合,用于取代Dictionary的。Map接口定义基本接口:get,put,remove,keySet,values,entrySet,并定义了一个Entry接口,用于表示Map中的节点。AbstractMap
抽象类实现了Map的基本接口,主要用到的是keySet,values,将entrySet重写了抽象方法,由具体Map实现类来实现。
HashMap
Map接口的实现,允许键和值为null,无序的Map。
- key/value允许为null,key为null的节点全部存储在第一个位置;
- HashMap是无序的,指的是key无序,无法按key值进行排序输出;
特性:
- 跟HashTable结构很像,除了HashMap是非同步的,以及允许null值。
- get和put操作的时间复杂度是常量值。
- 初始容量值 initial capacity 和 加载因子 load factor 影响HashMap性能,因为这两个参数决定Map的容量大小和扩容时机。
HashMap.Entry
HashMap基于数组和链表结构来存储元素,数组结构表示HashMap的容量值,链表用于存储实际的元素。内部实现了一个自定义的HashIterator迭代器。
- Entry有四个属性:key、value、next 下一个节点,还有一个hash 保存key的hash值,方面之后使用比较,不用再重新计算。
// 数组
private transient Entry table[];
// 单向链表
private static class Entry implements Map.Entry {
int hash;
Object key;
Object value;
Entry next;
Entry(int hash, Object key, Object value, Entry next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
HashMap.Entry重写了equals和hashCode方法。
- equals方法通过比较两个Entry的key和value是否都相等
- hashCode方法使用key的hash值与value的hash值进行与操作
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
(value==null ? e.getValue()==null : value.equals(e.getValue()));
}
public int hashCode() {
return hash ^ (value==null ? 0 : value.hashCode());
}
构造函数
初始化创建一个Entry类型的数组
private int threshold; // 根据capacity和loadFactor计算出来的,用于扩容HashMap用的
private float loadFactor;
private transient int modCount = 0; // 记录Map的修改次数,迭代的时候可以用来判断集合是否被修改
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Initial Capacity: "+
initialCapacity);
if (loadFactor <= 0)
throw new IllegalArgumentException("Illegal Load factor: "+
loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry[initialCapacity]; // 初始化的时候会创建默认一个Entry数组(JDK1.8版是在put操作初始化的)
threshold = (int)(initialCapacity * loadFactor); // 计算出Map扩容值,当Map的元素值超过这个阈值时,Map就会进行扩容
}
put
put(Object key, Object value)逻辑:
- 根据key的hash值,计算出index下标值,获取数组中对应index的Entry链表节点
- 如果Entry节点不为空,则遍历该Entry链表,通过hashCode和equals方法来判断key是否相等;如果key相等,则直接替换value值,并把旧的值返回。
- 如果没找到对应的key,说明key在map中不存在,将会新建一个Entry节点;在新建Entry前先判断map是否需要扩容;如果需要扩容,调用rehash方法进行扩容;
- 扩容完成后,开始新节点,节点的构造函数提供了四个参数(key的hash值,key,value,下一节点);这里采用头插法,将新节点插入到链表头。
// 总结下put的逻辑:
// key null和非null的处理逻辑不同
// key null,添加到tab数组第一个位置
// key 非null,先获取key的hash值,计算得出hash对于数组tab的下标值index,取出tab[index]的链表
// 通过hash和equals方法遍历链表值是否与key相等,如果相等则直接替换,并返回旧值。
public Object put(Object key, Object value) {
// Makes sure the key is not already in the HashMap.
Entry tab[] = table;
int hash = 0;
int index = 0;
if (key != null) {
hash = key.hashCode(); // 取得key的hashCode值,注意hashCode值有可能相同
index = (hash & 0x7FFFFFFF) % tab.length; // 计算key的数组下标
for (Entry e = tab[index] ; e != null ; e = e.next) { // 遍历Entry链表,这里从tab数组中取出的元素是一个链表结构。
if ((e.hash == hash) && key.equals(e.key)) { // 如果key已存在,则value覆盖,并返回old的值
Object old = e.value;
e.value = value;
return old;
}
} // 这里for循环的结果是:1)得出结论,key在这个Entry链表中不存在,可以直接插入;2)Entry为null。
} else {
// key允许为null
// 默认数组第一个位置放key为null的元素
for (Entry e = tab[0] ; e != null ; e = e.next) {
if (e.key == null) { // key为null的比对是直接用==,所以理论上tab[0]有且只要一个链表节点
Object old = e.value;
e.value = value;
return old;
}
}
}
// 只有当map的size值发生变化,modCount值才会变化
modCount++; // key不存在map中,为新的key,则modCount++,表示Map要增加了
// 如果集合当然容量超过阈值,则进行扩容
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry. 创建新的链表节点Entry
Entry e = new Entry(hash, key, value, tab[index]); // 创建新的Entry,插入到Entry链表头
// 这里并发执行会有问题,两个线程同时执行,其中一个线程的数据会丢失
tab[index] = e; // new Entry方法,会将现有Entry链表添加到新Entry的后面。
count++;
return null;
}
rehash
Map扩容,对集合元素进行重新hash。如果集合容量不够,将会对集合进行扩容,新建一个更大容量的Map,然后将旧Map的元素全部重新hash到新Map。
- 默认扩容1.5倍,创建一个新的Map集合,并根据新的容量值计算出新的threshold值
- 倒序遍历方式,遍历旧Map,取出每一个Entry节点,节点的key与新的容量值进行hash操作,计算Entry节点在新Map的位置。
// 对Map进行扩容,扩容到更大的数组,需要重新计算下标
private void rehash() {
int oldCapacity = table.length;
Entry oldMap[] = table;
int newCapacity = oldCapacity * 2 + 1;
Entry newMap[] = new Entry[newCapacity];
modCount++;
threshold = (int)(newCapacity * loadFactor);
// 并发rehash问题的引入,这里创建完新集合就直接赋值给全局table了。
// 多线程同时rehash
table = newMap;
// 取出每一个元素,重新计算下标,插入新数组。
for (int i = oldCapacity; i-- > 0;) { // 倒序遍历Entry数组
for (Entry old = oldMap[i] ; old != null ; ) { // 顺序遍历Entry链表
Entry e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity; // 重新计算新的数组下标
// 以下两行代码在并发扩容的时候会出现死循环,JDK1.8之后改为尾插入,可以避免死循环问题,但是在并发put的时候会出现数据覆盖问题。
e.next = newMap[index]; // 将Entry添加到链表头,这样扩容完后,是不是就改变的元素的顺序,对的。
newMap[index] = e;
}
}
}
get
根据key获取value
- 根据key的hash值计算出对应节点位置下标
- 然后遍历节点,通过hashCode和equals方法判断key是否相等,相等则返回对应key的value值,否者返回null
// 通过键获取值,通过hash获取key所在Entry数组下标,然后遍历该下标的Entry链表,返回值。注意:同一个下标的Entry链表,hash值可能是相同的
public Object get(Object key) {
Entry tab[] = table;
if (key != null) {
int hash = key.hashCode(); // 获取键的hash值
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry e = tab[index]; e != null; e = e.next)
if ((e.hash == hash) && key.equals(e.key)) // 遍历Entry链表,判断Entry的hash和键是否相等
return e.value;
} else {
for (Entry e = tab[0]; e != null; e = e.next)
if (e.key == null)
return e.value;
}
return null;
}
remove
删除key,首先要先找到对应的key,查找方式类似get操作。找到key后,执行删除操作,因为节点是双向链表结构,所以删除操作需要移动双向指针。
链表节点删除操作,变量previous保存上一个节点,删除时直接将previous的next指针指向Entry节点的next。
public Object remove(Object key) {
Entry tab[] = table;
if (key != null) {
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
// 所以这里就是一个链表基本删除操作
// 单链表删除,要保存上一个元素
for (Entry e = tab[index], prev = null; e != null; // prev变量用来保存上一个Entry
prev = e, e = e.next) {
if ((e.hash == hash) && key.equals(e.key)) {
modCount++; // 做删除操作,集合size有变,所以modCount++
if (prev != null) // 如果prev不为空,直接将prev的next指针指向要删除元素的next对象
prev.next = e.next;
else // 如果prev为null,说明e为链表头结点,则将链表头指向e的next对象
tab[index] = e.next;
count--;
Object oldValue = e.value;
e.value = null;
return oldValue;
}
}
} else {
for (Entry e = tab[0], prev = null; e != null; // key为null,默认保存在Entry数组的第一个位置
prev = e, e = e.next) {
if (e.key == null) {
modCount++;
if (prev != null)
prev.next = e.next;
else
tab[0] = e.next;
count--;
Object oldValue = e.value;
e.value = null;
return oldValue;
}
}
}
return null;
}
Map的迭代器
首先先介绍下keySet和entrySet方法,以及values方法。
keySet顾名思义就是返回集合中的所有key,以set集合方式返回。
entrySet就是返回集合中的所有Entry节点,也是以Set集合方式返回。
values就是返回集合中的所有value值,set集合方式返回。
三个方法的内部都是实例化了一个AbstractSet对象,并实现了iterator/size/contains/remove/clear方法
keySet
iterator方法返回一个HashIterator对象;
其余方法(size/contains/remove/clear)都是直接调用HashMap的方法实现的
这里先看一下HashMap的HashIterator迭代器实现方式。
HashMap.HashIterator
HashIterator实现了Iterator迭代器接口,支持3种类型的迭代(KEYS/VALUES/ENTRIES)。接下来看下HashIterator是如何实现hasNext/next/remove这三个方法的。
这里要先说明一下HashMap的迭代判断方式与ArrayLlist的不同,ArrayList是一个一维数组,所以只有判断索引不大于size即可;HashMap是一个数组链表,除了数组索引不能越界,还有去遍历判断链表节点。
HashIterator.hasNext()
判断HashMap是否还有下一个元素。
这里有两个判断条件,第一个是数组不能越界,第二个是链表节点不为空。
- entry节点不为null就直接返回true;
- entry节点为null,就判断index索引是否大于0,如果小于0,说明遍历结束,返回false;index大于0,说明遍历还没结束,取出index–取出下一个节点
// 只有entry不为null,就说明还可以继续遍历;如果entry为null,就判断index是否>0,如果小于0就没有结束了;
// index>0,entry=null,就继续取出下一个数组的节点赋值给entry
public boolean hasNext() {
// entry == null 有两种可能:1.还没开始遍历;2.遍历到链表末尾了;
// index > 0 这里的index初始值是size的值,所以只有>0说明数组坐标还未越界
while (entry==null && index>0)
entry = table[--index];
return entry != null;
}
HashIterator.next()
取出下一个元素值
public Object next() {
// 使用迭代器方式遍历Map,外部不能修改集合,否者抛异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
// 如果entry为null,且index大于0,则取出下一个数组节点
while (entry==null && index>0)
entry = table[--index]; // 倒序
if (entry != null) {
// lastReturned 遍历保存上一个节点值
// e 保存当前节点值
Entry e = lastReturned = entry;
// 取出下一个节点赋值给entry
entry = e.next;
// 根据迭代类型,判断返回的值。HashMap迭代器支持3种类型(构造器中要指明类型),1.返回entry的key;2.返回entry的value;3.直接将entry返回
return type == KEYS ? e.key : (type == VALUES ? e.value : e);
}
// 这里异常的抛出条件是 entry == 0 && index == 0 说明没有节点可以再遍历了
throw new NoSuchElementException();
}
HashIterator.remove()
删除当前元素,即上一次next方法返回的节点,lastReturned记录的节点值。
- 调用remove方法前一定要调用next方法,remove操作是基于next方法返回的节点
- 然后在Map中找到该节点的位置,然后通过移动指针将其删除
通过节点的hash计算出index值,取出Entry链表,然后通过==找到节点;节点找到后通过移动next指针实现删除操作
public void remove() {
// 说明上次没有调用next方法,或则next没有返回值,也直接抛出了异常
if (lastReturned == null)
throw new IllegalStateException();
// 说明在集合迭代过程中,外部有修改集合结构,比如新增或则删除
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry[] tab = HashMap.this.table;
// 取得要删除节点的数组index位置
int index = (lastReturned.hash & 0x7FFFFFFF) % tab.length;
for (Entry e = tab[index], prev = null; e != null;
prev = e, e = e.next) {
if (e == lastReturned) {
// 找到该节点,然后执行删除操作
modCount++;
expectedModCount++;
if (prev == null)
tab[index] = e.next;
else
prev.next = e.next;
count--;
lastReturned = null;
return;
}
}
throw new ConcurrentModificationException();
}
}
keySet
前面分析完了HashIterator,现在来看看KeySet和EntrySet具体是如何使用的这个迭代器的。
KeySet方法,内部创建了一个Set对象返回,并实现了interator/size/contains/remove/clear四个方法;
- iterator 方法,创建了一个KEYS类型的HashIterator迭代器对象返回;
- size/contains/remove/clear四个方法,都是直接调用HashMap对应的方法实现
HashMap.this
。
public Set keySet() {
if (keySet == null) {
keySet = new AbstractSet() {
public Iterator iterator() {
return new HashIterator(KEYS);
}
public int size() {
return count;
}
public boolean contains(Object o) {
return containsKey(o);
}
public boolean remove(Object o) {
return HashMap.this.remove(o) != null;
}
public void clear() {
HashMap.this.clear();
}
};
}
return keySet;
}
entrySet
EntrySet方法,内部也是创建了一个Set对象返回,一样实现了interator/size/contains/remove/clear五个方法;但是跟KeySet不同,EntrySet除了iterator方法,其余四个方法都自定义实现。
- iterator 方法,创建了一个ENTRIES类型的HashIterator迭代器对象返回;
- contains(Object o) 方法,实现了HashMap的get操作,通过查找对象o的hash值计算出数组坐标,取出节点链表,然后遍历链表通过hashCode和equals方法比对o。
- remove(Object o) 方法,实现了HashMap的remove操作,先查找到对象在链表的位置,然后通过移动next指针实现节点删除。
- size() 方法,直接返回HashMap的size值;
- clear() 方法,调用HashMap的clear方法。
values
values方法,内部创建的是一个Collection集合,实现了interator/size/contains/clear四个方法;
- iterator 方法,创建了一个VALUES类型的HashIterator迭代器对象返回;
- contains(Object o) 方法,调用HashMap的containsValue方法;
- size() 方法,直接返回HashMap的size值;
- clear() 方法,调用HashMap的clear方法。
HashMap的遍历方式
HashMap的遍历方式主要是通过keySet、entrySet两个方法来实现遍历的。
- forEach map.keySet() 遍历key,然后通过key获取value
- map.entrySet().interator(),通过entrySet的迭代器方法遍历
- forEach map.entrySet(),内部也是使用entrySet的迭代器实现
- forEach map.values(),这种方式只能遍历value。
HashMap的遍历删除
通过Iterator迭代器实现,比如entrySet方法返回的iterator迭代器。
HashMap的其他方法
// 判断值是否在Map中,要遍历整个Map
// 所以containsValue非常耗时
public boolean containsValue(Object value) {
Entry tab[] = table;
if (value==null) {
for (int i = tab.length ; i-- > 0 ;)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (e.value==null)
return true;
} else {
// 后续遍历
for (int i = tab.length ; i-- > 0 ;) // 循环遍历Entry数组,每个Entry是一个链表结构
for (Entry e = tab[i] ; e != null ; e = e.next) // 再循环判断Entry链表,判断值是否相等
if (value.equals(e.value)) // 通过equals方法判断值是否相等
return true;
}
return false;
}
// 判断键是否在集合中,通过hash获取key所在的Entry数组下标,然后判断该下标的Entry链表即可。
// contains逻辑与get一模一样
public boolean containsKey(Object key) {
Entry tab[] = table;
if (key != null) {
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length; // 获取key的Entry数组下标
for (Entry e = tab[index]; e != null; e = e.next) // 遍历Entry链表,判断键是否相等
if (e.hash==hash && key.equals(e.key))
return true;
} else {
for (Entry e = tab[0]; e != null; e = e.next)
if (e.key==null)
return true;
}
return false;
}
// 清空map
public void clear() {
Entry tab[] = table;
modCount++;
for (int index = tab.length; --index >= 0; ) // 遍历Entry数组
tab[index] = null; // 将Entry链表置为null
count = 0;
}
key hash值的设计技巧
可以自定义key的hash值计算方式
比如:64位的MD5码,对于10W的字符串来说,只需要取前3-4个字符串值的乘积当作hash值,因为MD5码的散列性质,得到的随机性就足够强。
HashMap线程不安全
首先HashMap是线程不安全的,其主要体现:
-
在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。
-
在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。
TreeMap
有序的HashMap,基于红黑树实现的Map,实现了SortedMap接口。时间复杂度O(lgn)
Java集合中的TreeSet和TreeMap(以及,JDK1.8的HashMap),C++ STL中的set、map,以及Linux虚拟内存的管理,都是通过红黑树去实现的。(MySQL索引是B+树)
红黑树
红黑树,树相关的内容可以专门放一篇文章介绍。
应用场景以及和B树的区别:
红黑树多用在内排(内部排序,即全放在内存中的),微软STL的map和set的内部实现就是红黑树。
B树多用在外排(内存里放不下,大部分数据存储在外存上时),因为B树层数少(这里的B表示平衡balance,红黑树也是一种平衡树),因此可以确保每次操作,读取磁盘的次数尽可能的少。B树是为了提高磁盘或外部存储设备查找效率而产生的一种多路平衡查找树。
B+树为B树的变形结构,用于大多数数据库或文件系统的存储而设计。
在数据较小,可以完全放到内存中时,红黑树的时间复杂度比B树低。 反之,数据量较大,外存中占主要部分时,B树因其读磁盘次数少,而具有更快的速度。
在大规模数据存储的时候,红黑树往往出现由于树的深度过大而造成磁盘IO读写过于频繁,进而导致效率低下的情况。磁盘IO代价主要花费在查找所需的柱面上,树的深度过大会造成磁盘IO频繁读写。根据磁盘查找存取的次数往往由树的高度所决定。
红黑树的约束条件是(这里要配一副画会更清晰):
- 节点要么是红色要么是黑色。
- 根节点固定是黑色。
- 红色父节点的两个子节点必须是黑色。
- 从叶子节点到根节点的所有路径必须包含相同数量的黑节点。
红黑树平衡调整操作:
- 变换颜色。
- 旋转节点。
红黑色操作技巧:
- 除根节点,每次新插入的节点都是红色。
- 插入时如果父节点是黑色,不用调整树,直接插入。
- 当前节点为左孩子时,如果父节点X是红色,父节点的兄弟节点Y也是红色(祖父节点P都是黑色) –> 将X,Y的变黑,P变红,然后将当前节点指向P,从P开始重新计算平衡。
- 当前节点为右孩子时,如果父节点X是红色,父节点的兄弟节点Y是黑色(祖父节点P都是黑色) –> 将当前节点指向X,然后以X节点做左旋转操作,再从X开始重新计算平衡。
- 当前节点为左孩子时,如果父节点X是红色,父节点的兄弟节点Y是黑色(祖父节点P都是黑色) –> 将X变黑,P变红,然后将当前节点指向P,以P节点做右旋转操作,再将P变黑,再从P开始重新计算平衡。
二叉树、平衡二叉树等概念先导
二叉树(Binary Tree, B Tree)
- 一个节点最多只能有两个子节点
- 左子树值 < 父节点值 < 右子树值
- 对树深度和结构没有要求,所以二叉树可以出现极端的N深度(退化成链表)。
平衡二叉树(AVL Tree)
- 解决二叉树深度不平衡问题,任何节点的两个子树高度最大高度差为1;
- 对AVL Tree进行插入或删除节点操作,导致AVL Tree失去平衡,则需要进行旋转让Tree恢复平衡(避免退化成链表)。
平衡多路二叉树(Balance Tree, B Tree, B+ Tree)
对子节点个数没有限制,一个节点可以有多个子节点
B+ Tree(Balance+ Tree)
- B+ Tree非叶子节点不存放数据,只存放keys;
- B+ Tree叶子节点之间存在指针相连,而且是单链表。
红黑树(RBTree)
先抛一个问题:有了平衡二叉树,为什么还要红黑树?
回答:平衡二叉树每次是通过旋转来调整树平衡,如果在插入、删除非常频繁的场景中,平衡二叉树就要频繁的进行调整,性能降低。所以为了解决频繁调整树结构问题,引入了红黑树。它能够在实现树平衡的基础上,避免频繁调整树结构。
红黑树是通过旋转和着色来实现平衡的
tips:日常说的B/B+树,的B表示平衡(balance),而不是二叉(binary),B/B+树是一种平衡二叉树。
树总结:平衡二叉树解决了二叉树在极端条件退化成链表的问题,红黑树解决了平衡二叉树在插入、删除等操作需要频繁调整的问题。
存储节点
TreeMap是基于红黑树存储的,树型结构需要保存父节点、左右子节点,红黑树结构还需保存节点颜色。
// 红黑树节点
static class Entry implements Map.Entry {
Object key; // 节点值
Object value;
Entry left = null; // 左孩子
Entry right = null; // 右孩子
Entry parent; // 父节点
boolean color = BLACK; // 节点颜色
}
节点构造函数提供参数为key、value,和父节点
Entry(Object key, Object value, Entry parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
构造函数
TreeMap的构造函数提供传入比较器Comparator,因为TreeMap是有序的Map,需要对key进行排序。用户可以实现自定义的比较器,TreeMap在插入元素时会使用用户传入的比较器对元素进行比较。
private transient Entry root = null;
private Comparator comparator = null; // 比较器,用于比较两个元素的大小来决定顺序位置
private transient Entry root = null; // 红黑树,树根
public TreeMap(Comparator c) {
this.comparator = c;
}
put
TreeMap的put操作,需要对key进行排序
public Object put(Object key, Object value) {
Entry t = root;
if (t == null) {
incrementSize();
root = new Entry(key, value, null); // 如果树为空树,则创建第一个root节点
return null;
}
// 这里为什么while循环?因为要递归判断子节点
// 左子树的值比右子树的小,左小右大
while (true) {
// 首先比较key值
int cmp = compare(key, t.key);
if (cmp == 0) {
return t.setValue(value); // 如果key相同,直接替换value值
} else if (cmp < 0) { // 如果小于,就放到左子树
if (t.left != null) {
t = t.left; // 如果左子树不为空,继续递归比较左子树
} else {
incrementSize(); // 如果是左子树的叶子节点,就新建一个节点
t.left = new Entry(key, value, t); // 将节点插入该位置
fixAfterInsertion(t.left); // 然后确保树平衡,尝试旋转树节点到平衡
return null;
}
} else { // cmp > 0 // 如果大于,就放到右子树
if (t.right != null) {
t = t.right; // 如果右子树不为空,继续递归比较右子树
} else {
incrementSize();
t.right = new Entry(key, value, t);
fixAfterInsertion(t.right); // 确认树平衡
return null;
}
}
}
}
再来看看compare方法
如果用户有传入自定义compartor比较器,TreeMap则使用自定义比较器对两个节点的key进行比较; 否者使用key自身的默认compartor实现进行比较,所以这里要注意,TreeMap的key元素一定要有实现了Comparable接口。
JDK里面自带的一些类都实现了Comparable接口,比如String、Integer、BigDecimal。
private int compare(Object k1, Object k2) {
return (comparator==null ? ((Comparable)k1).compareTo(k2)
: comparator.compare(k1, k2));
}
节点旋转代码,调整树结构,确保树的平衡。
/** From CLR **/
private void fixAfterInsertion(Entry x) {
x.color = RED;
while (x != null && x != root && x.parent.color == RED) {
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
Entry 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);
if (parentOf(parentOf(x)) != null)
rotateRight(parentOf(parentOf(x)));
}
} else {
Entry 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);
if (parentOf(parentOf(x)) != null)
rotateLeft(parentOf(parentOf(x)));
}
}
}
root.color = BLACK;
}
get
基于树查找,通过比较key的值,最大实际查找复杂度为树的深度O(logN)。
public Object get(Object key) {
Entry p = getEntry(key);
return (p==null ? null : p.value);
}
private Entry getEntry(Object key) {
Entry p = root;
while (p != null) {
int cmp = compare(key,p.key); // 比较key的大小
if (cmp == 0) // 这里使用二分查找,小于根节点,就从左子树迭代查找;大于根节点,从右子树查找;知道节点等于返回
return p;
else if (cmp < 0)
p = p.left;
else
p = p.right;
}
return null;
}
remove
public Object remove(Object key) {
Entry p = getEntry(key);
if (p == null)
return null;
Object oldValue = p.value;
deleteEntry(p);
return oldValue;
}
private void deleteEntry(Entry p) {
decrementSize();
// If strictly internal, first swap position with successor.
if (p.left != null && p.right != null) {
Entry s = successor(p);
swapPosition(s, p);
}
// Start fixup at replacement node, if it exists.
Entry 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;
// Null out links so they are OK to use by fixAfterDeletion.
p.left = p.right = p.parent = null;
// Fix replacement
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { // return if we are the only node.
root = null;
} else { // No children. Use self as phantom replacement and unlink.
if (p.color == BLACK)
fixAfterDeletion(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.Iterator
现在来看下TreeMap实现的迭代器,TreeMap迭代器也支持KEYS/VALUES/ENTRIES三种类型
hasNext
next
remove
TreeMap.SubMap
TreeMap其他方法
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
private int compare(Object k1, Object k2) {
return (comparator==null ? ((Comparable)k1).compareTo(k2) // 如果没有指定比较器,就用元素自带的比较方法
: comparator.compare(k1, k2)); // 否则用比较器实现的比较方法
}
// 查找元素是否在集合中,这里要遍历树,使用前序遍历整颗树。
public boolean containsValue(Object value) {
return (root==null ? false :
(value==null ? valueSearchNull(root)
: valueSearchNonNull(root, value)));
}
private boolean valueSearchNonNull(Entry n, Object value) {
// Check this node for the value
if (value.equals(n.value))
return true;
// Check left and right subtrees for value
return (n.left != null && valueSearchNonNull(n.left, value)) || // 前序遍历树查找值
(n.right != null && valueSearchNonNull(n.right, value));
}
// 返回集合中的第一个元素,采用前序遍历
public Object firstKey() {
return key(firstEntry());
}
private Entry firstEntry() {
Entry p = root;
if (p != null)
while (p.left != null) // 一直遍历左子树直到左叶子节点
p = p.left;
return p;
}
// 返回集合中的最后一个元素,采用后序遍历
public Object lastKey() {
return key(lastEntry());
}
private Entry lastEntry() {
Entry p = root;
if (p != null)
while (p.right != null) // 一直遍历右子树直到右叶子节点
p = p.right;
return p;
}
WeakHashMap
这里先介绍下Java里的四种引用类型
- 强引用 日常的Java对象都是强引用,只有将引用指向null,对象才会被回收
- 软引用 内存不足时会被回收
- 弱引用 只要发生垃圾回收,就会被回收
- 虚引用 只要发生垃圾回收,就会被回收。不过在回收前会被加入到一个队列里。可以操作该队列重新引用该对象。
WeakHashMap表示key为弱引用WeakKey,继承了WeakReference,垃圾回收队列通过ReferenceQueue实现。
ReferenceQueue保存要被垃圾回收的key,WeakHashMap每次对对集合进行操作前,都会先同步map和queue的值,把被垃圾回收的key从map中删除。
// 表示key是一个弱引用,在GC时会被回收
static private class WeakKey extends WeakReference {
private int hash; /* Hashcode of key, stored here since the key
may be tossed by the GC */
}
// 内部使用HashMap
private Map hash;
// 垃圾回收队列,当weakkey要被回收时,会先加入到该队列。
private ReferenceQueue queue = new ReferenceQueue();
Set
元素结合,集合内元素唯一、不重复。
AbstractSet抽象类实现了Set接口,重写了equals和hashCode方法。
HashSet
基于HashMap实现的,
private transient HashMap map; // 基于HashMap实现,内部其实只是用HashMap来保存元素
private static final Object PRESENT = new Object(); // 表示HashMap的value,因为Set不是键值对集合。
// 添加元素到Set中
public boolean add(Object o) {
return map.put(o, PRESENT)==null; // 用HashMap保存,key为元素值,value为固定值object
}
public boolean remove(Object o) {
return map.remove(o)==PRESENT; // 整个HashMap的value都是同一个对象PRESENT
}
Serializable
java.io.Serializable 接口是 JDK 1.1 引入的,对应的还有 java.io.Externalizable 接口继承了 Serializable。这两个接口都用于序列化Java对象。
对象序列化的作用:
- 将对象序列化永久存储在磁盘上,通常存放在一个文件中;
- 应用在网络通信的中,将对象序列化后进行数据传输。
序列化
Serializable接口会将对象的所有非静态属性(包括私有属性和引用的对象)进行序列化,不需要序列化的属性可以使用transient或static关键字修饰。序列化过程中会自动调用writeObject方法,反序列化会调用readObject方法。所以如果需要自定义序列化过程可以重写这两个方法。
Externalizable接口会调用WriteExternal和readExternal方法。
反序列化
反序列化是通过反射原理实现的,Serializable接口实现的类无需提供无参构造函数,Externalizable接口需要提供无参构造函数。
Serializeable接口反序列化会调用父类的无参构造函数,如果无父类,所有类的父类默认都是Object,最终会调用Object的无惨构造函数。
Externalizable接口反序列化会先调用类的public无参构造函数实例化对象,然后再调用readExternal方法进行反序列化
序列化版本号
static final long serialVersionUID 反序列化过程中会校验串中的serialVersionUID是否与当前对象相同,不同则反序列化失败。
如果serialVersionUID没有显式生成,系统就会自动生成一个。任何一个属性的变化都会导致自动生成的serialVersionUID发生变化。
为了避免这种问题, 一般系统都会要求实现serialiable接口的类显式的声明一个serialVersionUID。
显式定义serialVersionUID的两种用途:
- 希望类的不同版本对序列化兼容时,需要确保类的不同版本具有相同的serialVersionUID;
- 不希望类的不同版本对序列化兼容时,需要确保类的不同版本具有不同的serialVersionUID。
Thread
基本方法
- interrupt(); 中断此线程,但实际只是给线程设置一个中断标识,线程会继续执行。(那什么时候触发?哪里起作用?)
interrupt方法只是给线程设置为“中断”状态,不会停止线程。需要用户自己去监控线程状态并做出处理。
- Thread.interrupted(); 检查当前线程的“中断”标识位,如果被标识为“中断”返回true,并清除标志位。所以第二次调用返回false。
- isInterrupted(); 返回当前线程的“中断”标志位,但不清除标志位。所以可以多次调用检查。
Thread.interrupted和isInterrupted两个方法内部都是调用线程的native isInterrupted(bllean clearInterrupted)方法。native方法都是检查线程的“中断”标志位,参数clearInterrupted表示是否要重置标志位。Thread.interrupted方法默认为true重置,isInterrupted方法默认false不重置。
interrupted
这里再来多说两句。
Thread.interrupted()方法,检查的是当前线程,即调用者线程。
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
interrupt()和isInterrupted()方法配合使用。
wait/sleep/join三个方法会响应中断interrupt()方法,所以线程如果处于这三个方法的阻塞状态中,会检测线程“中断”状态,如果“中断”则会响应中断程序,抛出InterruptedException异常,同时将“中断”标志位重置。
注意:线程的当前状态处于非阻塞状态,那么仅仅是线程的中断标志被修改为true而已;如果线程的当前状态处于阻塞状态,那么在将中断标志设置为true后,才会响应中断。
调用Thread.interrupted方法,但不做任何处理,大部分情况只是为了清除“中断”标志位而已。
ThreadLocal
set方法
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
map.set(this. value);
this是ThreadLocal对象
key为什么用weak,因为当threadlocal=null,每个线程的key还引用着,就回收不了。
所以将key设置为weak