Collection
Collection 和 Collections 有什么区别?如何实现数组和 List 之间的转换?比较ArrayList、linkedList、Vector三者的异同Arraylist 与 linkedListArrayslist的扩容机制Vector和ArrayList的最大区别?区分List中remove(int index)和remove(Object obj)HashSet添加元素和检查重复的过程HashMap 和 HashSet区别linkedHashSetTreeSet Map
HashMap的底层实现HashMap扩容(put)Hash Map的长度为什么是2的幂次方HashMap 为什么是非线程安全的HashMap 和 HashtableConcurrentHashMap 和 Hashtable
Collection Collection 和 Collections 有什么区别?Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子接口,比如 List、Set 等。Collections 是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序方法: Collections. sort(list)。 如何实现数组和 List 之间的转换?
- 数组转 List:使用 Arrays. asList(array) 进行转换。List 转数组:使用 List 自带的 toArray() 方法。
同: 三个类都是实现了List接口,存储数据的特点相同:存储有序的、可重复的数据
Collection接口:单列集合,用来存储一个一个的对象List接口:存储有序的、可重复的数据。 -->“动态”数组,替换原有的数组ArrayList:作为List接口的主要实现类;线程不安全的,效率高;底层使用Object[] elementData存储linkedList:对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储Vector:作为List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储 Arraylist 与 linkedList
- 是否保证线程安全: ArrayList 和 linkedList 都是不同步的,也就是不保证线程安全;底层数据结构: Arraylist 底层使⽤的是 Object 数组; linkedList 底层使⽤的是 双向链表插⼊和删除是否受元素位置的影响: ① ArrayList 采⽤数组存储,所以插⼊和删除元素的时间复杂度受元素位置的影响。 ⽐如:执⾏ add(E e) ⽅法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i插⼊和删除元素的话( add(int index, E element) )时间复杂度就为 O(n-i)。因为在进⾏上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执⾏向后位/向前移⼀位的操作。 ②linkedList 采⽤链表存储,所以对于 add(E e) ⽅法的插⼊,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置 i 插⼊和删除元素的话( (add(int index, Eelement) ) 时间复杂度近似为 o(n)) 因为需要先移动到指定位置再插⼊。是否⽀持快速随机访问: linkedList 不⽀持⾼效的随机元素访问,⽽ ArrayList ⽀持。快速随机访问就是通过元素的序号快速获取元素对象(对应于 get(int index) ⽅法)。内存空间占⽤: ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留⼀定的容量空间,⽽ linkedList 的空间花费则体现在它的每⼀个元素都需要消耗⽐ ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
ListVector和ArrayList的最大区别?integers = new ArrayList<>(); //此时创建的数组的长度为0,也就是一个空数组 integers.add(1); //当向integers数组中加入第一个元素时,此时ArrayList的扩容就已经开始了。 //默认容量为10 //1.判断装下当前值的下一个输入时需要的容量 和默认容量进行比较 较大的值赋给minCapacity //2.newCapacity = oldCapacity + (oldCapacity >> 1); 旧容量扩容一点五倍 //3.实际扩容大小从minCapacity和newCapacity中更大的选取
Vector和ArrayList几乎是完全相同的,
- 唯一的区别在于Vector是同步类(synchronized),属于强同步类,是线程安全的。
- 因此开销就比ArrayList要大,访问要慢。正常情况下,
- 大多数的Java程序员使用ArrayList而不是Vector,
- 因为同步完全可以由程序员自己来控制。Vector每次扩容请求其大小的2倍空间,
- 而ArrayList是1.5倍。Vector还有一个子类Stack。是非线程安全的。
list.remove(2); //去除下标为2的元素list.remove(new Integer(2)); //去除数值为2的Integer对象 HashSet添加元素和检查重复的过程
HashSet 是基于 HashMap 实现的,HashSet 底层使用 HashMap 来保存所有元素,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置),判断数组此位置上是否已经有元素:
如果此位置上没有其他元素,则元素a添加成功。如果此位置上有其他元素b,则比较元素a与元素b的hash值:
如果hash值不相同,则元素a添加成功。如果hash值相同,进而需要调用元素a所在类的equals()方法:
equals()返回true,元素a添加失败equals()返回false,则元素a添加成功。
对于添加成功的后两种情况而言:元素a 与已经存在指定索引位置上数据以链表的方式存储。
HashMap 和 HashSet区别| HashMap | HashSet |
|---|---|
| 实现了 Map 接口 | 实现 Set 接⼝ |
| 存储键值对 | 仅存储对象 |
| 调⽤ put() | 调⽤ add() |
| 使⽤键(Key)计算hashcode | 使⽤成员对象来计算 hashcode 值,equals() ⽅法⽤来判断对象的相等性 |
linkedHashSet是HashSet的子类linkedHashSet根据元素的hashCode值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,每个数据还维护了两个引用,记录此数据的前后数据,这使得元素看起来是以插入顺序保存的。linkedHashSet插入性能略低于HashSet,但在迭代访问Set 里的全部元素时有很好的性能。linkedHashSet不允许集合元素重复。 TreeSet
TreeSet是SortedSet接口的实现类,TreeSet可以确保集合元素处于排序状态。TreeSet底层使用红黑树(自平衡的排序二叉树)结构存储数据TreeSet是有序,唯一的,查询速度比list快 Map
- Map : 使⽤键值对存储,Key 是⽆序的、不可重复的,value 是⽆序的、可重复 - hashmap - linkedHashMap - TreeMap - ConcurrentHashMap - Hashtable
HashMap : JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了很⼤的变化,当链表⻓度⼤于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间)(数组查找o(1) 链表过长时效率为o(n))linkedHashMap : linkedHashMap 继承⾃ HashMap ,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红⿊树组成。另外, linkedHashMap 在上⾯结构的基础上,增加了⼀条双向链表,使得上⾯的结构可以保持键值对的插⼊顺序。同时通过对链表进⾏相应的操作,实现了访问顺序相关逻辑。Hashtable : 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的TreeMap : 红⿊树(⾃平衡的排序⼆叉树)-进行有序的遍历TreeMap 是更好的选择。 HashMap的底层实现
JDK1.8 之前 HashMap 底层是数组和链表 结合在⼀起使⽤也就是-链表散列。
- HashMap 通过key. hashCode() 经过扰动函数(hash方法-减少碰撞)处理过后得到 hash 值,然后通过hash值判断当前元素存放的位置;如果当前位置存在元素的话,就判断该元素与要存⼊的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。(将链表和数组相结合。也就是说创建⼀个链表数组,数组中每⼀格就是⼀个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。)
相⽐于之前的版本, JDK1.8 之后在解决哈希冲突时有了较⼤的变化,当链表⻓度⼤于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。 HashMap扩容(put)
loadFactor 负载因子
loadFactor是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,(扩容一倍)而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。扩充HashMap的时候,不需要像JDK1.8前的实现那样重新计算hash,(链表中元素的低位都是相同的-因为hash%(size-1)都一样,比如根据高低位拆分成高位链和低位链 低位链表都是一样的),是0的话索引没变,是1的话索引变成“原索引+oldSize”如果是null,不处理,如果是红黑树,则用到对应的链表(没有消失,只是查找不用它),之后再2新的位置上进行红黑树的重构。
Hash Map的长度为什么是2的幂次方
hash()散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数是对应的数组下标。我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。 HashMap 为什么是非线程安全的
我们知道 HashMap 底层是一个 Entry 数组,当发生 hash 冲突的时候,HashMap 是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。
- 插入:现在假如 A 线程和 B 线程同时进行插入操作,然后计算出了相同的哈希值对应了相同的数组位置,然后对同一个数组位置,两个线程会同时得到现在的头结点,A 写入新的头结点之后,B 也写入新的头结点,那B的写入操作就会覆盖 A 的写入操作,造成 A 的写入操作丢失。扩容:HashMap 有个扩容的操作,这个操作会新生成一个新的容量的数组,然后对原数组的所有键值对重新进行计算和写入新的数组,之后指向新生成的数组。当多个线程同时进来,检测到总数量超过门限值的时候就会同时调用 resize 操作,各自生成新的数组并 rehash 后赋给该 map 底层的数组,结果最终只有最后一个线程生成的新数组被赋给该 map 底层,其他线程的均会丢失。
存储:HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有⼀个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出NullPointerException 。线程安全:Hashtable 是线程安全的,而 HashMap 是非线程安全的。(如果要保证线程安全的话就使⽤ConcurrentHashMap 吧!)底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了⼤的变化,当链表⻓度⼤于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。Hashtable 没有这样的机制。 ConcurrentHashMap 和 Hashtable
HashTable和HashMap的实现原理几乎一样,差别无非是
1.HashTable不允许key和value为null;
2.HashTable是线程安全的。
但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。
HashTable性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想。到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N)))。synchronized只锁定当前链表或红黑树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。



