面试题基础篇

news2024/9/20 18:49:18

文章目录

  • 1、二分查找
  • 2、冒泡排序
  • 3、选择排序
  • 4、插入排序
  • 5、希尔排序
  • 6、快速排序
  • 7、ArrayList
  • 8、Iterator
  • 9、LinkedList
  • 10、HashMap
    • 10.1、基本数据结构
      • 底层数据结构,1.7和1.8有什么不同?
    • 10.2、树化与退化
      • 为何要用红黑树,为何一上来不树化,数化阈值为何是8,何时会树化,何时会退化为链表?
    • 10.3、索引计算
      • 索引如何计算,hashCode都有了,为何还要提供hash方法,数组容量为何是2的n次幂?
    • 10.4、put 与扩容
    • 10.5、并发问题
      • 多线程下会有啥问题?
      • key能否为null,作为key的对象有什么要求?
      • String对象的hashCode() 如何设计的,为啥每次乘的都是31?
  • 11、单例模式
    • 1、什么是单例模式
    • 2、单例模式的类型
    • 3、单例模式的特点
    • 3、单例模式的五种实现方式

1、二分查找

算法描述

  1. 前提:有已排序数组 a(假设已经做好)

  2. 定义左边界 l、右边界 r,确定搜索范围,循环执行二分查找(3、4两步)

  3. 获取中间索引 m= (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 时,表示没有找到,应结束循环

在这里插入图片描述

算法实现

public class BinarySearch {
    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);
    }
    // 二分查找, 找到返回元素索引,找不到返回 -1
    public static int binarySearch(int[] a, int t) {
        int l = 0;
        int r = a.length - 1;
        int 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;
    }
}

解决整数溢出问题

当 l 和 r 都较大时,l + r 有可能超过整数范围,造成运算错误,解决方法有两种:

第一种:

int m = l + (r - l) / 2;

推导:

m = (l + r) / 2 => l/2 + r/2 => l - l/2 + r/2 => l + (r - l)/2

第二种:位运算,(l+r)若是正数,等价于第一种

int m = (l + r) >>> 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 次比较

    偶数二分取中间靠左

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

    问题转换为log_2 128 , 用 log_10 128 / log_10 2

    • 是整数,即该整数为最终结果
    • 是小数,则舍弃小数部分,整数加一为最终结果

2、冒泡排序

算法描述

  1. 依次比较数组中相邻两个元素大小,若 a[j] > a[j+1](前一个元素大于后一个元素),则交换两个元素,两两都比较一遍称为一轮冒泡,结果是让最大的元素排至最后

  2. 重复以上步骤,直到整个数组有序

算法实现

public static void bubble(int[] a) {
    for (int j = 0; j < a.length - 1; j++) {
        // 一轮冒泡
        boolean swapped = false; // 优化点2,是否发生了交换 
        for (int i = 0; i < a.length - 1 - j; i++) {//优化点1  
            System.out.println("比较次数" + i);
            if (a[i] > a[i + 1]) {
                Utils.swap(a, i, i + 1);
                swapped = true;
            }
        }
        System.out.println("第" + j + "轮冒泡" + Arrays.toString(a));
        if (!swapped) {
            break;
        }
    }
}
  • 优化点1:每经过一轮冒泡,内层循环就可以减少一次
  • 优化点2:如果某一轮冒泡没有发生交换,则表示所有数据有序,可以结束外层循环

进一步优化

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;
        }
    }
}
  • 优化点:每轮冒泡时,最后一次交换索引可以作为下一轮冒泡的比较次数,如果这个值为零,表示整个数组有序,直接退出外层循环即可

3、选择排序

算法描述

  1. 将数组分为两个子集,排序的和未排序的,每一轮从未排序的子集中选出最小的元素,放入排序子集

  2. 重复以上步骤,直到整个数组有序

算法实现

private static void selection(int[] a) {
    for (int i = 0; i < a.length - 1; i++) {
        // i 代表每轮选择最小元素要交换到的目标索引
        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));
    }
}

优化点:为减少交换次数,每一轮可以先找最小的索引,在每轮最后再交换元素

