《Java 高级篇》五:数据结构

news/2024/10/2 22:54:13

Author: ACatSmiling

Since: 2024-10-01

概述

在 Java 语言中,数组(Array)和集合都是对多个数据进行存储操作的结构,简称Java 容器。此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储。

数组在内存存储方面的特点:

  • 数组一旦初始化以后,其长度就确定了。
  • 数组一旦定义好,其元素的类型也就确定了。

数组在存储数据方面的弊端:

  • 数组一旦初始化以后,其长度就不可修改,不便于扩展。
  • 数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高。
  • 数组中没有现成的属性和方法,去直接获取数组中已存储的元素的个数(只能直接知道数组的长度)。
  • 数组存储的数据是有序的、可重复的。对于无序、不可重复的需求,不能满足,即数组存储数据的特点比较单一。

Java 集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组。

Java 集合框架可分为CollectionMap两种体系:

  • Collection 接口 :单列集合,用来存储一个一个的对象。
    • List 接口:存储有序的、可重复的数据。包括:ArrayList、LinkedList、Vector。
    • Set 接口:存储无序的、不可重复的数据。包括:HashSet、LinkedHashSet、TreeSet。
  • Map 接口:双列集合,用来存储具有映射关系 "key - value 对" 的数据。包括:HashMap、LinkedHashMap、TreeMap、Hashtable、Properties。

数组

**数组(Array)**,是多个相同类型的数据按一定顺序排列的集合,使用一个名字命名,并通过编号的方式对这些数据进行统一管理。

数组的相关概念:

  • 数组名
  • 元素
  • 下标/索引
  • 数组的长度

数组的特点:

  • 数组是有序排列的。
  • 创建数组对象会在内存中开辟一整块连续的空间,而数组名中引用的是这块连续空间的首地址。
  • 数组本身是引用数据类型的变量,但数组中的元素可以是任何数据类型,既可以是基本数据类型,也可以是引用数据类型。
  • 可以直接通过下标/索引的方式调用指定位置的元素,速度很快。
  • 数组的长度一旦确定,就不能修改。

数组的分类:

  • 按照维度:一维数组、二维数组、三维数组、…
  • 按照元素的数据类型分:基本数据类型元素的数组、引用数据类型元素的数组(即对象数组)。

一维数组

声明方式:type var[] 或 type[] var;。例如:

int a[];
int[] a1;
double b[];
String[] c;// 引用类型变量数组

不同写法:int[] x;int x[];

Java 语言中声明数组时,不能指定其长度(数组中元素的数), 例如:int a[5];// 非法

public static void main(String[] args) {// 1-1 静态初始化,方式一int[] ids = new int[]{1001, 1002, 1003, 1004, 1005};// 1-2 静态初始化,方式二,类型推断int[] ids2 = {1001, 1002, 1003, 1004, 1005};// 2 动态初始化String[] names = new String[5];names[0] = "Student A";names[1] = "Student B";names[2] = "Student C";names[3] = "Student D";names[4] = "Student E";// 3 数组的长度System.out.println("ids 的长度:" + ids.length);// 5System.out.println("names 的长度:" + names.length);// 5// 4 遍历数组for (int i = 0; i < ids.length; i++) {System.out.println(ids[i]);}for (int i = 0; i < names.length; i++) {System.out.println(names[i]);}// 5 简写方式遍历数组for (int id : ids) {System.out.println(id);}for (String name : names) {System.out.println(name);}// 6 数组元素的默认初始化值int[] arrs = new int[5];for (int arr : arrs) {System.out.println(arr);// 0}String[] arrs2 = new String[5];for (String arr2 : arrs2) {System.out.println(arr2);// null}
}
  • 静态初始化:数组的初始化,和数组元素的赋值操作同时进行,如:"int[] ids = new int[]{1001, 1002, 1003, 1004, 1005};"。

  • 动态初始化 :数组的初始化,和数组元素的赋值操作分开进行,如:"String[] names = new String[5]; names[1] = "a";"。

  • 定义数组并用运算符 new 为之分配空间后,才可以引用数组中的每个元素。

  • 数组元素的引用方式:数组名[数组元素下标]

  • 数组元素下标从 0 开始,长度为 n 的数组的合法下标取值范围:0 ~ n - 1。如:int a[] = new int[3];,则可引用的数组元素为 a[0]、a[1] 和 a[2]。

  • 数组元素下标可以是整型常量或整型表达式。如 a[3],b[i],c[6*i]。

  • 数组一旦初始化完成,其长度也随即确定,且长度不可变。每个数组都有一个属性 length 指明它的长度,例如:a.length 指明数组 a 的长度(元素个数)。

  • 数组是引用类型,它的元素相当于类的成员变量,因此数组一经分配空间,其中的每个元素也被按照成员变量同样的方式被隐式初始化。然后,再根据实际代码设置,将数组相应位置的元素进行赋值,即显示赋值

    数组元素类型 元素默认初始值
    byte 0
    short 0
    int 0
    long 0L
    float 0.0F
    double 0.0
    char 0 或写为 '\u000' (表现为空)
    boolean false
    引用类型 null
    • 对于基本数据类型而言,默认的初始化值各有不同;对于引用数据类型而言,默认的初始化值为 null。
    • char 类型的默认值是 0,不是 '0',表现的是类似空格的一种效果。

二维数组

Java 语言里提供了支持多维数组的语法。如果把一维数组当成几何中的线性图形,那么二维数组就相当于是一个表格。

对于二维数组的理解,可以看成是一维数组 array1,作为另一个一维数组 array2 的元素而存在。其实,从数组底层的运行机制来看,没有多维数组。

  • 可以理解为,array2 是外层数组,array1 则是外层数组每一个位置上的值,即内层数组。

不同写法:int[][] x;int[] x[];int x[][];

