CS-Wiki CS-Wiki
Home
知识体系总览
  • 数据结构与算法
  • 计算机网络
  • 操作系统
  • MySQL
  • Redis
  • 设计模式
  • Java 基础
  • Java 集合
  • Java 并发
  • Java 虚拟机
  • Spring
  • Kafka
  • 校招扫盲
  • 项目推荐
  • 唠唠嗑儿
  • 读书笔记
归档
GitHub (opens new window)
Home
知识体系总览
  • 数据结构与算法
  • 计算机网络
  • 操作系统
  • MySQL
  • Redis
  • 设计模式
  • Java 基础
  • Java 集合
  • Java 并发
  • Java 虚拟机
  • Spring
  • Kafka
  • 校招扫盲
  • 项目推荐
  • 唠唠嗑儿
  • 读书笔记
归档
GitHub (opens new window)
  • Java基础

    • 基础

      • 万物皆对象
      • HelloWorld
      • 运算符与控制流
      • 深入理解Java数组
        • 1. 一维数组详解
        • 2. 多维数组详解
        • 3. for each 循环
        • 4. 可变参数
        • 5. Arrays 类
        • 6. 总结
        • 参考资料
      • 面试官竟然问我这么简单的题目:Java 中 boolean 占多少字节?我脱出而出
    • 对象与类

    • 抽象类与接口

    • 异常

    • 反射和动态代理

    • IO

    • 注解

  • Java集合

  • Java并发

  • Java虚拟机

  • Java 新版本特性

  • 30-Java
  • Java基础
  • 基础
小牛肉
2022-03-20
目录

深入理解Java数组

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

# 1. 一维数组详解

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

第一种:

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

第二种:

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

第三种:

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[] {1, 2, 3};

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

# 2. 多维数组详解

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

第一种:

double[][] a = { 
    {16, 3, 2, 13}, 
    {5, 10, 11, 8}, 
    {9, 6, 7, 12}, 
    {4, 15, 14, 1} 
};

第二种:

// 构造一个 4 行 4 列的二维数组
double[][] b = new double[4][4] { 
    {16, 3, 2, 13}, 
    {5, 10, 11, 8}, 
    {9, 6, 7, 12}, 
    {4, 15, 14, 1} 
};

第三种:

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 = { 
  {16, 3, 2, 13},
  {5, 10, 11, 8}, 
  {9, 6, 7, 12}, 
  {4, 15, 14, 1} 
};

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 = { 1, 4, 62, 431, 2 };
        int sum = getSum(arr);
        System.out.println(sum);
        
        int sum2 = getSum(6, 7, 2, 12, 2121);
        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

🎁 公众号

各位小伙伴大家好呀,叫我小牛肉就行,目前在读东南大学硕士,上方扫码关注公众号「飞天小牛肉」,与你分享我的成长历程与技术感悟~

帮助小牛肉改善此页面 (opens new window)
Last Updated: 2023/02/16, 11:27:10
运算符与控制流
面试官竟然问我这么简单的题目:Java 中 boolean 占多少字节?我脱出而出

← 运算符与控制流 面试官竟然问我这么简单的题目:Java 中 boolean 占多少字节?我脱出而出→

最近更新
01
02
线上环境 CPU 使用率飙升如何快速排查?
03-05
03
面试官再跟你说中国没有根服务器,雪人计划让他了解下
02-23
更多文章>
Theme by Vdoing | Copyright © 2019-2023 飞天小牛肉 | 皖ICP备2022008966号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式