若文章内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系博主删除。
我自己是根据 黑马官网 公布的 《Java 八股文面试题教程_Java 面试八股文宝典》 的资料来学习的
黑马官方公开的免费资源的链接
推荐博客(推荐理由:关于视频里的分析过程,该博主记录的非常详细)

要求
算法描述
更形象的描述请参考:binary_search.html
对应视频演示:基础篇_02_二分查找_演示
算法实现
public static int binarySearch(int[] a, int t) {
int l = 0, r = a.length - 1, m;
while (l <= r) {
m = (l + r) / 2;
if (a[m] == t) {
return m;
} else if (a[m] > t) {
r = m - 1;
} else {
l = m + 1;
}
}
return -1;
}
测试代码
public static void main(String[] args) {
int[] array = {1, 5, 8, 11, 19, 22, 31, 35, 40, 45, 48, 49, 50};
int target = 47;
int idx = binarySearch(array, target);
System.out.println(idx);
}
解决整数溢出问题
当 l 和 r 都较大时,l + r 有可能超过整数范围,造成运算错误
解决方法有两种
int m = l + (r - l) / 2;
int m = (l + r) >>> 1;
其他考法
问题
解决方法
注意
要求
算法描述
更形象的描述请参考:bubble_sort.html
对应视频演示:基础篇_09_冒泡排序_演示
算法实现
public static void bubble(int[] a) {
for (int j = 0; j < a.length - 1; j++) {
// 一轮冒泡
boolean swapped = false; // swapped:是否发生了交换
for (int i = 0; i < a.length - 1 - j; i++) {
System.out.println("比较次数" + i);
if (a[i] > a[i + 1]) {
swap(a, i, i + 1);
swapped = true;
}
}
System.out.println("第" + j + "轮冒泡" + Arrays.toString(a));
if (!swapped) {
break;
}
}
}
public static void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
进一步优化
public static void bubble_v2(int[] a) {
int n = a.length - 1;
while (true) {
int last = 0; // 表示最后一次交换时的索引位置
for (int i = 0; i < n; i++) {
System.out.println("比较次数" + i);
if (a[i] > a[i + 1]) {
Utils.swap(a, i, i + 1); // 交换函数(具体实现省略)
last = i;
}
}
n = last;
System.out.println("排序情况" + Arrays.toString(a));
if (n == 0) {
break;
}
}
}
对应视频演示:基础篇_13_冒泡排序_优化_进一步优化比较次数
要求
算法描述
更形象的描述请参考:selection_sort.html
对应视频演示:基础篇_16_选择排序_演示
算法实现
public static void selection(int[] a) {
for (int i = 0; i < a.length - 1; i++) {
// i 代表每轮选择最小元素要交换到的目标索引
// s 代表最小元素的索引
int s = i;
for (int j = s + 1; j < a.length; j++) {
if (a[s] > a[j]) { // j 元素比 s 元素还要小, 更新 s
s = j;
}
}
if (s != i) {
swap(a, s, i);
}
System.out.println(Arrays.toString(a));
}
}
选择排序与冒泡排序的比较
稳定排序与不稳定排序
System.out.println("=================不稳定================");
Card[] cards = getStaticCards();
System.out.println(Arrays.toString(cards));
selection(cards, Comparator.comparingInt((Card a) -> a.sharpOrder).reversed());
System.out.println(Arrays.toString(cards));
selection(cards, Comparator.comparingInt((Card a) -> a.numberOrder).reversed());
System.out.println(Arrays.toString(cards));
System.out.println("=================稳定=================");
cards = getStaticCards();
System.out.println(Arrays.toString(cards));
bubble(cards, Comparator.comparingInt((Card a) -> a.sharpOrder).reversed());
System.out.println(Arrays.toString(cards));
bubble(cards, Comparator.comparingInt((Card a) -> a.numberOrder).reversed());
System.out.println(Arrays.toString(cards));
都是先按照花色排序(♠♥♣♦),再按照数字排序(AKQJ…)
不稳定排序算法按数字排序时,会打乱原本同值的花色顺序
[[♠7], [♠2], [♠4], [♠5], [♥2], [♥5]]
[[♠7], [♠5], [♥5], [♠4], [♥2], [♠2]]
原来 ♠2 在前 ♥2 在后,按数字再排后,他俩的位置变了
稳定排序算法按数字排序时,会保留原本同值的花色顺序,如下所示 ♠2 与 ♥2 的相对位置不变
[[♠7], [♠2], [♠4], [♠5], [♥2], [♥5]]
[[♠7], [♠5], [♥5], [♠4], [♠2], [♥2]]
要求
算法描述
更形象的描述请参考:insertion_sort.html
相关演示视频:基础篇_19_插入排序_演示
优化方式
算法实现
// 修改了代码与希尔排序一致
public static void insert(int[] a) {
// i 代表待插入元素的索引
// t 代表待插入的元素值
// j-1 代表已排序区域的元素索引
for (int i = 1; i < a.length; i++) {
int t = a[i];
int j = i;
System.out.println(j);
while (j >= 1) {
if (t < a[j - 1]) { // j-1 是上一个元素索引,如果 a[j-1] > t,后移
a[j] = a[j - 1];
j--;
} else { // 如果已经是 a[j-1] <= t, 则 j 就是插入位置
break;
}
}
a[j] = t;
System.out.println(Arrays.toString(a) + " " + j);
}
}
与选择排序比较
提示:插入排序通常被同学们所轻视,其实它的地位非常重要。小数据量排序,都会优先选择插入排序
一般对于冒泡排序会直接考查代码情况,而其他排序一般会要求推导某一轮排序结果,或者是阐明算法的实现思路
18 23 19 9 23 15
18 19 23 9 23 15
9 18 19 23 23 15
9 23 19 18 23 15
9 15 19 18 23 23
9 15 18 19 23 23
要求
算法描述
更形象的描述请参考:shell_sort.html
相关视频演示请见:基础篇_22_希尔排序_演示
算法实现
private static void shell(int[] a) {
int n = a.length;
for (int gap = n / 2; gap > 0; gap /= 2) {
// i 代表待插入元素的索引
for (int i = gap; i < n; i++) {
int t = a[i]; // 代表待插入的元素值
int j = i;
while (j >= gap) {
// 每次与上一个间隙为 gap 的元素进行插入排序
if (t < a[j - gap]) { // j-gap 是上一个元素索引,如果 > t,后移
a[j] = a[j - gap];
j -= gap;
} else { // 如果 j-1 已经 <= t, 则 j 就是插入位置
break;
}
}
a[j] = t;
System.out.println(Arrays.toString(a) + " gap:" + gap);
}
}
}
要求
算法描述
更形象的描述请参考:quick_sort.html
相关视频描述:基础篇_24_快速排序_描述
单边循环快排(lomuto 洛穆托分区方案)
相关视频描述:基础篇_25_快速排序_单边循环_演示
// 递归
public static void quick(int[] a, int l, int h) {
if (l >= h) {
return;
}
int p = partition(a, l, h); // p 索引值
quick(a, l, p - 1); // 左边分区的范围确定
quick(a, p + 1, h); // 左边分区的范围确定
}
// 分区
private static int partition(int[] a, int l, int h) {
int pv = a[h]; // 基准点元素
int i = l;
for (int j = l; j < h; j++) {
if (a[j] < pv) {
if (i != j) {
swap(a, i, j);
}
i++;
}
}
if (i != h) {
swap(a, h, i);
}
System.out.println(Arrays.toString(a) + " i=" + i);
// 返回值代表了基准点元素所在的正确索引,用它确定下一轮分区的边界
return i;
}
双边循环快排(不完全等价于 hoare 霍尔分区方案)
相关视频描述:基础篇_28_快速排序_双边循环_演示
要点
private static void quick(int[] a, int l, int h) {
if (l >= h) {
return;
}
int p = partition(a, l, h);
quick(a, l, p - 1);
quick(a, p + 1, h);
}
private static int partition(int[] a, int l, int h) {
int pv = a[l];
int i = l;
int j = h;
/* 下方必须是先进行 j 的循环,再进行 i 的循环
* 因为此处进行的是从小到大的排序(左边区域比基准值小,右边区域比基准值大)
* 如果先找小的元素(i 的循环),最后 i=j 停的位置的元素的值是一定大于等于基准点的
* 最终大的元素会被交换到基准点,从而导致整个方法的逻辑不正确 */
while (i < j) {
/* 在下方的 while 循环中仍然需要加入 i < j 的条件
* 不加该条件会出现 i=j 但循环不会停止的情况(比如 i 仍然继续查找比基准值大的值) */
while (i < j && a[j] > pv) { // j 从右找小的
j--;
}
while (i < j && a[i] <= pv) { // i 从左找大的
/* 因为 i 最开始指向的就是基准点
* 所以在第一次 j 的查找循环退出后,没有进入到 i 的循环
* 直接和 a[j] 交换掉了基准点的值
* 故必须要加入 = 来避免基准点被交换的情况出现 */
i++;
}
swap(a, i, j);
}
swap(a, l, j);
System.out.println(Arrays.toString(a) + " j=" + j);
return j;
}
快排特点
洛穆托分区方案 vs 霍尔分区方案
https://qastack.cn/cs/11458/quicksort-partitioning-hoare-vs-lomuto
以下为资料中提供的补充代码说明
- day01.sort.QuickSort3 演示了空穴法改进的双边快排,比较次数更少
- day01.sort.QuickSortHoare 演示了霍尔分区的实现
- day01.sort.LomutoVsHoare 对四种分区实现的移动次数比较
要求
扩容规则
其中第 4 点必须知道,其它几点视个人情况而定
idea 快捷键:Ctrl + F12