public static void main(String[] args) {// 1-1 静态初始化,方式一int[][] arr = new int[][]{{3, 8, 2}, {2, 7}, {9, 0, 1, 6}};// 1-2 静态初始化,方式二,类型推断int[][] arr2 = {{3, 8, 2}, {2, 7}, {9, 0, 1, 6}};System.out.println(arr2[2][3]);// 2-1 动态初始化,方式一/*定义了名称为 arr3 的二维数组,二维数组中有 3 个一维数组,内层每一个一维数组中有 2 个元素内层一维数组的名称分别为 arr3[0],arr3[1],arr3[2],返回的是地址值给内层第一个一维数组 1 脚标位赋值为 78 写法是:arr3[0][1] = 78;*/int[][] arr3 = new int[3][2];arr3[0][1] = 78;System.out.println(arr3[0]);// [I@78308db1System.out.println(arr3[0][1]);// 78// 2-2 动态初始化,方式二/*二维数组 arr4 中有 3 个一维数组,内层每个一维数组都是默认初始化值 null(注意:区别于格式 2-1)可以对内层三个一维数组分别进行初始化*/int[][] arr4 = new int[3][];// 初始化第一个arr4[0] = new int[3];// 初始化第二个arr4[1] = new int[1];// 初始化第三个arr4[2] = new int[2];// 3 特殊写法int[] x, y[];// x 是一维数组,y 是二维数组x = new int[3];y = new int[3][2];// 4 获取数组长度System.out.println("arr的长度:" + arr.length);// 3System.out.println("arr第一个元素的长度:" + arr[0].length);// 3// 5 遍历二维数组for (int i = 0; i < arr.length; i++) {for (int j = 0; j < arr[i].length; j++) {System.out.print(arr[i][j] + "\t");// 3	8 2	2 7	9 0	1 6}}System.out.println();// 6 简写遍历二维数组for (int[] valueArr : arr) {for (int value : valueArr) {System.out.print(value + "\t");// 3	8 2	2 7	9 0	1 6}}System.out.println();// 7 二维数组元素的默认初始化值int[][] arr5 = new int[3][2];System.out.println(arr5);// [[I@27c170f0System.out.println(arr5[1]);// [I@5451c3a8System.out.println(arr5[1][1]);// 0String[][] arr6 = new String[3][2];System.out.println(arr6);// [[Ljava.lang.String;@2626b418System.out.println(arr6[1]);// [Ljava.lang.String;@5a07e868System.out.println(arr6[1][1]);// nullString[][] arr7 = new String[3][];System.out.println(arr7[1]);// null,因为内层数组未初始化System.out.println(arr7[1][1]);// NullPointerException
}
  • 动态初始化方式一,初始化时直接规定了内层一维数组的长度,动态初始化方式二,可以在使用过程中根据需要另行初始化内层一维数组的长度。利用动态初始化方式二时,必须要先初始化内层一维数组才能对其使用,否则报空指针异常。
  • int[][] arr = new int[][3];的方式是非法的。
  • 注意特殊写法情况:int[] x,y[];// x是一维数组,y是二维数组
  • Java 中多维数组不必都是规则矩阵形式。
  • 数组元素的默认初始化值:针对形如int[][] arr = new int[4][3];的初始化方式,外层元素的初始化值为地址值,内层元素的初始化值与一维数组初始化情况相同;针对形如int[][] arr = new int[4][];的初始化方式,外层元素的初始化值为 null,内层元素没有初始化,不能调用。

数组应用

杨辉三角

杨辉三角:

image-20210219101905712

提示:

  1. 第一行有 1 个元素,第 n 行有 n 个元素。
  2. 每一行的第一个元素和最后一个元素都是 1。
  3. 从第三行开始,对于非第一个元素和最后一个元素的元素,有:yanghui[i][j] = yanghui[i-1][j-1] + yanghui[i-1][j];

实现:

public static void main(String[] args) {// 1 声明二维数组并初始化int[][] arrs = new int[10][];for (int i = 0; i < arrs.length; i++) {System.out.print("[" + i + "]\t");// 2 初始化内层数组,并给内层数组的首末元素赋值arrs[i] = new int[i + 1];arrs[i][0] = 1;arrs[i][arrs[i].length - 1] = 1;for (int j = 0; j < arrs[i].length; j++) {// 3 给从第三行开始内层数组的非首末元素赋值if (i >= 2 && j > 0 && j < arrs[i].length - 1) {arrs[i][j] = arrs[i - 1][j - 1] + arrs[i - 1][j];}System.out.print(arrs[i][j] + "\t");}System.out.println();}System.out.print("\t");for (int i = 0; i < arrs.length; i++) {System.out.print("[" + i + "]\t");}
}

回形数

回形数,从键盘输入一个 1 - 20 的整数,然后以该数字为矩阵的大小,把 1,2,3 … n*n 的数字按照顺时针螺旋的形式填入其中。例如:

  • 输入数字 2,则程序输出:

    image-20210219104822490

  • 输入数字 3,则程序输出:

    image-20210219104929770

  • 输入数字 4, 则程序输出:

    image-20210219105019369

实现方式一:

public static void main(String[] args) {Scanner scanner = new Scanner(System.in);System.out.println("输入一个数字");int len = scanner.nextInt();int[][] arr = new int[len][len];int s = len * len;/** k = 1:向右,k = 2:向下,k = 3:向左,k = 4:向上*/int k = 1;int i = 0, j = 0;for (int m = 1; m <= s; m++) {if (k == 1) {if (j < len && arr[i][j] == 0) {arr[i][j++] = m;} else {k = 2;i++;j--;m--;}} else if (k == 2) {if (i < len && arr[i][j] == 0) {arr[i++][j] = m;} else {k = 3;i--;j--;m--;}} else if (k == 3) {if (j >= 0 && arr[i][j] == 0) {arr[i][j--] = m;} else {k = 4;i--;j++;m--;}} else if (k == 4) {if (i >= 0 && arr[i][j] == 0) {arr[i--][j] = m;} else {k = 1;i++;j++;m--;}}}// 遍历数组for (int m = 0; m < arr.length; m++) {for (int n = 0; n < arr[m].length; n++) {System.out.print(arr[m][n] + "\t");}System.out.println();}
}

实现方式二:

public static void main(String[] args) {Scanner scanner = new Scanner(System.in);System.out.println("输入一个数字");int n = scanner.nextInt();int[][] arr = new int[n][n];int count = 0;// 要显示的数据int maxX = n - 1;// X 轴的最大下标int maxY = n - 1;// Y 轴的最大下标int minX = 0;// X 轴的最小下标int minY = 0;// Y 轴的最小下标while (minX <= maxX) {// 向右for (int x = minX; x <= maxX; x++) {arr[minY][x] = ++count;}minY++;// 向下for (int y = minY; y <= maxY; y++) {arr[y][maxX] = ++count;}maxX--;// 向左for (int x = maxX; x >= minX; x--) {arr[maxY][x] = ++count;}maxY--;// 向上for (int y = maxY; y >= minY; y--) {arr[y][minX] = ++count;}minX++;}// 遍历数组for (int i = 0; i < arr.length; i++) {for (int j = 0; j < arr.length; j++) {String space = (arr[i][j] + "").length() == 1 ? "0" : "";System.out.print(space + arr[i][j] + " ");}System.out.println();}
}

元素统计

定义一个 int 型的一维数组,包含 10 个元素,分别赋一些随机整数,然后求出所有元素的最大值,最小值,和值,平均值,并输出出来。要求:所有随机数都是两位数。

public static void main(String[] args) {// 初始化及赋值int[] arr = new int[10];int length = arr.length;for (int i = 0; i < length; i++) {int value = (int) (Math.random() * 90 + 10);arr[i] = value;}// 遍历for (int value : arr) {System.out.print(value + "\t");}System.out.println();// 计算int max = arr[0];int min = arr[0];int sum = 0;double average = 0;for (int value : arr) {max = max < value ? value : max;min = min > value ? value : min;sum += value;}average = sum / (length * 1.0);System.out.println("最大值:" + max);System.out.println("最小值:" + min);System.out.println("和值:" + sum);System.out.println("平均值:" + average);
}

数组的复制

虚假的复制,没有创建新对象:

public static void main(String[] args) {// 声明 arr1 和 arr2int[] arr1, arr2;arr1 = new int[]{2, 3, 5, 7, 11, 13, 17, 19};// 遍历 arr1for (int value : arr1) {System.out.print(value + "\t");}System.out.println();// 赋值 arr2 变量等于 arr1// 不能称作数组的复制,实际上是把 arr1 指向的地址(以及其他一些信息)赋给了 arr2,堆空间中只有一个数组对象arr2 = arr1;// 遍历 arr2for (int value : arr2) {System.out.print(value + "\t");}System.out.println();// 更改 arr2for (int i = 0; i < arr1.length; i++) {if (i % 2 == 0) {arr2[i] = i;continue;}arr2[i] = arr1[i];}// 遍历 arr2for (int value : arr2) {System.out.print(value + "\t");}System.out.println();// 遍历 arr1for (int value : arr1) {System.out.print(value + "\t");}System.out.println();
}
输出结果:
2	3	5	7	11	13	17	19	
2	3	5	7	11	13	17	19	
0	3	2	7	4	13	6	19	
0	3	2	7	4	13	6	19

真实的复制,创建了新对象:

public static void main(String[] args) {// 声明 arr1 和 arr2int[] arr1, arr2;arr1 = new int[]{2, 3, 5, 7, 11, 13, 17, 19};// 遍历 arr1for (int value : arr1) {System.out.print(value + "\t");}System.out.println();// 数组的复制arr2 = new int[arr1.length];for (int i = 0; i < arr1.length; i++) {arr2[i] = arr1[i];}// 遍历 arr2for (int value : arr2) {System.out.print(value + "\t");}System.out.println();// 更改 arr2for (int i = 0; i < arr1.length; i++) {if (i % 2 == 0) {arr2[i] = i;continue;}arr2[i] = arr1[i];}// 遍历 arr2for (int value : arr2) {System.out.print(value + "\t");}System.out.println();// 遍历 arr1for (int value : arr1) {System.out.print(value + "\t");}System.out.println();
}
输出结果:
2	3	5	7	11	13	17	19	
2	3	5	7	11	13	17	19	
0	3	2	7	4	13	6	19	
2	3	5	7	11	13	17	19

数组反转

public static void main(String[] args) {String[] arr = {"A", "B", "C", "D", "E", "F", "G"};// 遍历arrfor (String value : arr) {System.out.print(value + "\t");}System.out.println();// 反转,方式一for (int i = 0; i < arr.length / 2; i++) {String temp = arr[i];arr[i] = arr[arr.length - 1 - i];arr[arr.length - 1 - i] = temp;}for (String value : arr) {System.out.print(value + "\t");}System.out.println();// 反转,方式二for (int i = 0, j = arr.length - 1; i < j; i++, j--) {String temp = arr[i];arr[i] = arr[j];arr[j] = temp;}for (String value : arr) {System.out.print(value + "\t");}System.out.println();
}

数组查找

线性查找
public static void main(String[] args) {String[] arr = {"A", "B", "C", "D", "E", "F", "G"};// 遍历 arrfor (String value : arr) {System.out.print(value + "\t");}System.out.println();String dest = "D";boolean isFlag = true;for (int i = 0; i < arr.length; i++) {if (dest.equals(arr[i])) {System.out.println("找到了指定的元素:" + dest + ",位置为:" + i);isFlag = false;break;}}if (isFlag) {System.out.println("没找到指定的元素:" + dest);}
}
输出结果:
A	B	C	D	E	F	G	
找到了指定的元素:D,位置为:3
二分法查找

前提:所要查找的数组必须有序。

image-20210219153157857

public static void main(String[] args) {int[] arr = {2, 5, 7, 8, 10, 15, 18, 20, 22, 25, 28};// 遍历 arrfor (int value : arr) {System.out.print(value + "\t");}System.out.println();int dest = 10;// 初始的首索引int head = 0;// 初始的末索引int end = arr.length - 1;boolean isFlag = true;while (head <= end) {int middle = (head + end) / 2;if (dest == arr[middle]) {System.out.println("找到了指定的元素:" + dest + ",位置为:" + middle);isFlag = false;break;} else if (dest < arr[middle]) {end = middle - 1;} else {// dest2 > arr2[middle]head = middle + 1;}}if (isFlag) {System.out.println("没找到指定的元素:" + dest);}
}
输出结果:
2 5	7 8	10 15 18 20	22 25 28	
找到了指定的元素:10,位置为:4

数组排序

排序:假设含有 n 个记录的序列为 {R1, R2, ..., Rn},其相应的关键字序列为 {K1, K2, ..., Kn}。将这些记录重新排序为 {Ri1, Ri2, ..., Rin},使得相应的关键字值满足条 Ki1 <= Ki2 <= ... <= Kin,这样的一种操作称为排序。通常来说,排序的目的是快速查找。

衡量排序算法的优劣:

  1. 时间复杂度:分析关键字的比较次数和记录的移动次数。

  2. 空间复杂度:分析排序算法中需要多少辅助内存。

  3. 稳定性:若两个记录 A 和 B 的关键字值相等,但排序后 A、B 的先后次序保持不变,则称这种排序算法是稳定的。

排序算法分类:内部排序和外部排序。

  • 内部排序:整个排序过程不需要借助于外部存储器(如磁盘等),所有排序操作都在内存中完成。

  • 外部排序:参与排序的数据非常多,数据量非常大,计算机无法把整个排序过程放在内存中完成,必须借助于外部存储器(如磁盘等)。外部排序最常见的是多路归并排序。可以认为外部排序是由多次内部排序组成。

十大内部排序算法:

  • 选择排序
    • 直接选择排序、堆排序
  • 交换排序
    • 冒泡排序、快速排序
  • 插入排序
    • 直接插入排序、折半插入排序、Shell 排序
  • 归并排序
  • 桶式排序
  • 基数排序

排序算法性能对比:

image-20210219203707336

  • 从平均时间而言:快速排序最佳。但在最坏情况下时间性能不如堆排序和归并排序。
  • 从算法简单性看:由于直接选择排序、直接插入排序和冒泡排序的算法比较简单,将其认为是简单算法。对于 Shell 排序、堆排序、快速排序和归并排序算法,其算法比较复杂,认为是复杂排序。
  • 从稳定性看:直接插入排序、冒泡排序和归并排序时稳定的;而直接选择排序、快速排序、 Shell 排序和堆排序是不稳定排序。
  • 从待排序的记录数 n 的大小看,n 较小时,宜采用简单排序;而 n 较大时宜采用改进排序。

排序算法的选择:

  • 若 n 较小(如 n ≤50),可采用直接插入或直接选择排序。当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插入,应选直接选择排序为宜。
  • 若文件初始状态基本有序(指正序),则应选用直接插入、 冒泡或随机的快速排序为宜。
  • 若 n 较大,则应采用时间复杂度为 O(nlgn) 的排序方法: 快速排序、 堆排序或归并排序。
冒泡排序

冒泡排序的原理非常简单,它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。

排序思想:

  1. 比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较为止。

实现:

public static void main(String[] args) {int[] arr = new int[]{43, 32, 76, -98, 0, 64, 33, -21, 32, 99};// 遍历 arrfor (int value : arr) {System.out.print(value + "\t");}System.out.println();// 冒泡排序for (int i = 0; i < arr.length - 1; i++) {// 先把最大的数移到数组最后一位,然后再找第二大的数,以此类推for (int j = 0; j < arr.length - 1 - i; j++) {if (arr[j] > arr[j + 1]) {int temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;}}}// 遍历 arrfor (int value : arr) {System.out.print(value + "\t");}System.out.println();
}
输出结果:
43	32	76	-98	0	64	33	-21	32	99
-98	-21	0	32	32	33	43	64	76	99
快速排序

快速排序通常明显比同为 O(nlogn) 的其他算法更快,因此常被采用,而且快速排序采用了分治法的思想,所以在很多笔试面试中能经常看到快速排序的影子,可见掌握快速排序的重要性。

快速排序(Quick Sort)由图灵奖获得者 Tony Hoare 发明,被列为 20 世纪十大算法之一,是迄今为止所有内排序算法中速度最快的一种。

快速排序属于冒泡排序的升级版,交换排序的一种。快速排序的时间复杂度为 O(nlog(n))。

排序思想:

  1. 从数列中挑出一个元素,称为 "基准"(pivot)。
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
  4. 递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

image-20210219173315424

image-20210219173351751

Arrays 工具类的使用

java.util.Arrays类为操作数组的工具类,包含了用来操作数组(比如排序和搜索)的各种方法。常用的方法有:

  • boolean equals(int[] a, int[] b):判断两个数组是否相等。
  • String toString(int[] a):遍历数组信息。
  • void fill(int[] a, int val):将指定值填充到数组之中。
  • void sort(int[] a):对数组进行排序,底层使用的是快速排序。
  • int binarySearch(int[] a, int key):对排序后的数组进行二分法检索指定的值。

示例:

public static void main(String[] args) {// 1 boolean equals(int[] a, int[] b):判断两个数组是否相等int[] arr1 = new int[]{1, 2, 3, 4};int[] arr2 = new int[]{1, 3, 2, 4};boolean isEquals = Arrays.equals(arr1, arr2);System.out.println("arr1和arr2是否相等:" + isEquals);// 2 String toString(int[] a):遍历数组信息System.out.println("arr1:" + Arrays.toString(arr1));// 3 void fill(int[] a, int val):将指定值填充到数组之中Arrays.fill(arr1, 10);System.out.println("arr1填充后:" + Arrays.toString(arr1));// 4 void sort(int[] a):对数组进行排序,底层使用的是快速排序System.out.println("arr2排序前:" + Arrays.toString(arr2));Arrays.sort(arr2);System.out.println("arr2排序后:" + Arrays.toString(arr2));// 5 int binarySearch(int[] a, int key):对排序后的数组进行二分法检索指定的值int[] arr3 = new int[]{-98, -34, 2, 34, 54, 66, 79, 105, 210, 333};int dest = 211;int index = Arrays.binarySearch(arr3, dest);if (index >= 0) {System.out.println(dest + "在数组中的位置为:" + index);} else {System.out.println(dest + "在数组中未找到:" + index);}
}
输出结果:
arr1和arr2是否相等:false
arr1:[1, 2, 3, 4]
arr1填充后:[10, 10, 10, 10]
arr2排序前:[1, 3, 2, 4]
arr2排序后:[1, 2, 3, 4]
211在数组中未找到:-10

数组中的异常

public static void main(String[] args) {// ArrayIndexOutOfBoundsExceptionint[] arr = new int[]{7, 10};System.out.println(arr[2]);// 数组脚标越界System.out.println(arr[-1]);// 访问了数组中不存在的脚标// NullPointerException:空指针异常,arr 引用没有指向实体,却被操作实体中的元素// 情形一int[] arr2 = null;System.out.println(arr2[0]);// 情形二int[][] arr3 = new int[4][];System.out.println(arr3[0]);// nullSystem.out.println(arr3[0][0]);// NullPointerException// 情形三String[] arr4 = new String[]{"AA", "BB", "CC"};arr4[0] = null;System.out.println(arr4[0].toString());// 对 null 调用了方法
}

ArrayIndexOutOfBoundsException 和 NullPointerException,在编译时,不报错!!

List 接口

List 集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。

ArrayList

基础属性:

public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{@java.io.Serialprivate static final long serialVersionUID = 8683452581122892189L;/*** Default initial capacity.*/private static final int DEFAULT_CAPACITY = 10;/*** Shared empty array instance used for empty instances.*/private static final Object[] EMPTY_ELEMENTDATA = {};/*** Shared empty array instance used for default sized empty instances. We* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when* first element is added.*/private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};/*** The array buffer into which the elements of the ArrayList are stored.* The capacity of the ArrayList is the length of this array buffer. Any* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA* will be expanded to DEFAULT_CAPACITY when the first element is added.*/transient Object[] elementData; // non-private to simplify nested class access/*** The size of the ArrayList (the number of elements it contains).** @serial*/private int size;
}

本文源码,均基于 JDK 17。

new ArrayList() 时,底层 Object[] 数组 elementData 初始化为 {},是一个长度为 0 的空数组

/*** Constructs an empty list with an initial capacity of ten.*/
public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

第一次调用 add() 方法,初始化底层 Object[] 数组 elementData 的长度为 10,并将元素添加到 elementData 中:

/*** Appends the specified element to the end of this list.** @param e element to be appended to this list* @return {@code true} (as specified by {@link Collection#add})*/
public boolean add(E e) {modCount++;add(e, elementData, size);return true;
}
/*** This helper method split out from add(E) to keep method* bytecode size under 35 (the -XX:MaxInlineSize default value),* which helps when add(E) is called in a C1-compiled loop.*/
private void add(E e, Object[] elementData, int s) {// 第一次执行 add() 方法,size 属性的值为 0,elementData.length 为 0if (s == elementData.length)elementData = grow();// 添加元素到数组中elementData[s] = e;size = s + 1;
}
private Object[] grow() {return grow(size + 1);
}
/*** Increases the capacity to ensure that it can hold at least the* number of elements specified by the minimum capacity argument.** @param minCapacity the desired minimum capacity* @throws OutOfMemoryError if minCapacity is less than zero*/
private Object[] grow(int minCapacity) {int oldCapacity = elementData.length;if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {int newCapacity = ArraysSupport.newLength(oldCapacity,minCapacity - oldCapacity, /* minimum growth */oldCapacity >> 1           /* preferred growth */);return elementData = Arrays.copyOf(elementData, newCapacity);} else {// 初始化数组,长度为 DEFAULT_CAPACITY,即 10return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];}
}

之后,每次执行 add() 方法,直接将元素添加到 elementData 对应的位置,直到第 11 次添加元素。此时,添加的元素的总数,已经超过了数组的长度,需要进行扩容操作

/*** This helper method split out from add(E) to keep method* bytecode size under 35 (the -XX:MaxInlineSize default value),* which helps when add(E) is called in a C1-compiled loop.*/
private void add(E e, Object[] elementData, int s) {// 第 11 次添加元素,此时,满足 s == elementData.length 条件if (s == elementData.length)elementData = grow();elementData[s] = e;size = s + 1;
}

默认情况下,数组长度扩容为原来容量的 1.5 倍,同时,将原有数组中的数据复制到新的数组中

/*** Increases the capacity to ensure that it can hold at least the* number of elements specified by the minimum capacity argument.** @param minCapacity the desired minimum capacity* @throws OutOfMemoryError if minCapacity is less than zero*/
private Object[] grow(int minCapacity) {int oldCapacity = elementData.length;// 满足 oldCapacity > 0 条件if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {// 扩容数组长度到原来的 1.5 倍int newCapacity = ArraysSupport.newLength(oldCapacity,minCapacity - oldCapacity, /* minimum growth */oldCapacity >> 1           /* preferred growth */);// 复制原数组数据到新数组return elementData = Arrays.copyOf(elementData, newCapacity);} else {return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];}
}

结论:

  1. ArrayList 是第一次添加元素时,才创建一个初始容量为 10 的数组,延迟了数组的创建。
  2. 添加数据时,如果底层的数组需要扩容,均扩容为原来容量的 1.5 倍,同时,将原有数组中的数据复制到新的数组中。
  3. 开发中使用 ArrayList 时,建议按需求在初始化时就指定 ArrayList 的容量,以尽可能的避免扩容。

LinkedList

双向链表,内部定义了内部类 Node,作为 LinkedList 中保存数据的基本结构。

LinkedList 内部没有声明数组,而是定义了 Node 类型的 first 和 last,用于记录首末元素:

image-20210322150052189

对于频繁的插入或删除元素的操作,建议使用 LinkedList 类,效率较高。

new LinkedList() 时,内部声明了 Node 类型的 first 和 last 属性,默认值为 null:

public class LinkedList<E>extends AbstractSequentialList<E>implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{transient int size = 0;/*** Pointer to first node.*/transient Node<E> first;/*** Pointer to last node.*/transient Node<E> last;/*** Constructs an empty list.*/public LinkedList() {}
}
// Node 内部类
private static class Node<E> {// 当前 Node 存储的数据E item;// 指向链表的后一个元素Node<E> next;// 指向链表的前一个元素Node<E> prev;Node(Node<E> prev, E element, Node<E> next) {this.item = element;this.next = next;this.prev = prev;}
}

调用 add() 方法添加元素:

/*** Appends the specified element to the end of this list.** <p>This method is equivalent to {@link #addLast}.** @param e element to be appended to this list* @return {@code true} (as specified by {@link Collection#add})*/
public boolean add(E e) {linkLast(e);return true;
}
/*** Links e as last element.*/
void linkLast(E e) {// last,原链表的最后一个对象final Node<E> l = last;// 创建一个新的 Node 对象final Node<E> newNode = new Node<>(l, e, null);// 当前新创建的 Node 对象,成为新链表的最后一个对象last = newNode;if (l == null)// 如果原链表为 null,则将当前新创建的 Node 对象指定为链表的第一个节点 firstfirst = newNode;else// 如果原链表不为 null,则将原链表的最后一个对象,指向当前新的 Node 对象l.next = newNode;size++;modCount++;
}

Set 接口

Set 集合存储无序的、不可重复的数据,如果把两个相同的元素加入同一个 Set 集合中,则添加操作失败。

  • 无序性:不等于随机性。以 HashSet 为例,存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的。
  • 不可重复性:保证添加的元素按照 equals() 判断时,不能返回 true。即:相同的元素只能添加一个。

Set 接口是 Collection 的子接口,Set 接口没有提供额外的方法,使用的都是Collection中声明过的方法。

Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals()。对于存放在 Set(主要指:HashSet、LinkedHashSet)容器中的对象,其对应的类一定要重写equals()hashCode(),以实现对象相等规则。

  • 要求:重写的 hashCode() 和 equals() 尽可能保持一致性,即:相等的对象必须具有相等的散列码
    • 如果不重写所添加元素所在类的 hashCode(),则会调用 Object 类的 hashCode(),该方法是产生一个随机数,因此,即使添加两个一样的元素,其 hashCode 值也可能不同,也就都能添加成功。
  • 重写两个方法的小技巧:对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。
  • TreeSet 比较两个元素是否相同的方法,不是 equals() 和 hashCode(),而是元素对应类的排序方法。

重写 hashCode() 方法的基本原则:

  • 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。
  • 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法的返回值也应相等。
  • 对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。

重写 equals() 方法的基本原则,以自定义的 Customer 类为例,何时需要重写 equals():

  • 如果一个类有自己特有的 "逻辑相等" 概念,当重写 equals() 的时候,总是需要重写 hashCode()。因为根据一个类改写后的 equals(),两个截然不同的实例有可能在逻辑上是相等的,但是,根据 Object 类的 hashCode(),它们仅仅是两个对象。这种情况,违反了 "相等的对象必须具有相等的散列码" 的原则。

结论:重写 equals() 的时候,一般都需要同时重写 hashCode() 方法。通常参与计算 hashCode 的对象的属性也应该参与到 equals() 中进行计算。

Eclipse/IDEA 工具里 hashCode() 的重写,为什么会有 31 这个数字:

@Override
public int hashCode() {
int result = name.hashCode();
result = 31 * result + age;
return result;
}
  • 选择系数的时候要选择尽量大的系数,因为如果计算出来的 hashCode 值越大,所谓的冲突就越少,查找起来效率也会提高。---> 减少冲突
  • 31 只占用 5 bits,相乘造成数据溢出的概率较小。
  • 31 可以由i * 31 == (i << 5) - 1来表示,现在很多虚拟机里面都有做相关优化。---> 提高算法效率
  • 31 是一个素数,素数作用就是如果用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有 1 来整除!---> 减少冲突

HashSet

HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。

HashSet 具有以下特点:

  • 不保证元素的排列顺序。
  • 不是线程安全的。
  • 集合元素可以是 null,但是只能有一个。

HashSet 的底层,使用的是 HashMap:

/*** Constructs a new, empty set; the backing {@code HashMap} instance has* default initial capacity (16) and load factor (0.75).*/
public HashSet() {map = new HashMap<>();
}

LinkedHashSet

LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。

  • 遍历 LinkedHashSet 内部数据时,可以按照添加的顺序遍历。

LinkedHashSet 插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。对于频繁的遍历操作,LinkedHashSet 效率高于 HashSet。

LinkedHashSet 的底层,使用的是 LinkedHashMap:

public LinkedHashSet() {super(16, .75f, true);
}
HashSet(int initialCapacity, float loadFactor, boolean dummy) {map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

Map 接口

HashMap

HashMap 源码中的重要常量:

  • DEFAULT_INITIAL_CAPACITY:HashMap 的默认容量,16

    /*** The default initial capacity - MUST be a power of two.*/
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    
  • MAXIMUM_CAPACITY:HashMap 的最大支持容量,$2^{30}$。

    /*** The maximum capacity, used if a higher value is implicitly specified* by either of the constructors with arguments.* MUST be a power of two <= 1<<30.*/
    static final int MAXIMUM_CAPACITY = 1 << 30;
    
  • DEFAULT_LOAD_FACTOR:HashMap 的默认加载因子,0.75

    /*** The load factor used when none specified in constructor.*/
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    • 不同于 ArrayList,HashMap 不是在底层数组全部填满时才进行扩容操作,因为数组上有一些位置可能会一直都没有添加元素,但其他位置上元素可能有很多,导致链表和二叉树结构变多。因此,会在元素添加到一定数量时,就执行扩容操作,即添加元素数量达到 threshold 值时扩容。默认加载因子如果过小,会导致数组还有很多空位置时扩容,数组利用率低;默认加载因子如果过大,会导致数组中存在很多元素时才扩容,链表和二叉树结构过多。因此,默认加载因子在 0.7 ~ 0.75 左右比较合适。
  • TREEIFY_THRESHOLD:Bucket 中链表存储的 Node 长度大于该默认值,判断是否转换为红黑树,默认为 8

    /*** The bin count threshold for using a tree rather than list for a* bin.  Bins are converted to trees when adding an element to a* bin with at least this many nodes. The value must be greater* than 2 and should be at least 8 to mesh with assumptions in* tree removal about conversion back to plain bins upon* shrinkage.*/
    static final int TREEIFY_THRESHOLD = 8;
    
  • UNTREEIFY_THRESHOLD:Bucket 中红黑树存储的 Node 长度小于该默认值,转换为链表,默认为 6

    /*** The bin count threshold for untreeifying a (split) bin during a* resize operation. Should be less than TREEIFY_THRESHOLD, and at* most 6 to mesh with shrinkage detection under removal.*/
    static final int UNTREEIFY_THRESHOLD = 6;
    
  • MIN_TREEIFY_CAPACITY:桶中的 Node 被树化时最小的 hash 表容量,默认为 64。

    /*** The smallest table capacity for which bins may be treeified.* (Otherwise the table is resized if too many nodes in a bin.)* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts* between resizing and treeification thresholds.*/
    static final int MIN_TREEIFY_CAPACITY = 64;
    
    • 当桶中 Node 的数量大到需要变红黑树(8)时,若 hash 表容量小于 MIN_TREEIFY_CAPACITY,此时应执行 resize() 进行扩容操作。MIN_TREEIFY_CAPACITY 的值至少是 TREEIFY_THRESHOLD 的 4 倍。
  • table存储元素的数组,长度总是 2 的 n 次幂。

    /*** The table, initialized on first use, and resized as* necessary. When allocated, length is always a power of two.* (We also tolerate length zero in some operations to allow* bootstrapping mechanics that are currently not needed.)*/
    transient Node<K,V>[] table;
    
  • entrySet:存储具体元素的集。

    /*** Holds cached entrySet(). Note that AbstractMap fields are used* for keySet() and values().*/
    transient Set<Map.Entry<K,V>> entrySet;
    
  • size:HashMap 中已存储的键值对的数量。

    /*** The number of key-value mappings contained in this map.*/
    transient int size;
    
  • modCount:HashMap 扩容和结构改变的次数。

    /*** The number of times this HashMap has been structurally modified* Structural modifications are those that change the number of mappings in* the HashMap or otherwise modify its internal structure (e.g.,* rehash).  This field is used to make iterators on Collection-views of* the HashMap fail-fast.  (See ConcurrentModificationException).*/
    transient int modCount;
    
  • threshold:扩容的临界值,其值一般等于(容量 * 加载因子),(int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);。扩容的操作不是当底层数组全部被填满后再扩容,而是达到临界值后的下一次添加操作进行扩容。

    /*** The next size value at which to resize (capacity * load factor).** @serial*/
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;
    
  • loadFactor:加载因子。

    /*** The load factor for the hash table.** @serial*/
    final float loadFactor;
    

new HashMap<>() 时,赋值加载因子 loadFactor 为 DEFAULT_LOAD_FACTOR,即 0.75

/*** Constructs an empty {@code HashMap} with the default initial capacity* (16) and the default load factor (0.75).*/
public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

第一次调用 put() 方法时,通过 resize() 方法,创建一个长度为 16 的 Node 数组

/*** Associates the specified value with the specified key in this map.* If the map previously contained a mapping for the key, the old* value is replaced.** @param key key with which the specified value is to be associated* @param value value to be associated with the specified key* @return the previous value associated with {@code key}, or*         {@code null} if there was no mapping for {@code key}.*         (A {@code null} return can also indicate that the map*         previously associated {@code null} with {@code key}.)*/
public V put(K key, V value) {// key 做 hashreturn putVal(hash(key), key, value, false, true);
}
/*** Implements Map.put and related methods.** @param hash hash for key* @param key the key* @param value the value to put* @param onlyIfAbsent if true, don't change existing value* @param evict if false, the table is in creation mode.* @return previous value, or null if none*/
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)// 第一次调用 put() 方法,此时,table 未初始化,为 null,调用 resize() 方法,创建长度为 16 的 Node 数组n = (tab = resize()).length;// 然后,查看 Node 数组中的位置 i 的元素 p,是否为 nullif ((p = tab[i = (n - 1) & hash]) == null)// 如果 p 为 null,说明当前位置 i 没有元素,添加成功 ---> 情况 1tab[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))))// 位置 i 上的元素,与当前待添加元素的 key 相同e = p;else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {// 位置 i 上的元素,与当前待添加元素的 key 不同for (int binCount = 0; ; ++binCount) {// 位置 i 上,只有一个元素if ((e = p.next) == null) {// 位置 i 上的原元素指向当前待添加的元素,添加成功 ---> 情况 2 和 3p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st// 如果链表的长度超过 8 时,判断是否转为红黑树结构treeifyBin(tab, hash);break;}// 位置 i 上,不止一个元素,依次获得该链表上的每一个元素,与当前待添加元素的 key,对比 hash 值和 equals() 方法if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;
}
/*** Initializes or doubles table size.  If null, allocates in* accord with initial capacity target held in field threshold.* Otherwise, because we are using power-of-two expansion, the* elements from each bin must either stay at same index, or move* with a power of two offset in the new table.** @return the table*/
final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;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}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;else {               // zero initial threshold signifies using defaults// 默认数组长度 16newCap = DEFAULT_INITIAL_CAPACITY;// 默认扩容的临界值 0.75 * 16 = 12newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}// 赋值扩容的临界值 12threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})// 创建一个长度为 16 的 Node 数组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;if (e.next == null)newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // preserve orderNode<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;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.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;
}

计算 key 的 hash 值:

/*** Computes key.hashCode() and spreads (XORs) higher bits of hash* to lower.  Because the table uses power-of-two masking, sets of* hashes that vary only in bits above the current mask will* always collide. (Among known examples are sets of Float keys* holding consecutive whole numbers in small tables.)  So we* apply a transform that spreads the impact of higher bits* downward. There is a tradeoff between speed, utility, and* quality of bit-spreading. Because many common sets of hashes* are already reasonably distributed (so don't benefit from* spreading), and because we use trees to handle large sets of* collisions in bins, we just XOR some shifted bits in the* cheapest possible way to reduce systematic lossage, as well as* to incorporate impact of the highest bits that would otherwise* never be used in index calculations because of table bounds.*/
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

判断链表是否转红黑树:

/*** Replaces all linked nodes in bin at index for given hash unless* table is too small, in which case resizes instead.*/
final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)// 如果底层数组的长度小于 64,只扩容,不转红黑树resize();else if ((e = tab[index = (n - 1) & hash]) != null) {TreeNode<K,V> hd = null, tl = null;do {TreeNode<K,V> p = replacementTreeNode(e, null);if (tl == null)hd = p;else {p.prev = tl;tl.next = p;}tl = p;} while ((e = e.next) != null);if ((tab[index] = hd) != null)hd.treeify(tab);}
}

