前言
在初次看到 HashMap 时,都惊奇于其 O(1) 的 get 操作的时间复杂度。当时在我们已知的数据结构中,只有基于下标访问数组时,才能提供 O(1) get 操作的时间复杂度。
实际上,HashMap 所提供的 O(1) 是平均时间复杂度,大多数情况下保证 O(1) 。其实极端情况下,有可能退化为 O(N) 的时间复杂度噢,这又是为什么呢?
hashMap的属性
//初始化的容量 一般是调用无参构造方法后第一次put时初始化容量(延迟初始化)
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.
*/
//HashMap的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
*/
//默认的加载因子 当然也可以指定
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.
*/
//树化的阈值
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.
*/
//非树化的阈值其实就是退化链表
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;
```java
//底层存储的数组
transient Node<K,V>[] table;
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
*/
//调用 `#entrySet()` 方法后的缓存
transient Set<Map.Entry<K,V>> entrySet;
/**
key-value 的键值对数量
* The number of key-value mappings contained in this map.
*/
transient int size;
/**
HashMap 的修改次数
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;
/**
阀值 当 size 超过threshold时, 会进行扩容
*
* The next size value at which to resize (capacity * load factor).
*
* @serial
*/
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
int threshold;
/**
* The load factor for the hash table.
*
* @serial
* 扩容因子
*/
final float loadFactor;
**看下table数组组成**
```java
static class Node<K,V> implements Map.Entry<K,V> {
/**
* 哈希值
*/
final int hash;
/**
* KEY 键
*/
final K key;
/**
* VALUE 值
*/
V value;
/**
* 下一个节点
*/
Node<K,V> next;
// ... 省略实现方法
}
实现了 Map.Entry 接口,该接口定义在 java.util.Map 接口中
hash + key + value 属性,定义了 Node 节点的 3 个重要属性。
next 属性,指向下一个节点。通过它可以实现 table 数组的每一个位置可以形成链表。
构造方法
① #HashMap() 延迟初始化
#HashMap() 构造方法,创建一个初始化容量为 16 的 HashMap 对象。代码如下:
/**
* 默认的初始化容量
*
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 默认加载因子为 0.75
*
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* Constructs an empty {@code HashMap} with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
初始化 loadFactor 为 DEFAULT_LOAD_FACTOR = 0.75 。
在该构造方法上,我们并没有看到 table 数组的初始化。它是延迟初始化,在我们开始往 HashMap 中添加 key-value 键值对时,在 #resize() 方法中才真正初始化。
HashMap(int initialCapacity)
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
内部调用 #HashMap(int initialCapacity, float loadFactor) 构造方法。
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 避免 initialCapacity 超过 MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
// 计算 threshold 阀值
this.threshold = tableSizeFor(initialCapacity);
}
HashMap(Map<? extends K, ? extends V> m)
public HashMap(Map<? extends K, ? extends V> m) {
// 设置加载因子
this.loadFactor = DEFAULT_LOAD_FACTOR;
// <X> 批量添加到 table 中
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
// <1>
if (s > 0) {
// 如果 table 为空,说明还没初始化,适合在构造方法的情况
if (table == null) { // pre-size
// 根据 s 的大小 + loadFactor 负载因子,计算需要最小的 tables 大小
float ft = ((float)s / loadFactor) + 1.0F; // + 1.0F 的目的,是因为下面 (int) 直接取整,避免不够。
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// 如果计算出来的 t 大于阀值,则计算新的阀值
if (t > threshold)
threshold = tableSizeFor(t);
// 如果 table 非空,说明已经初始化,需要不断扩容到阀值超过 s 的数量,避免扩容
} else {
// Because of linked-list bucket constraints, we cannot
// expand all at once, but can reduce total resize
// effort by repeated doubling now vs later
while (s > threshold && table.length < MAXIMUM_CAPACITY)
resize(); // 扩容
}
// <2> 遍历 m 集合,逐个添加到 HashMap 中。
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
哈希函数
性能足够高。因为基本 HashMap 所有的操作,都需要用到哈希函数。
对于计算出来的哈希值足够离散,保证哈希冲突的概率更小。
在 HashMap 中,#hash(Object key) 静态方法,计算 key 的哈希值。代码如下:
static final int hash(Object key) {
int h;
// h = key.hashCode() 计算哈希值
// ^ (h >>> 16) 高 16 位与自身进行异或计算,保证计算出来的 hash 更加离散
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
高效性:从整个计算过程上来说,^ (h >>> 16) 只有这一块的逻辑,两个位操作,性能肯定是有保障的。那么,如果想要保证哈希函数的高效性,就需要传入的 key 自身的 Object#hashCode() 方法的高效即可。位运算比快余块
位运算是更底层的计算机运算方式,加减乘除是建立在位运算的基础上实现的。层次越高效率越低。这是一种比较浅显的认识,如果细追得用指令集解释
put(K key, V value) 添加单个元素
public V put(K key, V value) {
// hash(key) 计算哈希值
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; // tables 数组
Node<K,V> p; // 对应位置的 Node 节点
int n; // 数组大小
int i; // 对应的 table 的位置
// <1> 如果 table 未初始化,或者容量为 0 ,则进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize() /*扩容*/ ).length;
// <2> 如果对应位置的 Node 节点为空,则直接创建 Node 节点即可。
if ((p = tab[i = (n - 1) & hash] /*获得对应位置的 Node 节点*/) == null)
tab[i] = newNode(hash, key, value, null);
// <3> 如果对应位置的 Node 节点非空,则可能存在哈希冲突
else {
Node<K,V> e; // key 在 HashMap 对应的老节点
K k;
// <3.1> 如果找到的 p 节点,就是要找的,则则直接使用即可
if (p.hash == hash && // 判断 hash 值相等
((k = p.key) == key || (key != null && key.equals(k)))) // 判断 key 真正相等
e = p;
// <3.2> 如果找到的 p 节点,是红黑树 Node 节点,则直接添加到树中
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// <3.3> 如果找到的 p 是 Node 节点,则说明是链表,需要遍历查找
else {
// 顺序遍历链表
for (int binCount = 0; ; ++binCount) {
// `(e = p.next)`:e 指向下一个节点,因为上面我们已经判断了最开始的 p 节点。
// 如果已经遍历到链表的尾巴,则说明 key 在 HashMap 中不存在,则需要创建
if ((e = p.next) == null) {
// 创建新的 Node 节点
p.next = newNode(hash, key, value, null);
// 链表的长度如果数量达到 TREEIFY_THRESHOLD(8)时,则进行树化。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break; // 结束
}
// 如果遍历的 e 节点,就是要找的,则则直接使用即可
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break; // 结束
// p 指向下一个节点
p = e;
}
}
// <4.1> 如果找到了对应的节点
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 修改节点的 value ,如果允许修改
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 节点被访问的回调
afterNodeAccess(e);
// 返回老的值
return oldValue;
}
}
// <4.2>
// 增加修改次数
++modCount;
// 如果超过阀值,则进行扩容
if (++size > threshold)
resize();
// 添加节点后的回调
afterNodeInsertion(evict);
// 返回 null
return null;
}
<1> 处,如果 table 未初始化,或者容量为 0 ,则调用 #resize() 方法,进行扩容。
<2> 处,如果对应位置的 Node 节点为空,则直接创建 Node 节点即可。
i = (n - 1) & hash 代码段,计算 table 所在对应位置的下标。
调用 #newNode(int hash, K key, V value, Node<K,V> next) 方法,创建 Node 节点即可。代码如下:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
这样,一个新的链表就出现了。当然,此处的 next 肯定是 null 。
<3> 处,如果对应位置的 Node 节点非空,则可能存在哈希冲突。需要分成 Node 节点是链表<3.3>,还是红黑树<3.2>的情况。
3.1处,如果找到的 p 节点,就是要找的,则则直接使用即可。这是一个优化操作,无论 Node 节点是链表还是红黑树。
3.2 处,如果找到的 p 节点,是红黑树 Node 节点,则调用 TreeNode#putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v) 方法,直接添加到树中。这块,咱就先不深入了。
3.3处,如果找到的 p 是 Node 节点,则说明是链表,需要遍历查找。比较简单,胖友自己看下代码注释即可。其中,binCount >= TREEIFY_THRESHOLD - 1 代码段,在链表的长度超过 TREEIFY_THRESHOLD = 8 的时候,会调用 #treeifyBin(Node<K,V>[] tab, int hash) 方法,将链表进行树化。当然,树化还有一个条件,具体在 「TODO. 树化」 中详细来看。
<4> 处,根据是否在 HashMap 中已经存在 key 对应的节点,有不同的处理。
<4.1> 处,如果存在的情况,会有如下处理:
1)如果满足需要修改节点,则进行修改。
2)如果节点被访问时,调用 #afterNodeAccess((Node<K,V> p) 方法,节点被访问的回调。目前这是个一个空方法,用于 HashMap 的子类 LinkedHashMap 需要做的拓展逻辑。
3)返回老的值。
<4.2> 处,如果不存在的情况,会有如下处理:
1)增加修改次数。
2)增加 key-value 键值对 size 数。并且 size 如果超过阀值,则调用 #resize() 方法,进行扩容。
3)调用 #afterNodeInsertion(boolean evict) 方法,添加节点后的回调。目前这是个一个空方法,用于 HashMap 的子类 LinkedHashMap 需要做的拓展逻辑。
4)返回 null ,因为老值不存在。
#putIfAbsent(K key, V value)方法,
当 key 不存在的时候,添加 key-value 键值对到其中。代码如下:
@Override
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}
扩容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// <1> 开始:
// <1.1> oldCap 大于 0 ,说明 table 非空
if (oldCap > 0) {
// <1.1.1> 超过最大容量,则直接设置 threshold 阀值为 Integer.MAX_VALUE ,不再允许扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// <1.1.2> newCap = oldCap << 1 ,目的是两倍扩容
// 如果 oldCap >= DEFAULT_INITIAL_CAPACITY 满足,说明当前容量大于默认值(16),则 2 倍阀值。
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// <1.2.1>【非默认构造方法】oldThr 大于 0 ,则使用 oldThr 作为新的容量
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// <1.2.2>【默认构造方法】oldThr 等于 0 ,则使用 DEFAULT_INITIAL_CAPACITY 作为新的容量,使用 DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY 作为新的容量
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 1.3 如果上述的逻辑,未计算新的阀值,则使用 newCap * loadFactor 作为新的阀值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// <2> 开始:
// 将 newThr 赋值给 threshold 属性
threshold = newThr;
// 创建新的 Node 数组,赋值给 table 属性
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 如果老的 table 数组非空,则需要进行一波搬运
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
// 获得老的 table 数组第 j 位置的 Node 节点 e
Node<K,V> e;
if ((e = oldTab[j]) != null) {
// 置空老的 table 数组第 j 位置
oldTab[j] = null;
// <2.1> 如果 e 节点只有一个元素,直接赋值给新的 table 即可
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// <2.2> 如果 e 节点是红黑树节点,则通过红黑树分裂处理
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// <2.3> 如果 e 节点是链表
else { // preserve order
// HashMap 是成倍扩容,这样原来位置的链表的节点们,会被分散到新的 table 的两个位置中去
// 通过 e.hash & oldCap 计算,根据结果分到高位、和低位的位置中。
// 1. 如果结果为 0 时,则放置到低位
// 2. 如果结果非 1 时,则放置到高位
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 这里 do while 的原因是,e 已经非空,所以减少一次判断。细节~
do {
// next 指向下一个节点
next = e.next;
// 满足低位
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 满足高位
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 设置低位到新的 newTab 的 j 位置上
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 设置高位到新的 newTab 的 j + oldCap 位置上
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
树化
/**
* 每个位置链表树化成红黑树,需要的链表最小长度
*
* 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.
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* HashMap 允许树化最小 key-value 键值对数
*
* 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;
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// <1> 如果 table 容量小于 MIN_TREEIFY_CAPACITY(64) ,则选择扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// <2> 将 hash 对应位置进行树化
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 顺序遍历链表,逐个转换成 TreeNode 节点
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
// 树化
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
首先,参考 泊松概率函数(Poisson distribution) ,当链表长度到达 8 的概率是 0.00000006 ,不到千万分之一。所以绝大多数情况下,在 hash 算法正常的时,不太会出现链表转红黑树的情况。
其次,TreeNode 相比普通的 Node 来说,会有两倍的空间占用。并且在长度比较小的情况下,红黑树的查找性能和链表是差别不大的。例如说,红黑树的 O(logN) = log8 = 3 和链表的 O(N) = 8 只相差 5 。
毕竟 HashMap 是 JDK 提供的基础数据结构,必须在空间和时间做抉择。所以,选择链表是空间复杂度优先,选择红黑树是时间复杂度优化。在绝大多数情况下,不会出现需要红黑树的情况。
<1> 处,如果 table 容量小于 MIN_TREEIFY_CAPACITY = 64 时,则调用 #resize() 方法,进行扩容。一般情况下,该链表可以分裂到两个位置上。极端情况下,解决不了,这时候一般是 hash 算法有问题。
<2> 处,如果 table 容量大于等于 MIN_TREEIFY_CAPACITY = 64 时,则将 hash 对应位置进行树化。一共有两步,因为和红黑树相关,这里就不拓展开了。
有树化,必然有取消树化。当 HashMap 因为移除 key 时,导致对应 table 位置的红黑树的内部节点数小于等于 UNTREEIFY_THRESHOLD = 6 时,则将红黑树退化成链表
remove(Object key). 移除单个元素
// HashMap.java
public V remove(Object key) {
Node<K,V> e;
// hash(key) 求哈希值
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; // table 数组
Node<K,V> p; // hash 对应 table 位置的 p 节点
int n, index;
// <1> 查找 hash 对应 table 位置的 p 节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, // 如果找到 key 对应的节点,则赋值给 node
e;
K k; V v;
// <1.1> 如果找到的 p 节点,就是要找的,则则直接使用即可
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
// <1.2> 如果找到的 p 节点,是红黑树 Node 节点,则直接在红黑树中查找
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
// <1.3> 如果找到的 p 是 Node 节点,则说明是链表,需要遍历查找
else {
do {
// 如果遍历的 e 节点,就是要找的,则则直接使用即可
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break; // 结束
}
p = e; // 注意,这里 p 会保存找到节点的前一个节点
} while ((e = e.next) != null);
}
}
// <2> 如果找到 node 节点,则进行移除
// 如果有要求匹配 value 的条件,这里会进行一次判断先移除
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// <2.1> 如果找到的 node 节点,是红黑树 Node 节点,则直接在红黑树中删除
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// <2.2.1> 如果查找到的是链表的头节点,则直接将 table 对应位置指向 node 的下一个节点,实现删除
else if (node == p)
tab[index] = node.next;
// <2.2.2> 如果查找到的是链表的中间节点,则将 p 指向 node 的下一个节点,实现删除
else
p.next = node.next;
// 增加修改次数
++modCount;
// 减少 HashMap 数量
--size;
// 移除 Node 后的回调
afterNodeRemoval(node);
// 返回 node
return node;
}
}
// 查找不到,则返回 null
return null;
}
get(Object key)
public V get(Object key) {
Node<K,V> e;
// hash(key) 哈希值
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 查找 hash 对应 table 位置的 p 节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 如果找到的 first 节点,就是要找的,则则直接使用即可
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 如果找到的 first 节点,是红黑树 Node 节点,则直接在红黑树中查找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 如果找到的 e 是 Node 节点,则说明是链表,需要遍历查找
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}