一、概念
在编程时,可以使用数组来保存多个对象,但数组长度不可变化,一旦在初始化数组时指定了数组长度,这个数组长度就是不可变的。如果需要保存数量变化的数据,数组就有点无能为力了。而且数组无法保存具有映射关系的数据,如成绩表为语文——79,数学——80,这种数据看上去像两个数组,但这两个数组的元素之间有一定的关联关系。
为了保存数量不确定的数据,以及保存具有映射关系的数据(也被称为关联数组),JavaJava提供了集合类 。集合类主要负责保存、盛装其他数据,因此集合类也被称为容器类。Java 所有的集合类都位于 java.util 包下,提供了一个表示和操作对象集合的统一构架,包含大量集合接口,以及这些接口的实现类和操作它们的算法。
集合类和数组不一样,数组元素既可以是基本类型的值,也可以是对象(实际上保存的是对象的引用变量),而集合里只能保存对象(实际上只是保存对象的引用变量,但通常习惯上认为集合里保存的是对象)。
-
集合就是一个放数据的容器,准确的说是放数据对象引用的容器
-
集合类存放的都是对象的引用,而不是对象的本身
-
集合类型主要有3种:set(集)、list(列表)和map(映射)。
集合类的特点:
- 集合类这种框架是高性能的。对基本类集(动态数组,链接表,树和散列表)的实现是高效率的。一般人很少去改动这些已经很成熟并且高效的APl;
- 集合类允许不同类型的集合以相同的方式和高度互操作方式工作;
- 集合类容易扩展和修改,程序员可以很容易地稍加改造就能满足自己的数据结构需求
1.集合和数组的区别
a.对于对象:
- 集合用于存储对象的容器,对象是用来封装数据,对象多了也需要存储集中式管理。
- 和数组对比对象的大小不确定。因为集合是可变长度的。数组需要提前定义大小
b.存储方式
- 数组是固定长度的;集合可变长度的。
- 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
- 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。
2.常见集合类
Map接口和Collection接口是所有集合框架的父接口:
- Collection接口的子接口包括:Set接口和List接口;
- Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等;
- List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等;
- Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等。
集合关系结构图如下:
List、Set和Map的区别:
List:有序可重复集合;
Set:代表无序不可重复集合;
Map接口派生:存储key-value对的集合,可根据元素的key来访问value。
二、Collection
java.util.collection是单值集合操作的最大的父接口,在该接口中定义了所有的单值数据的处理操作。
Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements)。Java SDK不提供直接继承自Collection的类,Java SDK提供的类都是继承自Collection的“子接口”如List和Set。
所有实现Collection接口的类都必须提供两个标准的构造函数:无参数的构造函数用于创建一个空的Collection,有一个 Collection参数的构造函数用于创建一个新的Collection,这个新的Collection与传入的Collection有相同的元素。后一个构造函数允许用户复制一个Collection。
1.List
List接口常用的实现类有:ArrayList、LinkedList、Vector。
特点:
- 集合中的元素允许重复
- 集合中的元素是有顺序的,各元素插入的顺序就是各元素的顺序
- 集合中的元素可以通过索引来访问或者设置
List常用API:
返回值(出参) | 方法名(形参列表) | 作用 |
---|---|---|
boolean | add(E o) | 向列表的尾部追加指定的元素(可选操作)。 |
boolean | addAll(Collection<? extends E> c) | 追加指定 collection 中的所有元素到此列表的结尾,顺序是指定 collection 的迭代器返回这些元素的顺序(可选操作)。 |
boolean | addAll(int index, Collection<? extends E> c) | 将指定 collection 中的所有元素都插入到列表中的指定位置(可选操作)。 |
void | clear() | 从列表中移除所有元素(可选操作)。 |
boolean | contains(Object o) | 如果列表包含指定的元素,则返回 true。 |
boolean | containsAll(Collection<?> c) | 如果列表包含指定 collection 的所有元素,则返回 true。 |
boolean | equals(Object o) | 比较指定的对象与列表是否相等。 |
E | get(int index) | 返回列表中指定位置的元素。 |
int | hashCode() | 返回列表的哈希码值。 |
int | indexOf(Object o) | 返回列表中首次出现指定元素的索引,如果列表不包含此元素,则返回 -1。 |
boolean | isEmpty() | 如果列表不包含元素,则返回 true。 |
Iterator<E> | iterator() | 返回以正确顺序在列表的元素上进行迭代的迭代器。 |
int | lastIndexOf(Object o) | 返回列表中最后出现指定元素的索引,如果列表不包含此元素,则返回 -1。 |
ListIterator<E> | listIterator() | 回列表中元素的列表迭代器(以正确的顺序)。 |
ListIterator<E> | listIterator(int index) | 返回列表中元素的列表迭代器(以正确的顺序),从列表的指定位置开始。 |
E | remove(int index) | 移除列表中指定位置的元素(可选操作)。 |
boolean | remove(Object o) | 移除列表中出现的首个指定元素(可选操作)。 |
boolean | removeAll(Collection<?> c) | 从列表中移除指定 collection 中包含的所有元素(可选操作)。 |
boolean | retainAll(Collection<?> c) | 仅在列表中保留指定 collection 中所包含的元素(可选操作)。 |
E | set(int index, E element) | 用指定元素替换列表中指定位置的元素(可选操作)。 |
int | size() | 返回列表中的元素数。 |
List<E> | subList(int fromIndex, int toIndex) | 返回列表中指定的 fromIndex(包括 )和 toIndex(不包括)之间的部分视图。 |
Object[] | toArray() | 返回以正确顺序包含列表中的所有元素的数组。 |
<T> T[] | toArray(T[] a) | 返回以正确顺序包含列表中所有元素的数组;返回数组的运行时类型是指定数组的运行时类型。 |
a.ArrayList
ArrayList是一个动态数组,也是我们最常用的集合,是List类的典型实现。
它允许任何符合规则的元素插入甚至包括null,每一个ArrayList都有一个初始容量(10),该容量代表了数组的大小。
随着容器中的元素不断增加,容器的大小也会随着增加,在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作。
所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。
ArrayList擅长于随机访问,同时ArrayList是非同步的。
add原理:
//1.使用add()方法
public boolean add(E e){
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
//2.确定要创建的数组大小
private void ensureCapacityInternal(int minCapacity){
if(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA){
minCapacity = Math.max(DEFAULT_CAPACITY,minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity){
modCount++;
if(minCapacity - elementData.length > 0)
grow(minCapacity);
}
//3.创建数组,先确定添加元素大小,再将元素复制进新的数组
private void grow(int minCapacity){
//overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if(newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
b.LinkedList
LinkedList是采用双向循环链表实现,LinkedList是List接口的另一个实现,除了可以根据索引访问集合元素外,LinkedList还实现了Deque接口,可以当作双端队列来使用,也就是说,既可以当作“栈”使用,又可以当作队列使用。
内部类:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
add方法:
public void add(E e) {
checkForComodification();
lastReturned = null;
if (next == null)
linkLast(e);
else
linkBefore(e, next);
nextIndex++;
expectedModCount++;
}
//checkForComodification方法
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
//linkLast方法
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
//linkBefore方法
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
c.Vectory
与ArrayList相似,但是Vector是同步的,它的操作与ArrayList几乎一样。
2.知识点
三者的区别:
- ArrayList
- 优点: 底层数据结构是数组,查询快,增删慢。
- 缺点: 线程不安全,效率高
- Vector
- 优点: 底层数据结构是数组,查询快,增删慢。
- 缺点: 线程安全,效率低
- LinkedList
- 优点: 底层数据结构是链表,查询慢,增删快。
- 缺点: 线程不安全,效率高
ArrayList的随机访问模式:
ArrayList 实现了RandomAccess接口,因此查找的时候非常快。
ArrayList 和 LinkedList 的区别是什么?
- 数据结构实现: ArrayList是动态数组的数据结构实现,而LinkedList 是双向链表的数据结构实现;
- 随机访问效率: ArrayList 比 LinkedList在随机访问的时候效率要高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找;
- 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList效率要高,因为ArrayList增删操作要影响数组内的其他数据的下标;
- 内存空间占用: LinkedList 比 ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素;
- 线程安全: ArrayList和LinkedList都是不同步的,也就是不保证线程安全;
- 综合来说,在需要频繁读取集合中的元素时,更推荐使用ArrayList,而在插入和删除操作较多时,更推荐使用LinkedList;
- LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
2.Set
Set接口继承Collection接口,而且它不允许集合中存在重复项。所有原始方法都是现成的,没有引入新方法。具体的Set实现类以来添加的对象的equals()方法来检查等同性。
Set接口中定义的方法是Set是无序(无下标),不重复的,当使用(jdk1.9才有这个方法,1.8没有)of() 这个新方法的时候如何发现集合中存在重复的元素则会直接抛出异常,Set集合的常规使用形式一定是依靠子类进行实例化的,Set接口中有三个具体的实现类分别是:
-
HashSet(散列集)
-
TreeSet(树形集)
-
LinkedHashSet(链式散列集)
常用API:
返回值(出参) | 方法名(形参列表) | 作用 |
---|---|---|
boolean | add(E o) | 如果 set 中尚未存在指定的元素,则添加此元素(可选操作)。 |
boolean | addAll(Collection<? extends E> c) | 如果 set 中没有指定 collection 中的所有元素,则将其添加到此 set 中(可选操作)。 |
void | clear() | 移除 set 中的所有元素(可选操作)。 |
boolean | contains(Object o) | 如果 set 包含指定的元素,则返回 true。 |
boolean | containsAll(Collection<?> c) | 如果此 set 包含指定 collection 的所有元素,则返回 true。 |
boolean | equals(Object o) | 比较指定对象与此 set 的相等性。 |
int | hashCode() | 返回 set 的哈希码值。 |
boolean | isEmpty() | 如果 set 不包含元素,则返回 true。 |
Iterator<E> | iterator() | 返回在此 set 中的元素上进行迭代的迭代器。 |
boolean | remove(Object o) | 如果 set 中存在指定的元素,则将其移除(可选操作)。 |
boolean | removeAll(Collection<?> c) | 移除 set 中那些包含在指定 collection 中的元素(可选操作)。 |
boolean | retainAll(Collection<?> c) | 仅保留 set 中那些包含在指定 collection 中的元素(可选操作)。 |
int | size() | 返回 set 中的元素数(其容量)。 |
Object[] | toArray() | 返回一个包含 set 中所有元素的数组。 |
<T> T[] | toArray(T[] a) | 返回一个包含 set 中所有元素的数组;返回数组的运行时类型是指定数组的类型。 |
a.HashSet
HashSet底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素,元素的唯一性是靠所存储元素类型是否重写hashCode()和equals()方法来保证的,如果没有重写这两个方法,则无法保证元素的唯一性。
散列集HashSet是一个用于实现Set接口的具体类,可以使用它的无参构造方法来创建空的散列集,也可以由一个现有的集合创建散列集。在散列集中,有两个名词需要关注,初始容量和客座率。客座率是确定在增加规则集之前,该规则集的饱满程度,当元素个数超过了容量与客座率的乘积时,容量就会自动翻倍。
b.TreeSet
TreeSet扩展自AbstractSet,并实现了NavigableSet,AbstractSet扩展自AbstractCollection,树形集是一个有序的Set,其底层是一颗树,这样就能从Set里面提取一个有序序列了。在实例化TreeSet时,我们可以给TreeSet指定一个比较器Comparator来指定树形集中的元素顺序。树形集中提供了很多便捷的方法。
c.LinkedHashSet
LinkedHashSet是用一个链表实现来扩展HashSet类,它支持对规则集内的元素排序。HashSet中的元素是没有被排序的,而LinkedHashSet中的元素可以按照它们插入规则集的顺序提取。
d.HashSet和TreeSet
“集合框架”支持 Set
接口两种普通的实现:HashSet
和TreeSet
。在更多情况下,您会使用 HashSet
存储重复自由的集合。考虑到效率,添加到 HashSet
的对象需要采用恰当分配散列码的方式来实现hashCode()
方法。虽然大多数系统类覆盖了 Object
中缺省的hashCode()
实现,但创建您自己的要添加到 HashSet
的类时,别忘了覆盖 hashCode()
。当您要从集合中以有序的方式抽取元素时,TreeSet
实现会有用处。为了能顺利进行,添加到TreeSet
的元素必须是可排序的。 “集合框架”添加对 Comparable
元素的支持,在排序的“可比较的接口”部分中会详细介绍。我们暂且假定一棵树知道如何保持java.lang
包装程序器类元素的有序状态。一般说来,先把元素添加到 HashSet
,再把集合转换为TreeSet
来进行有序遍历会更快。
为优化 HashSet
空间的使用,您可以调优初始容量和负载因子。TreeSet
不包含调优选项,因为树总是平衡的,保证了插入、删除、查询的性能为log(n)
。
e.知识点
HashSet如何保证数据不重复
- 向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles方法比较。
- HashSet中的add )方法会使用HashMap 的put()方法。
- HashMap的 key是唯一的,由源码可以看出 HashSet添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复(HashMap比较key是否相等是先比较hashcode 再比较equals ) 。
HashSet部分源码如下:
private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
// 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
return map.put(e, PRESENT)==null;
}
三者的区别:
- HashSet
- 底层其实是包装了一个HashMap实现的
- 底层数据结构是数组+链表 + 红黑树
- 具有比较好的读取和查找性能, 可以有null 值
- 通过equals和HashCode来判断两个元素是否相等
- 非线程安全
- LinkedHashSet
- 继承HashSet,本质是LinkedHashMap实现
- 底层数据结构由哈希表(是一个元素为链表的数组)和双向链表组成。
- 有序的,根据HashCode的值来决定元素的存储位置,同时使用一个链表来维护元素的插入顺序
- 非线程安全,可以有null 值
- TreeSet
- 是一种排序的Set集合,实现了SortedSet接口,底层是用TreeMap实现的,本质上是一个红黑树原理
- 排序分两种:自然排序(存储元素实现Comparable接口)和定制排序(创建TreeSet时,传递一个自己实现的Comparator对象)
- 正常情况下不能有null值,可以重写Comparable接口 局可以有null值了。
3.Iterable接口
Iterable是一个可迭代接口,与之前版本相比,增加了forEach迭代和获取Spliterator方法。
Iterable提供获取Iterator迭代器方法,用以支持集合遍历。
Iterable提供获取Spliterator可分割迭代器方法,用以支持集合的并发遍历。
常用API:
返回值(出参) | 方法名(形参列表) | 作用 |
---|---|---|
boolean | hasNext() | 如果有下一个元素,则返回true |
E | next() | 返回迭代器的下一个元素 |
default void | remove() | 删除迭代器上次返回的元素 |
default void | forEachRemaining(Consumer<? super E> action) | 对剩下的元素执行给定消费器的accept方法 |
三、Map
Map接口不是Collection接口的继承。而是从自己的用于维护键-值关联的接口层次结构入手。按定义,该接口描述了从不重复的键到值的映射。
我们可以把这个接口方法分成三组:改变、查询和提供可选试图。
改变操作允许您从映射中添加和出去键-值对。键和值都可以为null。但是,不能把Map作为一个键或值添加给自身。
常用API:
返回值(出参) | 方法名(形参列表) | 作用 |
---|---|---|
void | put(Object key,Object value) | 给map集合中添加键值对 |
Object | get(Object key) | 查询key对应的value值,返回value值 |
void | clear() | 移除map里所有的映射关系 |
boolean | containsKey(Object key) | 调用了equals方法,查询key中是否包含某个元素 |
boolean | containsValue(Object value) | 查询value中是否包含某个元素 |
boolean | isEmpty() | 判断map集合中是否为空,若为空则返回true |
Set | keySet() | 获取map集合中所有的key,并将所有的key中存入set集合中 |
void | remove(Object key) | 利用key删除map集合中的元素 |
int | size() | 获取map集合中键值对的个数 |
Collection | values() | 获取map集合中所有的value值,并将value值存入Collection集合中返回 |
Set(Map.Entry<K,V>) | entrySet() | 将map集合转换为set集合,set集合中元素的类型是Map.entry<K,V> |
Set<K> | keySet | 将map中的key存储为一个Set集合 |
1.常用集合
a.HashMap
HashMap是基于哈希表的Map接口的非同步实现。元素以键值对的形式存放,并且允许null键和null值,因为key值唯一(不能重复),因此,null键只有一个。另外,hashmap不保证元素存储的顺序,是一种无序的,和放入的顺序并不相同(此类不保证映射的顺序,特别是它不保证该顺序恒久不变)。HashMap是线程不安全的。
b.TreeMap
TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序(自然顺序),也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。
c.HashTable
Hashtable和HashMap从存储结构和实现来讲有很多相似之处,不同的是它承自Dictionary类,而且是线程安全的,另外Hashtable不允许key和value为null,并发性不如ConcurrentHashMap。
Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
2.HashMap源码分析
HashMap的数据存储结构:
HashMap由数组(键值对entry组成的数组主干)+ 链表(元素太多时为解决哈希冲突数组的一个元素上多个entry组成的链表)+ 红黑树(当链表的元素个数达到8链表存储改为红黑树存储)进行数据的存储。
HashMap采用table数组存储Key-Value的,每一个键值对组成了一个Node节点(JDK1.7为Entry实体,因为jdk1.8加入了红黑树,所以改为Node)。Node节点实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Node节点,以此来解决Hash冲突的问题。
成员变量:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 默认初始容量大小:2的4次方 16
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量:2的30次方,Integer.MAX_VALUE
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认加载因子
static final int TREEIFY_THRESHOLD = 8; //计数阈值至少为8转化为使用树而不是列表
static final int UNTREEIFY_THRESHOLD = 6; //计数阈值小于6反树化,即红黑树转为列表
static final int MIN_TREEIFY_CAPACITY = 64; //可对桶进行树化的最小表容量
transient Node<K,V>[] table; //表在第一次使用时初始化,大小调整为必要的。在分配时,长度总是2的幂。在某些操作中,我们也允许长度为零。目前不需要的引导机制。)
transient Set<Map.Entry<K,V>> entrySet; //保存缓存的entrySet()
transient int size; //包含的键值映射的元素数量
transient int modCount; //HashMap在结构上被修改的次数,用于快速失败机制
int threshold; // 调整大小的阈值(容量*负载因子)
final float loadFactor; //哈希表扩容使用的负载因子
map.put(k,v)实现原理:
- 判断键值对数组table[i]是否为空或者为null,否则执行resize()进行扩容;
- 根据键值key计算hash值得到插入的数组索引 i ,如果table[i] == null ,直接新建节点添加即可,转入6,如果table[i] 不为空,则转向3;
- 判断table[i] 的首个元素是否和key一样,如果相同(hashCode和equals)直接覆盖value,否则转向4;
- 判断table[i] 是否为treeNode,即table[i]是否为红黑树,如果是红黑树,则直接插入键值对,否则转向5;
- 遍历table[i] ,判断链表长度是否大于8,大于8的话把链表转换成红黑树,进行插入操作,否则进行链表插入操作;便利时遇到相同key直接覆盖value;
- 插入成功后,判断实际存在的键值对数量size是否超过了threshold,如果超过,则扩容;
源码分析:
put():传入key、value和hash提供的索引h。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
hash():通过一个高位的异或运算将hash值散列开,目的是让key均匀的铺在broker中。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
putVal:真正进行存值的方法。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果数组为空,进行 resize() 初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash相当于取模,获取数组的索引位置
// 如果计算的位置上Node不存在,直接创建节点插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 如果计算的位置上Node 存在,链表或者红黑树处理
Node<K,V> e; K k;
// 如果已存在的key和传入的key一模一样,则需要覆盖
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果 index 位置元素已经存在,且是红黑树
else if (p instanceof TreeNode)
// 将元素put到红黑树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 否则如果是链表的情况,对链表进行遍历,并统计链表长度
for (int binCount = 0; ; ++binCount) {
// 如果节点链表的next为空
if ((e = p.next) == null) {
// 找到节点链表中next为空的节点,创建新的节点插入
p.next = newNode(hash, key, value, null);
// 如果节点链表中数量超过TREEIFY_THRESHOLD(8)个,转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 树化
treeifyBin(tab, hash);
break;
}
// 判断节点链表中的key和传入的key是否一样
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 如果一样的话,退出
break;
p = e;
}
}
// 如果存在相同key的节点e不为空
if (e != null) { // existing mapping for key
V oldValue = e.value;
// onlyIfAbsent 表示是否仅在 oldValue 为 null 的情况下更新键值对的值
if (!onlyIfAbsent || oldValue == null)
// 设置新的值
e.value = value;
afterNodeAccess(e);
// 返回旧的结果
return oldValue;
}
}
++modCount;
// 当前大小大于临界大小,扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
map.get(k)实现原理:
- 指定key通过hash函数得到key的hash值;
- 调用内部方法getNode(),得到桶号(一般为hash值对桶数求摸);
- 比较桶的内部元素是否和key相等,如不相等,则没有找到,相等,则取出相等记录的value;
- 如果得到key所在桶的头结点恰好是红黑树节点,就调用红黑树节点的getTreeNode()方法,否则就遍历链表节点。getTreeNode()方法通过调用树形节点的find()方法进行查找。由于之前添加时已经保证这个树是有序的,因此查找时基本就是折半查找,效率高;
- 如果对比节点的哈希值和要查找的哈希值相等,就会判断key是否相等,相等就直接返回;不相等就从子树中递归查找;
源码分析:
//put
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
//getNode
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
3.知识点
a.HashMap线程不安全:
1.8之前采用的是数组+链表的存储格式,对于产生哈希冲突的对象使用的是头插法,这种插入方式在扩容时会将链表反转,在多线程并发下会导致索引丢失、死循环问题。
在1.8采用的是数组+链表+红黑树的存储格式,通过红黑树这种特殊的平衡二叉树解决了哈希冲突时插入的链表过长导致的性能变差问题,同时优化插入方式为尾插法,解决了死环问题,同时也产生了新的问题,在多线程并发情况下,会导致索引覆盖、数据丢失问题。