JavaSE 基础语法 - 数组的定义与使用 - ①

在前面的学习中,我们知道变量能够 保存数据,例如:

int score1 = 90;
int score2 = 85;
int score3 = 92;
int score4 = 88;
int score5 = 95;

如果现在需要保存 5 个学生的成绩,这样写并没有什么问题。

但是,如果需要保存 100 个学生、1000 个学生,甚至更多学生的成绩呢?

难道要定义 100 个甚至上千个变量吗?

int score1;
int score2;
int score3;
...
int score1000;

很显然,这种方式不仅代码冗长,而且维护起来也非常困难。

例如:

  • 想计算所有学生的平均成绩;
  • 想统计最高分和最低分;
  • 想遍历所有成绩;

如果使用普通变量,几乎无法高效完成这些操作。

因此,Java 提供了一种能够存储一组相同类型数据的数据结构——数组(Array)

数组可以把多个相同类型的数据统一管理,大大提高了开发效率。

一、什么是数组

什么是数组?

数组(Array)是一种用于存储多个相同数据类型元素的数据结构。

简单来说:数组就是一块连续的内存空间,用来保存多个相同类型的数据

例如:

一个数组可以保存多个学生成绩:

90 85 92 88 95

也可以保存多个年龄:

18 19 20 21 22

还可以保存多个姓名:

Tom  Jack  Lucy  Alice

在 Java 中,可以使用一个数组统一管理这些数据,而不需要定义多个独立的变量。

例如:

int[] scores = {90, 85, 92, 88, 95};

这里:

  • scores 表示数组变量;
  • 数组中保存了 5 个整数;
  • 每一个数据称为数组元素(Element)

数组的特点

数组具有以下几个特点。

1、数组中只能存储相同类型的数据

例如:

下面的数组是合法的。

int[] nums = {10, 20, 30};

因为:

所有元素都是 int 类型。

下面的代码则是不合法的。

int[] nums = {10, "Hello", true};

因为:

数组中出现了:

  • int
  • String
  • boolean

三种不同的数据类型。

Java 不允许同一个数组存放不同类型的数据。

但是:

数组中存储的元素类型必须与数组声明时的数据类型兼容。对于基本类型数组(如 int[]),只能存储对应类型的数据;对于引用类型数组,可以存储该类型及其子类的对象。

// 合法:Object 数组可以存任何对象
Object[] arr = {10, "Hello", true}; 
// 10 会被自动装箱为 Integer,true 装箱为 Boolean
// 它们都是 Object 的子类,所以合法

2、数组长度固定

数组一旦创建,长度就不能再改变。

例如:

int[] nums = new int[5];

表示:创建了一个长度为 5 的数组。

以后:只能存放 5 个元素。

不能变成:6 个

也不能缩小为:3 个

如果需要更多空间,只能重新创建新的数组。

后续学习 ArrayList 时,就能够实现动态扩容。

3、数组中的元素有顺序

数组中的每个元素都有自己的位置。

Java 使用索引来表示元素的位置。

例如:

int[] nums = {10, 20, 30, 40};

对应关系如下:

索引0123
元素10203040

需要注意:Java 数组的索引从 0 开始,而不是从 1 开始

例如:

System.out.println(nums[0]);

输出:

10

而:

System.out.println(nums[3]);

输出:

40

4、数组属于引用数据类型

很多同学学习数组时,会认为数组和 int、double 一样,也是一种普通数据类型。

实际上,并不是。数组属于 引用数据类型(Reference Type)

例如:

int[] nums = new int[5];

这里真正创建的是:数组对象

而变量nums保存的是:数组对象在内存中的地址(引用)

这也是为什么数组作为方法参数时,修改数组元素会影响原数组。

关于数组为什么属于引用类型,以及数组在 JVM 中是如何存储的,我们后续讲。

数组的应用场景

数组适用于:

  • 保存学生成绩;
  • 保存考试分数;
  • 保存商品价格;
  • 保存多个姓名;
  • 保存每天的气温;
  • 保存多个对象。

凡是需要保存一组相同类型的数据,都可以考虑使用数组。

二、为什么需要数组

学习数组之前,我们先来看一个实际需求。

