近期总结
近期还好吧,主要是了解List集合、Map集合、Set集合,对他们源码进行一个了解,对大牛的思想进行一个解析、对其代码进行一个学习,从而进一步提高自己的水平。
List
首先来说List吧。List是咱们最常用的一个容器,他的实现类有很多种,首先就是ArrayList
ArrayList
ArrayList是一个动态数组
,在咱们的印象中,数组是声明空间大小后无法进行更改的,但是为什么是动态数组呢?这与ArrayList的扩容操作有关。
在ArrayList的源码中,咱们可以看到ArrayList有一个方法是:grow()
也正是这个方法使其进行了一个扩容操作。
咱们查看他的源码:
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);
}
在这里我们可以看到首先将容量扩容到原来的1.5倍,如果其大小容量仍然不满足所需的的最小容量,那么直接讲所需的最小容量设置为当前的数组的容量,继而进一步的进行比较,如果超过了所限制的最大容量,那么设置其容量为int类型的最大容量。随后进行一个Arrays.copyOf()操作进行一个数组的扩容。
我们观看其源码的过程中会发现内部有很多Arrays.copyOf
和System.arrayCopy()
这两个方法,我们可以知道其本质上是一个数组拷贝的过程中,继续深究可以发现,System.arrayCopy()
是一个本地方法,而Arrays.copyOf
的底层还是System.arrayCopy()
方法。这两个方法都可以用于数组的复制,但是System.arrayCopy()
也可以用于数组的扩容、移动等操作。
LinkedList
LinkedList是一个双向链表,由于是一个双向链表,那么他无需担心扩容问题,但是在某些方面他的效率比较低,例如查找、修改,在这个过程中需要进行一个遍历,那么遍历的过程往往是其最消耗时间的地方部分,而修改也是需要一个便利的过程。
LinkedList与ArrayList的异同点
- ArrayList底层是一个Object类型的数组,而LinkedList底层是一个Object类型的双向链表
- ArrayList在理论上空间利用率是100%,但是实际上小于100%,因为它在创建的过程中往往需要声明更大的空间以防溢出,而LinkedList是一个数组,他除了记录数据,还需要进行记录他的关系(上一节点的地址、下一节点的地址等),所以二者空间的利用率都是小于100%的。
- ArrayList与LinkedList都是一个线程不安全的List集合,但是其速度比较快。
Set
Set集合是一个不能够不能重复的集合,也就是说在添加进去之后如果重复不会进行添加。
但是Set集合在根本上脱离不了Map集合,也就是说Set集合是Map集合的一种特殊形式。
那么为什么说Set集合是Map集合的一种特殊形式呢?
我们查看源码,查看其无参构造:
public HashSet() {
map = new HashMap<>();
}
再查看一个Set集合
public TreeSet() {
this(new TreeMap<E,Object>());
}
我们可以看到其本质上都是调用Map集合,所以我们可以理解为这是一个特殊的Map集合,那么Map是一个K-V的形式,而Set是一个K的形式,二者再插入的时候如何实现,实际上Set内部有一个Object对象PRESENT,这是一个静态的不可修改的对象,这个对象被用于作为一个公共的Value,也就是说Set集合的Key是你输入的数据,而Value是PRESENT这个对象。
那么为什么Set集合不能够重复呢?我们知道Set集合是Map集合的一种特殊形式,Set不能重复其实是因为Map的一个特征:Key不能重复 ,这个特征所导致的,那么Key为什么不能重复?如果重复又会怎么样?咱们还得去Map集合一探究竟。
Map集合
Map集合是咱们最常用的K-V的集合,那么Map集合的底层是什么呢?
HashMap集合的底层:
我们查看Map集合的底层,会发现其本质上是Entry这个对象,在这个对象中进行记录数组的Key Value Hash next,到后来可以看到其本质上是 数组+链表+红黑树。
数组的类型是一个Entry类型,其实是一个Node类型,这个Node类型是Entry的实现类。
那么为什么要使用红黑树?为什么要使用链表?
使用红黑树、链表的原因:
在底层我们可以看到其有一个函数hash() 被我们称之为扰动函数,如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这个函数使其高位与地位进行一个运算,使其进行一个扰动,能够散列开来,尽可能地避免这个哈希冲突,也就是说其排列方式是一个哈希排列的方法,这也是其无序的原因,那么位置是如何进行确定的呢?
这时候我们就需要借助路由函数:
(p = tab[i = (n - 1) & hash])
正式这个路由函数让他确定了自己所在的位置。
hashMap底层采用的是哈希排列的方法进行一个散列、排列,那么哈希排列必然会有冲突,这个冲突的解决方法有很多种例如:
- 开放定址法
所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入
-
再哈希法
再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,….,等哈希函数,计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间。
-
拉链法
发生冲突就将其放在这个位置的链表后面
-
二次侦测法:
再次进行侦测,一次进行1 ,-1 , 4,-4,9,-9。。。。。。这种形式
这里采用的是拉链法,那么为什么采用红黑树呢?
为什么使用红黑树
我们都知道链表查找的时间效率为O(n)而树查找的时间效率为O(log2n)很显然树的查找效率更高,那么为什么非得使用红黑树呢?
树的查找效率一般情况下确实为log2n,但是还有一些极端情况:所有的节点都位于某一侧,最后形成的结果和链表一样,这时候我们就需要使用到平衡二叉树了,平衡二叉树的左右两侧高度差的绝对值小于等于1,这就解决了二叉树的极端情况,那么为什么使用红黑树?因为红黑树的效率更高。
HashMap注意点:
-
HashMap的结构是一个数组+链表+红黑树
-
HashMap是延迟声明空间。
什么意思,也就是说,map集合在世new完之后数组还没有被创建,只有你添加第一个元素的时候才会进行空间的声明。
-
HashMap存在这阈值、负载因子,这两个东西是HashMap的灵魂之一,
为什么说是灵魂呢?因为这两个东西对Map十分重要这里进行一个细说:
阈值大家都理解是一个临界值,如果某个操作使其超过了阈值,那么就会引发一系列的反应,这里的阈值是如何计算的呢?
阈值的计算公式为: 当前的容量*负载因子,
那么负载因子怎么来:默认情况下是0.75f,这是人家进行过计算的,咱也不知道为啥是默认0.75f,一般不要进行更改。
如果达到阈值会引发扩容操作,扩容操作的代码:
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; 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) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order 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; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
-
HashMap的key不能重复的,如果重复就会进行一个修改操作:
我们查看HashMap的底层源码:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
在这里调用了putVal方法:
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; }
在这里我们可以看到其分为三个类型:
- 如果没有发生冲突,确定的位置没有元素,那么直接放入
- 如果当前位置的元素于自己的元素的key相同或者key的内容相同,那么将p付给e,也就是说将咱们这个元素的值赋给e,
- 如果当前位置的值存在元素,且不符合条件2,那么判断是否为一个TreeNode节点,如果是,那么说名此处是一个红黑树的结构,需要在红黑树中进行下一步操作
- 如果2、3条件都不满足,那么只能对链表进行一个遍历,如果找到相同的,那么就会p付给e,也就是说将咱们这个元素的值赋给e,如果没有找到,就直接放在链表尾部。
那么如果找到相同的话会怎么办?:
我们查看源码,:
if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; }
也就是说如果找到相同的,那么e就不为null,那么就会进行一个赋值操作,将传进来的元素的值赋给e,使得value被修改,并且返回原来的值。
如果不相同的话,那么返回一个null。
这个返回值十分的重要,为什么呢?因为这个返回值是HashSet集合添加成功的判断标志:
如果添加成功,那么HashMap的put返回值是一个null,而HashSet就对其进行判断如果是Null,那么就返回true,表示添加成功,否则添加失败。
代码如下:
public boolean add(E e) { return map.put(e, PRESENT)==null; }