首先说一下Collection,它继承了Iterable,说明它的可实例化的子类都可以使用Iterater来做遍历。
下面开始讲一下List
List的所有实现类都是可以重复添加元素的,是可以通过索引获取元素的,是保证插入和取出顺序的。
List的实现类:
一、ArrayList:
ArrayList的底层是一个可变数组,先看他的重要成员和构造方法。
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
ArrayList的构造方法:
1:无参构造器:它会将一个 成员Object[ ]的空数组(Object[ ]==null在内存里是没有开辟空间的) 赋值给它的另一个成员 elementData[ ]。要注意的是,这里的赋值只是简单的引用而已。
2:有参构造器:它会接收一个 初始容量的值 创建一个初始容量大小的Object[ ]数组 (在内存里开辟了空间)。
ArrayList的add( )方法与扩容机制:
size 是 ArrayList的长度(大小)成员。初始默认值是0。
private int size;
进入add( )方法后 传入一个 最小容量(minCapacity)值为size+1。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
calculateCapacity( )方法非常关键:它将会给无参构造的ArrayList 定义一个最小容量( 10 )
private static final int DEFAULT_CAPACITY = 10;
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
如果 add( )方法需要的最小的容量 比现在的 elemenData[ ] 的空间要大 则增长扩容。
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
grow( )方法就是扩容机制:
其中有连个关键的变量 newCapacity-----oldCapacity
扩容机制:
newCapacity = oldCapacity +(oldCapacity >> 1) : 将旧的容量 乘以 1.5 赋值给新的容量。
然后将新的容量传入copyOf方法--->构造一个 以新容量为大小的数组,将原来elementData[ ]里面的元素拷贝到新的数组 并将引用指向 新的数组 。
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
总结ArrayList:
底层数据结构:
数组 ----->> 有索引,插入与取出有序
ArrayList的扩容机制:
如果用无参构造创建ArrayList对象, 则在第一次添加元素的时候会 分配10 个内存空间 , 直到添加超出容量的第11个元素的时候,才会把容量增加到1.5倍,往后每次超出都扩容1.5倍。
(重点:无参构造是在添加元素的时候才会分配空间 )
而如果是有参构造的话 创建对象的时候就会 按照传入的 最小容量 创建指定大小的 数组 ,直到超出 该容量 才会 扩容至 1.5 倍。
(重点:有参构造是在创建对象的时候就会分配空间 )
二、Vector
Vector 的底层也是数组,扩容机制与ArrayList是一模一样的,只不过在CRUD方法中都加了
synchronized 关键字------>>>>加了锁。
也就是说 Vector与ArrayList的不同之处就是 Vector是线程安全的,而ArrayList不是线程安全的,正因为这个原因,Vector的效率 比 ArrayList的效率要差。
( get方法加了锁 其他的CRUD方法也是如此就不一一列举了)
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
三、LinkedList
LinkedList底层的数据结构是双向链表。
首先看一下LinkedList里面的一个私密内部类。
Node------>>>结点
此内部类有三个成员:
item next prev 这三个都是引用类型 (可以理解为指针)其中有 指向前后节点的引用(指针)所以:LinkedList的底层原理是双向链表。
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
add( )方法:
创建第一个Node对象 LinkdeList 的 first(头指针) last(尾指针都指向该节点)
item(指针)会指向它引用的对象 由于是第一个Node 其prev 和 next 都是null。
第二次 add( )后:
两个节点形成双向链表。但第一个结点的prev和最后一个结点的next都是null。
由于底层的结构是双向链表,所以增删操作效率相对数组比较高,其复杂度是O(n),但由于双向链表没有索引,所以查询,修改操作是需要遍历链表的,其复杂度为O(n)。
扩容机制:
无参构造之后,其first和last都是指向空,并不会开辟空间,而每次插入一个元素的时候才会在链表的最后一位添加一个结点。所以,可以理解为LinkedList没有扩容机制。
下面讲一下Set,Set是不能有多个重复元素的,且插入和查询都是不能保证顺序的。(但插入完成后每次查询的顺序都是相同的。)
Set的实现类:
一、HashSet:
HashSet的底层是一个HashMap,我们可以直接从构造器中看出来。
public HashSet() {
map = new HashMap<>();
}
Set是单列的,而Map是双列的,但Set的底层是Map,这要怎么解释呢?这里就需要看一下HashSet的两个重要的成员和add( )方法了。
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
可以看见add( )方法其实是往HashSet的一个成员 中放入一个 传来的参数做键(Key) 将一个Object类的PRESENT作为值(Value)做占位。
所以说想要知道HashSet的数据结构和扩容机制就要看HashMap的。接下来我们看看HashSet的add( )方法所调用的HashMap.put( )方法。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -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;
}
解析一下putVal( )方法 每次put的时候都会检查 一个名为table的Node[ ]数组是否为空或者长度为0,如果是,则会调用resize( ),这个方法非常重要解释了Set 乃至 Map的扩容机制。这个将会在后面讲到。
经过了table的扩容或者不扩容之后,传进来的K-V会被封装成一个HashMap$Node对象然后经行Hash运算找到它要插入的table的索引位置
如果该索引上已经有结点,则会经行equals( )比较,如果equals( )==true 则说明两个Node指向的对象相同,则不添加该Node, 如果equals( )==false,则会向后一位 比较hash值(hash值不等于hashCode)和 equals( )方法 ,一直这样循环下去,直到找到一个相同就放弃添加,或者找到最后都没找到相同的,则添加到最后。Node会形成一条链表,在特殊情况下会变成一颗树。
由此可以看出HashSet和HashMap底层都是 (数组+链表+树)
下面讲解一下扩容机制和resize( )方法
由于resize( )方法太长了,所以这里将会一个一个代码块来讲解。
resize( )与扩容机制
先看三个常量:
分别是默认初始化容量==16,最小的树化长度==64,临界因子==0.75
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MIN_TREEIFY_CAPACITY = 64;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
下面这段是关于初始化HashMap的代码块:定义了table的长度为16,还有临界值为12。
(当table的长度达到邻接值,且某一链表的长度大于8,则该链表会树化)
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
当达到table阈值的时候就会把table的长度增加到 原来的长度*loadFactor(增长因子,默认为2,可以有参构造的时候覆盖)
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
当链表长度 大于等于7 且 table的 长度大于64 则树化。(如下)
static final int TREEIFY_THRESHOLD = 8;
static final int MIN_TREEIFY_CAPACITY = 64;
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
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();
treeifyBin的后面的else代码块就是树化的过程,篇幅较长这里就不展示了。
Set的其他子类晚点再补充,
先写一下HashMap
Map的实现类:
一、HashMap:
先来看看HashMap的一些结构:
一对K-V===>>>作为一个Entry===>>>所有的entry组成===>>>entrySet
所有的key===>>>组成keySet
所有的value===>>>组成values
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
public Collection<V> values() {
Collection<V> vs = values;
if (vs == null) {
vs = new Values();
values = vs;
}
return vs;
}
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}