假设需要保存 10 个学生的 Java 成绩。

如果不用数组,只能这样定义变量:

int score1 = 90;
int score2 = 85;
int score3 = 92;
int score4 = 88;
int score5 = 95;
int score6 = 76;
int score7 = 81;
int score8 = 89;
int score9 = 94;
int score10 = 87;

虽然能够保存数据,但会带来很多问题。

例如:

问题一:变量太多

如果有:100 个学生。

就需要:100 个变量。

程序将变得非常臃肿。

问题二:无法方便地遍历

例如:

想输出所有成绩。

只能这样写:

System.out.println(score1);
System.out.println(score2);
System.out.println(score3);
...

代码重复,非常繁琐。

问题三:统计困难

例如:

计算平均分。

需要:

(score1 + score2 + score3 + ... + score10) / 10

如果学生人数发生变化,还需要重新修改代码。

使用数组之后

使用数组,只需要定义一个变量。

int[] scores = {90, 85, 92, 88, 95, 76, 81, 89, 94, 87};

输出所有成绩:

for (int i = 0; i < scores.length; i++) {
    System.out.println(scores[i]);
}

计算总分:

int sum = 0;

for (int i = 0; i < scores.length; i++) {
    sum += scores[i];
}

可以看到:

使用数组后:

  • 数据统一管理;
  • 可以使用循环遍历;
  • 更容易统计和计算;
  • 代码更加简洁。

因此,数组最大的作用就是:使用一个变量统一管理多个相同类型的数据,提高程序的开发效率和可维护性

三、数组的定义

在 Java 中,定义数组主要有两种方式:

  • 动态初始化(Dynamic Initialization)
  • 静态初始化(Static Initialization)

3.1 动态初始化

动态初始化是指:只指定数组长度,由系统自动为数组元素赋默认值

语法格式:

数据类型[] 数组名 = new 数据类型[数组长度];

例如:

int[] nums = new int[5];

表示:创建一个能够存放 5 个整数 的数组。

此时:数组中的元素都有默认值。

0 0 0 0 0

再例如:

double[] prices = new double[3];

默认值:

0.0 0.0 0.0

3.2 静态初始化

静态初始化是指:创建数组的同时,直接指定数组中的元素

语法格式:

数据类型[] 数组名 = new 数据类型[]{元素1, 元素2, 元素3...};

例如:

int[] nums = new int[]{10, 20, 30, 40, 50};

数组长度由元素个数自动决定。

这里:

数组长度就是:5

静态初始化简写

Java 提供了更简洁的写法。

例如:

int[] nums = {10, 20, 30, 40, 50};

这是开发中最常见的数组定义方式。

需要注意:这种简写形式只能在声明数组的同时使用

例如:

下面的代码是错误的:

int[] nums;

nums = {10, 20, 30};

如果数组已经声明,则必须使用完整写法:

int[] nums;

nums = new int[]{10, 20, 30};

3.3 动态初始化与静态初始化的区别

比较项动态初始化静态初始化
是否指定元素
是否指定长度否,由元素个数决定
元素默认值无(由程序员指定)
适用场景暂时不知道具体数据已知全部数据

一般来说:

  • 已知数据内容,优先使用静态初始化;
  • 只知道需要多少个元素,不知道具体值,使用动态初始化。

四、数组的组成

一个完整的数组主要由以下几个部分组成。

1、数组变量

数组变量用于保存数组对象的引用(地址值)。

例如:

int[] nums = {10, 20, 30};

这里:

nums

就是数组变量。

2、数组对象

真正存放数据的是数组对象。

例如:

┌────────────────────┐
│102030│
└────────────────────┘

数组对象创建后,会存储在 JVM 的堆内存中。

但是:

数组元素如果是字符串字面量(如 String[] names = {"Tom", "Jack"}),字符串本身可能存储在字符串常量池(在方法区),而不是堆内存中。

3、数组元素(Array Element)

数组中的每一个数据,都称为数组元素。

例如:

int[] nums = {10, 20, 30};

其中:

  • 10
  • 20
  • 30

都是数组元素。

4、数组索引

索引用于访问数组中的元素。

索引从 0 开始。

例如:

nums[0]

表示第一个元素。

