1、数组的基本概念
1.1、为什么使用数组
为了方便将同一种数据类型的数据进行存储。
1.2、什么是数组
指的是一种容器,可以同来存储同种数据类型的多个值。但是数组容器在存储数据的时候,需要结合隐式转换考虑。如:定义一个int
类型的数组,则byte
类型,short
类型,int
类型的数据是可以存到这个数组里面的。
数组可以看成是相同数据类型元素的一个集合。在内存中占用一段连续的内存空间。
【数组的特点】
- 数组中存放的元素数据类型是相同的。
- 数组所占的内存空间是连续的。
- 每个内存空间有自己编号,开始位置编号为 0,也就是数组的下标。
1.3、数组的创建和初始化
1.3.1、数组的创建
【语法格式】
数据类型[] 数组名 = new 数据类型[N];
数据类型:表示数组中存放元素的类型
数据类型[]:表示数组的类型
N:表示数组的长度
如:
int[] arr1 = new int[10];//创建一个可以存储10个int类型元素的数组
double[] arr2 = new double[10];//创建一个可以存储10个double类型元素的数组
String[] arr3 = new String[20];//创建一个可以存储20个字符串类型元素的数组
1.3.2、数组的初始化
初始化:也就是在内存中,为数组容器开辟空间,并将数据存入容器中的过程。
数组初始化有动态初始化和静态初始化。
动态初始化:在创建数组时,直接指定数组中元素的个数。由系统给出默认初始值。
数据类型[] 数组名 = new 数据类型[数组的长度];
例子:
int[] arr = new int[10];
String[] arr1 = new String[20];
静态初始化:在创建数组时不直接指定元素个数,而是将具体元素进行指定。
//完整格式
数据类型[] 数组名 = new 数据类型[]{元素1,元素2,元素3,元素4...};
例子:
int[] arr1 = new int[]{0,1,3,4,5,6,7};//数组大小是7
double[] arr2 = new double[]{2.5,3.14,5.69,5.12};//创建的数组大小是4
//简化格式,也就是常用格式
数据类型[] 数组名 = {元素1,元素2,元素3,元素4...};
例子:
int[] array = {1,2,3,4,5};
double[] array = {1.1,1.2,1.3};
【注意事项】
- 静态初始化编译器在编译时会根据
{}
中元素个数来确定数组长度。 - 数组的创建也可以按照 C语言语法格式,但是不推荐:
int arr[] = {1,2,3}
。 - 如果没对数组进行初始化,数组中元素会有默认值
- 注意
char
默认值是'/u0000'
,打印出来是空格
char[] arr3 = new char[3];
for (int i = 0; i < arr3.length; i++) {
System.out.print(arr3[i]);
}
System.out.println(1);
//运行结果
// 1
- 输入数组中存储元素类型为引用类型,默认值是
null
【总结】
- 静态初始化:手动指定数组元素,系统根据元素的个数,计算出数组的长度。
- 动态初始化:手动指定数组长度,系统给出默认初始化值。
1.4、数组的使用
1.4.1、地址值
int[] arr = {1,2,3,4,5};
System.out.println(arr);
double[] arr2 = {1.1,2.2,3.3};
System.out.println(arr2);
【运行结果】
[I@776ec8df
[D@4eec7777
打印数组的时候,实际出现的是数组的地址值。数组的地址值表示数组在内存中的位置。
以[I@776ec8df
为例:
[
:表示现在打印的是一个数组。
I
:表示现在打印的数组是int
类型的。
@
:仅仅是一个间隔符号而已。
776ec8df
:就是数组在内存中真正的地址值。(十六进制的)
但是,我们习惯性会把[I@776ec8df
这个整体称之为数组的地址值。
1.4.2、数组元素访问
可以根据数组下标进行访问,从 0 开始依次递增。
例子:
public static void main(String[] args) {
int[] arr1 = new int[10];//创建一个可以存储10个int类型元素的数组
arr1[0] = 1;
arr1[1] = 3;
System.out.println(arr1[0]);
System.out.println(arr1[1]);
System.out.println(arr1[2]);//int类型数组默认值是0
}
【运行结果】
1
3
0
【注意事项】
- 数组是一段连续内存空间,支持随机访问,可以通过下标访问任意位置的元素。
- 访问数组元素时不能越界,否则会报出下标越界异常。
1.4.3、遍历数组
public static void main(String[] args) {
//遍历数组
int[] arr1 = {1,2,3,4,5,6};
for (int i = 0; i < 6; i++) {
System.out.print(arr1[i] + " ");
}
}
【运行结果】
1 2 3 4 5 6
虽然此时可以成功访问数组元素,但是前提是因为我们知道了数组元素个数;计算机怎么知道数组元素个数??
在数组中可以通过数组名.length
来获取数组的长度。
public static void main(String[] args) {
//遍历数组
int[] arr1 = {1,2,3,4,5,6};
for (int i = 0; i < arr1.length; i++) {
System.out.print(arr1[i] + " ");
}
}
【运行结果】
1 2 3 4 5 6
也可以通过for-each
遍历数组
public static void main(String[] args) {
int[] arr = new int[]{1,2,3,4,56,10};
for (int x:arr) {
System.out.print(x + " ");
}
System.out.println();
String[] arr1 = new String[]{"hello","world","xaioming","zhangsan"};
for (String x: arr1) {
System.out.print(x + " ");
}
}
【运行结果】
与for
循环的区别:
【扩展】
自动快速生成数组的遍历方法(IDEA提供)
数组名.fori
2、数组是引用类型
2.1、初始 JVM 的内存分布
- 虚拟机栈:方法运行时使用的内存,比如
main
方法运行,进入方法栈中执行。每个方法在执行时,都会创建一个栈帧,里面包含:局部变量等信息。方法运行结束后,栈帧就会销毁,栈帧中保存的数据也就被销毁。 - 堆:存储对象或者数组,
new
来创建的,都是存储在堆内存中。堆是随着程序开始运行时而创建,随着程序的退出而销毁,堆中的数据只要还在使用,就不会被销毁。 - 方法区:存储可以运行的
class
文件。 - 本地方法栈:JVM在使用操作系统功能的时候使用,和开发无关。
- 程序计数器:保存下一条执行的指令地址。
2.2、基本数据类型变量与引用数据类型变量的区别
引用数据类型创建的变量,一般称为对象的引用,其空间中存储的是对象所在空间的地址。
例子1:
基本数据类型变量内存:
public static void main(String[] args) {
int a = 10;
int b = 10;
int c = a + b;
}
【内存分析】
例子2:
public static void main(String[] args) {
int[] arr1 = new int[2];//默认值为0
System.out.println(arr1);//打印数组arr1在内存中的地址
int[] arr2 = {1,2,3,4};
System.out.println(arr2);//打印数组arr2在内存中的地址
}
【运行结果】
[I@776ec8df
[I@4eec7777
【内存分析】
即arr1这个引用指向了数组对象
【总结】
- 只要是
new
出来的一定是在堆中开辟空间 - 如果
new
多次,则在堆中开辟多个空间,每个空间有自己各自的数据 - 引用变量并不直接存储对象本身,可以理解为存储的是对象在堆中空间的起始地址。通过该地址,引用变量就可以去操作对象;类比 C语言中的指针
两个数组指向同一个空间情况:
例子3:
public static void main(String[] args) {
int[] arr1 = {11,22};
System.out.println(arr1);
int[] arr2 = arr1;
System.out.println(arr2);
System.out.println(arr1[0]);
arr1[1] = 10;
System.out.println(arr1[1]);
System.out.println(arr2[1]);
}
【运行结果】
[I@776ec8df
[I@776ec8df
11
10
10
【内存分析】
2.3、认识null
null
在 Java 中表示“空引用”,也就是一个不指向对象的引用。
null
的作用类似于 C 语言中的 NULL (空指针), 都是表示一个无效的内存位置. 因此不能对这个内存进行任何读写操作. 一旦尝试读写, 就会抛出 NullPointerException
.
例子 :
【运行结果】
会报错!!
Exception in thread "main" java.lang.NullPointerException: Cannot read the array length because "arr" is null
at Test01.main(Test01.java:16)
3、数组的应用场景
3.1、保存数据
可以通过数组将相同数据类型的数据进行保存,之后可以通过数组遍历进行访问数据。
3.2、作为函数的参数
例子
public static void func1(int[] arr){
arr = new int[10];
}
public static void func2(int[] arr){
arr[0] = 19;
}
public static void main(String[] args) {
int[] arr1 = {1,2,3,4};
func1(arr1);
for (int i = 0; i < arr1.length; i++) {
System.out.print(arr1[i] + " ");
}
System.out.println();
int[] arr2 = {1,2,3,4};
func2(arr2);
for (int i = 0; i < arr2.length; i++) {
System.out.print(arr2[i] + " ");
}
}
func2
是改变原来指向的对象的值,而func1
是改变所指向的对象
函数func1
内存分析:
之后执行函数func1
中的代码arr = new int[10];
,这样就相当于在堆中新创建一片内存空间,然后将栈中arr
存储的值进行修改。如下所示:
这样在执行main
函数的for
循环时,func1
创建的栈帧和在堆中的变量空间都会被销毁,接下来打印输出的arr1
操作的数据还是原来的数据,所以输出结果是1 2 3 4
函数func2
内存分析:
即没有修改数组的引用,这样在函数func2
的函数栈帧中,数组arr
和main
函数中的数组指向的是同一片堆内存空间,所以修改数据之后,在main
函数进行调用时也会发生改变。如下所示:
所以在main
函数的for
循环时,打印输出的数组值改为19 2 3 4
【总结】
所谓的“引用”本质上就是存了一个地址。Java 将数组设定为引用类型,在后续将数组作为参数进行传参时,其实只是将数组地址传入到函数形参中,这样可以避免对整个数组的拷贝。
3.3、作为函数的返回值
例子:
public static int[] func3(){
//方式1
/*int[] arr = {1,2,3,4};
return arr;*/
//方式2
return new int[]{1,2,3,4};
}
public static void main(String[] args) {
int[] ret = func3();
for (int i = 0; i < ret.length; i++) {
System.out.print(ret[i] + " ");
}
}
【内存分析】
【结论】
new
出来的对象一定是创建在堆上,同时在栈中局部变量里存储的是在堆中的地址值。
3.4、Arrays
工具类
可以借助工具类进行输出:import java.util.Arrays;
以下方法构成方法的重载
利用toString()
可以接收数组,然后将其转换成字符串,这样就可以方便输出,而不是在用for
循环遍历数组。
import java.util.Arrays;
public static int[] func3(){
/*int[] arr = {1,2,3,4};
return arr;*/
return new int[]{1,2,3,4};
}
public static void main(String[] args) {
int[] ret = func3();
String s = Arrays.toString(ret);
System.out.println(s);
}
【运行结果】
[1,2,3,4]
4、数组练习(熟悉 Arrays
工具类)
4.1、数组转字符串
- 使用
import java.util.Arrays;
利用Arrays
中的toString
方法可以实现 - 自行实现
toString
方法:
public static String myToString(int[] arr){
String ret = "[";
for (int i = 0; i < arr.length; i++) {
ret += arr[i];
if(i < arr.length-1){
ret += ",";
}
}
ret += "]";
return ret;
}
public static void main(String[] args) {
int[] array = {1,2,3,4,5};
String ret = myToString(array);
System.out.println(ret);
}
【运行结果】
[1,2,3,4,5]
4.2、数组拷贝
将一个数组元素拷贝到另一个数组中的操作
方法1
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
//拷贝数组元素到另一个数组中
int[] copy = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
copy[i] = arr[i];
}
System.out.println(Arrays.toString(copy));//遍历输出数组copy的元素 [1,2,3,4,5]
}
【注意】
以下情况不是拷贝!!
int[] arr = {1,2,3,4,5};
int[] newArr = arr;
System.out.println(Arrays.toString(newArr));
【内存分析】
方法2
利用Arrays
工具类进行拷贝:
语法:
Arrays.copyOf(要拷贝的数组名,要拷贝的数组大小)
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
//利用数组类进行拷贝
int[] copy = Arrays.copyOf(arr,arr.length);
//利用数组类将数组输出
System.out.println(Arrays.toString(copy));//[1,2,3,4,5]
}
如果将int[] copy = Arrays.copyOf(arr,arr.length);
改成int[] copy = Arrays.copyOf(arr,arr.length * 2);
将相当于数组扩容了:
如果想拷贝数组中的一部分元素,可以使用Arrays
中的Arrays.copyOfRange()
:
//其中的from,to表示数组下标,范围是[from,to)
int[] copy1 = Arrays.copyOfRange(arr,0,3);
System.out.println(Arrays.toString(copy1));
【运行结果】
方法3
利用System
的arraycopy
拷贝:
语法分析:
native
表示的是本地方法
例子:
//利用System.arraycopy方法
int[] arr = {1,2,3,4,5};
int[] copy2 = new int[arr.length * 2];
//将数组arr从下标为1的位置开始拷贝到copy2数组的下标位置为2开始的位置,拷贝的长度是4
System.arraycopy(arr,1,copy2,2,arr.length-1);
System.out.println(Arrays.toString(copy2));//[0, 0, 2, 3, 4, 5, 0, 0, 0, 0]
4.3、求数组元素的平均值
//给定一个整型数组, 求平均值
public static double avg(int[] arr){
double sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum / arr.length;
}
public static void main(String[] args) {
//给定一个整型数组, 求平均值
int[] arr = new int[]{1,2,3,4,5};
System.out.println(avg(arr));//3.0
}
4.4、查找数组中指定元素(顺序查找)
public static int findVal(int[] arr,int key){
for (int i = 0; i < arr.length; i++) {
if(arr[i] == key){
return i;
}
}
return -1;//表示没有找到
}
public static void main(String[] args) {
int[] arr = {1,3,5,7,9};
int ret = findVal(arr,10);
if(ret != -1){
System.out.println("找到了,下标是:" + ret);
}else {
System.out.println("没有找到");
}
}
4.5、查找数组中指定元素(二分查找)
二分查找针对的是有序数组,在 Java 中可以利用Arrays
工具类中的方法Arrays.sort()
将无序数组从小到大进行排列。
//二分查找
public static int binarySearch(int[] arr,int key){
int left = 0;
int right = arr.length-1;
while(left <= right){
int mid = (left + right) / 2;
if(key > arr[mid]){
left = mid + 1;
} else if (key < arr[mid]) {
right = mid - 1;
}else {
return mid;//返回目标值在数组的下标
}
}
return -1;//表示没有找到
}
利用Arrays
工具类中的方法Arrays.binarySearch()
可以直接查找
Arrays
工具类方法的补充
- 判断两个数组对应位置上的元素是否相等
Arrays.equals(arr1,arr2);
- 把指定数据直接填充到数组的所有位置
Arrays.fill(arr,key)
- 将指定数据填充到数组的指定位置
Arrays.fill(arr,from,to,key)
,区间是左闭右开[from,to)
4.6、数组排序(冒泡排序)
按照从小到大顺序排序
//冒泡排序
public static void bubbleSort(int[] arr){
for (int i = 0; i < arr.length - 1; i++) {//趟数
boolean flag = true;//初始表示没有发生交换
for (int j = 0; j < arr.length-1-i; j++) {//j表示每趟比较的次数
if(arr[j] >= arr[j+1]){
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
flag = false;//发生数据交换
}
}
//一趟下来不发生交换说明已经有序
if(flag){
break;
}
}
}
【运行结果】
4.7、数组逆序
public class Test01 {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4};
reverse(arr);
System.out.println(Arrays.toString(arr));
}
public static void reverse(int[] arr) {
int left = 0;
int right = arr.length - 1;
while (left < right) {
int tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
left++;
right--;
}
}
}
4.8、打乱数据
定义一个数组,存入1~5。要求打乱数组中所有数据的顺序。
代码示例:
//1.定义数组存储1~5
int[] arr = {1, 2, 3, 4, 5};
//2.循环遍历数组,从0索引开始打乱数据的顺序
Random r = new Random();
for (int i = 0; i < arr.length; i++) {
//生成一个随机索引
int randomIndex = r.nextInt(arr.length);//随机数范围[0,5)
//拿着随机索引指向的元素 跟 i 指向的元素进行交换
int temp = arr[i];
arr[i] = arr[randomIndex];
arr[randomIndex] = temp;
}
//3.当循环结束之后,那么数组中所有的数据已经打乱顺序了
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
5、二维数组
本质上也是一维数组,只不过每个元素又是一个一维数组
基本语法:
数据类型[][] 数组名 = new 数据类型[行数][列数]{初始化数据};
举例:
//二维数组的创建
int[][] arr1 = {{1,2,3},{4,5,6}};//表示2行3列
int[][] arr2 = new int[][]{{1,2,3},{4,5,6}};
int[][] arr3 = new int[2][3];//没有赋初始值,则默认都为0
二维数组的遍历:
//二维数组的遍历
int[][] arr1 = {{1,2,3},{4,5,6}};//表示2行3列
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
System.out.print(arr1[i][j] + " ");
}
System.out.println();
}
二维数组的本质分析:
所以我们可以直接求出二维数组的行和列:
//如何确定行和列?
//二维数组其实就是一维数组,只不过存储的元素是地址,对应的是另一个一维数组
int[][] arr1 = {{1,2,3},{4,5,6}};//表示2行3列
for (int i = 0; i < arr1.length; i++) {
//arr1.length计算的是数组的元素个数,{{1,2,3},{4,5,6}},也就是有两个元素{1,2,3},{4,5,6}
for (int j = 0; j < arr1[i].length; j++) {
//arr1[0].length 表示的是元素{1,2,3}的长度,也就是第一行的长度...
System.out.print(arr1[i][j] + " ");
}
System.out.println();
}
可以利用Arrays
工具类对二维数组进行遍历:Arrays.deepToString()
特殊情况:不规则的二维数组:二维数组可以不规定列数,但是也可以将其进行遍历
画图分析:
情况 1:
//可以不指定列数,而C语言中需要指定列数
int[][] arr1 = new int[2][];
System.out.println(Arrays.deepToString(arr1));
【运行结果】
[null, null]
情况2
int[][] arr1 = new int[2][];
for (int i = 0; i < arr1.length; i++) {
for (int j = 0; j < arr1[i].length; j++) {
System.out.print(arr1[i][j] + " ");
}
System.out.println();
}
【运行结果】
Exception in thread "main" java.lang.NullPointerException: Cannot read the array length because "arr1[i]" is null
at Test01.main(Test01.java:44)
原因:
因为创建一一个二维数组arr1
的具体情况是如下所示,此时没有赋值,则默认是null
对于情况 2,报错的原因在于arr1[i].length
,因为对于null
的数组,不可以进行任何读写操作,所以会报出错误。