与冒泡排序比较

  1. 二者平均时间复杂度都是 O(n^2)

  2. 选择排序一般要快于冒泡,因为其交换次数少

  3. 但如果集合有序度高,冒泡优于选择

  4. 冒泡属于稳定排序算法,而选择属于不稳定排序

    • 稳定排序指,按对象中不同字段进行多次排序,不会打乱同值元素的顺序
    • 不稳定排序则反之

稳定排序与不稳定排序

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]]
    

面试题

使用直接选择排序算法对序列18,23,19,9,23,15进行排序,第三趟排序后的结果为 9,15,18,19,23,23

  1. 9,23,19,18,23,15
  2. 9,15,19,18,23,23
  3. 9,15,18,19,23,23

4、插入排序

算法描述

  1. 将数组分为两个区域,排序区域和未排序区域,每一轮从未排序区域中取出第一个元素,插入到排序区域(需保证顺序)

  2. 重复以上步骤,直到整个数组有序

在这里插入图片描述

算法实现

// 修改了代码与希尔排序一致
public static void insert(int[] a) {
    // i 代表待插入元素的索引
    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 是上一个元素索引,如果 > t,后移
                a[j] = a[j - 1];
                j--;
            } else { // 如果 j-1 已经 <= t, 则 j 就是插入位置
                break;
            }
        }
        a[j] = t;
        System.out.println(Arrays.toString(a) + " " + j);
    }
}

与选择排序比较

  1. 二者平均时间复杂度都是 O(n^2)

  2. 大部分情况下,插入都略优于选择

  3. 有序集合插入的时间复杂度为 O(n)

  4. 插入属于稳定排序算法,而选择属于不稳定排序

提示

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

面试题:
使用直接插入排序算法对序列18,23,19,9,23,15进行排序,第三趟排序后的结果为 9,18,19,23,23,15

  1. 18,23
  2. 18,19,23
  3. 9,18,19,23
  4. 9,18,19,23,23
  5. 9,15,18,19,19,23

插入排序有个缺陷,要是大的元素全都集中在前面,那交换的次数会很多,为了解决这个问题,引入了希尔排序。

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);
        }
    }
}

6、快速排序

算法描述

  1. 每一轮排序选择一个基准点(pivot)进行分区

    1. 让小于基准点的元素的进入一个分区,大于基准点的元素的进入另一个分区
    2. 当分区完成时,基准点元素的位置就是其最终位置
  2. 在子分区内重复以上过程,直至子分区元素个数少于等于 1,这体现的是分而治之的思想 (divide-and-conquer)

  3. 从以上描述可以看出,一个关键在于分区算法,常见的有洛穆托分区方案、双边循环分区方案、霍尔分区方案

单边循环快排(lomuto 洛穆托分区方案)

  1. 选择最右元素作为基准点元素s

  2. j 指针负责找到比基准点小的元素,一旦找到则与 i 进行交换

  3. i 指针维护小于基准点元素的边界,也是每次交换的目标索引

  4. 最后基准点与 i 交换,i 即为分区位置

//递归
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++;
        }
    }
    //将基准点元素与i进行交换
    if (i != h) {//优化点
        swap(a, h, i);
    }
    System.out.println(Arrays.toString(a) + " i=" + i);
    // 返回值代表了基准点元素所在的正确索引,用它确定下一轮分区的边界
    return i;
}

双边循环快排(不完全等价于 hoare 霍尔分区方案)

  1. 选择最左元素作为基准点元素
  2. j 指针负责从右向左找比基准点小的元素,i 指针负责从左向右找比基准点大的元素,一旦找到二者交换,直至 i,j 相交
  3. 最后基准点与 i(此时 i 与 j 相等)交换,i 即为分区位置

要点

  1. 基准点在左边,并且要先 j 后 i

  2. while(i < j&& a[j] > pv ) j - -

  3. while ( i < j && a[i] <= pv ) i++

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;
    while (i < j) {
        //先进行j的查找,然后进行i的查找;
        
        // j 从右找小的,j与基准点元素作比较
        //在内层循环为啥还要加i<j的条件呢?
        //必须加
        while (i < j && a[j] > pv) {
            j--;
        }
        // i 从左找大的,i与基准点元素作比较
        //为什么是<=呢?
        //因为刚开始基准点元素和i元素相等,a[i] < pv不满足条件,也就不会进入循环,后面进行交换的时候就将基准点元素交换走了,肯定不对呀
        while (i < j && a[i] <= pv) {
            i++;
        }
        swap(a, i, j);
    }
    //交换基准点元素与j或者i,此时i==j
    swap(a, l, j);
    System.out.println(Arrays.toString(a) + " j=" + j);
    return j;
}

