深入理解 Java 数组

共 7008字,需浏览 15分钟

 ·

2021-02-28 19:10

虽然在平常开发中,使用集合(容器)的频率比数组高得多,不过集合的底层也是通过数组来实现的。而且,尽管集合相比数组来说强大得多,但是其执行效率远不及数组。所以在讲集合之前,非常有必要深入了解一下数组。全文脉络思维导图如下:

1. 一维数组详解

所谓数组,就是「相同数据类型的元素按一定顺序排列而成的集合」。先来看看一维数组的三种声明和赋值方式:

第一种:

int[] a = {123};

第二种:

int[] b = new int[] {123};

第三种:

int[] c = new int[3];
c[0] = 1;
c[1] = 2;
c[2] = 3;

以上这三种方式的效果都是一样的,创建了一个存储 1、2、3 这三个整数的数组。

可以使用下面两种形式声明数组 :

int[] a; 或  int a[];

通常都会使用第一种风格, 因为它将类型 int[] ( 整型数组)与变量名分开了。

我们来反编译一下这三段代码的 .class 文件,你就会发现其实「在底层它们的创建方式都是一样的」,编译器自动的给我们加上了 new 关键字,甚至还把 c 的声明和赋值一体化了。

另外,需要注意的是:new int[3]; 这条语句会创建一个能够存储 3 个元素的数组,不过该数组的最后一个元素的下标是 2(因为下标从 0 开始计数,相信我,刷算法题的时候,这个鬼东西经常会让你脑子短路)。并且这条语句会自动的初始化所有元素,比如对于 int 数组来说就是全部初始化为 0,对于 boolean 数组来说就会全部初始化为 false, 对象数组就会初始化为 null 等。

从上面这些代码和分析中,我们也不难看出,「数组创建之后是无法改变其存储空间大小的」(存储能力),尽管它可以改变每一个数组元素。

我们通过 IDEA 的联想功能来看看数组能够调用什么东西:

可以发现,数组拥有 Object 类的所有方法,并且还会新增一个属性 length(注意是属性,而不是方法),用来表示这个数组的长度,我们可以这样调用:a.length

注意区别于 String 类的 length() 「方法」,数组拥有的是 length 「属性」,而非方法。

综上,数组不仅能够封装数据,还能调用属性和方法,那这和对象有啥区别?没错,这也就是为什么说「数组的本质是对象」了。回顾一下我们之前总结的 Java 中方法参数的使用情况(按值调用):

  • 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
  • 一个方法可以改变一个对象参数的状态。
  • 一个方法不能让对象参数引用一个新的对象。

而因为数组的本质是对象,因此,将数组作为参数传递给方法,这个数组是可以被改变的。

OK,接下来,以下面这段代码为例,我们来看看一维数组在内存中的存储方式:

int[] b = new int[] {123};

int 数组对象 b 存储在 栈中,而数组元素既然是 new 出来的,那当然是存储在堆中。只有当 JVM 执行 new int[] 时,才会在堆中开辟相应的内存区域。

2. 多维数组详解

我们再来看看多维数组,就以二维数组为例,同样的三种声明与赋值方式:

第一种:

