我是靠谱客的博主 还单身小蚂蚁,最近开发中收集的这篇文章主要介绍数据结构 -- 数组+链表 HashMap属性们构造方法们HashMap的常用方法简略陈列:HashMap的添加操作:HashMap的删除操作:HashMap的查找相关方法:HashMap中的Iterator:,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

主要讲HashMap, 好像还有一个叫HashTable来着。一个一个讲吧。

HashMap,首先我的思路就转到了Hash这种字眼上。HashCode,是一个常见的东西,可是这东西究竟要怎么用那?

HashMap图解--转载

HashMap的数据结构

先讲讲HashCode是个什么?先说是干嘛的把,目的就是为了找内存中的某段内存比较方便。我们都知道内存那么大的地方,存着那么多东西假设我们要找一个指定的对象啥的,一个一个找,这个也行得通哈,但是呢,太慢,又或者是有别的方法可以找的更快。hashCode 出现就是解决这个问题的。。一些学术的词我也不是特别懂。接下来谈谈到底是什么个办法,用上了hashcode,能加快查找速度。。

就举个例子吧,上学谈论某人的时候,,“哎?你知道娅娅梨吗?就5班的那个英语课代表,知道吗?”,和“哎?你知道娅娅梨吗?”这俩的区别,我想第一句话最能体现出hashcode的含义了。想想为什么要这么谈论?娅娅梨这个人可能学校都好几个都是同名,先不管,但是一提5班的话,,那么旁听者的脑子就立马锁定一个范围了,脑子里只在5班所有同学里找娅娅梨是谁!这样的话,立马就能锁定目标。但是如果只说第二句,效果是啥??全校2000多名学生,我哪知道你说的是谁?脑子都糊了,半天才想起来谁是娅娅梨!

以上的例子是hashcode思路的基本描述,上述的5班,这个含义实则就是HashCode的地位。那么回归一下,hashcode的真实含义和用途。。个人感觉是分类用的。。hashcode 实则是内存指针位置经过一系列算法得到的一个值,,假设内存中有10个内存点,,地址位置分别是 2, 3, 4, 5, 6, 7, 8, 9, 10; 那就将这几个元素用取模的方式计算得到一个所谓的hash值。当然,真实情况下要算的复杂的多哈:   就以4位模吧。  那么   2/4 = 0 余 2, hashcode是2,  3/4 = 0 余 3, hashcode是3, ,依次计算下来,大家的hashcode值分别为:  2,3,0,1,2,7,0,1,2

对比下来

好了,这么算下来,你会发现,以上的元素似乎因为hashcode 值,被分为了4类:

那如果这样的话,我们假设要找 3 在哪里,是不是不同从这9个数里面一个一个找,反而是算哈希值,找找它到底值那个班级的,这样直接在指定的班级找,这不就更快了,,这里3被分配到了3班,,里面只有俩学生,就是3 和 7。撑死查看俩,是不是快多了。这就是hashCode主要功能。

那么接下来步入正题,HashMap原理。HashMap,就是为快而快的数据结构。。这里不得不说到最基本的线性表结构,数组和链表。。数组呢,查找啥的挺快,但是一到插入删除,就麻烦了,涉及到一波调换位置,全体前移或者后移。所以插入删除有些吃力,尤其是插入删除到前面的。。 那链表呢,,可以说是很好的解决了插入删除要全体动元素这个问题,但是随之而来的就是查找吃力,不是像数组那样一个i[n]就能很快的得知地址在哪里。。他得顺藤摸瓜慢慢捋。。有些吃力。所谓鱼与熊掌不可兼得。。但是他俩要是组合一下呢?各取所优?于是hashMap就是取各自的优点。。数组不是查找的快吗?那我就让你充当班级的角色,,链表不是添加删除无压力吗?我就让你的每个单元充当学生。。每当一个学生入校的时候,我就根据这个学生的特质,算出来它应属于哪个班级,得出来一个班级号,就是hash,,然后我就把它加到hash指定的班里面,假设hash是3,那就分到三班。。然后学生到三班报道,总得有个座位吧,,然而这些座位相当于链表的结构,,用链表的那一套给他得出来位置就行了,其实就是加个指针。

 

如图:以16取模,分的位置。

这样看来 HashMap实则也不是辣么神秘啦,反而还是挺有趣呢,数组和单向链表的结合体。

