1. 说说List,Set,Map三者的区别?
- List(对付顺序的好帮手): List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象
- Set(注重独一无二的性质): 不允许重复的集合。不会有多个元素引用相同的对象。
- Map(用Key来搜索的专家): 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。
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更多的空间(因为要存放直接
后继
和直接前驱
以及数据)。
补充内容:RandomAccess接口
public interface RandomAccess {
}
查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,在我看来 RandomAccess 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问
功能。
ArrayList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。为什么呢?我觉得还是和底层数据结构有关!ArrayList 底层是数组,而 LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问
。ArrayList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功能。
RandomAccess 接口只是标识,并不是说 ArrayList 实现 RandomAccess 接口才具有快速随机访问功能的!
补充内容:双向链表和双向循环链表
双向链表: 包含两个指针,一个prev
指向前一个节点,一个next
指向后一个节点。
双向循环链表: 最后一个节点的 next 指向head,而 head 的prev指向最后一个节点,构成一个环。
3. ArrayList 与 Vector 区别呢?为什么要用Arraylist取代Vector呢?
Vector类的所有方法都是同步
的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。
Arraylist不是同步
的,所以在不需要保证线程安全时建议使用Arraylist。
4. 总结一下 list 的遍历方式选择:
- 实现了 RandomAccess 接口的list,优先选择
普通 for 循环
,其次foreach
- 未实现 RandomAccess接口的list,优先选择
iterator遍历
(foreach遍历底层也是通过iterator实现的)
大size的数据,千万不要使用普通for循环
5. ArrayList的扩容机制
初始容
ArrayList有多个不同的构造函数,不同的构造函数的初始容量是不同的。
如果不传入初始容量,就使用默认容量
如果传入初始容量,会判断这个传入的值,如果大于0,就new一个新的Object数组
如果传入一个Collection,则会调用toArray()方法把它变成一个数组并赋值给elementData
扩容
ArrayList里面有两个概念,一个是capacity
,它表示的就是“容量”,其实质是数组elementData的长度。而size
则表示的“存放的元素的个数”。
因为Java中,数组操作不能越界
,所以我们必须要保证在插入操作的时候,不会抛出数组越界异常。
底层其实是调用了Arrays.copyOf
方法(拷贝)来进行扩充数组容量的。这里我们主要看一下最后一个方法newCapacity(int minCapacity)
的实现。
默认情况下,新的容量会是原容量的1.5
倍,这里用了位运算提高效率
。
这里对默认容量发现不同jdk是不一样的,关于(1.5倍+1)出现在jdk1.6,其他1.7和1.8都是(1.5倍扩容)。
ArrayList有缩容吗?
ArrayList没有缩容。无论是remove方法还是clear方法,它们都不会改变现有数组elementData的长度。但是它们都会把相应位置的元素设置为null,以便垃圾收集器回收掉不使用的元素,节省内存。
6. HashMap 和 Hashtable 的区别
-
线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过
synchronized
修饰。(如果你要保证线程安全的话就使用ConcurrentHashMap
吧!); -
效率: 因为
线程安全的问题
,HashMap 要比 HashTable 效率高一点
。另外,HashTable 基本被淘汰,不要在代码中使用它; - 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。
-
初始容量大小和每次扩充容量大小的不同:
①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11
,之后每次扩充,容量变为原来的2n+1
。HashMap 默认的初始化大小为16
。之后每次扩充,容量变为原来的2
倍。
②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方
大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。 -
底层数据结构: JDK1.8 以后的 HashMap 在解决
哈希冲突
时有了较大的变化,当链表长度大于阈值(默认为8
)时,将链表
转化为红黑树
,以减少搜索时间。Hashtable 没有这样的机制。
7. HashMap 和 HashSet区别
HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone()、writeObject()、readObject()是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。
8. HashSet如何检查重复
当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。
hashCode()与equals()的相关规定:
如果两个对象相等,则hashcode一定也是相同的
两个对象相等,对两个equals方法返回true
两个对象有相同的hashcode值,它们也不一定是相等的
综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
==与equals的区别
==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同
==是指对内存地址进行比较 equals()是对字符串的内容进行比较
==指引用是否相同 equals()指的是值是否相同
8. HashMap 的底层实现
JDK1.8之前
JDK1.8 之前 HashMap 底层是 数组和链表
结合在一起使用也就是 链表散列
。
HashMap 通过 key
的 hashCode
经过扰动函数处理过后得到 hash 值
,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法
解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
所谓 “拉链法
” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8之后
相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
9. HashMap 的长度为什么是2的幂次方
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方
。
10. ConcurrentHashMap 和 Hashtable 的区别
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,竞争会越来越激烈效率越低。
HashTable:
image.png
JDK1.7的ConcurrentHashMap:
image.png
JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):
image.png
11. ConcurrentHashMap线程安全的具体实现方式/底层具体实现
ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N)))
synchronized只锁定当前链表或红黑二叉树的首节点
,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
12. comparable 和 Comparator的区别
- comparable接口实际上是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序
- comparator接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)方法用来排序
一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()方法或compare()方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo()方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 Collections.sort()
Comparator定制排序
ArrayList<Integer> arrayList = new ArrayList<Integer>();
arrayList.add(-1);
arrayList.add(3);
arrayList.add(3);
arrayList.add(-5);
arrayList.add(7);
arrayList.add(4);
arrayList.add(-9);
arrayList.add(-7);
System.out.println("原始数组:");
System.out.println(arrayList);
// void reverse(List list):反转
Collections.reverse(arrayList);
System.out.println("Collections.reverse(arrayList):");
System.out.println(arrayList);
// void sort(List list),按自然排序的升序排序
Collections.sort(arrayList);
System.out.println("Collections.sort(arrayList):");
System.out.println(arrayList);
// 定制排序的用法
Collections.sort(arrayList, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
System.out.println("定制排序后:");
System.out.println(arrayList);
重写compareTo方法实现按年龄来排序
// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列
// 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他
// 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了
public class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
/**
* TODO重写compareTo方法实现按年龄来排序
*/
@Override
public int compareTo(Person o) {
// TODO Auto-generated method stub
if (this.age > o.getAge()) {
return 1;
} else if (this.age < o.getAge()) {
return -1;
}
return age;
}
}
13. 集合框架底层数据结构总结
Collection
1. List
- Arraylist: Object数组
- Vector: Object数组
- LinkedList: 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环)
2. Set
- HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素
- LinkedHashSet: LinkedHashSet 继承于 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的
- TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树)
Map
- HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
-
LinkedHashMap: LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,
增加了一条双向链表
,使得上面的结构可以保持键值对的插入顺序
。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:《LinkedHashMap 源码详细分析(JDK1.8)》 - Hashtable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
- TreeMap: 红黑树(自平衡的排序二叉树)
14. 如何选用集合
主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap
,需要保证线程安全就选用ConcurrentHashMap
.当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一
时选择实现Set接口的集合比如TreeSet或HashSet
,不需要就选择实现List接口的比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用。