Java八股文面试笔记整理(一)

简介: Java八股文面试笔记整理

Java面试

1-Java基础

基础篇要点:算法、数据结构、基础设计模式

1-1.二分查找

要求
  • 能够用自己语言描述二分查找算法
  • 能够手写二分查找代码
  • 能够解答一些变化后的考法
算法描述
  1. 前提:有已排序数组 A(假设已经做好)
  2. 定义左边界 L、右边界 R,确定搜索范围,循环执行二分查找(3、4两步)
  3. 获取中间索引 M = Floor((L+R) /2)
  4. 中间索引的值 A[M] 与待搜索的值 T 进行比较
    ① A[M] == T 表示找到,返回中间索引
    ② A[M] > T,中间值右侧的其它元素都大于 T,无需比较,中间索引左边去找,M - 1 设置为右边界,重新查找
    ③ A[M] < T,中间值左侧的其它元素都小于 T,无需比较,中间索引右边去找, M + 1 设置为左边界,重新查找
  5. 当 L > R 时,表示没有找到,应结束循环
算法实现
/**
 * @author bobochang
 * @description 二分查找代码
 * @created 2022/9/20-00:11
 **/
public class BinarySearch {
    public static void main(String[] args) {
        // 1.定义有序的int类型数组
        int[] array = {24, 33, 45, 56, 58, 62, 74, 89, 102};
        // 2.定义需要查找的对象
        int target = 74;
        // 3.调用二分查找方法函数 获得对象在数组中索引值
        int index = binarySearch(array, target);
        // 4.输出结果
        System.out.println("index = " + index);
    }
    private static int binarySearch(int[] array, int target) {
        // 1.定义左右边界和中间值
        int left = 0, right = array.length - 1, m = 0;
        // 2.循环执行二分查找
        while (left <= right) {
            // 避免int整数相加溢出 更换减法 left/2 + right/2 ==> 
            // left + ( -left/2 + right/2 ) ==> left + ( right - left ) / 2
            // m = (left + right) / 2;
            // m = left + (right - left) / 2;
            // 采用位运算右移1位则代表除以2
            m = (left + right) >>> 1;
            // 3.判断查找对象和遍历的当前值
            if (array[m] == target) {
                return m;
            } else if (array[m] > target) {
                right = m - 1;
            } else {
                left = m + 1;
            }
        }
        return -1;
    }
}
选择题考法
  1. 有一个有序表为 1,5,8,11,19,22,31,35,40,45,48,49,50 当二分查找值为 48 的结点时,需要经过( 4 )次比较
  2. 使用二分法在序列 1,4,6,7,15,33,39,50,64,78,75,81,89,96 中查找元素 81 时,需要经过( 4 )次比较

对于前两个题目,记得一个简要判断口诀:奇数二分取中间,偶数二分取中间靠左。

  1. 在拥有128个元素的数组中二分查找一个数,需要比较的次数最多不超过多少次

image.png

1-2.冒泡排序

要求
  • 能够用自己语言描述冒泡排序算法
  • 能够手写冒泡排序代码
  • 了解一些冒泡排序的优化手段
算法描述
  1. 依次比较数组中相邻两个元素大小,若 a[j] > a[j+1],则交换两个元素,两两都比较一遍称为一轮冒泡,结果是让最大的元素排至最后
  2. 重复以上步骤,直到整个数组有序
算法实现
/**
     * 通过判断当前是否发生变化从而优化算法
     *
     * @param array
     */
    private static void bubble(int[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            boolean swapped = false;
            for (int j = 0; j < array.length - 1 - i; j++) {
                if (array[j] > array[j + 1]) {
                    swap(array, j, j + 1);
                    swapped = true;
                }
            }
            System.out.println("第" + (i + 1) + "轮数组 = " + Arrays.toString(array));
            if (!swapped) {
                break;
            }
        }
    }
  • 优化点1:每经过一轮冒泡,内层循环就可以减少一次
  • 优化点2:如果某一轮冒泡没有发生交换,则表示所有数据有序,可以结束外层循环
