Java复习
第一次写文章。开始复习Java,同时也顺便写个文章,为了年后的面试做准备。本人是个菜鸡,有写错的地方请大佬们指出(不过应该没什么人看的)。这篇文章不会针对每个基础知识复习,只会记录某个知识点的相关面试题
以下图片部分来自网络,微信公众号三分恶
1. JDK各个版本间的特性
这里举例1.5 ,1.7,1.8 JDK 1.5新特性:
- 支持静态导入
- 可变参数
- 增强for循环
- 自动装拆箱
- 枚举
- 泛型
JDK 1.7新特性:
- switch支持String类型
- 自动关闭资源try-with-resources
- 菱形语法;使用泛型的时候,等号右边会进行类型推断
- 可以catch多个异常
JDK1.8新特性:
- 拉姆达表达式
- 接口可以有default默认实现
- 增加Stream类,用于操作集合类
- 新的日期类:LocalDate,LocalDateTime
- JVM使用元空间代替永久代
2. 短整型与整形的运算
short s1 = 1;
// 编译不通过;因为s1是short,1是int,运算结果是int,需要进行强转
s1 = s1 + 1;
// 可以运行;因为+=内部做了隐式的转换操作,即s1=(short(s1+1))
s1+=1;
3. 自增运算
大家可以先看看结果是多少
public static void main(String[] args) {
int i = 1;
i = i++;
int j = i++;
int k = i + ++i * i++;
System.out.println(i);
System.out.println(j);
System.out.println(k);
}
答案是
4
1
11
下面是我的分析 i=i++;内部的操作是
- 加载变量,将i压入操作数栈,此时操作数栈中的i为1
- 对局部变量表中的i进行加1操作,此时局部变量表的i为2
- 将操作数栈中的i赋值回局部变量表中,此时局部变量表的i又变回1
j = i++
- 加载i变量,将i压入操作数栈,此时操作数栈中的i为1
- 对局部变量表中的i进行加1操作,此时局部变量表的i为2
- 将操作数栈中的i赋值给局部变量表的j,所以,此时局部变量表的i=2,j=1
int k = i + ++i * i++
- 首先取i,压入操作数栈,此时操作数栈的i为2
- 局部变量表i加1,压入操作数栈中,此时第二个i为3
- 获取局部变量表中的i,压入操作数栈中,第三个i为3
- 局部变量表的i加1,此时局部变量表中的i为4
- 将栈顶两个i相乘,再压回操作数栈,即3*3 = 9,此时操作数栈只剩下两个数,分别是2和9
- 将两数相加得到11,赋值给变量k
4. Integer
public class Main {
public static void main(String[] args) {
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1==i2); // true
System.out.println(i3==i4); // false
}
}
出现这一现象的原因是Integer内部维护着IntegerCache 当值在-128~127之间的时候,会从缓存中获取;当超出这个范围则会通过new的方式得到Integer变量
public static Integer valueOf(int i) {
if (i >=IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
5. String
基本用法
String str = "菜只因复习Java";
String str1 = new String("菜只因复习Java");
两种写法的区别就在于是否会在堆中创建对象
创建字符串str时,会在字符串常量池查找是否已经存在该字符串;没有的话则会创建,有的话会直接指向这个引用
创建str1时,首先会在堆中创建该str1对象,然后再从字符串常量池查找是否有该字符串,有则会指向该引用,没有则会创建后再指向该引用
String.intern()
这个方法可以返回字符串对象在字符串常量池中的对象
public class Test {
public static void main(String[] args) {
String s = "abc";
String s1 = new String("abc");
System.out.println(s == s1); // false; 说明s和s1不是同一个对象
String sIntern = s.intern();
String s1Intern = s1.intern();
System.out.println(sIntern == s1Intern); // true; s,s1返回的都是字符串常量池中的对象
}
}
简单说一下String的源码
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** 保存字符串 */
private final char value[];
/** 保存字符串的哈希码 */
private int hash;
...
}
由于String被final修饰的,这也保证了String的不可变性 例如
String str = "菜只因复习Java";
str = str.replace("只因","鸡")
看起来修改了str,其实是产生了一个新的字符串对象,覆盖了str而已
String str = "复习Java";
str = "菜只因" + str;
'+' 进行字符串拼接也是产生一个新的字符串对象
当使用 '+' 进行拼接的时候,底层其实是创建了StringBuilder进行字符串拼接的 所以当我们拼接的字符串比较多的时候,应直接使用StringBuilder去进行字符串拼接,避免创建过多的StringBuilder对象
6. 抽象类和接口的区别
烂大街的问题了,不过以前面试遇到过,说的不是很好,所以这里记录一下 抽象类 可以使用各种访问修饰符(public,protected,private等)进行定义变量,方法 可以定义抽象方法,也可以定义普通方法 只支持单继承 侧重于对一种事物的抽象。当不同的类具有某些相同行为,且其中一部分行为的实现方式一致,可以让这些类派生于一个抽象类 如Bird类,可以有不同的实现类(鸟的品种)
接口 定义方法的默认修饰符是public abstract 定义变量的默认修饰符是public static final 支持多实现 对类进行约束,可以强制要求不同的类具有相同的行为 如鸟和飞机,他们没有共同的属性,但是却都会飞,此时就可以将飞行抽象为Fly接口,不管什么类,只要会飞行就可以实现Fly接口
7. 类的初始化和实例化
大家可以看看下面这段代码的打印顺序
public class Father {
private int i=test();
private static int j = method();
static {
System.out.print("(1)");
}
Father(){
System.out.print("(2)");
}
{
System.out.print("(3)");
}
public int test(){
System.out.print("(4)");
return 1;
}
public static int method(){
System.out.print("(5)");
return 1;
}
}
public class Son extends Father{
private int i = test();
private static int j = method();
static {
System.out.print("(6)");
}
Son() {
// super(); //写或不写都存在,在子类构造器中一定会调用父类构造器
System.out.print("(7)");
}
{
System.out.print("(8)");
}
public int test() {
System.out.print("(9)");
return 1;
}
public static int method() {
System.out.print("(10)");
return 1;
}
public static void main(String[] args) {
Son s1 = new Son();
System.out.println();
Son s2 = new Son();
}
}
类初始化
static是创建独立于对象的变量或者方法,即使没有创建对象也可以通过 '类名.xxx' 的形式直接调用。static成员是被类的所有实例所共享的。 static块(静态代码块):static块可以写在类中的任意位置,一个类可以有多个static块。
在字节码中执行 <clinit> 方法就表示执行类初始化
而类的初始化就是由静态变量显示赋值代码和静态代码块块组成,从上到下执行,只执行一次。
实例初始化
在字节码中执行 <init> 方法就表示执行实例初始化
实例初始化是由实例变量显示赋值代码和非静态代码块从上到下顺序执,对应的构造方法最后执行。
每次创建实例对象,调用对应构造器,执行的就是<init>方法
<init>方法的首行是super()或有参super方法,即对应父类的<init>方法
了解这些知识点后,答案就应该是 5 1 10 6 9 3 2 9 8 7 9 3 2 9 8 7
为什么Father中的4没有打印
因为创建的类型是Son类,所以当前this指向的是Son;而在执行Father中的test的时候,调用的是当前对象,即Son,的test方法
8. IO流
根据流向可以分为:输入流和输出流
根据处理数据单位可分为:字节流和字符流;字符流本质其实就是字节流
IO流最核心的4个顶层抽象类
- InputStream
- OutputStream
- Reader
- Writer 这里就只复习一些个人认为常用的流,因为大部分用的时候都是使用已经封装好的IO工具类
InputStream
主要的方法
public abstract int read() // 从输入流中读取下一个字节,读到尾部时返回 -1
public int read(byte b[]) // 从输入流中读取长度为 b.length 个字节放入字节数组 b 中
public int read(byte b[], int off, int len) // 从输入流中读取指定范围的字节数据放入字节数组 b 中
public void close() // 关闭此输入流并释放与该输入流相关的所有资源
常用的有实现类
- FileInputStream;文件输入流,可以读取文件,对文件进行拷贝,移动
- BufferedInputStream;缓冲流,它是一种处理流,对节点流进行封装并增强,其内部拥有一个 buffer 缓冲区,用于缓存所有读入的字节,当缓冲区满时,才会将所有字节发送给客户端读取,而不是每次都只发送一部分数据,提高了效率
- ObjectInputStream;对象输入流,用于对象的反序列化,将读入的字节数据反序列化为一个对象,实现对象的持久化存储
OutputStream
public abstract void write(int b) // 将指定的字节写出到输出流,写入的字节是参数 b 的低 8 位
public void write(byte b[]) // 将指定字节数组中的所有字节写入到输出流当中
public void write(byte b[], int off, int len) // 指定写入的起始位置 offer,字节数为 len 的字节数组写入到输出流当中
public void flush() // 刷新此输出流,并强制写出所有缓冲的输出字节到指定位置,每次写完都要调用
public void close() // 关闭此输出流并释放与此流关联的所有系统资源
- FileOutputStream;将数据转换成字节数据写出文件中
- BufferedOutputStream;缓冲输出流,只有在缓冲区满时,才会将所有的字节写出到目的地,减少了 IO 次数
- ObjectOutputStream;用于对象的序列化,将对象转换成字节数组后,将所有的字节都写入到指定文件
Reader
字符流用于操作字符,比起用字节流来操作保存字符的文件方便得多
public int read(java.nio.CharBuffer target) 将读入的字符存入指定的字符缓冲区中
public int read() 读取一个字符
public int read(char cbuf[]) 读入字符放入整个字符数组中
abstract public int read(char cbuf[], int off, int len) 将字符读入字符数组中的指定范围中
- BufferedReader 是字符输入缓冲流,将读入的数据放入字符缓冲区中,实现高效地读取字符
- InputStreamReader 是一种转换流,可以实现从字节流转换为字符流,将字节数据转换为字符
- CharArrayReader 和 StringReader 是两种基本的节点流,它们分别从读取 字符数组 和 字符串 数据,StringReader 内部是一个 String 变量值,通过遍历该变量的字符,实现读取字符串,本质上也是在读取字符数组
Writer
public void write(char cbuf[]) 将 cbuf 字符数组写出到输出流
abstract public void write(char cbuf[], int off, int len) 将指定范围的 cbuf 字符数组写出到输出流
public void write(String str) 将字符串 str 写出到输出流,str 内部也是字符数组
public void write(String str, int off, int len) 将字符串 str 的某一部分写出到输出流
abstract public void flush() 刷新,如果数据保存在缓冲区,调用该方法才会真正写出到指定位置
abstract public void close() 关闭流对象,每次 IO 执行完毕后都需要关闭流对象,释放系统资源
- CharArrayWriter、StringWriter 是两种基本的节点流,它们分别向Char 数组、字符串中写入数据。StringWriter 内部保存了 StringBuffer 对象,可以实现字符串的动态增长
- BufferedWriter 是缓冲输出流,可以将写出的数据缓存起来,缓冲区满时再调用 flush() 写出数据,减少 IO 次数。
- OutputStreamWriter 将字符流转换为字节流,将字符写出到指定位置
9. 集合
List
List保存的元素的特定是唯一,有序的 常用的就是ArrayList和LinkedList
ArrayList
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
/**
* 默认大小
*/
private static final int DEFAULT_CAPACITY = 10;
// 元素数组
transient Object[] elementData;
/**
* 先检查下标是否合法;超出规定长度则会扩容,扩容1.5倍
* 默认插入到数组的尾部
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public E remove(int index) {
// 检查索引
rangeCheck(index);
modCount++;
// 用于返回
E oldValue = elementData(index);
// 移动元素
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
/**
* 创建新的数组,容量是原来数组的1.5倍,然后将数据再赋值回elementData
*/
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);
}
}
ArrayList的特点: ArrayList通过数组保存元素,保存的元素地址是连续的,可以通过下标快速获得元素; 新增,删除操作都在数组尾部的话,时间复杂度都是O(1); 若操作元素在数组中间的话,就需要移动元素,时间复杂度都是O(n)。
LinkedList
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;
/**
* 保存链表第一个元素
*/
transient Node<E> first;
/**
* 保存链表最后一个元素
*/
transient Node<E> last;
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
/**
* 目标索引小于链表长度一半,则从头部开始遍历查找
* 否则从尾部开始遍历查找
* 查找下标越接近 链表长度/2,那么查找效率越慢
*/
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
/**
* 插入到succ元素之前
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
}
LinkedList相当于一个双端队列 可以在队列头部尾部增加元素,也可以插入到指定索引处 虽然插入删除的元素时间复杂度是O(1),但是需要先遍历查找到元素,所以整体还是O(n) LinkedList除了目标元素需要保存,还需要保存前一个节点和后一个节点,使用的内存比ArrayList要多;元素之间保存的地址是不连续的,所以查找效率没有ArrayList快
Map
Map是以key,value的形式保存元素 常见的实现类有:HashMap,LinkedHashMap;这里主要讲JDK1.8的HashMap,然后讲一下1.7和1.8之间的区别
HashMap
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认HashMap长度=16
/**
* Map最大容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 负载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* HashMap转换为的阈值红黑树条件一
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 红黑树转化为链表的阈值
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* HashMap转换为的阈值红黑树条件二
*/
static final int MIN_TREEIFY_CAPACITY = 64;
...
HashMap存储结构
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 记录hash值, 以便重hash时不需要再重新计算
final K key;
V value;
Node<K,V> next;
...// 其余的代码
}
构造方法 这里只看这个构造方法
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
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;
// 这个方法返回的是 大于initialCapacity的最小2的次幂
// 稍后会说一下为什么要这样做
this.threshold = tableSizeFor(initialCapacity);
}
至于HashMap为什么要将长度设置为2的次幂,稍后会说到
添加元素
/**
* 首先先调用一个hash()方法,得到当前key的一个hash值,
* 用于确定当前key应该存放在数组的哪个下标位置
*/
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,跳出else代码块,后续需要做值的覆盖处理
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 1st
treeifyBin(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 key
V 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;
}
删除元素
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
// 通过hash和长度-1取余得到指定位置元素
Node<K,V> node = null, e; K k; V v;
//1. 判断p的hash,key是否传入参数的hash,key相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
// 2. 遍历链表或者红黑树,寻找符合的元素
if (p instanceof TreeNode)
// 遍历红黑树
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 遍历链表
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 3. node不等于空说明找到目标元素
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 红黑树删除节点
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 刚好是链表的第一个元素
else if (node == p)
tab[index] = node.next;
// 在链表中间,将目标节点的上一个节点,指向目标节点的下一个节点
else
p.next = node.next;
++modCount;
--size;
// 空实现,主要是用于LinkedHashMap
afterNodeRemoval(node);
return node;
}
}
return null;
}
get
public V get(Object key) {
Node<K,V> e;
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;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 若比较成功则返回第一个元素
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 遍历红黑树
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);
}
}
return null;
}
hash HashMap中自定义了hash方法:将key 和 key无符号右移16位进行异或运算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这样的做法就是将高16位值和当前h的低16位进行了混合,这样可以尽量保留高16位的特征,从而降低哈希碰撞的概率。
那为什么采用异或运算呢?
可以从上图知道,异或运算得出0和1的结果比较均衡,可以让结果的随机性更大,而随机性大了之后,哈希碰撞的概率当然就更小了
扩容 在第一次初始化或者容量超出threshold的时候会进扩容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 1. 判断旧容量,数组已经有值的情况
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
}
// 2. 指定容量进行初始化的情况
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 3. 默认初始化的情况
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 2.1 指定容量进行初始化的情况
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 4. 下面的操作是将旧数组数据转移新数组中
@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;
// 1. 索引j下面只有一个元素
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. 链表
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;
}
为什么要数组长度要设置成2的次幂?
在初始化Map的时候会调用tableSizeFor,得到一个2的次幂数赋给threshold
public HashMap(int initialCapacity, float loadFactor) {
...
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
// 这个方法就将我们传入的长度转换成 大于cap的最小2的次幂
// 如传入20,得到的就是16因为2^4 < 20 且2^5 > 20,所以调用该方法得到的数就是32
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;
}
在第一次put的时候会调用扩容方法,会将threshold作为数组长度
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
...
}
}
我们可以看到在获取元素的时候,是将hash值(HashMap中的hash方法返回值)和数组长度-1进行与运算,从而得到元素。 与运算,相当于%取余运算,只不过效率比%更高 2^n^的二进制就是1,后面跟n个0;而2^n^-1 的二进制就是(n-1)个1 若hash值直接与n进行与运算,运算结果的后n-1位必定是0 而如果和n-1进行与运算,那么运算结果就取决于hash的后n-1位了 这也刚好解释了hash方法中的操作,目的就是为了让hash值更加具有随机性,减少哈希碰撞
为什么负载因子是0.75
Java开发团队选择0.75作为默认的加载因子,完全是时间和空间成本上寻求的一种折衷选择
- 加载因子过高,例如为1,意味着只有当hashMap装满之后才会进行扩容,虽然空间利用率有大的提升,但是这就会导致大量的hash冲突,使得查询效率变低。
- 加载因子过低,例如0.5,hash冲突降低,查询效率提高,但是由于负载因子太低,导致原来只需要1M的空间存储信息,现在用了2M的空间。最终结果就是空间利用率太低
链表什么时候会转成红黑树?
会在链表长度大于=8,数组长度>=64的时候转换成红黑树
static final int TREEIFY_THRESHOLD = 8;
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
...
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
}
static final int MIN_TREEIFY_CAPACITY = 64;
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 数组长度小于64时,就先进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 如果新增的node 要插入的数组位置已经有node存在了,取消插入操作
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 步骤一:遍历链表中每个节点,将Node转化为TreeNode
// hd指向头节点,tl指向尾节点
TreeNode<K,V> hd = null, tl = null;
do {
// 将链表Node转换为红黑树TreeNode结构
TreeNode<K,V> p = replacementTreeNode(e, null);
// 以hd为头结点,将每个TreeNode用prev和next连接成新的TreeNode链表
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
// 步骤二:如果头结点hd不为null,则对TreeNode双向链表进行树型化操作
if ((tab[index] = hd) != null)
// 执行链表转红黑树的操作
hd.treeify(tab);
}
}
这里就不详细说红黑树,自己也没掌握好,只是了解而已 这里推荐一篇红黑树的文章,写的通俗易懂
HashMap 1.7与1.8的区别
- 1.7中存储元素的结构是Entry,1.8是Node
- 1.7使用的数据结构:链表+数组 1.8使用的数据结构:链表+数组+红黑树
- 1.7链表使用头插法,1.8中使用尾插法 头插法在多线程中可能会发生死循环,当然它们都是线程不安全,不建议在多线程中使用
- 对于hash方法,1.7做了4次移位和4次异或,jdk1.8只做了一次
LinkedHashMap
继承自HashMap,大部分方法都是使用HashMap中的方法,然后根据LinkedHashMap的特性再做额外操作。 LinkedHashMap的Entry节点继承自HashMap.Node,还额外维护了before,after前后节点 LinkedHashMap重写了newNode方法,在调用put方法的时候会调用
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
// 保持插入节点的顺序
// 第一次会更新map的队头节点
// 之后的操作会将新的节点更新为队尾节点
linkNodeLast(p);
return p;
}
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
TreeMap
TreeMap底层使用了红黑树,可以保证遍历是有序的,也可根据我们传入的Comparator比较器进行排序;这个用的不多,也没被问过
Set
Set主要有TreeSet,HashSet
- HashSet:存储的元素唯一,无序的;底层使用的是HashMap,添加的值会作为HashMap的key,所有的value是Object对象。大部分的操作也是依靠HashMap
- TreeSet:存储的元素唯一,有序的;底层使用TreeMap
若文中有哪些错误,请各位大佬指出,谢谢 以上图片部分来自网络,还有部分来自公众号大佬:三分恶