Java 集合的用途就是“保存对象”,并将其划分为两个不同的概念 ,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。
1)Collection。表示一个独立的元素序列,这些元素都服从一条或多条规则。List必须按照插入的顺序保存元素,Set不能有重复的元素。Queue按照队列的和规则来确定对象存放的顺序
2) Map。一组成对的“键值对”对象,使用键来查找值
List 接口 有序,可重复
- ArrayList,底层数据结构是数组,查询快,增删慢,线程不安全。初始长度为10,超出后扩容为原来的1.5倍。
- LinkedList,底层数据结构是双向链表,查询慢,增删快,线程不安全。
- Vector,底层数据结构是数组,线程不安全,速度比ArrayList慢。
Set接口 无序,唯一
-
HashSet,底层数据结构是Hash表(链地址法)。无序,唯一。查看HashSet源码发现其实是一个HashMap,初始大小16,负载因子0.75
/** * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has * default initial capacity (16) and load factor (0.75). */ public HashSet() { map = new HashMap<>(); }
-
LinkedHashSet,底层数据结构是Hash表(链地址法)。(FIFO插入有序,唯一),由链表保证元素有序,由哈希表保证元素唯一。是一个LinkedHashMap
public LinkedHashSet() { super(16, .75f, true); }
-
TreeSet,底层数据结构是红黑树。(唯一,有序)。是一个TreeMap
public TreeSet() { this(new TreeMap<E,Object>()); }
Map接口,有三个比较重要的实现类,分别是HashMap、HashTable、和TreeMap。
-
HashMap实现原理,HashMap底层使用Hash表(链地址法)数据结构
/** * The default initial capacity - MUST be a power of two. * 默认初始大小2^4=16,必须是2的次幂 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * The load factor used when none specified in constructor. * 默认负载因子0.75,元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大 一倍,然后重新计算每个元素在数组中的位置 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * The bin count threshold for using a tree rather than list for a * bin. Bins are converted to trees when adding an element to a * bin with at least this many nodes. The value must be greater * than 2 and should be at least 8 to mesh with assumptions in * tree removal about conversion back to plain bins upon * shrinkage. * 链表长度超过8将链表转换为红黑树 */ static final int TREEIFY_THRESHOLD = 8; /** * The bin count threshold for untreeifying a (split) bin during a * resize operation. Should be less than TREEIFY_THRESHOLD, and at * most 6 to mesh with shrinkage detection under removal. * 小于6将树转换为链表 */ static final int UNTREEIFY_THRESHOLD = 6; /** * The smallest table capacity for which bins may be treeified. * (Otherwise the table is resized if too many nodes in a bin.) * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts * between resizing and treeification thresholds. * 转换为树的最小长度 */ static final int MIN_TREEIFY_CAPACITY = 64;
可以看出HashMap的结构其实是这样的:
当我们往HashMap中put元素的时候,先根据key的hash值得到这个元素在数组中的位置(即下标),然后就可以把这个元素放到对应的位置中了。如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾(头插法)。从HashMap中get元素时,首先计算key的HashMap,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。从这里我们可以得知,如果每个位置上的链表只有一个元素,那么hashmap的get效率将是最高的。
HashMap的hash算法
在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过hashmap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。
理论上散列值是一个int型,如果直接拿散列值作为下标访问HashMap主数组的话,考虑到2进制32位带符号的int表值范围从-2147483648到2147483648。前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。HashMap扩容之前的数组初始大小才16。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来访问数组下标,源码中模运算是在这个getNode方法中完成的
“扰动函数”,取得初始hash值右移16位异或混合。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
然后把得到的散列值和数组长度做一个"与"操作
这也正好解释了为什么HashMap的数组长度要取2的整次幂。因为这样(数组长度-1)正好相当于一个“低位掩码”,“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。以初始长度16为例,16-1=15。二进制表示为00000000 00000000 0000 1111。
和散列值一 10100101 11000100 0010 0101 做“与”操作如下,结果就是截取了最低的四位值。
10100101 11000100 0010 0101
& 00000000 00000000 0000 1111
---------------------------------------------------------------
00000000 00000000 0000 0101 //高位全部归零,只保留末四位 index 为5
扩容后长度为32,32-1=31 二进制表示为00000000 00000000 0001 1111。
10100101 11000100 0010 0101
& 00000000 00000000 0001 1111
---------------------------------------------------------------
00000000 00000000 0000 0101 //高位全部归零,只保留末四位 index 还为5
和散列值二 10100101 11000100 0001 0101 做“与”操作如下,结果就是截取了最低的四位值。
10100101 11000100 0001 0101
& 00000000 00000000 0000 1111
---------------------------------------------------------------
00000000 00000000 0000 0101 //高位全部归零,只保留末四位 index 为5
扩容后长度为32,32-1=31 二进制表示为00000000 00000000 0001 1111。
10100101 11000100 0001 0101
& 00000000 00000000 0001 1111
---------------------------------------------------------------
00000000 00000000 0001 0101 //高位全部归零, index 变为5+16=21
因此,在扩容时,不需要重新计算元素的hash了,只需要在与n-1与操作后判断最高位是1还是0就好了。 是0,index不变,是1,index=index+数组原大小
注意:HashMap是线程不安全的,在并发情况下扩容时可能会造成环形链表,造成get时进入死循环
- Hashtable(线程安全)
Hashtable的底层实现和HashMap是一样的也是使用了Hash表(链地址法),
不过Hashtable是线程安全的,HashMap不是线程安全的(并发情况下扩容时会产生环形链表);
Hashtable不允许null值,HashMap允许null值(key和value都允许,最多允许一条记录的键为null)
- TreeMap(可排序)
TreeMap的底层实现使用了红黑树,有序的,
TreeMap有两种排序方式:
自然排序:TreeMap的所有key必须实现Comparable接口,而且所有的key应该是同一个类的对象,否则会抛出ClassCastException。
定制排序:创建TreeMap时,传入一个Comparator对象,该对象负责对TreeMap中的所有key进行排序。
- LinkedHashMap(记录插入顺序)
LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历
ps:
ConcurrentHashMap:采用了链表+红黑树+Segment+分段锁的方式实现 ,
- ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
- 内部结构:ConcurrentHashMap使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。如下图是ConcurrentHashMap的内部结构图: