目录
Iterator 和 ListIterator 的区别是什么?
数组 (Array) 和列表 (ArrayList) 有什么区别?什么时候应该使用 Array 而不是 ArrayList?
Set 里的元素是不能重复的,那么用什么方法来区分重复与否呢? 是用 == 还是 equals()? 它们有何区别?
两个对象值相同 (x.equals(y) == true),但却可有不同的 hash code,这句话对不对?
⽐较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
ConcurrentHashMap 和 Hashtable 的区别
为什么集合类没有实现 Cloneable 和 Serializable 接口?
快速失败 (fail-fast) 和安全失败 (fail-safe) 的区别是什么?
概述
从上图可以看出,在 Java 中除了以 Map 结尾的类之外, 其他类都实现了 Collection 接⼝。 并且,以 Map 结尾的类都实现了 Map 接⼝
List,Set,Map
- List (对付顺序的好帮⼿): 存储的元素是有序的、可重复的。
- Set (注重独⼀⽆⼆的性质): 存储的元素是⽆序的、不可重复的。
- Map (⽤ Key 来搜索的专家): 使⽤键值对(kye-value)存储,类似于数学上的函数 y=f(x), “x”代表 key,"y"代表 value,Key 是⽆序的、不可重复的,value 是⽆序的、可重复的,每个键最多映射到⼀个值。
集合框架底层数据结构总结
List
- Arraylist : Object[] 数组
- Vector : Object[] 数组
- LinkedList : 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
Set
- HashSet (⽆序,唯⼀): 基于 HashMap 实现的,底层采⽤ HashMap 来保存元素
- LinkedHashSet : LinkedHashSet 是 HashSet 的⼦类,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的 LinkedHashMap 其内部是基于 HashMap 实现⼀样,不过还是有⼀点点区别的
- TreeSet (有序,唯⼀): 红⿊树(⾃平衡的排序⼆叉树)
Map
- HashMap : JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主 要为了解决哈希冲突⽽存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较⼤ 的变化,当链表⻓度⼤于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓ 度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以 减少搜索时间
- LinkedHashMap : LinkedHashMap 继承⾃ HashMap ,所以它的底层仍然是基于拉链式散 列结构即由数组和链表或红⿊树组成。另外, LinkedHashMap 在上⾯结构的基础上,增加了 ⼀条双向链表,使得上⾯的结构可以保持键值对的插⼊顺序。同时通过对链表进⾏相应的操作, 实现了访问顺序相关逻辑。
- Hashtable : 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突 ⽽存在的
- TreeMap : 红⿊树(⾃平衡的排序⼆叉树)
为什么要使⽤集合?
- 当我们需要保存⼀组类型相同的数据的时候,我们应该是⽤⼀个容器来保存,这个容器就是数组,但是,使⽤数组存储对象具有⼀定的弊端, 因为我们在实际开发中,存储的数据的类型是多种多样的, 于是,就出现了“集合”,集合同样也是⽤来存储多个数据的。
- 数组的缺点是⼀旦声明之后,⻓度就不可变了;同时,声明数组时的数据类型也决定了该数组存储的数据的类型;⽽且,数组存储的数据是有序的、可重复的,特点单⼀。 但是集合提⾼了数据存储的灵活 性,Java 集合不仅可以⽤来存储不同类型不同数量的对象,还可以保存具有映射关系的数据
如何选⽤集合?
- 主要根据集合的特点来选⽤,⽐如我们需要根据键值获取到元素值时就选⽤ Map 接⼝下的集合,需 要排序时选择 TreeMap ,不需要排序时就选择 HashMap ,需要保证线程安全就选⽤ ConcurrentHashMap 。
- 当我们只需要存放元素值时,就选择实现 Collection 接⼝的集合,需要保证元素唯⼀时选择实现 Set 接⼝的集合⽐如 TreeSet 或 HashSet ,不需要就选择实现 List 接⼝的⽐如 ArrayList 或 LinkedList ,然后再根据实现这些接⼝的集合的特点来选⽤。
迭代器 Iterator 是什么?
Iterator 对象称为迭代器(设计模式的⼀种),迭代器可以对集合进⾏遍历,但每⼀个集合内部的数据结构可能是不尽相同的,所以每⼀个集合存和取都很可能是不⼀样的,虽然我们可以⼈为地在每⼀个类中定义 hasNext() 和 next() ⽅法,但这样做会让整个集合体系过于臃肿。于是就有了迭代 器。
迭代器 Iterator 有啥⽤?
Iterator 主要是⽤来遍历集合⽤的,它的特点是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常,触发快速失败机制(fail-fast)。
Iterator 和 ListIterator 的区别是什么?
- Iterator 可用来遍历 Set 和 List 集合,
ListIterator 只能用来遍历 List。 - Iterator 对集合只能是前向遍历
ListIterator 既可以正向也可以反向遍历。 ListIterator 实现了 Iterator 接口,并包含其他的功能,比如:增加元素,替换元 素,获取前一个和后一个元素的索引,等等。
有哪些集合是线程不安全的?怎么解决呢?
- 1. ConcurrentHashMap : 可以看作是线程安全的 HashMap
- 2. CopyOnWriteArrayList :可以看作是线程安全的 ArrayList ,在读多写少的场合性能⾮常 好,远远好于 Vector .
- 3. ConcurrentLinkedQueue :⾼效的并发队列,使⽤链表实现。可以看做⼀个线程安全的 LinkedList ,这是⼀个⾮阻塞队列。
- 4. BlockingQueue : 这是⼀个接⼝,JDK 内部通过链表、数组等⽅式实现了这个接⼝。表示阻塞 队列,⾮常适合⽤于作为数据共享的通道。
- 5. ConcurrentSkipListMap :跳表的实现。这是⼀个 Map ,使⽤跳表的数据结构进⾏快速查 找。
数组 (Array) 和列表 (ArrayList) 有什么区别?什么时候应该使用 Array 而不是 ArrayList?
- Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类型。
- Array 大小是固定的,ArrayList 的大小是动态变化的。
- ArrayList 处理固定大小的基本数据类型的时候,这种方式相对比较慢。
Arraylist 和 Vector 的区别?
- ArrayList 是 List 的主要实现类,底层使⽤ Object[ ](数组)存储,适⽤于频繁的查找⼯作,线程不 安全,需要储存空间时,扩容到原来1.5倍 ;
- Vector 是 List 的古⽼实现类,底层使⽤ Object[ ](数组)存储,线程安全的,可以代替ArrayList做线程安全的集合,在读多写少的情况下可以使用JUC(java.util.concurrent)包下的ConcurrentArrayList,需要扩容时扩容到原来2倍,且可以设置增长的空间大小。
都是插入删除慢,查询快
Arraylist 与 LinkedList 区别?
- 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
- 底层数据结构: Arraylist 底层使⽤的是 Object 数组; LinkedList 底层使⽤的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下⾯有介绍到!)
- 插⼊和删除是否受元素位置的影响: ① 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, E element) ) 时间复杂度近似为 o(n)) 因为需要先移动到指定位置再插⼊。
- 是否⽀持快速随机访问: LinkedList 不⽀持⾼效的随机元素访问,⽽ ArrayList ⽀持。 快速随机访问就是通过元素的序号快速获取元素对象(对应于 get(int index) ⽅法)。
- 内存空间占⽤: ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留⼀定的容量空 间,⽽ LinkedList 的空间花费则体现在它的每⼀个元素都需要消耗⽐ ArrayList 更多的空间 (因为要存放直接后继和直接前驱以及数据)。
双向链表和双向循环链表
comparable 和 Comparator 的区别
- comparable 接⼝实际上是出⾃ java.lang 包 它有⼀个 compareTo(Object obj) ⽅法⽤ 来排序
- comparator 接⼝实际上是出⾃ java.util 包它有⼀个 compare(Object obj1, Object obj2) ⽅法⽤来排序
⼀般我们需要对⼀个集合使⽤⾃定义排序时,我们就要重写 compareTo() ⽅法或 compare() ⽅法,当我们需要对某⼀个集合实现两种排序⽅式,⽐如⼀个 song 对象中的歌名和歌⼿名分别采⽤种排序⽅法的话,我们可以重写 compareTo() ⽅法和使⽤⾃制的 Comparator ⽅法或者以两个 Comparator 来实现歌名排序和歌星名排序,第⼆种代表我们只能使⽤两个参数版的Collections.sort() .
Set 里的元素是不能重复的,那么用什么方法来区分重复与否呢? 是用 == 还是 equals()? 它们有何区别?
equals()
equals() 和 == 方法决定引用值是否指向同一对象 equals() 在类中被覆盖,为的是当两个分离的对象的内容和类型相配的话,返回真值。
两个对象值相同 (x.equals(y) == true),但却可有不同的 hash code,这句话对不对?
- 如果对象要保存在 HashSet 或 HashMap 中,它们的 equals 相等,那么,它 们的 hashcode 值就必须相等。
- 如果不是要保存在 HashSet 或 HashMap,则与 hashcode 没有什么关系了,这时 候 hashcode 不等是可以的
⽆序性和不可重复性的含义是什么
- 什么是⽆序性?⽆序性不等于随机性 ,⽆序性是指存储的数据在底层数组中并⾮按照数组索引的顺序添加 ,⽽是根据数据的哈希值决定的。
- 什么是不可重复性?不可重复性是指添加的元素按照 equals()判断时 ,返回 false,需要同时重 写 equals()⽅法和 HashCode()⽅法。
HashSet 的底层实现是什么?
通过看源码知道 HashSet 的实现是依赖于 HashMap 的,HashSet 的值都是存储在 HashMap 中的。在 HashSet 的构造法中会初始化一个 HashMap 对象,HashSet 不允许值重复,因此,HashSet 的值是作为 HashMap 的 key 存储在HashMap 中的,当存储的值已经存在时返回 false。
⽐较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
- HashSet 是 Set 接⼝的主要实现类 ,HashSet 的底层是 HashMap,线程不安全的,可以存储 null 值;
- LinkedHashSet 是 HashSet 的⼦类,能够按照添加的顺序遍历;
- TreeSet 底层使⽤红⿊树,能够按照添加元素的顺序进⾏遍历,排序的⽅式有⾃然排序和定制排序。
HashMap 和 Hashtable 的区别
- 线程是否安全: HashMap 是⾮线程安全的,HashTable 是线程安全的,因为 HashTable 内部的 ⽅法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使⽤ ConcurrentHashMap 吧!);
- 效率: 因为线程安全的问题,HashMap 要⽐ HashTable 效率⾼⼀点。另外,HashTable 基本被淘汰,不要在代码中使⽤它;
- 对 Null key 和 Null value 的⽀持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有⼀个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会 抛出 NullPointerException。
- 初始容量⼤⼩和每次扩充容量⼤⼩的不同 : ① 创建时如果不指定容量初始值,Hashtable 默 认的初始⼤⼩为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化⼤⼩为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使⽤你给定的⼤⼩,⽽ HashMap 会将其扩充为 2 的幂次⽅⼤⼩(HashMap 中的 tableSizeFor() ⽅法保证,下⾯给出了源代码)。也就是说 HashMap 总是使⽤ 2 的幂作为哈希表的⼤⼩,后⾯会介绍到为什么是 2 的幂次⽅。
- 5. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较⼤的变化,当链表⻓度⼤于 阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择 先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。Hashtable 没有这样的机制。
HashMap 和 HashSet 区别
HashMap 和 TreeMap 区别
- TreeMap 和 HashMap 都继承⾃ AbstractMap ,但是需要注意的是 TreeMap 它还实现了
- NavigableMap 接⼝和 SortedMap 接⼝。
- 实现 NavigableMap 接⼝让 TreeMap 有了对集合内元素的搜索的能⼒。
- 实现 SortMap 接⼝让 TreeMap 有了对集合中的元素根据键排序的能⼒。默认是按 key 的升序排序,不过我们也可以指定排序的⽐较器。
- 相⽐于 HashMap 来说 TreeMap 主要多是对集合中的元素根据键排序的能⼒以及对集合内元素的搜索的能⼒。
HashSet 如何检查重复
当你把对象加⼊ HashSet 时, HashSet 会先计算对象的 hashcode 值来判断对象加⼊的位置,同时也会与其他加⼊的对象的 hashcode 值作⽐较,如果没有相符的 hashcode , HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调⽤ equals() ⽅法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加⼊操作成功。
HashMap 的底层实现
JDK1.8之前
JDK1.8 之前 HashMap 底层是 数组和链表 结合在⼀起使⽤也就是 链表散列 。 HashMap 通过 key 的hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这⾥的 n 指的是数组的⻓度),如果当前位置存在元素的话,就判断该元素与要存⼊的元素的 hash值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
扰动函数:指的就是 HashMap 的 hash ⽅法。使⽤ hash ⽅法也就是扰动函数是为了防⽌⼀些实现 ⽐较差的 hashCode() ⽅法 换句话说使⽤扰动函数之后可以减少碰撞。
拉链法 :将链表和数组相结合。也就是说创建⼀个链表数组,数组中每⼀格就是⼀个链 表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8 之后在解决哈希冲突时有了较⼤的变化,当链表⻓度⼤于阈值(默认为8 )(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64 ,那么会选择先进⾏数组扩容,⽽不 是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。
HashMap 的工作原理是什么?
Java 中的 HashMap 是以键值对 (key-value) 的形式存储元素的。HashMap 需要一个 hash 函数,它使用 hashCode()和 equals()方法来向集合 / 从集合添加和检索元素。当调用 put() 方法的时候,HashMap 会计算 key 的 hash 值,然后把键值对存储在集合中合适的索引上。 如果 key 已经存在了,value 会被更新成新值。
Hashmap 什么时候进行扩容呢?
当 hashmap 中的元素个数超过(负载因子*数组长度) loadFactor 时,就会进行数组扩容, loadFactor (负载因子)的默认值为 0.75,也就是说,默认情况下,数组大小为 16,那么当 hashmap 中元素个数超过 160.75=12 的时候,就把数组的大小扩展为 216=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的问题。
HashMap 的⻓度为什么是 2 的幂次⽅
Hash 值的范围值 -2147483648 到 2147483647 ,前后加起来⼤概 40 亿的映射空间,只要哈希函数 映射得⽐较均匀松散,⼀般应⽤是很难出现碰撞的。但问题是⼀个 40 亿⻓度的数组,内存是放不下 的。所以这个散列值是不能直接拿来⽤的。⽤之前还要先做对数组的⻓度取模运算,得到的余数才能⽤ 来要存放的位置也就是对应的数组下标。这个数组下标的计算⽅法是“ (n - 1) & hash ” 。( n 代表数组⻓度)这也就解释了 HashMap 的⻓度为什么是 2 的幂次⽅。
为什么这样能均匀分布减少碰撞呢?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 ,不同位置上,不碰撞;
HashMap 多线程操作导致死循环问题
HashMap 有哪⼏种常⻅的遍历⽅式?
HashMap 遍历从大的方向来说,可分为以下 4 类:
-
迭代器(Iterator)方式遍历;
-
For Each 方式遍历;
-
lambda 表达式遍历(JDK 1.8+);
-
Streams API 遍历(JDK 1.8+)。
但每种类型下又有不同的实现方式,因此具体的遍历方式又可以分为以下 7 种:
- 使用迭代器(Iterator)EntrySet 的方式进行遍历;
- 使用迭代器(Iterator)KeySet 的方式进行遍历;
- 使用 For Each EntrySet 的方式进行遍历;
- 使用 For Each KeySet 的方式进行遍历;
- 使用 Lambda 表达式的方式进行遍历;
- 使用 Streams API 单线程的方式进行遍历;
- 使用 Streams API 多线程的方式进行遍历。
LinkedHashMap 的实现原理?
ConcurrentHashMap 和 Hashtable 的区别
- 底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采⽤ 分段的数组+链表 实现,JDK1.8 采 ⽤的数据结构跟 HashMap1.8 的结构⼀样,数组+链表/红⿊⼆叉树。Hashtable 和 JDK1.8 之前 的 HashMap 的底层数据结构类似都是采⽤ 数组+链表 的形式,数组是 HashMap 的主体,链表 则是主要为了解决哈希冲突⽽存在的;
- 实现线程安全的⽅式(重要): ① 在 JDK1.7 的时候,ConcurrentHashMap(分段锁) 对整个 桶数组进⾏了分割分段(Segment),每⼀把锁只锁容器其中⼀部分数据,多线程访问容器⾥不同 数据段的数据,就不会存在锁竞争,提⾼并发访问率。 到了 JDK1.8 的时候已经摒弃了 Segment 的概念,⽽是直接⽤ Node 数组+链表+红⿊树的数据结构来实现,并发控制使⽤ synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化) 整个看起 来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但 是已经简化了属性,只是为了兼容旧版本;② Hashtable(同⼀把锁) :使⽤ synchronized 来保 证线程安全(全表锁),效率⾮常低下。当⼀个线程访问同步⽅法时,其他线程也访问同步⽅法,可能会进 ⼊阻塞或轮询状态,如使⽤ put 添加元素,另⼀个线程不能使⽤ put 添加元素,也不能使⽤get,竞争会越来越激烈效率越低。
Collections ⼯具类
排序
- reverse 反转
- sort排序
- swap交换元素位置
查找
- max找最大
同步控制
collections里提供了很多方法来实现线程安全,但不推荐用,尽量使用java.util.concurrent(JUC)包下的并发集合
为什么集合类没有实现 Cloneable 和 Serializable 接口?
克隆 (cloning) 或者是序列化 (serialization) 的语义和含义是跟具体的实现相关的。因此,应该 由集合类的具体实现来决定如何被克隆或者是序列化。
Collection 和 Collections 的区别。
- collection 是集合类的上级接口, 继承与它的接口主要是 set 和 list。
- collections 类是针对集合类的一个帮助类. 它提供一系列的静态方法对各种集合的搜 索, 排序, 线程安全化等操作
什么是快速失败(fail-fast)?
什么是安全失败(fail-safe)呢?
快速失败 (fail-fast) 和安全失败 (fail-safe) 的区别是什么?
- Iterator 的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。
- java.util 包下面的所有的集合类都是快速失败的,而 java.util.concurrent 包下面的 所有的类都是安全失败的。
- 快速失败的迭代器会抛出 ConcurrentModificationException 异常,而安全失败的迭代器永远不会抛出这样的异常。