快排特点

  1. 平均时间复杂度是 O(nlog_2⁡n ),最坏时间复杂度 O(n^2)

  2. 数据量较大时,优势非常明显

  3. 属于不稳定排序

洛穆托分区方案 vs 霍尔分区方案

  • 霍尔的移动次数平均来讲比洛穆托少3倍

7、ArrayList

扩容规则

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

代码演示

我们先来看一下add(Object)方法的扩容规则

public class TestArrayList {
    public static void main(String[] args) {
        System.out.println(arrayListGrowRule(30));
    }

    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;
    }
}

在这里插入图片描述

这里有小伙伴就有疑问了,第三次扩容是15没问题,那第四次扩容不应该是15*1.5等于22.5,怎么会等于22呢?其实啊,查看代码我们会发现扩容1.5倍并不是乘以1.5,而是15右移一位(整数的右移1相当于除2)等于7,加上之前的15就等于22了。

接下来看一下addAll(Collection c) 方法的扩容规则

  • 集合为空时的扩容规则
  • 集合里面有一些元素时的扩容规则

先来看集合里面没有元素时的情况

private static void testAddAllGrowEmpty() {
    ArrayList<Integer> list = new ArrayList<>();
    list.addAll(List.of(1, 2, 3));
    //list.addAll(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11));
    System.out.println(length(list));//10
}

在这里插入图片描述

此时list的长度为10

那我们换一下,list里加11个元素呢,此时list的长度会是15吗

private static void testAddAllGrowEmpty() {
    ArrayList<Integer> list = new ArrayList<>();
    list.addAll(List.of(1, 2, 3));
    //list.addAll(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11));
    System.out.println(length(list));//11
}

在这里插入图片描述

list的长度其实是11,为什么呢?因为addAll扩容规则是下次扩容的容量跟实际的元素个数取一个较大值 作为下次扩容的容量

我们再来看一下集合里面有元素的情况

private static void testAddAllGrowNotEmpty() {
    ArrayList<Integer> list = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        list.add(i);
    }
    list.addAll(List.of(1, 2, 3));
    //list.addAll(List.of(1, 2, 3, 4, 5, 6));
    System.out.println(length(list));//15
}

在这里插入图片描述

private static void testAddAllGrowNotEmpty() {
    ArrayList<Integer> list = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        list.add(i);
    }
    //list.addAll(List.of(1, 2, 3));
    list.addAll(List.of(1, 2, 3, 4, 5, 6));
    System.out.println(length(list));//16
}

在这里插入图片描述

补充

通过反射机制获取到当前集合的长度

public static int length(ArrayList<Integer> list) {
    try {
        Field field = ArrayList.class.getDeclaredField("elementData");
        field.setAccessible(true);
        return ((Object[]) field.get(list)).length;
    } catch (Exception e) {
        e.printStackTrace();
        return 0;
    }
}

注意,示例中用反射方式来更直观地反映 ArrayList 的扩容特征,但从 JDK 9 由于模块化的影响,对反射做了较多限制,需要在运行测试代码时添加 VM 参数 --add-opens java.base/java.util=ALL-UNNAMED 方能运行通过,后面的例子都有相同问题

8、Iterator

要求

  • 掌握什么是 Fail-Fast、什么是 Fail-Safe

Fail-Fast 与 Fail-Safe

  • ArrayList 是 fail-fast 的典型代表,遍历的同时不能修改,尽快失败

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

源码分析

看下ArrayList的源码,增强for循环会调用迭代器对象

在这里插入图片描述

  • expectedModCount是迭代器的成员变量,记录了迭代器在刚开始迭代时的修改次数

  • modCount是List集合的成员变量,记录list 集合被修改了多少次

继续往下看有个next 方法

在这里插入图片描述

执行该方法时先执行checkForComodification(); 点进去看看

在这里插入图片描述

  • 它会做个判断,如果modCount != expectedModCount,会抛出并发修改异常。

接下来看看CopyOnWriteArrayList 的源码

在这里插入图片描述

进入COWIterator

在这里插入图片描述

  • 可以看到,es 记录了当前遍历的数组

再看一下add方法,点进源码

在这里插入图片描述

  • 可以看到添加是一个数组,遍历是另一个数组,是有两个数组的,读写分离

9、LinkedList

LinkedList

  1. 基于双向链表,无需连续内存
  2. 随机访问慢(要沿着链表遍历)
  3. 头尾插入元素删除性能高,中间插入元素的性能还不如ArrayList
  4. 占用内存多,相当于ArrayList的五倍多

ArrayList

  1. 基于数组,需要连续内存
  2. 随机访问快(指根据下标访问)
  3. 尾部插入、删除性能可以,其它部分插入、删除都会移动数据,因此性能会低
  4. 可以利用 cpu 缓存,局部性原理

CPU缓存:第一次读取数据的时候将数据存到缓存中,之后再去读取数据就不会内存中读取了,直接从缓存中读取,效率更高

局部性原理:读取数据的时候,相邻的数据有很大的几率被访问到,因此读取的时候一次性的全都读到缓存中

10、HashMap

10.1、基本数据结构

底层数据结构,1.7和1.8有什么不同?

  • 1.7 数组 + 链表 链表过长会影响性能
  • 1.8 数组 + (链表 | 红黑树)当链表中的数据过多时会转换成红黑树,反之亦然

hashmap何时会扩容呢?

  • 当元素的个数超过容量的四分之三时,HashMap会扩容

10.2、树化与退化

为何要用红黑树,为何一上来不树化,数化阈值为何是8,何时会树化,何时会退化为链表?

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

10.3、索引计算

索引如何计算,hashCode都有了,为何还要提供hash方法,数组容量为何是2的n次幂?

  1. 首先,计算对象的 hashCode(),再调用 HashMap 的 hash() 方法进行二次哈希,最后二次哈希值跟数组容量模运算,就得到桶下标,也就是索引

    等价于二次哈希值 & (capacity – 1) 得到索引;等价运算前提,除数必须是2的n次幂

  2. 二次 hash() 是为了综合高位数据,让哈希分布更为均匀,不会造成链表过长的情况

  3. 计算索引时,如果是2的n次幂可以使用位与运算代替取模,效率更高;扩容时重新计算索引效率更高:二次 hash值 & 原始容量 == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap(批量移动)。

  4. 但前三点都是为了配合容量为2的n次幂时的优化手段,例如HashTable的容量就不是2的n次幂,设计者综合考虑了各种因素,最终选择了2的n次幂作为容量。

注意:

  • 二次 hash 是为了配合 容量是 2 的 n 次幂这一设计前提,如果 hash 表的容量不是 2 的 n 次幂,则不必二次 hash
  • 容量是 2 的 n 次幂 这一设计计算索引效率更好,但 hash 的分散性就不好,需要二次 hash 来作为补偿,没有采用这一设计的典型例子是 Hashtable

10.4、put 与扩容

put 流程

  1. HashMap 是懒惰创建数组的,首次使用才创建数组
  2. 计算索引(桶下标)
  3. 如果桶下标还没人占用,创建 Node 占位返回
  4. 如果桶下标已经有人占用
    1. 已经是 TreeNode 走红黑树的添加或更新逻辑
    2. 是普通 Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑
  5. 返回前检查容量是否超过阈值,一旦超过进行扩容
  6. 扩容时,先把元素加到旧的数组里,然后扩容,再把就数组中的数据迁移到新数组

1.7 与 1.8 的区别

  1. 链表插入节点时,1.7 是头插法,1.8 是尾插法

  2. 1.7 是大于等于阈值且没有空位时才扩容,而 1.8 是大于阈值就扩容

  3. 1.8 在扩容计算 Node 索引时,会优化

扩容(加载)因子为何默认是 0.75f

  1. 在空间占用与查询时间之间取得较好的权衡
  2. 大于这个值,空间节省了,但链表就会比较长影响性能
  3. 小于这个值,冲突减少了,但扩容就会更频繁,空间占用也更多

