【Java数组精通之路】:从创建到内存管理,全面掌握Java数组
发布时间: 2024-09-22 08:05:16 阅读量: 91 订阅数: 42
# 1. Java数组概述
Java数组是用于存储多个相同类型数据项的集合,它允许程序员通过一个变量名引用多个数据,从而提高编程效率。数组是Java中基本的数据结构之一,广泛应用于软件开发的各个方面。无论是在算法实现、数据处理还是框架开发中,数组都扮演着重要的角色。在这一章中,我们将简要介绍数组在Java编程语言中的重要性以及它的基本概念。随后,我们将在后续章节中深入探讨数组的创建、初始化、操作技巧以及最佳实践等方面。让我们开始深入了解数组的世界吧。
# 2. 数组的创建和初始化
## 2.1 数组的基本概念
### 2.1.1 数组的定义和声明
在Java中,数组是一种数据结构,用于存储固定大小的同类型元素。数组中的每个元素可以通过数组索引访问,索引从0开始。数组的声明包括指定数组类型以及变量名,例如:
```java
int[] myArray;
```
这里声明了一个名为`myArray`的数组,它可以存放整型元素。在Java中声明数组时,并不需要立即指定数组的大小,数组的大小在创建数组对象时确定。
### 2.1.2 数组的类型和结构
数组的类型指的是数组可以存放的元素类型。Java支持基本类型(如`int`, `double`, `char`等)和对象类型(如`String`, `Integer`, 自定义对象等)。数组可以是一维的也可以是多维的:
- **一维数组**:最常见,可以视为一系列有序的元素集合。
- **多维数组**:可以看作是数组的数组。例如,二维数组可以视为表格,每个数组元素是一个行数组。
数组的结构可以用以下方式定义:
```java
type[] arrayName;
```
其中`type`是数组元素的数据类型,`arrayName`是数组的标识符。
## 2.2 数组的创建和初始化过程
### 2.2.1 动态创建数组
创建数组的两种常见方式是静态初始化和动态初始化。动态初始化使用`new`关键字后跟数组类型和大小,例如:
```java
int[] numbers = new int[5];
```
这条语句创建了一个整型数组`numbers`,它包含5个元素,每个元素的初始值是0(对于整型数组,默认初始化值是0)。
### 2.2.2 数组元素的初始化
在创建数组时,可以同时初始化数组元素:
```java
int[] primes = new int[]{2, 3, 5, 7, 11};
```
这里我们创建了一个整型数组`primes`并立即初始化了它的元素为一组素数。
### 2.2.3 默认初始化值的探讨
如果创建数组时没有进行显式初始化,那么数组的元素将被自动初始化为默认值。对于不同的数据类型,这些默认值也不同:
- 对于数值类型,默认值为0。
- 对于`boolean`类型,默认值为`false`。
- 对于对象引用,默认值为`null`。
## 2.3 多维数组的创建与管理
### 2.3.1 多维数组的声明和创建
多维数组在声明时需要指定所有维度的大小,例如:
```java
int[][] matrix = new int[3][3];
```
这将创建一个3x3的二维数组,其中每个元素的默认初始化值为0。
### 2.3.2 多维数组的初始化技巧
创建多维数组时,可以只在最左边的维度指定大小,其他的维度大小会自动确定:
```java
int[][] jaggedArray = new int[3][];
jaggedArray[0] = new int[2];
jaggedArray[1] = new int[3];
jaggedArray[2] = new int[4];
```
这里我们创建了一个所谓的“锯齿数组”,其中每个子数组可以有不同的长度。
### 2.3.3 多维数组的应用场景
多维数组在现实世界中有广泛的应用,如存储矩阵、表格式数据等。以下是创建并初始化一个2x3的二维数组的代码:
```java
int[][] matrix = {
{1, 2, 3},
{4, 5, 6}
};
```
这里我们用了一种更简洁的初始化方式,直接在声明时指定数组的初始值。
为了更好地理解数组的结构和创建过程,我们可以通过表格展示不同类型的数组:
| 数组类型 | 初始化方式 | 描述 |
|----------|------------|------|
| 一维数组 | `int[] arr = new int[3];` | 创建一个包含3个整数的数组 |
| 多维数组 | `int[][] arr = new int[2][3];` | 创建一个2x3的二维整数数组 |
| 锯齿数组 | `int[][] arr = new int[3][];` | 创建一个包含3个不同长度的一维数组 |
以上表格描述了不同数组类型的初始化语法和它们的含义。
在本章节中,我们详细探讨了Java数组的基本概念、创建、初始化方法以及多维数组的处理技巧。通过理解这些基础知识,可以为后续章节中对数组进行高级操作和优化打下坚实的基础。
# 3. 数组操作详解
数组是编程中的基础数据结构之一,熟练掌握数组的操作是每个IT从业者必备的技能。本章将从数组的遍历方法、排序与搜索、以及与集合框架之间的转换等几个方面深入探讨数组的操作方法。
## 3.1 数组的遍历方法
数组的遍历是数组操作中最基础也是最常见的需求,它指的是按照一定的顺序访问数组中的每一个元素,直到数组末尾。常见的数组遍历方法包括使用for循环、foreach循环以及数组工具类。
### 3.1.1 使用for循环遍历
在传统的数组遍历方法中,for循环是使用得最为广泛的。它通过索引的方式访问数组的每一个元素,这需要编写者清楚地知道数组的长度和索引的范围,以避免数组越界。
```java
int[] numbers = {1, 2, 3, 4, 5};
for (int i = 0; i < numbers.length; i++) {
System.out.println(numbers[i]);
}
```
在这段代码中,通过for循环初始化一个数组并打印每一个元素。循环变量`i`从0开始,直到`numbers.length - 1`,保证了每次循环访问数组的下一个元素。
### 3.1.2 使用foreach循环遍历
Java提供了foreach循环,它能够简化遍历数组的过程。foreach循环自动遍历数组中的每个元素,无需手动处理索引。
```java
int[] numbers = {1, 2, 3, 4, 5};
for (int num : numbers) {
System.out.println(num);
}
```
在这段代码中,foreach循环直接将每个数组元素赋值给变量`num`,无需手动编写索引代码,使代码更加简洁易懂。
### 3.1.3 使用数组工具类遍历
Java标准库中的Arrays工具类提供了一些辅助方法来遍历数组。虽然这并不是遍历数组的首选方式,但在某些特定场景下可以提供便利。
```java
import java.util.Arrays;
int[] numbers = {1, 2, 3, 4, 5};
Arrays.stream(numbers).forEach(System.out::println);
```
在这段代码中,通过`Arrays.stream()`方法将数组转换成流对象,然后使用流的`forEach`方法进行遍历。尽管这种方式在内部仍然使用迭代,但对于习惯了函数式编程的开发者来说,这种方法显得更加现代和简洁。
## 3.2 数组的排序和搜索
数组的排序和搜索是数组操作中重要的内容,它们涉及到算法的知识。在Java中,数组排序可以通过Arrays类中的`sort()`方法实现,而搜索则可以通过Arrays类中的`binarySearch()`方法实现。
### 3.2.1 数组排序算法对比
Java提供了多种排序算法,包括冒泡排序、选择排序、插入排序、快速排序等。Java的Arrays类内部使用了优化后的快速排序算法。
```java
import java.util.Arrays;
int[] numbers = {3, 1, 4, 1, 5, 9};
Arrays.sort(numbers);
System.out.println(Arrays.toString(numbers)); // 输出排序后的数组
```
通过`Arrays.sort()`方法,可以轻松实现数组的排序。需要指出的是,排序算法的选择依赖于数组的大小和数据的特点。
### 3.2.2 实现自定义排序
Java允许开发者通过实现`Comparator`接口来实现自定义排序。
```java
import java.util.Arrays;
***parator;
String[] fruits = {"Banana", "Apple", "Orange"};
Arrays.sort(fruits, new Comparator<String>() {
@Override
public int compare(String fruit1, String fruit2) {
return fruit1.length() - fruit2.length();
}
});
System.out.println(Arrays.toString(fruits)); // 按长度排序后的数组
```
通过自定义`Comparator`实现,可以针对数组元素的具体情况实现特定的排序逻辑。
### 3.2.3 二分查找法的应用
当数组已经排好序时,可以使用二分查找法来加快查找速度。二分查找法适用于静态查找表,它通过比较数组中间元素与目标值的大小,从而缩小搜索范围。
```java
import java.util.Arrays;
int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int target = 7;
int index = Arrays.binarySearch(numbers, target);
if (index >= 0) {
System.out.println("找到目标值索引:" + index);
} else {
System.out.println("数组中不存在目标值。");
}
```
在这段代码中,`Arrays.binarySearch()`方法尝试找到目标值在数组中的索引。如果找到了目标值,返回其索引;如果没有找到,返回一个负数。
## 3.3 数组与集合框架的转换
数组和集合框架是Java中常用的两种数据结构,它们各有优缺点。在很多情况下,开发者需要在数组与集合之间进行转换。在Java中,Arrays类和Collection框架提供了相关的方法来实现数组和集合之间的转换。
### 3.3.1 数组转集合
将数组转换为集合可以使用`Arrays.asList()`方法,但是需要注意,`Arrays.asList()`返回的列表是固定大小的,不能进行添加或删除元素操作。
```java
import java.util.Arrays;
import java.util.List;
Integer[] numbers = {1, 2, 3, 4, 5};
List<Integer> numberList = Arrays.asList(numbers);
System.out.println(numberList); // 输出转换后的列表
```
在这段代码中,通过`Arrays.asList()`方法实现了数组到列表的转换。需要注意的是,如果数组是基本数据类型,则不能直接转换。
### 3.3.2 集合转数组
集合转数组可以通过`Collection.toArray()`方法实现。这个方法需要指定一个数组参数,用于存放转换后的元素。
```java
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
List<Integer> numberList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
numberList.add(i + 1);
}
Integer[] numbers = numberList.toArray(new Integer[0]);
System.out.println(Arrays.toString(numbers)); // 输出转换后的数组
```
在这段代码中,使用`toArray()`方法将列表转换为数组。需要注意的是,如果指定的数组长度小于列表元素的个数,`toArray()`方法会自动创建一个足够大的数组。
### 3.3.3 性能考量与选择
在进行数组与集合之间的转换时,需要考虑到性能因素。例如,当频繁在数组和集合间进行转换时,应当考虑使用缓存机制来优化性能,避免重复创建不必要的数据结构。
```java
// 假设有一个频繁执行的转换操作
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
// 将数组转换为列表,进行一些操作
// 将列表转换回数组
}
```
如果这样的操作频繁发生,应当考虑使用如`LinkedList`这种允许动态数组与列表之间快速转换的数据结构,或者直接使用数组以减少内存分配和垃圾回收的开销。
通过上述的详细解析,我们了解了数组的遍历方法、排序和搜索机制,以及数组和集合之间的转换方式。这些操作是构建更复杂数据处理功能的基础,因此它们值得我们投入时间和精力去深刻理解和掌握。
# 4. 数组的高级特性与最佳实践
## 4.1 泛型与数组
在Java中,泛型提供了编译时的类型安全检查机制,可以用来实现类型参数化。然而数组和泛型在Java中的某些特性之间存在冲突。接下来,将详细探讨如何在数组和泛型之间进行平衡。
### 4.1.1 泛型数组的创建与限制
尽管Java允许使用泛型,但直接创建泛型数组是不允许的。这是由于泛型类型信息在运行时被擦除,导致数组需要在运行时知道其元素的确切类型。代码如下示例:
```java
// 以下是错误的示例,尝试创建一个泛型数组
List<String>[] arrayOfLists = new ArrayList<String>[2]; // 编译错误
```
这段代码尝试创建一个泛型数组,但是在编译阶段会报错,因为Java编译器无法保证类型安全。为了解决这个问题,我们可以使用类型擦除的特性:
```java
// 使用Object数组代替泛型数组
Object[] strings = new String[2];
strings[0] = "Hello";
strings[1] = "World";
// 下面的代码无法通过编译,因为类型不匹配
// strings[0] = 123; // 编译错误
```
### 4.1.2 泛型在数组中的实际应用案例
考虑到直接创建泛型数组的限制,实际应用中经常通过使用`ArrayList`或者`LinkedList`来代替泛型数组,因为它们在内部是通过数组来实现的,并且能够提供类型安全。示例代码如下:
```java
// 使用ArrayList代替泛型数组
List<String>[] arrayOfLists = new ArrayList[2];
arrayOfLists[0] = new ArrayList<String>();
arrayOfLists[1] = new ArrayList<String>();
arrayOfLists[0].add("Hello");
arrayOfLists[1].add("World");
// 现在可以安全地访问数组元素
System.out.println(arrayOfLists[0].get(0)); // 输出:Hello
System.out.println(arrayOfLists[1].get(0)); // 输出:World
```
使用`ArrayList`使得类型安全得到保证,同时也解决了直接创建泛型数组的问题。
## 4.2 数组的内存管理
由于数组在Java中是一种静态数据结构,其内存管理也相对简单。我们将探索数组对象如何在堆内存中进行分配和如何避免潜在的内存泄漏。
### 4.2.1 堆内存与数组对象
在Java中,所有的对象都是在堆内存中分配空间的,包括数组对象。堆内存是JVM管理的内存区域之一,用于存储所有Java对象实例,包括数组。了解堆内存和数组对象之间的关系,可以帮助我们更好地进行内存管理和优化。
### 4.2.2 引用类型与数组内存分配
数组内存分配通常涉及到引用类型和基本类型的差异。基本类型的数组存储的是数据本身,而引用类型的数组存储的是指向对象的引用。
### 4.2.3 如何避免内存泄漏
由于数组是静态分配的,如果创建了大型数组但未能适当地进行垃圾收集,那么可能会导致内存泄漏。合理使用`System.gc()`来建议JVM进行垃圾收集,以及使用软引用、弱引用等来避免长期持有的大型数组不被垃圾收集器回收。
## 4.3 数组的性能优化策略
性能在许多场景中都是至关重要的,特别是在处理大量数据时。数组因其连续内存分配的特点,常常在性能上有优势。这里将探讨如何通过优化数组来提高性能。
### 4.3.1 循环展开技术
循环展开是一种常见的性能优化技术,通过减少循环的迭代次数,减少循环控制开销。例如,将一个简单的for循环展开为两个for循环,可以提高性能。
```java
// 原始for循环
for (int i = 0; i < 100; i++) {
// 执行操作...
}
// 循环展开
for (int i = 0; i < 50; i++) {
// 执行操作...
// 执行操作...
}
```
循环展开通常需要根据实际情况进行调整,因为过度展开可能会导致代码复杂且难以维护。
### 4.3.2 代码剖析与性能监控
为了优化数组操作的性能,首先需要了解当前的性能瓶颈。使用代码剖析工具可以识别出性能热点,并据此进行针对性的优化。例如,Java提供了JProfiler、VisualVM等工具。
### 4.3.3 算法优化与内存使用平衡
在进行数组操作时,算法的选择对性能有重要影响。选择合适的算法,并优化内存使用,可以在保证性能的同时避免不必要的内存浪费。例如,在数组排序时选择快速排序而不是冒泡排序。
在实际操作中,需要综合考虑数据量、算法复杂度和内存使用情况来找到最佳的性能优化方案。
在本章节中,我们深入探讨了泛型和数组的结合、数组的内存管理以及性能优化的策略。理解了泛型数组创建的限制,内存分配的原理以及如何避免内存泄漏。此外,我们还了解了一些关键的性能优化技术,比如循环展开、代码剖析和算法优化,并强调了在性能和内存使用之间的平衡。这些高级特性与最佳实践对于开发高性能和内存效率的应用程序至关重要。
# 5. 数组与并发编程
## 5.1 并发环境下数组的使用
在Java中,数组作为一种基础的数据结构,在并发编程的场景下使用时,必须考虑到线程安全的问题。数组本身并不具备线程安全的特性,因此,在并发环境下操作数组需要特别注意。
### 5.1.1 线程安全的数组操作
为了在多线程环境下安全地操作数组,我们可以使用同步机制,比如使用`synchronized`关键字或者显式锁`java.util.concurrent.locks.Lock`。这样可以保证同一时间只有一个线程能够访问数组,防止数据不一致的问题。
```java
synchronized void safeArrayAccess(int[] array) {
// 线程安全地访问和修改数组元素
for(int i = 0; i < array.length; i++) {
array[i] += 1; // 增加元素值
}
}
```
在上述代码中,通过`synchronized`关键字修饰的方法,确保了每次只有一个线程可以执行此方法内的代码,从而保证了数组操作的线程安全。
### 5.1.2 使用并发集合处理数组
除了使用同步机制来确保线程安全外,我们还可以利用Java提供的并发集合来处理数组。例如,使用`java.util.concurrent.CopyOnWriteArrayList`,这是一个线程安全的列表,它在每次修改时都会创建并复制底层数组。它适用于读多写少的场景。
```java
CopyOnWriteArrayList<Integer> cowList = new CopyOnWriteArrayList<>(Arrays.asList(array));
```
这里我们首先将数组转换为列表,然后使用`CopyOnWriteArrayList`来处理并发访问。这样,每次修改列表时,实际上是在创建一个新的数组副本进行修改,这样就避免了并发修改异常(`ConcurrentModificationException`)。
## 5.2 并行流处理数组
Java 8引入了Stream API,它提供了一种高级的方式来进行数组和集合的并行处理,极大地简化了并发处理的过程。
### 5.2.1 并行流基础
并行流可以自动地利用多核处理器的优势来加速数据处理。我们只需要在流上调用`parallel()`方法,就可以将顺序流转换为并行流。
```java
int[] numbers = {1, 2, 3, 4, 5};
int sum = Arrays.stream(numbers).parallel().sum();
```
上述代码展示了如何计算一个整型数组元素的总和,通过调用`parallel()`方法使得操作并行化。
### 5.2.2 并行流的性能分析
使用并行流虽然可以提升性能,但是并不总是最佳选择。并行流的性能受多种因素影响,包括数据集的大小、计算机的CPU核心数、操作的复杂度等。有时候,对于小数据集,使用并行流反而会由于线程调度和同步的开销导致性能下降。
因此,在使用并行流之前,建议进行适当的性能测试。我们可以使用Java的`jvisualvm`工具或`JMH`基准测试框架来分析并行流的性能。
### 5.2.3 使用并行流进行数组操作
并行流特别适用于元素独立操作且计算密集型的任务。例如,数组中的每个元素独立计算其平方并求和。
```java
int[] numbers = {1, 2, 3, 4, 5};
int sumOfSquares = Arrays.stream(numbers)
.parallel()
.map(n -> n * n)
.sum();
```
这里使用了`map`操作来计算每个元素的平方,随后通过`sum`操作求得所有平方数的总和。并行流会自动将任务分配到不同的处理器核心上执行。
并行流是Java并发编程中非常有用的一个工具,它可以使我们更方便地利用多线程的优势来处理数据,但是也需要注意它的适用场景和潜在的性能影响。在使用时,应当结合具体的业务逻辑和计算负载,进行适当的性能评估和优化。
# 6. 数组在实际项目中的应用
## 6.1 数组在算法中的应用
数组是算法设计中不可或缺的基础数据结构之一。它们在各种算法的实现中扮演着核心角色,无论是用于存储临时数据,还是作为算法的主要数据存储结构。下面将通过案例分析,展示数组如何在算法设计中发挥作用。
### 6.1.1 数组排序算法案例分析
在算法竞赛和实际软件开发中,数组排序是经常遇到的问题。排序算法的种类繁多,包括冒泡排序、选择排序、插入排序、快速排序、归并排序等。它们各有优劣,适用于不同的场景。
例如,快速排序是目前公认最快的通用排序算法之一。它利用分治法的策略,将大问题分解为小问题来解决,时间复杂度平均为O(n log n)。快速排序算法通过递归的方式,选择一个元素作为pivot(基准),然后将数组中小于pivot的元素放在左边,大于pivot的元素放在右边。
下面是一个快速排序算法的简单实现代码:
```java
public class QuickSort {
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
// 找到pivot的正确位置
int pivotIndex = partition(arr, low, high);
// 对pivot左边的子数组进行快速排序
quickSort(arr, low, pivotIndex - 1);
// 对pivot右边的子数组进行快速排序
quickSort(arr, pivotIndex + 1, high);
}
}
private static int partition(int[] arr, int low, int high) {
// pivot为数组最后一个元素
int pivot = arr[high];
int i = (low - 1); // 小于pivot的元素的索引
for (int j = low; j < high; j++) {
// 如果当前元素小于或等于pivot
if (arr[j] <= pivot) {
i++;
// 交换arr[i]和arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 交换arr[i+1]和arr[high](或pivot)
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return i + 1;
}
}
```
### 6.1.2 数据处理中的数组应用
在数据处理中,数组可以用来统计频率、处理时间序列数据等。例如,计数排序算法适用于一定范围内的整数排序。计数排序算法不基于比较,而是通过统计每个元素的出现次数(计数)来确定元素的位置。
下面是一个计数排序算法的实现代码:
```java
public class CountingSort {
public static void countingSort(int[] arr, int max) {
// 计数数组,用于存放每个元素出现的次数
int[] count = new int[max + 1];
// 存放排序结果的数组
int[] output = new int[arr.length];
// 计算每个元素出现的次数
for (int value : arr) {
count[value]++;
}
// 根据计数数组,计算每个元素在输出数组中的位置
for (int i = 1; i <= max; i++) {
count[i] += count[i - 1];
}
// 构建排序后的数组
for (int i = arr.length - 1; i >= 0; i--) {
output[count[arr[i]] - 1] = arr[i];
count[arr[i]]--;
}
// 复制到原数组
System.arraycopy(output, 0, arr, 0, arr.length);
}
}
```
## 6.2 数组在框架和库中的应用
在很多流行的编程框架和库中,数组被广泛用作底层数据结构。了解这些用法,能够帮助开发者更好地利用现有工具,并为设计自己的软件提供灵感。
### 6.2.1 框架中数组的应用实例
在Java的Spring框架中,数组被用于许多组件的配置。例如,在Spring Security中,配置角色权限时,通常会定义一个数组来存储权限值。
```java
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.and()
.formLogin();
}
}
```
### 6.2.2 Java标准库中的数组使用技巧
Java标准库中有许多使用数组的方法。例如,`java.util.Arrays`类提供了很多实用的数组操作方法。它可以用来复制数组、排序数组、填充数组、比较数组等。
```java
import java.util.Arrays;
public class ArraysExample {
public static void main(String[] args) {
int[] array = {1, 3, 5, 7, 9};
// 排序数组
Arrays.sort(array);
// 填充数组
Arrays.fill(array, 2);
// 输出数组内容
System.out.println(Arrays.toString(array));
}
}
```
## 6.3 数组的常见问题与解决方案
数组作为一种基本的数据结构,在使用过程中可能会遇到各种问题。了解这些问题的解决方法,对于提高编程能力和解决实际问题至关重要。
### 6.3.1 常见数组错误类型
在使用数组时,常见的错误类型包括数组越界、空指针异常、内存泄漏等。
- **数组越界**:访问数组时,索引超出了数组的有效范围。
- **空指针异常**:在访问数组元素之前没有正确地检查数组是否为`null`。
- **内存泄漏**:在使用数组存储大量数据时,如果不再使用数组中的数据,需要及时清理引用,避免内存泄漏。
### 6.3.2 调试技巧与错误预防
要有效预防和调试数组相关的错误,可以采取以下措施:
- **始终检查数组边界**:在访问数组之前,确保索引在合法范围内。
- **初始化数组为null或默认值**:在使用数组之前,先将其初始化为`null`或合适的默认值,避免空指针异常。
- **合理利用IDE功能**:现代集成开发环境(IDE)提供了强大的工具,如断点、条件表达式等,可以有效帮助开发者定位问题。
- **内存泄漏监控**:利用性能分析工具定期检查应用程序是否存在内存泄漏,特别是在使用大型数组时。
通过掌握这些技巧和方法,开发者可以更加自信地应对数组相关的编程挑战,并在实际项目中实现更加稳定、高效的应用程序。
0
0