ArrayList 的三个构造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
}
}
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
ArrayList 的 add 方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
ArrayList 的 addAll 方法
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
提示
--add-opens java.base/java.util=ALL-UNNAMED 才能运行通过,后面的例子都有相同问题代码说明
- day01.list.TestArrayList#arrayListGrowRule 演示了 add(Object) 方法的扩容规则,输入参数 n 代表打印多少次扩容后的数组长度
src/main/java/day01/list/TestArrayList.java
private static List<Integer> arrayListGrowRule(int n) {
List<Integer> list = new ArrayList<>();
int init = 0;
list.add(init);
if (n >= 1) {
init = 10;
list.add(init);
}
for (int i = 1; i < n; i++) {
init += (init) >> 1;
list.add(init);
}
return list;
}
要求
Fail-Fast 与 Fail-Safe
ArrayList 是 fail-fast 的典型代表,遍历的同时不能修改,尽快失败
CopyOnWriteArrayList 是 fail-safe 的典型代表,遍历的同时可以修改,原理是读写分离
提示
相关分析视频
要求
LinkedList
ArrayList
代码说明
- day01.list.ArrayListVsLinkedList#randomAccess 对比随机访问性能
- day01.list.ArrayListVsLinkedList#addMiddle 对比向中间插入性能
- day01.list.ArrayListVsLinkedList#addFirst 对比头部插入性能
- day01.list.ArrayListVsLinkedList#addLast 对比尾部插入性能
- day01.list.ArrayListVsLinkedList#linkedListSize 打印一个 LinkedList 占用内存
- day01.list.ArrayListVsLinkedList#arrayListSize 打印一个 ArrayList 占用内存
要求
HashMap 的底层数据结构,1.7 与 1.8 的区别
更形象的演示,见资料中的 hash-demo.jar,运行环境是 jdk 14 及以上版本,进入 jar 包目录,执行下面命令
java -jar --add-exports java.base/jdk.internal.misc=ALL-UNNAMED hash-demo.jar
当 hashmap 中的元素个数大于当前数组容量 * 负载因子(负载因子默认 0.75),就会进行数组扩容。
例:默认情况下数组大小为 16,那么当 HashMap 中的元素个数超过 12 个时 ,就会对当前数组进行扩容。
为何要使用红黑树?为何不一开始就树化?树化阈值为何是 8?何时会树化?何时会退化为链表?
树化意义
树化规则
退化规则
索引如何计算?HashCode 都有了,为何还要提供 hash() 方法?数组容量为何是 2 的 n 次幂?
索引计算方法
数组容量为何是 2 的 n 次幂
注意
put 流程
1.7 与 1.8 的区别
扩容(加载)因子为何默认是 0.75f
扩容死链(1.7 会存在)
1.7 源码如下:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}