优化算法实现
/**
     * 通过判断最后一次交换值是否发送改变
     * 从而判断最后一次被交换的索引至最后一位已有序 进而优化算法
     *
     * @param array
     */
    private static void bubble(int[] array) {
        int swapIndex = array.length - 1;
        do {
            // 1.进入循环至0
            int last = 0;
            for (int i = 0; i < swapIndex; i++) {
                if (array[i] > array[i + 1]) {
                    swap(array, i, i + 1);
                    // 2.记录最后一次交换值的索引
                    last = i;
                }
            }
            // 3.将最后一次交换的索引值赋值 作为下一次排序遍历的长度
            swapIndex = last;
            // 4.判断当前是否已经交换完 形成有序数组
            System.out.println("Arrays.toString(array) = " + Arrays.toString(array));
        } while (swapIndex != 0);
    }

每轮冒泡时,最后一次交换索引可以作为下一轮冒泡的比较次数,如果这个值为零,表示整个数组有序,直接退出外层循环即可

1-3.选择排序

要求
  • 能够用自己语言描述选择排序算法
  • 能够比较选择排序与冒泡排序
  • 理解非稳定排序与稳定排序
算法描述
  1. 将数组分为两个子集,排序的和未排序的,每一轮从未排序的子集中选出最小的元素,放入排序子集
  2. 重复以上步骤,直到整个数组有序
算法实现
/**
     * 选择排序-算法实现
     *
     * @param array
     */
    private static void selection(int[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            // i代表每轮排序最小值需要交换到的具体索引
            // selectIndex代表每轮已被选择的值当前在数据中的具体索引
            int selectIndex = i;
            for (int j = selectIndex + 1; j < array.length; j++) {
                // 遍历(选择)剩余未被排序的其他数组
                if (array[selectIndex] > array[j]) {
                    // 记录当前该轮出现最小值
                    selectIndex = j;
                    // 当争论执行完再进行交换 减少交换次数
                    //swap(array, i, j);
                }
            }
            if (selectIndex != i) {
                swap(array, selectIndex, i);
            }
            System.out.println("Arrays.toString(array) = " + Arrays.toString(array));
        }
    }
与冒泡排序比较
  1. 二者平均时间复杂度都是 O(n2)O(n^2)O(n2)
  2. 选择排序一般要快于冒泡,因为其交换次数少
  3. 但如果集合有序度高,冒泡优于选择
  4. 冒泡属于稳定排序算法,而选择属于不稳定排序
  • 稳定排序指,按对象中不同字段进行多次排序,不会打乱同值元素的顺序
  • 不稳定排序则反之

1-4.插入排序

要求
  • 能够用自己语言描述插入排序算法
  • 能够比较插入排序与选择排序
算法描述
  1. 将数组分为两个区域,排序区域和未排序区域,每一轮从未排序区域中取出第一个元素,插入到排序区域(需保证顺序)
  2. 重复以上步骤,直到整个数组有序
算法实现
/**
     * 插入排序-算法实现
     * @param array
     */
    private static void insertSort(int[] array) {
        // 当前i表示需要进行插入的元素的索引值
        for (int i = 1; i < array.length; i++) {
            // 通过临时变量存储需要向前插入元素的值
            int temp = array[i];
            // 通过i值找到与之前一位的索引值
            int front = i - 1;
            while (front >= 0) {
                if (temp < array[front]) {
                    // 若需要插入元素值小于前一位元素值 较大值后移
                    array[front + 1] = array[front];
                } else {
                    // 已找到需要插入的位置索引值 退出循环
                    break;
                }
                front--;
            }
            // 向比较索引值的后一位进行插入操作
            array[front + 1] = temp;
            System.out.println("Arrays.toString(array) = " + Arrays.toString(array));
        }
    }
与选择排序比较
  1. 二者平均时间复杂度都是 O(n2)O(n^2)O(n2)
  2. 大部分情况下,插入都略优于选择(选择略优于冒泡)
  3. 有序集合插入的时间复杂度为 O(n)O(n)O(n)
  4. 插入属于稳定排序算法,而选择属于不稳定排序
警示

插入排序易被轻视,其实它的地位非常重要。小数据量排序,都会优先选择插入排序

1-5.希尔排序

要求
  • 能够用自己语言描述希尔排序算法
