原文链接.
一、List,Map和Set三者区别:
list(注重顺序): List接⼝存储⼀组不唯⼀(可以有多个元素引⽤相同的对象),有序的对象;
Set(注重独⼀⽆⼆的性质): 不允许重复的集合。不会有多个元素引⽤相同的对象。HashSet可以存一个空值(null),TreeSet不能存空值(null)
Map(⽤Key来搜索的专家): 使⽤键值对存储。 Map会维护与Key有关联的值。两个Key可以引⽤相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。
二、常用集合是否能存储空值(null):
1. List中:ArrayList 和 LinkedList 都可以存储多个空值(null);
2. Map中:
Ⅰ. HashMap,可以存储一个 key 为 null 的的值:
HashMap的put方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这里put时需要计算hash()方法,当key为null时,hash()直接返回0,不会调用key。hashCode()方法。不会产生空指针报错。不过HashMap的key只能存储一个null,因为key是不能重复的,所以有且只能有一个null。
注意: HashMap 允许有一个 Node 的 Key 为 null,该 Node 一定会放在第 0 个桶的位置,因为这个 Key 无法计算 hashCode(),因此只能规定一个桶让它存放。
Ⅱ. TreeMap,不能存 key 为 null 的值
TreeMap的put方法:
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
可以看到key为空时会抛出空指针异常,这里没有直接看出value是否能为空,但是通过自己写单元测试后发现是可以存储value为null的值的。
Ⅲ. HashTable,不能存 key 为 null 的值
3. Set:HashSet有且仅有一个 null 值,TreeSet 不能存空值
由于HashSet的底层是HashMap,Set存储的值都是在key中,所以可以看出Set不能存储重复值的原因就来自于Key不能重复,由于是HashMap,所以key是可以为null的即HashSet有且仅有一个null值。
TreeSet,其底层也是TreeMap,所以对于null的限制也同TreeMap。
三、Arraylist 详解:
1. ArrayList不保证线程安全;
2. ArrayList 采用 Object[] 数组存储,
3. 插⼊和删除
⽐如:执⾏ add( E element) ⽅法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插⼊和删除元素的话(add(int index, E element) )时间复杂度就为 O(n-i)。因为在进⾏上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执⾏向后位/向前移⼀位的操作。
4. ArrayList ⽀持 快速随机访问: 就是通过元素的序号快速获取元素对象(对应于 get(int index) ⽅法)
5. 内存空间占⽤:ArrayList的空间浪费主要体现在在 list 列表的结尾会预留⼀定的容量空间
6. 继承了RandomAccess接口:
public interface RandomAccess {
}
查看源码我们发现实际上 RandomAccess 接⼝中什么都没有定义。RandomAccess 接口只是⼀个标识。标识实现这个接⼝的类具有随机访问功能。
public static <T>
int binarySearch(List<? extends Comparable<? super Tjk list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}
在 binarySearch()⽅法中,它要判断传⼊的list 是否 RamdomAccess 的实例,如果是,调⽤indexedBinarySearch() ⽅法,如果不是,那么调⽤ iteratorBinarySearch() ⽅法
- ArrayList 底层是数组,⽽ LinkedList 底层是链表。
- 数组天然⽀持随机访问,时间复杂度为 O(1),所以称为快速随机访问。
- 链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不⽀持快速随机访问。
- ArrayList 实现了 RandomAccess 接⼝,就表明了他具有快速随机访问功能。 RandomAccess 接⼝只是标识,并不是说 ArrayList 实现 RandomAccess接⼝才具有快速随机访问功能的!
下⾯再总结⼀下 list 的遍历⽅式选择:
实现了 RandomAccess 接⼝的list,优先选择普通 for 循环 ,其次foreach
未实现 RandomAccess 接⼝的list,优先选择iterator遍历(foreach遍历底层也是通过 iterator实现的,⼤size的数据,不要使⽤普通for循环
7. ArrayList的扩容机制:
ArrayList是List接口的实现类,它是支持根据需要而动态增长的数组。java中标准数组是定长的,在数组被创建之后,它们不能被加长或缩短。这就意味着在创建数组时需要知道数组的所需长度,但有时我们需要动态程序中获取数组长度。ArrayList就是为此而生的。
- ArrayList扩容发生在add()方法调用的时候,下面是add()方法的源码:
public boolean add(E e) {
// 扩容
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
- 根据意思可以看出ensureCapacityInternal()是用来扩容的,形参为最小扩容量,进入此方法后:
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
- 进入方法calculateCapacity(elementData, minCapacity):
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果传入的是个空数组则最小容量取默认容量与minCapacity之间的最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
- ensureExplicitCapacity方法可以判断是否需要扩容:
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果最小需要空间比elementData的内存空间要大,则需要扩容
if (minCapacity - elementData.length > 0)
// 扩容
grow(minCapacity);
}
- 接下来重点来了,ArrayList扩容的关键方法grow():
private void grow(int minCapacity) {
// 获取到ArrayList中elementData数组的内存空间长度
int oldCapacity = elementData.length;
// 扩容至原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 再判断一下新数组的容量够不够,够了就直接使用这个长度创建新数组,
// 不够就将数组长度设置为需要的长度
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//若预设值大于默认的最大值检查是否溢出
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 调用Arrays.copyOf方法将elementData数组指向新的内存空间时newCapacity的连续空间
// 并将elementData的数据复制到新的内存空间
elementData = Arrays.copyOf(elementData, newCapacity);
}
四、LinkedList 详解:
1. LinkedList 不保证线程安全
2. LinkedList 底层使用的是 双向链表 数据结构(JDK1.6之前为循环链表, JDK1.7取消了循环。)
- 双向链表: 包含两个指针,⼀个prev指向前⼀个节点,⼀个next指向后⼀个节点。
- 双向循环链表: 最后⼀个节点的 next 指向head,⽽ head 的prev指向最后⼀个节点,构成⼀个环。
3. 插入与删除:
LinkedList 采⽤链表存储
所以对于 add(E element) ⽅法的插⼊,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置 i 插⼊和删除元素的话((add(int index, E element) ) 时间复杂度近似为 O(n)) 因为需要先移动到指定位置再插⼊。
4. LinkedList 不⽀持快速随机访问
5. 内存空间占用:LinkedList 的空间花费则体现在它的每⼀个元素都需要消耗⽐ ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
五、HashMap详解:
1. HashMap 基础,与 Hashtable 的区别(HashTable基本已经被淘汰,不在代码中使用):
-
线程是否安全: HashMap 是⾮线程安全的, HashTable 是线程安全的; HashTable 内部的⽅法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使⽤ ConcurrentHashMap);
-
效率: 因为线程安全的问题, HashMap 要⽐ HashTable 效率⾼⼀点。另外, HashTable 基本被淘汰,不要在代码中使⽤它;
-
对Null key 和Null value的⽀持:见标题二:。
-
初始容量⼤⼩和每次扩充容量⼤⼩的不同 :
① 创建时如果不指定容量初始值, Hashtable 默认的初始⼤⼩为11,之后每次扩充,容量变为原来的 2n+1。 HashMap 默认的初始化⼤⼩为16。之后每次扩充,容量变为原来的2倍。
② 创建时如果给定了容量初始值,那么 Hashtable 会直接使⽤你给定的⼤⼩,⽽ HashMap 会将其扩充为2的幂次⽅⼤⼩(HashMap 中的 tableSizeFor() ⽅法保证,下⾯给出了源代码)。也就是说 HashMap 总是使⽤2的幂次方作为哈希表的⼤⼩。
HashMap:为什么容量总是为2的次幂.
/**
* (n - 1) & hash,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n
* HashMap 中带有初始容量的构造函数:
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
/**
* 下⾯这个⽅法保证了 HashMap 总是使⽤2的幂作为哈希表的⼤⼩。
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
2. HashMap的底层实现:
JDK1.8 之前
JDK1.8 之前 HashMap 底层是 数组和链表 结合在⼀起使⽤也就是 链表散列。 HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这⾥的 n 指的是数组的⻓度),如果当前位置存在元素的话,就判断该元素与要存⼊的元素的 hash值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash ⽅法。使⽤ hash ⽅法也就是扰动函数是为了防⽌⼀些实现
⽐较差的 hashCode() ⽅法 换句话说使⽤扰动函数之后可以减少碰撞。
JDK 1.8 HashMap 的 hash ⽅法源码:
JDK 1.8 的 hash⽅法 相⽐于 JDK 1.7 hash ⽅法更加简化,但是原理不变。
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:⽆符号右移,忽略符号位,空位都以0补⻬
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
所谓 “拉链法” 就是:
将链表和数组相结合。也就是说创建⼀个链表数组,数组中每⼀格就是⼀个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8 之后
相⽐于之前的版本, JDK1.8之后在解决哈希冲突时有了⼤的变化,当链表⻓度⼤于阈值(默认为8)时,将链表转化为红⿊树,以减少搜索时间。
TreeMap、 TreeSet 以及JDK1.8之后的 HashMap 底层都⽤到了红⿊树。 红⿊树就是为了解决⼆叉查找树的缺陷,因为⼆叉查找树在某些情况下会退化成⼀个线性结构。
3. HashMap 多线程操作导致死循环问题
主要原因在于 并发下的Rehash 会造成元素之间会形成⼀个循环链表。不过, jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使⽤ HashMap,因为多线程下使⽤ HashMap 还是会存在其他问题⽐如数据丢失。并发环境下推荐使⽤ ConcurrentHashMap 。
详情请查看: 疫苗:JAVA HASHMAP的死循环.
4. ConcurrentHashMap 和 Hashtable 的区别
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的⽅式上不同。
-
底层数据结构: JDK1.7的 ConcurrentHashMap 底层采⽤ 分段的数组+链表 实现, JDK1.8 采⽤的数据结构跟 HashMap 1.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,竞争会越来越激烈效率越低。
5. HashTable,ConcurrentHashMap对比图:
HashTable:
ConcurrentHashMap JDK1.7
JDK1.8的ConcurrentHashMap(TreeBin: 红⿊⼆叉树节点 Node: 链表节点):
6. ConcurrentHashMap线程安全的具体实现方式 / 底层具体实现
JDK1.7(上⾯有示意图)
⾸先将数据分为⼀段⼀段的存储,然后给每⼀段数据配⼀把锁,当⼀个线程占⽤锁访问其中⼀个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。
Segment 实现了 ReentrantLock,所以 Segment 是⼀种可重⼊锁,扮演锁的⻆⾊。 HashEntry ⽤于存储键值对数据。
static class Segment<K,V> extends ReentrantLock implements Serializable {
}
⼀个 ConcurrentHashMap ⾥包含⼀个 Segment 数组。 Segment 的结构和HashMap类似,是⼀种数组和链表结构,⼀个 Segment 包含⼀个 HashEntry 数组,每个 HashEntry 是⼀个链表结构的元素,每个Segment 守护着⼀个HashEntry数组⾥的元素,当对 HashEntry 数组的数据进⾏修改时,必须⾸先获得对应的 Segment的锁。
JDK1.8 (上⾯有示意图)
ConcurrentHashMap 取消了 Segment 分段锁,采⽤ CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红⿊⼆叉树。 Java 8在链表⻓度超过⼀定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红⿊树(寻址时间复杂度为O(log(N)))synchronized 只锁定当前链表或红⿊⼆叉树的⾸节点,这样只要hash不冲突,就不会产⽣并发,效率⼜提升N倍。
六、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);
输出:
原始数组:
[-1, 3, 3, -5, 7, 4, -9, -7]
Collections.reverse(arrayList):
[-7, -9, 4, 7, -5, 3, 3, -1]
Collections.sort(arrayList):
[-9, -7, -5, -1, 3, 3, 4, 7]
定制排序后:
[7, 4, 3, 3, -1, -5, -7, -9]
重写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;
}
}
public static void main(String[] args) {
TreeMap<Person, String> pdata = new TreeMap<Person, String>();
pdata.put(new Person("张三", 30), "zhangsan");
pdata.put(new Person("李四", 20), "lisi");
pdata.put(new Person("王五", 10), "wangwu");
pdata.put(new Person("⼩红", 5), "xiaohong");
// 得到key的值的同时得到key所对应的值
Set<Person> keys = pdata.keySet();
for (Person key : keys) {
System.out.println(key.getAge() + "-" + key.getName());
}
}
输出:
5-⼩红
10-王五
20-李四
30-张三
七、集合框架底层数据结构总结
- 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)时,将链表转化为红⿊树,以减少搜索时间
- LinkedHashMap: LinkedHashMap 继承⾃ HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红⿊树组成。另外, LinkedHashMap 在上⾯结构的基础上,增加了⼀条双向链表,使得上⾯的结构可以保持键值对的插⼊顺序。同时通过对链表进⾏相应的操作,实现了访问顺序相关逻辑。详细可以查看: 《LinkedHashMap 源码详细分析(JDK1.8)》.
- Hashtable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的
- TreeMap: 红⿊树(⾃平衡的排序⼆叉树)