10.5、并发问题

多线程下会有啥问题?

扩容死链(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;
        }
    }
}
  • e 和 next 都是局部变量,用来指向当前节点和下一个节点
  • 线程1(绿色)的临时变量 e 和 next 刚引用了这俩节点,还未来得及移动节点,发生了线程切换,由线程2(蓝色)完成扩容和迁移

在这里插入图片描述

  • 线程2 扩容完成,由于头插法,链表顺序颠倒。但线程1 的临时变量 e 和 next 还引用了这俩节点,还要再来一遍迁移

在这里插入图片描述

  • 第一次循环
    • 循环接着线程切换前运行,注意此时 e 指向的是节点 a,next 指向的是节点 b
    • e 头插 a 节点,注意图中画了两份 a 节点,但事实上只有一个(为了不让箭头特别乱画了两份)
    • 当循环结束是 e 会指向 next 也就是 b 节点

在这里插入图片描述

  • 第二次循环
    • next 指向了节点 a
    • e 头插节点 b
    • 当循环结束时,e 指向 next 也就是节点 a

在这里插入图片描述

  • 第三次循环
    • next 指向了 null
    • e 头插节点 a,a 的 next 指向了 b(之前 a.next 一直是 null),b 的 next 指向 a,死链已成
    • 当循环结束时,e 指向 next 也就是 null,因此第四次循环时会正常退出

在这里插入图片描述

数据错乱(1.7,1.8 都会存在)

key能否为null,作为key的对象有什么要求?

  1. HashMap 的 key 可以为 null,但 Map 的其他实现则不可以
  2. 作为 key 的对象,必须实现 hashCode 和 equals,并且 key 的内容不能修改(不可变)
    • 重写hashcode是为了key在整个hashmap中有良好的分布性,提高查询性能
    • 重写equals是为了防止万一两个key计算出来的hashcode一样,通过equals比较看看是否是相同的对象
  3. key 的 hashCode 应该有良好的散列性

String对象的hashCode() 如何设计的,为啥每次乘的都是31?

  • 目标是达到较为均匀的散列效果,每个字符串的 hashCode 足够独特
  • 字符串中的每个字符都可以表现为一个数字,称为 S i S_i Si,其中 i 的范围是 0 ~ n - 1
  • 散列公式为: S_0∗31^{(n-1)}+ S_1∗31^{(n-2)}+ … S_i ∗ 31^{(n-1-i)}+ …S_{(n-1)}∗31^0
  • 31 代入公式有较好的散列特性,并且 31 * h 可以被优化为
    • 即 32 ∗h -h
    • 即 2^5 ∗h -h
    • 即 h≪5 -h

11、单例模式

1、什么是单例模式

确保一个类只有一个实例,并提供该实例的全局访问点。

单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都调用这个单例对象,防止了频繁地创建对象使得内存飙升。

2、单例模式的类型

  • 懒汉式:在使用对象时才去创建该单例对象
  • 饿汉式:在类加载时已经创建好该单例对象

3、单例模式的特点

  1. 单例类只能有一个实例
  2. 单例类必须自己创建自己的唯一实例
  3. 单例类必须给所有其他对象提供这个实例

3、单例模式的五种实现方式

饿汉式

class Singleton1 implements Serializable {
    //无参构造私有,对象只能在本类被创建
    private Singleton1() {
        //防止反射破坏单例
        if (INSTANCE != null) {
            throw new RuntimeException("单例对象不能重复创建");
        }
        System.out.println("Singleton1()");
    }
    //静态成员变量
    //静态变量的赋值是在静态代码块中执行的,由jvm保证其线程安全
    private static final Singleton1 INSTANCE = new Singleton1();

    //公共的静态方法,供外部调用
    public static Singleton1 getInstance() {
        return INSTANCE;
    }
    
	//防止反序列化破坏单例
    public Object readResolve() {
        return INSTANCE;
    }
    