算法描述
  1. 首先选取一个间隙序列,如 (n/2,n/4 … 1),n 为数组长度
  2. 每一轮将间隙相等的元素视为一组,对组内元素进行插入排序,目的有二
    ① 少量元素插入排序速度很快
    ② 让组内值较大的元素更快地移动到后方
  3. 当间隙逐渐减少,直至为 1 时,即可完成排序
算法实现
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);
        }
    }
}

1-6.快速排序

要求
  • 能够用自己语言描述快速排序算法
  • 掌握手写单边循环、双边循环代码之一
  • 能够说明快排特点
  • 了解洛穆托与霍尔两种分区方案的性能比较
算法描述
  1. 每一轮排序选择一个基准点(pivot)进行分区
  1. 让小于基准点的元素的进入一个分区,大于基准点的元素的进入另一个分区
  2. 当分区完成时,基准点元素的位置就是其最终位置
  1. 在子分区内重复以上过程,直至子分区元素个数少于等于 1,这体现的是分而治之的思想 (divide-and-conquer
  2. 从以上描述可以看出,一个关键在于分区算法,常见的有洛穆托分区方案、双边循环分区方案、霍尔分区方案
单边循环快排(lomuto 洛穆托分区方案)
  1. 选择最右元素作为基准点元素
  2. j 指针负责找到比基准点小的元素,一旦找到则与 i 进行交换
  3. i 指针维护小于基准点元素的边界,也是每次交换的目标索引
  4. 最后基准点与 i 交换,i 即为分区位置
算法实现

/**
 * @author bobochang
 * @description 快速排序-单边循环
 * @created 2022/9/20-14:23
 **/
public class QuickSortPartition {
    public static void main(String[] args) {
        int[] array = {5, 3, 7, 2, 9, 8, 1, 4};
        quick(array, 0, array.length - 1);
    }
    public static void quick(int[] array, int l, int h) {
        // 判断当前是否已经排序完成
        if (l >= h) {
            return;
        }
        // 找出基准点的索引值 放入partition变量中
        int partition = partition(array, l, h);
        // 左边界进行排序
        quick(array, l, partition - 1);
        // 右边界进行排序
        quick(array, partition + 1, h);
    }
    private static int partition(int[] array, int l, int h) {
        // 1.定义基准点(当前为默认最右侧元素为基准点)
        int pv = array[h];
        // 2.定义初始指针 i j
        int i = l;
        // 3.循环对比查找比基准点小的值并进行交换 将其置于基准点左侧
        for (int j = l; j < h; j++) {
            if (array[j] < pv) {
                // 比基准点小 将维护左边界的索引与值进行交换
                // 当i和j不相等时候才进行交换 避免性能消耗浪费
                if (j != i) {
                    swap(array, i, j);
                }
                i++;
            }
        }
        // 基准点位置与j进行交换
        // 当i和h不相等时候才进行交换 避免性能消耗浪费
        if (i != h) {
            swap(array, i, h);
        }
        System.out.println("Arrays.toString(array) = " + Arrays.toString(array));
        return i;
    }
    /**
     * 交换数组中指定的两个值
     *
     * @param array
     * @param first
     * @param second
     */
    private static void swap(int[] array, int first, int second) {
        int temp = array[first];
        array[first] = array[second];
        array[second] = temp;
    }
}
双边循环快排(不完全等价于 hoare 霍尔分区方案)
  1. 选择最左元素作为基准点元素
  2. j 指针负责从右向左找比基准点小的元素,i 指针负责从左向右找比基准点大的元素,一旦找到二者交换,直至 i,j 相交
  3. 最后基准点与 i(此时 i 与 j 相等)交换,i 即为分区位置
要点
  1. 基准点在左边,并且要先 j 后 i(先进行右指针查找 再进行左指针的查找)

因为基准点元素在最左边, 而左边分区都是小于等于基准点元素

如果是i先动的话, 最后i=j停的位置一定是比基准点元素大的地方, 此时发生交换, 必然乱序

  1. while( i< j && a[j] > pv ) j--
  2. while ( i< j && a[i] <= pv ) i++
算法实现

/**
 * @author bobochang
 * @description 快速排序-双边循环
 * @created 2022/9/20-14:23
 **/
public class QuickSortPartition {
    public static void main(String[] args) {
        int[] array = {5, 3, 7, 2, 9, 8, 1, 4};
        quick(array, 0, array.length - 1);
    }
    public static void quick(int[] array, int l, int h) {
        // 判断当前是否已经排序完成
        if (l >= h) {
            return;
        }
        // 找出基准点的索引值 放入partition变量中
        int partition = partition(array, l, h);
        // 左边界进行排序
        quick(array, l, partition - 1);
        // 右边界进行排序
        quick(array, partition + 1, h);
    }
    private static int partition(int[] array, int l, int h) {
        // 以最左侧的元素值作为基准值
        int pv = array[l];
        // 定义左边界起始指针i 右边界其实指针j
        int i = l;
        int j = h;
        // 循环遍历数组 当两个指针重合时停下 并将基准值与重合元素值进行交换
        while (i < j) {
            // 右边界指针由右往左选择比基准值小的元素
            while (i < j && array[j] > pv) {
                j--;
            }
            // 左边界指针由左往右选择比基准值大的元素
            while (i < j && array[i] <= pv) {
                i++;
            }
            // 将左右边界找到的值进行交换
            swap(array, i, j);
        }
        swap(array, l, i);
        System.out.println("Arrays.toString(array) = " + Arrays.toString(array));
        return i;
    }
    /**
     * 交换数组中指定的两个值
     *
     * @param array
     * @param first
     * @param second
     */
    private static void swap(int[] array, int first, int second) {
        int temp = array[first];
        array[first] = array[second];
        array[second] = temp;
    }
}
快排特点
  1. 平均时间复杂度是 O(nlog2⁡n)O(nlog_2⁡n )O(nlog2n),最坏时间复杂度 O(n2)O(n^2)O(n2)

快速排序时间复杂度总体小于其他排序(冒泡排序、选择排序、插入排序、希尔排序)

  1. 数据量较大时,优势非常明显
  2. 属于不稳定排序

1-7.集合ArrayList

要求
  • 掌握 ArrayList 扩容规则
扩容规则
  1. ArrayList() 会使用长度为零的数组
  2. ArrayList(int initialCapacity) 会使用指定容量的数组
  3. public ArrayList(Collection<? extends E> c) 会使用集合的大小作为数组容量
  4. add(Object o) 首次扩容为 10,再次扩容为上次容量的 1.5 倍
  5. addAll(Collection c) 没有元素时,扩容为 Math.max(10, 实际元素个数),有元素时为 Math.max(原容量 1.5 倍, 实际元素个数)

Math.max(a,b) ==> 函数意义:从值ab中取最大值

1-8.集合LinkedList

要求
  • 能够说清楚 LinkedList 对比 ArrayList 的区别,并重视纠正部分错误的认知
LinkedList
  1. 基于双向链表,无需连续内存
  2. 随机访问慢(要沿着链表遍历)
  3. 头尾插入删除性能高
  4. 占用内存多
ArrayList
  1. 基于数组,需要连续内存
  2. 随机访问快(指根据下标访问)
  3. 尾部插入、删除性能可以,其它部分插入、删除都会移动数据,因此性能会低
  4. 可以利用 cpu 缓存,局部性原理

内存连续存储 使得放入cache缓存中可存入多个数值 提高性能

1-9.Iterator

要求
  • 掌握什么是 Fail-Fast、什么是 Fail-Safe
Fail-Fast 与 Fail-Safe
  • ArrayList 是 fail-fast 的典型代表,遍历的同时不能修改,尽快失败

底层源码根据记录创建迭代器时的修改数值expeectedModCount

将遍历记录的expeectedModCount值与实时值modCount进行比较 不相等则表示遍历时发生修改

并进行抛出遍历时并发异常ConcurrentModificationException

  • CopyOnWriteArrayList 是 fail-safe 的典型代表,遍历的同时可以修改,原理是读写分离

底层源码实现是通过创建迭代器时 将传入数组进行复制并扩容(传入数组长度+1)

并在遍历时 遍历对象为复制扩容后的数组 该操作便牺牲了数据的一致性

补充

List中迭代器实现-Vector(采用也是Fail-Fast

Vector在添加前也会对比是否一致,if (modCount != expectedModCount)

不一致则throw new ConcurrentModificationException

1-10.HashMap

要求
  • 掌握 HashMap 的基本数据结构
  • 掌握树化
  • 理解索引计算方法、二次 hash 的意义、容量对索引计算的影响
  • 掌握 put 流程、扩容、扩容因子
  • 理解并发使用 HashMap 可能导致的问题
  • 理解 key 的设计
基本数据结构
  • 1.7 数组 + 链表
  • 1.8 数组 + (链表 | 红黑树)

选择红黑树原因:1.7中的链表过长影响性能

何时会树化:当链表长度超过树化阈值[链表长度] 8 时,先尝试扩容来减少链表长度,如果数组容量已经 >=64,才会进行树化

何时会退化为链表:

  • 情况1:在扩容时如果拆分树时,树元素个数 <= 6 则会退化链表
  • 情况2:remove 树节点时,若 root、root.left、root.right、root.left.left 有一个为 null ,也会退化为链表
树化意义
  • 红黑树用来避免 DoS 攻击,防止链表超长时性能下降,树化应当是偶然情况,是保底策略
  • hash 表的查找,更新的时间复杂度是 O(1)O(1)O(1),而红黑树的查找,更新的时间复杂度是 O(log2⁡n)O(log_2⁡n )O(log2n),TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表
  • hash 值如果足够随机,则在 hash 表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006,树化阈值选择 8 就是为了让树化几率足够小
树化规则
  • 当链表长度超过树化阈值 8 时,先尝试扩容来减少链表长度,如果数组容量已经 >=64,才会进行树化
退化规则
  • 情况1:在扩容时如果拆分树时,树元素个数 <= 6 则会退化链表
  • 情况2:remove 树节点时,若 root、root.left、root.right、root.left.left 有一个为 null ,也会退化为链表
索引计算方法
  • 首先,计算对象的 hashCode()
  • 再进行调用 HashMap 的 hash() 方法进行二次哈希

二次 hash() 是为了综合高位数据,让哈希分布更为均匀

  • 最后 & (capacity – 1) 得到索引(或根据容量capacity求mod运算)
数组容量为何是 2 的 n 次幂
  1. 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
  2. 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap
注意
  • 二次 hash 是为了配合 容量是 2 的 n 次幂 这一设计前提,如果 hash 表的容量不是 2 的 n 次幂,则不必二次 hash
  • 容量是 2 的 n 次幂 这一设计计算索引效率更好,但 hash 的分散性就不好,需要二次 hash 来作为补偿,没有采用这一设计的典型例子是 Hashtable
put 流程
  1. HashMap 是懒惰创建数组的,首次使用才创建数组
  2. 计算索引(桶下标)
  3. 如果桶下标还没人占用,创建 Node 占位返回
  4. 如果桶下标已经有人占用
  1. 已经是 TreeNode 走红黑树的添加或更新逻辑
  2. 是普通 Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑
  1. 返回前检查容量是否超过阈值,一旦超过进行扩容
1.7 与 1.8 的区别
  1. 链表插入节点时,1.7 是头插法,1.8 是尾插法
  2. 1.7 是大于等于阈值且没有空位时才扩容,而 1.8 是大于阈值就扩容
  3. 1.8 在扩容计算 Node 索引时,会优化
扩容(加载)因子为何默认是 0.75f
  1. 在空间占用与查询时间之间取得较好的权衡
  2. 大于这个值,空间节省了,但链表就会比较长影响性能
  3. 小于这个值,冲突减少了,但扩容就会更频繁,空间占用也更多
常见HashMap面试问题
  1. 多线程下会有什么问题
  • 扩容死链(JDK1.7)
  • 数据错乱(JDK1.8)
  1. HashMap中的key能否为null 作为key的对象(value)有什么要求
  • HashMap的key可以为null 但Map接口的其他实现不可以
  • 作为Key的对象 必须实现hashCode()和equels() 并且key的值不能发生改变(不可变)

若key发生变, 会导致再次查询时查询不到

  1. String对象的hashCode()如何实现 为什么每次乘的是31
  • String对象进行hashCode()的目标是达到较为均匀的散列效果 使得每个字符串的 hashCode 足够独特
  • 字符串中的每个字符都可以表现为一个数字,称为 SiS_iSi,其中 i 的范围是 0 ~ n - 1
  • 散列公式为: S0∗31(n−1)+S1∗31(n−2)+…Si∗31(n−1−i)+…S(n−1)∗310S_0∗31^{(n-1)}+ S_1∗31^{(n-2)}+ … S_i ∗ 31^{(n-1-i)}+ …S_{(n-1)}∗31^0S031(n1)+S131(n2)+Si31(n1i)+S(n1)310
  • 31 代入公式有较好的散列特性,并且 31 * h 可以被优化为
  • 32∗h−h32 ∗h -h 32hh
  • 25∗h−h2^5 ∗h -h25hh
  • h≪5−hh≪5 -hh5h

1-11.单例模式

要求
  • 掌握五种单例模式的实现方式
  • 理解为何 DCL 实现时要使用 volatile 修饰静态变量
  • 了解 jdk 中用到单例的场景
饿汉式

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()");
    }
}
  • 枚举饿汉式能天然防止反射反序列化破坏单例