那咱们用的时候不是有key value 这一说吗?先上一个毁三观的图,告诉我不是只有我一个刚刚接触的时候是这么以为的:

曾经的我知道map不是数组,脑海中的图像俨然是数组的样子。。够毁三观吧。。。请勿模仿。不过以后绝不会这么想啦。

 

其实key value 呢,是存在链表节点中的,,并且这个key,就是作为算hash值的关键人物。value呢就是咱们加入的数据没啥好说的。。在HashMap中,有不少地方根据key算出了hash值,然后根据这个值,来确定这个元素是属于 数组结构中哪个单元格格里面的,找到哪个格格里,,再对这个格格存的链表头顺藤摸瓜进行查找,添加,更改等工作,大概就是这么个思路。

好讲到这里那么我们分析一下源码吧。数据结构相关类的潜规则不能忘。我想设计一个数据结构,增删查改这四点是必须要有的!hashMap也是不例外,增好说,看看原理,估计就是要做那种先找key所属的桶(桶就认为是数组的某个元素就行),然后在桶里将链表补全即可。 删呢?就是根据key找到所属的桶,遍历桶里的链表找到对应的那个key,断链儿。查找,如果给的是key,那么找到key所在的桶,再在桶里遍历。 如果给的是value,那么遍历key,比对对应的value。 删除,找桶,再加断链操作。。听这么一说,貌似hashmap没啥了哈哈。但是经过面试我被吐槽研究的不深,郁闷了三天之后终于重振旗鼓,打算仔细的码!(话说深与不深,这种判断标准我想每个人是不同的,与阅历有关系。比如一个刚学hashMap的人他可能就是了解怎么用, 一个用熟了的他就会考虑怎么实现的,知道了怎么实现的人他们之后可能会考虑这种结构有什么欠缺,感觉自己啥都会了,可能会把代码扒到C/C++那层看最终的原理,,连这都懂了,估计就会想着横向扩展。所以依据自身的阅历来判断别人研究的深不深,是不合理,具有主观性的。所以我的心情就好些了。因为我知道我不知道的至少目前来看是正常的,但是我知道自己不知道哪里却不去探究那就不正常了。)

那么什么是更深的问题呢?仔细想的话,有n个点需要我们探究,map虽然结构很巧妙很好,但是如果元素多了会导致每个桶里的元素越来越多,不作处理的话,查找起来照样慢。这是其一。 那么其二是,分桶的时候基本是按着取模的算法来的。那么这时候就考验到咱们的hashcode算法算出来的值合不合理。 当然你可以使hashcode等于任意值,但是,如果你的算法不好,恰好有好多key的hashcode恰好算着相等呢?这样就会导致大部分元素都分到一个桶里去了,饱的撑死,饥的饿死,赶到这样的算法,那就随时体验被黑暗支配的恐惧吧。 其三Iterator这个接口经常用吧。原理捏?为啥推荐用它呀?反正HashMap里面遍历也用到它了。


属性们


//数组的最小容量
private static final int MINIMUM_CAPACITY = 4;
//数组的最大容量,1左移30个,相当于2的30次方,相当大了
private static final int MAXIMUM_CAPACITY = 1 << 30;
//空数组
private static final Entry[] EMPTY_TABLE
= new HashMapEntry[MINIMUM_CAPACITY >>> 1];
static final float DEFAULT_LOAD_FACTOR = .75F;
//这个是实际的,map存的横向数组。
transient HashMapEntry<K, V>[] table;
//一个空元素
transient HashMapEntry<K, V> entryForNullKey;
//记录map元素的数量
transient int size;
//记录修改的次数。
transient int modCount;
//记录当前可承受的数组最大容量,一般是到了原来的3/4的时候就会扩容,扩到2倍的量
private transient int threshold;
// Views - lazily initialized
//将所有的key记录其中
private transient Set<K> keySet;
//将所有的整个key value 对象作为元素记录其中
private transient Set<Entry<K, V>> entrySet;
//将所有的value记录其中
private transient Collection<V> values;

(这个编辑器目前更新的挺不错。)

从属性们可见,关于整个数据存储的,估计就是那个叫table的成员变量。在那里面只要存一个链表头,就可以顺藤摸瓜找到所在桶里面的所有元素。另外其他的,比如threshold这个参数,则是与容量有关的一个变量。涉及到的逻辑其实就是扩容相关的。其余的比如 keyset, entrySet, values是有关于遍历,查找之类的。

构造方法们

 /**
* Constructs a new empty {@code HashMap} instance.
* 无参构造方法,里面就定义了一个空数组
*/
@SuppressWarnings("unchecked")
public HashMap() {
table = (HashMapEntry<K, V>[]) EMPTY_TABLE;
threshold = -1; // Forces first put invocation to replace EMPTY_TABLE
}
/**
* Constructs a new {@code HashMap} instance with the specified capacity.
*
* @param capacity
*
the initial capacity of this hash map.
* @throws IllegalArgumentException
*
when the capacity is less than zero.
*/
/**capacity参数,决定初始的数组的容量*/
public HashMap(int capacity) {
/**不管是哪种情况下,写方法,首先要判断参数的合法性。要养成这样的好习惯*/
if (capacity < 0) {
throw new IllegalArgumentException("Capacity: " + capacity);
}
if (capacity == 0) {
@SuppressWarnings("unchecked")
HashMapEntry<K, V>[] tab = (HashMapEntry<K, V>[]) EMPTY_TABLE;
table = tab;
threshold = -1; // Forces first put() to replace EMPTY_TABLE
return;
}
/**给的容量小于4,那就默认是4其余同理*/
if (capacity < MINIMUM_CAPACITY) {
capacity = MINIMUM_CAPACITY;
} else if (capacity > MAXIMUM_CAPACITY) {
capacity = MAXIMUM_CAPACITY;
} else {
/**利用位运算符算出合适的容量*/
capacity = Collections.roundUpToPowerOfTwo(capacity);
}
makeTable(capacity);
}
/**
* Constructs a new {@code HashMap} instance with the specified capacity and
* load factor.
*
* @param capacity
*
the initial capacity of this hash map.
* @param loadFactor
*
the initial load factor.
* @throws IllegalArgumentException
*
when the capacity is less than zero or the load factor is
*
less or equal to zero or NaN.
*/
public HashMap(int capacity, float loadFactor) {
this(capacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new IllegalArgumentException("Load factor: " + loadFactor);
}
/*
* Note that this implementation ignores loadFactor; it always uses
* a load factor of 3/4. This simplifies the code and generally
* improves performance.
*/
}
/**
* Constructs a new {@code HashMap} instance containing the mappings from
* the specified map.
*
* @param map
*
the mappings to add.
*/
public HashMap(Map<? extends K, ? extends V> map) {
this(capacityForInitSize(map.size()));
constructorPutAll(map);
}

hashmap有四个构造方法通常使用的是 HashMap() , 和 HashMap(Map map)这俩。

HashMap的常用方法简略陈列:

下面简单概述一下HashMap经常用到的那一波公共方法:

添加类:put(K, V), putAll(Map map)

删除类:remove(Object), clear()

修改:操作的时候直接用put即可。因为put里面涵盖了如果有key,更换value的逻辑

查找:containsKey(Object), containsValue(Object), 返回的都是布尔值,get(Object key)

HashMap的添加操作:

/**
* Maps the specified key to the specified value.
*
* @param key
*
the key.
* @param value
*
the value.
* @return the value of any previous mapping with the specified key or
*
{@code null} if there was no such mapping.
*/
/**添加一个数组,并返回上一个key对应的元素。如果上一个无内容返回null*/
@Override public V put(K key, V value) {
if (key == null) {
return putValueForNullKey(value);
}
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
int index = hash & (tab.length - 1);
/**发现有相同的,就覆盖*/
for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
if (e.hash == hash && key.equals(e.key)) {
preModify(e);
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
// No entry for (non-null) key is present; create one
modCount++;
if (size++ > threshold) {
/**进行扩容,这个里面的操作略微复杂,并且很重要*/
tab = doubleCapacity();
index = hash & (tab.length - 1);
}
/**这个是真正的添加操作,这行代码的意思仅仅是根据参数new一个新的元素,并且加到index桶的
第一位,当做新的链表头儿。而已*/
addNewEntry(key, value, hash, index);
return null;
}

上面的方法是我们最常用的一个方法。首先会找给定的key是否已经有了,有了的话,就更换其中的value。 没有的话就考虑容量是不是应该增加了。如果需要增加(即当前容量使用率超过原有容量的3/4),则容量变为原来的2倍.容量变了,那么以前的按照hash值分的那些桶,也便不能按照以前那样分了,必须全部按照新的容量进行相应的搬家操作。

那么这里面,扩容这个操作可以来说,是对HashMap拥有大容量数据的时候,可以优化其查找元素的速度。尤其是 对于get(Object key) 这个方法而言。所以接下来重点看看这个方法:doubleCapacity();

 /**
* Doubles the capacity of the hash table. Existing entries are placed in
* the correct bucket on the enlarged table. If the current capacity is,
* MAXIMUM_CAPACITY, this method is a no-op. Returns the table, which
* will be new unless we were already at MAXIMUM_CAPACITY.
*/
private HashMapEntry<K, V>[] doubleCapacity() {
/**原先的数组和原先数组的长度记下来先*/
HashMapEntry<K, V>[] oldTable = table;
int oldCapacity = oldTable.length;
/**如果这个数组的长度是足够足够大的话,容量已经是HashMap所允许的最大值了,直接返回原先的
* 数组
* 至于以后会做什么优化操作,目前不知*/
if (oldCapacity == MAXIMUM_CAPACITY) {
return oldTable;
}
//新的数组容量计算,为原先的二倍。
int newCapacity = oldCapacity * 2;
HashMapEntry<K, V>[] newTable = makeTable(newCapacity);
if (size == 0) {
return newTable;
}
//根据以前的数据,对元素进行搬家
for (int j = 0; j < oldCapacity; j++) {
/*
* Rehash the bucket using the minimum number of field writes.
* This is the most subtle and delicate code in the class.
*/
//得到老数组相应位置的元素。这个元素就是链表头
HashMapEntry<K, V> e = oldTable[j];
//为什么有空呢?其实不难理解,第一确实没有添加过参数。第二,不是有个超过原先容量
//四分之三就扩容吗这就导致一般情况下总有那么四分之一里面没有元素
if (e == null) {
continue;
}
//相当于一个很省内存并且很快捷的取模运算
//结果是实际运算的模 - 1
int highBit = e.hash & oldCapacity;
HashMapEntry<K, V> broken = null;
//把原先的数组中的链表头取出重新计算
newTable[j | highBit] = e;
//链表头接下来的元素们.期初值是头部的next,逻辑完之后 e往后移, n 往后移。
for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) {
//利用二进制取模运算,我怎么感觉这句有毛病啊。。按理来说应该是新的模
int nextHighBit = n.hash & oldCapacity;
//倘若与原先的标号不一致
if (nextHighBit != highBit) {
if (broken == null)
//将节点放到新数组的相应位置去, 别忘了,这个节点也有next。一旦移位,
//在想象中是带着其余的一大串note来移的。相当于拖家带口。但是有个值得注意的地方是
//被除数变了,你不确定他们的模是否和以前相等。如 15 对 8, 和16取模 得到结果不同一样的道理。
newTable[j | nextHighBit] = n;
else
broken.next = n;
broken = e;
/**将新的位置赋给原先的,因为再次循环的时候取模相同下个元素就不搬家了,只有与当前所处位置不匹配的时候搬家
* 这样会省一波运算*/
highBit = nextHighBit;
}
}
/**当代码走到这里代表搬家完毕*/
if (broken != null)
broken.next = null;
}
return newTable;
}

总的来讲,这个扩容其实并不是那么复杂。总的来说是两步,1扩容,2搬家。

那么再来看看关于添加操作的另外一个重要的方法:putAll(map)


/**
* Copies all the mappings in the specified map to this map. These mappings
* will replace all mappings that this map had for any of the keys currently
* in the given map.
*
* @param map
*
the map to copy mappings from.
*/
@Override public void putAll(Map<? extends K, ? extends V> map) {
/**这个方法的步骤是计算出容量*/
ensureCapacity(map.size());
super.putAll(map);
}

其实实现是很简单的。两句代码。但是对于第一句,大致上就是一个比较容量,合适就不扩容,不合适就需要扩容,ensureCapacity(int size)代码解析如下:


