java集合
- 什么是Java的集合?使用集合有什么好处?
Java 的集合也称为容器,是用来存放数据的容器;不过注意,集合存放的只能是引用数据类型的数据,也就是一个个的对象(如果存入基本数据类型的数据,会自动装箱成包装类)。
集合的好处:
集合的长度是可变的。
集合可以存放不同类型的对象。
使用集合之后,可以像操作基本数据类型那样来操作对象。
集合为我们提供了多种数据结构和操作的API,选用合适的集合,能够提程序性能和开发效率。
- 常用的集合类以及它们的特点?
Java的集合类有两个父接口:Collection 接口和 Map 接口。
Collection接口主要的子接口:List接口、Set接口。
Map接口的主要实现类:HashMap、Hashtable、TreeMap 等。
List接口的主要实现类:ArrayList、Vector、LinkedList 等。
Set接口的主要实现类:HashSet、TreeSet、LinkedHashSet 等。
3.3 ArrayList 和 LinkedList 的区别/异同?
(其实大部分的区别就是数据结构的区别,一个是数组,一个是双向链表)。
ArrayList 底层使用的是数组实现,LinkedList 底层使用的是双向链表实现。
ArrayList 随机查找和遍历速度快,插入删除速度慢;LinkedList 随机查找和遍历速度快,插入和删除速度快。
ArrayList 插入和删除元素的速度会受插入位置的影响;LinkedList 插入和删除元素的速度不会受插入位置的影响。
ArrayList 内存空间会耗费在列表后面的预留空间;LinkedList 内存空间会耗费在每个数据要多存储一个前驱和后继。
ArrayList 需要扩容,扩容是 当前容量×1.5+1 ; LinkedList 无需扩容。
ArrayList 和 LinkedList 都不是同步的,都是不保证线程安全。
3.4 ArryList 是线程不安全的?为什么?
ArrayList 是线程不安全的,因为ArrayList里的方法没有加锁,也没有使用其他保证线程安全的措施;当多个线程来对 ArrayList 进行操作时,就会出现并发修改异常。
3.5 如何解决 ArrayList 线程不安全的问题?
解决 ArrayList 线程安全的办法有3种:
Vector 类替代。
Collections 工具类转换。
JUC 中的 CopyOnWriteArrayList。
- Map
4.1 HashMap的底层实现原理?
七上八下!!!!
jdk7及jdk7之前,底层是用 数组+链表 来实现的;
实现过程:
new HashMap() 之后,层并不会直接创建数组。而是等 put 数据时才会创建数组。
put 数据时( .put(key,value) ),如果是第一次向这个集合中put数据,会先创建一个长度为 16 的一维数组( Node[] table ),然后存储数据。存储数据时会先调用 key 所在类的 hashCode 方法,计算出此key的哈希值,再将此哈希值经过处理计算后,得到该数据在数组table上的位置。
然后根据此位置来分情况判断是否存储:
3.1 情况一:
此位置为空, 直接在此位置上存储put的数据。
3.2 情况二:
此位置不为空,则说明此位置上已有一个或多个数据了(多个数据以链表形式存储);
那么将 put数据的key的哈希值 与 此位置上已有数据的key的哈希值进行依次比较;
如果和它们都不同,则存储put的数据;
将put的数据放在此位置上,原有数据以链表形式存储:
3.3 情况三:
此位置不为空,且 put数据的key (假设为 key1) 的哈希值 与 此位置上已有的某个数据的key (假设为 key2 ) 的哈希值相同;
则调用 key1 所在类的 equals() 方法与 key2 比较;(此 equals() 方法是重写过的,比较的是值;)
若不同 (既返回false),则存储put的数据;
同样,将put的数据放在此位置上,原有数据以链表形式存储。
3.4 情况四:
若情况三中,key1,key2 equals()方法比较后的结果是相同 (既返回true),
则用 key1 的value1 替换 key2 的value2。
扩容,当存储的数据超出临界值,且要存放数据的位置非空时,则扩容,扩容为原来容量的2倍。
临界值 = 当前容量 x 填充因子
(填充因子是 0.75)
jdk8及jdk8之后:底层是用 数组+链表+红黑树 来实现的;
实现过程:
new HashMap() 之后,底层并不会直接创建数组。而是等 put 数据时才会创建数组。
put 数据时( .put(key,value) ),如果是第一次向这个集合中put数据,会先创建一个长度为 16 的一维数组( Node[] table ),然后存储数据。
存储数据的过程 和 jdk7及之前基本一样,既 先计算哈希值,然后分4种情况判断。
不同点在于 用链表存储数据时:
jdk7 是将新数据放在数组位置上,原有数据以链表形式存储在后面;
jdk8 是原有数据位置不变,而新数据以链表形式存储在最后。
容量扩充
如果使用的是默认初始容量,每次扩充,容量变为原来的 2 倍;
如果使用的是自己指定的初始容量,会先将这个容量扩充为 2 的幂次方大小。
jdk8还有一点不同的是,当数组的某一索引位置上的 以链表形式存储的数据 大于 8 个,
且当前数组长度大于64时,此索引位置上所有数据改为红黑树存储,这样可以减少搜索查找的时间。
4.2HashMap 容量的长度为什么总是2的幂次方?
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 ,不同位置上,不碰撞;
4.3 知道HashMap 扩容时候的死循环问题吗?
HashMap 1.7 插入数据时,使用的是头插法,并发下扩容时的Rehash,会出现死循环问题;
而 HashMap 1.8 插入数据时,改成了尾插法,解决了扩容时的死循环问题。
(如果还要具体一点的话,可以说一说rehash的流程,建议找篇文章看看,或者后面有时间我再写一篇。)
4.4 如何解决 HashMap 线程不安全的问题?
解决 HashMap 线程安全的办法同样也有3种:
Hashtable类替代。
Collections 工具类转换。
JUC 中的 ConcurrentHashMap 替代。
Hashtable类替代 和 Collections 工具类转换 这两种方法和在 ArrayList 里的用法一样,照着说就可以。
这里主要说一下 ConcurrentHashMap:
用ConcurrentHashMap 替代HashMap,可以解决线程安全的问题,但是其实 ConcurrentHashMap 也是有 jdk1.7 和 jdk1.8 的区别。
jdk1.7
采用Segment分段锁方式保证线程安全,将数据分成一段一段的存储,然后每一段数据单独一个锁;
所以当一个线程占用一个锁访问其中的一段数据时,其他段的数据可以被其他线程访问;
(jdk1.7 ConcurrentHashMap底层结构由 Segment数组和 HashEntry数组 组成。一个ConcurrentHashMap里包含一个 Segment数组,数组中的每个Segment都包含一个HashEntry数组,每个HashEntry是一个链表结构的元素。)
jdk1.8
取消了Segment分段锁方式,改成了用 CAS 和 synchronized 来保证线程安全。
jdk1.8 的ConcurrentHashMap中锁的锁更细粒度了, synchronized 只锁定当前链表或红黑树的首节点,这样只要hash不冲突,就不会有线程安全问题,效率大幅提升。
(jdk1.8 ConcurrentHashMap 底层结构由 数组+链表+红黑树 实现。)
网上看到的一个对比图,感觉很好的展示了 ConcurrentHashMap jdk1.7 和 jdk1.8 的特点和不同,能让我们更好的理解,
给大家分享一下:
4.5 ConcurrentHashMap能完全替代Hashtable吗?
不能。
首先说一下 ConcurrentHashMap 和 Hashtable的异同:
线程安全:
ConcurrentHashMap 和 Hashtable 都是线程安全的;
底层数据结构:
ConcurrentHashMap 的底层数据结构: jdk1.7 是用 分段数组+链表 实现,jdk1.8 是用 数组+链表+红黑树 实现,红黑树可以保证查找效率;
Hashtable 底层数据结构是用 数组+链表 实现。
保证线程安全的方式:
ConcurrentHashMap jdk1.7 是用分段锁的方式保证线程安全,jdk1.8 是用 synchronized 和 CAS 保证线程安全;
Hashtable 是用全表锁来保证线程安全(既一个Hashtable 只用一把锁),这种的方式的效率非常低。
这样一看,好像ConcurrentHashMap什么都比Hashtable好啊!为什么还是不能完全替代Hashtable ?
原因在于一致性:
虽然ConcurrentHashMap 的效率远高于 Hashtable,但因为 ConcurrentHashMap的迭起器是弱一致性的,而Hashtable的迭代器是强一致性的。所以ConcurrentHashMap是不能能完全替代Hashtable的。
弱一致性 简单来说就比如 我put了一个数据进去,本来应该立刻就可以get到,但是却可能在一段时间内 get不到,一致性比较弱。
而如果是 强一致性 的话,加入数据后,马上就能get到。
其实要变成强一致性,就要处处用锁,甚至是用全局锁,Hashtable就是全局锁,但是这样的效率会很低。
而ConcurrentHashMap 为了提升效率,一致性自然会变弱。
4.6 HashMap 是 TreeMap 如何选用?
在需要大量插入、删除和查找元素这种操作的,选择HashMap,因为HashMap 底层使用数据+链表+红黑树实现,对于插入、删除、查找的性能都不错,但是HashMap的结果是没有排序的。
在需要对集合排序的时候,选择 TreeMap ,TreeMap 基于红黑树实现,TreeMap 的映射根据键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
- Set
5.1 HashSet 和 HashMap 的区别?
HashMap是实现了Map接口,存储的是键值对;HashSet 是实现了Set接口,只存储对象。
HashMap 使用键来计算哈希值;HashSet 是使用成员对象来计算哈希值;
HashMap 比 HashSet 快。
HashSet 的底层其实是基于 HashMap 实现的,大部分方法都是直接调用 HashMap中的方法。
看下源码:
HashSet是基于HashMap实现的:
7. 什么是快速失败(fast-fail)机制?
快速失败是Java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast。
例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候,线程2 修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就可能会抛出 ConcurrentModificationException异常,从而产生fast-fail快速失败。
而迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedModCount值,是的话就返回遍历;否则抛出异常,终止遍历。