本文使用了很多这篇文章的图和讲解:详解 HashMap 数据结构。
前置知识
HashMap的应用和技术思路
1、Hash主要用于信息安全领域中加密算法,它把一些不同长度的信息转化成杂乱的128位的编码,这些编码值叫做Hash值. 也可以说,Hash就是找到一种数据内容和数据存放地址之间的映射关系。
2、查找:哈希表,又称为散列,是一种更加快捷的查找技术。我们之前的查找,都是这样一种思路:集合中拿出来一个元素,看看是否与我们要找的相等,如果不等,缩小范围,继续查找。而哈希表是完全另外一种思路:当我知道key值以后,我就可以直接计算出这个元素在集合中的位置,根本不需要一次又一次的查找!
HashMap涉及到的概念
拉链法
往hashmap中加入元素的时候,可能会存在不同的key,有相同的hash值,这时候就将该元素插入到计算出来的位置上的链表中。
红黑树
是一种左右平衡的树,插入和删除的时候都要进行调整,保持左右子树的高度差在控制范围内,这样查询的时候复杂度可以降低到logn
底层数据结构
-
JDK1.8之前的HashMap由数组+链表组成,用Entry数组来存储数据(Entry对象包括:key、value、hash和指向下一个entry的引用变量),使用链表来解决哈希冲突。
-
JDK1.8之后的HashMap由数组+链表+红黑树组成。用Node数组来存储数据(Node对象包括的属性:key、value、hash和指向下一个node的引用变量)),链表和红黑树是为了解决哈希冲突的。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
HashMap内部使用数组来存储键值对,这个数组就是 HashMap 的主体。数组中的每个元素都是一个Entry对象(JDK1.8之前)或者Node对象(JDK1.8及以后)
代码debug结果中,Node类型的数组格式如下(Node<K,V>[] tab)
debug的方式可参考:blog.csdn.net/qq_39940674…
在数组中存储的每个位置上,可能会有多个键值对,这些键值对通过链表的形式链接在一起。
更细节一点的,图源:详解 HashMap 数据结构
源码分析
类属性
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//序列号
private static final long serialVersionUID = 362498820763181265L;
//默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当bucket上的节点数(数组中的某个结点+其连接的链表结点数)大于等于这个值的时候,会转为红黑树
static final int TREEIFY_THRESHOLD = 8;
//当bucket上的结点数小于等于这个值的时候,树转为链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转为红黑树对应的table的最小容量(只有桶内数据量大于 64 的时候才会允许转红黑树)
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组?,总是2的幂次倍
transient Node<K,V>[] table;
// 存放具体元素的集
transient Set<Map.Entry<K,V>> entrySet;
// 存放元素的个数,不等于数组的长度
transient int size;
// 每次扩容和更改map结构的计数器
// HashMap 内部结构发生变化的次数,即新增、删除数据的时候都会记录,
// 注意:修改某个 key 的值,并不会改变这个 modCount
transient int modCount;
// 阈值(容量*负载因子),当实际大小超过阈值的时候,会进行扩容
// 如果table没有被初始化,threshold实际上是存放的初始化容量.
// threshold代表最多能容纳的Node数量,一般 `threshold = length * loadFactor`,也就是说要想 HashMap 能够存储更多的数据(即获得较大的 threshold),有两种方案,一种是扩容(即增大数组长度 length),另一种便是增大负载因子。
int threshold;
// 负载因子,默认为 0.75 // 注意,这个值是可以大于 1 的
final float loadFactor;
几个字段的解释
MIN_TREEIFY_CAPACITY:虽然说当链表长度大于 8 的时候,链表会转为红黑树,但是也是需要满足桶内存储的数据量大于MIN_TREEIFY_CAPACITY的值,否则不仅不会转红黑树,反而会进行扩容操作。
threshold:代表最多能容纳的Node数量,一般
threshold = length * loadFactor,也就是说要想 HashMap 能够存储更多的数据(即获得较大的 threshold),有两种方案,一种是扩容(即增大数组长度 length),另一种便是增大负载因子。DEFAULT_LOAD_FACTOR:0.75 这个默认的负载因子的值是基于时间和空间考虑而得的一个比较平衡的点,所以负载因子我们一般不去调整,除非有特殊的需求:
- 比如 以空间换时间,意思是如果内存比较大,并且需要有较高的存取效率,则可以适当降低负载因子,这样做的话,就会减小哈希碰撞的概率。
- 再比如 以时间换空间,意思是如果内存比较小,并且接受适当减小存取效率,则可以适当调大负载因子,哪怕大于 1,这样做的话,就会增大哈希碰撞的概率。
Node<K,V>
HashMap 底层是一个 Node[] table,所以 Node 是一个很重要的数据结构。
Node 实现了 Entry 接口,所以,Node 本质上就是一个 Key-Value 数据结构。
static class Node<K,V> implements Map.Entry<K,V> {
// key 的 hash 值
final int hash;
final K key;
V value;
// 记录下一个 Node
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {...}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {...}
public final V setValue(V newValue) {...}
public final boolean equals(Object o) {...}
}
静态内部类:链表结点
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
静态内部类:树结点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
/**
* Returns root of tree containing this node.
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
...
四个构造方法
HashMap()
默认构造方法,会将loadfactor指定为默认的,默认构造方法就只初始化了loadfactor,创建了一个空的map
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
HashMap(int initialCapacity)
指定容量大小的构造方法
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
HashMap(int initialCapacity, float loadFactor)
指定容量大小和loadfactor的构造方法
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
// 初始容量暂时存放到 threshold ,在resize中再赋值给newCap进行table初始化
this.threshold = tableSizeFor(initialCapacity);
}
HashMap(Map<? extends K, ? extends V> m)
包含另一个Map的构造方法 如:HashMap<Map<Integer, String>, String> hashMap1 = new HashMap(map1);, 其中map1是已经赋值的hashmap
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//threshold是初始容量大小
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
// 将m中的所有元素添加至HashMap中,如果table未初始化,putVal中会调用resize初始化或扩容
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);
}
}
}
关键方法
tableSizeFor(int cap)
这个方法的调用入口如下
主要都是初始化HashMap对象的时候调用。比如下面这个
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
tableSizeFor源码如下
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;
}
看调用方的代码,可以发现该方法的作用,是初始化门限值threshold。
但该方法本质上还是初始化数组的大小。因为 HashMap 中没有容量大小(capacity)这样的字段,即使指定了初始化容量 initialCapacity ,也只是通过 tableSizeFor 将其扩容到与 initialCapacity 最接近的 2 的幂次方大小,然后暂时赋值给 threshold ,后续通过 resize 方法将 threshold 赋值给 newCapacity 进行 table 的初始化。
该方法的结果是返回一个大于等于传入的参数的数值,并且返回值会满足以下两点:
- 返回值是 2 的幂次方
- 返回值是最接近传入的参数的值 比如:传入 5,则返回 8;传入 8,则返回 8;
2 的幂次方有个特点,就是它的字节码除了最高位是 1,其余全是 0。 所以方法内使用了大量的 “或运算”和 “右移”操作,目的是保证从最高位起的每个 bit 都是 1。
- 首行
int n = cap - 1;的作用,是为了防止传入的参数本身就是一个 2 的幂次方,否则会返回两倍于参数的值; n |= n >>> 1;的作用,是保证倒数第二个高位也是 1,下面的代码类似。- 最后一行之前,得到的数类似 0000 1111 这种从第一个高位起全是 1,这样只要加了 1,则返回的数值必然是 2 的幂次方。
详细的计算过程详解见下图:
putMapEntries(Map<? extends K, ? extends V> m, boolean evict)
源码
这个方法的作用,是将m中的所有元素存到新的map中
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
// 获取 m 中的参数个数(key-value 的数量)
int s = m.size();
// 判断 table 是否被初始化过,否则初始化一遍。(PS:table 是在 put 操作的时候进行初始化的,所以如果当前的 HashMap 没有进行过 put 操作,则当前的 table 并不会被初始化)
if (s > 0) {
if (table == null) { // pre-size
// 根据传进来的 map 的元素数量来计算当前 HashMap 需要的容量
float ft = ((float)s / loadFactor) + 1.0F;
// 上述计算的容量是不能大于最大容量的,所以如果大于了,需要处理一下,t是最终的hasmap容量
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// 将计算而得的容量赋值给 threshold,前提是大于当前容量(即不会减小当前HashMap的容量)
if (t > threshold)
// 将容量转换为最近的 2 的 幂次方
threshold = tableSizeFor(t);
}
// table 不为空,即已经初始化过了,
// 如果 m 中的元素数量超过了当前 HashMap 的容量,则要进行扩容
else if (s > threshold)
resize();
// 遍历 m 的每个元素,将它的 key-value 插入到当前的 HashMap 中去
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
// 插入数据(注意,为什么不是 put() 呢,因为 put() 其实也是调用的 putVal() 方法)
putVal(hash(key), key, value, false, evict);
}
}
}
流程图
hash(Object Key)
方法的作用
该方法的作用是根据 key 计算一个 hash 数值,然后根据这个 hash 数值计算得到数组的索引 i,基于这个索引我们才能进行相关的增删改查操作,所以这个索引很关键。
比如put方法里,计算位置的公式为:(n - 1) & hash(在计算结果上等价于hash % length),其中hash为已经根据key计算出来的hash值,n为hashMap的容量大小?
为什么要这么做呢?因为 '%' 操作相对于位运算是比较消耗性能的,所以采用了'&' 运算。但是为什么结果是和取模运算是一致的呢?其实还是因为table的length 的问题。
我们上文提到过,HashMap 的长度 length 始终是 2 的幂次方,这个是关键,所以才会有这种结果,简单分析见下图:
使用 & 位运算替代常规的 % 取模运算,性能上提高了很多,这个是 table 如此设计数组长度的优势之一,另一个很大的优势是在扩容的时候,下文会分析。
算法详情
HashMap 中的 hash 算法,是将 key 进行 hashCode 计算得到 h,然后将 h 的高16位与低16位进行异或运算。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
算法优点
-
将高位和低位进行混合运算,这样是可以有效降低冲突概率的。
-
右移16位之后再进行异或运算的结果中,高位是可以保证不变的,变的是低位,并且低位中掺杂了高位的信息,最后生成的 hash 值的随机性会增大。
下图举例介绍异或计算(例如 h 为 467,926,597):
resize()
当往HashMap插入的数据超过了一定的容量限制,则需要进行扩容。扩容会伴随着一次重新 hash 分配,并且会遍历 hash 表中所有的元素,是非常耗时的。在编写程序中,要尽量避免 resize。resize 方法实际上是将 table 初始化和 table 扩容 进行了整合,底层的行为都是给 table 赋值一个新的数组。
final Node<K,V>[] resize() {
// 当前table
Node<K,V>[] oldTab = table;
// 计算当前table的大小(容量)
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 当前table的threshold,即允许存储的数据量阈值
int oldThr = threshold;
// 新的 table 的大小和阀值暂时初始化为 0
int newCap, newThr = 0;
// 1、开始计算新的 table 的大小和阀值
// a、当前 table 的大小大于 0,则意味着当前的 table 肯定是有数据的
if (oldCap > 0) {
// 当前 table 的大小已经到了上限了,就不扩容了,自个儿继续哈希碰撞去吧
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 否则,新的 table 的大小直接翻倍,阀值也直接翻倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// b、当前的 table 中无数据,但是阀值不为零,说明初始化的时候指定过容量或者阀值,但是没有被 put 过数据,因为在上文中有提到过,此时的阀值就是数组的大小,所以直接把当前的阀值当做新 table 的数组大小即可 (这个就是指定容量或阈值时,且第一次往map中put数据的时候,进行扩容,会走到这一步)
// 回忆一下:threshold = tableSizeFor(t);
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// c、这种情况就代表当前的 table 是调用的空参构造来初始化的,所有的数据都是默认值,所以新的table也只要使用默认值即可(也是第一次往map中put数据的时候,会进行扩容)
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新的阈值时0,则需要重新计算一下:
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//2、初始化新的 table
// 这个 newTab 就是新的 table,数组大小就是上面这一堆逻辑所计算出来的
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 遍历当前老的 table,处理每个下标处的 bucket,将其处理到新的 table 中去
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
// 释放当前老table 数组的对象引用(for循环后,当前 table 数组不再引用任何对象,就是这个位置上的Node结点为空)
oldTab[j] = null;
// a、如果这个位置上只有一个节点(没形成拉链的链表),则直接 rehash 赋值即可
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// b、当前的 bucket 是红黑树,直接进行红黑树的 rehash 即可
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// c、如果当前的bucket是链表
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 遍历链表中的每个 Node,分别判断是否需要进行 rehash 操作
// (e.hash & oldCap) == 0 算法是精髓,充分运用了table 大小为 2 的幂次方这一优势
do {
next = e.next;
// 根据 e.hash & oldCap 算法重新计算,在新数组中的索引
// (e.hash & oldCap) == 0,说明在新数组中索引不变
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 否则的话,新索引是:原索引 + oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原 bucket 位置的尾指针不为空(即还有 node )
if (loTail != null) {
// 链表末尾必须置为 null
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
// 链表末尾必须置为 null
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
下图展示的是扩容前和扩容后的计算索引的方法,主要关注红色框中的内容。这种方案是没问题的,但是之前我们提到,table 的大小为 2 的幂次方,为什么要这么设计呢,期间的又一个奥秘便体现在此,请看图中的备注(以下两图都来自:详解 HashMap 数据结构)
既然扩容后每个 key 的新索引的生成规则是固定有规律的,即只有两种形式,要么不变 i,要么增加原先的数组大小的量(i+n),所以我们其实并不需要真的去计算每个 key 的索引,而只需要判断索引是否不变即可。所以此处巧妙地使用了 (e.hash & oldCap) == 0 这个判断,着实精妙,计算的细节过程看下面的图即可。
put(K key, V value)
HashMap 只提供了 put 给用户使用,用于添加元素,put方法直接调用putVal进行处理
源码
public V put(K key, V value) {
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; Node<K,V> p; int n, i;
// 第一次 put 值的时候,会触发下面的 resize()
// 第一次 resize 和后续的扩容有些不一样,因为这次是数组从 null 初始化到默认的 16 或自定义的初始容量
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash 确定元素存放在哪个桶中,
// 若算出来的桶没有元素,则直接新生成结点放入桶中(此时,这个结点是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 如果算出来的位置上已有元素,则
else {
Node<K,V> e; K k;
// 快速判断第一个节点table[i]的key是否与插入的key一样,若相同就直接使用插入的值p替换掉旧的值e。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 否则,如果该节点是代表红黑树的节点,调用红黑树的插值方法
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 否则,说明是链表结点,按照插入链表的方式进行插值
else {
for (int binCount = 0; ; ++binCount) {
// 到达链表的尾部
if ((e = p.next) == null) {
// 在链表尾部插入新结点
p.next = newNode(hash, key, value, null);
// 如果结点数量达到阈值,则执行treeifyBin方法,
// 这个方法会根据HashMap数组情况来决定是否转红黑树
// 结点数量大于8,而且数组长度大于等于64的时候,会执行转红黑树的操作;否则就是对数组进行扩容
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值、hash值与插入元素相等的结点
if (e != null) { // existing mapping for key
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null,则用新值替换旧值。
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 实际大小大于阈值则扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
putVal方法执行流程图
自己画的图有点烂,用了别人的图,图源:详解 HashMap 数据结构
get(Object key)
主要调用的getNode方法,所以这里介绍getNode
public V get(Object key) {
Node<K,V> e;
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;
// 当前table不为空,且根据传入key的hash算出来的位置上的元素不为空(first不为空)
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 判断first的key是否与传入的key相同,如果相同,则返回该Node
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 上述条件不满足,则判断,first的下一个结点(e)是否为空(走到链表或者红黑树上了?)
if ((e = first.next) != null) {
// 如果结点是红黑树结点,则调用getTreeNode获取Node
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 如果是链表结构,则while循环遍历链表:若遍历到结点e的key与传入的key相同,则返回结点e
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}