懒汉式

public class Singleton3 implements Serializable {
    private Singleton3() {
        System.out.println("private Singleton3()");
    }
    private static Singleton3 INSTANCE = null;
    // Singleton3.class
    public static synchronized Singleton3 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }
    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}
  • 其实只有首次创建单例对象时才需要同步,但该代码实际上每次调用都会同步
  • 故在方法上加synchronized会导致性能下降,便有了双检锁懒汉式优化
双检锁懒汉式

csharp

复制代码

第一次检查目的:通过判断当前是否已经存在单例 减少性能消耗

第二次检查目的:通过判断释放锁的资源是否已经完成单例创建,避免资源浪费与单例的重复创建

为何必须加 volatile:

  • INSTANCE = new Singleton4() 不是原子的,分成 3 步:创建对象、调用构造、给静态变量赋值,其中后两步可能被指令重排序优化,变成先赋值、再调用构造
  • 如果线程1 先执行了赋值,线程2 执行到第一个 INSTANCE == null 时发现 INSTANCE 已经不为 null,此时就会返回一个未完全构造(属性值未进行初始化)的对象
内部类懒汉式
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()");
    }
}
  • 避免了双检锁的缺点
  • 内部类懒汉式解决了在静态代码块中完成单例创建,利用内部类在加载时才完成单例创建,通过JVM虚拟机确保线程安全解决破坏单例问题
