Map接口
Map接口实现类的特点
先说明一下,有可能有一部分实现类的特点和这不一样,但是大部分是差不多的,,,
1.Map接口用于保存具有映射关系的数据:K-V
Set接口也是一样,但是Set接口中保存的K是变量,但是V是直接默认用一个常量PRESENT【如图所示】来代替的
我们在前面的学习可知,HashMap底层是对于哈希值的判断以及equals方法进行添加元素的。
所以Map接口的实现类HashMap 进行put方法 输入输出的元素对象也是无序的
2.
table数组的类型是HashMap$Node类型
无论Map中Key和Value是什么样的引用类型,都可以封装到这个元素对象【类型是HashMap$Node】中,然后存放到这个table数组【类型是HashMap$Node】中
3.
当Key值相等的时候,相当于替换了
过程分析:
若key值相等,添加到同一个索引处【key值相等,那么生成的哈希值相等】。
那么用这个新put的新节点的value值来覆盖替换原来同key值的对象的value值
4.
Map中value值是可以重复的,
分析:当我们put一个元素到table数组的时候,相当于new一个Node对象。当value值相等的时候,和no1存放的索引都不一样,所以可以添加成功。
5.
Map中key和value可以为null,但是key为null只可以有一个【因为put的key值相等的替换机制】
但是value为null可以有多个,因为一条链表可以链接多个Node节点
6.
常用String类这个引用类型作为Key,但我们其实是可以用任意类型作为Key值
7.
8.
8.重点Map存放数据的key-value:
一对K-V是存放在一个HashMap$Node中的,因为Node这个内部类是实现了Entry接口。
所以一对K-V就是一个Entry
————————
1.无论Map中Key和Value是什么样的引用类型,都可以封装到这个元素对象【类型是HashMap$Node】中,然后存放到这个table数组【类型是HashMap$Node】中
HashMap$Node node=newNode(hash,key,value,null);
Entry里面的K指向node里面的key,V指向node里面的value
————————
transient Set<Map.Entry<K,V>> entrySet==Set<Map.Entry<K,V>> entrySet();
K-V为了方便遍历操作,会创建EntrySet集合,该集合存放的元素类型是Entry。而一个Entry对象就有K,V EntrySet<Entry<K,V>>。即是。。。。。
————————
重点
定义的类型是Map.Entry,但是实际上存放的是HashMap$Node。
因为Node<K,V>实现了Map.Entry<K,V>这个接口。
Entry<K,V>写完整之后就是Map.Entry<K,V>
所以 Entry<K,V> <=> Node<K,V> <=>Map.Entry<K,V>
只要一个类实现了一个接口类型,那么这个类的对象实例就可以赋给这个接口类型
比如:Map leomessi=new HashMap();
——————————
调试:
map.entrySet()返回的是一个Set<Map.Entry<K,V>>的集合,类型即是Set,
之后再用一个遍历操作,把set接收到的元素一个个的取出来给到obj。
之后再向下转型,
因为 EntrySet<Entry<K,V>>等价于Set<Map.Entry<K,V>> entrySet();
【重点】使用entrySet方法遍历输出Map集合中一组key和value值的流程【重点】底层源码分析得出的结论:
一开始给定任意引用类型的key和value值,把其封装为一个元素对象【类型是HashMap$Node】
即是HashMap$Node node=newnode(hash,key,value,null);
之后由于Node<K,V>实现了Entry<K,V>这个接口。
其实在内存当中是:一个类型HashMap$Node的元素对象存放到对应的一个Entry<K,V>的地址
那么一个Entry<K,V>指向一个类型HashMap$Node的元素对象中存储的一对key-value键值对,
即是Entry中的K指向一个key,V指向一个value
原因如下:
因为Node<K,V>实现了Map.Entry<K,V>这个接口。
【Entry<K,V>写完整之后就是Map.Entry<K,V>,因为Entry实现了Map.Entry<K,V>接口】
所以 Entry<K,V> <=> Node<K,V> <=>Map.Entry<K,V>
即是我们上面说的一个Entry就是一个HashMap$Node
之后为了更方便的管理:
我们把一个个的Entry<K,V>存入到一个集合中。即是EntrySet<Entry<K,V>>
——
之后得出 Set<Map.Entry<K,V>> entrySet() 等价于 EntrySet<Map.Entry<K,V>>
【Entry<K,V>写完整之后就是Map.Entry<K,V>,因为Entry实现了Map.Entry<K,V>接口】Entry==Map.Entry【我好憨啊,解释了好多次了,就怕忘记了哈哈哈哈】
之后我们得调用entrySet()这个方法,调用这个方法之后相当于把 EntrySet<Map.Entry<K,V>>存放到set这个集合中去了。。。
那么之后再向下转型遍历输出,即是输出EntrySet集合中每一个元素 Map.Entry<K,V>
那么一个Entry<K,V>指向一个类型HashMap$Node的元素对象中存储的一对key-value键值对,
即是Entry中的K指向一个key,V指向一个value
重点:
因为EntrySet集合中存放的就是每一个HashSet$Node类型元素对象的地址,所以每一个地址存放在集合中的每一个元素 Map.Entry<K,V>,这样集合中的每一个元素Map.Entry<K,V>都对应指向一组类型是HashMap$Node的元素对象【这个对象是由随意的引用类型的key-Value值封装得出的对象】。
【Entry<K,V>写完整之后就是Map.Entry<K,V>,因为Entry实现了Map.Entry<K,V>接口】Entry==Map.Entry
那么我们可以直接调用Entry<K,V>接口中的getValue和getKey方法进行得出,相对应实现该接口的实例对象Node的key和value值
内部类的形式
下图表示的意思就是HashMap的内部类是EntrySet
为了方便管理,先把任意引用类型的一对key和value封装为一个对象HashMap$Node实现这个Entry接口,然后一个Entry<K,V>管理一个HashMap$Node对象,之后再把许多个Entry<K,V>放到EntrySet<Entry<K,V>>即是Set<Entry<K,V>>这个集合中去。。。。
重点
这里entrySet中的对象不是复制过来的,而是传过去一个地址的。
相当于entrySet中的对象指向table数组中的元素中对应Node<K,V>
【切记entrySet中不是复制的对象,而只是一个地址】
我们用一个对象实例再来演示一遍
最后总结一下【重点】:
1.统一管理 Key和Value:
为了方便管理,先把任意引用类型的一对key和value封装为一个对象实现这个Entry,之后再把其放到EntrySet中去,之后我们根据前面的操作可以根据 map.entrySet()得到一组组HashMap$Node类型对应的Key-Value
2.单独管理Key时:
我们可以把其所有的Key封装到Set这个集合
之后通过调用【下图所示的方法】来得到一份份HashMap$Node类型对应的Key值
3. 单独管理Value时:
我们可以把所有的Value放到Collection这个集合中去,之后通过调用【下图所示的方法】来得到一份份HashMap$Node类型对应的Value值
4.
其实无论是单独管理Key,Value。还是统一管理Key-Value。
核心思想就是一个:我们是把地址传过去到对应的集合中了,而不是把对象复制一份过去。
整体的指向如图,单个元素对象的指向省略画出指向了。。。是随机分布的。。。
————————————————————————
Map接口常用方法:
Map接口六大遍历方式
分析一下红色圈起来的部分:
getClass方法表示得到运行类型。EntrySet<Map.Entry<K,V>>
迭代器每一次得出的next都是对应的EntrySet集合中的每一个元素Map.Entry<K,V>
根据前面的知识我们知道,每一个元素当中存放的都是一个HashEntry$Node类型对象的地址
也就是指向一个Hashentry$Node类型对象,那么每一个得出的运行类型都是HashEntry$Node
【如图所示】
HashMap总结
【面试重点】HashMap底层源码分析
第一步先进去执行HashMap的构造器
第二步:执行put方法
由于加入的参数10是基本数据类型。所以要进行一个装箱操作,把它封装为一个对象
退出之后执行put方法‘
对于一个方法的参数又是一个方法的时候,我们点1进入putVal方法,我们点2进入参数方法hash
进入hash方法
主线putVal方法
进入resize方法进行扩容操作
HashMap部分底层源码【putVal和resize分析】
/**
* 对HashMap底层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;
//table是HashMap内部类Node<K,V>的一个属性:
//transient Node<K,V>[] table;【复制自HashMap源码】
//如果说这个table数组一开始为null,或者说长度为0,都证明没有加元素进去。
if ((tab = table) == null || (n = tab.length) == 0)
//那么要进行扩容操作,一开始数组为空时默认扩容为16,接着去分析resize()方法【见下即可】
n = (tab = resize()).length;
//根据key值 调用hashCode方法,通过哈希算法找到对应的hash值
//再通过对应的hash值找到对应的索引下标,
//如果索引处无Node,那么直接创建一个Node加入到Node<K,V>[]这个数组中去
if ((p = tab[i = (n - 1) & hash]) == null)
/**
*【复制自HashMap源码】:
* Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
*/
//创建一个新节点
tab[i] = newNode(hash, key, value, null);
//如果说通过key值找到hash值,对应hash值找到索引下标时,
//由于这个索引处对应的Node不为null,那么进入else
else {
Node<K,V> e; K k;
//如果将要添加的Node和找到对应索引处的Node是同一个对象【分析如下】
//若这个索引位置的key对应的hash值和我们将要添加的key对应的hash值是相同的,
//但是凭上面一点不可以确定就是同一个对象
//并且对应的table数组这个索引处现有的Node的key和我们将要添加的key是同一个对象【即(k = p.key) == key】
//或者说equals返回true【equals方法可以被程序员直接重写,规则自己制定】
//那么才可以说明这俩是同一个对象,那么直接退出
//那么先保留p值,退出之后再进行value值的替换【e=p】
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果是红黑树形式,那么要用putTreeVal方法进行添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果是链条形式
else {
//先设置一个死循环
for (int binCount = 0; ; ++binCount) {
//如果一直找到最后一个Node
if ((e = p.next) == null) {
//如果一直找到最后一个Node,那么直接进行添加即可
p.next = newNode(hash, key, value, null);
//判断是否要进行红黑树树化
//树化规则:
//当一个索引处链条的长度达到8,并且table数组的Node<K,V>个数达到64
//那么才可以进行树化
//注意:这里仅仅是判断链条上的Node是否达到8个,
//判断第二个树化条件还是要去看 treeifyBin方法
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//这个和上面一样的分析
//如果发现是同一个对象,那么先保留e值【p=e】,退出之后再进行value值的替换
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果说e!=null,
//证明是因为发现找到的是同一个对象才退出的,那么就要进行value值的替换
if (e != null) {
//替换value值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
//把这个替换之后的value值返回,
return oldValue;
}
}
//如果说e==null,那么modCount++,表示的意思是添加Node加一个
++modCount;
//如果table数组用的大小大于了临界值,那么要进行2倍扩容操作
if (++size > threshold)
resize();
//退出操作
afterNodeInsertion(evict);
//返回null表示添加成功
return null;
}
/**
* 扩容机制总结:
* //16*0.75=12 12作为临界值。当数组元素超过12这个临界值之后,
//再进行扩容,按照2倍扩容,扩容到32 那么新的临界值是:32*0.75==24
//当数组元素个数大于等于24时 再进行下一次扩容 32*2=64 临界值变化为64*0.75==48
//以此类推
//直到扩容到64之后,且索引处链条长度达到8之后,会进行红黑树树化
*/
final Node<K,V>[] resize() {
//transient Node<K,V>[] table;【复制自HashMap源码】
//把table赋给oldTab数组
Node<K,V>[] oldTab = table;
//也就是把原来table数组的长度给到oldCap
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// int threshold;【复制自HashMap源码】:threshold也是HashMap的一个属性
int oldThr = threshold;
int newCap, newThr = 0;
//如果原来的数组长度不为0
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0)
newCap = oldThr;
//如果原来的数组长度为0
else {
//static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;【复制自HashMap源码】//即是16
newCap = DEFAULT_INITIAL_CAPACITY;
//static final float DEFAULT_LOAD_FACTOR = 0.75f;【复制自HashMap源码】
//转化因子为0.75 每一次确定一个边界值
//当添加达到这个边界值的时候 就要开始进行扩容了
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果边界值newThr为0,那么要进行更改这个边界值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//把这个边界值给到threshold
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//进行创建一个新数组newTab【数组中每一个元素的类型是Node<K,V>】,设置大小为newCap
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//把这个数组赋给table数组即可
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
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);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//返回这个新创建的数组
return newTab;
}
图片形式如图:
模拟HashMap触发扩容,树化情况
每一次哈希值相同,那么每一次找到的索引下标是相同的
但是我们这里每一次new的对象是不同的,所以key值对应的对象是不同的。
并且equals方法也没有进行重写,所以比较的还是是否是同一个对象地址。
那么比较之后判断出不是一个对象,那么会形成链条进行排布起来。。。。。
如图所示:一个接一个的挂在了一起,形成链条状。。。。
进行树化之前都还是HashMap$Node
我们继续进行扩容,直到扩容64
当扩容到64,并且链条的长度达到了8,那么就将会进行树化。。。。
当运行类型是TreeNode的时候,就会进行树类型的添加了。。。。上图也是TreeNode。。
那么就证明出,树化了已经。。。。 已经变成了红黑树了啊
HashMap源码中putVal方法中的核心添加规则:
先根据哈希值找到一个索引值:
如果该索引值处Node为null,那么直接进行put添加
如果该索引处Node不为null,那么执行以下操作:
——————————
这里插入一下比较两个对象是否是同一个对象的比较规则:
【
我们列出比较两个对象是否为同一个对象的判断规则:
第一点:根据key值得到的哈希值是相同的,
第二点:进行比较是否是key对应是否为同一个对象或看对应的equals方法返回的是否为true【第一点必须满足。第二点中的两个条件有一个满足即认为第二点满足】
】
——————————
如果两点都满足,证明是同一个对象,那么直接进行value值的覆盖替换。
如果不满足,那么看是否是红黑树状,如果是,那么进行树状时的添加方式进行添加
如果不满足,看是否是链条,如果是,
那么进去之后进去循环比较,如果找到一个Node是相同的【相同的判断规则是:根据前面两点判断规则】,那么用这个将要添加的value值覆盖这个链条上找到的相同对象的Node。如果找到最后还找不到,那么挂在后面,并且看是否满足树化的规则,如果满足记得进行树化操作。
——————————————————————————————
Map实现类之HashTable
1.
4.
hashTable是线程安全的 最常用的put方法 是由synchronized修饰的,具有线程同步和互斥的作用
synchronized
synchronized修饰,具有线程同步和互斥的作用。
HashTable底层分析put方法:
这里我们先通过key值求得一个哈希值,通过这个哈希值我们可以找到一个索引位置。。
之后把这个key和value封装为一个Entry对象,之后再存放到table数组。
——————————————
下图中这个是Map接口大部分实现类【如HashMap】的特点,
上面那个HashMap$Entry是Map接口实现类Hashtable的独有特点。
图中这个分析一下:
把这个key和value封装为一个Node对象之后
之后再把地址给到Entry接口对象,之后再搞出来一个集合EntrySet来装这些Entry指向管理的存在table数组里面的封装完成的对象【本文开头第八点是细讲分析】
————————————————————
Hashtable总结
4.
扩容机制:
Hashtable put方法源码分析
进入put方法
在put方法中调用的addEntry方法
进入addEntry方法
执行addEntry方法
之后把这个key和value封装为一个Entry对象,之后再存放到table数组。
调用Entry类构造器
封装完成之后,存放到table数组中去
完成任务,退出即可。。。。
Map接口实现类之Properties【继承Hashtable并且实现Map接口】
第三点:
如果我们单纯的把用户名和密码直接写到代码中的程序中,当我们调用外部的数据库的时候。我们不可以保证数据库的用户名和密码是与我们程序中的用户名和密码是一样的。
如果直接进行修改程序中的内容,会十分的麻烦,它需要加载一个class文件,,
所以我们可以在外部搞一个这个玩意,把用户名和密码放到其中去
如果有相同的key,那么value值被替换
通过get(key)可以返回对应value值
【重点重点】开发如何选择Map接口实现类
jdk7中HashMap底层是数组+链表的效率比较低,链表过长效率就低了。。
jdk8我们引进了红黑树
Map接口实现类之TreeSet
为什么TreeSet可以实现排序?
因为构造器可以传入一个比较器过去
只写一个这样的,其实也是不可以进行排序的。我们是进行匿名内部类作为参数传进去一个比较器进行排序
重点TreeSet的add底层源码替换value值的分析
当我们add两个相同的字符串的时候,
输出结果为:我们发现add不成功
原因是:
如果当我们通过比较规则得出两个是相等的话,,,,,那么compareTo得到的结果为0,那么返回给compare的值为0,那么根据源码可知执行得cmp==0【自己看看上面的源码追的过程】
即是源码中的cmp==0 那么就会进行替换value值,
key值就是我们add方法传入进来的参数,比如说:上图中的"leo"
value值是一个final类型静态常量值PRESENT,
但是我们知道,替换的这个value值是一个final类型静态常量值PRESENT,这是一个被共享的常量值,只是用来占value值这个位的
所以无论怎么去替换这个value值,那么key值是不会进行变化的
但是我们注意这里的key值并没有发生变化,即是并没有加入进去成功,只是替换了value值
所以搞出来还是原来的key值【看看源码方法setValue】
__________________________
变式【重点】:
我们也可以自己设置规则,按照add的字符串的长度来当作比较规则
key值就是我们add方法传入进来的参数,比如说:图中的"jack"
value值是一个final类型静态常量值PRESENT,
当我们再添加一个abc,结果只显示tom
为什么?
如果当我们通过比较规则得出两个是相等的话,,,,,那么compareTo得到的结果为0,那么返回给compare的值为0,那么根据源码可知执行得cmp==0【自己看看上面的源码追的过程】
key值就是我们add方法传入进来的参数,比如说:图中的"jack"
value值是一个final类型静态常量值PRESENT,
但是这个替换只是进行value值替换,然而我们又知道每一个value值,是由一个常量PRESENT占用的。【见上一题的分析】
这里setValue方法,只是替换value,key值还是原来的key,那么就相当于啥也没干
但是我们注意这里的key值并没有发生变化,即是并没有加入进去成功,只是替换了value值
因为我们compare比较的规则是用长度来比较的,当加一个长度相等的时候,底层源码cmp==0,
TreeSet的源码 add方法及其比较器的调用:
创建对象之前 我们要进行类加载【类加载的过程跳过】
重点来了,构造器参数是比较器
惊不惊喜,意不意外,TreeSet底层是TreeMap
我们知道TreeMap是继承AbstractMap的,所以我们知道子类构造器第一行默认是super()
super()就是调用对应父类构造器
开始add添加
进去add
再进去
一开始add第一个的时候,我们必须自己提供一个compare方法
返回
执行String类的compareTo方法进行比较
add第二个
直接到最关键的地方:
这个comparator就相当于传入的构造器的参数【即是那个匿名内部类】
那么这个cpr就相当于那个匿名内部类【构造器和匿名内部类如下所示】
我们继续追
当调用到这一句时,发生运行时绑定,cpr的运行类型所在的类即是那个匿名内部类
所以cpr一开始调用这个compare方法就是从那个匿名内部类开始找的,发现正好重写了这个compare方法。那么直接调用这个compare方法
调用String类的compareTo方法
key值就是我们add方法传入进来的参数,比如说:图中的"jack"
value值是一个final类型静态常量值PRESENT,
这里setValue方法,只是替换value,key值还是原来的key,那么就相当于啥也没干
——————————
【重点调试TreeSet底层源码】
1.
当不传comparator比较器的时候,会默认调用String类的CompareTo方法
调试一下:
直接看重点:
重点分析向下转型,强制转化的情况
进入else之后,,
此时key=="jack" 无论是哪一个key 用add加入进去,编译类型都是Object 。运行类型可能不一样,
我们通过肉眼可以看出来,,"jack"这个key的运行类型是一个String类型的,所以我们可以直接进行强制转换
因为String类实现了Comparable<String>接口,记住一点无论add哪一个key,这个key的运行类型是不确定的,但是编译类型一定是Object类型。这也是向下转型的根本所在,
【如果一开始编译类型就是String,那么直接就调用String类重写Comparable<String>接口的抽象方法compareTo】
那么此时我们可以进行向下转型
即是
所以才可以把key强转为Comparable<String>类型的k。
假如说传进来的key运行类型不是String类型的【编译类型肯定是Object】,那么就不可以直接进行强制转换为Comparable<String>类型,我们要让这个key的运行类型这个类去实现Comparable接口,并且重写接口中的抽象方法
此时key=="jack" key的运行类型是String类型,编译类型是Object类型
又因为String类实现了Comparable<String>接口,所以才可以把key强转为Comparable<String>类型的k
Comparable<String>这个接口,里面还有一个compareTo方法。
所以这个String类必须实现这个compareTo方法 。
那么把key强转为k之后,那么当后面k调用compareTo方法的时候,根据运行时绑定机制,那么调用的就是String类的compareTo方法,如果String类没有,再接着向上【即父类或更高类】去寻找这个compareTo方法。
强转为Comparable类型使我们运行调用方法的更加安全,运行时直接进行了绑定,,,,
2.
当传Comparator比较器并且重写compare方法的时候,并且当我们重写compare方法的时候,也可以调用String类的compareTo方法,当然也可以自己去设置比较的规则哈哈哈哈
为什么要重写compare方法?
【如图源码可知Comparator接口 有一个抽象方法compare 实现接口的类必须实现啊】
直接到重点
运行时绑定调用自己传的比较器,即是匿名内部类的方法compare方法
调用String类的compareTo方法
Map接口实现类之TreeMap
TreeMap底层源码分析就省略了,和上面的差不多,因为TreeSet底层就是TreeMap啊哈哈哈
唯一一点不同的地方在于,
我们这里value值不再是一个final的静态常量了,而是一个真真切切的value值,
如果当我们通过比较规则得出两个是相等的话,,,,,那么compareTo得到的结果为0,那么返回给compare的值为0,那么根据源码可知执行得cmp==0【自己看看上面的源码追的过程】
那么即是当cmp==0 执行setValue方法的时候
key值不发生变化,但是value值被进行替换。
如图:dsa被替换为"替换的value值"
但是我们注意这里的key值并没有发生变化,即是并没有加入进去成功,只是替换了value值
源码如下:
这个compare方法是为了,检测第一次加入进来的对象key-value是否可能存在null
如果存在,那么要抛出异常
Collections工具类
用到抽奖游戏中去
分析String类方法compareTo源码
分析如下:
value就是调用这个方法的对象对应的value属性
如 a.compareTo(b)。那么a对象对应value属性
集合作业
HashMap
前三问:
四五问
【重点】迭代器总结
首先我们要明白一点,我们add的key,编译类型都是Object 但是运行类型不清楚,不确定
——————————
正如上面所说:
因为String类实现了Comparable<String>接口,那么此时我们可以进行向下转型
把String类型的key(即是键)向下转型为Comparable<String>类型
即是
所以才可以把key强转为Comparable<String>类型的k。
——————————
假如说传进来的key运行类型不是String类型的,并且该key的运行类型的这个类没有实现这个接口。【【我们add的key,编译类型都是Object 但是运行类型不清楚,不确定】】
那么就不可以直接进行强制转换为Comparable<String>类型
本题中传进去的key运行类型是new Person()类型,运行类型不是String类型,也没有实现Comparable<String>接口,所以不可以直接进行源码上的强制转化
【我们add的key,编译类型都是Object 但是运行类型不清楚,不确定】
————————
执行代码及结果如下:
所以当没有传比较器的时候,追溯到源码分析可知,它若强转会发生异常
——————————————
解决办法很简单哈哈哈哈哈,
就是让这个key的运行类型所在的类 即Person类实现Comparable接口
并且把这个接口中的抽象方法进行重写即可
————————
【重点】
但是假如说重写的compareTo方法直接返回0的话,只能加入一个key,
调试源码分析一下:
我们没有比较器,那么进入else之后,因为上面Person类已经实现了这个接口,所以可以把运行类型为new Person(),编译类型为Object的key强转为 运行类型和编译类型都是Comparable接口类型的k
继续调试:
运行时绑定,k调用Person类重写实现的这个compareTo方法 原因如下:
强转转化为Comparable接口类型之后,k调用compareTo方法,发现接口中的compareTo方法压根就没有实现,那么只能调用实现接口类Person重写的方法,一种类似于运行时绑定的机制
cmp接收返回值,但是重写的compareTo方法,无论如何只会返回0
那么cmp一直等于0,那么一直调用setValue方法【上面有分析这个方法的底层源码】
value这个常量值,一直在被替换,key值一直不会变化
那么只可以添加一个key(键)
测试结果如图:【只会add成功一个key】
HashSet底层还是HashMap
分析:
第一个打印:
当我们改p1的name之后,把AA改为CC了。
所以当我们set.remove(p1) 但是p1中 构造器参数被改了,即是这个key(即是键)是变化了啊。
我们下一次通过key(即是键)得到的哈希值可能和上一次不相同,所以找到索引位置也可能不相同,所以删除不成功。
那么第一个打印 打印2
第二个打印:
通过上面我们可以知道,
当我们改p1的name之后,把AA改为CC了。
但是p1中 构造器参数被改了,即是这个key(即是键)是变化了啊。
我们下一次通过key(即是键)得到的哈希值可能和上一次不相同,所以找到索引位置也可能不相同,
这个索引处无Node,
所以我们可以add添加成功
那么第二个打印 打印3
第三个打印:
当我们添加一个new Person(1001,"AA")的时候,我们会找到索引1,也即是p1所在处
因为在p1改变name之前,就已经add添加了【自己想一想】
由于当p1改变name之后 那么key肯定和新加的new Person(1001,"AA")不一样,所以会以链条的形式挂在后面。。【上面源码有分析这种情况,哪天忘了把源码自己再调试调试就好】
那么第三个打印 打印4
【530-553】