Java:数组的定义和使用(万字解析)
目录
1. 数组的概念
2. 数组的基础知识
2.1 数组的创建
\1. 基础创建格式:
\2. 类似C语言的创建格式:
【错误的创建(初始化)格式】
2.2 数组的数据类型
2.3 数组的初始化 —— 两种方式
\1.动态初始化:(完全默认初始化)
\2. 静态初始化:
【注意事项】
3. 数组的使用
3.1 数组元素的下标访问
3.2 计算数组长度
3.3 循环遍历数组的2种方式
\1. for语句
\2. for - each语句
【两种遍历方式的区别】
4. 数组是引用类型(内存空间的底层知识)
4.1 JVM的内存区域划分
4.2 内存分配:基础类型的变量 和 引用类型的变量
4.3 数组大小可变吗?
4.4 “数组1 = 数组2”会发生什么?
4.5 空引用null
5. 数组 与 方法
5.1 数组作为方法参数
5.2 数组作为方法返回类型
6. Arrays的一些实用工具方法
6.1 数组转字符串
6.2 数组的拷贝
6.3 数组的排序
6.4 二分查找指定元素
6.5 判断两个数组是否相等
6.6 填充数组元素
1. 数组的概念
数组:可以看成是相同类型元素的一个集合。在内存中是一段连续的空间。
1. 数组中存放的元素其类型相同。
2. 数组的空间是连在一起的。
3. 每个空间有自己的编号,起始位置的编号为0,即数组的下标。
2. 数组的基础知识
首先要明确一点,数组在Java中属于引用类型,引用类型变量的创建都需要new出来。
2.1 数组的创建
\1. 基础创建格式:
T[ ] 数组名 = new T[N];
- T:表示数组中存放元素的类型
- T[ ]:表示数组的类型
- N:表示数组的长度
例如:
- int[] array1 = new int[10]; // 创建一个可以容纳10个int类型元素的数组
- double[] array2 = new double[5]; // 创建一个可以容纳5个double类型元素的数组
- String[] array3 = new String[3]; // 创建一个可以容纳3个字符串元素的数组
数组的创建也可以类似C语言那样,把方括号[]写在变量名的右边。
\2. 类似C语言的创建格式:
T 数组名[ ] = new T[N];
- T:表示数组中存放元素的类型
- T[ ]:表示数组的类型
- N:表示数组的长度
例如:
- int array1[] = new int[10]; // 创建一个可以容纳10个int类型元素的数组
- double array2[] = new double[5]; // 创建一个可以容纳5个double类型元素的数组
- String array3[] = new String[3]; // 创建一个可以容纳3个字符串元素的数组
【错误的创建(初始化)格式】
- 没有new一个数组对象。
- 将数组长度的定义写在了第一个方括号[ ]里面。
例如:
2.2 数组的数据类型
注意:java中数组的数据类型是T[],而不是T[n]。
【在C语言中,数组的数据类型是T[n]】
比如现在有1个元素个数为3的整型数组“int[] arr = new int[3]”,那么该数组arr的数据类型是int[ ],而不是int[3]。
2.3 数组的初始化 —— 两种方式
数组的初始化主要分为动态初始化以及静态初始化。
\1.动态初始化:(完全默认初始化)
在创建数组时,直接指定数组中元素的个数N。创建后,从0到N-1的数组元素都被默认初始化。
语法格式:T[ ] 数组名 = new T[N];
例如:int[] array = new int[10];
- 如果数组中存储元素类型为基础类型,默认值为基础数据类型对应的默认值,比如:
\u
是用来表示Unicode转义字符的前缀。它的格式为\u
后面跟着四个十六进制数字,用于表示一个特定的Unicode字符。例如:
\u0041
表示字符A
,因为0041
是A
的Unicode编码。
- 如果数组中存储元素类型为引用类型,默认值为null
\2. 静态初始化:
在创建数组时不直接指定数据元素个数,而直接将具体的数据内容进行指定。
(完全格式)语法格式①:T[ ] 数组名 = new T[ ]{data1, data2, data3, ..., datan}; (左边也可以写成“T 数组名[ ]”)
(省略格式)语法格式②:T[ ] 数组名 = {data1, data2, data3, ..., datan}; (左边也可以写成“T 数组名[ ]”)
【这写法类似C语言的数组完全初始化,虽然看上去省去了new T[ ],但是编译器编译代码时还是会还原】
例如:
格式1的静态初始化:
- int[] array1 = new int[]{0,1,2,3,4,5,6,7,8,9};
- String[] array3 = new String[]{"hell", "Java", "!!!"};
格式2的静态初始化:
- int[] array1 = {0,1,2,3,4,5,6,7,8,9};
- double[] array2 = {1.0, 2.0, 3.0, 4.0, 5.0};
- String[] array3 = {"hell", "Java", "!!!"};
【注意事项】
- 1.静态和动态初始化也可以分为两步,但是省略格式不可以。【类似C语言的初始化格式不能分成两步,而且这在C语言中也是不可以的】
int[] array1;array1 = new int[10];int[] array2;array2 = new int[]{10, 20, 30};// 注意省略格式不可以拆分, 否则编译失败 // int[] array3;// array3 = {1, 2, 3};
- 2. 在使用省略格式的静态初始化时,浮点数数组不能用float[ ]类型接收,整数无限制。(所以不太建议使用省略格式)
- 3. 数组的初始化不能既是动态初始化,又是静态初始化。(同时也说明:Java中不能像C语言那样不完全初始化。)
这里既直接指定了数组的大小是5(动态初始化),又用大括号来指定数组大小(静态初始化),所以报错了。
3. 数组的使用
3.1 数组元素的下标访问
数组在内存中是一段连续的空间,空间的编号都是从0开始的,依次递增,该编号称为数组的下标,数组可以通过 下标访问其任意位置的元素。比如:
int[]array = new int[]{10, 20, 30, 40, 50};System.out.println(array[0]);System.out.println(array[1]);System.out.println(array[2]);System.out.println(array[3]);System.out.println(array[4]);// 也可以通过[]对数组中的元素进行修改
array[0] = 100;System.out.println(array[0]);
tips:在Java中,方括号 []
确实用于访问数组中的元素,但它并不被称为“下标访问运算符”或者索引运算符。这与其他支持运算符重载的语言有所不同。Java将 []
视为数组访问的直接语法,仅用于数组,并没有定义为运算符或允许重载。
【注意事项】
- 数组是一段连续的内存空间,因此支持随机访问,即通过下标访问快速访问数组中任意位置的元素。
- 下标从0开始,介于[0, N)之间不包含N,N为元素个数,不能越界,否则会报出下标越界异常。(java.lang.ArrayIndexOutOfBoundsException异常代表数组越界访问)
3.2 计算数组长度
在数组中可以通过“ 数组对象.length ”来获取数组的长度。
例如:
输出:
为什么可以这样获得数组的长度?(简单了解)
- 我们注意到,我们用到了一个符号".",这个符号是成员访问运算符,用法就类似C语言中的结构体。Java中的对象就类似C语言的结构体(但会多了很多复杂的语法,比如对象中可以有方法)
- 其实在Java中使用数组时,会自动导入“java.lang.reflect.Array”类。我们new的int[10]或String[4]等等,其实是new了(创建了)一个Array类型的对象。
- 而Array类中有一个成员变量length,它记录着数组的长度。【可以简单得理解为C语言中的顺序表】
3.3 循环遍历数组的2种方式
\1. for语句
这是最简单的方法,例如:
int[] arr = new int[10];
for(int i = 0; i < arr.length; i++){System.out.println(arr[i]);
}
\2. for - each语句
for-each 是 for 循环的另外一种使用方式。
语法格式:
for (元素类型 变量名 : 数组名) {
// 循环体
}
- “元素类型”是数组中元素的类型
- “变量名”是在每次迭代中用来引用当前元素的变量
- “数组名”是要遍历的数组。
注意:迭代变量与数组名之间有一个冒号“ : ”。
例如:
int[] arr = new int[10]; for(int x: arr){System.out.println(x); }
- arr数组中的每个元素都是int型,所以创建的变量x是int类型的。
- 第一次循环中,x等于arr[0];第二次循环中,x等于arr[1]……以此类推。
【两种遍历方式的区别】
循环条件控制:
- for语句中的循环条件由程序员编写,可以完成更复杂的要求,但也容易出错。
- for-each语句不需要手动管理索引,使得代码的意图更加明确,减少了出错的可能性,但循环的条件不能手动改变。
元素访问方式:
- for循环中通过访问数组下标会直接对数组元素进行操作。
- for-each循环通过迭代变量间接访问数组元素,即使发生错误操作也不会改变原数组的数据。
4. 数组是引用类型(内存空间的底层知识)
4.1 JVM的内存区域划分
JVM对所使用的内存按照功能的不同进行了划分,主要分为5的区域:
- 程序计数器 (PC Register): 只是一个很小的空间, 保存下一条执行的指令的地址。
- 虚拟机栈(JVM Stack): 与方法调用相关的一些信息,每个方法在执行时,都会先创建一个栈帧,栈帧中包含有:局部变量表、操作数栈、动态链接、返回地址以及其他的一些信息,保存的都是与方法执行时相关的一 些信息。比如:局部变量。当方法运行结束后,栈帧就被销毁了,即栈帧中保存的数据也被销毁了。(局部变量的存储、方法栈帧空间的开辟和销毁就在这里)
- 本地方法栈(Native Method Stack): 本地方法栈与虚拟机栈的作用类似。只不过保存的内容是Native方法的局部变量。在有些版本的 JVM 实现中(例如HotSpot), 本地方法栈和虚拟机栈是一起的。
- 堆区(Heap): JVM所管理的最大内存区域。使用 new 创建的对象都是在堆上保存 ,堆是随着程序开始运行时而创建,随着程序的退出而销毁。在运行时,堆中的数据只要还有在使用,就不会被销毁。
- 方法区(Method Area): 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法编译出的的字节码就是保存在这个区域。
补充一点:字节码是与平台无关的中间代码表示形式。字节码是Java平台核心特性之一,它的存在使得Java程序具备了高度的可移植性和灵活性。
4.2 内存分配:基础类型的变量 和 引用类型的变量
基本数据类型创建的变量,该变量空间中直接存放的是其所对应的数值; 而引用数据类型创建的变量,一般称为对象的引用,其空间中存储的是对象所在空间的地址。
例如:
public static void func() {int a = 10;int b = 20;int[] arr = new int[]{1,2,3};}
- 在上述代码中,a、b、arr,都是函数内部的局部变量,因此其空间都在func方法对应的栈帧中分配。(即它们的空间在栈区上)
- a、b是内置类型的变量,因此其空间中保存的就是给该变量初始化的值。
- array是数组类型的引用变量,其内部保存的内容可以简单理解成是数组在堆空间中的首地址,而数组的本体在堆区连续存放。
引用变量并不直接存储对象本身,可以简单理解成存储的是对象在堆中空间的起始地址。通过该 地址,引用变量便可以去操作对象。有点类似C语言中的指针,但是Java中引用要比指针的操作更简单。
4.3 数组大小可变吗?
先说结论:数组的大小是固定的,一旦创建无法改变其大小。
public static void main(String[] args) {int arr[] = new int[10];System.out.println(arr);arr = new int[5];System.out.println(arr);}
可以发现,虽然我们用new的方式把数组arr的大小从10改变成了5,但前后两次arr存储的地址值不同了。
这说明数组的大小是无法改变的,每次使用new时都会重新创建一个新的数组。
4.4 “数组1 = 数组2”会发生什么?
- arr1 = arr2,相当于把arr2所引用的地址交给了arr1,所以现在arr1和arr2都指向了同一个数组。
- arr1现在改变了引用的对象,如果arr1原来所引用的数组没有被其他引用变量引用,则系统会自动回收该堆上的空间。
【C语言中不支持“数组1 = 数组2”的语法】
例如:
public static void func() {int[] array1 = new int[]{10,20,30};int[] array2 = new int[]{1,2,3,4,5};int[] t = array1;array1 = array2;array2 = t;for(int x:array1){System.out.print(x+" ");}System.out.println();for (int x:array2){System.out.print(x+" ");}}
运行结果:
可以发现array1和array2所引用的对象成功被交换。
【注意事项】
在对象交换的过程中,数组array1、数组array2和临时变量 t 都必须是同类型的。(刚刚的例子中它们都是int[]类型的变量)
4.5 空引用null
null 在 Java 中表示 "空引用" , 也就是一个不指向对象的引用.
int[] arr = null;System.out.println(arr[0]);// 执行结果
Exception in thread "main" java.lang.NullPointerExceptionat Test.main(Test.java:6)
null 的作用类似于 C 语言中的 NULL (空指针),都是表示一个无效的内存位置。 因此不能对这个内存进行任何读写操作。 一旦尝试读写,就会抛出 NullPointerException。
注意事项:
1. 当引用类型的变量没有new一个对象的时候,必须要赋值null。
2. null不能写成NULL,这是因为:
- NULL是“ (void*)0 ”的宏定义,在Java中既没有宏定义的语法,也没有指针的语法。
5. 数组 与 方法
5.1 数组作为方法参数
数组作为方法参数时,形参数组要与实参数组的数组类型一致。
看一个例子:
public static void main(String[] args) {int[] arr = {1, 2, 3};func(arr);System.out.println("arr[0] = " + arr[0]);}public static void func(int[] a) {a[0] = 10;System.out.println("a[0] = " + a[0]);}// 执行结果
a[0] = 10
arr[0] = 10
可以发现,在func方法内部修改数组的内容, 方法外部的数组内容也发生改变。
与C语言类似:数组作为参数传递的也是数组的地址。
但不同的是:
- C语言中的形参数组是一个“伪数组”,本质上是一个指针变量,所以形参的类型可以用指针类型来代替;
- 而Java中的形参数组是一个“真数组”,本质上是数组类型的引用变量,所以形参的类型不能用其他引用类型来代替。
将数组的地址传入到函数形参中,这样可以避免对整个数组的拷贝(数组可能比较长, 那么拷贝开销就会很大)。
5.2 数组作为方法返回类型
数组作为返回值,一般都是在方法内部new了一个数组对象,然后返回该对象的地址。
例如:
public static int[] makeIntArray(int n){int[] arr = new int[n];return arr;}public static void main(String[] args) {int[] array = makeIntArray(10);}
这种用法就类似C语言中,给顺序表中的数组用malloc函数或colloc函数创建数组一样。
6. Arrays的一些实用工具方法
Java为我们提供了一个专门处理数组的工具类Arrays,里面有一系列实用的方法。
【注意】我们本章使用的工具类是Arrays类,不是Array类:
- 数组类型:是Java语言的核心部分,没有特定的包路径。
Array
类:位于java.lang.reflect
包中,主要用于反射机制中的数组操作。Arrays
类:位于java.util
包中,提供了更多实用的数组操作方法,适用于日常开发中的数组处理需求。它们三者都是引用类型,前者是数组类型,后两者属于类类型;三者均无包含关系。
- 即使没有显式导入
Array
类,你仍然可以创建和操作数组,这是因为数组是Java语言的核心部分。- 不过Arrays需要显式导入“import java.util.Arrays”。
6.1 数组转字符串
toString方法可以把数组的信息转换为字符串。
- 参数是要被转化数组
- 返回类型是字符串。
- 原数组的信息不会被修改。
例如:
public static void main(String[] args) {int[] arr = new int[]{1,2,3,4,5};String str = Arrays.toString(arr);//把数组转换为字符串,由字符串str接收//打印数组被转换后的结果System.out.println(str);//打印原数组for(int x: arr){System.out.print(x+" ");}}
输出结果:
模拟实现Arrays.toString
public static String myToString(int[] arr){String str = "[";for(int i = 0; i < arr.length; i++){str += arr[i];if(i != arr.length - 1){ //最后一个元素不要加逗号str += ",";}}str += "]";return str;}
public static void main(String[] args) {int[] arr1 = new int[]{1,2,3,4,5};String str = myToString(arr1);System.out.println(str);}
输出:
6.2 数组的拷贝
1. 两个参数的copyOf (数组名arr,拷贝长度N):
- 在方法中新创建一个长度为N的数组,把arr中的数据拷贝到该方法中的数组,最后返回该数组的地址。
- 从序号0一直拷贝到下标N-1处。
- 如果长度N大于arr的大小,则用默认值填充。
public static void main(String[] args) {int[] arr1 = new int[]{1,2,3,4,5};int[] arr2 = Arrays.copyOf(arr1,7);for(int x: arr2){System.out.print(x+" ");}}
输出:
2. 三个参数的copyOfRange(数组名arr,整数1,整数2):
- 使用方式与上面的方法类似,区别是该方法是范围拷贝。
- 拷贝范围是 [ 整数1,整数2 )。(左闭右开)
- 拷贝的范围如果超出arr数组的右边,则用用默认值填充。
例如:
public static void main(String[] args) {int[] arr1 = new int[]{1,2,3,4,5};int[] arr2 = Arrays.copyOfRange(arr1,3,7); //拷贝区间[3,7)for(int x: arr2){System.out.print(x+" ");}}
输出:
6.3 数组的排序
使用sort方法可以为数组排序
例如:
public class Blog {public static void main(String[] args) {int[] arr = {9, 5, 2, 7};Arrays.sort(arr);System.out.println(Arrays.toString(arr));}
}
6.4 二分查找指定元素
1. 两个参数的binarySearch (数组名arr, 要查找的元素key):
- 返回值 是key所在的序号
- 如果找不到该元素,则会返回一个负数。
例如:
public static void main(String[] args) {int[] arr = {0,1,2,3,4,5,6,7,8,9};int pos = Arrays.binarySearch(arr,5);System.out.println("要查找的元素位于序号"+pos);}
二分查找方法的模拟
public static int myBinarySearch(int[] arr, int key){int left = 0;int right = arr.length-1;while(left <= right) { //如果left不能等于right的话,那么当key在最右边时,会误以为找不到该元素int mid = (left + right) / 2;if(arr[mid] > key){right = mid - 1;//此时要找的数据在mid左边,right变成mid-1,则区间[left,right]就位于mid左边了}else if(arr[mid] < key){left = mid + 1;//此时要找的数据在mid右边,left变成mid+1则区间[left,right]就位于mid右边了}else{return mid;}}return -1;}
public static void main(String[] args) {int[] arr = {0,1,2,3,4,5,6,7,8,9};int pos = myBinarySearch(arr,9);System.out.println("要查找的元素位于序号"+pos);}
2. 四个参数的binarySearch (数组名arr, 左边界k1, 右边界k2, 要查找的元素key):
- 效果与上一个方法类似,区别是在规定范围内查找,区间[ k1, k2 )。(左闭右开)
例如:
public static void main(String[] args) {int[] arr = {0,1,2,3,4,5,6,7,8,9};int pos = Arrays.binarySearch(arr,6,9,9);//区间[6,9)System.out.println("要查找的元素位于序号"+pos);pos = Arrays.binarySearch(arr,6,10,9);//区间[6,10)System.out.println("要查找的元素位于序号"+pos);}
6.5 判断两个数组是否相等
使用equals(数组1,数组2)可以判断两个数组是否相等。
- 相等返回true,不相等返回false。
例如:
public static void main(String[] args) {int[] arr = new int[]{1,2,3,4};int[] arr1 = new int[]{1,2,3,4};int[] arr2 = new int[]{1,2,3,3};System.out.println(Arrays.equals(arr, arr1));System.out.println(Arrays.equals(arr, arr2));}
6.6 填充数组元素
1. 两个参数的 fill (数组名arr,填充值m):
- 用填充值m对数组arr中的所有元素进行赋值。
- 该方法会直接操作数组arr。
例如:
public static void main(String[] args) {int[] arr = new int[]{1,2,3,4};Arrays.fill(arr,5);System.out.println(Arrays.toString(arr));}
2. 四个参数的 fill (数组名arr,左边界k1,右边界k2,填充值m):
- 范围填充,区间是[ k1, k2 )。
例如:
public static void main(String[] args) {int[] arr = new int[]{1,2,3,4};Arrays.fill(arr,1,3,6);System.out.println(Arrays.toString(arr));}
总的来说,数组作为Java编程中最基本且常见的数据结构之一,其重要性不言而喻。通过深入了解数组的特点、用法以及在实际应用中的优势,开发者可以更好地应用它来解决问题,提高代码的效率和可读性。
本期分享完毕,谢谢大家的支持Thanks♪(・ω・)ノ