/**
* Ensures that the hash table has sufficient capacity to store the
* specified number of mappings, with room to grow. If not, it increases the
* capacity as appropriate. Like doubleCapacity, this method moves existing
* entries to new buckets as appropriate. Unlike doubleCapacity, this method
* can grow the table by factors of 2^n for n > 1. Hopefully, a single call
* to this method will be faster than multiple calls to doubleCapacity.
*
*
<p>This method is called only by putAll.
*/
private void ensureCapacity(int numMappings) {
int newCapacity = Collections.roundUpToPowerOfTwo(capacityForInitSize(numMappings));
HashMapEntry<K, V>[] oldTable = table;
int oldCapacity = oldTable.length;
/**本身新添加的map容量就比当前map的量小*/
if (newCapacity <= oldCapacity) {
return;
}
/**恰好新添加的map容量是当前容量的二倍,代表当前的map不做操作压根就兜不住这么些数据,
* 那么就利用写好的二倍扩容方法*/
if (newCapacity == oldCapacity * 2) {
doubleCapacity();
return;
}
// We're growing by at least 4x, rehash in the obvious way
/**基本走到这一步,就是新增的map是原先map的2倍以上,也就是2的n次幂倍, n > 1 这种情况*/
/**新建一个新的数组,该数组是按照最新的容量来的*/
HashMapEntry<K, V>[] newTable = makeTable(newCapacity);
if (size != 0) {
/**根据最新的容量得到一个最新的掩码,以便于根据这个值找到每个元素是在哪个桶里面*/
int newMask = newCapacity - 1;
/**遍历老数组里面的元素*/
for (int i = 0; i < oldCapacity; i++) {
/**for循环的条件里少了第三个条件。不过这种用法也是对的,只是平常不怎么这样写
* 这句意思是遍历原来数组里面桶里的元素*/
for (HashMapEntry<K, V> e = oldTable[i]; e != null;) {
HashMapEntry<K, V> oldNext = e.next;
/**取模得出当前元素的hash,按规则应该在新数组的哪个格格里面*/
int newIndex = e.hash & newMask;
/**找到新数组应该添加的新位置,并把当前元素添加对应链表头部
* 但是感觉这种方法不如doubleCapacity()处理的算法好,这里面复杂度略高
* 理应记录一下老的index和新的index值,比对,不一样的情况下再进行搬家*/
HashMapEntry<K, V> newNext = newTable[newIndex];
newTable[newIndex] = e;
e.next = newNext;
e = oldNext;
}
}
}
}

那么这样的话,关于HashMap的添加元素,修改元素就讲完了。里面主要注意的一点是扩容的处理。 也就是扩大数组长度,并根据新的长度对原来的数据进行一次大搬家。

HashMap的删除操作:

删除的话,主要是针对指定的元素进行删除,和全部删除。不过HashMap里面只看到了 remove(Object key)这个,就是根据指定的key进行删除,但是没有看到根据指定的value进行删除这种函数。应该是后者这种情况不常用把。

下面看看remove(Object key)这个方法:

/**删除指定key*/
@Override public V remove(Object key) {
/**key == null的情况下是会存的。所以这种情况下会删除entryForNullKey这个里面的东西*/
if (key == null) {
return removeNullKey();
}
/**计算hash并找到index*/
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
int index = hash & (tab.length - 1);
/**遍历指定桶里的元素*/
for (HashMapEntry<K, V> e = tab[index], prev = null; e != null; prev = e, e = e.next) {
/**找到的话,进行断链儿*/
if (e.hash == hash && key.equals(e.key)) {
if (prev == null) {
tab[index] = e.next;
} else {
prev.next = e.next;
}
modCount++;
size--;
/**这个方法事实上没有任何实现逻辑,很明显是用来让开发人员重写用的*/
postRemove(e);
return e.value;
}
}
return null;
}

另外一个就是HashMap的清空方法  clear()

 /**
* Removes all mappings from this hash map, leaving it empty.
*
* @see #isEmpty
* @see #size
*/
@Override public void clear() {
if (size != 0) {
//为数组的每个元素赋值为null。but,,为何没有做类似于缩小容量的操作????
Arrays.fill(table, null);
entryForNullKey = null;
modCount++;
size = 0;
}
}

HashMap的查找相关方法:

关于HashMap的查找,有三个 get(Object key), containsKey(Object key), containsValue(Object value).下面是这三个方法的详解:

