文章目录
HashMap详解
初始化
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;
}
初始化时调用此方法返回2的次幂 如 cap = 14 ; n = 14 - 1二进制为1101; >>>无符号右移
(1) 1101 |= 1101 >>> 1 (0110) ; 结果 1111
(2) 1111 |= 1101 >>> 2; 结果 1111
其余搞完结果 1111
最后 1111 + 1 = 16
则返回16
当选则指定大小创建hashMap时,初始化值自动转为2的次幂
cap - 1 的意义为 防止数值不对,如16高位不减1 则输出值为32 10000 | 1111 结果 11111
putVal
putVal(hash(key), key, value, false, true);
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
首先会对 传入key 计算hashcode算法位传入对象的hashcode算
为什么要右移16位?
1保证高16位也参与计算, 我们直到int占4字节 32位,16是中位数
2因为大部分情况下,都是低16位参与运算,高16位可以减少hash冲突
代码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//声明所需变量,tab要操作的数组 p接点代表 n数组长度 i 数组中的位置
Node<K,V>[] tab; Node<K,V> p; int n, i;
//tab变量 n变量赋值
if ((tab = table) == null || (n = tab.length) == 0)
//如果hashmap 还未操作过则重新设置数组 //resize 后面讲
n = (tab = resize()).length;
//确定要添加元素的位置
//为什么要 (n - 1) & hash??? 答:合理确定数组中的位置, 更均匀的分配
//在初始化数组大小时为什么要限制位2的次幂??? 答:因为2的次幂如减一则 后面位置全为1保证跟均匀分配每位都有意义 如 16 二进制 10000 - 1 = 01111
//如果不是 2次幂 如 5 101 - 1 = 100 因为 任何一个数&0都是0这样hash冲突会变大
if ((p = tab[i = (n - 1) & hash]) == null)
//创建一个node节点
tab[i] = newNode(hash, key, value, null);
else {
//如果不是空,那么此槽位置可能是链表或者红黑树
Node<K,V> e; K k;
//如果入参hash在数组中已经存在且是当前节点直接替换
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);
如果链表长度大于等 7 尝试转换成树,届时里边还会判断数组个数 如个数小于64则resize扩容
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//转树
treeifyBin(tab, hash);
break;
}
//结束条件,找到相同hash匹配节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//指向下一个节点
p = e;
}
}
//不等于空则替换value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//后置处理
afterNodeAccess(e);
return oldValue;
}
}
//修改次数++ 用于快速失败,如并抛出发修改异常
++modCount;
//如果当前数组长度大于临界值
if (++size > threshold)
//比较判断扩容
resize();
//后置处理
afterNodeInsertion(evict);
return null;
}
resize扩容
原理:老数组长度如大于0,则数组长度 << 1即乘 2 后重新hash计算确定每个值的槽位置
代码
final Node<K,V>[] resize() {
//必要属性相关初始化
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//如果已经加过元素
if (oldCap > 0) {
//如果数组已经是最大长度了,设置临界值为最大返回此数组
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
} //扩容新容量,并判断容量是否大于默认值16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//如果大于16 新临界值等于旧临界值×2
//为什么大于 16临界值才可以 <<计算? 答:保证计算精度,如果负载因子特殊,则此 计算不一定准确,即 newCap * loadFactor 不一定等于 2 * **(oldCap * loadFactory)** 一直乘2
//例子:
/* float f = 0.6797f;
float l = 1.34f;
System.out.println(64*f);
for (int i = 0; i < 5; i++) {
l=l * 2;
}
System.out.println(l);*/
//上述代码输出 43.500 42.88
//所以达到一定精度在转换
newThr = oldThr << 1; // double threshold
}//已经有初始化的临界
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//没有值默认初始化为16 临界值12 负载因子0.75
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//创建新的数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
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;
}
常见的hash算法
Hash算法的特点
输入敏感:原始输入信息发生任何变化,新的Hash值都应该出现很大变化。
不可逆性:给定明文和Hash算法,在有限时间和有限资源内能计算得到Hash值。但是给定Hash值,在有限时间内很难逆推出明文。
冲突避免:很难找到两段内容不同的明文,使得它们的Hash值一致。
常用的构造散列函数的方法
散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位:
1. 直接寻址法:取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a?key + b,其中a和b为常数(这种散列函数叫做自身函数)
2. 数字分析法:分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
3. 平方取中法:取关键字平方后的中间几位作为散列地址。
4. 折叠法:将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。
5. 随机数法:选择一随机函数,取关键字的随机值作为散列地址,通常用于关键字长度不同的场合。
6. 除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p, p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。
处理冲突的方法
1. 开放寻址法;Hi=(H(key) + di) MOD m, i=1,2,…, k(k<=m-1),其中H(key)为散列函数,m为散列表长,di为增量序列,可有下列三种取法:
(1) di=1,2,3,…, m-1,称线性探测再散列;
(2)di=1^2, (-1)^2, 22,(-2)2, (3)^2, …, ±(k)^2,(k<=m/2)称二次探测再散列;
(3)di=伪随机数序列,称伪随机探测再散列。 ==
2. 再散列法:Hi=RHi(key), i=1,2,…,k RHi均是不同的散列函数,即在同义词产生地址冲突时计算另一个散列函数地址,直到冲突不再发生,这种方法不易产生“聚集”,但增加了计算时间。
3. 链地址法(拉链法)
4. 建立一个公共溢出区
TreeMap和TreeSet的深入理解
旋转
树的左旋和右旋的过程用一个图来表示比较简单直观:
从图中可以看到,我们的左旋和右旋主要是通过交换两个节点的位置,同时将一个节点的子节点转变为另外一个节点的子节点。具体以左旋为例,在旋转前,x是y的父节点。旋转之后,y成为x的父节点,同时y的左子节点成为x的右子节点。x原来的父节点成为后面y的父节点。这么一通折腾过程就成为左旋了。同理,我们也可以得到右旋的过程。
红黑树的官方定义如下:
红黑树是一种二叉树,同时它还满足下列5个特性:
-
每个节点是红色或者黑色的。
-
根节点是黑色的。
-
每个叶节点是黑色的。(这里将叶节点的左右空子节点作为一个特殊的节点对待,设定他们必须是黑色的。)
-
如果一个节点是红色的,则它的左右子节点都必须是黑色的。
-
对任意一个节点来说,从它到叶节点的所有路径必须包含相同数目的黑色节点。
这部分的定义看得让人有点不知所云,我们先看一个红黑树的示例
treeSet怎么排序
A:自然排序:要在自定义类中实现Comparerable接口 ,并且重写compareTo方法
B:比较器排序:在自定义类中实现Comparetor接口,重写compare方法
2.自然排序
自然排序要进行一下操作:
1.Student类中实现 Comparable接口
2.重写Comparable接口中的Compareto方法
int compareTo(T o)
比较此对象与指定对象的顺序。
故Student类为: 特别注意在重写Compareto方法时,注意排序
如果比较引用类型的需要实现 Comparable接口,重写Compareto方法
3、比较器排序
比较器排序步骤:
1.单独创建一个比较类,这里以MyComparator为例,并且要让其继承Comparator接口
2.重写Comparator接口中的Compare方法
int compare(T o1,T o2)
比较用来排序的两个参数。
3.在主类中使用下面的 构造方法
TreeSet(Comparator<? superE> comparator)构造一个新的空 TreeSet,它根据指定比较器进行排序。
版权声明:本文为CSDN博主「晓锋残月」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xiaofei__/article/details/53138681
四.数组(Array)和列表(ArrayList)有什么区别?什么时候应该使用 Array 而不是
ArrayList?
答:不同点:定义上:Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类
型。容量上:Array 大小固定,ArrayList 的大小是动态变化的。操作上:ArrayList 提供更多
的方法和特性,如:addAll(),removeAll(),iterator()等等。使用基本数据类型或者知道数
据元素数量的时候可以考虑 Array;ArrayList 处理固定数量的基本类型数据类型时会自动装
箱来减少编码工作量,但是相对较慢。
2.15 快速失败(fail-fast)和安全失败(fail-safe) 一:快速失败(fail—fast)
在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增
加、删除、修改),则会抛出 Concurrent Modification Exception。
原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCo
unt 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使
用 hashNext()/next()遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmod
Count 值,是的话就返回遍历;否则抛出异常,终止遍历。
注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如
果集合发生变化时修改 modCount 值刚好又设置为了 expectedmodCount 值,则异常不会
抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检
测并发修改的 bug。
场景:java.util 包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过
程中被修改)。
二:安全失败(fail—safe)
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原
有集合内容,在拷贝的集合上进行遍历。
原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改
并不能被迭代器检测到,所以不会触发 Concurrent Modification Exception。
缺点:基于拷贝内容的优点是避免了 Concurrent Modification Exception,但同样地,
迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,
在遍历期间原集合发生的修改迭代器是不知道的。
场景:java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并
发修改。
快速失败和安全失败是对迭代器而言的。 快速失败:当在迭代一个集合的时候,如果有另
外一个线程在修改这个集合,就会抛出 ConcurrentModification 异常,java.util 下都是快速
失败。 安全失败:在迭代时候会在集合二层做一个拷贝,所以在修改集合上层元素不会影
响下层。在 java.util.concurrent 下都是安全失败