List(注重有序):List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象。
Set(注重独一无二):不允许重复的集合,不会有多个元素引用相同的对象。
Map(注重key-value):使用键值对进行存储。Map会维护与Key有关联的value,两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。
public interface RandomAccess {
}
查看源码我们发现实际上RandomAccess接口中什么都没有定义,所以RandomAccess接口只是一个标识接口,标识实现该接口的类具有随机访问功能。
在binarySearch()方法中,它要判断传入的list是否为RandomAccess
的实例,如果是,调用indexBinarySearch()
,如果不是则调用iteratorBinarySearch()
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}
为什么ArrayList实现了RandomAccess接口,而LinkedList没有实现?
ArrayList底层是数组,而LinkedList底层是链表,数组天然支持随机访问,时间复杂度为O(1),链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为O(n),所以不支持快速随机访问。
ArrayList实现了RandomAccess接口,表面它具有快速随机访问的功能,而RandomAccess只是标识。
下面再总结以下list的遍历方式选择:
实现RandomAccess接口的list,优先选择普通for循环,其次foreach。
未实现RandomAccess接口的list,优先选择iterator遍历(foreach底层也是通过iterator实现的),大size的list,千万不能采用普通for循环。
双向链表:包含两个指针,一个prev指向前一个结点,一个next指向后一个结点
双向循环链表:最后一个结点的next指向head,而head的prev指向最后一个结点,构成一个环
Vector类的所有方法都是同步的,可以由两个线程安全的访问同一个Vector对象,但是Vector再同步操作上会消耗大量的时间。
ArrayList不是同步的,所以不需要保证线程安全的情况下,建议采用ArrayList
JDK1.8之前:
HashMap底层采用数组加链表结合在一起使用,也就是链表散列。HashMap通过key的hashCode方法(经过扰动函数处理)计算出其hash值,然后通过(数组长度n - 1)&hash 判断元素存放位置,如果当前位置存在元素的话,就判断当前存在的元素与要存入的元素的hash值以及key是否相同,如果相同的话则覆盖,不相同通过拉链法解决冲突。
扰动函数指HashMap的hash方法,可以减少hash碰撞。
JDK1.8的HashMap的hash方法源码:
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
对比JDK1.7的hash方法源码:
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
相比起JDK1.8的hash方法,1.7的性能稍微差一些,因为其扰动了4次
将链表和数组结合,创建一个链表数组,数组中每一个都是一个链表。若遇到了hash冲突,则将冲突的值加到链表中即可。
而JDK1.8之后在解决hash冲突又有了较大的变化,当链表的长度大于8时,将该链表自动转换为红黑树,而当其红黑树长度小于6时,又自动转换为链表
这里值得一提的是,普通HashMap中,其链表为单链表,而LinkedHashMap是将链表变为双向链表的HashMap,底层实现并没有改变。
线程安全:HashMap是非线程安全的,而HashTable是线程安全的;HashTable内部的方法基本都通过synchronized关键字修饰。(如果想要保证线程安全,建议使用CurrentHashMap);
效率:因为线程安全问题,HashMap效率要比HashTable稍微高一些,此外,HashTable基本被淘汰了,不要再代码中使用它。
对Null key 还有 Null value的支持:HashMap中,null是可以作为key的,但是这样的key只能存在一个(这是因为HashMap中key不能重复);可以存在一个或多个key对应的value为null,但是在HashTable中,put(key,value)时只要key和value有其中一个为null,就抛出空指针异常。
初始容量大小和每次扩容大小的不同:
①创建时,如果不指定容量初始值,HashTable默认容量为11,之后每次扩容都变为原本的2n+1。HashMap默认容量为16(与ArrayList相同,毕竟是数组加链表构成的),之后每次扩容,容量为之前的2倍。
②创建时,如果指定了容量初始值,HashTable会直接采用给定的大小;HashMap会将其扩充为2的幂次方大小。
底层数据结构:JDK1.8以后,HashMap在解决hash冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转换为红黑树,以减少搜索时间。而HashTable并没有这样的机制。
HashMap中的带有初始容量的构造函数:
public HashMap(int initialCapacity, float loadFactor) {
//若初始容量小于0 则抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//若初始容量大于其定义的最大容量,则将最大容量作为初始容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//若装载因子小于等于0 抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
下面这个方法保证了 HashMap 总是使用2的幂作为哈希表的大小。
/**
* Returns a power of two size for the given target capacity.
*/
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;
}
为了能让HashMap存取高效,尽量减少碰撞,也就是要尽量把数据在数组上分配均匀,避免资源浪费。Hash值的取值范围-2147483648到2147483647,前后加起来大概40亿的映射空间,只要hash函数映射均匀松散,一般是很难出现hash碰撞的。但一个40亿长度的数组,是不可能存放于内存的,所以这个散列值并不能拿来直接使用,用之前还要先对数组的长度取模运算,得到余数才能用作数组下标进行存放,(n-1)&hash。
例如一个初始数组长度为16的HashMap,16-1=15,换算成二进制:1111,再于某个hash值进行&操作。
10100101 11000100 00100101
& 00000000 00000000 00001111
= 0101 //高位全部归零,只保留末四位
hash%length == hash&(n-1),这里有个前提条件时n必须为2的幂次方,而HashMap中之所以采用二进制&操作,是因为相比起%,&能提高运算效率。这就是为什么HashMap的长度为什么是2的幂次方(为了二进制&运算)。
那么这时问题又出现了,即便hash值分布再松散,进行模运算后得到的结果都大概率会出现碰撞。这时,就需要调用HashMap的扰动函数了。当HashMap数组长度为512的时候,也就是用掩码取低9位的时候,在没有扰动函数的情况下,发生了103次碰撞,接近30%。而在使用了扰动函数之后只有92次碰撞。碰撞减少了将近10%。看来扰动函数确实还是有功效的。
HashSet底层就是基于HashMap实现的
HashMap | HashSet |
---|---|
实现了Map接口 | 实现了Set接口 |
存储键值对 | 仅存储对象 |
调用put(key,value)在map中添加元素 | 调用add(Obj obj)向Set中添加元素 |
采用key值计算hashCode | 采用成员对象计算HashCode,对于两个对象来说hashCode可能相同,所以必须通过equals方法判断对象的相等性 |
当把对象加入到HashSet时,会先计算对象的hashCode值判断对象加入的位置,同时也会与其他加入的对象的hashCode比较,如果没有相同的hashCode则假设没有出现重复,如果发现重复的hashCode时,通过调用equals方法判断加入对象与具有相同hashCode的对象内存地址是否相同,如果二者相同,则HashSet不会让其加入成功。
hashCode方法与equals方法的相关规定:
二者区别主要体现在实现线程安全的方式上的不同:
HashTable
JDK1.7的ConcurrentHashMap:
JDK1.8的ConcurrentHashMap(TreeBin:红黑树结点,Node:链表结点):
Collection接口:
List:
Set:
4. HashSet(无序,唯一):基于HashMap实现,底层采用HashMap保存
5. LinkedHashSet:继承HashSet,且内部通过LinkedHashMap实现。
6. TreeSet(有序,唯一): 红黑树
Map接口:
7. HashMap:JDK1.8以前HashMap由数组+链表构成,数组为HashMap的主体,链表则主要为了解决hash冲突而存在的。JDK1.8在解决hash冲突时,当链表长度大于阈值(默认8),将链表转换为红黑树,当红黑树长度小于6时,将红黑树转换为链表。
8. LinkedHashMap:LinkedListHashMap继承了HashMap,所以底层仍然基于拉链式散列结构,即数组和链表或红黑树组成。此外,在以上结构的基础之上增加了双向链表,使得以上的结构可以保持键值对的插入顺序。同时对链表进行相应操作,实现了访问顺序的相关逻辑。
9. HashTable:没有红黑树结构的HashMap
10. TreeMap:红黑树