Java中级编程大师班<第一篇:初识数据结构与算法-数组(2)>
数组(Array)
数组是计算机编程中最基本的数据结构之一。它是一个有序的元素集合,每个元素都可以通过索引进行访问。本文将详细介绍数组的特性、用法和注意事项。
数组的基本特性
数组具有以下基本特性:
- 有序性: 数组中的元素是有序排列的,可以通过索引访问。
- 固定长度: 数组的长度在创建时固定,无法动态改变。
- 相同数据类型: 数组中的元素通常是相同数据类型的。
示例代码 - 创建和访问数组
让我们首先了解如何创建和访问数组:
// 创建一个整数数组
int[] numbers = new int[5];// 插入元素
numbers[0] = 1;
numbers[1] = 2;
numbers[2] = 3;
numbers[3] = 4;
numbers[4] = 5;// 访问元素
int thirdNumber = numbers[2]; // 访问第三个元素,值为3
数组的源码分析
数组是一种非常基础的数据结构,通常由编程语言的底层机制直接支持。在 Java 中,数组是通过内存分配和索引访问实现的。你可以在 Java 的 java.util
包中找到一些与数组相关的工具类和方法,如 Arrays
类。
注意事项
在使用数组时,需要注意以下事项:
- 固定长度: 数组的长度一旦确定,就无法更改。如果需要动态添加或删除元素,可能需要使用其他数据结构。
- 索引越界: 访问数组元素时必须确保索引在有效范围内,否则会导致索引越界异常。
- 内存占用: 数组的内存占用是固定的,如果分配了过多的空间,可能会浪费内存。
数组的使用场景
数组在许多场景下都非常有用,包括:
- 列表存储: 数组可以用来存储列表数据,如学生成绩、购物清单等。
- 数据检索: 由于数组的有序性,可以快速检索元素,例如查找最大值或最小值。
- 算法实现: 数组是许多算法的基础,如排序算法和搜索算法。
主要知识点讲解
动态数组和静态数组的区别
动态数组和静态数组是两种常见的数组类型,它们在内存管理和使用灵活性方面存在显著的区别。以下是它们的主要区别:
-
大小变化:
- 静态数组: 静态数组在创建时需要指定固定的大小,一旦分配了空间,大小不能更改。如果需要更多的存储空间,必须创建一个新的数组,将数据复制到新数组中,然后释放旧数组。这导致了内存的浪费和复杂的操作。
- 动态数组: 动态数组在创建时可以指定初始大小,但如果数组元素数量超过当前容量,动态数组会自动扩展,分配更多的内存空间。这使得动态数组能够有效地处理大小变化的数据。
-
内存管理:
- 静态数组: 静态数组在创建时分配了固定大小的内存,因此可能浪费内存(如果未充分利用)或不足以存储所有数据(如果超过容量)。
- 动态数组: 动态数组能够根据需要分配和释放内存,以适应数据的大小。这减少了内存浪费,并确保有效使用可用内存。
-
使用灵活性:
- 静态数组: 静态数组的大小是固定的,因此对于不同大小的数据集可能不够灵活。如果需要存储的数据数量不确定,静态数组可能无法满足需求。
- 动态数组: 动态数组的大小可以动态增加,因此适用于处理不同大小的数据集。这使得它们更灵活且适用于各种情况。
-
实现复杂度:
- 静态数组: 静态数组的实现通常比较简单,因为它们不需要处理大小变化的逻辑。
- 动态数组: 动态数组的实现相对复杂,因为它们需要管理内存分配和复制数据的逻辑。通常,动态数组使用静态数组作为底层存储,以便动态扩展和收缩。
综上所述,动态数组在处理不确定数据大小的情况下更为灵活和高效,因为它们可以自动调整大小,而静态数组则需要在创建时指定大小,并且无法轻松适应不同的数据集。因此,大多数编程语言和应用程序更倾向于使用动态数组或提供动态数组的数据结构,如 Java 中的 ArrayList 或 Python 中的列表。
以下是Java中的一个简单示例,展示了静态数组和动态数组(ArrayList)的用法,以及它们之间的一些主要区别:
import java.util.ArrayList;public class ArrayExample {public static void main(String[] args) {// 静态数组(固定大小)int[] staticArray = new int[5]; // 创建一个包含5个整数的静态数组staticArray[0] = 10;staticArray[1] = 20;staticArray[2] = 30;staticArray[3] = 40;staticArray[4] = 50;// 注意:静态数组大小不可更改// staticArray[5] = 60; // 这将导致数组越界错误System.out.println("静态数组元素:");for (int i = 0; i < staticArray.length; i++) {System.out.print(staticArray[i] + " ");}System.out.println();// 动态数组(ArrayList)ArrayList<Integer> dynamicArray = new ArrayList<Integer>(); // 创建一个空的动态数组dynamicArray.add(10);dynamicArray.add(20);dynamicArray.add(30);System.out.println("动态数组元素:");for (int i = 0; i < dynamicArray.size(); i++) {System.out.print(dynamicArray.get(i) + " ");}System.out.println();// 动态数组可以根据需要自动扩展dynamicArray.add(40);dynamicArray.add(50);System.out.println("动态数组元素(添加后):");for (int i = 0; i < dynamicArray.size(); i++) {System.out.print(dynamicArray.get(i) + " ");}System.out.println();}
}
在这个示例中,我们首先创建了一个静态数组 staticArray
和一个动态数组 dynamicArray
(使用 ArrayList 实现)。静态数组的大小在创建时固定,无法更改,而动态数组可以根据需要自动扩展。我们演示了如何向这两种数组中添加和访问元素。
请注意,在使用动态数组时,我们使用 ArrayList
的 add
方法来添加元素,而使用 size
和 get
方法来访问元素。此外,动态数组的大小可以根据添加的元素自动增长,而静态数组的大小是固定的。
当进一步扩展示例以更好地理解静态数组和动态数组之间的差异时,我们可以考虑以下方面:
-
动态数组的自动扩展: 通过向动态数组添加元素来展示其自动扩展的特性。
-
静态数组的大小限制: 展示静态数组的大小限制以及如何处理超出容量的情况。
-
性能比较: 比较静态数组和动态数组在插入、删除和访问元素方面的性能差异。
下面是一个扩展示例,演示了这些方面的比较:
import java.util.ArrayList;public class ArrayExample {public static void main(String[] args) {// 静态数组(固定大小)int[] staticArray = new int[5];// 添加元素到静态数组for (int i = 0; i < staticArray.length; i++) {staticArray[i] = i * 10;}// 试图添加超出容量的元素// staticArray[5] = 50; // 这将导致数组越界错误// 动态数组(ArrayList)ArrayList<Integer> dynamicArray = new ArrayList<Integer>();// 添加元素到动态数组for (int i = 0; i < 5; i++) {dynamicArray.add(i * 10);}// 展示动态数组的大小自动扩展for (int i = 5; i < 10; i++) {dynamicArray.add(i * 10);}// 访问静态数组的元素System.out.println("静态数组元素:");for (int i = 0; i < staticArray.length; i++) {System.out.print(staticArray[i] + " ");}System.out.println();// 访问动态数组的元素System.out.println("动态数组元素:");for (int i = 0; i < dynamicArray.size(); i++) {System.out.print(dynamicArray.get(i) + " ");}System.out.println();// 比较静态数组和动态数组的性能long startTime = System.nanoTime();// 在静态数组中执行一系列操作long endTime = System.nanoTime();System.out.println("静态数组操作耗时:" + (endTime - startTime) + "纳秒");startTime = System.nanoTime();// 在动态数组中执行相同系列操作endTime = System.nanoTime();System.out.println("动态数组操作耗时:" + (endTime - startTime) + "纳秒");}
}
在这个扩展示例中,我们添加了更多元素到动态数组,展示了其自动扩展的特性。我们还演示了如何访问静态数组和动态数组的元素,并进行性能比较。
请注意,在性能比较方面,动态数组通常具有更好的性能,因为它可以自动扩展,而不需要手动管理容量。但这也取决于具体的操作和场景。
多维数组的概念和使用
多维数组是一种特殊类型的数组,它可以包含其他数组作为其元素,从而形成多维数据结构。多维数组在处理复杂的数据和表格时非常有用,通常用于表示矩阵、图像、表格等具有多个维度的数据集。
以下是多维数组的概念和使用示例:
多维数组的概念
多维数组是一个包含多个维度的数组,每个维度都可以看作是一个单独的数组。通常,我们使用二维数组和三维数组最为常见,但我们可以创建具有任意数量维度的多维数组。
-
二维数组: 二维数组是一个表格状的数组,具有行和列。它可以看作是一个数组的数组,其中每个元素是一个数组,代表一行或一列的数据。
-
三维数组: 三维数组可以被视为一堆二维数组。每个元素是一个二维数组,代表了一个平面或一层的数据。
-
多维数组: 多维数组可以扩展到更多的维度,例如四维数组、五维数组等。每个维度都会增加一个方向,用于组织数据。
多维数组的声明和初始化
在Java中,声明和初始化多维数组的语法如下:
// 声明和初始化二维数组
dataType[][] arrayName = new dataType[rows][columns];// 例如,创建一个3x4的整数二维数组
int[][] twoDArray = new int[3][4];// 声明和初始化三维数组
dataType[][][] arrayName = new dataType[depth][rows][columns];// 例如,创建一个2x3x4的整数三维数组
int[][][] threeDArray = new int[2][3][4];
多维数组的访问和操作
多维数组的访问和操作涉及到多个索引,每个索引对应一个维度。例如,对于二维数组:
int value = twoDArray[rowIndex][columnIndex];
对于三维数组:
int value = threeDArray[layerIndex][rowIndex][columnIndex];
我们可以使用嵌套的循环来遍历和操作多维数组的元素,例如:
// 遍历二维数组
for (int i = 0; i < rows; i++) {for (int j = 0; j < columns; j++) {int value = twoDArray[i][j];// 进行操作}
}// 遍历三维数组
for (int i = 0; i < depth; i++) {for (int j = 0; j < rows; j++) {for (int k = 0; k < columns; k++) {int value = threeDArray[i][j][k];// 进行操作}}
}
示例:二维数组的应用
以下是一个示例,演示了如何使用二维数组表示一个矩阵,并进行一些基本的操作:
public class TwoDArrayExample {public static void main(String[] args) {// 创建一个3x4的整数二维数组int[][] matrix = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};// 访问元素int value = matrix[1][2]; // 访问第2行第3列的元素(值为7)// 遍历并打印矩阵for (int i = 0; i < matrix.length; i++) {for (int j = 0; j < matrix[i].length; j++) {System.out.print(matrix[i][j] + " ");}System.out.println();}}
}
这个示例创建了一个3x4的整数二维数组,访问了其中的元素,并遍历了整个矩阵。
多维数组在编程中具有广泛的应用,尤其是在处理矩阵、图像、游戏地图等需要多维数据结构的场景中。通过多维数组,我们可以更方便地组织和访问复杂的数据。
多维数组是一种包含多个维度的数组,每个维度可以看作是一个单独的数组。它们用于表示多维数据结构,例如矩阵、立方体、三维坐标等。在多维数组中,每个元素都由多个索引确定,每个索引对应一个维度。
以下是多维数组的更详细描述和代码示例:
二维数组
二维数组是最常见的多维数组类型,它包含两个维度:行和列。在Java中,可以通过嵌套数组来创建二维数组。下面是一个示例:
// 创建一个3x3的整数二维数组
int[][] matrix = {{1, 2, 3},{4, 5, 6},{7, 8, 9}
};// 访问元素
int value = matrix[1][2]; // 访问第2行第3列的元素(值为6)
三维数组
三维数组包含三个维度,通常用于表示立体数据。在Java中,可以使用嵌套的数组来创建三维数组。以下是一个示例:
// 创建一个2x3x4的整数三维数组
int[][][] cube = {{{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}},{{13, 14, 15, 16},{17, 18, 19, 20},{21, 22, 23, 24}}
};// 访问元素
int value = cube[1][2][3]; // 访问第2层、第3行、第4列的元素(值为24)
多维数组的遍历
遍历多维数组需要嵌套循环,每个维度一个循环。以下是遍历二维数组和三维数组的示例:
// 遍历二维数组
for (int i = 0; i < matrix.length; i++) {for (int j = 0; j < matrix[i].length; j++) {int value = matrix[i][j];// 进行操作}
}// 遍历三维数组
for (int i = 0; i < cube.length; i++) {for (int j = 0; j < cube[i].length; j++) {for (int k = 0; k < cube[i][j].length; k++) {int value = cube[i][j][k];// 进行操作}}
}
多维数组的应用
多维数组在编程中有许多应用,例如:
-
图像处理: 图像通常表示为二维数组,其中每个元素代表像素的颜色或亮度值。
-
游戏开发: 游戏地图、三维模型等数据可以使用多维数组表示。
-
科学计算: 在科学和工程领域中,多维数组用于表示复杂的数据集,如物理模拟、地理信息系统(GIS)等。
-
数据分析: 在数据分析和机器学习中,多维数组用于存储和处理大规模数据集。
多维数组在处理需要多个维度的数据时非常有用,它们提供了一种有效的方式来组织和访问这些数据。根据需求,我们可以创建具有更多维度的多维数组,以满足特定的应用需求。
数组的时间复杂度分析,包括插入、删除和查找操作的性能分析
数组是一种基本的数据结构,其性能分析涉及到插入、删除和查找操作的时间复杂度。下面是关于数组各种操作的时间复杂度分析:
-
插入操作:
-
在末尾插入元素(尾部插入):插入操作的时间复杂度为 O(1)。因为在数组末尾添加元素不需要移动其他元素,只需将新元素放在最后即可。
-
在中间或开头插入元素:如果要在数组中间或开头插入元素,需要将插入位置之后的所有元素都向后移动一个位置。这样的操作的时间复杂度是 O(n),其中 n 是数组中的元素数量,因为它需要线性时间来移动元素。
-
-
删除操作:
-
删除末尾元素(尾部删除):与插入类似,删除操作的时间复杂度为 O(1)。只需将末尾元素标记为删除即可,不需要移动其他元素。
-
删除中间或开头元素:与插入相同,删除中间或开头的元素需要将删除位置之后的所有元素向前移动一个位置,时间复杂度为 O(n)。
-
-
查找操作:
-
按索引查找元素:查找特定索引位置的元素的时间复杂度是 O(1)。因为可以通过数组的索引直接访问元素。
-
线性查找:如果需要在数组中查找特定值,且数组未排序,那么最坏情况下需要遍历整个数组,时间复杂度为 O(n)。
-
二分查找:如果数组已排序,可以使用二分查找来提高查找效率,时间复杂度为 O(log n)。但前提是数组必须是有序的。
-
总结:
-
插入和删除操作的性能受到插入或删除的位置以及元素数量的影响。在尾部插入或删除元素时性能最好,时间复杂度为 O(1),而在中间或开头插入或删除元素时性能较差,时间复杂度为 O(n)。
-
查找操作的性能取决于具体情况。按索引查找元素的时间复杂度为 O(1)。在无序数组中线性查找的时间复杂度为 O(n),而在有序数组中二分查找的时间复杂度为 O(log n)。
因此,对于需要频繁进行插入、删除和查找操作的情况,可能需要考虑使用其他数据结构,如链表或哈希表,以获得更好的性能。数组在具体应用中的选择应根据操作的频率和性能要求来决定。
以下是关于数组插入、删除和查找操作的Java代码示例:
import java.util.Arrays;public class ArrayOperationsExample {public static void main(String[] args) {// 创建一个整数数组int[] arr = {10, 20, 30, 40, 50};// 打印原始数组System.out.println("原始数组:" + Arrays.toString(arr));// 插入操作int insertValue = 60;int insertIndex = 2; // 在索引2处插入元素insertElement(arr, insertValue, insertIndex);System.out.println("插入元素 " + insertValue + " 后的数组:" + Arrays.toString(arr));// 删除操作int deleteIndex = 3; // 删除索引3处的元素deleteElement(arr, deleteIndex);System.out.println("删除索引 " + deleteIndex + " 处的元素后的数组:" + Arrays.toString(arr));// 查找操作int searchValue = 30;int searchIndex = searchElement(arr, searchValue);if (searchIndex != -1) {System.out.println("元素 " + searchValue + " 在数组中的索引位置为 " + searchIndex);} else {System.out.println("元素 " + searchValue + " 不在数组中。");}}// 插入元素public static void insertElement(int[] arr, int value, int index) {if (index < 0 || index > arr.length) {System.out.println("插入位置无效。");return;}// 创建一个新数组,比原数组多一个元素int[] newArr = new int[arr.length + 1];// 将原数组前半部分复制到新数组中for (int i = 0; i < index; i++) {newArr[i] = arr[i];}// 插入新元素newArr[index] = value;// 将原数组后半部分复制到新数组中for (int i = index; i < arr.length; i++) {newArr[i + 1] = arr[i];}// 更新原数组for (int i = 0; i < arr.length; i++) {arr[i] = newArr[i];}}// 删除元素public static void deleteElement(int[] arr, int index) {if (index < 0 || index >= arr.length) {System.out.println("删除位置无效。");return;}// 创建一个新数组,比原数组少一个元素int[] newArr = new int[arr.length - 1];// 将原数组前半部分复制到新数组中for (int i = 0; i < index; i++) {newArr[i] = arr[i];}// 将原数组后半部分复制到新数组中for (int i = index + 1; i < arr.length; i++) {newArr[i - 1] = arr[i];}// 更新原数组for (int i = 0; i < arr.length - 1; i++) {arr[i] = newArr[i];}}// 查找元素public static int searchElement(int[] arr, int value) {for (int i = 0; i < arr.length; i++) {if (arr[i] == value) {return i; // 找到元素并返回索引}}return -1; // 元素不在数组中}
}
当进行插入、删除和查找操作时,我们需要综合考虑时间复杂度和空间复杂度。在数组中执行这些操作时,它们的性能表现如下:
1. 插入操作:
-
时间复杂度: 对于数组的插入操作,最好的情况是在末尾插入,时间复杂度为O(1)。这是因为只需要在数组的末尾追加元素,不需要移动其他元素。但如果要在中间或开头插入元素,则需要将插入位置之后的元素向后移动,平均时间复杂度为O(n),其中n是数组的大小。
-
空间复杂度: 插入操作的空间复杂度通常很低,仅包括新插入的元素所占用的空间。
示例代码 - 插入操作(数组):
public class ArrayExample {public static void main(String[] args) {int[] arr = {10, 20, 30, 40, 50};int insertValue = 60;int insertIndex = 2; // 在索引2处插入元素insertElement(arr, insertValue, insertIndex);}// 插入元素public static void insertElement(int[] arr, int value, int index) {if (index < 0 || index > arr.length) {System.out.println("插入位置无效。");return;}// 创建一个新数组,比原数组多一个元素int[] newArr = new int[arr.length + 1];// 将原数组前半部分复制到新数组中for (int i = 0; i < index; i++) {newArr[i] = arr[i];}// 插入新元素newArr[index] = value;// 将原数组后半部分复制到新数组中for (int i = index; i < arr.length; i++) {newArr[i + 1] = arr[i];}// 更新原数组for (int i = 0; i < arr.length; i++) {arr[i] = newArr[i];}}
}
2. 删除操作:
-
时间复杂度: 删除操作的时间复杂度与插入类似,最好的情况是删除末尾元素,时间复杂度为O(1)。但如果要删除中间或开头的元素,则需要将删除位置之后的元素向前移动,平均时间复杂度为O(n),其中n是数组的大小。
-
空间复杂度: 删除操作的空间复杂度通常很低,仅包括被删除元素的空间。
示例代码 - 删除操作(数组):
public class ArrayExample {public static void main(String[] args) {int[] arr = {10, 20, 30, 40, 50};int deleteIndex = 2; // 删除索引2处的元素deleteElement(arr, deleteIndex);}// 删除元素public static void deleteElement(int[] arr, int index) {if (index < 0 || index >= arr.length) {System.out.println("删除位置无效。");return;}// 创建一个新数组,比原数组少一个元素int[] newArr = new int[arr.length - 1];// 将原数组前半部分复制到新数组中for (int i = 0; i < index; i++) {newArr[i] = arr[i];}// 将原数组后半部分复制到新数组中for (int i = index + 1; i < arr.length; i++) {newArr[i - 1] = arr[i];}// 更新原数组for (int i = 0; i < arr.length - 1; i++) {arr[i] = newArr[i];}}
}
3. 查找操作:
-
时间复杂度: 在数组中查找操作通常需要线性搜索,时间复杂度为O(n),其中n是数组的大小。如果数组是有序的,可以使用二分查找,时间复杂度为O(log n)。
-
空间复杂度: 查找操作的空间复杂度通常很低,仅包括存储查找结果所需的空间。
示例代码 - 查找操作(数组):
public class ArrayExample {public static void main(String[] args) {int[] arr = {10, 20, 30, 40, 50};int searchValue = 30;int searchIndex = searchElement(arr, searchValue);if (searchIndex != -1) {System.out.println("元素 " + searchValue + " 在数组中的索引位置为 " + searchIndex);} else {System.out.println("元素 " + searchValue + " 不在数组中。");}}// 线性查找元素public static int searchElement(int[] arr, int value) {for (int i = 0; i < arr.length; i++) {if (arr[i] == value) {return i; // 找到并返回元素的索引}}return -1; // 元素不在数组中}
}
总结:
- 数组在插入和删除操作中的性能通常受到插入或删除位置和数组大小的影响。查找操作的性能取决于数组是否有序。
- 在选择数据结构和算法时,需要根据特定的需求和性能要求来做出决策。不同的数据结构和算法适用于不同的情况。
数组的应用扩展
除了基本的数组,还有其他高级的数据结构,用于更复杂的数据存储和检索需求。其中,HashMap 和 HashTable 是两个重要的数据结构,它们在实际编程中具有广泛的应用。
HashMap 和 HashTable
HashMap 和 HashTable 都是键值对存储的数据结构,它们可以用于快速查找和检索数据。虽然它们在用法上很相似,但也存在一些重要的区别:
-
HashMap: HashMap 是 Java 集合框架中的一部分,它允许空键(key)和空值(value),并且是非线程安全的。HashMap 使用了哈希表的数据结构,能够在常数时间内查找元素。
-
HashTable: HashTable 也是键值对存储的数据结构,但它不允许空键和空值,而且是线程安全的。HashTable 使用了类似于 HashMap 的哈希表实现,但在多线程环境下性能更好。
示例代码 - 使用 HashMap
让我们看一下如何使用 HashMap 存储和检索数据:
// 创建一个 HashMap
HashMap<String, Integer> scores = new HashMap<>();// 插入键值对
scores.put("Alice", 95);
scores.put("Bob", 88);
scores.put("Charlie", 92);// 检索值
int aliceScore = scores.get("Alice"); // 获取 Alice 的成绩
示例代码 - 使用 HashTable
使用 HashTable 与 HashMap 类似,但需要注意不允许空键或空值:
// 创建一个 HashTable
Hashtable<String, Integer> scores = new Hashtable<>();// 插入键值对
scores.put("Alice", 95);
scores.put("Bob", 88);
scores.put("Charlie", 92);// 检索值
int aliceScore = scores.get("Alice"); // 获取 Alice 的成绩
HashMap底层代码解析
理解 HashMap 内部工作原理的关键部分是研究其核心方法,如 resize
、treeifyBin
和 putVal
。以下是这些方法的简要源码示例和解释:
1. resize
方法
resize
方法用于对 HashMap 进行扩容。当键值对数量达到阈值时,调用 resize
方法进行扩容,通常将容量翻倍。
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) { // 如果旧容量大于0if (oldCap >= MAXIMUM_CAPACITY) { // 如果旧容量已经达到最大值threshold = Integer.MAX_VALUE; // 阈值设置为最大值,禁止再次扩容return oldTab; // 返回旧的桶数组} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // 扩容为原容量的两倍,并更新新阈值} else if (oldThr > 0) // 如果旧的阈值大于0newCap = oldThr; // 使用阈值作为新容量else { // 如果没有指定容量和阈值,使用默认值newCap = DEFAULT_INITIAL_CAPACITY; // 默认初始容量newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 默认阈值}if (newThr == 0) { // 如果新阈值为0float ft = (float)newCap * loadFactor; // 计算新阈值newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE); // 如果未超过最大容量,使用计算值,否则使用最大值}threshold = newThr; // 更新阈值@SuppressWarnings({"rawtypes","unchecked"})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 { // 如果是链表Node<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) { // 如果哈希码对旧容量求与为0if (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; // 返回新的桶数组
}
resize
方法首先计算新的容量和阈值。- 如果旧表(
oldTab
)不为空,它将遍历旧表的桶,根据哈希值重新分配节点到新表(newTab
)中。 - 如果节点的数量超过一定阈值,它可能会将链表转化为红黑树,以提高性能。
2. treeifyBin
方法
treeifyBin
方法用于将链表转换为红黑树,以提高查找性能。该方法通常在链表长度超过8时被调用。
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)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);}
}
treeifyBin
方法首先检查表的容量,如果小于MIN_TREEIFY_CAPACITY
,则会调用resize
方法进行扩容。- 如果表的容量足够大,它将遍历桶中的链表,将每个节点替换为红黑树节点,并构建红黑树。
3. pubVal
方法
putVal
方法是 HashMap 中用于向哈希表中添加键值对的核心方法。
/*** 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 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; // n为桶数组的长度,i为计算出的槽位索引// 如果桶数组为空或长度为0,需要初始化//判断tab是不是为空,如果为空,则将容量进行初始化,也就是说,初始换操作不是在new HashMap()的时候进行的,而是在第一次put的时候进行的if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 计算槽位索引:初始化操作以后,根据当前key的哈希值算出最终命中到哪个桶上去,并且这个桶上如果没有元素的话,则直接new一个新节点放进去if ((p = tab[i = (n - 1) & hash]) == null) // 如果当前槽位为空tab[i] = newNode(hash, key, value, null); // 直接创建新节点并放入槽位else {Node<K,V> e; K k;// 先判断一下这个桶里的第一个Node元素的key是不是和即将要存的key值相同,如果相同,则把当前桶里第一个Node元素赋值给e,这个else的最下边进行了判断,如果e!=null就执行把新value进行替换的操作if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p; // 如果找到相同键的节点,将e指向该节点else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {for (int binCount = 0; ; ++binCount) {// 如果遍历到链表尾部仍未找到相同键的节点,将新节点添加到链表尾部if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);// 如果链表长度达到阈值,将链表转化为红黑树if (binCount >= TREEIFY_THRESHOLD - 1)treeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break; // 如果找到相同键的节点,退出循环p = e;}}/*** 只要e不为空,说明要插入的key已经存在了,覆盖旧的value值,然后返回原来oldValue* 因为只是替换了旧的value值,并没有插入新的元素,所以不需要下边的扩容判断,直接* return掉*/if (e != null) { // 如果找到相同键的节点V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value; // 替换值afterNodeAccess(e);return oldValue; // 返回旧值}}++modCount;/*** 判断容量是否已经到了需要扩充的阈值了,如果到了,则进行扩充* 如果上一步已经判断key是存在的,只是替换了value值,并没有插入新的元素,所以不需要判断* 扩容,不会走这一步的*/if (++size > threshold)resize(); // 如果超过阈值,进行扩容afterNodeInsertion(evict);return null; // 返回null表示插入成功
}
putVal
方法用于将键值对放入HashMap中。此方法涵盖了查找、替换、插入和扩容等关键操作,是HashMap内部工作的核心部分。
JDK1.7和1.8对比
HashMap 在 JDK 1.7 和 JDK 1.8 中都有存在,但在 JDK 1.8 中进行了一些重要的改进。以下是 JDK 1.7 和 JDK 1.8 中 HashMap 的主要区别:
-
数据结构:
-
JDK 1.7:JDK 1.7 中的 HashMap 使用数组 + 链表的数据结构。具体说,它使用数组存储桶(buckets),每个桶存储一个链表。这意味着当多个键映射到同一个桶时,它们会在同一个链表上存储。
-
JDK 1.8:JDK 1.8 中的 HashMap 在链表长度达到一定阈值(8)时,将链表转化为红黑树。这一改进在处理大量键值对时提高了查找性能,因为红黑树的查找时间复杂度为 O(log n)。
-
-
哈希冲突解决:
-
JDK 1.7:JDK 1.7 使用链表来解决哈希冲突。当多个键映射到同一个桶时,它们会形成一个链表,需要遍历链表来查找。
-
JDK 1.8:JDK 1.8 在链表长度达到一定阈值时,会将链表转化为红黑树,这大大提高了处理长链表的性能。
-
-
并发性能:
-
JDK 1.7:JDK 1.7 中的 HashMap 不是线程安全的,如果多个线程同时操作一个 HashMap,可能会导致数据不一致或死锁等问题。为了在多线程环境下使用 HashMap,需要自行添加同步机制。
-
JDK 1.8:JDK 1.8 中的 HashMap 在处理并发操作时进行了优化。它引入了更高效的锁机制,例如分段锁和 CAS 操作,以提高并发性能。此外,JDK 1.8 还引入了
ConcurrentHashMap
类,专门用于高并发环境。
-
-
迭代性能:
-
JDK 1.7:JDK 1.7 中的 HashMap 在迭代时性能较差,因为即使没有哈希冲突,它也需要遍历整个桶数组,包括空桶。
-
JDK 1.8:JDK 1.8 中的 HashMap 在迭代时性能得到了提升,特别是在没有哈希冲突的情况下。这是由于它使用了更好的数据结构和算法来加速迭代。
-
-
空间利用:
-
JDK 1.7:JDK 1.7 中的 HashMap 对空间的利用不是很高,因为桶的数量必须是 2 的幂次方,可能会导致浪费空间。
-
JDK 1.8:JDK 1.8 中的 HashMap 在一定程度上改进了空间利用,通过采用树结构来存储哈希冲突的键值对,减少了空间浪费。
-
总的来说,JDK 1.8 中的 HashMap 在性能和并发性能上有重大改进,特别是在处理大量数据和高并发访问时表现更优越。因此,如果使用 Java 8 或更高版本,通常建议使用 JDK 1.8 中的 HashMap 实现。但要注意,如果需要在多线程环境中使用 HashMap,最好考虑使用 ConcurrentHashMap
或其他线程安全的数据结构。
注意事项
在使用 HashMap 时,有一些重要的注意事项和最佳实践,以确保正确性和性能。以下是一些关键的注意事项:
-
线程安全性:HashMap 不是线程安全的数据结构,如果在多线程环境下使用,需要考虑采取适当的同步机制,或者使用线程安全的替代品,如
ConcurrentHashMap
。 -
键的不可变性:HashMap 中的键应该是不可变的对象。如果键发生了变化,可能导致无法正常获取或删除值。
-
哈希冲突:哈希冲突是指不同的键映射到相同的哈希桶。为了处理冲突,HashMap 使用链表或红黑树。为了获得良好的性能,尽量避免大量哈希冲突,可以考虑使用良好的哈希函数或合适的数据分布。
-
哈希函数重写:如果自定义对象作为键,应该确保重写了
hashCode()
和equals()
方法,以确保正确的哈希和相等性比较。 -
初始化容量和负载因子:在创建 HashMap 时,可以指定初始容量和负载因子。根据预期的键值对数量,选择适当的初始容量可以提高性能。负载因子是用于触发扩容的阈值,通常选择合适的默认值即可。
-
遍历:在遍历 HashMap 时,尽量不要修改其结构(添加或删除键值对),否则可能会导致不确定的行为或异常。
-
Null 键和 Null 值:HashMap 允许键和值为 null。但要小心处理 null 键,以防止
NullPointerException
。 -
性能考虑:HashMap 在查找操作上有很好的性能,但在插入和删除操作上可能会有较差的性能,特别是在存在大量哈希冲突时。在需要频繁插入和删除的场景中,可以考虑使用
LinkedHashMap
或ConcurrentHashMap
。 -
扩容代价:HashMap 在达到负载因子阈值时会自动进行扩容,这涉及到重新分配键值对到新的桶数组。频繁的扩容操作可能会影响性能,因此应根据应用的需求选择适当的初始容量和负载因子。
-
equals 和 hashCode 方法:如果自定义对象作为键,确保正确实现了
equals
和hashCode
方法,以便正确地比较和查找键。
总的来说,HashMap 是一个非常有用的数据结构,但在使用时需要谨慎考虑上述注意事项,以确保其正确性和性能。根据应用的需求,还可以考虑使用其他实现了特定场景需求的 Map 接口的实现类,如 ConcurrentHashMap
、LinkedHashMap
等。
HashTable底层代码解析
HashTable
是 Java 中的一个古老的哈希表实现,它在 Java 的早期版本中被引入。虽然它在新的 Java 版本中不太常用,但仍然值得了解其内部实现。
HashTable
使用一个哈希函数将键映射到存储桶(buckets
)中,并在每个桶中存储一个键值对。每个桶实际上是一个链表,用于处理哈希冲突。当多个键映射到同一个桶时,它们会以链表的形式存储在该桶中。如果链表变得太长,性能会下降,因此 HashTable
需要定期进行 rehash 操作来重新分配键值对到新的桶中,以保持性能。
以下是 HashTable
的主要代码片段,我会添加注释来解释其关键部分。
public class Hashtable<K,V> extends Dictionary<K,V>implements Map<K,V>, Cloneable, java.io.Serializable {// 哈希表的默认初始容量private static final int DEFAULT_INITIAL_CAPACITY = 11;// 哈希表的默认负载因子private static final float DEFAULT_LOAD_FACTOR = 0.75f;// 哈希表的键值对数量private transient int count;// 哈希表的容量private int threshold;// 哈希表的装载因子private float loadFactor;// 存储键值对的数组,每个元素是一个链表头private transient Entry<?,?>[] table;// 哈希表结构发生变化的次数,用于支持快速失败机制private transient int modCount;// ...// 内部类,表示键值对的节点private static class Entry<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Entry<K,V> next;// ...}// 构造函数,初始化哈希表public Hashtable(int initialCapacity, float loadFactor) {// ...}// 计算键的哈希码private static int hash(int h) {// ...}// 扩容哈希表protected void rehash() {// ...}// 在哈希表中查找键对应的值public synchronized V get(Object key) {// ...}// 在哈希表中插入键值对public synchronized V put(K key, V value) {// ...}// ...// 内部方法,用于枚举哈希表中的键private static class Enumerator<K,V> implements Enumeration<K>, Iterator<K> {// ...}// ...// 复制哈希表public synchronized Object clone() {// ...}// ...// 其他方法,如remove, contains, clear, keys, values, size 等
}
HashTable
的 rehash 原理可以概括如下:
-
初始化:当创建一个新的
HashTable
实例时,它会分配一定数量的桶(通常是默认大小的素数)。这些桶会存储键值对,每个桶可以存储多个键值对。 -
插入操作:当执行插入操作时,
HashTable
首先使用哈希函数确定键应该存储在哪个桶中。然后,它将键值对插入到相应的桶中。如果该桶中已经存在键值对,它会将新键值对追加到链表的末尾。 -
Rehash 触发:当
HashTable
中的键值对数量达到一定阈值(通常是桶数量的 75%)时,触发 rehash 操作。这个阈值被称为负载因子。 -
Rehash 过程:在 rehash 过程中,
HashTable
将创建一个新的更大的桶数组。新的桶数量通常是原来的两倍。然后,它会遍历旧桶数组中的每个桶,将其中的键值对重新分配到新的桶数组中,根据它们的哈希值重新计算它们应该存储的位置。这个过程可能会导致链表被重新排列,以便更均匀地分布键值对。 -
完成 Rehash:一旦所有键值对都被重新分配到新的桶数组中,旧的桶数组会被丢弃,完成了 rehash 过程。
-
继续操作:在 rehash 过程中,
HashTable
仍然可以处理其他操作,例如查询和删除。但是,新的插入操作可能需要等待 rehash 完成。
以下是 HashTable
的部分 rehash 源码分析。请注意,HashTable
是一个古老的类,不建议在新代码中使用,但仍然有助于理解其内部工作原理。
private void rehash() {int oldCapacity = table.length;Entry<?,?>[] oldMap = table;// 计算新的桶数组大小,通常是原来的两倍int newCapacity = (oldCapacity << 1) + 1;if (newCapacity - MAX_ARRAY_SIZE > 0) {if (oldCapacity == MAX_ARRAY_SIZE)// Keep running with MAX_ARRAY_SIZE bucketsreturn;newCapacity = MAX_ARRAY_SIZE;}// 创建新的桶数组Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];modCount++;threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);table = newMap;// 重新分配旧桶中的键值对到新桶数组中for (int i = oldCapacity ; i-- > 0 ;) {for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {Entry<K,V> e = old;old = old.next;// 计算新的桶索引并插入到新的桶中int index = (e.hash & 0x7FFFFFFF) % newCapacity;e.next = (Entry<K,V>)newMap[index];newMap[index] = e;}}
}
这段代码展示了 HashTable
的 rehash 过程:
-
首先,计算新的桶数组大小
newCapacity
,通常是原来的两倍,并确保不会超过MAX_ARRAY_SIZE
。 -
创建新的桶数组
newMap
。 -
更新
threshold
,它表示下一次触发 rehash 的阈值。 -
遍历旧的桶数组,将每个桶中的键值对重新分配到新的桶数组中,根据它们的哈希值计算新的桶索引。
-
最后,更新
table
,将旧的桶数组替换为新的桶数组。
这样就完成了 HashTable
的 rehash 过程,以保持性能并维护负载因子。需要注意的是,HashTable
的 rehash 过程是同步的,因此可能会影响其他线程的操作,需要小心使用。在现代 Java 中,推荐使用 HashMap
或 ConcurrentHashMap
,它们提供更好的性能和灵活性。
上述代码摘录展示了 HashTable
类的关键部分。它使用了一个数组来存储键值对,每个数组元素是一个链表的头节点,解决哈希冲突。哈希表的扩容、查找、插入等操作都有对应的方法来实现。
需要注意的是,HashTable
是线程安全的,但性能不如后续引入的 ConcurrentHashMap
,因为 HashTable
使用了全表锁来保证线程安全。在多线程环境中,通常建议使用 ConcurrentHashMap
来获得更好的性能。
此外,HashTable
在现代 Java 中很少使用,因为它的功能有限,不支持 null
键和值,而且性能相对较差。通常情况下,推荐使用 HashMap
或 ConcurrentHashMap
来替代 HashTable
。
注意事项
在使用 HashTable
时,有一些重要的注意事项,特别是在多线程环境下。以下是一些关键的注意事项:
-
线程安全性:
HashTable
是线程安全的数据结构,所有公共方法都是同步的。这意味着多个线程可以同时访问和修改HashTable
的内容而不会出现数据不一致的情况。但要注意,虽然它是线程安全的,但性能相对较低,不适用于高度并发的场景。 -
null 键和值:
HashTable
不允许键或值为null
。如果尝试将null
键或值放入HashTable
,将会抛出NullPointerException
。这一点与HashMap
不同,后者允许键和值都为null
。 -
遍历:在遍历
HashTable
时,可以使用迭代器或枚举器。请注意,在遍历期间修改HashTable
的结构可能会导致ConcurrentModificationException
异常。 -
初始化容量和负载因子:与
HashMap
类似,HashTable
也有初始容量和负载因子的概念。可以在构造函数中指定这些参数,以适应不同的应用场景。 -
性能考虑:
HashTable
在多线程环境下提供了线程安全,但其性能可能相对较低。如果需要更高性能的线程安全哈希表,可以考虑使用ConcurrentHashMap
。 -
不建议使用:尽管
HashTable
是一个线程安全的数据结构,但由于其性能相对较差,不允许null
键和值,以及其他限制,通常不建议在新的 Java 代码中使用它。更常见的做法是使用HashMap
或ConcurrentHashMap
,根据需求选择合适的实现。
总的来说,HashTable
是一个古老而受限的数据结构,虽然它具有线程安全性,但在现代 Java 中有更好的替代方案。在编写新的 Java 代码时,通常建议选择更现代的线程安全哈希表实现,以获得更好的性能和更多的灵活性。
结语
下一章我们将继续探讨其他与 HashMap 相关的关键数据结构,包括 ConcurrentHashMap
、HashSet
、LinkedHashMap
。这些数据结构在特定的应用场景中发挥了重要作用,它们具有不同的特性和性能特点,我将详细介绍它们的使用方法、优势以及适用的情况。如果您在这些内容中发现任何不准确或需要进一步说明的地方,欢迎提出,我将尽力提供准确和有用的信息。让我们共同学习,共同进步。
相关文章:
Java中级编程大师班<第一篇:初识数据结构与算法-数组(2)>
数组(Array) 数组是计算机编程中最基本的数据结构之一。它是一个有序的元素集合,每个元素都可以通过索引进行访问。本文将详细介绍数组的特性、用法和注意事项。 数组的基本特性 数组具有以下基本特性: 有序性: 数…...
杰哥教你面试之一百问系列:java集合
文章目录 1. 什么是Java集合?请简要介绍一下集合框架。2. Java集合框架主要分为哪几种类型?3. 什么是迭代器(Iterator)?它的作用是什么?4. ArrayList和LinkedList有什么区别?它们何时适用&#…...
【数据结构】树和二叉树概念
1.树概念及结构 树概念 树是一种非线性的数据结构,它是由n(n>0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。 有一个特殊的结点,…...
C盘清理教程
C盘清理教程 首先使用space Sniffer 扫一下c盘,然后看一下到底是哪个文件这么大 第二步,创建软链接。 首先将我们需要移动的文件的当前路径拷贝下来:C:\Users\Tom\Desktop\test-link\abc\ghi.txt 然后假设剪切到D盘下:D:\ghi.…...
【实战-05】 flinksql look up join
摘要 look up join 能做什么? 不饶关子直接说答案, look up join 就是 广播。 重要是事情说三遍,广播。flinksql中的look up join 就类似于flinks flink Datastream api中的广播的概念,但是又不完全相同,对于初次访问…...
C++数据结构--红黑树
目录 一、红黑树的概念二、红黑树的性质三、红黑树的节点的定义四、红黑树结构五、红黑树的插入操作参考代码 五、代码汇总 一、红黑树的概念 红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过…...
Linux perf使用思考
目录 一、参考资料(建议阅读)二、值得思考的几个问题1、perf使用不同的性能事件进行统计有什么区别呢?2、那使用不同的性能事件统计出来的数据?排序是如何决定的,其中的百分比数值在不同的性能事件进行统计时各自的意义…...
自定义路由断言工厂
我们来设定一个场景: 假设我们的应用仅仅让age在(min,max)之间的人来访问。 第1步:在配置文件中,添加一个Age的断言配置 spring: application:name: api-gateway cloud:nacos:discovery:server-addr: 127.0.0.1:8848gateway:discovery:locator:enabled: trueroute…...
Nacos安装及在项目中的使用
目录 概要一、安装 Nacos1、下载 Nacos2、解压3、启动 Nacos 服务器4、自定义Nacos启动脚本5、访问Nacos Web控制台 二、Nacos----服务注册与发现1、添加 Nacos 依赖2、配置 Nacos 服务器地址3、使用 Nacos 注册服务4、启动服务 三、Nacos----配置管理1、创建配置数据2、从 Nac…...
overleaf中latex语法总结
α和bata $\alpha$ $\beta$上标和下标同时使用 $A_{IJ}^{IJ}$\\ %上标^下标_多个使用{}行内公式 \noindent $abc$\\ %行内公式\documentclass{article} \usepackage[utf8]{inputenc} \usepackage[namelimits]{amsmath} %数学公式 \usepackage{amssymb} %数学公式…...
Grafana配置邮件告警
1、创建一个监控图 2、grafana邮件配置 vim /etc/grafana/grafana.ini [smtp] enabled true host smtp.163.com:465 user qinziteng05163.com password xxxxx # 授权码 from_address qinziteng05163.com from_name Grafanasystemctl restart grafana-serv…...
setup中的nextTick函数
await nextTick() 是 Vue 3 的一个异步函数,用于等待 DOM 更新完成后执行回调函数, 它在 setup 函数中非常有用,可以确保在对 DOM 进行操作之前,先等待 Vue 完成相关的 DOM 更新。 下面是一个示例,演示了 await nextT…...
Matlab信号处理3:fft(快速傅里叶变换)标准使用方式
Fs 1000; % 采样频率 T 1/Fs; % 采样周期:0.001s L 1500; % 信号长度 t (0:L-1)*T; % 时间向量. 时间向量从0开始递增,0s~1.499sS 0.7*sin(2*pi*50*t) sin(2*pi*120*t); % 模拟原信号 X S 2*randn(size(t)); …...
Python|合并两个字典的几种方法
在Python中,有多种方法可以通过使用各种函数和构造函数来合并字典。在本文中,我们将讨论一些合并字典的方法。 1. 使用方法update() 通过使用Python中的update()方法,可以将一个列表合并到另一个列表中。但是在这种情况下,第二个…...
ElementUI浅尝辄止24:Message 消息提示
常用于主动操作后的反馈提示。与 Notification 的区别是后者更多用于系统级通知的被动提醒。 1.如何使用? Message 在配置上与 Notification 非常类似,所以部分 options 在此不做详尽解释,可以结合 Notification 的文档理解它们。Element 注…...
让照片动起来的软件,轻松制作照片动效
随着社交媒体的日益普及,我们对于照片的要求也越来越高。普通的照片已经不能满足我们的需求,我们希望照片更加生动有趣。照片动效便应运而生,它可以让照片动起来,吸引更多的注意力,让照片更加生动有趣。 照片动效制作起…...
【图解RabbitMQ-7】图解RabbitMQ五种队列模型(简单模型、工作模型、发布订阅模型、路由模型、主题模型)及代码实现
🧑💻作者名称:DaenCode 🎤作者简介:CSDN实力新星,后端开发两年经验,曾担任甲方技术代表,业余独自创办智源恩创网络科技工作室。会点点Java相关技术栈、帆软报表、低代码平台快速开…...
Linux命令200例:write用于向特定用户或特定终端发送信息
🏆作者简介,黑夜开发者,CSDN领军人物,全栈领域优质创作者✌。CSDN专家博主,阿里云社区专家博主,2023年6月csdn上海赛道top4。 🏆数年电商行业从业经验,历任核心研发工程师࿰…...
javaee spring整合mybatis spring帮我们创建dao层
项目结构 pom依赖 <?xml version"1.0" encoding"UTF-8"?><project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/P…...
修改Tomcat的默认端口号
1、找到Tomcat的安装路径。 2、打开conf文件夹。 3、用记事本打开server.xml文件 4、找到 <Connector port"8080" protocol"HTTP/1.1",其中的8080就是tomcat的默认端口,将其修改为你需要的端口即可。...
Open3D Ransac拟合空间直线(python详细过程版)
RANSAC拟合直线 一、算法原理1、算法简介2、参考文献二、代码实现三、结果展示本文由CSDN点云侠原创,原文链接。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫。 一、算法原理 1、算法简介 见:Open3D——RANSAC 三维点云空间直线拟合 2、参考文献...
题目:2729.判断一个数是否迷人
题目来源: leetcode题目,网址:2729. 判断一个数是否迷人 - 力扣(LeetCode) 解题思路: 对 n,2*n,3*n 中的数字出现次数计数,若数字 0 出现 0 次,数字 1~9…...
微服务模式:服务发现模式
由于微服务应用的动态性,很难调用具有固定 IP 地址的服务。这就是服务发现的概念出现的背景。服务发现有助于客户端了解服务实例的位置。在这种情况下,服务发现组件将充当服务注册表。 服务注册表是一个包含服务实例位置的集中式服务器/数据库。在微服务…...
9.4 数据库 TCP
#include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);//判断数据库对象是否包含了自己使用的数据库if(!db.contains("Stu.db")){//不存在数据库࿰…...
普通用户使用spark的client无法更新Ranger策略
普通用户使用spark的client无法更新Ranger策略 报错图片: WARN org.apache.ranger.admin.client.RangerAdminRESTClient: Error getting Roles. secureModetrue, usercaojianxiangUCDIPA.VIATRIS.CC (auth:KERBEROS),responsef"httpStatusCode&quo…...
Git超详细教程
文章目录 一、安装并配置Git二、Git的基本操作三、Github/GitLab/Gitee四、分支 一、安装并配置Git 查看所有的全局配置项 git config --list --global查看指定的全局配置项 git config user.name git config user.email配置用户信息 git config --global user.name "…...
C++ 回调函数
一、使用方法 1.定义一个函数指针 typedef int (*pCallback)(int a, int b);2.定义一个带参的回调函数(注释部分是普通回调函数,不用定义第一步里的函数指针) //带参 int oneCallback(int a, int b, pCallback p) //int oneCallback(int a, i…...
xilinx FPGA IOB约束使用以及注意事项
文章目录 一、什么是IOB约束二、为什么要使用IOB约束1、在约束文件中加入下面约束:2、直接在代码中加约束, 三、IOB约束使用注意事项 一、什么是IOB约束 在xilinx FPGA中,IOB是位于IO附近的寄存器,是FPGA上距离IO最近的寄存器&am…...
如何统计iOS产品不同渠道的下载量?
一、前言 在开发过程中,Android可能会打出来很多的包,用于标识不同的商店下载量。原来觉得苹果只有一个商店:AppStore,如何做出不同来源的统计呢?本篇文章就是告诉大家如何做不同渠道来源统计。 二、正文 先看一下苹…...
大模型学习
大模型 大规模语言模型(Large Language Model)简称,具有庞大的参数规模和复杂程度的机器学习模型。在深度学习领域,指具有数百万到数十亿参数的神经网络模型。 优点: 更强大、更准确的模型性能,可面对复杂…...
日照的网站建设/推广平台网站有哪些
数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关,数据结构有以下几种基本的结构算法…...
网站描述优化/做百度推广怎么做才能有电话
Activity进场和出场动画从MainActivity进入到SecondActivity,再点击返回键从SecondActivity进入到MainActivity这样一个过程中如何设置两个Activity创建和销毁的动画呢?第一步:在MainActivity设置Intent进入SecondActivity的代码:…...
做外贸网站选美国服务器的费用/电商的运营模式有几种
过滤器的区别 捕捉过滤器(CaptureFilters):用于决定将什么样的信息记录在捕捉结果中。需要在开始捕捉前设置。显示过滤器(DisplayFilters):在捕捉结果中进行详细查找。他们可以在得到捕捉结果后随意修改。那…...
杭州投资公司自适应网站/dw网页制作教程
SpringBoot入门一 推荐: Spring Boot系列文章Spring Boot基础教程Spring Boot参考指南springboottutorial 项目属性配置 参考: Spring Boot属性配置文件详解 可以使用properties文件,YAML文件配置。YAML文件相对来说更简洁一点。 如下…...
网站如何选择关键词/绍兴seo网站管理
https://www.runoob.com/linux/linux-vim.html这个里面比较详细。 1、vim是一种文本编辑器,为啥要学? 答:1)所有的UNIX Like 系统都会内置vi文本编辑器,其他的文本编辑器不一定存在; 2)很多软…...
营销型网站的三元素/seo关键词优化方法
将以下代码复制粘贴到txt文件中,另存为bat格式,并将文件编码格式修改为ANSI,跟要处理的文件放一个文件夹内运行。 代码中制定的是删除1,2行,可根据需求自行修改。 echo off rem 根据指定的行号范围删除多个txt文件里的连续多行内…...