java集合笔记
1、基础
1、集合架构图
2、一种是集合(Collection),存储一个元素集合,另一种Map,存储键/值对映射。Collection 接口又有 3 种子类型,List、Set 和 Queue。再下面是一些抽象类。最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。
3、集合与数组的区别,主要是定长的问题。另外数组可以为基本数据类型。有些集合如ArrayList的本质也是数组。
4、具体实现
Collection 接口的接口 对象的集合(单列集合)
├——-List 接口:元素按进入先后有序保存,可重复
│—————-├ LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全
│—————-├ ArrayList 接口实现类, 数组, 随机访问, 没有同步, 线程不安全
│—————-└ Vector 接口实现类 数组, 同步, 线程安全
│ ———————-└ Stack 是Vector类的实现类
└——-Set 接口: 仅接收一次,不可重复,并做内部排序
├—————-└HashSet 使用hash表(数组)存储元素
│————————└ LinkedHashSet 链表维护元素的插入次序
└ —————-TreeSet 底层实现为TreeMap,元素排好序
Map 接口 键值对的集合 (双列集合)
├———Hashtable 接口实现类, 同步, 线程安全
├———HashMap 接口实现类 ,没有同步, 线程不安全
│—————–├ LinkedHashMap 双向链表和哈希表实现,插入和获取有序
│—————–└ WeakHashMap 弱引用的键值对
├ ——–TreeMap 红黑树对所有的key进行排序
└———IdentifyHashMap ==完全一致才算同一个
2、List
List集合,常用的具体实现类有,ArrayList、 LinkedList、Vector。
1、ArrayList动态数组
底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素
查询快,增删慢。无参new的时候是一个空数组,第一次添加才设置默认大小为10。当后期数组需要扩容时,会产生一个新的数组,该数组大小为原来数组大小的1.5倍,然后再把原数组元素复制到新数组中。
由于里面维护了一个数组,且可以自动扩容和手动缩容,也就是复制到一个新的数组上。
查询快是因为直接根据索引就可以找到,更新该索引的数据也很快。public E set(int index, E element)
删除时因为要整体复制到新的一个数组System.arraycopy(elementData, index+1, elementData, index,numMoved);,新增时因为可能要随时扩容,所以相当而言会慢。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final int DEFAULT_CAPACITY = 10;
transient Object[] elementData; // non-private to simplify nested class access
private int size; // 实际元素个数
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);
}
}
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
}
}
删除
可以使用for从尾开始,或者使用迭代器。不能使用增强for循环。
迭代器 iterator有使用过集合的都知道,在用增强for循环遍历集合的时候是不可以对集合进行 remove操作的,因为 remove 操作会改变集合的大小。从而容易造成结果不准确甚至数组下标越界,更严重者还会抛出 ConcurrentModificationException。迭代器是删除之后,会赋值到新的数组,且游标会后移一位,这也就不会越界了。
总结
ArrayList 底层基于数组实现容量大小动态可变。 扩容机制为首先扩容为原始容量的 1.5 倍。 扩容之后是通过数组的拷贝来确保元素的准确性的,所以尽可能减少扩容操作。
ArrayList 的最大存储能力:Integer.MAX_VALUE。
size 为集合中存储的元素的个数。
elementData.length 为数组长度,表示最多可以存储多少个元素。
如果需要边遍历边 remove ,必须使用 iterator。且 remove 之前必须先 next,next 之后只能用一次 remove。
2、Vector 类实现了一个动态数组。和 ArrayList 很相似,只不过它的操作加了锁,是线程安全的。Stack栈,先进后出,子弹夹。另外栈和队列是一种结构,数组和链表是实现方式。
pop( )移除堆栈顶部的对象,并作为此函数的值返回该对象。
push(Object element)把项压入堆栈顶部。
peek( )查看堆栈顶部的对象,但不从堆栈中移除它。
3、LinkedList,是一种双向链表(Linked list),在每一个节点里存到下一个节点的地址和上一个节点的地址。
与 ArrayList 相比,LinkedList 的增加和删除对操作效率更高,而查找和修改的操作效率较低。
主要适用于需要频繁的在列表开头、中间、末尾等位置进行添加和删除元素操作。
由于里面是维护一个第一个node,和最后一个node,所以查找/赋值set的时候,是索引【折半】之后一个一个往后查找,效率较慢。
新增或者删除的时候,只需要前后node与其断开地址连接,且前后相互连接即可。比数组复制为新的要快。
所以,LinkedList插入效率高是相对的,因为它省去了ArrayList插入数据可能的数组扩容和数据元素移动时所造成的开销,但数据扩容和数据元素移动却并不是时时刻刻都在发生的。
链表可分为单向链表和双向链表。
一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接。
一个双向链表有三个整数值: 数值、向后的节点链接、向前的节点链接。
public class LinkedList<E>extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
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;
}
}
增强for循环实现原理也是使用了迭代器。单对原有对象变化之后,迭代器是不知道的,当游标往后移找不到对象就会报错。
3、Map
Map 接口中键和值一一映射. 可以通过键key来获取值value。
1、HashMap是一个数组加链表结构。数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的
哈希冲突的解决方案有多种:
开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,
链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。
public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
static int indexFor(int h, int length) {
return h & (length-1);
}
public V put(K key, V value) {
//如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(=16)
if (table == EMPTY_TABLE) {
inflateTable(threshold);//分配数组空间
}
//如果key为null,存储位置为table[0]或table[0]的冲突链上
if (key == null)
return putForNullKey(value);
int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
int i = indexFor(hash, table.length);//获取在table中的实际位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);//调用value的回调函数,其实这个函数也为空实现
return oldValue;
}
}
modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
addEntry(hash, key, value, i);//新增一个entry
return null;
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//扩容的关键位置,头插法,e和next,jdk1.7
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//通过key值的hash值和新数组的大小算出在当前数组中的存放位置
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
基本问题
jdk1.7和1,8的区别,主要是红黑树和扩容
1、1.7数组加链表,1.8是数组加链表+红黑树,大于等于8且数组长度大于等于64之后就变成了红黑树,一般小于等于6只会退位链表。但并不是简单地将链表转换为红黑树,而是先判断table的长度是否大于等于64,如果小于64,就通过扩容的方式来解决,避免红黑树结构化。
2、1.7扩容的情况,是第一次put;大于了阈值(但不一定,可能刚好那里不hash冲突,即为null),1.8则是第一次put;大于阈值;或者某个链表数量大于8时但table的长度不超过64。
3、扩容时之后新的数组位置计算方式不同。
4、1.7是头插法,1.8是尾插法,可以避免成环,且适配红黑树,不知道此时插入的是链表还是红黑树。
5、在扩容的时候当前数据:1.7在插入数据之前扩容,若插入地为null,可以避免本次就进行扩容,而1.8插入数据成功之后扩容。
jdk1.8的hashmap真的是大于等于8就转换成红黑树,小于等于6就变成链表吗
不一定,
大于等于8且数组长度大于等于64才会变为红黑树,若大于等于8但数组没有到达64则先扩容。
一般是小于等于6缩容,也有另一种方式。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
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();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
为什么是8和6?
首先出结论:和hashcode碰撞次数的泊松分布有关,主要是为了寻找一种时间和空间的平衡。
红黑树中的TreeNode是链表中的Node所占空间的2倍,虽然红黑树的查找效率为o(logN),要优于链表的o(N),但是当链表长度比较小的时候,即使全部遍历,时间复杂度也不会太高。固,要寻找一种时间和空间的平衡,即在链表长度达到一个阈值之后再转换为红黑树。
之所以是8,容器中节点分布在hash桶中的频率遵循泊松分布,桶的长度超过8的概率非常非常小,hash碰撞发生8次的概率已经降低到了0.00000006,几乎为不可能事件,如果真的碰撞发生了8次,那么这个时候说明由于元素本身和hash函数的原因,此次操作的hash碰撞的可能性非常大了,后序可能还会继续发生hash碰撞。所以,这个时候,就应该将链表转换为红黑树了,也就是为什么链表转红黑树的阈值是8。
最后,红黑树转链表的阈值为6,主要是因为,如果也将该阈值设置于8,那么当hash碰撞在8时,会反生链表和红黑树的不停相互激荡转换,白白浪费资源
扩容是保持2的次幂的原因
是计算新的数组位置的时候,使用与&运算进行取模提高效率,即hash值与长度-1。且为了减少碰撞次数,减1之后的二进制全部为1,这样忽略了hash容量以上的高位,可以极大的减少hash碰撞,增加效率的,其他值能与的时候,可能得出相同的值
环链的形成原因,
多线程,resize扩容。可见成环了之后,要是调用get一下,就会一直循环导致cpu达到100%。
https://www.cnblogs.com/chanshuyi/p/java_collection_hashmap_17_infinite_loop.html。
大于阈值就一定会扩容吗?
不一定,若是刚好该值没有hash冲突,即数组的此处没有数据为null。
扩容因子是0.75
回来总结了一下; 提高空间利用率和 减少查询成本的折中,主要是泊松分布,0.75的话碰撞最小,作为一般规则,默认负载因子(0.75)在时间和空间成本上提供了很好的折衷。较高的值会降低空间开销,但提高查找成本(体现在大多数的HashMap类的操作,包括get和put)。较低的话,提高了查找效率,但是加大了扩容的几率,增加了空间的开销。
2、LinkedHashMap
它继承hashmap,同时也有链表的结构,用于存入的数据有序。
代码中,有一个header的Entry,用于链表的头节点。而的Entry,除了常见的4个之外,还有一个before和after的Entry,这样所有的entry就可以链表的形式连接起来了。
/**
* 双向链表的头节点或者下面的这种head和tail
*/
private transient Entry<K,V> header;
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
/**
* true表示最近最少使用次序,false表示插入顺序
*(1)false,所有的Entry按照插入的顺序排列
*(2)true,所有的Entry按照访问的顺序排列
*/
private final boolean accessOrder;
private static class Entry<K,V> extends HashMap.Entry<K,V> {
// These fields comprise the doubly linked list used for iteration.
Entry<K,V> before, after;
Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
super(hash, key, value, next);
}
...
}
3、weakHashMap
弱引用hashmap,弱key,适合做缓存场景。
当除了自身有对key的引用外,此key没有其他引用,GC会回收这个key,一旦这个弱引用指向的对象的key成为垃圾,就该键值对会加入一个队列ReferenceQueue(queue)中,那么WeakHashMap会在下次对WeakHashMap进行增删改查操作时及时丢弃该键值对。
1)强引用,任何时候都不会被垃圾回收器回收,如果内存不足,宁愿抛出OutOfMemoryError。
2)软引用,只有在内存将满的时候才会被垃圾回收器回收,如果还有可用内存,垃圾回收器不会回收。
3)弱引用,只要垃圾回收器运行,就肯定会被回收,不管还有没有可用内存。
4)虚引用,虚引用等于没有引用,做一些特殊处理。
过程:若key没有被其他对象引用,则被GC回收,且将该元素放入一个队列中,当存在weakHashMap的时候会清理这个队列里面的元素,即删除table中被GC回收的键值对。
4、Hashtable
安全的hash表。它和HashMap类很相似,但是它支持同步。像HashMap一样,Hashtable在哈希表中存储键/值对。
安全的原因,是很多方法都加了synchronized
public synchronized V put(K key, V value)
public synchronized V remove(Object key)
Hashtable与hashmap的不同点:
1、Hashtable安全,hashmap后者不安全。
2、Hashtable中,key和value都不允许出现null值。hashmap可有一个key为null
3、遍历方式,hash计算,扩容方式不同等
5、TreeMap
TreeMap输出结果是有序的map。它是一个有序的key-value集合,是通过红黑树来实现的。映射根据其键的自然排序进行排序。
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
}
TreeMap<Integer, String> pairs = new TreeMap<>();
pairs.put(2, "B");
pairs.put(1, "A");
pairs.put(3, "C");
//Key: 1, Value: A
//Key: 2, Value: B
//Key: 3, Value: C
而linkedHashMap的有序是加入到map中的顺序不变,新加的放最后面,属于放入有序。
但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。如果需要输出的顺序和输入的相同,那么用LinkedHashMap 可以实现,它还可以按读取顺序来排列.
6、IdentityHashMap
IdentityHashMap使用的是==比较key的值。同一个对象才算相同。而hashmap是hashCode相等且equals为true,一般而言对象基本上都重写了这个2个方法。所以导致equals一般是去比较字面值相同就好。
Map identityMap = new IdentityHashMap();
identityMap.put("a", 1);
identityMap.put(new String("a"), 2);
identityMap.put("a", 3);
System.out.println("Identity Map KeySet Size :: " + identityMap.keySet().size());
//输出结果为Identity Map KeySet Size :: 2
private static int hash(Object x, int length) {
int h = System.identityHashCode(x);
// Multiply by -127, and left-shift to use least bit as part of hash
return ((h << 1) - (h << 8)) & (length - 1);
}
//比较对象的时候,是直接使用==;hashMap则是比较key的hashCode和equals方法
若使用hashmap,则size为1,因为底层比较的是string的equals。
两者最主要的区别是IdentityHashMap使用的是==比较key的值,而HashMap使用的是equals()
HashMap使用的是hashCode()查找位置,IdentityHashMap使用的是System.identityHashCode(object)
System类提供一个identifyHashCode(Object o)的方法,该方法返回指定对象的精确hashCode值,也是根据该对象的地址计算得到的HashCode值。
即原生Object方法的hashCode,而不是子类重写过后的hashCode方法。
4、Set
就是不重复,底层使用的是map
1、HashSet
HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合。
HashSet 允许有 null 值。
HashSet 是无序的,即不会记录插入的顺序。
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;
}
2、LinkedHashSet
LinkedHashSet底层是用LinkedHashMap实现的,可以按照插入顺序进行顺序排列。
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
3、TreeSet
TreeSet底层是用TreeMap,使用红黑树。
public TreeSet() {
this(new TreeMap<E,Object>());
}
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
}
【完,喜欢就点个赞呗】
正在去BAT的路上修行