Collection
所有的Java集合都继承了Collection接口,而Collection接口又集成了Iterable(迭代器)接口。也就是说所有的Java集合类(ArrayList、HashMap等)都可以使用iterator进行遍历,且都实现了Collection接口中规定的方法。
List
ArrayList
先说结论ArrayList是动态数组,底层实现是一个Object数组。当使用无参构造器ArrayList()
时,初始容量elementData
为0。第一次扩容时容量+10。之后再次需要扩容时,扩为原容量的1.5倍。当使用有参构造器时,初始初始容量为传进去参数的容量大小。需要扩容时,扩为原容量的1.5倍。
ArrayList的底层实现
ArrayList的底层实现是一个Object数组。由于是Object数组所以,int等基本类型不能往里面放得先转成Integer等类。transient
表示在序列化时忽略
transient Object[] elementData;
ArrayList扩容
ArrayList有三个构造器ArrayList()
,ArrayList(Collection<? extends E> c)
,ArrayList(int initialCapacity)
当使用无参构造器ArrayList()
时,初始容量elementData
为0。第一次扩容时容量+10。之后再次需要扩容时,扩为原容量的1.5倍。源码中扩为原容量的1.5倍是使用位运算来进行的。
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
oldCapacity >> 1
表示oldCapacity 向右移动一位也就是oldCapacity 除以2。位运算详细内容可以看我之前写的位运算说明
当使用有参构造器时,初始初始容量为传进去参数的容量大小。需要扩容时,扩为原容量的1.5倍。
扩容时会new一个新的Object[],再把老的数组拷贝过去,因此扩容后ArrayList中的元素内存地址会发生改变。当然ArrayList的内存地址没变。
elementData和Size的区别
elementData
容量和size
大小是不一样的,size表示ArrayList最后一个元素所在的位置,elementData数组是ArrayList实际存放元素的地方。size<=elementData的容量。
ArrayList的最大容量
根据ArrayList源码,ArrayList的最大容量为Integer.MAX_VALUE - 8也就是2147483639。为什么不是Integer.MAX_VALUE呢?因为有些虚拟机在数组中保留一些header words,所以预留了8来防止越界
/**
* The maximum size of array to allocate (unless necessary).
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
Vector
先说结论Vector也是动态数组,基本使用方式和ArrayList差不多,但是两者有些许区别。
| | 效率 |线程安全|
|–|–|–|–|–|–|
| Vector |低 |安全|
| ArrayList| 高 |不安全|
Vector有三个构造器,不传参默认初始容量为10(调用了那个单参构造器)。其他的两个有参构造器默认容量为参数大小。当Vector扩容时,如果capacityIncrement(扩容增量不为0),那么就在原基础上扩容 capacityIncrement的大小。如果capacityIncrement为0则容量翻倍。
Vector类
Vector类中重要的三个成员变量和一个常量分别是
protected Object[] elementData; 存放元素的数组
protected int elementCount; 实际元素个数(类似ArrayList的Size变量)
protected int capacityIncrement; 每次扩容的增长量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 最大容量
以及三个构造器
双参构造器,传入初始容量大小和容量增量大小。
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);
}
可以看到,不传参默认初始容量为10(调用了那个单参构造器)。其他的两个有参构造器默认容量为参数大小。
Vector扩容机制
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,newCapacity(minCapacity));
}
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);最重要
if (newCapacity - minCapacity <= 0) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
当Vector扩容时,如果capacityIncrement(扩容增量不为0),那么就在原基础上扩容 capacityIncrement的大小。如果capacityIncrement为0则容量翻倍。
Vector的线程安全
Vector的get
、remove
、add
方法都有synchronized关键字。因此Vector是线程安全的。
public synchronized boolean add(E e) {
modCount++; 定义在AbstractList中,记录被修改了几次
add(e, elementData, elementCount);
return true;
}
public synchronized boolean removeElement(Object obj) {
modCount++;
int i = indexOf(obj);
if (i >= 0) {
removeElementAt(i);
return true;
}
return false;
}
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
LinkedList
LinkedList是java实现的双向队列,他有三个成员变量。LinkedList因为是链表引起增加和删除操作速度快于ArrayList,修改和查询慢于ArrayList。
transient int size = 0; 表示链表长度
transient Node<E> first; 头节点
transient Node<E> last; 尾节点
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;
}
}
Node(根据索引查找节点)
LinkList的Node()方法是根据索引查找节点。Node()是LinkedList里面最重要的方法,增删改查都需要用到这个方法。这个Node()并不是傻傻的从头到尾遍历一遍。他是先判断索引的大小,如果索引大于size/2 (用的还是位运算),那就从尾向前遍历。如果索引小于size/2,那就从头到尾遍历。
Node<E> node(int 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;
}
}
indexOf(根据节点值查找索引)
这个就真的是傻傻的从头到尾遍历了
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
Set
HashSet
当我们new了一个HashSet然后点进他的构造器,我们就会惊讶的发现HashSet的底层实现是HashMap。而当我们传入一个集合时,HashMap的初始容量是max(集合容量/0.75+1,16)。
HashSet的实际数据存在Node里面的key
。Node中的value使用了一个静态变量PERSENT
占坑
public HashSet() {
map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
add方法
由于HashSet的底层实现是HashMap,所以很容易就能猜到HashSet的add()
方法必然会调用HashMap的put()
方法。由于HashMap的put
方法必须传入key-value。所以HashSet预留了一个PRESENT常量来占坑(占value的坑,无任何实际意义),实际的元素存放在key中。 下面代码中的E e
就是实际存放的元素。
HashSet源码
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
List和Set的区别
- 在List中允许插入重复的元素,而在Set中不允许重复元素存在。
- List是有序集合,会保留元素插入时的顺序,Set是无序集合。
- List可以通过下标来访问,而Set不能。
Map
HashMap
HashMap的底层是一个Node数组。而Node有一个next属性,也就是说他是一个链表的结点。因此HashMap的存储结构是链表头的数组。
transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
HashMap扩容机制
先说结论,HashMap除了传入集合的构造器外,其他构造器初始化的初始容量都是0。使用传入集合的构造器时,初始容量为大于等于集合size的最小2的n次。
首先来看源码中的几个常量,可以粗暴的得出结论初始容量为16,最大容量为2^30,当容量达到当前容量的75%时,就会扩容,扩容就是直接乘以2。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 2的四次方
static final int MAXIMUM_CAPACITY = 1 << 30; 2的30次方
static final float DEFAULT_LOAD_FACTOR = 0.75f; 默认载入系数
put方法
首先使用hash()
方法获得key的hash索引(hash索引!=hashcode)。可以看到如果key是空则返回0。不为空则返回key的hashCode^key的hashCode无符号右移16位(为了减少冲突)。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
获得hash索引后,根据hash索引将数据插入Map中。
- 如果那个位置无元素那就直接放入
- 如果那个位置有元素,那就比较是否相同。(判断是否是同一个对象且
equals()
,(不同类的equals是不同的。具体得看是怎么重写的)为否)
2.1 相同就直接放弃
2.2 不同就继续对比这个位置的链表中的元素,直到链表末尾都不同就放在最后。 - 元素添加到链表后,如果长度大于等于8。
3.1 数组长度大于等于64,树化该位置的链表
3.2 否则就扩容数组。