01、String、StringBuffer、StringBuilder的区别
三者共同之处:都是final类,不允许被继承,主要是从性能和安全性上考虑的,因为这几个类都是经常被使用着,且考虑到防止其中的参数被参数修改影响到其他的应用。
- StringBuffer是线程安全,可以不需要额外的同步用于多线程中;
- StringBuilder是非同步,运行于多线程中就需要使用着单独同步处理,但是速度就比StringBuffer快多了;
- StringBuffer与StringBuilder两者共同之处:可以通过append、indert进行字符串的操作。
- String实现了三个接口:Serializable、Comparable、CarSequence
- StringBuilder只实现了两个接口Serializable、CharSequence,相比之下String的实例可以通过compareTo方法进行比较,其他两个不可以。
这三个类之间的区别主要是在两个方面,即运行速度和线程安全这两方面。
1、首先说运行速度,或者说是执行速度,
在这方面运行速度快慢为:StringBuilder > StringBuffer > String
String最慢的原因:String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的。以下面一段代码为例:
String str="abc";
System.out.println(str);
str=str+"de";
System.out.println(str);
运行这段代码会发现先输出“abc”,然后又输出“abcde”,好像是str这个对象被更改了,其实,这只是一种假象罢了,JVM对于这几行代码是这样处理的,首先创建一个String对象str,并把“abc”赋值给str,然后在第三行中,其实JVM又创建了一个新的对象也名为str,然后再把原来的str的值和“de”加起来再赋值给新的str,而原来的str就会被JVM的垃圾回收机制(GC)给回收掉了,所以,str实际上并没有被更改,也就是前面说的String对象一旦创建之后就不可更改了。所以,Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。
而StringBuilder和StringBuffer的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行创建和回收的操作,所以速度要比String快很多。
另外,有时候我们会这样对字符串进行赋值
String str="abc"+"de";
StringBuilder stringBuilder=new StringBuilder().append("abc").append("de");
System.out.println(str);
System.out.println(stringBuilder.toString());
这样输出结果也是“abcde”和“abcde”,但是String的速度却比StringBuilder的反应速度要快很多,这是因为第1行中的操作和String str=“abcde”;是完全一样的,所以会很快,而如果写成下面这种形式
public static void main(String[] args) {
long a=new Date().getTime();
String cc="";
int n=10000;
for (int i = 0; i < n; i++) {
cc+="."+i;
}
System.out.println("String使用的时间"+(System.currentTimeMillis()-a)/1000.0+"s");
long s1=System.currentTimeMillis();
StringBuilder sb=new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append("."+i);
}
System.out.println("StringBuilder使用的时间"+(System.currentTimeMillis()-s1)/1000.0+"s");
long s2=System.currentTimeMillis();
StringBuffer sbf=new StringBuffer();
for (int i = 0; i < n; i++) {
sbf.append("."+i);
}
System.out.println("StringBuffer使用的时间"+(System.currentTimeMillis()-s2)/1000.0+"s");
}
2、再来说线程安全
在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的
如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。
(一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞)
3、总结一下
String:适用于少量的字符串操作的情况
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
02、Java异常-Error和Exception的区别
Error:
Error类对象由 Java虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。
例如,
Java虚拟机运行错误(VirtualMachineError),当JVM不再有继续执行操作所需的内存资源时,将出现OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止;还有发生在虚拟机试图执行应用时,如类定义错误(NoClassDefFoundError)、链接错误(LinkageError)。这些错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在Java中,错误通常是使用Error的子类描述。
Exception
在Exception分支中有一个重要的子类RuntimeException(运行时异常),该类型的异常自动为你所编写的程序定义ArrayIndexOutOfBoundsException(数组下标越界)、NullPointerException(空指针异常)、ArithmeticException(算术异常)、MissingResourceException(丢失资源)、ClassNotFoundException(找不到类)等异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生;而RuntimeException之外的异常我们统称为非运行时异常,类型上属于Exception类及其子类,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
- Error:程序无法处理系统异常,编译器不做检查。
- Exception:程序可以处理的异常,捕获后可能恢复。
- 总结:前者是程序无法处理的错误,后者是可以通过程序处理的异常。
package com.kuangstudy.kuangstudyjavajob;
import java.io.IOException;
/**
* @author 飞哥
* @Title: 学相伴出品
* @Description: 我们有一个学习网站:https://www.kuangstudy.com
* @date 2021/8/8 17:19
*/
public class ErrorAndException {
public void throwError(){
throw new StackOverflowError();
}
// 运行时异常
public void throwRumtimeException(){
throw new RuntimeException();
}
// 检查时异常
public void throwCheckException(){
try {
throw new IOException();
} catch (IOException e) {
e.printStackTrace();
}
}
}
03、常见Error以及Exception
RuntimeException
-
NullPointerException:空指针异常
-
ArithmeticException算术异常类
-
ArrayIndexOutOfBoundsException数组下标越界异常
-
NegativeArraySizeException数组负下标异常
-
ClassCastException类型强制转换异常
-
StringIndexOutOfBoundsException
# 指示索引或者为负,或者超出字符串的大小,抛出异常; "hello".indexOf(-1);
-
IllegalArgumentException参数异常
抛出的异常表明向方法传递了一个不合法或不正确的参数 -
NumberFormatException数字格式异常
-
ClassNotFoundException找不到类异常
-
NoSuchMethodException 方法未找到异常
-
FileNotFoundException 文件未找到异常
Error
- NoClassDefFoundError 找不到class定义异常类
- StackOverflowError 深度递归导致栈被耗尽而抛出的异常
- OutOfMemoryError 内存异常异常
04、Java异常处理的机制
1、抛出异常:创建异常对象,交由运行时系统处理
2、捕获异常:寻找合适的异常处理器处理异常,否则终止运行
05、Java异常处理的原则
1、具体明确:抛出的异常应能通过异常类名和Message准备说明异常的类型和产生异常的原因
2、提早抛出:应尽可能早的发现并抛出异常,便于精准定位问题
3、延迟捕获:异常的捕获和处理应尽可能延迟,让掌握更多信息的作用域来处理异常。
06、Java异常中return和finally的关系
01、Java中的Collection框架
在工作中我们每天在使用集合框架,但是面试中每次都不厌其烦的背诵集合框架的底层源码。真的是面试造火箭,开发拧螺丝。Java中的集合框架融合了很多算法和数据结构的思想,让我们的开发者不用去思考这些问题。因为优秀的算法和数据结构被封装到了Java的集合框架之中
。相比java语言其他的语言,比如C,可能需要程序员自己去实现和定义这些算法数据结构。
01、常见的数据结构
- 数组和链表的区别
- 链表的操作、如反转、链表环路检测、双向链表、循环链表相关操作
- 队列、栈的应用
- 二叉树的遍历方式及其递归和非递归的实现
- 红黑树的旋转
02、算法考点
- 内部排序;如递归排序、交换排序(冒泡,快速)、选择排序、插入排序。
- 外部排序;应该掌握如何利用有限的内存配合海量的外部存储来处理超大的数据集。
- 那些排序是不稳定的,稳定意味着什么?
- 不同数据集,各种排序的最好和最差的情况。
- 如何优化算法。
03、Java集合框架示意图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C1GXt1qt-1628781022809)(assets/2243690-9cd9c896e0d512ed.gif)]
04、集合框架体系如图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-URrDlHSv-1628781022811)(assets/java-coll-2020-11-16.png)]
05、Set和List的区别
- \1. Set 接口实例存储的是无序的,不重复的数据。List 接口实例存储的是有序的,可以重复的元素。
- \2. Set检索效率低下,删除和插入效率高,插入和删除不会引起元素位置改变 <实现类有HashSet,TreeSet>。
- \3. List和数组类似,可以动态增长,根据实际存储的数据的长度自动增长List的长度。查找元素效率高,插入删除效率低,因为会引起其他元素位置改变 <实现类有ArrayList,LinkedList,Vector> 。
06、Java集合List和Set分析
package com.example.kuangstudyjava;
import java.util.LinkedList;
import java.util.TreeSet;
public class ListDemo {
public static void main(String[] args) {
LinkedList linkedList = new LinkedList();
linkedList.add("aaaa");
linkedList.add("bbbb");
linkedList.add("aaaa");
linkedList.add("ccccc");
linkedList.add("dddd");
linkedList.add("eeee");
System.out.println(linkedList);
TreeSet treeSet = new TreeSet();
treeSet.add("aaaa");
treeSet.add("dddd");
treeSet.add("ccccc");
treeSet.add("bbbb");
treeSet.add("aaaa");
treeSet.add("eeee");
System.out.println(treeSet);
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I49lfLJx-1628781022812)(assets/image-20210808184545028.png)]
分析ArrayList:
1、底层是一个全局的数组结构进行元素的存储
2、在实现构造函数的,进行对象数组的初始化,初始化了一个并且指定定长的扩容。默认长度是:10
3、当调用add()方法添加元素的时候,如果超过固定长度10,就会调用grow方法进行扩容
ArrayList源码中的数组扩容的疑问
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
这里的int newCapacity = oldCapacity + (oldCapacity >> 1);这句怎么理解?
------解决方案--------------------
int newCapacity = oldCapacity + (oldCapacity >> 1)
相当于int newCapacity = oldCapacity + (oldCapacity / 2)
也就是说newCapacity = oldCapacity * (1.5)
扩容50%的意思
4、关于数组的最大容量问题
如果newCapacity - MAX_ARRAY_SIZE > 0
就会调用hugeCapacity
, 判断传入参数minCapacity
的大小,当 minCapacity > MAX_ARRAY_SIZE
时, ArrayList
的最大容量就会设置为Integer.MAX_VALUE
。
/**
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
5、关于删除元素
源代码实现
public E remove(int index) {
rangeCheck(index);//先判断数组是否越界
modCount++;
E oldValue = elementData(index);
//处理数据
int numMoved = size - index - 1;
//remove方法是将原数组的指定下标位置后面的值复制好然后再覆盖原有的指定下标位置,再将最后的一个置为空方便gc
调用的system.arraytcopy
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
代码解释:
Object src : 原数组
int srcPos : 从元数据的起始位置开始
Object dest : 目标数组
int destPos : 目标数组的开始起始位置
int length : 要copy的数组的长度
如果一个arraylist集合有0,1,2,3,4,5的数据
然后 remove(3)从原数组的下标为4就是4开始,复制两个长度也就是4和5复制这两个,
接着 从目标数组开始(这里也是当前数组)的下标为3这里将其覆盖也就是变成0,1,2,4,5,5,
最后将最后一位置为null就变成0,1,2,4,5,null
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
02、LinkedeList和ArrayList的区别
1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
3.对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
https://www.cnblogs.com/jiezzy/archive/2012/08/28/2660563.html
03、HashSet源码分析
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
Hashset的底层原理是:HashMap
03、HashMap
01、HashMap(jdk1.8前):数组 (查询快)+ 链表(添加删除快)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z8lS0CFI-1628781022813)(assets/image-20210808195450852.png)]
1、默认情况:hashmap的数组的长度是:16 。
2、长度为16的数组的头部,存储的是链表的头部元素节点。
3、通过类似于hash(key%hashCode()) % len的hash算法,来确定元素存储在数组的那个位置。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7WIzNCiO-1628781022815)(assets/image-20210808195924442.png)]
4:通过上面的hash可能会存在一种极端的情况,就是大量的数据进行hash取模的时候,换算出来的元素数组元素位置都是一样的,元素都分配到一个bucket中,这样会使得某个数组的列表会变得非常的长。因为链表查询需要从头部逐个去遍历,因为性能恶化的情况是由:O1变成On。
02、HashMap:JDK1.8以后:数组+链表+红黑树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d9P7rvho-1628781022816)(03%E3%80%81Java%E5%9F%BA%E7%A1%80.assets/image-20210811144858678.png)]
JDK1.8-HashMap源码分析
在jdk1.8中对HashMap进行了优化,在发生hash碰撞,不再采用头插法方式,而是直接插入链表尾部,因此不会出现环形链表的情况,但是在多线程的情况下仍然不安全。
JDK1.8-HashMap构造器
这里的this.threshold本应该指的是HashMap的下次扩容的阈值,仔细看你会发现这里并没有对组成HashMap的数组按你写的大小进行初始化,而是把你的参数赋值给下次的扩容的阈值。
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(int initialCapacity, float loadFactor) {
//如果初始容量小0 则报错
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果初始容量 大于冗余的最大容量 2的30次幂,
//则改变初始容量为允许的最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//如果传入的负载因子小于等于 0 或者 负载因子为空,则报错
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//赋值传入的负载因子
this.loadFactor = loadFactor;
//通过这个方法可以实现提前设置一个较为稳定的容量,从而避免频繁扩容导致的性能下降。
this.threshold = tableSizeFor(initialCapacity);
}
HashMap中有这样一段代码,当初始化HashMap时,如果指定了初始容量initialCapacity,由于哈希桶的数目必须是2的n次幂,因此要把initialCapacity转化为比它大的最小的2的n次幂数,例如initialCapacity = 10 ,那就返回16, initialCapacity = 17,那么就返回32。
/**
这个函数是用来对你申请的容量进行处理让他变成最接近你申请的容量的2次幂的大小,
这里注意:假如你申请的容量为0,最后处理的结果会变成1,代表着你最小的容量为1
**/
static final int tableSizeFor(int cap) {
int n = cap - 1;//n 为初始化容量 - 1
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
/**
如果(初始化容量-1)小于0,则初始化容量为1
如果 (初始化容量-1)的值大于 允许的最大容量,则把容量设置为允许的最大容量
否则 设置为 ((初始化容量-1) + 1)
**/
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
JDK1.8-HashMap的put操作
1、如果HashMap没有初始化过,则初始化。
2、对Key求Hash值,然后在计算下标
3、如果没有碰撞、直接放入桶中
4、如果碰撞了,以链表的方式链接到后面
5、如果链表的长度超过阈值、就把链表转成红黑树
6、如果链表的长度低于6,就把红黑树转回链表
7、如果节点已经存在就替换旧值
8、如果桶满了(容量16 * 加载因子0.75),就需要resize扩容2倍后重排。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
/**
* tab = table 值为当前哈希表的值
* n = tab.length 值为当前哈希表长度
* 如果当前哈希表为空 或者 当前哈希表长度为0
* 则tab = resize
* n = resize.length;
*/
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;<br> //没有hash碰撞时,后续值直接覆盖
/**
* i = (n - 1) & hash 得到的值为当前hash应该插入的数组位置
* p = tab[i]; 把p 指向哈希表下标为i的位置
* 如果该位置为空 ,代表该哈希位置还未插入过数据,
* 则把当前要插入的数据生成新Node直接插入
*/
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {//如果哈希表下标为i的的位置有数据则执行以下操作
Node<K,V> e; K k;
/**
* 判断一个两个node是否相同,有两个指标 1.两个node的hash值相同;2.两个node的key相同
* 注意:当前p指向哈希表中下标为i的位置的首位
* 如果首位的哈希值与要新插入的哈希值相同 并且
* k = p.key
* (k == key || (key != null && key.equeals(k);
* 其实意思就是如果要新插入的node的与当前p指向的位置为同一个元素
* 则 e = p;
*/
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
/**
* 注意:当前p指向哈希表中下标为i的位置的首位
* 如果当前p指向的位置的类型已经是红黑树
* 则把新node数据直接插入红黑树中
*/
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
/**
* 注意:当前p指向哈希表中下标为i的位置的首位
* 否则当前p指向的哈希表中下标为i的位置是一个线性链表
*/
else {
for (int binCount = 0; ; ++binCount) {
/**
* 注意:当前p指向哈希表中下标为i的位置的首位
* 循环执行 e = p.next ; 直到 e == null 其实就是循环访问线性链表直到线性链表结尾
* 把要插入的值生成新Node插入线性链表结尾
*/
if ((e = p.next) == null) {
//把要插入的值生成新Node插入线性链表结尾
p.next = newNode(hash, key, value, null);
//如果操作的长度大于等于(8 - 1) 则转红黑树 TREEIFY_THRESHOLD为转红黑树的门槛因子
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);//把当前线性链表转为红黑树
break;//插入新数据后跳出for循环
}
/**
* 循环访问线性链表的过程中对每一个node元素与要插入的元素进行判断
* 判断一个两个node是否相同,有两个指标 1.两个node的hash值相同;2.两个node的key相同
* 如果 为同一个元素则跳出for循环
*/
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//如果未到达线性链表末尾且当前线性链表中不存在于要插入的元素相同的node则继续for循环
p = e;
}
}
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;
}
每个put操作都有可能会触发哈希表扩容
/**
* JDK1.8---哈希表扩容
* @return
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
/**
* 获取原哈希表容量 如果哈希表为空则容量为0 ,否则为原哈希表长度
*/
int oldCap = (oldTab == null) ? 0 : oldTab.length;
/**
* 获取原哈希表扩容门槛
*/
int oldThr = threshold;
/**
* 初始化新容量和新扩容门槛为0
*/
int newCap, newThr = 0;
/**
//如果原容量大于 0
---这个if语句中计算进行扩容后的容量及新的负载门槛
*/
if (oldCap > 0) {
//判断原容量是否大于等于HashMap允许的容量最大值 2的30次幂
if (oldCap >= MAXIMUM_CAPACITY) {
//如果原容量已经大于等于了允许的最大容量,
// 则把当前HashMap的扩容门槛设置为Integer允许的最大值
threshold = Integer.MAX_VALUE;
return oldTab;//不再扩容直接返回
}
/**
* newCap = oldCap << 1 ; 类似 newCap = oldCap * 2 移位操作更加高效
* 表示把原容量的二进制位向左移动一位,
* 扩大为原来的2倍,同样还是2的n次幂
* 如果新的数组容量小于HashMap允许的容量最大值 2的30次幂
* 并且 原数组容量小于默认的初始化数组容量 2的4次幂 =16
*/
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
/**
* //新的扩容门槛为原来的扩容门槛的2倍,同样二进制左移操作
//类似 newThr = oldThr * 2 移位操作更加高效
*/
newThr = oldThr << 1; // double threshold
}
/**
* 如果 原数组容量小于等于零
* 并且 原负载门槛大于0 则
* 新数组容量为原负载门槛大小
*/
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
/**
* 这个elese语句 初始化默认容量和默认负载门槛
* 如果原数组容量小于等于0
* 并且原负载门槛也小于等于0
* 则
* 新 数组容量为 默认HashMap设置的默认初始化容量 1《4 = 2的4次幂 = 16
* 新 负载门槛为 默认负载因子(0.75f) * 16;
*/
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;
/**
* 如果新数组容量 小于 HashMap允许的最大容量(2的30次幂)
* 并且 新计算的负载门槛 小于 HashMap允许的最大容量(2的30次幂)
* 则新的 负载门槛为 计算后的值 的最大整型 -直接截取
* 否则 新的负载门槛为Integer.MAX_VALUE
*/
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//设置全局负载门槛为计算后的新的负载门槛
threshold = newThr;
/**
* 根据新的数组容量创建新的哈希桶 赋值给newTab
*/
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
/**
* 把新创建的哈希桶赋值给全局table;
*/
table = newTab;
/**
* 现在开始真正的扩容
*/
if (oldTab != null) {//如果老的哈希表不为空则执行以下语句
//for 循环,循环老的容量次
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
/**
* //从哈希数组的第一个下标(0)开始开始递增
* 注释:
* e = oldTab[0] ;
* e = oldTab[1] ; 循环访问每次哈希数组下标的内容
* e = oldTab[j];
* 如果 e != null 则开始访问数组中的内容
*/
if ((e = oldTab[j]) != null) {
把原数组中下标为j的位置置空
oldTab[j] = null;
//e.next == null 则代表线性链表只有一个元素e
if (e.next == null)
/**
* //根据e 的哈希值和 (新数组容量 -1)相与得到 e该存放到新数组中的下标
* 然后把e放入对应新数组的下标中。
*/
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
/**
* //如果当前e的类型已经改变为红黑树
* 则对红黑树进行分割 ?
*/
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
/**
* 进入这个else循环代表当前数组下标中存放的元素还是线性链表
*/
Node<K,V> loHead = null, loTail = null;//定义两个指针,分别指向低位头部和低位尾部
Node<K,V> hiHead = null, hiTail = null;//定义两个指针,分别指向高位头部和高位尾部
Node<K,V> next;
/**
* do-while循环中针对数组下标为j的 线性链表进行循环查询,直到线性链表结束
* 并根据每个Node的hash值与原数组容量相与得到新的值。
* 与原数组容量相与后的值只会为0 或 原数组容量。
* 根据得到的这两个值 进行判断
* 如果 值为 0 则把他们放到 loHead和loTail指向的新的线性链表当中--尾部插入
* 如果 值为 原数组容量 则把他们放到 hiHead和hiTail指向的新的线性链表当中--尾部插入
*/
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);
/**
* 原线性链表结束
* 如果新的loTail指向的线性链表不为空,则把它的最后结尾值的指针指向null值
* 并把loHeah与loTail指向的新的链表放到新数组下标为j的位置上。
* 如果新的hiTail指向的线性链表不为空,则把它的最后结尾值的指针指向null值
* 并把hiHeah与hiTail指向的新的链表放到新数组下标为 (j + oldCap) 的位置上。
*/
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
04、深入理解JDK1.8中HashMap哈希冲突解决方案
hash函数是先拿到通过key 的hashcode,是32位的int值,然后让hashcode的高16位和低16位进行异或操作。
/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
hash函数是先拿到通过key 的hashcode,是32位的int值,然后让hashcode的高16位和低16位进行异或操作。
- 一定要尽可能降低hash碰撞,越分散越好;
- 算法一定要尽可能高效,因为这是高频操作, 因此采用位运算;
为什么采用hashcode的高16位和低16位异或能降低hash碰撞?hash函数能不能直接用key的hashcode?
我们知道:==HashMap是采用数组+链表+红黑树。==进行hash的作用只不过是告诉每次put元素的时候,存放到那个数组中。因为key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。int值范围为**-2147483648~2147483647**,前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。==但问题是一个40亿长度的数组,内存是放不下的。==你想,如果HashMap数组的初始大小才16,用之前需要对数组的长度取模运算,得到的余数才能用来访问数组下标。
源码中模运算就是把散列值和数组长度-1做一个"与"操作,位运算比%运算要快。
最后我们来看一下Peter Lawley的一篇专栏文章《An introduction to optimising a hashing strategy》里的的一个实验:他随机选取了352个字符串,在他们散列值完全没有冲突的前提下,对它们做低位掩码,取数组下标。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yo17tTD3-1628781022817)(03%E3%80%81Java%E5%9F%BA%E7%A1%80.assets/image-20210811163908238.png)]
结果显示,**当HashMap数组长度为512的时候(2的9次方),也就是用掩码取低9位的时候,在没有扰动函数的情况下,发生了103次碰撞,接近30%。而在使用了扰动函数之后只有92次碰撞。碰撞减少了将近10%。**看来扰动函数确实还是有功效的。
另外Java1.8相比1.7做了调整,1.7做了四次移位和四次异或,但明显Java 8觉得扰动做一次就够了,做4次的话,多了可能边际效用也不大,所谓为了效率考虑就改成一次了。
1.7的hash代码:
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
1.8的hash代码:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
05、HashMap其他面试题
HashMap为什么使用数组?
数组在有下标的情况下时间复杂度是O(1)。
hashcode % array. length = Index。
通过对象的hashcode取余数组的长度得到对象在数组中下标的位置,使查找某个对象所在位置的时间复杂度降低为O(1)。
在HashMap中默认的数组长度为16,如果要指定,
那么传入的参数须为2的n次方的值,
如果传入的初始长度不为2的n次方的值,
也会通过位移改变为大于传入值且是最小的2的n次方的值。
/**
* The default initial capacity - MUST be a power of two.
默认的初始容量为16---为2的4次方
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
变更传入值的大小,如果不为2的n次幂,则变更为2的n次幂。
如果传入的容量不为2的n次幂,则变更为大于传入数值且为2的n次幂的最小值。
例如:传入11,则大于11且大于2的n次幂的最小值为16.则把初始容量变更为16.
2的3次幂为8,2的4次幂为16. 2的5次幂为32.
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
数组—》默认16长度的数组,最大长度2的30次幂,
数组——》当有下标读取时候,时间复杂度是O(1)。增删的时候由于要移动数组中数组的位置,因此时间复杂度为O(n)。
hash碰撞(冲突),如何解决?
Object1.hashcode % array.length = Object2.hashcode % array. length
当出现Hash冲突的时候,把数组中的位置变为线性链表。
线性链表的特点:
- 查找慢,需要一个节点一个节点的访问,时间复杂度是O(n)。
- 增删快,时间复杂度是O(1)。
在JDK1.7中的线性链表插入方式为头部插入法。
在JDK1.8中的线性链表插入方式为尾部插入法。
链表—》头部插入法(JDK1.7),尾部插入法(JDK1.8),equles
-
由于线性链表的查找速度为0(n),因此当HashMap中数据越来越多的时候,Hash冲突的概率也越来越大,因此线性链表的长度也越来越长,性能也越来越低。为了解决这个问题,在JDK1.8之后,当线性链表的长度超过8之后,把线性链表转为红黑树。
红黑树的特性: -
红黑树—》JDK1.8之后-链表长度超过8之后转为红黑树,左旋,右旋;
-
红黑树是接近于平衡的搜索二叉树。
-
红黑树确保最长长度不是最低长度的两倍。
-
性能均衡,查找,插入,删除等都是0(logn)的时间复杂度。
五个特性: 1.节点要么是红色、要么是黑色。 2.根节点必须是黑色。 3.每个叶子结点必须是黑色。 4.如果一个节点是红色,那么他的两个儿子必须是黑色。 5.对于任意节点而言,其到叶子节点树尾端指针的每条路径都包含相同数量的黑节点。
JDK1.8-之后长度超过8转变为红黑树,长度8的由来?
为什么这个长度是8.
因为统计学角度,一般很少hash的碰撞值会达到7.
泊松分布----> 根据泊松分布的概率统计学角度上。
在负载因子在0.75的时候,如果链表长度大于8之后的分布概率上来说,概率可以忽略不计了。
也就是说在大多数情况下是不会到达8转红黑树的。一亿分之六的概率。
/**
* The load factor used when none specified in constructor.
默认的负载因素为不太精确的 0.75f构造
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
链表转红黑树是链表长度达到阈值,这个阈值是多少?
阈值是8,红黑树转链表阈值为6
又为什么红黑树转链表的阈值是6,不是8了呢?
因为经过计算,在hash函数设计合理的情况下,发生hash碰撞8次的几率为百万分之6,概率说话。。因为8够用了,至于为什么转回来是6,因为如果hash碰撞次数在8附近徘徊,会一直发生链表和红黑树的转化,为了预防这种情况的发生。
为什么HashMap允许的最大容量为2的30次方?
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
HashMap允许的最大容量。---必须为2的幂且小于2的30次方,传入过大的值,将被替换
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
首先:JAVA中规定了该 static final 类型的静态变量为int 类型,
至于为什么不是byte、long等类型,
原因是由于考虑到HashMap的性能问题而做的这种处理。
由于int类型的长度为4字节,也就是32个二进制位。按理说可以向左移动31位,
即2的31次幂。但是由于二进制数字中最高的一位,也就是最左边的一位是符号位,
用来表示正负之分(0为正,1为负),所以只能向左移动30位,而不能移动到最高位。
HashMap扩容机制-为什么负载因子默认为0.75f?
负载因子0.75 如果容量大大0.75则扩容为原来的两倍。扩容因此 0.75
空间利用率和时间效率在0.75的时候达到了平衡。
在统计学上0.693是最佳的选择。然后可能更想着有空间利用率,而且在。Net语言中 hashmap的负载因子是0.7。
为什么Hashmap长度保证2的n次幂
Hashmap默认初始长度16,后续每次加入的值都是2的指数次幂的值。
如果传入的值不是2的指数次幂,则变成大于这个值的最接近的2的指数次幂的值。
变更传入值的大小,如果不为2的n次幂,则变更为2的n次幂。
如果传入的容量不为2的n次幂,则变更为大于传入数值且为2的n次幂的最小值。
例如:传入11,则大于11且大于2的n次幂的最小值为16.则把初始容量变更为16.
2的3次幂为8,2的4次幂为16. 2的5次幂为32.
06、Final
final关键字的作用
相信对于final的用法,大多数人都可以随口说出三句话:
1、被final修饰的类不可以被继承
2、被final修饰的方法不可以被重写
3、被final修饰的变量不可以被改变
重点就是第三句。被final修饰的变量不可以被改变,什么不可以被改变呢,是变量的引用?还是变量里面的内容?还是两者都不可以被改变?写个例子看一下就知道了:
如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。但是引用的值是可变 的。
如果被fianl修改的引用变量,这个这个引用是对象是可以去修改成员属性和调用成员方法,但是引用本身不运行被修改
4、被final修饰的局部变量
可以被final进行定义,并不赋值,但是一旦被赋值,后续就不可以被更改
public void test(){
final Integer localA; //局部变量只声明没有初始化,不会报错,与final无关。
localA = 0;
}
07、接口和抽象类的区别
1、抽象类、修饰符abstract修饰,集成关键字:extends ,而接口是:implments。
2、抽象类是有构造函数,有普通方法,有抽象方法、还成员变量。
接口:没有构造函数,在jdk1.7之前全部都是抽象方法,在jdk1.8之后 新增了静态方法和默认方法。
所有的属性都是静态常量
3、抽象类是单一集成,接口可以多实现 (java单一集成多实现)
学习目的是什么:学会看懂源代码 spring 抽象类,接口,实现类。
08、 抽象类为什么是有构造函数?但是又不能new对象?
接口和抽象类:可以约束子类的作用。
1、抽象类不能自己创建创建对象。但是有构造函数?
答案:就是给抽象类的中成员属性进行:赋值操作,让子类通过super()方法把子类的参数传递给父类成员变量。
2、接口中:所有的方法都是,公开的并且抽象的方法,所有属性都是公开的,并且是静态常量并且一定要赋值。