容器
Collection
Collection 和 Collections 有什么区别?
- Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子接口,比如 List、Set 等。
- Collections 是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序方法: Collections. sort(list)。
如何实现数组和 List 之间的转换?
- 数组转 List:使用 Arrays. asList(array) 进行转换。
- List 转数组:使用 List 自带的 toArray() 方法。
比较ArrayList、LinkedList、Vector三者的异同
同: 三个类都是实现了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 更多的空间(因为要存放直接后继和直接前驱以及数据)。
Arrayslist的扩容机制
List<Integer> integers = new ArrayList<>();
//此时创建的数组的长度为0,也就是一个空数组
integers.add(1);
//当向integers数组中加入第一个元素时,此时ArrayList的扩容就已经开始了。
//默认容量为10
//1.判断装下当前值的下一个输入时需要的容量 和默认容量进行比较 较大的值赋给minCapacity
//2.newCapacity = oldCapacity + (oldCapacity >> 1); 旧容量扩容一点五倍
//3.实际扩容大小从minCapacity和newCapacity中更大的选取
Vector和ArrayList的最大区别?
Vector和ArrayList几乎是完全相同的,
- 唯一的区别在于Vector是同步类(synchronized),属于强同步类,是线程安全的。
- 因此开销就比ArrayList要大,访问要慢。正常情况下,
- 大多数的Java程序员使用ArrayList而不是Vector,
- 因为同步完全可以由程序员自己来控制。Vector每次扩容请求其大小的2倍空间,
- 而ArrayList是1.5倍。Vector还有一个子类Stack。是非线程安全的。
区分List中remove(int index)和remove(Object obj)
- 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
- 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 和 Hashtable
- 存储: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倍。