/**
* Returns the value of the mapping with the specified key.
*
* @param key
*
the key.
* @return the value of the mapping with the specified key, or {@code null}
*
if no mapping for the specified key is found.
*/
public V get(Object key) {
/**老道理,写一个方法的第一步就是对参数进行判断
* 此处如果是空的话,就找专门记录空元素的属性去*/
if (key == null) {
HashMapEntry<K, V> e = entryForNullKey;
return e == null ? null : e.value;
}
/**我认为是hash的逆向运算*/
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
K eKey = e.key;
/**首先是地址,地址相同肯定是同一个对象,然后是hash相同,这样就会在第一个判断条件下
* 就会淘汰掉很多不符合条件的。节省运算
* 从这里可以得到一个技巧,就是判断条件的顺序其实不是瞎排的。怎么省步骤怎么写
*/
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
return e.value;
}
}
return null;
}
 /**
* Returns whether this map contains the specified key.
*
* @param key
*
the key to search for.
* @return {@code true} if this map contains the specified key,
*
{@code false} otherwise.
*/
/**这个就是算出hash,然后到指定的桶里面找有木有key*/
@Override public boolean containsKey(Object key) {
if (key == null) {
return entryForNullKey != null;
}
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
e != null; e = e.next) {
K eKey = e.key;
if (eKey == key || (e.hash == hash && key.equals(eKey))) {
return true;
}
}
return false;
}

 

 /**
* Returns whether this map contains the specified value.
*
* @param value
*
the value to search for.
* @return {@code true} if this map contains the specified value,
*
{@code false} otherwise.
*/
/**这个相对于找key来说,更加复杂。因为首先我不确定value对应的key是几
* 所以只好对map进行遍历,来一一比对*/
@Override public boolean containsValue(Object value) {
HashMapEntry[] tab = table;
int len = tab.length;
/**将整个逻辑分为两个情况,1 是 value本身是null,那么这样的话,不需要进行equals
* 那么此时你会想了,就算不用equals我用 == 符号难道不行吗?这样会省掉下面的一波代码直接和下一段写到一块去
* 哈哈,结果是不行的。不信你试试, 两个为null 的对象,用 == 号判断的话,其实是false!故只能用 ==null*/
if (value == null) {
/**遍历数组*/
for (int i = 0; i < len; i++) {
/**遍历数组中的链式结构*/
for (HashMapEntry e = tab[i]; e != null; e = e.next) {
if (e.value == null) {
return true;
}
}
}
return entryForNullKey != null && entryForNullKey.value == null;
}
// value is non-null
for (int i = 0; i < len; i++) {
for (HashMapEntry e = tab[i]; e != null; e = e.next) {
if (value.equals(e.value)) {
return true;
}
}
}
/**实在找不到就看看key=null 那个里面有没有。其实个人感觉这句话应该提早判断*/
return entryForNullKey != null && value.equals(entryForNullKey.value);
}

看了这么多代码是不是发现HashMap是线程不安全的

HashMap中的Iterator:

在看HashMap的源码中其实有不少Iterator的使用。我们知道这个东西通常被用在遍历集合删除集合中。那么为什么呢?好好执行集合的删除方法难道不行么?当做附加题看看吧。

package java.util;
//可以看出Iterator是一个很简单的接口,只有三种方法。
//并且通过这些方法可见,这了接口几乎是为删除元素而生的。
public interface Iterator<E> {
public boolean hasNext();
public E next();
public void remove();
}

我们看看HashMap其中一个Iterator的实现方式,
 

private final class KeyIterator extends HashIterator
implements Iterator<K> {
public K next() { return nextEntry().key; }
}