总结:

  • new HashMap<>() 时,底层没有创建数组,只赋值加载因子 loadFactor 为 0.75。
  • 首次调用 put() 方法时,底层创建长度为 16 的 Node 数组。
  • 执行 map.put(key1, value1) 操作,可能已经执行过多次 put() 方法:
    • 首先,计算 key1 所在类的 hashCode() 以及其他操作计算 key1 的哈希值,此哈希值经过某种算法计算以后,得到在 Node 数组中的存放位置。

    • 如果此位置上的数据为空,此时的 key1 - value1 添加成功。---> 情况 1

    • 如果此位置上的数据不为空,意味着此位置上存在一个或多个数据,比较 key1 和已经存在的一个或多个数据的哈希值:

      • 如果 key1 的哈希值与已经存在的数据的哈希值都不相同,此时 key1 - value1 添加成功。---> 情况 2
      • 如果 key1 的哈希值和已经存在的某一个数据(key2 - value2)的哈希值相同,则调用 key1 所在类的 equals(key2),继续比较:
        • 如果 equals() 返回 false:此时 key1 - value1 添加成功。---> 情况 3
        • 如果 equals() 返回 true:使用 value1 替换 value2。
    • 补充:关于情况 2 和情况 3,此时 key1 - value1 和原来的数据以链表的方式存储。

  • 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的数据改为使用红黑树存储。

存储结构:数组 + 链表 + 红黑树

1722141753815

  • HashMap 的内部存储结构其实是数组 + 链表 + 红黑树的结合。当实例化一个 HashMap 时,会初始化 initialCapacity 和 loadFactor,在 put 第一对映射关系时,系统会创建一个长度为 initialCapacity 的 Node 数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为 "桶"(Bucket),每个 Bucket 都有自己的索引,系统可以根据索引快速的查找 Bucket 中的元素。
  • 每个 Bucket 中存储一个元素,即一个 Node 对象,但每一个 Node 对象可以带一个引用变量 next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个 Node 链。也可能是一个一个 TreeNode 对象,每一个 TreeNode 对象可以有两个叶子结点 left 和 right,因此,在一个桶中,就有可能生成一个 TreeNode 树。而新添加的元素作为链表的 last,或树的叶子结点。

扩容过程:

  • 当 HashMap 中的元素越来越多的时候,hash 冲突的几率也就越来越高,因为底层数组的长度是固定的。所以为了提高查询的效率,就要对 HashMap 的底层数组进行扩容,而在 HashMap 数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize()
  • 当 HashMap 中的元素个数超过 "数组大小(数组总大小 length,不是数组中存储的元素个数 size) * loadFactor" 时 , 就会进行数组扩容 。其中,loadFactor 的 默认值为 0.75,这是一个折中的取值,默认情况下,数组大小为 16,那么当 HashMap 中元素个数 ≥ 16 * 0.75 = 12 (这个值就是代码中的 threshold 值,也叫做临界值)且要存放的位置非空的时候,就把数组的大小扩展为 2 * 16 = 32,即扩大一倍,然后重新计算每个元素在数组中的位置,把原有的数据复制到新数组中。
  • 扩容是一个非常消耗性能的操作,如果已经预知 HashMap 中元素的个数,那么预设元素的个数能够有效的提高 HashMap 的性能。

LinkedHashMap

LinkedHashMap 在 HashMap 存储结构的基础上,使用了一对双向链表来记录添加元素的顺序,对于频繁的遍历操作,执行效率高于 HashMap。

  • LinkedHashMap 在遍历元素时,可以按照添加的顺序实现遍历。

LinkedHashMap 在原有的 HashMap 底层结构基础上,添加了一对指针 befor 和 after,指向当前元素的前一个和后一个元素:

/*** HashMap.Node subclass for normal LinkedHashMap entries.*/
static class Entry<K,V> extends HashMap.Node<K,V> {Entry<K,V> before, after;Entry(int hash, K key, V value, Node<K,V> next) {super(hash, key, value, next);}
}

扩展

红黑树在 HashMap 中的应用

HashMap 在 Java 8 中引入了红黑树作为链表的替代数据结构,用于在发生哈希冲突时优化查询性能,红黑树的引入主要是为了提高高冲突情况下的性能。

下面是详细解释:

  1. 链表的性能问题。在早期版本的 HashMap 中,当多个键的哈希值相同时,这些键值对会存储在同一个链表中。如果哈希冲突较多,链表变长,查找、插入和删除操作的时间复杂度会从平均情况下的 O(1) 降到最坏情况下的 O(n),其中 n 是链表的长度。

  2. 红黑树的引入。红黑树是一种自平衡二叉搜索树,具有以下特性:

  • 每个节点都是红色或黑色。
  • 根节点是黑色。
  • 所有叶子节点(NIL 节点)都是黑色。
  • 如果一个节点是红色,则其两个子节点都是黑色(即不能有两个连续的红色节点)。
  • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
  • 以上这些特性,使得红黑树在最坏情况下的时间复杂度为 O(log n),其中 n 是树的节点数。这意味着,即使在发生大量哈希冲突的情况下,查找、插入和删除操作的性能也能得到保证。
  1. 红黑树在 HashMap 中的应用。在 Java 8 及之后的版本中,HashMap 在发生哈希冲突时,会根据链表的长度决定是否将链表转换为红黑树:
  • 当链表长度超过某个阈值(默认是 8),HashMap 会将链表转换为红黑树。
  • 当红黑树节点数量下降到某个阈值(默认是 6)以下时,红黑树会重新转换为链表。
  • 这样做的目的是在链表较短时保持简单的链表结构(因为链表在长度较短时性能很好且占用内存较小),而在链表较长时切换到红黑树以提高性能。
  1. 红黑树的实现。HashMap 中的红黑树由一个内部类 TreeNode 表示,TreeNode 类继承了 HashMap 的 Node 类,并增加了红黑树特有的属性和方法。以下是简化的 TreeNode 类定义:
/**
* Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
* extends Node) so can be used as extension of either regular or
* linked node.
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {// 父节点TreeNode<K,V> parent;  // red-black tree links// 左子节点TreeNode<K,V> left;// 右子节点TreeNode<K,V> right;// 前一个节点TreeNode<K,V> prev;    // needed to unlink next upon deletion// 节点颜色boolean red;TreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}// 红黑树相关的方法,如插入、删除、旋转等
}

结论:HashMap 使用红黑树来处理高冲突情况下的性能问题,这样可以保证在最坏情况下,查找、插入和删除操作的时间复杂度为 O(log n),从而提高了 HashMap 的整体性能和稳定性。这种设计使得 HashMap 在面对大量数据和高冲突时依然能够保持高效的性能表现。

红黑树 vs B+Tree

红黑树和 B+Tree是两种常用的数据结构,它们都有各自的应用场景和特点。下面详细介绍这两种数据结构的区别:

  1. 数据结构和节点类型
  • 红黑树
    • 是一种自平衡的二叉搜索树。
    • 每个节点包含一个键和值,以及颜色属性(红色或黑色)。
    • 每个节点最多有两个子节点。
  • B+Tree:
    • 是一种多路搜索树,常用于数据库和文件系统中。
    • 每个节点可以有多个子节点和多个键。
    • 所有的键值对都存储在叶子节点,内节点只存储键不存储值。
    • 叶子节点形成一个有序链表,便于范围查询。
  1. 平衡机制
  • 红黑树
    • 通过红黑规则(如红色节点不能有红色子节点、从根到叶子的所有路径包含相同数量的黑色节点等)来保持平衡。
    • 通过旋转和重新着色操作进行自平衡,插入和删除操作的时间复杂度为 O(log n)。
  • B+Tree
    • 通过分裂和合并操作来保持平衡。
    • 每个节点(除根节点外)至少有 ⌈m/2⌉ - 1 个键,最多有 m - 1 个键,其中 m 是树的阶数。
    • 内节点的插入和删除操作可能会引发分裂和合并,但时间复杂度也是 O(log n)。
  1. 应用场景
  • 红黑树
    • 主要用于内存中的数据结构,例如 TreeMap 和 TreeSet。
    • 适合需要频繁插入、删除和查找操作的场景。
  • B+Tree
    • 主要用于磁盘存储的数据库索引和文件系统。
    • 适合需要高效范围查询和顺序访问的场景。
    • 由于叶子节点构成有序链表,范围查询效率较高。
  1. 查询性能
  • 红黑树
    • 查找单个元素的时间复杂度为 O(log n)。
    • 不适合大规模范围查询,因为需要遍历树结构。
  • B+Tree
    • 查找单个元素的时间复杂度也是 O(log n)。
    • 范围查询性能优越,因为叶子节点形成有序链表,查询可以在链表中顺序遍历。
  1. 存储效率
  • 红黑树
    • 每个节点存储一个键和值,以及指向左右子节点的指针和颜色信息。
    • 节点的存储开销相对较小。
  • B+Tree
    • 内节点存储多个键和指向子节点的指针,叶子节点存储键值对。
    • 内节点的存储开销较大,但可以有效减少树的高度,适合磁盘存储。
  1. 叶子节点
  • 红黑树
    • 叶子节点可以包含数据,也可以为空(表示树的末端)。
  • B+Tree
    • 所有数据都存储在叶子节点,内节点只存储索引。
    • 叶子节点通过指针相连,形成有序链表。
  1. 插入和删除
  • 红黑树

    • 插入操作:

      • 新节点首先作为红色节点插入。
      • 可能需要通过旋转和重新着色来修复红黑树的平衡性质。
    • 删除操作:

      • 如果删除的节点是红色节点,直接删除即可。
      • 如果删除的节点是黑色节点,可能需要通过旋转和重新着色来修复红黑树的平衡性质。
  • B+Tree

    • 插入操作:

      • 插入新键时,如果目标节点已满,需要进行节点分裂。
      • 新的键会被推到父节点,可能引发递归的节点分裂。
    • 删除操作:

      • 删除键时,如果节点中的键数量低于最低要求(通常是 ⌈m/2⌉ - 1 个键),需要进行节点合并或重新分配。
      • 这些操作可能引发递归的合并或重新分配,直到树恢复平衡。

总结:

  • 红黑树
    • 适用于内存中的高效查找、插入和删除操作。
    • 不适合大规模的范围查询。
    • 结构相对简单,节点存储开销较小。
  • B+Tree
    • 适用于数据库和文件系统中的索引结构,特别是磁盘存储。
    • 适合大规模的范围查询和顺序访问。
    • 由于内节点存储多个键和指向子节点的指针,存储开销较大,但树的高度较小,适合减少磁盘 I/O。

原文链接

https://github.com/ACatSmiling/zero-to-zero/blob/main/JavaLanguage/java-advanced.md

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

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

相关文章

06-论说文:审题与立意(3)

06-论说文:审题与立意(3) 立意 就提论题 不能写创新 违反同一律 手段 目的 多谈主张 立意过宽 立意过窄 多谈主张 手段+目的 要结合题干信息 假大空 大而不当 审题立意是整体性的

linux系统配置及相关文件

一.本地服务器:几个相关软件 123432 (一)vmware <关键字:虚拟机(搭建虚拟环境) (二)MobaXterm 关键字:连接 (三) 1231314参考链接 1231314

python必会的函数或者操作

排序 sorted(data,reverse=True or False)zip()将多个可迭代对象打包成一个元组列表 list or set (zip())map()对可迭代对象中的每个元素应用函数 map(data,func)filter() 跟map类似的用法reduce()对可迭代对象中的元素进行累计计算 from functools import reduce 用法跟…

29. GIL全局解释器锁、信号量、线程池进程池

1. GIL全局解释器锁1.1 概念 In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiplenative threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (How…

[操作系统]进程同步

临界区 互斥量 信号量 事件

05-论说文:审题与立意(2)

争论性材料 描述性材料 审题最难的 有个三段论!! 人工智能的作用 有好有坏 技术变革是中项 三段论 、 这怎么写? 经济联考: 蚂蚁 ==》资源 可持续发展 题干明确 ==》给啥说啥 题干不明确,典故、实验、自然现象==》社会、企业管理 见人…

本文来自博客园,作者:雨中秋,转载请注明原文链接:https://www.cnblogs.com/zengzi/p/18445105,不然会AFO