整体上两大类接口,一类是Collection接口,另一类是Map接口。
Collection接口包含两大类:List接口和Set接口。
List接口:存储一组不唯一且按插入顺序排序的对象,可以操作索引。
Set接口:存储一组唯一且无序的对象。
Map接口:以键值对的形式存储元素,键(key)是唯一的。
1.如果元素可以重复,实现List接口
(1)需要查询快
(a)线程安全
Vector:由数组实现,访问元素的效率比较高,删除和添加元素效率低。
(b)线程不安全
ArrayList:由数组实现,访问元素的效率比较高,删除和添加元素效率低。
(2)需要增删快
(a)线程不安全
LinkedList:由链表实现,插入、删除元素效率比较高,访问效率比较低。
2.如果元素需要唯一不重复,实现Set接口
(1)需要查询快
(a)线程不安全
HashSet:由哈希表实现,使用了HashTable。添加、查询、删除元素的效率都很高,缺点是元素无序。通过hashcode与equals方法确保元素的唯一。
TreeSet:由二叉树实现。查询效率高,且元素有序的。存放自定义类型的对象需要实现Comparable接口,重写compareTo方法,提供对象排序的方式。
LinkedHashSet:由哈希表实现元素的存储,由链表实现元素的顺序。添加、查询、删除元素的效率都高,且元素都是有序的。
(2)需要排序
(a)线程不安全 TreeSet
3.通过键值对存取,实现map接口
(1)需要查询快
(a)线程不安全
HashMap:由hash表实现,底层是Hashtable,添加、查询、删除元素的效率都很高。
LinkedHashMap:由哈希表实现元素的存储,由链表实现元素的顺序。添加、查询、删除元素的效率都高,且元素都是有序的。
TreeMap:由二叉树实现。查询效率高,且元素有序的。
Hashtable:任何非null对象都可以用作键或值。为了成功地在哈希表中存储和获取对象,用作键的对象必须实现hashCode和equals方法。
ArrayList和LinkedList的区别。
(1)ArryaList和LinkedList都是线程不安全的。
(2)Arraylist底层是由数组实现的,数组的优点是通过下标访问,查询效率高,时间复杂度为O(1);增删因为牵扯到数组复制操作,所以增删效率不高。
(3)LinkedList的底层是由链表实现的,链表的优点是每个结点都有指向下条数据的指针,所以增删时只需要修改指针地址,查询时因为链表的数据结构在内存空间内是不连续的的且没有索引,所以查询效率低,时间复杂度为O(n)。 五.HashMap实现原理
HashMap先在内存中创建一个数组,然后每个元素都会被计算hash值,然后通过hash值再计算,得到其在数组中的位置,由于hash值的特殊性,可能会存在两个元素被安排在数组中重复的位置,这时候就创建一个链表,来维护重复的数据。
搞清楚HashMap,首先需要知道HashMap是什么,即它的存储结构-字段;其次弄明白它能干什么,即它的功能实现-方法。
1.从结构实现来讲,HashMap是数组+链表+红黑树实现的。
(1)从源码可知,HashMap类中有一个非常重要的字段,就是Node[]table,即哈希桶数组,是一个Node的数组。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //用来定位数组索引位置
final K key;
V value;
Node<K,V> next; //链表的下一个node
Node(int hash, K key, V value, Node<K,V> next) { ... }
public final K getKey(){ ... }
public final V getValue() { ... }
public final String toString() { ... }
public final int hashCode() { ... }
public final V setValue(V newValue) { ... }
public final boolean equals(Object o) { ... }
}
Node是HashMap的一个内部类,实现了Map.Entry接口,本质上就是一个映射(键值对)。
(2)HashMap使用哈希表存储,使用哈希表来解决哈希冲突。Java中的HashMap采用了链地址法。数组加链表的结合。在每个数组元素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表。例如程序执行下面代码:
map.put("美团","小美");
系统将调用"美团"这个key的hashCode()方法得到其hashCode()值,然后通过Hash算法的后两步运算(高位运算和取模运算)来定位该键值对的存储位置,有时两个key会定位到相同的位置,表示发生了Hash碰撞。当然Hash算法计算结构越分散均匀,Hash碰撞的概率就越小,map的存取效率就越高。好的Hash算法和扩容机制来控制map使得Hash碰撞的概率变小,哈希桶数组Node[]table占用空间又少。
HashMap的默认构造函数源码,
int threshold; //所能容纳的key-value的极限
final float loadFactor; //负载因子
int modCount;
int size;
Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值为0.75),threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold=length*Load factor。
在HashMap中,哈希桶数组table的长度length大小必须为2的n次方。
2.功能实现—方法
(1)确定哈希桶数组索引位置
使用hash算法避免了遍历链表,大大优化了查询的效率。HashMap定位数组索引位置,直接决定了hash方法的离散性能。
方法一:
static final int hash(Object key){
int h;
//h = key.hashCode();取hashcode值
//h^(h>>>16); 高位参与运算
return (key==null)?0:(h=key.hashCode())^(h>>>16);
}
方法二:
static int indexFor(int h,int length){
return h&(length-1);
}
Hash算法本质:取key的hashCode、高位运算、取模运算。
(2)put方法的详细执行
(a)判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;——判断数组是否为空
(b)根据键值key计算hash值得到插入的数组索引i,如果table[i]=null,直接新建节点添加,转向(f),如果table[i]不为空,转向(c)——计算hash值判断索引i的位置
(c)判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向(d),相同指的是hashcode以及equals;——判断table[i]的首个元素是否和key一样
(d)判断table[i]是否为treeNode,判断是否是红黑树
(e)遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树
(f)插入成功后,判断实际存在的键值对数量size是否超过了最大容量threshold,如果超过,进行扩容。
(3)扩容过程
HashTable、HashMap、ConcurrentHashMap的区别?
HashMap线程不安全的出现场景?
HashMap put方法存放数据时是怎么判断是否重复的?
JDK7和JDK8 中HashMap的实现有什么区别?
下面先说这三个Map的区别: HashTable
底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
初始size为11,扩容:newsize = olesize*2+1
计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
HashMap
底层数组+链表实现,可以存储null键和null值,线程不安全
初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
计算index方法:index = hash & (tab.length – 1)
HashMap的初始值还要考虑加载因子: 哈希冲突:若干Key的哈希值按数组大小取模后,如果落在同一个数组下标上,将组成一条Entry链,对Key的查找需要遍历Entry链上的每个元素执行equals()比较。 加载因子:为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容。因此,如果预估容量是100,即需要设定100/0.75=134的数组大小。 空间换时间:如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。
HashMap的内部结构可以看作是数组(Node<K,V>[] table)和链表的复合结构,数组被分为一个个桶(bucket),通过哈希值决定了键值对在这个数组中的寻址(哈希值相同的键值对,则以链表形式存储),如下图所示。有一点需要注意,如果链表大小超过阈值(TREEIFY_THRESHOLD,8),图中的链表就会被改造为树形结构。
HashMap和Hashtable都是用hash算法来决定其元素的存储,因此HashMap和Hashtable的hash表包含如下属性:
容量(capacity):hash表中桶的数量
初始化容量(initial capacity):创建hash表时桶的数量,HashMap允许在构造器中指定初始化容量
尺寸(size):当前hash表中记录的数量
负载因子(load factor):负载因子等于“size/capacity”。负载因子为0,表示空的hash表,0.5表示半满的散列表,依此类推。轻负载的散列表具有冲突少、适宜插入与查询的特点(但是使用Iterator迭代元素时比较慢)
除此之外,hash表里还有一个“负载极限”,“负载极限”是一个0~1的数值,“负载极限”决定了hash表的最大填满程度。当hash表中的负载因子达到指定的“负载极限”时,hash表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,这称为rehashing。
HashMap和Hashtable的构造器允许指定一个负载极限,HashMap和Hashtable默认的“负载极限”为0.75,这表明当该hash表的3/4已经被填满时,hash表会发生rehashing。
“负载极限”的默认值(0.75)是时间和空间成本上的一种折中:
较高的“负载极限”可以降低hash表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作(HashMap的get()与put()方法都要用到查询)
较低的“负载极限”会提高查询数据的性能,但会增加hash表所占用的内存开销
程序猿可以根据实际情况来调整“负载极限”值,但一般不建议轻易修改,因为JDK自身的默认负载因子是非常符合通用场景需求的。如果确实需要修改,建议不要设置超过0.75,因为会显著增加冲突,降低HashMap的性能。
根据容量和负载因子的关系,我们可以预先设置合适的容量大小,具体数值我们可以根据扩容发生的条件来做简单预估,计算公式如下:
负载因子 * 容量 > 元素数量
所以预先设置的容量需要大于“预估元素数量/负载因子”,同时它是2的幂数。
上面提到HashMap会树化,为什么会这样呢?
本质上这是个安全问题。因为在元素放置过程中,如果一个对象哈希冲突,都被放置到同一个桶里,则会形成一个链表,我们知道链表查询是线性的,会严重影响存取的性能。而在现实世界,构造哈希冲突的数据并不是非常复杂的事情,恶意代码就可以利用这些数据大量与服务器端交互,导致服务器端CPU大量占用,这就构成了哈希碰撞拒绝服务攻击,国内一线互联网公司就发生过类似攻击事件。
ConcurrentHashMap
底层采用分段的数组+链表实现,线程安全
通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
Hashtable和HashMap都实现了Map接口,但是Hashtable的实现是基于Dictionary抽象类的。Java5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
HashMap基于哈希思想,实现对数据的读写。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来存储值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞时,对象将会储存在链表的下一个节点中。HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时,它们会储存在同一个bucket位置的链表中,可通过键对象的equals()方法来找到键值对。如果链表大小超过阈值(TREEIFY_THRESHOLD,8),链表就会被改造为树形结构。
在HashMap中,null可以作为键,这样的键只有一个,但可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示HashMap中没有该key,也可以表示该key所对应的value为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个key,应该用containsKey()方法来判断。而在Hashtable中,无论是key还是value都不能为null。
Hashtable是线程安全的,它的方法是同步的,可以直接用在多线程环境中。而HashMap则不是线程安全的,在多线程环境中,需要手动实现同步机制。
Hashtable与HashMap另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。
先看一下简单的类图:
从类图中可以看出来在存储结构中ConcurrentHashMap比HashMap多出了一个类Segment,而Segment是一个可重入锁。
ConcurrentHashMap是使用了锁分段技术来保证线程安全的。
锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。
ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。