nums[2]

表示第三个元素。

访问数组时,索引必须在 [0, 数组长度-1] 范围内,否则会抛出 ArrayIndexOutOfBoundsException(数组索引越界异常)

五、JVM 内存分析

前面我们已经学习了数组的定义和初始化方式。

例如:

int[] nums = {10, 20, 30};

这行代码看起来非常简单,但在 JVM 内部却发生了很多事情。

例如:

  • 数组到底存放在哪里?
  • nums 变量保存的是什么?
  • 为什么数组属于引用数据类型?
  • 为什么修改数组元素会影响原数组?

要是想知道这些,就要先了解 JVM 的内存结构。

5.1 JVM 内存区域简介

当 Java 程序运行时,JVM(Java Virtual Machine,Java 虚拟机)会向操作系统申请一块内存,用于存放程序运行过程中产生的数据。

内存是一段连续的存储空间,主要用来存储程序运行时数据的。比如:

  • 程序运行时代码需要加载到内存
  • 程序运行产生的中间数据要存放在内存
  • 程序中的常量也要保存
  • 有些数据可能需要长时间存储,而有些数据当方法运行结束后就要被销毁

如果对内存中存储的数据不加区分的随意存储,那对内存管理起来将会非常麻烦。比如:一个非常凌乱的房间,和一个非常整洁的房间,如果说需要找一个东西,那么在整洁的房间里面就非常容易找到。

因此JVM也对所使用的内存按照功能的不同进行了划分:
在这里插入图片描述

  • 程序计数器 (PC Register):只是一个很小的空间, 保存下一条执行的指令的地址。
  • 虚拟机栈(JVM Stack):与方法调用相关的一些信息,每个方法在执行时,都会先创建一个栈帧,栈帧中包含有:局部变量表、操作数栈、动态链接、返回地址以及其他的一些信息,保存的都是与方法执行时相关的一些信息。比如:局部变量。当方法运行结束后,栈帧就被销毁了,即栈帧中保存的数据也被销毁了。
  • 本地方法栈(Native Method Stack):本地方法栈与虚拟机栈的作用类似。只不过保存的内容是Native方法的局部变量。在有些版本的 JVM 实现中(例如HotSpot),本地方法栈和虚拟机栈是一起的。
  • 堆(Heap):JVM所管理的最大内存区域,使用 new 创建的对象都是在堆上保存 (例如 new int[]{1, 2, 3} ),堆是随着程序开始运行时而创建,随着程序的退出而销毁,堆中的数据只要还有在使用,就不会被销毁。
  • 方法区(Method Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法编译出的的字节码就是保存在这个区域。

现在我们只简单关心虚拟机栈这两块空间,后序JVM中还会更详细介绍。

5.2 栈内存

栈内存主要负责:

  • 方法调用;
  • 方法参数;
  • 局部变量。

例如:

public static void main(String[] args) {

    int age = 18;

}

变量 age 直接存放在栈内存中。

main() 方法执行结束后:

整个栈帧都会被销毁。

因此:局部变量也随之消失。

5.3 堆内存

堆内存主要负责存放:

  • 对象;
  • 数组;
  • 字符串对象等引用类型数据。

例如:

int[] nums = new int[3];

执行后:

真正创建的是:数组对象

这个数组对象会存放到:

而:nums这个变量仍然位于:

因此:数组变量和数组对象不是同一个东西。

5.4 一个数组的内存分析

来看下面这段代码。

public class Demo {

    public static void main(String[] args) {

        int[] nums = new int[3];

    }

}

程序运行后:

第一步:

执行:

new int[3]

JVM 会在堆内存中创建一个数组对象。

例如:
在这里插入图片描述
这里:

三个元素都为:

0

因为:

int 类型默认值就是:

0

第二步:

变量:

nums

保存在栈内存中。

它里面保存的是:数组对象的地址值

例如:
在这里插入图片描述

因此:数组变量保存的是地址值,而真正的数据保存在堆内存中的数组对象里

这也是为什么数组属于引用数据类型。

5.5 修改数组元素

例如:

int[] nums = new int[3];

nums[0] = 100;
nums[1] = 200;
nums[2] = 300;

此时:

堆内存变成:
在这里插入图片描述
而:

栈中的 nums始终指向0x100这个地址。

因此修改的是:数组对象中的数据,而不是 数组变量

5.6 两个数组的内存分析

例如:

int[] arr1 = {1,2,3};

int[] arr2 = {4,5,6};

运行后:

栈内存:
在这里插入图片描述
堆内存:
在这里插入图片描述
可以看到:

两个数组,分别对应:两个不同的数组对象。

因此,修改:

arr1[0] = 100;

不会影响:

arr2

因为它们指向的是:不同的对象。

5.7 两个引用指向同一个数组

来看下面代码。

int[] arr1 = {1,2,3};

int[] arr2 = arr1;

很多同学会认为:创建了两个数组。

实际上并没有,这里只创建了:一个数组对象

第二行代码只是把:arr1保存的地址值复制给arr2

内存如下:
在这里插入图片描述
可以看到:

虽然有两个变量:arr1、arr2

但是它们都指向:同一个数组对象。

如果执行:

arr2[1] = 999;

堆内存变成:
在这里插入图片描述

此时:

输出:

System.out.println(arr1[1]);

结果:

999

因为:arr1 和 arr2 访问的是同一个数组对象

修改其中一个引用看到的数据,本质上就是修改了同一个对象,因此另一个引用也能看到最新的数据。

六、数组参数传递

例如以下代码:

public class Demo {

    public static void printArray(int[] arr) {

        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }

    }

    public static void main(String[] args) {

        int[] nums = {10, 20, 30};

        printArray(nums);

    }

}