double[][] a = { 
    {163213}, 
    {510118}, 
    {96712}, 
    {415141
};

第二种:

// 构造一个 4 行 4 列的二维数组
double[][] b = new double[4][4] { 
    {163213}, 
    {510118}, 
    {96712}, 
    {415141
};

第三种:

double[][] c = new double[4][4];
c[0][0] = 16// 第一行第一列值为 16
c[0][1] = 3// 第一行第二列值为 3
c[0][2] = 2;
c[0][3] = 13;
c[1][0] = 5// 第二行第一列值为 5
c[1][1] = 10;
c[1][2] = 1;
c[1][3] = 8;
......

同样的,我们来反编译一下这三段代码的 .class 文件,底层它们的创建方式基本也都是一样的,不过有些细微的差别。编译器还是自动的给我们加上了 new 关键字,不过没有像一维数组那样把 c 的声明和赋值一体化了。

到目前为止,我们所看到的数组与其他程序设计语言中提供的数组没有多大区别。但实际存在着一些细微的差异, 而这正是 Java 的优势所在:「Java 实际上没有多维数组,只有一维数组」。多维数组被解释为「数组的数组」。请看下图:

由于可以单独地存取数组的某一行, 所以可以让两行交换。

int[] temp = b[1];
b[1] = b[2];
b[2] = temp;

3. for each 循环

Java 有一种功能很强的循环结构, 可以用来依次处理数组中的每个元素而不必为指定下标值而分心。这种增强的 for 循环的语句格式为:

for(variable : collection){
    // todo
}

collection这一集合表达式必须是一个数组或者是一个实现了 Iterable 接口的类对象」,例如 ArrayList

下面我们来对比一下使用下标遍历数组和使用 for each 循环遍历数组这两种方式:

// 使用下标遍历数组
int[] a = new int[100];
for(int i = 0; i < 100; i++) {
    a[i] = i;
}

// 使用 for each 循环遍历数组
for(int element: a){
    System.out.println(element);
}

for each 循环语句的循环变量将会遍历数组中的每个元素, 而不需要使用下标值。

不过,需要注意的是,「for each 循环语句不能自动处理多维数组的每一个元素,它是按照行, 也就是一维数组处理的」。以二维数组为例,要想访问二维数组的所有元素, 需要使用两个嵌套的循环, 如下所示:

int[][] a = { 
  {163213},
  {510118}, 
  {96712}, 
  {415141
};

for(int[] row : a) { // 遍历每一行
  for(int value : row) { // 遍历每一列
   System.out.println(value);
  }
}

4. 可变参数

「JDK 1.5」 之后,如果我们定义一个方法需要接受多个参数,并且「多个参数类型一致」,我们可以对其简化成如下格式:

修饰符 返回值类型 方法名 (参数类型... 形参名){  }

... 用在参数上,称之为「可变参数」,它「表明这个方法可以接收任意数量的参数」。其实这个写法完全等价与

修饰符 返回值类型 方法名 (参数类型[] 形参名){  }

虽然同样是代表数组,但是在调用这个带有可变参数的方法时,不用创建数组,直接将数组中的元素作为实际参数进行传递,这就是简单之处。当然,其实这种方式的底层实现也是将这些元素先封装到一个数组中,在进行传递,不过这些动作都在编译 .class 文件时就自动完成了。

代码演示:

public class ChangeArgs {
    
    //可变参数写法
    public static int getSum(int... arr) {
        int sum = 0;
        for (int a : arr) {
            sum += a;
        }
        return sum;
    }
    
    public static void main(String[] args) {
        int[] arr = { 14624312 };
        int sum = getSum(arr);
        System.out.println(sum);
        
        int sum2 = getSum(672122121);
        System.out.println(sum2);
    }
}

需要注意的是:如果在方法书写时,这个方法拥有多个参数,并且参数中包含可变参数,「可变参数一定要写在参数列表的末尾」

5. Arrays 类

Java 中,提供了一个很有用的数组工具类:java.util.Arrays。它提供的主要操作有:

1)Arrays.toString - 将一维数组转成字符串类型(打印一维数组的所有元素)

2)Arrays.deepToString - 将二维数组转成字符串类型(打印二维数组的所有元素)

3)Arrays.copyOf - 数组拷贝。举个例子,将 a 数组中的元素全部拷贝给 c 数组:

int[] c = Arrays.copyOf(a, 2 * a.length());

第 2 个参数是新数组的长度。这个方法通常用来增加新数组的大小:如果数组元素是数值型,那么多余的元素将被赋值为 0 ; 如果数组元素是布尔型,则将赋值为 false 等。相反,如果长度小于原始数组的长度,则只拷贝最前面的数据元素。

4)Arrays.sort - 对数组中的元素进行排序

5)Arrays.equals -  Arrays 类提供了重载后的 equals 方法,用来基于内容比较数组,数组相等的条件是元素个数和对应位置的元素都相等。

6. 总结

不可否认,在 Java 中,数组是一种效率最高的存储和随机访问对象引用序列的方式。数组就是一个简单的线性序列,在内存中采用「连续空间分配」的存储方式,这使得通过下标访问元素非常快速。但是代价就是「一旦创建了数组, 就不能再改变它的大小」(尽管可以改变每一个数组元素)。

如果「经常需要在运行过程中扩展数组的大小, 可以使用集合 ArrayList 。它可以通过创建一个新实例,然后把旧实例中所有的引用移到新实例中,从而实现更多空间的自动分配。但是这种弹性需要开销,因此,ArrayList 的效率比数组低很多。当然,无论数组还是集合,如果越界,都会得到一个 RuntimeException异常。

关于集合会写成一个系列,下篇文章就会陆续开更,内容没啥难度,不过要记的东西非常多,啃完集合后面还有个硬骨头多线程,这俩学完 Java 基础部分基本就没啥了。

参考资料

  • 《Java 核心技术 - 卷 1 基础知识 - 第 10 版》
  • 《Thinking In Java(Java 编程思想)- 第 4 版》
  • 《On Java 8》中文版(《Java 编程思想》- 第 5 版)
  • 清浅池塘 - Java 中的数组-Java那些事儿:https://juejin.cn/post/6844903498207756295



😁 点击下方卡片关注公众号「飞天小牛肉」(专注于分享计算机基础、Java 基础和面试指南的相关原创技术好文,帮助读者快速掌握高频重点知识,有的放矢),与小牛肉一起成长、共同进步 

🎉 并向大家强烈推荐我维护的 Gitee 仓库 「CS-Wiki」(Gitee 推荐项目,目前已 1.0k+ star。致力打造完善的后端知识体系,在技术的路上少走弯路。相比公众号,该仓库拥有更健全的知识体系,欢迎给位小伙伴前来交流学习,仓库地址 https://gitee.com/veal98/CS-Wiki。也可直接下方扫码访问


原创不易,读完有收获不妨点赞|分享|在看支持

浏览 33
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报