    //其他方法
    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

public class AppTest {
    public static void main(String[] args) {
        Singleton1.otherMethod();
        System.out.println("--------------------");
        System.out.println(Singleton1.getInstance());
        System.out.println(Singleton1.getInstance());
    }
    /**
     * Singleton1()
     * otherMethod()
     * --------------------
     * com.hh.demo.designpattern.e.Singleton1@135fbaa4
     * com.hh.demo.designpattern.e.Singleton1@135fbaa4
     */
}
  • 构造方法抛出异常是防止反射破坏单例
  • readResolve() 是防止反序列化破坏单例

枚举饿汉式(重要)

package com.hh.demo.designpattern.e;

enum Singleton2 {

    INSTANCE;
    
    //公共的静态方法
    public static Singleton2 getInstance() {
        return INSTANCE;
    }

    @Override
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
	//其他方法
    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}
public class AppTest {
    public static void main(String[] args) {
        Singleton2.otherMethod();
        System.out.println("--------------------");
        System.out.println(Singleton2.getInstance());
        System.out.println(Singleton2.getInstance());
    }
    /**
     * Singleton2()
     * otherMethod()
     * -------------------------
     * com.hh.demo.designpattern.e.Singleton2@135fbaa4
     * com.hh.demo.designpattern.e.Singleton2@135fbaa4
     *
     * 进程已结束,退出代码0
     */
}

  • 枚举饿汉式能天然防止反射、反序列化破坏单例

懒汉式

package com.hh.demo.designpattern.e;

class Singleton3 implements Serializable {
    //构造方法私有
    private Singleton3() {
        System.out.println("Singleton3()");
    }
    //私有的静态变量
    private static Singleton3 INSTANCE = null;

	//公共的静态方法,加锁保证线程安全
    public static synchronized Singleton3 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }
	//其他方法
    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}
public class AppTest {
    public static void main(String[] args) {
        Singleton3.otherMethod();
        System.out.println("--------------------");
        System.out.println(Singleton3.getInstance());
        System.out.println(Singleton3.getInstance());
    }
    /**
     * otherMethod()
     * --------------------
     * Singleton3()
     * com.hh.demo.designpattern.e.Singleton3@135fbaa4
     * com.hh.demo.designpattern.e.Singleton3@135fbaa4
     *
     * 进程已结束,退出代码0
     */
}

上面代码每次去获取对象都需要先获取锁,并发性能非常差;

其实只有首次创建单例对象时才需要同步,但该代码实际上每次调用都会同步;

接下来要做的就是优化性能,如果没有实例化对象则加锁创建,如果已经实例化了,则不需要加锁,直接获取实例

因此有了下面的双检锁改进

双检锁懒汉式

package com.hh.demo.designpattern.e;

class Singleton4 implements Serializable {
    //无参构造私有
    private Singleton4() {
        System.out.println("private Singleton4()");
    }
	//volatile保证了变量的可见性,有序性,禁止指令重排
    private static volatile Singleton4 INSTANCE = null; 

    public static Singleton4 getInstance() {
        if (INSTANCE == null) {// 线程A和线程B同时看到 INSTANCE = null,如果不为null,则直接返回 INSTANCE
            synchronized (Singleton4.class) {//线程A或线程B获得该锁进行初始化
                if (INSTANCE == null) {// 其中一个线程进入该分支,另外一个线程则不会进入该分支
                    INSTANCE = new Singleton4();
                }
            }
        }
        return INSTANCE;
    }
	//其他方法
    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}
public class AppTest {
    public static void main(String[] args) {
        Singleton4.otherMethod();
        System.out.println("--------------------");
        System.out.println(Singleton4.getInstance());
        System.out.println(Singleton4.getInstance());
    }
    /**
     * otherMethod()
     * --------------------
     * private Singleton4()
     * com.hh.demo.designpattern.e.Singleton4@135fbaa4
     * com.hh.demo.designpattern.e.Singleton4@135fbaa4
     *
     * 进程已结束,退出代码0
     */
}

为何必须加 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()");
    }
}
  • 避免了双检锁的缺点
  • 既实现了线程安全,又避免了同步带来的性能影响。

JDK 中单例的体现

  • Runtime 体现了饿汉式单例
  • Console 体现了双检锁懒汉式单例
  • Collections 中的 EmptyNavigableSet 内部类懒汉式单例
  • ReverseComparator.REVERSE_ORDER 内部类懒汉式单例
  • Comparators.NaturalOrderComparator.INSTANCE 枚举饿汉式单例

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/565094.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【开放原子训练营(第三季)inBuilder低代码开发实验室学习心得】