数据错乱(1.7,1.8 都会存在)
day01.map.HashMapMissData,具体调试步骤参考视频补充代码说明
- day01.map.HashMapDistribution 演示 map 中链表长度符合泊松分布
- day01.map.DistributionAffectedByCapacity 演示容量及 hashCode 取值对分布的影响
- day01.map.DistributionAffectedByCapacity#hashtableGrowRule 演示了 Hashtable 的扩容规律
- day01.sort.Utils#randomArray 如果 hashCode 足够随机,容量是否是 2 的 n 次幂影响不大
- day01.sort.Utils#lowSameArray 如果 hashCode 低位一样的多,容量是 2 的 n 次幂会导致分布不均匀
- day01.sort.Utils#evenArray 如果 hashCode 偶数的多,容量是 2 的 n 次幂会导致分布不均匀
- 由此得出对于容量是 2 的 n 次幂的设计来讲,二次 hash 非常重要
- day01.map.HashMapVsHashtable 演示了对于同样数量的单词字符串放入 HashMap 和 Hashtable 分布上的区别
key 的设计要求
如果 key 可变,例如修改了 age 会导致再次查询时查询不到
public class HashMapMutableKey {
public static void main(String[] args) {
HashMap<Student, Object> map = new HashMap<>();
Student stu = new Student("张三", 18);
map.put(stu, new Object());
System.out.println(map.get(stu));
stu.age = 19;
System.out.println(map.get(stu));
}
static class Student {
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
}
String 对象的 hashCode() 设计
要求
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。
这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。
这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
结构
单例模式的主要有以下角色:
实现
单例设计模式分类两种:
该方式在成员位置声明 Singleton 类型的静态变量,并创建 Singleton 类的对象 instance。
instance 对象是随着类的加载而创建的。
如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
public class Singleton1 implements Serializable {
// 构造方法私有
private Singleton1() {
if (INSTANCE != null) {
throw new RuntimeException("单例对象不能重复创建");
}
System.out.println("private Singleton1()");
}
// 在成员(静态成员变量)位置创建该类的对象 --- (静态变量方式)
private static final Singleton1 INSTANCE = new Singleton1();
// 对外提供静态方法获取该对象
public static Singleton1 getInstance() {
return INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
public Object readResolve() {
return INSTANCE;
}
}
readResolve() 是防止反序列化破坏单例枚举类实现单例模式是极力推荐的单例实现模式
因为枚举类型是线程安全的,并且只会装载一次
设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单
而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。(其实 Unsafe 依旧可以破坏)
public enum Singleton2 {
INSTANCE;
private Singleton2() {
System.out.println("private Singleton2()");
}
@Override
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
public static Singleton2 getInstance() {
return INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
该方式也实现了懒加载效果,同时又解决了线程安全问题。
但是在 getInstance() 方法上添加了 synchronized 关键字,导致该方法的执行效果特别低。
其实就是在初始化 instance 的时候才会出现线程安全问题,一旦初始化完成就不存在了。
public class Singleton3 implements Serializable {
// 私有构造方法
private Singleton3() {
System.out.println("private Singleton3()");
}
// 在成员位置创建该类的对象
private static Singleton3 INSTANCE = null;
// 对外提供静态方法获取该对象 Singleton3.class
// 在方法上加上 synchronized 关键字,即给该线程加锁,从而获得线程安全保护
public static synchronized Singleton3 getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton3();
}
return INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
对于 getInstance() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的
所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。
由此也产生了一种新的实现模式:双重检查锁模式
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题。
但双重检测锁模式也存在问题:在多线程的情况下,可能会出现空指针问题。
出现空指针问题的原因是 JVM 在实例化对象的时候会进行优化和指令重排序操作。
要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性。
添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。
public class Singleton4 implements Serializable {
private Singleton4() {
System.out.println("private Singleton4()");
}
private static volatile Singleton4 INSTANCE = null; // 可见性,有序性(阻止指令重排)
public static Singleton4 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton4.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton4();
}
}
}
return INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
为何必须加 volatile:
INSTANCE = new Singleton4() 不是原子的,分成 3 步:
INSTANCE == null 时发现INSTANCE已经不为null,此时就会返回一个未完全构造的对象静态内部类单例模式中实例由内部类创建
由于 JVM 在加载外部类的过程中,是不会加载静态内部类的
只有内部类的 属性/方法 被调用时才会被加载, 并初始化其静态属性。
静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。
第一次加载 Singleton 类时不会去初始化 INSTANCE
只有第一次调用 getInstance,虚拟机加载 SingletonHolder,才会初始化 INSTANCE
这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。
在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
public class Singleton5 implements Serializable {
private Singleton5() {
System.out.println("private Singleton5()");
}
/* 静态内部类 */
private static class Holder {
static Singleton5 INSTANCE = new Singleton5();
}
public static Singleton5 getInstance() {
return Holder.INSTANCE;
}
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
在项目中使用单例模式是非常容易出错的,一般是库中才会用到单例模式(JDK 是其中的典型代表)




