1 log4j
主流日志框架
1.JUL —不能分天,分文件,被支持淘汰
2.log4j —老牌主流的日志框架
日志门面框架
3.JCL —仅支持JUL和log4j,被支持淘汰
4.slf4j —市场上比较主流的日志框架
5.logback —springboot默认的日志实现框架
6.log4j2 —logback出来后,log4j就很少人用了,于是apache就推出了log4j2,功能上和logback很像
现在有log4j2的人比 较少。slf4j门面+log4j2实现 应该是未来的大势所趋
1.1 理论知识
1.1.1 Loggers日志记录器
Loggers 日志记录器 ——-控制日志的输出级别和日志是否输出
logger.fatal(“fatal”); //严重错误,一般会造成系统崩溃并终止运行
logger.error(“error”); //错误信息,不会影响系统运行
logger.warn(“warn”); //警告信息,可能会发生问题
logger.info(“info”); //程序运行信息
logger.debug(“debug”); //调试信息,一般在开发中使用,
记录程序变量参数传递信息
logger.trace(“trace”); //追踪信息,记录程序所有的流程信息
1.1.2 Appenders输出端
Appenders 输出端 ——指定日志的输出方式(输出到控制台,文件)
ConsoleAppender 将日志输出到控制台
FileAppender 将日志输出到文件(意义不大)
DailyRollingFileAppender 按日期拆分 将日志输出到一个日志文件,
并且每个输出到一个新的文件
RollingFileAppender 按大小拆分 将日志信息输出到一个日志文件,
并且制定文件的尺寸,当文件大小达到制定
尺寸时,会自动把文件改名,同时产生一个新的文件
1.1.3 Layout日志格式化器
Layout 日志格式化器 —控制日志信息的输出格式
HTMLLayout html表格形式
SimpleLayout 简单的日志输出格式 info–message
PatternLayout 最强大的格式化器 一般使用这种
一般格式为:%d{yyyy-mm-dd HH:mm:ss} [%-5p]%r %l %m%n
%p 输出优先级及debug,info%r 输出自应用启动到输出该log信息耗费的毫秒数%d 输出服务器当前时间 默认为ISO08601 也可以指定格式,如:%d{yyyy年MM月dd日 HH:mm:ss}%l 输出日志发生的位置 包括类名,线程,在代码中的行数 如:Test.main(Test.java:10)# %l = %c %t %F %L%c 输出打印语句所属的类的全名%t 输出产生该日志的线程全名%F 输出日志消息产生时所在的文件名称%L 输出代码中的行数%% 输出一个"%"字符%m 输出代码中指定的日志信息%n 换行%5 宽度为5 右对齐%-5 宽度为5 左对齐
1.2 代码实现
1.2.1 maven依赖
<dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.17</version></dependency>
1.2.2 log4j.properties
#指定RootLogger顶级父元素默认的配置信息#指定日志级别为=trace 使用的appender为=console控制台# trace级别 控制台 日期info级别 日期error级别 按大小log4j.rootLogger = trace,console,dailyInfoFile,dailyErrorFile,rollingFile#指定控制台日志输出appender对象log4j.appender.console=org.apache.log4j.ConsoleAppender#日志输出消息格式:SimpleLayout info--message 其中 PatternLayout用得最多log4j.appender.console.layout = org.apache.log4j.PatternLayout#指定消息格式的内容 标准格式:%d{yyyy-mm-dd HH:mm:ss} [%-5p]%r %l %m%nlog4j.appender.console.layout.conversionPattern =%d{yyyy-mm-dd HH:mm:ss} [%-5p]%r %l %m%n#不拆分,永远一个日志文件#指定文件日志输出appender对象log4j.appender.file=org.apache.log4j.FileAppender#日志输出消息格式:log4j.appender.file.layout = org.apache.log4j.PatternLayout#指定消息格式的内容log4j.appender.file.layout.conversionPattern =%d{yyyy-mm-dd HH:mm:ss} [%-5p]%r %l %m%n#指定日志文件保存路径log4j.appender.file.file= /logs/log4j.log#指定日志文件的字符集log4j.appender.file.encoding = UTF-8#按日期_info_拆分#指定文件日志输出appender对象log4j.appender.dailyInfoFile=org.apache.log4j.DailyRollingFileAppender#日志输出消息格式:log4j.appender.dailyInfoFile.layout = org.apache.log4j.PatternLayout#指定消息格式的内容log4j.appender.dailyInfoFile.layout.conversionPattern =%d{yyyy-mm-dd HH:mm:ss} [%-5p]%r %l %m%n#指定日志文件保存路径log4j.appender.dailyInfoFile.file= /logs/daily_info.log#指定日志文件的字符集log4j.appender.dailyInfoFile.encoding = UTF-8#按指定日期拆分规则: 1份/天 1份/时log4j.appender.dailyInfoFile.datePattern = '.'yyyy-MM-dd HH-mm-ss#输出info等级日志 到该文件log4j.appender.dailyInfoFile.threshold = info#按日期_error_拆分#指定文件日志输出appender对象log4j.appender.dailyErrorFile=org.apache.log4j.DailyRollingFileAppender#日志输出消息格式:log4j.appender.dailyErrorFile.layout = org.apache.log4j.PatternLayout#指定消息格式的内容log4j.appender.dailyErrorFile.layout.conversionPattern =%d{yyyy-mm-dd HH:mm:ss} [%-5p]%r %l %m%n#指定日志文件保存路径log4j.appender.dailyErrorFile.file= /logs/daily_error.log#指定日志文件的字符集log4j.appender.dailyErrorFile.encoding = UTF-8#按指定日期拆分规则: 1份/天 1份/时log4j.appender.dailyErrorFile.datePattern = '.'yyyy-MM-dd HH-mm-ss#输出error等级日志 到该文件log4j.appender.dailyErrorFile.threshold = error#按大小拆分#指定文件日志输出appender对象log4j.appender.rollingFile=org.apache.log4j.RollingFileAppender#日志输出消息格式:log4j.appender.rollingFile.layout = org.apache.log4j.PatternLayout#指定消息格式的内容log4j.appender.rollingFile.layout.conversionPattern =%d{yyyy-mm-dd HH:mm:ss} [%-5p]%r %l %m%n#指定日志文件保存路径log4j.appender.rollingFile.file= /logs/rollingFile.log#指定日志文件的字符集log4j.appender.rollingFile.encoding = UTF-8#指定日志文件内容的大小log4j.appender.rollingFile.maxFileSize = 1MB#指定日志文件的数量log4j.appender.rollingFile.maxBackupIndex =10
1.2.3 Log4jTest.java
package com.tangguanlin.log;import org.apache.log4j.Logger;/*** 说明:log4j的使用*/public class Log4jTest {public static void main(String[] args) {//获取日志记录器对象Logger logger = Logger.getLogger(Log4jTest.class);//日志记录输出//日志级别logger.fatal("fatal"); //严重错误,一般会造成系统崩溃并终止运行logger.error("error"); //错误信息,不会影响系统运行logger.warn("warn"); //警告信息,可能会发生问题logger.info("info"); //程序运行信息logger.debug("debug"); //调试信息,一般在开发中使用,记录程序变量参数传递信息logger.trace("trace"); //追踪信息,记录程序所有的流程信息}}
Token ,Cookie和Session的区别
Cookie
cookie 是一个非常具体的东西,指的就是浏览器里面能永久存储的一种数据,仅仅是浏览器实现的一种数据存储功能
cookie由服务器生成,发送给浏览器,浏览器把cookie以kv形式保存到某个目录下的文本文件内,下一次请求同一网站时会把该cookie发送给服务器
由于cookie是存在客户端上的,所以浏览器加入了一些限制确保cookie不会被恶意使用,同时不会占据太多磁盘空间,所以每个域的cookie数量是有限的
Session
session 从字面上讲,就是会话。这个就类似于你和一个人交谈,你怎么知道当前和你交谈的是张三而不是李四呢?对方肯定有某种特征(长相等)表明他就是张三
session 也是类似的道理,服务器要知道当前发请求给自己的是谁。为了做这种区分,服务器就要给每个客户端分配不同的“身份标识”,然后客户端每次向服务器发请求的时候,都带上这个“身份标识”,服务器就知道这个请求来自于谁了。至于客户端怎么保存这个“身份标识”,可以有很多种方式,对于浏览器客户端,大家都默认采用 cookie 的方式
服务器使用session把用户的信息临时保存在了服务器上,用户离开网站后session会被销毁。这种用户信息存储方式相对cookie来说更安全,可是session有一个缺陷:如果web服务器做了负载均衡,那么下一个操作请求到了另一台服务器的时候session会丢失
Token
Token的引入:Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token便应运而生
Token的定义:Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。最简单的token组成:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,由token的前几位+盐以哈希算法压缩成一定长的十六进制字符串,可以防止恶意第三方拼接token请求服务器)
使用Token的目的:Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮
HashMap的底层原理
HashMap存储结构
这里需要区分一下,JDK1.7和 JDK1.8之后的 HashMap 存储结构
在JDK1.7及之前,是用数组加链表的方式存储的
但是,众所周知,当链表的长度特别长的时候,查询效率将直线下降,查询的时间复杂度为 O(n)
因此,JDK1.8 把它设计为达到一个特定的阈值之后,就将链表转化为红黑树
这里简单说下红黑树的特点:
- 每个节点只有两种颜色:红色或者黑色
- 根节点必须是黑色
- 每个叶子节点(NIL)都是黑色的空节点
- 从根节点到叶子节点,不能出现两个连续的红色节点
- 从任一节点出发,到它下边的子节点的路径包含的黑色节点数目都相同
由于红黑树,是一个自平衡的二叉搜索树,因此可以使查询的时间复杂度降为O(logn)
HashMap 结构示意图:
常用的变量
在 HashMap源码中,比较重要的常用变量,主要有以下这些。还有两个内部类来表示普通链表的节点和红黑树节点
//默认的初始化容量为16,必须是2的n次幂static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16//最大容量为 2^30static final int MAXIMUM_CAPACITY = 1 << 30;//默认的加载因子0.75,乘以数组容量得到的值,用来表示元素个数达到多少时,需要扩容。//为什么设置 0.75 这个值呢,简单来说就是时间和空间的权衡。//若小于0.75如0.5,则数组长度达到一半大小就需要扩容,空间使用率大大降低,//若大于0.75如0.8,则会增大hash冲突的概率,影响查询效率。static final float DEFAULT_LOAD_FACTOR = 0.75f;//刚才提到了当链表长度过长时,会有一个阈值,超过这个阈值8就会转化为红黑树static final int TREEIFY_THRESHOLD = 8;//当红黑树上的元素个数,减少到6个时,就退化为链表static final int UNTREEIFY_THRESHOLD = 6;//链表转化为红黑树,除了有阈值的限制,还有另外一个限制,需要数组容量至少达到64,才会树化。//这是为了避免,数组扩容和树化阈值之间的冲突。static final int MIN_TREEIFY_CAPACITY = 64;//存放所有Node节点的数组transient Node<K,V>[] table;//存放所有的键值对transient Set<Map.Entry<K,V>> entrySet;//map中的实际键值对个数,即数组中元素个数transient int size;//每次结构改变时,都会自增,fail-fast机制,这是一种错误检测机制。//当迭代集合的时候,如果结构发生改变,则会发生 fail-fast,抛出异常。transient int modCount;//数组扩容阈值int threshold;//加载因子final float loadFactor;//普通单向链表节点类static class Node<K,V> implements Map.Entry<K,V> {//key的hash值,put和get的时候都需要用到它来确定元素在数组中的位置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;}}//转化为红黑树的节点类static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {//当前节点的父节点TreeNode<K,V> parent;//左孩子节点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);}}
HashMap 构造函数
HashMap有四个构造函数可供我们使用
//默认无参构造,指定一个默认的加载因子public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR;}//可指定容量的有参构造,但是需要注意当前我们指定的容量并不一定就是实际的容量,下面会说public HashMap(int initialCapacity) {//同样使用默认加载因子this(initialCapacity, DEFAULT_LOAD_FACTOR);}//可指定容量和加载因子,但是笔者不建议自己手动指定非0.75的加载因子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;//这里就是把我们指定的容量改为一个大于它的的最小的2次幂值,如传过来的容量是14,则返回16//注意这里,按理说返回的值应该赋值给 capacity,即保证数组容量总是2的n次幂,为什么这里赋值给了 threshold 呢?//先卖个关子,等到 resize 的时候再说this.threshold = tableSizeFor(initialCapacity);}//可传入一个已有的mappublic HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);}//把传入的map里边的元素都加载到当前mapfinal void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {int s = m.size();if (s > 0) {if (table == null) { // pre-sizefloat ft = ((float)s / loadFactor) + 1.0F;int t = ((ft < (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);if (t > threshold)threshold = tableSizeFor(t);}else if (s > threshold)resize();for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {K key = e.getKey();V value = e.getValue();//put方法的具体实现,后边讲putVal(hash(key), key, value, false, evict);}}}
tableSizeFor()
上边的第三个构造函数中,调用了 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;}
我们以传入参数为14 来举例,计算这个过程。
首先,14传进去之后先减1,n此时为13。然后是一系列的无符号右移运算,将它转为十进制,就是 2^4 = 16
我们会发现一个规律,以上的右移运算,最终会把最低位的值都转化为 1111 这样的结构,然后再加1,就是1 0000 这样的结构,它一定是 2的n次幂
因此,这个方法返回的就是大于当前传入值的最小(最接近当前值)的一个2的n次幂的值
put()方法详解
//put方法,会先调用一个hash()方法,得到当前key的一个hash值,//用于确定当前key应该存放在数组的哪个下标位置//这里的 hash方法,我们姑且先认为是key.hashCode(),其实不是的,一会儿细讲public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}//把hash值和当前的key,value传入进来//这里onlyIfAbsent如果为true,表明不能修改已经存在的值,因此我们传入false//evict只有在方法 afterNodeInsertion(boolean evict) { }用到,可以看到它是一个空实现,因此不用关注这个参数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是否为空,如果空的话,会先调用resize扩容if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//根据当前key的hash值找到它在数组中的下标,判断当前下标位置是否已经存在元素,//若没有,则把key、value包装成Node节点,直接添加到此位置。// i = (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;//1.当前位置元素的hash值等于传过来的hash,并且他们的key值也相等,//则把p赋值给e,跳转到①处,后续需要做值的覆盖处理if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;//2.如果当前是红黑树结构,则把它加入到红黑树else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {//3.说明此位置已存在元素,并且是普通链表结构,则采用尾插法,把新节点加入到链表尾部for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {//如果头结点的下一个节点为空,则插入新节点p.next = newNode(hash, key, value, null);//如果在插入的过程中,链表长度超过了8,则转化为红黑树if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);//插入成功之后,跳出循环,跳转到①处break;}//若在链表中找到了相同key的话,直接退出循环,跳转到①处if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}//①//说明发生了碰撞,e代表的是旧值,因此节点位置不变,但是需要替换为新值if (e != null) { // existing mapping for keyV oldValue = e.value;//用新值替换旧值,并返回旧值。if (!onlyIfAbsent || oldValue == null)e.value = value;//看方法名字即可知,这是在node被访问之后需要做的操作。其实此处是一个空实现,//只有在 LinkedHashMap才会实现,用于实现根据访问先后顺序对元素进行排序,hashmap不提供排序功能// Callbacks to allow LinkedHashMap post-actions//void afterNodeAccess(Node<K,V> p) { }afterNodeAccess(e);return oldValue;}}//fail-fast机制++modCount;//如果当前数组中的元素个数超过阈值,则扩容if (++size > threshold)resize();//同样的空实现afterNodeInsertion(evict);return null;}
hash()计算原理
前面 put 方法中说到,需要先把当前key进行哈希处理,我们看下这个方法是怎么实现的
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
这里,会先判断key是否为空,若为空则返回0。这也说明了hashMap是支持key传 null 的。若非空,则先计算key的hashCode值,赋值给h,然后把h右移16位,并与原来的h进行异或处理
resize() 扩容机制
在上边 put 方法中,我们会发现,当数组为空的时候,会调用 resize 方法,当数组的 size 大于阈值的时候,也会调用 resize方法。
final Node<K,V>[] resize() {
//旧数组
Node<K,V>[] oldTab = table;
//旧数组的容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//旧数组的扩容阈值,注意看,这里取的是当前对象的 threshold 值,下边的第2种情况会用到。
int oldThr = threshold;
//初始化新数组的容量和阈值,分三种情况讨论。
int newCap, newThr = 0;
//1.当旧数组的容量大于0时,说明在这之前肯定调用过 resize扩容过一次,才会导致旧容量不为0。
//为什么这样说呢,之前我在 tableSizeFor 卖了个关子,需要注意的是,它返回的值是赋给了 threshold 而不是 capacity。
//我们在这之前,压根就没有在任何地方看到过,它给 capacity 赋初始值。
if (oldCap > 0) {
//容量达到了最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//新数组的容量和阈值都扩大原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//2.到这里,说明 oldCap <= 0,并且 oldThr(threshold) > 0,这就是 map 初始化的时候,第一次调用 resize的情况
//而 oldThr的值等于 threshold,此时的 threshold 是通过 tableSizeFor 方法得到的一个2的n次幂的值(我们以16为例)。
//因此,需要把 oldThr 的值,也就是 threshold ,赋值给新数组的容量 newCap,以保证数组的容量是2的n次幂。
//所以我们可以得出结论,当map第一次 put 元素的时候,就会走到这个分支,把数组的容量设置为正确的值(2的n次幂)
//但是,此时 threshold 的值也是2的n次幂,这不对啊,它应该是数组的容量乘以加载因子才对。别着急,这个会在③处理。
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//3.到这里,说明 oldCap 和 oldThr 都是小于等于0的。也说明我们的map是通过默认无参构造来创建的,
//于是,数组的容量和阈值都取默认值就可以了,即 16 和 12。
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//③ 这里就是处理第2种情况,因为只有这种情况 newThr 才为0,
//因此计算 newThr(用 newCap即16 乘以加载因子 0.75,得到 12) ,并把它赋值给 threshold
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//赋予 threshold 正确的值,表示数组下次需要扩容的阈值(此时就把原来的 16 修正为了 12)。
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//我们可以发现,在构造函数时,并没有创建数组,在第一次调用put方法,导致resize的时候,才会把数组创建出来。这是为了延迟加载,提高效率。
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//如果原来的数组不为空,那么我们就需要把原来数组中的元素重新分配到新的数组中
//如果是第2种情况,由于是第一次调用resize,此时数组肯定是空的,因此也就不需要重新分配元素。
if (oldTab != null) {
//遍历旧数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//取到当前下标的第一个元素,如果存在,则分三种情况重新分配位置
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//1.如果当前元素的下一个元素为空,则说明此处只有一个元素
//则直接用它的hash()值和新数组的容量取模就可以了,得到新的下标位置。
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//2.如果是红黑树结构,则拆分红黑树,必要时有可能退化为链表
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//3.到这里说明,这是一个长度大于 1 的普通链表,则需要计算并
//判断当前位置的链表是否需要移动到新的位置
else { // preserve order
// loHead 和 loTail 分别代表链表旧位置的头尾节点
Node<K,V> loHead = null, loTail = null;
// hiHead 和 hiTail 分别代表链表移动到新位置的头尾节点
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//如果当前元素的hash值和oldCap做与运算为0,则原位置不变
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;
}
上边还有一个非常重要的运算,我们没有讲解。就是下边这个判断,它用于把原来的普通链表拆分为两条链表,位置不变或者放在新的位置
if ((e.hash & oldCap) == 0) {} else {}
和新旧数组做取模运算,得到的结果,要么就是原来的位置不变,要么就是原来的位置加上旧数组的长度
get()方法
有了前面的基础,get方法就比较简单了
public V get(Object key) {
Node<K,V> e;
//如果节点为空,则返回null,否则返回节点的value。这也说明,hashMap是支持value为null的。
//因此,我们就明白了,为什么hashMap支持Key和value都为null
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值计算出来的下标位置的第一个元素
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//若hash值和key都相等,则说明我们要找的就是第一个元素,直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果不是的话,就遍历当前链表(或红黑树)
if ((e = first.next) != null) {
//如果是红黑树结构,则找到当前key所在的节点位置
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//如果是普通链表,则向后遍历查找,直到找到或者遍历到链表末尾为止。
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//否则,说明没有找到,返回null
return null;
}
为什么HashMap链表会形成死循环
准确的讲应该是 JDK1.7 的 HashMap 链表会有死循环的可能,因为JDK1.7是采用的头插法,在多线程环境下有可能会使链表形成环状,从而导致死循环
JDK1.8做了改进,用的是尾插法,不会产生死循环