这是一个继承自HashIterator并且又要实现Iterator接口的类。是不是很奇怪?按理来讲应该实现Iterator里面的所有抽象方法呀,怎么就实现了一个next()?且慢,看看KeyIterator父类是怎么写的:

 private abstract class HashIterator {
int nextIndex;
HashMapEntry<K, V> nextEntry = entryForNullKey;
HashMapEntry<K, V> lastEntryReturned;
int expectedModCount = modCount;
HashIterator() {
if (nextEntry == null) {
HashMapEntry<K, V>[] tab = table;
HashMapEntry<K, V> next = null;
while (next == null && nextIndex < tab.length) {
/**遍历到最后 nextIndex是最后一个索引*/
next = tab[nextIndex++];
}
/**最后一个桶的链表头*/
nextEntry = next;
}
}
public boolean hasNext() {
return nextEntry != null;
}
/**这是一个调用次数略多的方法
* 他的意义是不断地找下一个元素,从数组依次往下找,碰见空桶直接跳过找下一个桶*/
HashMapEntry<K, V> nextEntry() {
/**比较修改次数,如果不符合,代表不是一整条操作上的,就是抛出异常
* 由此可以看出,多线程里如果进行这样的操作,简直是找死*/
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (nextEntry == null)
throw new NoSuchElementException();
/**entryToReturn初始值是最后一个桶的链表头*/
HashMapEntry<K, V> entryToReturn = nextEntry;
/**tab初始值就是HashMap持有的那个数组*/
HashMapEntry<K, V>[] tab = table;
/**初始值是最后一个桶的链表头的下一个元素*/
HashMapEntry<K, V> next = entryToReturn.next;
/**如果桶里面没有元素,
* 并且接下来数组里还有其他桶,那就把next赋值为下一个桶的链表头*/
while (next == null && nextIndex < tab.length) {
next = tab[nextIndex++];
}
/**next*/
nextEntry = next;
/**给lastEntryReturned赋值并返回*/
return lastEntryReturned = entryToReturn;
}
public void remove() {
if (lastEntryReturned == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
HashMap.this.remove(lastEntryReturned.key);
lastEntryReturned = null;
expectedModCount = modCount;
}
}

这个父类很有趣,他里面有hasNext()方法和remove()方法。这样的话子类继承了父类自然也有了同样的方法。但是这些方法名称参数是和Iterator接口里面的方法完全一致!这样就可以瞒天过海了。

那么在这个自定义的Itorator里面,你会发现多次使用到了ecpectrdModCount这个参数 和 modCount来进行比较这样的操作。个人感觉这个操作可以说是Iterator优势的核心。就是操作步骤一致性。不是同一个操作步骤的话,就给你抛异常。这样操作的好处是什么呢?假设你是在多线程的情况下进行 next() 查找的,但是此时又有别的线程对map进行了其他操作,那么这样的话就会导致modCount != expectedModCount  直接出异常。。这样就是为了防止你进行next, remove等操作的时候,绝对不可以同时有其他的逻辑同时在偷偷的对此数据结构进行任何操作!用以保障删除的时候不会因为其他的意外而导致空指针异常。因为iterator的一大作用就是用来删除!

那么有没有想过,为什么需要Iterator这种接口?让我们仔细看下,Iterator都在什么地方被实现了:

与Iterator有直接关系的,就那么多,接下来看看具体类,就拿ListIterator为例子,点击进去发现这个其实是一个接口!看看什么地方用他们就拿里面的hasNext()方法为例: 

发现集合的操作类 Collections里面大量用到这波东西!!Collections 是一个集合的操作类。跟Collection不一样,要了解。

那么经过这样的倒推,再正推一下。假设目前什么类都没有,让你重新写一个集合相关的数据结构架构,你会怎么写?我们知道每一样数据结构里面的组成是不同的,但是他们的根本目的就是存数据!并且数据要有增删查改的功能。在你不知道数据怎么存的情况下,你也得定下相关的增删查改这类抽象方法吧。但是增删查改的过程中难免遇到这堆遍历问题,有木有下个元素啊啥的,删除要安全的删除啊啥的。但是遍历,这种操作,是根据一定的数据结构来的,比如说你写的数据结构1用的是线性结构,数据结构2是链表结构,数据结构3 是一个优化了的线性结构。那么针对这里面的遍历操作来讲,有必要每个结构都要写么?显然不是必须的。那么数据结构1和数据结构2 何不用相同原理的遍历方式?我想Iterator就是这样以接口的方式被大家实现的。以便于总的控制类,也就是Collections即使不知道具体的数据结构依然可以调取Iterator相应方法,来对数据结构进行排序,遍历甚至删除之类的操作。

 

对了,关于集合类的组成结构需要理一下关系:暂时粘贴一个从别处拿来的图片吧, 如果被问到map是不是collection,别傻傻分不清楚了。

 

 

 

 

 

最后

以上就是还单身小蚂蚁为你收集整理的数据结构 -- 数组+链表 HashMap属性们构造方法们HashMap的常用方法简略陈列:HashMap的添加操作:HashMap的删除操作:HashMap的查找相关方法:HashMap中的Iterator:的全部内容,希望文章能够帮你解决数据结构 -- 数组+链表 HashMap属性们构造方法们HashMap的常用方法简略陈列:HashMap的添加操作:HashMap的删除操作:HashMap的查找相关方法:HashMap中的Iterator:所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(62)

评论列表共有 0 条评论

立即
投稿
返回
顶部