JDK 中单例的体现
  • Runtime 体现了饿汉式单例
  • Console 体现了双检锁懒汉式单例
  • Collections 中的 EmptyNavigableSet 内部类懒汉式单例
  • ReverseComparator.REVERSE_ORDER 内部类懒汉式单例
  • Comparators.NaturalOrderComparator.INSTANCE 枚举饿汉式单例


目录
相关文章
|
11天前
|
监控 Dubbo Java
Java Dubbo 面试题
Java Dubbo相关基础面试题
|
11天前
|
SQL Java 数据库连接
Java MyBatis 面试题
Java MyBatis相关基础面试题
|
11天前
|
存储 监控 算法
Java JVM 面试题
Java JVM(虚拟机)相关基础面试题
|
11天前
|
SQL 监控 druid
Java Druid 面试题
Java Druid 连接池相关基础面试题
|
11天前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
5月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
2月前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
|
2月前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
2月前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
84 4
|
3月前
|
算法 Java 数据中心
探讨面试常见问题雪花算法、时钟回拨问题,java中优雅的实现方式
【10月更文挑战第2天】在大数据量系统中,分布式ID生成是一个关键问题。为了保证在分布式环境下生成的ID唯一、有序且高效,业界提出了多种解决方案,其中雪花算法(Snowflake Algorithm)是一种广泛应用的分布式ID生成算法。本文将详细介绍雪花算法的原理、实现及其处理时钟回拨问题的方法,并提供Java代码示例。
122 2