- PART2
- 038 ArrayList的动态扩容
- 039 ArrayList的增加与删除操作
- 040 Comparable 和Comparator
- 041 Collection与Collections
- 042 LinkedList原理
- 043 Vector Stack
- 044 HashMap中key
- 045 HashMap底层原理
- 046 HashMap初始长度与扩容长度
- 047 HashMap 初始容量与负载因子
- 048 HashMap扩容
- 049 HashMap线程不安全特性
- 050 HashMap 和 LinkedHashMap
- 051 LinkedHashMap
- 052 Hashtable HashMap
- 053 fail_fast机制
- 054 TreeMap HashMap
- 055 HashSet基本原理
- 056 HashSet 与 HashMap
- 057 LinkedHashSet
- 058 List Map Set Queue
PART2
038 ArrayList的动态扩容
Q:Arraylist 的动态扩容机制是如何自动增加的?简单说说你理解的流程?
- 当在 ArrayList 中增加一个对象时 Java 会去检查 Arraylist 以确保已存在的数组中有足够的容量来存储这个新对象,
- 默认初始容量为10,没有足够容量新建一个长度为原来的1.5倍的容量
- 旧的数组采用copyOf()方法被复制到新的数组中,现有的数组引用指向新的数组,
039 ArrayList的增加与删除操作
Q: 为什么 ArrayList 的增加或删除操作相对来说效率比较低?能简单解释下为什么吗?
- ArrayList 在小于扩容容量的情况下其实增加操作效率是非常高的,在涉及扩容的情况下添加操作效率确实低,删除操作需要移位拷贝,效率是低点。
- 因为 ArrayList 中增加(扩容)或者是删除元素要调用 System.arrayCopy 这种效率很低的方法进行处理,所以如果遇到了数据量略大且需要频繁插入或删除的操作效率就比较低了,具体可查看 ArrayList 的 add 和 remove 方法实现,但是 ArrayList 频繁访问元素的效率是非常高的,因此遇到类似场景我们应该尽可能使用 LinkedList 进行替代效率会高一些。
040 Comparable 和Comparator
Q: Comparable 和 Comparator 的区别和场景?
- Comparable 对实现它的每个类的对象进行整体排序,这个接口需要类本身去实现,若一个类实现了 Comparable 接口,实现 Comparable 接口的类的对象的 List 列表(或数组)可以通过 Collections.sort(或 Arrays.sort)进行排序,此外实现 Comparable 接口的类的对象可以用作有序映射(如TreeMap)中的键或有序集合(如TreeSet)中的元素而不需要指定比较器, 实现 Comparable 接口必须修改自身的类(即在自身类中实现接口中相应的方法)
- 若一个类实现了 Comparable 接口就意味着该类支持排序,而 Comparator 是比较器,我们若需要控制某个类的次序,可以建立一个该类的比较器来进行排序。 Comparable 比较固定,和一个具体类相绑定,而 Comparator 比较灵活,可以被用于各个需要比较功能的类使用,所以尽量推荐使用 Comparator 而不是 Comparable,因为这样可以保证单一职责原则。
041 Collection与Collections
- java.util.Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,在 Java 类库中有很多具体的实现,意义是为各种具体的集合提供最大化的统一操作方式
- java.util.Collections 是一个包装类,它包含有各种有关集合操作的静态多态方法,此类构造 private 不能实例化,就像一个工具类
042 LinkedList原理
Q: LinkedList 工作原理和实现?
LinkedList 是以双向链表实现,链表无容量限制(但是双向链表本身需要消耗额外的链表指针空间来操作),其内部主要成员为 first 和 last 两个 Node 节点,在每次修改列表时用来指引当前双向链表的首尾部位,所以 LinkedList 不仅仅实现了 List 接口,还实现了 Deque 双端队列接口(该接口是 Queue 队列的子接口),故 LinkedList 自动具备双端队列的特性,
043 Vector Stack
Q: 什么是 Vector 和 Stack,各有什么特点?
- Vector 是线程安全的动态数组,同 ArrayList 一样继承自 AbstractList 且实现了 List、RandomAccess、Cloneable、Serializable 接口,内部实现依然基于数组,Vector 与 ArrayList 基本是一致的,唯一不同的是 Vector 是线程安全的,会在可能出现线程安全的方法前面加上 synchronized 关键字
- Stack 是继承自 Vector 基于动态数组实现的线程安全栈,不过现在已经不推荐使用了,Stack 是并发安全的后进先出,实现了一些栈基本操作的方法。其共同点都是使用了方法锁(即 synchronized)来保证并发安全的。
044 HashMap中key
Q:HashMap 中的 key 如果是 Object 则需要实现哪些方法?
- hashCode 方法用来计算 Entry 在数组中的 index 索引位置,
- equals 方法用来比较数组指定 index 索引位置上链表的节点 Entry 元素是否相等。否则由于 hashCode 方法实现不恰当会导致严重的 hash 碰撞,从而使 HashMap 会退化成链表结构而影响性能。
045 HashMap底层原理
Q: 简单说说 HashMap 的底层原理?
1. PUT:HashMap 中 put 元素时,先根据 key 的 hash 值得到这个 Entry 元素在数组中的位置(即下标),然后把这个 Entry 元素放到对应的位置中,如果这个 Entry 元素所在的位子上已经存放有其他元素就在同一个位子上的 Entry 元素以链表的形式存放,新加入的放在链头,
2. get:从 HashMap 中 get Entry 元素时先计算 key 的 hashcode,找到数组中对应位置的某一 Entry 元素,然后通过 key 的 equals 方法在对应位置的链表中找到需要的 Entry 元素,所以 HashMap 的数据结构是数组和链表的结合,此外 HashMap 中 key 和 value 都允许为 null,key 为 null 的键值对永远都放在以 table[0] 为头结点的链表中。
3. 对于 JDK 1.8 开始 HashMap 实现原理变成了数组+链表+红黑树的结构,JDK 1.8 以前碰撞节点会在链表头部插入,而 JDK 1.8 开始碰撞节点会在链表尾部插入,对于扩容操作后的节点转移 JDK 1.8 以前转移前后链表顺序会倒置,而 JDK 1.8 中依然保持原序
046 HashMap初始长度与扩容长度
Q:HashMap 默认的初始化长度是多少?为什么默认长度和扩容后的长度都必须是 2 的幂?
- 在 JDK 中默认长度是 16,并且默认长度和扩容后的长度都必须是 2 的幂
- 原因: 减少哈希冲突,数据均匀,查询速度快
HashMap 的长度为 2 的幂时减一的值的二进制位数一定全为 1,这样数组下标 index 的值完全取决于 key 的 hash 值的后几位,因此只要存入 HashMap 的 Entry 的 key 的 hashCode 值分布均匀,HashMap 中数组 Entry 元素的分部也就尽可能是均匀的(也就避免了 hash 碰撞带来的性能问题),所以当长度为 2 的幂时不同的 hash 值发生碰撞的概率比较小,这样就会使得数据在 table 数组中分布较均匀,查询速度也较快
047 HashMap 初始容量与负载因子
Q: HashMap 构造方法中 initialCapacity(初始容量)、loadFactor(加载因子)的理解?
1. initialCapacity 初始容量代表了哈希表中桶的初始数量,即 Entry< K,V>[] table 数组的初始长度,不过特别注意,table 数组的长度虽然依赖 initialCapacity,但是每次都会通过 roundUpToPowerOf2(initialCapacity) 方法来保证为 2 的幂次。
2. oadFactor 加载因子是哈希表在其容量自动增加之前可以达到多满的一种饱和度百分比,其衡量了一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小
3. 当哈希表中 Entry 的数量超过了 loadFactor 加载因子乘以当前 table 数组桶长度时就会触发扩容操作。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大则对空间的利用更充分,从而导致查找效率的降低,如果负载因子太小则散列表的数据将过于稀疏,从而对空间造成浪费。系统默认负载因子为 0.75,一般情况下无需修改。
048 HashMap扩容
Q:JDK 1.8 中 HashMap 是如何扩容的?与 JDK 1.7 有什么区别?
- jdk1.7 :整个扩容过程就是一个取出数组元素(实际数组索引位置上的每个元素是每个独立单向链表的头部,也就是发生 Hash 冲突后最后放入的冲突元素)然后遍历以该元素为头的单向链表元素,依据每个被遍历元素的 hash 值计算其在新数组中的下标然后进行交换(即原来 hash 冲突的单向链表尾部变成了扩容后单向链表的头部)。
- JDK1.8: 由于扩容数组的长度是 2 倍关系,所以对于假设初始 tableSize =4 要扩容到 8 来说就是 0100 到 1000 的变化(左移一位就是 2 倍),在扩容中只用判断原来的 hash 值与左移动的一位(扩容后的数组)按位与操作是 0 或 1 就行,0 的话索引就不变,1 的话索引变成原索引加上扩容前数组,
- 优化:
1. 之前的哈希冲突的元素再随机的分布到不同的索引中去
2. jDK1.7 中扩容操作时哈希冲突的数组索引处的旧链表元素扩容到新数组时如果扩容后索引位置在新数组的索引位置与原数组中索引位置相同,则链表元素会发生倒置;而在 JDK1.8 中不会出现链表倒置现象。
3. JDK1.8 中为了性能在同一索引处发生哈希冲突到一定程度时链表结构会转换为红黑数结构存储冲突元素
049 HashMap线程不安全特性
Q: 简单说说 HashMap 为什么是线程不安全的?具体体现在哪些方面?
- fail_fast: 如果在使用迭代器的过程中有其他线程修改了 HashMap 就会抛出 ConcurrentModificationException 异常(fail-fast 策略)
- JDK1.7 的 HashMap 并发 put 操作触发扩容导致潜在可能的死循环现象,JDK1.8 的 HashMap 并发 put 操作不会导致潜在的死循环,JDK1.7 中并发扩容操作可能会导致哈希碰撞的链表结构为循环链表,从而导致在后续 put、get 操作时发生死循环。而对于 JDK1.8 中扩容链表的顺序是不会发生逆向的,所以自然怎么遍历都不会出现循环链表的情况,故 JDK1.8 中不会出现并发循环链表,但由于 JDK1.7 与 JDK1.8 中都是无锁保护的,所以依然是并发不安全的。(jdk1.7扩容时链表需要反转)
050 HashMap 和 LinkedHashMap
Q: 简单说说 HashMap 和 LinkedHashMap 的区别?
HashMap 可以允许一条键为 Null 的键值对,允许多条值为 Null 的键值对,其并发不安全,如果想并发安全操作可以使用 Collections.synchronizedMap() 方法或 ConcurrentHashMap 来代替。
LinkedHashMap 是 HashMap 的一个子类,其特殊实现的仅仅是保存了记录的插入顺序,所以在 Iterator 迭代器遍历 LinkedHashMap 时先得到的键值对是先插入的(也可以在构造时用带参数构造方法来改变顺序为按照使用进行排序),由于其存储沿用了 HashMap 结构外还多了一个双向顺序链表,所以在一般场景下遍历时会比 HashMap 慢,此外具备 HashMap 的所有特性和缺点。
051 LinkedHashMap
Q: 谈谈你对 LinkedHashMap 的原理理解?
- 其实 LinkedHashMap 是 HashMap 的子类,其在 HashMap 的基础上只添加了一个双向链表和一个顺序模式属性,
- 其每次 put 元素都会往这个双向链表上添加节点,其构造方法比 HashMap 多了一个 boolean 类型的 accessOrder 参数,当该参数为 true 时则按照元素最后访问时间在双向链表中排序,为 false 则按照插入顺序排序,默认为 false。
052 Hashtable HashMap
Q: 说说 Hashtable 与 HashMap 的区别?
- 并发性:HashMap 是非 synchronized 的,而 Hashtable 是 synchronized 的。
- null值: HashMap 可以接受 null 的键和值,而 Hashtable 的 key 与 value 均不能为 null 值。
- 性能: 单线程情况下使用 HashMap 性能要比 Hashtable 好,因为 HashMap 是没有同步操作的。
- 继承树: Hashtable 继承自 Dictionary 类且实现了 Map 接口,而 HashMap 继承自 AbstractMap 类且实现了 Map 接口。
- 容量: HashTable 的默认容量为11,而 HashMap 为 16(安卓中为 4)。
- 扩容:Hashtable 不要求底层数组的容量一定是 2 的整数次幂,而 HashMap 则要求一定为 2 的整数次幂。
- 扩容:Hashtable 扩容时将容量变为原来的 2 倍加 1,而 HashMap 扩容时将容量变为原来的 2 倍。
- 方法:Hashtable 有 contains 方法,而 HashMap 有 containsKey 和 containsValue 方法。
- HashMap 的迭代器 Iterator 是 fail-fast 机制的,而 Hashtable 的 Enumerator 迭代器不是 fail-fast 机制的(历史原因)。
053 fail_fast机制
- fail-fast 机制,即快速失败机制,是java集合(Collection)中的一种错误检测机制。
- 当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出ConcurrentModificationException异常
- fail-fast机制并不保证在不同步的修改下一定会抛出异常,它只是尽最大努力去抛出,所以这种机制一般仅用于检测bug。
054 TreeMap HashMap
Q: TreeMap 与 HashMap 的区别?
- TreeMap 实现了 SortMap 接口,其能够根据键排序,默认是按键的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时得到的记录是排过序的,所以在插入和删除操作上会有些性能损耗,TreeMap 的键和值都不能为空,其为非并发安全 Map,此外 TreeMap 基于红黑树实现。
- HashMap 是最常用的 Map,其基于哈希散列表实现,主要根据键的 hashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度,当用 Iterator 遍历 HashMap 时得到的记录顺序是随机的,HashMap 只允键值均为空,其为非并发安全 Map。
055 HashSet基本原理
- HashSet 在存元素时会调用对象的 hashCode 方法计算出存储索引位置,如果其索引位置已经存在元素(哈希碰撞)则和该索引位置上所有的元素进行 equals 比较,如果该位置没有其他元素或者比较的结果都为 false 就存进去,否则就不存。
- 可以看见元素是按照哈希值来找位置的,故而无序且可以保证无重复元素,因此我们在往 HashSet 集合中存储元素时,元素对象应该正确重写 Object 类的 hashCode 和 equals 方法,否则会出现不可预知的错误。
056 HashSet 与 HashMap
Q:说说 HashSet 与 HashMap 的区别?
从实质上说 HashSet 的实现实质就是一个 Map 对象的包装,只是 Map 的 value 为 Object 固定对象,Set 只利用了 Map 的 key 而已。具体区别来说如下:
1. 接口: HashMap 实现了 Map 接口,而 HashSet 实现了 Set 接口。
2. 键值对: HashMap 储存键值对,而 HashSet 仅仅存储对象。
3. 方法:HashMap 使用 put 方法将元素放入 Map 中,而 HashSet 使用 add 方法将元素放入 Set 中。
4. HashMap 中使用键对象来计算 hashcode 值,而 HashSet 使用成员对象来计算 hashcode 值。
057 LinkedHashSet
Q: LinkedHashSet 的理解及其与 HashSet 的关系?
- LinkedHashSet 是可以按照插入顺序或者访问顺序进行迭代的非并发安全 Set,其继承自 HashSet,而 HashSet 又专门提供了一个包访问权限的构造方法给 LinkedHashSet 使用,这个构造方法内部基于 LinkedHashMap 来实现,而普通的 HashSet 则是基于 HashMap 来实现,所以可以说 LinkedHashSet 底层使用 LinkedHashMap 来保存所有元素,其所有的操作方法与 HashSet 相同,
- LinkedHashSet 与 HashSet 的不同之处在于 LinkedHashSet 依靠 LinkedHashMap 维护着一个顺序链表结构用来迭代,而 HashSet 依靠 HashMap 维持着一个简单的哈希表存储访问;此外 LinkedHashSet 与 HashSet 没有任何区别,都支持元素为 null,都是非并发安全 Set。
058 List Map Set Queue
Q: 简单说说你理解的 List 、Map、Set、Queue 的区别和关系?
- 继承关系:List、Set、Queue 都继承自 Collection 接口,而 Map 则不是(继承自 Object),所以容器类有两个根接口,分别是 Collection 和 Map,Collection 表示单个元素的集合,Map 表示键值对的集合。
- 各自特点List: 有序性和元素可空性,他维护了元素的特定顺序,其主要实现类有 ArrayList 和 LinkList。
- Set 的主要特性就是唯一性,存入 Set 的每个元素都必须唯一,加入 Set 的元素都必须确保对象的唯一性,Set 不保证维护元素的有序性,其主要实现类有 HashSet、LinkHashSet、TreeSet。HashSet 是为快速查找元素而设计,
- Queue:主要特性就是队列,其主要的实现类有 LinkedList、PriorityQueue。
Map:主要特性就是维护键值对关联和查找特性,其主要实现类有 HashTab、HashMap、LinkedHashMap、TreeMap
数据结构角度:动态数组:ArrayList 内部是动态数组,HashMap 内部的链表数组也是动态扩展的,ArrayDeque 和 PriorityQueue 内部也都是动态扩展的数组。
- 链表:LinkedList 是用双向链表实现的,HashMap 中映射到同一个链表数组的键值对是通过单向链表链接起来的,LinkedHashMap 中每个元素还加入到了一个双向链表中以维护插入或访问顺序。
- 哈希表:HashMap 是用哈希表实现的,HashSet, LinkedHashSet 和 LinkedHashMap 基于 HashMap,内部当然也是哈希表。
- 排序二叉树:TreeMap 是用红黑树(基于排序二叉树)实现的,TreeSet 内部使用 TreeMap,当然也是红黑树,红黑树能保持元素的顺序且综合性能很高。
- 堆:PriorityQueue 是用堆实现的,堆逻辑上是树,物理上是动态数组,堆可以高效地解决一些其他数据结构难以解决的问题。
- 循环数组:ArrayDeque 是用循环数组实现的,通过对头尾变量的维护,实现了高效的队列操作。
- 位向量:EnumSet 是用位向量实现的,对于只有两种状态且需要进行集合运算的数据使用位向量进行表示、位运算进行处理,精简且高效。