集合主要分为两大系列Collection接口和Map接口,Collection 表示一组对象,Map表示一组映射关系或键值对。
Colletion的子接口有List、Set和Queue,List的实现类有ArrayList、LinkedList、Vector,
Set的实现类有HashSet和TreeSet,Map的实现类有HashMap、Hashtable和TreeMap,
LinkedHashMap是HashMap的子类,Properties是HashTable的子类。
Vector、HashTable、Properties是线程安全的;
ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的。
List接口特点:
- List集合所有的元素是以一种线性方式进行存储的,例如,存元素的顺序是11、22、33。那么集
合中,元素的存储就是按照11、22、33的顺序完成的) - 它是一个元素存取有序的集合。即元素的存入顺序和取出顺序有保证。
- 它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道
理)。 - 集合中可以有重复的元素,通过元素的equals方法,来比较是否为重复的元素。
List集合类中元素有序、且可重复。这就像银行门口客服,给每一个来办理业务的客户分配序号:第一
个来的是“张三”,客服给他分配的是0;第二个来的是“李四”,客服给他分配的1;以此类推,最后一个
序号应该是“总人数-1”。
注意:
List集合关心元素是否有序,而不关心是否重复,请大家记住这个原则。例如“张三”可以领取两个号。
Set接口是Collection的子接口,set接口没有提供额外的方法。但是比 Collection 接口更加严格了。
Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败。
Set集合支持的遍历方式和Collection集合一样:foreach和Iterator。
Set的常用实现类有:HashSet、TreeSet、LinkedHashSet。
Set集合类无序不可重复。
1、谈谈HashMap
HashMap基于哈希表的Map接口实现,是以key-value存储形式存在,即主要用来存放键值对。
HashMap 的实现不是同步的,它不是线程安全的。HashMap存取是无序的,键位置是唯一的,键和值
位置都可以是null,但是键位置只能是一个null。它是按照两倍的扩容的。
JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈
希冲突(两个对象调用的hashCode方法计算的哈希码值一致导致计算的数组索引值相同)而存在的(“拉
链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的
边界值,默认为 8)并且当前数组的长度大于64时,此时此索引位置上的所有数据改为使用红黑树存
储。
2、HashMap的几个重要参数静态常量
初始化容量16
默认的负载因子,默认值是0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
集合最大容量:2的30次幂
//集合最大容量的上限是:2的30次幂
static final int MAXIMUM_CAPACITY = 1 << 30;
阈值:8,当链表的值超过8则会转红黑树(1.8新增)
//当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
当链表的值小于6则会从红黑树转回链表
//当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
3、HashMap什么时候进行扩容呢?
当hashmap中元素个数超过16x0.75=12的时候会进行扩容。在不断的添加数据的过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。
当hashmap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16x0.75=12的时候,就把数组的大小扩展为2x16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们
已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 <1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。
4、阈值大于8,但是数组长度小于64,链表会变成红黑树吗?
此时并不会将链表变为红黑树。而是选择进行数组扩容。
这样做的目的是因为数组比较小,尽量避开红黑树结构,这种情况下变为红黑树结构,反而会降低效
率,因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡 。同时数组长度小于64时,搜索时间相对要快些。
所以综上所述为了提高性能和减少搜索时间,底层在阈值大于8并且数组长度大于64时,链表才转换为红黑树。具体可以参考treeifyBin 方法。
当然虽然增了红黑树作为底层数据结构,结构变得复杂了,但是阈值大于8并且数组长度大于64时,链表转换为红黑树时,效率也变的更高效
5、HashMap中hash函数是怎么实现的?还有哪些hash函数的实现方式?
对于key的hashCode做hash操作,无符号右移(>>>)16位然后做异或(^)运算。
还有平方取中法,伪随机数法和取余数法。这三种效率都比较低。而无符号右移16位异或运算效率是最高 的。至于底层是如何计算的我们下面看源码时给大家讲解。
6、当两个对象的hashCode相等时会怎么样?
会产生哈希碰撞,若key值内容相同则替换旧的value.不然连接到链表后面,链表长度超过阈值8就转换 为红黑树存储。
7、何时发生哈希碰撞和什么是哈希碰撞,如何解决哈希碰撞?
只要两个元素的key计算的哈希码值相同就会发生哈希碰撞。jdk8前使用链表解决哈希碰撞。jdk8之后 使用链表+红黑树解决哈希碰撞。
8、如果两个键的hashcode相同,如何存储键值对?
hashcode相同,通过equals比较内容是否相同。 相同:则新的value覆盖之前的value 不相同:则将新的键值对添加到哈希表中
9、jdk1.8为什么引入红黑树?这样结构的话不是更麻烦了吗?
哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。
JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分
布。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。 当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定
的影响,所以才需要转成树。
10、说一下put方法
版本一:
HashMap 的 put 的原理是将 key,value 封装成 node 节点,然后经过 key的 hashCode()方法算出
key 的 hash 值,再经过 hash 算法算出对应的数组下标,下标位置上如果没有任何元素,那么将Node
添加到这个位置上。如果下标位置已经存在链表,此时,将会拿着key和链表上的每个节点的key使用
equals方法进行比较。如果所有的equlas方法返回的都是false,那么这个新的节点将被添加到链表的
末尾,如果有一个equals返回true,那么这个节点的value将会被覆盖。
版本二:
put方法是比较复杂的,实现步骤大致如下:
1)先通过hash值计算出key映射到哪个桶;
2)如果桶上没有碰撞冲突,则直接插入;
3)如果出现碰撞冲突了,则需要处理冲突:
a:如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;
b:否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树;
4)如果桶中存在重复的键,则为该键替换新值value; 5)如果size大于阈值threshold,则进行扩容;
注:
高低位运算:
(h = key.hashCode()) ^ (h >>> 16) 对于key的hashCode做hash操作,无符号右移(>>>)16位然后做异
或(^)运算。
与运算可以说是取模运算:
hash&(length-1)
11、hashmap的容量为什么是以2的倍数方式实现的呢?
2的n次方(或2的倍数)的主要核心原因是hash函数的源码中右移了16位让低位保留高位信息,原本的低
位信息不
要,那么进行&操作另一个数低位必须全是1,否则没有意义,所以len容量必须是2的n次方,这样能实
现分布均
匀,有效减少hash碰撞!
HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这
个实现就在
把数据存到哪个链表中的算法;这个算法实际就是取模,hash%length,计算机中直接求余效率不如位
移运算,
源码中做了优化hash&(length-1),
hash%length==hash&(length-1)的前提是length是2的n次方;
为什么这样能均匀分布减少碰撞呢?2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1;
例如长度为9时候,3&(9-1)=0,2&(9-1)=0 ,都在0上,碰撞了;
例如长度为8时候,3&(8-1)=3,2&(8-1)=2 ,不同位置上,不碰撞;
其实就是按位“与”的时候,每一位都能 &1 ,也就是和1111……1111111进行与运算
12、为什么HashMap链表长度超过8会转成树结构?而不是其他的?
在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为 8 的时候,概率比千
万分之一还小,通常我们的 Map 里面是不会存储这么多的数据的,所以通常情况下,并不会发生从链
表向红黑树的转换。事实上,链表长度超过 8 就转为红黑树的设计,更多的是为了防止用户自己实现了
不好的哈希算法时导致链表过长,从而导致查询效率低,而此时转为红黑树更多的是一种保底策略,用
来保证极端情况下查询的效率。
通常如果 hash 算法正常的话,那么链表的长度也不会很长,那么红黑树也不会带来明显的查询时间上
的优势,反而会增加空间负担。所以通常情况下,并没有必要转为红黑树,所以就选择了概率非常小,
小于千万分之一概率,也就是长度为 8 的概率,把长度 8 作为转化的默认阈值。
13、为什么是6变回链表?
为6的时候退转为链表。中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表
个数超过8
则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元
素,链表个
数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
14、为什么初始容量是16?负载因子是0.75?
首先我们看hashMap的源码可知当新put一个数据时会进行计算位于table数组(也称为桶)中的下标:
int index =key.hashCode()&(length-1);
因为是将二进制进行按位与,(16-1) 是 1111,末位是1,这样也能保证计算后的index既可以是奇数也可
以是偶数,
并且只要传进来的key足够分散,均匀那么按位与的时候获得的index就会减少重复,这样也就减少了
hash的碰撞
以及hashMap的查询效率。
那么到了这里你也许会问? 那么既然16可以,是不是只要是2的整数次幂就可以呢?
答案是肯定的。那为什么不是8,4呢? 因为是8或者4的话很容易导致map扩容影响性能,如果分配的太
大的话又会
浪费资源,所以就使用16作为初始大小。
为什么加载因子设置为0.75,初始化临界值是12?
loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增
加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。
如果希望链表尽可能少些。要提前扩容,有的数组空间有可能一直没有存储数据。加载因子尽可能小一
些。
所以既兼顾数组利用率又考虑链表不要太多,经过大量测试0.75是最佳方案。
14、HashMap与LinkedHashMap的区别?
LinkedHashMap
保存插入顺序:LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,
先得到的记录肯定是先插入的。也可以在构造时带参数,按照应用次数排序。
速度慢:在遍历的时候会比HashMap慢,不过有种情况例外:当HashMap容量很大,实际数据较
少时,遍历起来可能会比LinkedHashMap慢。因为LinkedHashMap的遍历速度只和实际数据有
关,和容量无关,而HashMap的遍历速度和他的容量有关。
15、List转Map的几种方式
1、foreach:增强for循环,遍历list再添加元素到map
2、使用guava:
Map<Long, User> maps = Maps.uniqueIndex(userList, new Function<User, Long> () { @Override public Long apply(User user) { return user.getId(); } });
3、使用jdk1.8流表达式:
Map<Long, User> maps = userList.stream().collect(Collectors.toMap(User::getId,Function.identity()));
16、map转换为list
map.entrySet().stream().map(e -> new Person(e.getKey(),e.getValue())).collect(Collectors.toList());
17、如何使HashMap变成线程安全的?在多线程环境下使用hashmap吗?我的意思是怎么保证
hashmap的线程安全?
使用Collections.synchronizedMap()包装,原理就是对所有的修改操作都加上synchronized,保证了
线程的安全。
我认为主要可以通过以下三种方法来实现:
1.替换成Hashtable,Hashtable通过对整个表上锁实现线程安全,因此效率比较低
2.使用Collections类的synchronizedMap方法包装一下。方法如下:
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) 返回由指定映射支持的同步(线程安
全的)映射
3.使用ConcurrentHashMap,它使用分段锁来保证线程安全
18、ConcurrentHashMap的锁分段技术
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都
必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁住容器中的一部分数据,那么当多线程访
问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是
ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一
把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
在ConcurrentHashMap中不同于普通的HashMap,不仅有数组+链表的概念,还多了一个Segment数
组,一个Segment包含多个HashEntry数组(HashMap中的数组) ,这样就能通过分段加锁,解决多线程
下的安全性问题,又不至于像Hashtable那样全表加锁,导致性能下降。
JDK1.8中HashMap实现当链表长度大于等于7且数组长度大于64时,会自动将链表转换为红黑树存储,
这样的目的是尽量提高查询效率,而在JDK1.8中ConcurrentHashMap相对于JDK1.7的
ConcurrentHashMap也做了优化,JDK1.7中ConcurrentHashMap使用分段锁的形式,比较细粒度的
控制线程安全性问题,不至于像Hashteble那样,使用全表加锁,限制了并发性,但看过源码后发现,
每次在get或put的时候,都会先查询到链表的第一个元素,换个思路想想,如果能就在第一个记录上加
锁,这样不也可以解决线程安全性问题,所以JDK1.8就不存在Segmrnt[]了,而是使用
CAS+synchronized的形式解决线程安全性问题,这样就比JDK1.7更加细粒度的加锁,并发性能更好。
那多线程环境使用Map呢?
多线程环境可以使用Collections.synchronizedMap同步加锁的方式,还可以使用HashTable,但是同
步的方式显然性能不达标,而ConurrentHashMap更适合高并发场景使用。
Map<Long, User> maps = userList.stream().collect(Collectors.toMap(User::getId,Function.identity())); 1 map.entrySet().stream().map(e -> new Person(e.getKey(),e.getValue())).collect(Collectors.toList()); 1
ConcurrentHashmap在JDK1.7和1.8的版本改动比较大,1.7使用Segment+HashEntry分段锁的方式实
现,1.8则抛弃了Segment,改为使用CAS+synchronized+Node实现,同样也加入了红黑树,避免链表
过长导致性能的问题。
1.7分段锁
从结构上说,1.7版本的ConcurrentHashMap采用分段锁机制,里面包含一个Segment数组,
Segment继承与ReentrantLock,Segment则包含HashEntry的数组,HashEntry本身就是一个链表的
结构,具有保存key、value的能力能指向下一个节点的指针。
实际上就是相当于每个Segment都是一个HashMap,默认的Segment长度是16,也就是支持16个线程
的并发写,Segment之间相互不会受到影响。
put流程
其实发现整个流程和HashMap非常类似,只不过是先定位到具体的Segment,然后通过
ReentrantLock去操作而已,后面的流程我就简化了,因为和HashMap基本上是一样的。
- 计算hash,定位到segment,segment如果是空就先初始化
- 使用ReentrantLock加锁,如果获取锁失败则尝试自旋,自旋超过次数就阻塞获取,保证一定获取
锁成功 - 遍历HashEntry,就是和HashMap一样,数组中key和hash一样就直接替换,不存在就再插入链
表,链表同样
get流程
get也很简单,key通过hash定位到segment,再遍历链表定位到具体的元素上,需要注意的是value是
volatile的,所以get是不需要加锁的。
1.8CAS+synchronized
1.8抛弃分段锁,转为用CAS+synchronized来实现,同样HashEntry改为Node,也加入了红黑树的实
现。主要还是看put的流程。
put流程 - 首先计算hash,遍历node数组,如果node是空的话,就通过CAS+自旋的方式初始化
- 如果当前数组位置是空则直接通过CAS自旋写入数据
- 如果hash==MOVED,说明需要扩容,执行扩容
- 如果都不满足,就使用synchronized写入数据,写入数据同样判断链表、红黑树,链表写入和
HashMap的方式一样,key hash一样就覆盖,反之就尾插法,链表长度超过8就转换成红黑树
get查询
get很简单,通过key计算hash,如果key hash相同就返回,如果是红黑树按照红黑树获取,都不是就
遍历链表获取
19、put过程讲完,那你重写过equals和hashcode方法吗?
20、hashcode到底是什么东西吗?比如说,new了一个对象他的hashcode值到底是一个什么值?
hash是一个函数,该函数中的实现就是一种算法,就是通过一系列的算法来得到一个hash值,这个时
候,我们就需要知道另一个东西,hash表,通过hash算法得到的hash值就在这张hash表中,也就是
说,hash表就是所有的hash值组成的,有很多种hash函数,也就代表着有很多种算法得到hash值。
hashcode就是通过hash函数得来的,通俗的说,就是通过某一种算法得到的,hashcode就是在hash
表中有对应的位置。
首先一个对象肯定有物理地址,在别的博文中会hashcode说成是代表对象的地址,这里肯定会让读者
形成误区,对象的物理地址跟这个hashcode地址不一样,hashcode代表对象的地址说的是对象在hash
表中的位置,物理地址说的对象存放在内存中的地址,那么对象如何得到hashcode呢?通过对象的内
部地址(也就是物理地址)转换成一个整数,然后该整数通过hash函数的算法就得到了hashcode,所
以,hashcode是什么呢?就是在hash表中对应的位置。