这里的printArray(nums);把数组传递给了方法,那么:

  • 是把整个数组传递进去了吗?
  • 还是把数组复制了一份?
  • 为什么方法中能够访问原来的数组?

6.1 数组作为方法参数

数组属于引用数据类型,因此方法参数也可以定义为数组类型。

例如:

public static void printArray(int[] arr) {

}

这里:

int[] arr

就是一个数组类型的形参。

调用时:

int[] nums = {10,20,30};

printArray(nums);

这里nums就是实参。

需要注意的是:这里传递的并不是整个数组对象,而是数组变量中保存的地址值(对象引用)

6.2 数组参数传递过程

来看下面这段代码:

public class Demo {

    public static void print(int[] arr) {

        System.out.println(arr[0]);

    }

    public static void main(String[] args) {

        int[] nums = {10,20,30};

        print(nums);

    }

}

程序执行过程如下。

第一步:创建数组

执行:

int[] nums = {10,20,30};

JVM 创建数组对象:
在这里插入图片描述
第二步:调用方法

执行:

print(nums);

很多初学者误以为:Java 把整个数组复制了一份。

其实不是。Java 只是把nums里面保存的:地址值(0x100)

复制了一份。

于是内存情况为:
在这里插入图片描述
可以看到,这里有两个变量:nums、arr

但是:它们保存的地址值相同。

因此共同指向:同一个数组对象。

第三步:访问数组元素

执行:

System.out.println(arr[0]);

实际上访问的是地址:0x100 的第一个元素

因此输出:10

6.3 为什么修改数组会影响原数组?

来看下面代码:

public class Demo {

    public static void change(int[] arr){

        arr[0] = 100;

    }

    public static void main(String[] args) {

        int[] nums = {10,20,30};

        change(nums);

        System.out.println(nums[0]);

    }

}

运行结果:100

为什么?

因为:
在这里插入图片描述
两个变量共同指向:同一个数组。

6.4 为什么重新创建数组不会影响原数组?

再来看一个容易混淆的例子。

public class Demo {

    public static void change(int[] arr){

        arr = new int[]{1,2,3};

    }

    public static void main(String[] args) {

        int[] nums = {10,20,30};

        change(nums);

        System.out.println(nums[0]);

    }

}

运行结果:10

为什么?

运行在没调用方法前:
在这里插入图片描述
nums 指向地址 0x100 的对象。

调用方法:

在没执行arr = new int[]{1,2,3};前,arr还是指向地址 0x100 的对象。
在这里插入图片描述
执行arr = new int[]{1,2,3};,创建了新数组,随后:
在这里插入图片描述
arr就指向了新对象的地址。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值