Map接口和Collection接口是所有集合框架的父接口:
容器
主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。
Collection集合主要有List和Set两大接口
TreeMap:基于红黑树实现。
HashMap:数组+链表组成的,数组是HashMap 的主体,链表则是主要为了解决哈希冲突。DK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
HashTable:和HashMap 类似,但它是线程安全的它是遗留类,不应该去使用它,而是使用 ConcurrentHashMap ,ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增删改),
则会抛出Concurrent Modification Exception;
机制原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。这个机制是防止多线程并发访问list可能带来错误,所以抛出异常提醒一下。
Fail-Fast : List维护一个modCount变量,add、remove、clear等涉及了改变list元素的个数的方法都会
导致modCount的改变。
如果判断出modCount != expectedModCount, 抛出ConcurrentModificationException 异常,
从而产生fail-fast机制。
Iterator 接口提供遍历任何 Collection 的接口。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。
List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()){
String obj = it. next();
System. out. println(obj);
}
边遍历边修改 Collection 的正确方式是使用 Iterator.remove() 方法和倒叙边遍历边删除
Iterator<Integer> it = list.iterator();
while(it.hasNext()){
*// do something*
it.remove();
}
public static void remove(ArrayList<String> list){
for(int i=list.size()-1;i>=0;i--){
String s=list.get(i);
if(s.equals("bb")){
list.remove(s);
}
}
}
当使用 foreach(for(Integer i : list)) 语句时,会报 ConcurrentModificationException 异常。它会自动生成一个iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。
for循环删除ArrayList重复元素会导致漏删情况。
for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。
迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。
foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。
Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。
序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。
而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的
writeObject() 来实现序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法.
ArrayList list = new ArrayList();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(list);
CopyOnWriteArrayList
HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。
HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。
所以equals方法被覆盖过,则hashCode方法也必须被覆盖
hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
底层是数组+链表+红黑树。内部是table数组,每个数组存放的是链表,链表的每一个节点都是
允许使用null值和null键,最多只允许一条记录的键为 null,允许多条记录的值为 null。此类不保证映射的顺序。
默认加载因子是 0.75, 默认的初始容量是16,容量始终保持 2^n,如果size大于容量*加载因子,就进行扩容,容量扩容成两倍;
JDK1.8之后,当链表长度超过阈值,默认为8时,为加快检索速度,将链表转化成红黑树,提高查询效率,从原来的O(n)到O(logn)
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
//hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
JDK1.7的 HashMap 的 hash 方法源码
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:
①.判断键值对数组table[i]是否为空或为null,为空则执行resize()进行扩容;
②.根据键值key计算hash值得到插入的索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648到2147483647(-(2 ^ 31)~(2 ^ 31 - 1)),前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。
首先可能会想到采用%取余的操作来实现。而“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)
在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是二进制方式比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了 哈希值与数组大小范围不匹配的问题;
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
总结:HashMap 没有考虑同步,HashTable 考虑了同步的问题。但是 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的, 结合了 HashMap 和 HashTable 二者的优势。
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
① 判断存储的key、value是否为空,若为空,则抛出异常,否则,进入步骤②
② 计算key的hash值,随后进入无限循环,该无限循环可以确保成功插入数据,若table表为空或者长度为0,则初始化table表,否则,进入步骤③
③ 根据key的hash值取出table表中的结点元素,若取出的结点为空(该桶为空),则使用CAS将key、value、hash值生成的结点放入桶中。否则,进入步骤④
④ 若该结点的的hash值为MOVED,则对该桶中的结点进行转移,否则,进入步骤⑤
⑤ 对桶中的第一个结点(即table表中的结点)进行加锁,对该桶进行遍历,桶中的结点的hash值与key值与给定的hash值和key值相等,则根据标识选择是否进行更新操作(用给定的value值替换该结点的value值),若遍历完桶仍没有找到hash值与key值和指定的hash值与key值相等的结点,则直接新生一个结点并赋值为之前最后一个结点的下一个结点。进入步骤⑥
⑥ 若binCount值达到红黑树转化的阈值,则将桶中的结构转化为红黑树存储,最后,增加binCount的值。
在Collections工具类中有一个排序方法sort,前者可以只传一个集合,后者重载是传入集合和比较器,前者默认使用的就是Comparable中的compareTo方法,后者使用的便是我们传入的外部比较器Comparator,java的很多类已经实现了Comparable接口,比如说String,Integer等类,而我们其实也是基于这些实现了Comparator或者Comparable接口的原生类来比较我们自己的类,比如说自定义一个类User,属性有name和age,俩个user之间的比较无非就是比较name和age的大小。在设计初时有需求就选择Comparable,若后期需要扩展或增加排序需求是,再增加一个比较器Comparator。
总结一下,两种比较器Comparable和Comparator,后者相比前者有如下优点:
1、如果实现类没有实现Comparable接口,又想对两个类进行比较(或者实现类实现了Comparable接口,但是对compareTo方法内的比较算法不满意),那么可以实现Comparator接口,自定义一个比较器,写比较算法
2、实现Comparable接口的方式比实现Comparator接口的耦合性要强一些,如果要修改比较算法,要修改Comparable接口的实现类,而实现Comparator的类是在外部进行比较的,不需要对实现类有任何修改。
红黑树的特性
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。(确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。)
它的时间复杂度是O(lgn),一棵含有n个节点的红黑树的高度至多为2log(n+1).