今天要给大家介绍的项目是UBML 什么是UBML呢&#xff1f; UBML&#xff08;统一业务建模语言 Unified-Business-Modeling-Language&#xff09;是一种用于快速构建应用软件的低代码开发建模语言&#xff0c;是开放原子开源基金会&#xff08;OpenAtom Foundation&#xff09;…

数据结构与算法:树形查找

一.二叉排序树&#xff08;BST&#xff09; 1.个人理解 左子树结点值 < 根结点值 < 右子树结点值对二叉排序树进行中序遍历&#xff0c;可以得到一个递增的有序数列 2.二叉树查找 原理&#xff1a; 对于一个给定的二叉排序树&#xff0c;如果要查找一个节点&#xff0…

并发知识杂谈

在JAVA语言层面&#xff0c;怎么保证线程安全&#xff1f; 有序性&#xff1a;使用happens-before原则 可见性&#xff1a;可以使用 volatile 关键字来保证&#xff0c;不仅如此&#xff0c;volatile 还能起到禁止指令重排的作用&#xff1b;另外&#xff0c; synchronized 和…

进程和编码

一、python代码的运行方式 1.脚本式 2. 交互式 一般用于代码的测试 二、进制及相互之间的转换 1. 进制 2.进制之间相互转换 在python中&#xff0c;十进制是以整形的形式存在&#xff0c;其他进制是已字符串的形式存在。 二进制/八进制/十六进制都可与十进制相互转换。但…

走向编程大师之路的几个里程碑

走向编程大师之路的几个里程碑 1语言关 2算法关 3系统关 4 编译器关 如下的系统的核心代码都有一万行以上&#xff0c;是规模和复杂度足够 大&#xff0c;可以检验开发者的模块化编程能力&#xff0c;掌控复杂度的能力。 使用什么编程语言本身是不重要的&#xff0c;能够有能…

常用消息中间件简介

一、 分布式系统消息通信技术简介 分布式系统消息通信技术主要包括以下几种&#xff1a; 1. RPC(Remote Procedure Call Protocol). 一般是C/S方式&#xff0c;同步的&#xff0c;跨语言跨平台&#xff0c;面向过程 2. CORBA(Common Object Request Broker Architecture). CO…

一个命令搞定Linux大文件下载

问题 Linux下log日志太大了&#xff0c;下载太慢了&#xff0c;即使下载下来&#xff0c;打开也费劲&#xff0c;咋办&#xff1f;将某文件夹打包成xx.tar.gz包&#xff0c;但依然很大&#xff0c;公司无法下载这么大的压缩包&#xff0c;咋办&#xff1f; split 以上2个问题…

[golang gin框架] 37.ElasticSearch 全文搜索引擎的使用

一.全文搜索引擎 ElasticSearch 的介绍&#xff0c;以 及安装配置前的准备工作 介绍 ElasticSearch 是一个基于 Lucene 的 搜索服务器,它提供了一个 分布式多用户能力的 全文搜索引擎&#xff0c;基于 RESTful web 接口,Elasticsearch 是用 Java 开发的&#xff0c;并作为 Apac…

PIC18F26单片机波特率配置

只需要配置以下三个寄存器&#xff1a; BRGCON1 BRGCON2 BRGCON3 BRGCON10x07; > 0000 0111 BRGCON20x90; > 1001 0000 BRGCON30x42; > 0101 0010 BRGCON1&#xff1a; Sync_Sog (bit7~bit6)1TQ,BRP(bit5~bit0)1 &#xff0c;则TQ((2*(BRP1))/Fosc16/32M&am…

Mysql存储时间,对应Api及对应的java属性

1.Mysql存储时间的类型 常用的储存时间/日期的类型&#xff1a; DATE&#xff1a;仅用于存储日期值&#xff08;年、月、日&#xff09;&#xff0c;格式为YYYY-MM-DD。TIME&#xff1a;仅用于存储时间值&#xff08;小时、分钟、秒&#xff09;&#xff0c;格式为HH:MM:SS。DA…

朴素贝叶斯算法实现英文文本分类

