Java集合,也叫作容器,主要是由两大接口派生而来:—个是 collection 接口
主要用于 存放单一元素 ;另一个是 Map 接口,主要用于 存放键值对 。对于collection接口,下面又有三个主要的子接口:List、set和queue 。
Java集合框架如下图所示:
① java.util.Collection 是一个 集合接口 ,它提供了对集合对象进行基本操作的通用接口方法。List,Set,Queue接口都继承Collection,主要用于存放 单⼀元素 。
② java.util.Collections 是一个 集合工具类 。它包含有各种有关集合操作的静态方法(对集合的查找、排序、反转、线程安全化等),大多数方法都是用来处理线性表的。此类不能实例化,就像一个工具类,服务于Java的Collection框架。
① ArrayList是基于数组的实现,是 非线程安全的 ,效率高,所有的方法都没有synchronized修饰。
② Vector是 线程安全的 ,效率低,实现线程安全是直接通过 synchroized 修饰方法来完成的。
①Vector、ArrayList都是以类似 数组的形式 存储在内存中,LinkedList则以 链表的形式 进行存储。
②List中的元素有序、允许有重复的元素,Set中的元素无序、不允许有重复元素。
③Vector线程同步,线程安全,ArrayList、LinkedList线程不同步,线程不安全。
④LinkedList适合指定位置插入、删除操作,不适合查找;ArrayList、Vector适合查找,不适合指定位置的插入、删除操作。
⑤ArrayList在元素填满容器时会自动扩充容器大小的50%,而Vector则是100%,因此ArrayList更节省空间。
参见ArrayList扩容机制分析
ArrayList是基于数组的集合,数组的容量是在定义的时候确定的,如果数组满了,再插入,就会数组溢出。所以在插入时候,会先检查是否需要扩容,如果当前容量+1超过数组长度,就会进行扩容(先扩容再插入) 。
ArrayList的扩容是创建一个1.5倍的新数组,然后把原数组的值拷贝过去。
ArrayList的序列化不太一样,它使用 transient 修饰存储元素的 elementData 的数组, transient 关键字的作用是 让被修饰的成员属性不被序列化 。
为什么ArrayList不直接序列化元素数组呢?
出于效率的考虑,数组可能长度100,但实际只用了50,剩下的50不用其实不用序列化,这样可以提高序列化和反序列化的效率,还可以节省内存空间。
那ArrayList怎么序列化呢?
ArrayList通过两个方法readObject、writeObject自定义序列化和反序列化策略,实际直接使用两个流ObjectOutputStream和ObjectInputStream来进行序列化和反序列化。
快速失败(fail—fast): 快速失败是Java集合的 一种错误检测机制 。
在用迭代器遍历一个集合对象时,如果线程A遍历过程中,线程B对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
原理: 迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个modCount变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
注意: 这里异常的抛出条件是检测到modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
场景: java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改),比如ArrayList 类。
安全失败(fail—safe)
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
原理: 由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
缺点: 基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
场景: java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改,比如CopyOnWriteArrayList类。
保证ArrayList的线程安全可以通过这些方案:
CopyOnWriteArrayList就是线程安全版本的ArrayList。
它的名字叫 CopyOnWrite——写时复制,已经明示了它的原理。
CopyOnWriteArrayList采用了一种 读写分离的并发策略 。
CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则 首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器 。
所有类中的equals方法都是继承自Object类,Object类中原生的equals方法就是在通过 "" 进行判断。
public boolean equals (Object obj) {
return (this == obj);
}
但是每个类都可以对equals方法进行重写,覆盖掉之前使用进行判断的逻辑,改用新的逻辑进行判断是否相等。
**:** 判断的是栈内存中的值。
引用类型 的数据,栈内存中存储的是地址,所以此时 == 判断的是引用地址。
基本数据类型,栈内存中存储的是具体的数据,判断的是值。
将对象的内部信息(内存地址、属性值等),通过某种特定规则转换成一个散列值,就是该对象的hashCode。
两个不同对象的hashCode值可能相等。
hashCode值不相等的两个对象一定不是一个对象。
集合在判断两个对象是否相等的时候,会①先比较他们的hashCode值,如果hashCode不相等,则认为不是同一个对象,可以添加。
如果②hashCode值相等,还不能认为两个对象是相等的,需要通过equals方法进行进一步的判断,equals相等,则两个对象相等,否则两个对象不相等。
hashCode相等,未必相等;hashCode不相等,则一定不相等。
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;
tab[i = (n - 1) & hash])
treeifyBin(tab, hash);
HashMap的查找:
1. 使用扰动函数,获取新的哈希值。
2. 计算数组下标,获取节点。
3. 当前节点和key匹配,直接返回。
4. 否则,当前节点是否为树节点,查找红黑树。
5. 否则,遍历链表查找。
链表转红黑树,是牺牲空间换时间。阈值为什么要选8呢? 为了尽可能低的发生哈希碰撞,也为了避免资源浪费。
对 空间成本 和 时间成本 平衡的考虑。
假如我们设的比较大,元素比较多,空位比较少的时候才扩容,那么发生哈希冲突的概率就增加了,查找的时间成本就增加了。
我们设的比较小的话,元素比较少,空位比较多的时候就扩容了,发生哈希碰撞的概率就降低了,查找时间成本降低,但是就需要更多的空间去存储元素,空间成本就增加了。
1. 多线程下扩容死循环。
2. 多线程的put操作可能导致元素丢失。
3. put和get并发时,可能导致get为null。
在 hashMap1.7 中扩容的时候,因为采用的是 头插法 ,所以会可能会有循环链表产生,导致数据有问题,在 1.8 版本已修复,改为了尾插法。
在任意版本的 hashMap 中,如果在插入数据时多个线程命中了同一个位置,可能会有数据覆盖的情况发生,导致线程不安全。
Java中有HashTable、Collections.synchronizedMap、ConcurrentHashMap可以实现线程安全的Map。
参见HashMap 的 7 种遍历方式与性能分析!「修正篇」
红黑树本质上是一种二叉查找树,为了保持平衡,它又在二叉查找树的基础上增加了一些规则:
为什么不用二叉树?
红黑树是一种平衡的二叉树,插入、删除、查找的 最坏时间复杂度都为O(logn) ,避免了二叉树最坏情况下的O(n)时间复杂度。
为什么不用平衡二叉树?
平衡二叉树是比红黑树更严格的平衡树,为了保持保持平衡,需要旋转的次数更多,也就是说平衡二叉树保持平衡的效率更低,所以平衡二叉树插入和删除的效率比红黑树要低。
红黑树有两种方式保持平衡:旋转(左旋和右旋)和染色。
参见ConcurrentHashMap源码&底层数据结构分析
LinkedHashMap 继承自HashMap ,底层基于HashMap和双向链表来实现的。
HashMap无序,LinkedHashMap有序,可分为插入顺序和访问顺序两种。
如果是访问顺序,那put和get操作已经存在的Entry时都会把Entry移动到双向链表的标为(先删除再插入)。遍历时按照插入顺序排序,原因在于LinkedHashMap的内部类LinkedHashIterator,执行Iterator.next访问链表的下一个元素,所以可以按照插入顺序的输出。
LinkedHashMap存储数据,还是跟HashMap一样使用Entry的方式,双向链表只是为了保证顺序。
HashMap的遍历速度和他的容量有关,而LinkedHashMap只跟实际数量有关。
LinkedHashMap按照插入顺序排序,HashMap基于哈希表乱序。
注意:
1、ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCastException异常。
2、不要在foreach循环里进行元素的remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。