JavaSE总结(个人总结、面试八股文)
```java
/**
*@author Roud
*@date 2021.10.16
*/
### 1. Java字节码
**一个class文件被加载到JVM内存之后,首先要经过字节码验证,主要包含:**
检查当前class文件的版本和JVM的版本是否兼容
检查当前代码是否会破坏系统的完整性
检查当前代码是否有栈溢出的情况
检查当前代码中的参数类型是否正确
检查当前代码中的类型转换操作是否正确
> 注意:
>
> 一个java文件中允许存在多个类,可以没有public修饰的类,如果有**只能有一个public修饰的类且该类的类名与java文件名一致**。
>
> 可以没有与类名与java文件名一致的类存在,但无法调用main方法。
>
> 一个java文件中如果存在多个类,该文件编译时会生成多个.class文件,除了**使用lambda表达式创建的匿名内部类不生成.class文**
>
> **件**,其他都生成
------
### 2. String、StringBuffer、StringBuilder
String是final修饰的,不可变,每次操作都会生成新的String对象
StringBuffer和StringBuilder都是在原对象上操作
StringBuffer由synchronized修饰,线程安全
StringBuilder线程不安全
性能:StringBuilder>StringBuffer>String
优先使用StringBuilder,多线程涉及共享变量使用StringBuffer
### 3. GC垃圾回收器
在食堂里吃饭,吃完把餐盘端走清理的,是 C++ 程序员,吃完直接就走的,是 Java 程序员。
**内存泄漏**(Memory Leak)使用完的对象不能及时回收
**内存溢出**(Out of Memory):程序申请内存时没有足够的内存供程序使用,内存泄漏的堆积会造成内存溢出
###### 1.如何判断对象可以被回收?
1 **引用计数器:**每一个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0的时候可以回收。
**弊端**:无法解决循环引用的问题,对象A和对象B相互引用对方,则这两个对象一直不会被回收,造成内存泄漏
Python垃圾回收使用的是引用计数器算法
2 **可达性分析**:从GcRoot开始向下搜索对象,搜索所走过的路径被称为引用链,当一个对象到GcRoot没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就可以判定回收。
什么是GcRoots?可以当做GcRoot s的对象有:
1 虚拟机栈中引用的对象
2 方法区中静态属性引用的对象。
3 方法区中常量引用的对象
4 本地方法栈中(即一般说的**native**方法)引用的对象
可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会,对象被系统宣告死亡至少要经历两次标记过程,第一次是经过可达性分析发现没有与GcRoots 相连接的引用链,第二次是对象执行finalize()方法后(收尾工作)即可被回收。
当对象变成GcRoot不可达时,Gc会判断该对象是否覆盖了finalize方法,若未覆盖,则直接回收,否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。
###### 2.常见的垃圾回收算法
1.**标记-清除算法**(Mark-Sweep)
分为两个阶段:标记阶段和清除阶段。标记阶段的任务是**通过可达性分析标记出所有需要被回收的对象**,清除阶段就是**回收被标记的对象所占用的空间**。
从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是**容易产生内存碎片**(就是导致可用内存不连续,零零散散的),碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
![img](https://img-blog.csdnimg.cn/img_convert/cd3883bb26fb787121dd2e00dfc73627.png)
2.**复制算法**
1969年Fenichel提出了一种称为“半区复制”(Semispace Copying)的垃圾收集算法,它将**可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉**,这样一来就不容易出现内存碎片的问题。具体过程如下图所示
**缺陷:空间浪费**
![img](https://img-blog.csdnimg.cn/img_convert/23b40f5798ae738bd7d0173eaee9cc26.png)
3.**标记-整理算法**(标记-整理-清除)
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,在**通过可达性分析标记出可以被回收的对象,然后将存活对象都向一端移动,然后清理掉端边界以外的内存**。(在标记清除的基础上对可利用内存进行整理)具体过程如下图所示:(**效率不高**)
![img](https://img-blog.csdnimg.cn/img_convert/831d995973d560f517e2ba7eb42f1d2e.png)
4.**分代收集算法**
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是**每次垃圾收集时只有少量对象需要被回收**,而新生代的特点是**每次垃圾回收时都有大量的对象需要被回收**,那么就可以根据不同代的特点采取最适合的收集算法。
目前大部分垃圾收集器对于**新生代**都采取**复制算法**,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
而由于老年代的特点是每次回收都只回收少量对象,一般使用的是**标记-整理算法**(压缩法)。
*首先,新创建的对象会存放在新生代的Eden区,当eden区被对象占满的时候,会触发Minor( [ˈmaɪnər] ) GC或Young GC,引发STW(停止事件,停止用户线程,专注垃圾回收),未被回收的对象会被移动到Survivor区(幸存区),此时年龄+1,经历一次垃圾回收年龄就+1,此处年经代中使用的是复制算法,当Survior区中的对象年龄到达15且没有被回收,会被移动到老年代,当老年代被对象占满,会触发Full GC 引发STW,且STW时间较长,老年代使用的垃圾回收算法一般是标记-整理算法*
根据对象的生命周期不同,放在不同代中,不同代使用不同的垃圾回收算法,能有效提高垃圾回收的效率
###### 3. 典型的垃圾收集器
![image-20211017211948380](C:\Users\25531\AppData\Roaming\Typora\typora-user-images\image-20211017211948380.png)
1.**Serial**/Serial Old收集器 是最基本最古老的收集器,它是一个**单线程串行收集器**,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。
2.**ParNew**收集器 是**Serial收集器的多线程**版本,使用多个线程进行垃圾收集。
3.**Parallel Scavenge**收集器 是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量,吞吐率(代码运行时间/代码运行时间+垃圾收集时间)优先,代码运行的时间更多,由于垃圾回收的时间。
4.Parallel Old收集器 是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。
5.CMS(Concurrent Mark Sweep)收集器 是一种以获取最短回收停顿时间为目标的收集器,它是一种**并发收集器**,采用的是Mark-Sweep算法。
1. 初始标记:可达性分析标记,会引发stw停止事件(停止用户线程)
2. 并发标记:可以和用户线程并发执行
3. 重新标记:重新标记,会引发stw停止事件
4. 并发清除:可以与用户线程并发执行,不会引发stw停止事件
6.G1收集器 是当今收集器技术发展最前沿的成果,它是一款**面向服务端应用的收集器**,它能充分利用多CPU、多核环境。因此它是一款**并行与并发收集器**,并且它能建立可预测的停顿时间模型。
###### 4.何为性能调优?
1. 根据项目需求对JVM规划和预调优
2. 优化运行程序中JVM导致的卡顿
3. 解决JVM运行中的各种问题
------
### 4. Java的特点
1. 面向对象
面向过程注重程序的实现步骤及顺序(C语言)
面向对象注重程序的实现有哪些参与者,各自实现了哪些方法(Java、python、c++)。优点有:
易维护、易拓展、开发效率更高
面向对象三大特性:
封装、继承、多态
继承:super关键字用于访问父类成员(包含成员变量、成员方法、构造方法)
2. 简单性
Java 与 C++ 很相近,但Java舍弃了很多 C++ 中难以理解的特性,比如多继承、指针等,此外java还加入了垃圾回收机制,解决了程序员需要管理内存的问题。
3. 平台无关性
java通过JVM实现跨平台运行,可以一次编译,四处运行
4. 解释执行
java文件经过编译生成.class文件,.class文件被类加载器加载进JVM内存后经过解释器解释成机器语言供计算机识别
5. 多线程
java语言支持多线程并发执行
6. 健壮性
java语言有很多类库
...
------
### 5. String常量池
String str = "hello";直接赋值方式。在jdk1.8中,变量str会先去字符串常量池中寻找字符串hello,如果**有相同的字符串则返回常量句柄**;如果**没有此字符串,则会在字符串常量池中创建此字符串,然后再返回常量句柄**
------
### 6. Integer进行“==”操作
**==与equals**
在进行==操作时,如果**两边是基本数据类型,比较的是值;如果是引用数据类型,则比较的是在内存中的地址**
equals方法在Object类中没有重写的情况下,默认使用的是==进行比较
1. **数组对象使用equals()方法使用的是Object中没有重写的equals()方法**(重要)
```java
public boolean equals(Object obj) {
return (this == obj);
}
-
String类重写了equals()方法,会逐个遍历其中的字符,对每个字符进行比较
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
-
当Integer与int比较,Integer会自动拆箱后进行比较,所以返回为true。
Integer a = 128; Integer b = new Integer(128); int c = 128; //进行比较 a == c;//true b == c;//true
-
当两个new Integer()生成的变量比较时,结果必为false。new生成的变量会再堆区中开辟一个新的内存空间,地址指向不同。
Integer a = new Integer(128); Integer b = new Integer(128); //进行比较 a == b;//false
-
非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。
Integer a = 128; Integer b = new Integer(128); //进行比较 a == b;//false
-
当两个非new Integer()生成的变量比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false。
Integer缓冲区:Integer是对小数据(**-128127**)是有缓存的,JVM初始化的时候,数据-128127之间的数字便被缓存到了本地内存中,这样,如果初始化-128~127之间的数字,便会直接从内存中取出,而不需要再新建一个对象。
Integer a = 127; Integer b = 127; //进行比较 System.out.println(a==b);//true
Integer a = 128; Integer b = 128; //进行比较 System.out.println(a==b);//false
7. 常用命令
-
javac 编译命令
-
java 运行命令
-
javap 反编译命令
-
javadoc 生成api文档命令
-
jar 打包命令
8. 常用的包
-
java.lang 最常用的包,包中的类我们可以直接调用,无需import导入
-
java.io 用于输入/输出、读取/写入操作的类包
-
java.net 这个包下的类主要用于网络编程
-
java.util 这个包下的类都是一些常用工具类
9. Java环境变量配置
-
JAVA_HOME:指明jdk的安装路径
-
PATH:指明java命令(如java、javac、jar、javadoc、javap等等)的路径
-
CLASSPATH:为类加载器指明.class文件的路径
10. Java访问权限修饰符
同一个类中 | 同一个包下 | 不同包下的子孙类 | 不同包下的其他类 | |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | x |
default | √ | √ | x | x |
private | √ | x | x | x |
11. Java代码执行顺序
class Son extends Father{
static {
System.out.println("子类静态代码块");
}
public Son() {
super();
System.out.println("子类无参构造器");
}
{
System.out.println("子类匿名代码块");
}
public static void main(String[] args) {
System.out.println("main方法");
Father s=new Son();
}
}
class Father {
static int a;
int b;
static {
System.out.println("父类静态代码块");
}
public Father() {
super();
System.out.println("父类无参构造器");
}
{
System.out.println("父类匿名代码块");
}
}
输出结果如下:
父类静态代码块
子类静态代码块
main方法
父类匿名代码块
父类无参构造器
子类匿名代码块
子类无参构造器
执行顺序:
注意:java静态代码块在类初始化的时候执行,先执行静态代码块再执行静态的main方法
12. 类加载器
在java中,负责把class文件加载到JVM的是类加载器(ClassLoader)在 java.lang.ClassLoader下。
JVM的作用:将.class字节码文件解释成机器语言,让计算机识别
作用:加载指定路径中jar里面的class文件。JVM启动后,默认会有几种类加载器:
1. bootstrapClassLoader
启动类加载器,非java语言实现
路径:C:\Program Files\Java\jdk1.8.0_74\jre\lib,例如:rt.jar
2. ExtClassLoader
拓展类加载器,java语言实现,是ClassLoader类型的对象
路径:C:\Program Files\Java\jdk1.8.0_74\jre\lib\ext,例如:ext中默认存在的jar,或者用户放到ext目录下的jar包
3. AppClassLoader
应用类加载器,java语言实现,是ClassLoader类型的对象
路径:CLASSPATH中配置路径,这个是用户自己配置的,例如:.:bin:hello.jar
13. 双亲委托机制
双亲委派机制,当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,
如果有那就无需再加载了。如果没有,那么向上委托到父类加载器ExtClassLoader,ExtClassLoader同理也会先检查自己是否已经加载
过,如果有那就无需再加载了,如果没有再往上委托。类似递归的过程,最后到达BootstrapclassLoader之前,都是在检查是否加载过,
并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候如果还没被加载过就开始考虑自己是否能加载了,
如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException异常。
双亲委托机制的优点:
1.避免了类的重复加载
2.保证了java核心API不被篡改
14. 基本数据类型
1. 八种基本数据类型
整数形:byte、short、int、long
浮点型:float、double
字符型:char
布尔型:boolean(false、true)
注意:基本数据类型直接存储在栈区中,引用数据类型的地址存储在栈区中
注意:基本数据类型都是小写字母开头,包装类是大写字母开头
注意:如果不明确指定,整数型的默认类型是int,浮点型的默认类型是double
2. 基本数据类型占位大小
(单位:byte,即1字节,1字节占8位)
byte | short | int | long |
---|---|---|---|
1 | 2 | 4 | 8 |
boolean | char | float | double |
1 | 2 | 4 | 8 |
注意:byte类型的表示的数据范围是-pow(2,8-1)pow(2,8-1)-1,即-128127;依次类推,short类型表示的数据范围是-pow(2,16-1)pow(2,16-1)-1===>-pow(2,15)pow(2,15)-1……
15. 数组
1. 概念
一组连续的内存空间,顺序存储结构,支持随机下标访问
2. 数组的创建
- new int[10];
- new int[]{1,2,3};
- {1,2,3};
3. length属性
数组对象只有一个属性length,没有多余的方法和属性
4. 二维数组
int[] [] arr = new int[4] [];
以上声明创建表示这个二维数组中有4个一维数组
注意:java.system中提供了一个名为arrarycopy的方法用于复制数组元素
public static void arrarycopy(Object src, int srcPos, Object dest, int destPos, int length);
参数依次为源数组、开始位置、目的数组、目的数组开始位置、复制长度
17. 可变参
声明可变参后,方法的参数可以为1个或多个,还可以为数组
注意:可变参的底层是一个数组
注意:当可变参与普通参数在同一参数列表下,可变参必须放到最后一个参数的位置
18. volatile关键字
- volatile 修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值;或者作为状态变量,如 flag = ture,实现轻量级同步。
- volatile 属性的读写操作都是无锁的,它不能替代 synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
- volatile 只能作用于属性,我们用 volatile 修饰属性,这样编译器就不会对这个属性做指令重排序。
- volatile 提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile 属性不会被线程缓存,始终从主存中读取。
- volatile 提供了 happens-before 保证,对 volatile 变量 V 的写入 happens-before 所有其他线程后续对 V 的读操作。
- volatile 可以使纯赋值操作是原子的,如
boolean flag = true; falg = false
。 - volatile 可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性
19. 抽象类和接口
抽象类:被abstract修饰的类
-
抽象类可以没有抽象方法
-
有抽象方法的类一定是抽象类
-
抽象类中可以有构造器方法,供子类调用
-
抽象方法(只有方法声明没有方法体)使用abstract修饰
-
不能实例化为对象,但可以在抽象类中实例非抽象类
-
抽象类可以继承抽象类和实现接口
接口:
- 接口中的抽象方法不用abstract修饰
- 接口中的属性都是public static final修饰的
- 接口不能继承类,但可以继承接口(可以多继承)
- jdk1.8之前接口中的方法都是抽象方法,jdk1.8之后接口中除了抽象方法还允许存在静态方法和默认方法
- 接口中不存在构造方法
- 接口方法的默认访问修饰符是public,不可以使用其他访问修饰符
20. 回车与换行
\r:回车符,使光标到行首(一个字符,两个字节)
\n:换行符,使光标下移一格(一个字符,两个字节)
不同操作系统下Enter的含义:
MAC:\r
UNIX:\n
window:\r\n
位:1个二进制位,1bit(计算机最小存储单位)
字节:byte,1byte=8bit
字符:char,1个字符占两个字节
1Kb = 1024byte
...
1个英文字母、一个数字占1个字节,utf-8编码中一个中文字符占3的字节,字符流足够聪明,如果遇到英文一次读取1个字节,如果是中文一次读取2或3个字节
21. 外连接
//例如,查询所有员工 以及对应的部门的名字,没有任何员工的部门也要显示出来
select last_name,dept_id,name
from s_emp left outer join s_dept
on s_emp.dept_id=s_dept.id;
//例如,查询所有员工 以及对应的部门的名字,没有部门的员工也要显示出来
select last_name,dept_id,name
from s_emp right outer join s_dept
on s_emp.dept_id=s_dept.id;
//例如,查询所有员工 以及对应的部门的名字,没有任何员工的部门也要显示出来,没有部门的员工也要
显示出来
select last_name,dept_id,name
from s_emp full outer join s_dept
on s_emp.dept_id=s_dept.id;
22. 位运算
转成二进制进行运算
& 与运算(同1则1,否则为0)
- | 或运算(有1为1,否则为0)
- 取反(补码取反,不是取反码)
^ 异或(相同为0,相异为1)
原码、反码、补码、移码
原码:数字的二进制表示,最高位为符号位,0为正,1为负
正数:原码、反码、补码相同
负数:
反码:原码除符号位取反
补码:反码+1
无论正负,移码都为其补码符号位取反
计算机中以补码存储数据,以原码显示数据(方便将减法运算化为加法运算)
所以运算以补码运算,运算完以原码显示
注意:补码运算完,如果是正数,原码就是补码。如果是负数,则是减1除符号位取反
23.异常
异常的根类是java.lang.Throwable (意为可抛出的),该类下面有两个子类型, java.lang.Error 和java.lang.Exception
Error:表示错误情况,一般是程序出现了比较严重的问题,并且程序本身无法处理
Exception:表示异常情况,经过特定处理后,程序仍可以继续正常执行
常见的运行时异常:
- ArithmeticException,算数异常
- NullPointerException,空指针异常
- IndexOutOfBoundsException,下标越界异常
- NoSuchElementException,元素不存在异常
- NumberFormatException,数字格式异常
- ClassCastException,类型转换异常
异常传播:如果一个方法抛出了异常,并一直没有处理,那么这个异常将抛给当前方法的调用者,并一直向上抛出,直到抛给了JVM,最后JVM将这个异常信息打印出,同时程序停止运行。
24.死锁
进程:是执行着的应用程序,一个进程包含多个线程
线程:是进程内部的一个执行序列,又称轻量级进程
死锁:两个线程A、B,线程A拿着线程B等待的锁对象不释放,线程B拿着线程A等待的锁对象不释放,就这样僵持下去。
产生死锁的四个条件:
- 互斥条件:一个资源在同一时间只能被一个进程占有
- 占有并等待:一个进程至少占有一个资源并申请新的资源
- 不可抢夺:一个进程占有资源并且没有使用完之前另一个进程不能强行抢夺
- 循环等待:若干个资源头尾相连形成一个循环等待资源的关系
/*
* 两个线程A、B,线程A拿着B等待的锁不释放,线程B拿着A等待的锁不释放,就这样僵持下去
*/
public class ThreadCase {
public static void main(String[] args) {
final Object lockA = new Object();
final Object lockB = new Object();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
synchronized(lockA) {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"获取了lockA");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(lockB) {
System.out.println(Thread.currentThread().getName()+"获取了lockB");
}
}
}
},"线程A").start();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
synchronized(lockB) {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"获取了lockB");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(lockA) {
System.out.println(Thread.currentThread().getName()+"获取了lockA");
}
}
}
},"线程B").start();
}
}
如何避免死锁:
-
线程按照一定的顺序加锁
-
线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁(加锁实现)
-
死锁检测
破坏死锁条件即可(互斥条件不可破坏)
✭25.HashTable与HashMap对比
✭✮✭HashMap(JDK1.8)
HashMap主要的成员变量
//数组,每个下标都对应一条链表;换言之,所有哈希冲突的数据,都会存放到同一条链表中
transient Entry<K,V>[] table;
//HashMap中键值对数量
transient int size;
//负载因子
final float loadFactor;
Entry<K,v>包含如下成员变量
final K key;
V value;
//指向下一个结点的引用
Entry<K,V> next;
//该结点的码
int hash;
HashMap put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
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 {
Node<K,V> e; K k;
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);
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;
}
}
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;
}
-
首先调用hash()方法,判断key是否为空,元素为空,返回0
-
拿到 key 的 hashCode 值
-
将 hashCode 的高位参与运算,重新计算 hash 值 (h=key.hashCode())^(h>>>16)
-
将计算出来的 hash 值与 (table.length - 1) 进行 & 运算,得到元素在数组中下标的位置
-
如果数组中该下标的位置为空,则直接插入。如果不为空,则遍历链表调用equals方法,判断有没有对象与其相同,相同则替换。不同则尾插
HashMap 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 & (table.length - 1)获取该key对应的数据节点的hash槽;
-
判断该数组位置节点是否为空, 为空则直接返回空;
首节点放在数组中
-
再判断位置处的entry 的key 是否和目标值相同, 相同则直接返回(首节点不用区分链表还是红黑树);
-
首节点.next为空, 则直接返回空;
-
首节点是树形节点, 则进入红黑树数的取值流程, 并返回结果;
-
进入链表的取值流程, 并返回结果
问:HashMap线程不安全的表现
- 在两个线程同时尝试扩容HashMap时,可能将链表形成环状链表,所有的next都不为空,进入死循环
- 在两个线程同时进行put操作时可能造成一个线程数据的丢失
问:HashMap如何保证线程安全?
-
HashTable
HashTable中几乎所有方法都是sychronized修饰的,由于加锁导致效率比较低
-
ConcurrentHashMap
在jdk1.8中ConcurrentHashMap使用CAS算法
jdk1.8之前HashMap的底层实现都是数组+链表(数组存放键的哈希值不同的entry对象,链表存放的是键的哈希值相同的entry对象)
1.8之前HashMap的实现中存在一个很大的问题:当 Hash 冲突严重时,在数组上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。
jdk1.8的时候当链表高度大于等于8的时候,链表转变成红黑树(一种自平衡查找二叉树),当链表高度小于等于6时,红黑树回退回链表。究其原因是因为节点过多的时候,使用红黑树查找节点的效率更高,毕竟红黑树是一种查找二叉树
Node 的核心组成其实和 1.7 中的 HashEntry 一样,存放的都是 key value hashcode next 等数据。
扩容机制:默认初始大小是16,负载因子(加载因子)为0.75,即当存储容量大于16*0.75的时候,扩容2倍,并将原数据复制到新的数组上
hashCode(key)=key mod 16;(resize(int i)用于给HashMap扩容)
初始大小为16与扩容2倍的目的是保证容量是2的幂次方,这样确认位置碰撞概率会比较低
当为容量为初始值,最大哈希索引值是15
0 --- 15
0000 1111
当确认元素的位置是,只需将元素的哈希码与15进行&运算(h&(length))
//jdk1.8之前
static int indexFor(int h,int lenght){
return h & (lenght-1);
}
如97 为0110 0001
97&15 为 0000 0001,即97在索引值为1的位置
resize()步骤:
- 扩容:创建一个新的Entry空数组,长度是原数组的2倍。
- ReHash:遍历原数组,把所有的Entry重新Hash到新数组
为什么重新Hash?
因为长度扩大以后,Hash规则也随之改变
hash :该方法主要是将Object转换成一个整型。
indexFor :该方法主要是将hash生成的整型转换成链表数组中的下标。
static int indexFor(int h, int length) {
return h & (length-1);
}
indexFor方法其实主要是将hashcode换成链表数组中的下标。h表示元素的hashcode值,length就是HashMap的容量
HashMap 遍历使用的是一种快速失败(fail-fast)机制,它是 Java 非安全集合中的一种普遍机制,这种机制可以让集合在遍历时,如果有线程对集合进行了修改、删除、增加操作,会触发并发修改异常。
它的实现机制是在遍历前保存一份 modCount ,在每次获取下一个要遍历的元素时会对比当前的 modCount 和保存的 modCount 是否相等。
快速失败也可以看作是一种安全机制,这样在多线程操作不安全的集合时,由于快速失败的机制,会抛出异常。
HashMap与HashTable比较:
(1)线程安全:HashMap是线程不安全的类,多线程下会造成并发冲突,但单线程下运行效率较高;HashTable是线程安全的类,很多方法都是用synchronized修饰,但同时因为加锁导致并发效率低下,单线程环境效率也十分低;
(2)插入null:HashMap允许有一个键为null,允许多个值为null;HashMap插入的key为null时,hash算法返回值为0,不会调用key的hashcode方法。
但HashTable不允许键或值为null;HashTable插入的key为null,会抛出NullPointerException。
(3)容量:HashMap底层数组初始默认长度为16,加载因子为0.75,一次扩容两倍,扩容后长度必须为2的幂次方,这样做是为了hash准备;而HashTable底层数组长度默认为11,加载因子为0.75,一次扩容2n+1;
(4)扩容机制:HashMap创建一个为原先2倍的数组,然后对原数组进行遍历以及rehash;HashTable扩容将创建一个原长度2倍的数组,再使用头插法将链表进行反序;
(5)结构区别:HashMap是由数组+链表形成,在JDK1.8之后链表长度大于8时转化为红黑树;而HashTable一直都是数组+链表;
(6)继承关系:HashMap继承自AbstractMap类;HashTable继承自Dictionary类;
26.线程安全的集合
- Vector
- HashTable
- ConcurrentHashMap
- Stack
27.hashCode()和equals()
哈希函数 hashCode(key)=key mod p;
如:key mod 13;
哈希值就像人的姓氏
- 两个对象相同,哈希值一定相等
- 哈希值相等,两个对象不一定相同
比如HashSet要重写hashcode()和equals()方法来确定对象是否重复。
hashCode()用于返回对象的哈希值,即散列码。哈希码的作用是确定对象在哈希表中的索引位置。
首先会计算对象的hashcode来确定插入对象的位置,如果这个位置没有值,则认为该对象没有重复出现。如果这个位置有值,这个会调用equals()方法判断两个对象是否相同;相同的话不会HashSet不会让其加入成功,不同的话就会重新散列到其他位置。这样会减少调用equals()方法的调用次数,大大提高执行速度。
28.Thread和Runnable的区别
没有太大区别,Thread是类,Runnable是接口,使用Runnable会更灵活
29.守护线程
为用户线程提供服务,地位比较低,JVM是否停止运行不会考虑守护线程的运行情况,比如GC垃圾回收线程
30.ThreadLocal
多线程环境下对非线程安全变量进行并发访问,且这个对象不需要在线程中共享,可以使用ThreadLocal让每个线程持有一个变量的副本,达到线程隔离的作用
原理:每个线程内部拥有一个ThreadLocalMap,由Thread维护,ThreadLocalMap由一个个entry对象构成,entry中的键存储一个ThradLocal对象,值为变量的副本
31.ThreadLocal内存泄漏
内存泄漏(Memory Leak)是指程序在申请内存后,无法释放已申请的内存空间。一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。
内存溢出(out of Memory):指程序申请内存时,没有足够的内存供申请者使用。给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。
内存泄漏的堆积最终会导致内存溢出
强引用: 当内存空间不足时,java虚拟机宁愿抛出OutOfMemoryError错误,也不会靠随意回收具有强引用的对象来解决内存不足问题。一般采用 new 方法创建强引用。
软引用:一般垃圾回收时不会被回收,当内存不足时才会被回收
弱引用:垃圾回收器碰到即回收,也就是说它只能存活到下一次垃圾回收发生之前。一般采用WeakReference 类来创建弱引用。
原因:
- key 使用强引用:引用的
ThreadLocal
的对象被回收了,但是ThreadLocalMap
还持有ThreadLocal
的强引用,如果没有手动删除,ThreadLocal
不会被回收,导致Entry
内存泄漏。 - key 使用弱引用:引用的
ThreadLocal
的对象被回收了,由于ThreadLocalMap
持有ThreadLocal
的弱引用,即使没有手动删除,ThreadLocal
也会被回收。value
在下一次ThreadLocalMap
调用set
,get
,remove
的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap
的生命周期跟Thread
一样长,如果都没有手动删除对应key
,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal
不会内存泄漏,对应的value
在下一次ThreadLocalMap
调用set
,get
,remove
的时候会被清除。
因此,ThreadLocal
内存泄漏的根源是:由于ThreadLocalMap
的生命周期跟Thread
一样长,如果没有手动删除对应key
就会导致内存泄漏,而不是因为弱引用。
防止内存泄露:
- 每次使用完ThreadLocal都调用它的remove()方法清除数据
- 把ThreadLocal对象定义为private static ,这样就能存在ThreadLocal的强引用,能保证每次通过ThradLocal的强引用访问到entry的value,进而清除掉
32.二义性
在C++中,派生类继承基类,对基类成员的访问应该是确定的、唯一的,但是常常会有以下情况导致访问不一致,产生二义性。
1.在继承时,基类之间、或基类与派生类之间发生成员同名时,将出现对成员访问的不确定性——同名二义性。
2.当派生类从多个基类派生,而这些基类又从同一个基类派生,则在访问此共同基类中的成员时,将产生另一种不确定性——路径二义性。
33.synchronized关键字
synchronized修饰成员方法,默认使用this作为锁对象,并不需要自己指定
synchronized修饰静态方法,默认使用当前类的Class对象作为锁对象,并不需要自己指定
34.线程安全的理解
多线程并发访问共享变量,并且能获得正确结果
35.并发、并行、串行
- 并发:同一时间段内,多个线程抢夺同一个CPU,交替运行
- 并行:同一时间段内,多个线程各自使用一个CPU,同时任务
- 串行:时间上不发生重叠,前一个任务没有执行完,后面的任务只能等着
36.并发三大特性
-
原子性
要么执行,要么都不执行
关键字:synchronized
-
可见性
一个线程修改了共享变量,其他线程能够立即得知这个修改
关键字:volatile、synchronized、final
-
有序性
定义:虚拟机在编辑代码时,对于那些改变顺序之后不会对最终结果造成影响的代码,有可能将他们重排序。但是,对于有些代码进行重排序之后,虽然对变量的值没有造成影响,但有可能会出现线程安全问题。
关键字:volatile、synchronized
37.线程池以及参数
线程池
优点:
- 提高线程利用率
- 提高程序响应速度
- 便于统一管理线程对象
- 可以控制最大并发数
一个线程池提交到线程池后的过程:
- 线程池核心线程有空闲,则核心线程执行该任务
- 线程池中核心线程无空闲线程,但等待队列未满,任务进入等待队列等候
- 线程池线程池中核心线程无空闲线程,且等待队列已满,此时线程工厂会创建新的线程执行任务
- 线程池线程池中线程数达到最大线程数,且等待队列已满,触发拒绝策略
Executor,顶层接口,只定义了一个execute()方法
接口ExectorService实现了Executor接口
抽象类AbstractExecutorService实现了ExecutorService()接口
TheadPoolExecutor类继承了AbstractExecutorService
TheadPoolExecutor参数:
一、corePoolSize 核心线程数
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会 被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。
二、maximumPoolSize 最大线程数
三、keepAliveTime 线程最大空闲时间
四、unit 时间单位
五、workQueue 等待队列
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
①ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
②LinkedBlockingQuene
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
③SynchronousQuene
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
④PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
六、threadFactory 线程工厂
七、handler 拒绝策略
policy[ˈpɑːləsi]
①CallerRunsPolicy
重新尝试提交该任务
②AbortPolicy
丢弃任务并抛出异常
③DiscardPolicy
直接抛弃当前任务但不抛出异常
④DiscardOldestPolicy
抛弃队列里等待最久的任务并把当前任务加入队列
java.lang.Executors工厂类,可以便捷的帮我们生产出线程池对象,同时该类中也提供了一些实用的工厂方法:
-
工厂方法1,创建一个定长的线程池
-
工厂方法2,创建一个可缓存的线程池
-
工厂方法3,创建一个单线程化的线程池
-
工厂方法4,创建一个定长线程池,支持定时及周期性任务执行
38. JDK1.8中新特性
1.注解(Annotation)
在java.lang下,java语言内置了三种注解方式:
- @Override(表示子类覆盖父类方法)
- @Deprecated(表示当前元素是不赞成使用的)
- @SuppressWarmings(表示关闭一些不当编译器警告信息)
自定义注解
//元注解,注解中的注解
//@Target表示注解可以应用在类、属性还是方法上
@Target({ElementType.FIELD, ElementType.TYPE})
//@Retention表示注解的生命周期,比如源代码、类文件、运行阶段
@Retention(RetentionPolicy.RUNTIME)
// @Documented表明该注解标记的元素可以被Javadoc 或类似的工具文档化
@Documented
public @interface Info {
String value() default "tracy";
boolean isDelete();
}
2.Optional类
解决需要使用大量代码来处理空指针异常的问题,使用Optional类,我们只需将的得到的结果作为参数传入,再进行非空判断后进一步处理即可
Optional<传入的参数对象类型> opt = Optional.ofNullable(参数对象);
if(opt.isPresent){
}else{
}
3.Stream类
将一个类型的对象转换成Stream类对象进行操作,最终返回想要的对象。
中间操作,方法返回的是Stream类型
最终操作,返回的是最终想要的对象类型
支持链式调用
xx.xxx().xxx().xxx()
4.Lambda表达式
简写函数式接口(有且仅有一个抽象方法)的匿名内部类
5.方法引用
Lambda表达式借用其他类的方法体
6.Base64
7.并行数组
8.调用JavaScript
39. this关键字和super关键字
this关键字可以认为是当前这个对象
this关键字的用法:
- 调用当前类的成员变量,如this.age(可省略this)
- 调用当前类的成员方法,如this.sayHello()
- 调用当前类的构造器,调用无参构造器this();调用有参构造器,this("tom",20)。this调用当前类构造器必须写在代码块的第一行
super关键字可以认为是父类对象
super关键字的用法(直接父类:当前类最近的父类,以下父类默认为直接父类):
- 引用父类的实例变量,如superr.age
- 调用父类的成员方法,如super.sayHello()
- 调用父类的构造器,如调用父类无参构造器super(),调用父类有参构造器super("james",50);必须写在代码块的第一行
static代码块和方法不能出现this和super
this()和super()不可以同时出现在一个构造函数中
注意,super()由编译器隐式提供。如果不写任何super关键字调用父类构造器,编译器默认提供super()调用父类无参构造器
40. 单例模式
public class Singleton{
public static final Singleton INSTANCE = new Singleton();
public static getInstance(){
return INSTANCE;
}
private Singleton{
if(INSTANCE!=null){
throw new RuntimeException();
}
}
}
41. 泛型
如果尖括号里面带有问号,那么代表一个范围,<? extends A> 代表小于等于A的范围,<? super A>代表大于等于A的范围
List<?>和List 是相等的,都代表最大范围
42. 构造方法
特点:
- 构造方法名与类名相同
- 构造方法没有返回值类型,但不用void修饰
- 构造方法可以重载
作用:
初始化成员变量
使用:
构造方法在创建对象时被调用
new Person();//此时无参构造方法被调用
new Person("tom",20);//此时含参构造方法被调用
java程序默认提供类无参构造方法,当重写含参构造方法后,默认无参构造方法消失,需要使用无参构造方法则需重写
拓展:接口、匿名内部类中没有构造器,抽象类可以有构造器供子类调用
43. SVN与Git
两者的区别:
首先两者之间最核心的区别就是SVN属于集中式控制系统, git属于分布式控制系统
简单来说Git就是以每一台主机都当成一台服务器,而SVN则是只有一台服务器来维护和控制代码。
SVN的管理方法是一台主服务器管理所有主机,这样所有代码全部传输到服务器上统一管理。而git采用单个主机管单个主机的方法,即主机2将主机1中所有的信息和内容拷贝到自己的主机下,而主机3,主机4以此类推
SVN:
git:
44.数组查找的时间复杂度
数组适合查找操作,但是查找的时间复杂度并不为O(1),即便是排序好的数据,用二分查找,时间复杂度也是O(log2n),所以正确的表述是,数组支持随机访问,根据下标随机访问的时间复杂度为O(1)
45.编译和链接
编译:将源代码转换为字节码
链接:为程序调用系统提供的库
45.字符集和编码集
Unicode 是字符集
UTF-8 是编码集
46.HTTP请求报文
http:超文本传输协议,规定了浏览器与万维网服务器之间的通信规则
HTTP报文又分为:
请求行:
①是请求方法,GET和POST是最常见的HTTP方法,除此以外还包括DELETE、HEAD、OPTIONS、PUT、TRACE。
②为请求对应的URL地址,它和报文头的Host属性组成完整的请求URL。
③是协议名称及版本号。
请求头:
④是HTTP的报文头,报文头包含若干个属性,格式为“属性名:属性值”,服务端据此获取客户端的信息。
与缓存相关的规则信息,均包含在header中
请求体:
⑤是报文体,它将一个页面表单中的组件值通过param1=value1¶m2=value2的键值对形式编码成一个格式化串,它承载多个请求参数的数据。不但报文体可以传递请求参数,请求URL也可以通过类似于“/chapter15/user.html? param1=value1¶m2=value2”的方式传递请求参数。
47.进程与线程的区别:
- 进程:指在系统中正在运行的一个应用程序;线程:程序中的一个执行单元
- 一个线程只能属于一个进程,一个进程可以包含多个线程
- 进程的资源开销比线程大
48.java继承父类、实现多接口同名方法问题
子类继承父类,实现接口,父类和接口有同名方法
子类直接继承父类和实现接口不实现接口方法不报错,说明这种情况下默认父类实现该方法
一个类实现多个接口,而多个接口中出现同名方法(需要看同名方法的返回值是否相同)
如果该同名方法的返回值也一样,即同名方法的方法名、参数名、参数个数、返回值都一样,那么该类只需要实现其中一个方法就可以了。
如果多个接口有同名方法,但是它们的返回值不一样,那么需要分别实现返回值不同的同名方法。如果只实现一个是无法编译的。
49.system.exit(int status)
- 正常退出
status为0时为正常退出程序,也就是结束当前正在运行中的java虚拟机。
- 非正常退出
status为非0的其他整数,表示非正常退出当前程序。
50.线程释放锁对象
wait()
join()
51.BIO NIO AIO
- BIO(Blocking IO)同步阻塞IO
- NIO(New IO)同步非阻塞IO
- AIO(Asynchronous IO)异步非阻塞IO
- 同步阻塞:你到饭馆点餐,然后在那等着,啥都干不了,餐没做好,你就必须等着!
- 同步非阻塞:你在饭馆点完餐,就去玩儿了。不过玩一会儿,就回饭馆问一声:好了没?
- 异步非阻塞:饭馆打电话说,我们知道您的位置,一会给你送过来,安心玩儿就可以了,类似于外卖。
51.哈希冲突
对于不同对象在数组中映射的位置相同发生冲突
解决:
-
开放地址法:
所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入
-
链地址法:
对于哈希冲突的元素,将它们存放在一条单链表上
52.数组和链表的区别
1、数组是一组连续的内存空间,是顺序存储结构。
链表是链式存储结构,通过指针连接元素。
2.数组支持随机下标访问,查询效率高;但是增删需要移动元素,所以增删效率低
链表的插入和删除元素比较简单,不需要移动元素,只需改变元素的指针指向,所以链表的增删效率比较好;但是链表不支持随机访问,查询比较慢。
53.Array与ArrayList的区别
Array是数组,一组连续的内存空间,定长格式,数据类型在必须在数组创建的时候声明
ArrayList属于集合,底层是数组,但它是动态数组,会自动扩容
54.Queue
1.add()、offer()方法的区别
- Queue 中 add() 和 offer() 都是用来向队列添加一个元素。
- 在容量已满的情况下,add() 方法会抛出IllegalStateException异常,offer() 方法只会返回 false 。
2.remove()、poll()方法的区别
- Queue 中 remove() 和 poll() 都是用来从队列头部删除一个元素。
- 在队列元素为空的情况下,remove() 方法会抛出NoSuchElementException异常,poll() 方法只会返回 null 。
3.element()、peek()方法的区别
- Queue 中 element() 和 peek() 都是用来返回队列的头元素,不删除。
- 在队列元素为空的情况下,element() 方法会抛出NoSuchElementException异常,peek() 方法只会返回 null。
55.面向对象设计原则
- 开闭原则
- 单一职责原则
- 李氏替换原则
- 组合重用原则
- 依赖导致原则
- 迪米特原则
- 接口隔离原则
56.软件的生命周期
- 可行性分析
- 需求分析
- 概要设计
- 详细设计
- 编码
- 测试
- 维护
57.Runnable接口与Callable接口的区别
1、两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;
2、Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;