目录 1. 作者介绍2. 朴素贝叶斯算法简介及案例2.1朴素贝叶斯算法简介2.2文本分类器2.3对新闻文本进行文本分类 3. Python 代码实现3.1文本分类器3.2 新闻文本分类 参考&#xff08;可供参考的链接和引用文献&#xff09; 1. 作者介绍 梁有成&#xff0c;男&#xff0c;西安工程…

【UE】连续射击Niagara特效

效果 步骤 1. 新建一个粒子系统 选择“来自所选发射器的新系统” 添加“Fountain” 2. 打开这个新建的粒子系统 选中“Initialize Particle”模块&#xff0c;将颜色设置为&#xff08;100,0,0&#xff09; 再让生成的粒子大一些 选中“Spawn Rate”模块&#xff0c;将粒子的…

如何编写接口自动化框架系列之unittest测试框架的详解(二)

在编写自动化框架过程中 &#xff0c;我们首先想到的就是选择一个合适的测试框架 &#xff0c;目前常用的测试框架有unittest和pytest , unittest比较简单&#xff0c;适合入门着学习 &#xff1b;而pytest比较强大&#xff0c;适合后期进阶 。本文主要介绍的就是unittest框架 …

pytorch笔记(十)Batch Normalization

环境 python 3.9numpy 1.24.1pytorch 2.0.0+cu117一、Batch Normalize 作用 加快收敛、提升精度:对输入进行归一化,从而使得优化更加容易减少过拟合:可以减少方差的偏移可以使得神经网络使用更高的学习率:BN 使得神经网络更加稳定,从而可以使用更大的学习率,加速训练过程…

Chapter5: SpringBoot与Web开发2

接上一篇 Chapter4: SpringBoot与Web开发1 10. 配置嵌入式Servlet容器 SpringBoot默认采用Tomcat作为嵌入的Servlet容器&#xff1b;查看pom.xml的Diagram依赖图&#xff1a; 那么如何定制和修改Servlet容器的相关配置? 下面给出实操方案。 10.1 application.properties配…

依赖范围和编译classpath、测试classpath、运行classpath的关系

最近学习maven&#xff0c;这里看了下别人解释的区别原文&#xff0c;机翻一下&#xff0c;看的懵懵懂懂的 这其实应该是一个简单的区别&#xff0c;但我一直在Stackoverflow上回答一连串类似的问题&#xff0c;而人们往往会误解这个问题。 那么&#xff0c;什么是classpath&am…

[CF复盘] Codeforces Round 874 (Div. 3) 20230520】

[CF复盘] Codeforces Round 874 (Div. 3 20230520 总结A. Musical Puzzle![在这里插入图片描述](https://img-blog.csdnimg.cn/01ab8d835b4343659e8b80680dd9d639.png)2. 思路分析3. 代码实现 B. Restore the Weather1. 题目描述2. 思路分析3. 代码实现 C. Vlad Building Beaut…

FinClip | 2023 年 4 月产品大事记

我们的使命是使您&#xff08;业务专家和开发人员&#xff09;能够通过小程序解决您的关键业务流程挑战。不妨让我们看看在本月的产品与市场发布亮点&#xff0c;看看它们如何帮助您实现目标。 产品方面的相关动向&#x1f447;&#x1f447;&#x1f447; 全新版本的小程序统…

知识图谱实战应用12-食谱领域智能问答系统,实现菜谱问答

大家好,我是微学AI,今天给大家介绍一下知识图谱实战应用12-食谱领域智能问答系统,实现菜谱问答,本项目基于py2neo和neo4j图数据库,将知识图谱应用于菜谱领域。通过构建菜谱知识图谱,实现简单的菜谱食材问答系统。用户可以通过问答系统,快速获取简单的菜谱食材信息。 一…

Vivado综合属性系列之十一 GATED_CLOCK

目录 一、前言 二、GATED_CLOCK 2.1 属性说明 2.2 工程代码 2.3 综合结果 一、前言 在工程设计中&#xff0c;时钟信号通常来源于专用的时钟单元&#xff0c;如MMCM和PLL等。但也存在来自逻辑单元的信号作为时钟&#xff0c;这种时钟信号为门控时钟。门控时钟可以降低时…