Java数组

O泡李华 9

Java 基础笔记:数组

本章位置:Java 核心基础
学习目标:掌握 Java 数组的创建、初始化、访问、遍历和常见操作,能够使用数组保存和批量处理一组同类型数据。


一、为什么需要数组

在学习变量时,我们可以这样保存一个学生的成绩:

int score = 90;

如果只有 3 个学生,也可以这样写:

int score1 = 90;
int score2 = 85;
int score3 = 92;

但是如果有:

50 个学生
100 个学生
1000 个学生

继续定义:

score1
score2
score3
score4
...

显然不现实。

这时候就需要:

数组

数组可以一次保存多个同类型的数据。

例如:

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

现在:

scores

这个数组中一次保存了 5 个整数。


二、什么是数组

数组可以理解为:

一组类型相同、按顺序排列的数据。

例如:

int[] scores = {90, 80, 70, 60};

可以简单画成:

scores

索引      0     1     2     3
        ┌────┬────┬────┬────┐
数据     │ 90 │ 80 │ 70 │ 60 │
        └────┴────┴────┴────┘

数组中的每一个数据称为:

数组元素

例如:

90
80
70
60

都是数组元素。


三、数组的特点

Java 数组有几个非常重要的特点。

1. 可以保存多个数据

例如:

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

一个数组中保存了多个整数。


2. 数组中的元素类型必须一致

例如:

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

这个数组是:

int[]

因此里面主要保存:

int

类型的数据。

不能这样写:

int[] numbers = {10, "Java", true};

因为:

10       int
"Java"   String
true     boolean

类型不同。


3. 数组长度固定

数组一旦创建:

长度不能改变

例如:

int[] numbers = new int[5];

这个数组长度就是:

5

后面不能直接把它扩展成:

10

这也是数组和后面集合框架的重要区别之一。


4. 数组有索引

数组中的每一个位置都有编号。

这个编号称为:

索引

或者:

下标

索引从:

0

开始。

例如:

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

对应:

数据     10    20    30
索引      0     1     2

注意:

Java 数组索引不是从 1 开始,而是从 0 开始。


四、数组的声明方式

Java 中声明数组主要有两种写法。

推荐:

int[] numbers;

也可以:

int numbers[];

两种语法都正确。

但是更推荐:

int[] numbers;

因为:

int[]

可以更直观地看成:

整数数组类型

五、不同类型的数组

数组可以保存各种类型的数据。

整数数组:

int[] numbers;

小数数组:

double[] prices;

字符数组:

char[] chars;

布尔数组:

boolean[] flags;

字符串数组:

String[] names;

以后学习对象之后,还可以创建:

Student[] students;
User[] users;
Product[] products;

也就是说:

数组不仅可以保存基本数据类型,也可以保存引用类型。


六、数组初始化

数组声明之后,还需要真正创建数组。

数组初始化主要分为:

动态初始化
静态初始化

七、动态初始化

如果我们只知道数组需要多长,但是暂时不知道里面保存什么数据,可以使用动态初始化。

语法:

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

例如:

int[] numbers = new int[5];

表示:

创建一个可以保存 5 个 int 数据的数组

此时:

numbers.length = 5

动态初始化后的数组

代码:

int[] numbers = new int[5];

可以理解成:

索引      0    1    2    3    4
        ┌───┬───┬───┬───┬───┐
数据     │ 0 │ 0 │ 0 │ 0 │ 0 │
        └───┴───┴───┴───┴───┘

为什么都是:

0

因为 Java 会给数组元素设置默认值。


八、数组元素的默认值

动态初始化数组时,不同类型会得到不同默认值。

数组元素类型 默认值
byte 0
short 0
int 0
long 0L
float 0.0F
double 0.0
char '\u0000'
boolean false
引用类型 null

例如:

int[] numbers = new int[3];

System.out.println(numbers[0]);
System.out.println(numbers[1]);
System.out.println(numbers[2]);

输出:

0
0
0

boolean 数组

boolean[] flags = new boolean[3];

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

输出:

false

String 数组

String[] names = new String[3];

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

输出:

null

这里的:

null

表示当前位置还没有指向一个具体对象。

后面学习引用类型和面向对象时会继续讲。


九、静态初始化

如果创建数组时已经知道具体数据,可以直接写出来。

例如:

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

这叫:

静态初始化

数组长度会根据元素数量自动确定。

这里:

numbers.length

等于:

4

十、静态初始化的完整写法

静态初始化也可以写成:

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

更常见的简写:

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

两种都可以。

日常开发中如果创建数组时就知道数据,一般会使用:

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

十一、动态初始化和静态初始化怎么选

如果只知道长度:

int[] scores = new int[50];

适合:

动态初始化

例如:

需要保存 50 名学生成绩
但是成绩后面才输入

如果一开始就知道数据:

int[] scores = {90, 85, 76, 98};

适合:

静态初始化

可以简单记:

知道长度,不知道数据
→ 动态初始化

知道具体数据
→ 静态初始化

十二、访问数组元素

数组通过:

索引

访问数据。

例如:

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

访问第一个元素:

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

输出:

10

访问第二个:

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

输出:

20

访问第四个:

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

输出:

40

十三、为什么第一个元素是 0

这是初学数组必须习惯的规则。

例如:

String[] names = {"张三", "李四", "王五"};

对应关系:

第 1 个元素 → names[0]

第 2 个元素 → names[1]

第 3 个元素 → names[2]

因此:

第 n 个元素

对应索引:

n - 1

十四、修改数组元素

数组中的元素可以修改。

例如:

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

numbers[1] = 100;

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

输出:

100

原来的:

20

被覆盖。

此时数组变成:

10
100
30

十五、数组长度 length

Java 数组提供:

length

获取长度。

例如:

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

System.out.println(numbers.length);

输出:

5

注意:

数组使用:

numbers.length

不是:

numbers.length()

这里没有括号。


十六、最后一个元素的索引

假设数组长度:

numbers.length

因为索引从:

0

开始,

所以最后一个索引是:

numbers.length - 1

例如:

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

长度:

3

最后一个索引:

3 - 1 = 2

访问最后一个元素:

System.out.println(
        numbers[numbers.length - 1]
);

输出:

30

这个写法非常常用。


十七、数组索引越界

这是数组中最常见的错误之一。

例如:

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

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

这里会报错。

因为数组长度:

3

有效索引:

0
1
2

不存在:

3

运行时会出现类似:

ArrayIndexOutOfBoundsException

十八、数组合法索引范围

数组长度为:

array.length

合法索引范围永远是:

0 ~ array.length - 1

例如数组长度为:

5

合法索引:

0
1
2
3
4

不是:

1
2
3
4
5

十九、遍历数组

所谓遍历:

把数组中的元素按照一定顺序一个一个处理。

例如:

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

如果手动输出:

System.out.println(numbers[0]);
System.out.println(numbers[1]);
System.out.println(numbers[2]);
System.out.println(numbers[3]);

数据一多就不方便。

因此数组通常配合:

for

循环使用。


二十、使用 for 遍历数组

标准写法:

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

for (int i = 0; i < numbers.length; i++) {

    System.out.println(numbers[i]);
}

输出:

10
20
30
40

这一段代码非常重要。

以后会大量使用:

for (int i = 0; i < array.length; i++) {

    array[i]
}

二十一、为什么条件是 i < array.length

例如:

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

长度:

3

索引:

0
1
2

循环:

for (int i = 0; i < numbers.length; i++)

实际就是:

i = 0
i = 1
i = 2

当:

i = 3

条件:

3 < 3

为:

false

循环结束。

这样正好不会越界。


二十二、错误的数组循环写法

错误:

for (int i = 0; i <= numbers.length; i++) {

    System.out.println(numbers[i]);
}

假设长度:

3

最后一次:

i = 3

程序会访问:

numbers[3]

于是发生:

数组索引越界

因此数组遍历基本写法必须记住:

i < array.length

而不是:

i <= array.length

二十三、增强 for 循环

Java 还提供一种遍历数组的写法:

for (元素类型 变量 : 数组) {

}

例如:

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

for (int number : numbers) {

    System.out.println(number);
}

输出:

10
20
30
40

这叫:

增强 for

也经常叫:

foreach

二十四、增强 for 的执行过程

代码:

for (int number : numbers) {

    System.out.println(number);
}

可以简单理解为:

第一次:

number = numbers[0]

第二次:

number = numbers[1]

第三次:

number = numbers[2]

...

直到数组遍历完成。


二十五、普通 for 和增强 for 怎么选

普通 for:

for (int i = 0; i < array.length; i++) {

}

优点:

可以拿到索引
可以根据索引修改元素
控制更加灵活

增强 for:

for (int value : array) {

}

优点:

写法简单
适合只读取数据

可以简单记:

需要索引
→ 普通 for

只想一个一个读取数据
→ 增强 for

二十六、增强 for 修改变量不等于修改数组

看代码:

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

for (int number : numbers) {

    number = 100;
}

很多初学者会认为数组变成:

100
100
100

实际上不会。

继续输出:

for (int number : numbers) {

    System.out.println(number);
}

结果仍然是:

10
20
30

原因是:

number

只是每次拿到了当前元素的值。

修改:

number

并没有直接修改:

numbers[i]

真正修改数组元素

应该使用索引:

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

for (int i = 0; i < numbers.length; i++) {

    numbers[i] = 100;
}

此时数组才会变成:

100
100
100

二十七、数组求和

需求:

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

计算所有元素的和。

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

int sum = 0;

for (int number : numbers) {

    sum += number;
}

System.out.println("总和:" + sum);

输出:

总和:150

二十八、数组求平均值

int[] scores = {90, 80, 70, 100};

int sum = 0;

for (int score : scores) {

    sum += score;
}

double average =
        (double) sum / scores.length;

System.out.println("平均分:" + average);

结果:

平均分:85.0

注意:

(double) sum

用于避免整数除法。

也可以:

double average =
        sum * 1.0 / scores.length;

二十九、求数组最大值

例如:

int[] numbers = {12, 35, 8, 99, 24};

要找最大值。

基本思路:

先假设第一个元素最大

然后从第二个元素开始比较

如果发现更大的

就替换 max

代码:

int[] numbers = {12, 35, 8, 99, 24};

int max = numbers[0];

for (int i = 1; i < numbers.length; i++) {

    if (numbers[i] > max) {

        max = numbers[i];
    }
}

System.out.println("最大值:" + max);

输出:

最大值:99

三十、为什么最大值使用 numbers[0]

不推荐随便写:

int max = 0;

例如数组:

int[] numbers = {-10, -20, -5, -30};

如果:

int max = 0;

最终结果会错误地得到:

0

但数组里面根本没有:

0

所以更稳妥的方式:

int max = numbers[0];

即:

使用数组中的真实元素作为初始值。


三十一、求数组最小值

和最大值思路相同。

int[] numbers = {12, 35, 8, 99, 24};

int min = numbers[0];

for (int i = 1; i < numbers.length; i++) {

    if (numbers[i] < min) {

        min = numbers[i];
    }
}

System.out.println("最小值:" + min);

输出:

最小值:8

三十二、统计满足条件的元素个数

例如:

int[] scores = {90, 50, 75, 40, 88, 59};

统计及格人数。

int[] scores = {90, 50, 75, 40, 88, 59};

int count = 0;

for (int score : scores) {

    if (score >= 60) {

        count++;
    }
}

System.out.println(
        "及格人数:" + count
);

输出:

及格人数:3

三十三、查找数组中的元素

例如:

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

判断:

30 是否存在

代码:

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

int target = 30;

boolean found = false;

for (int number : numbers) {

    if (number == target) {

        found = true;

        break;
    }
}

if (found) {

    System.out.println("找到了");

} else {

    System.out.println("没有找到");
}

三十四、查找元素所在索引

如果不仅要知道是否存在,还要知道它在哪个位置,就需要普通 for。

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

int target = 30;

int index = -1;

for (int i = 0; i < numbers.length; i++) {

    if (numbers[i] == target) {

        index = i;

        break;
    }
}

System.out.println("索引:" + index);

输出:

索引:2

三十五、为什么 index 初始值常写 -1

数组合法索引:

0
1
2
3
...

不会出现:

-1

所以:

int index = -1;

可以表示:

暂时没有找到

如果循环结束后仍然:

index == -1

说明数组中不存在目标元素。

例如:

if (index == -1) {

    System.out.println("没有找到");

} else {

    System.out.println(
            "找到,索引为:" + index
    );
}

三十六、数组反转

例如:

原数组:

10 20 30 40 50

希望变成:

50 40 30 20 10

可以通过首尾交换完成。

代码:

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

for (int left = 0, right = numbers.length - 1;
     left < right;
     left++, right--) {

    int temp = numbers[left];

    numbers[left] = numbers[right];

    numbers[right] = temp;
}

for (int number : numbers) {

    System.out.println(number);
}

三十七、数组反转的交换过程

原数组:

10 20 30 40 50

第一次交换:

10 ↔ 50

变成:

50 20 30 40 10

第二次:

20 ↔ 40

变成:

50 40 30 20 10

中间:

30

不需要动。


三十八、数组元素交换

交换两个位置的数据经常需要临时变量。

例如:

int[] numbers = {10, 20};

int temp = numbers[0];

numbers[0] = numbers[1];

numbers[1] = temp;

结果:

20
10

核心模板:

int temp = a;

a = b;

b = temp;

后面学习排序算法时会大量使用。


三十九、冒泡排序入门

排序的目标:

把数组按照从小到大或从大到小排列

例如:

5 2 8 1 3

排序后:

1 2 3 5 8

最经典的入门排序之一:

冒泡排序

四十、冒泡排序基本思想

以从小到大排序为例。

每一轮:

比较相邻两个元素

如果:

前面的 > 后面的

就交换。

例如:

5 2 8 1

先比较:

5 和 2

因为:

5 > 2

交换:

2 5 8 1

再比较:

5 和 8

不用交换。

再比较:

8 和 1

交换:

2 5 1 8

第一轮结束后:

最大的 8

被移动到了最后。


四十一、冒泡排序代码

int[] numbers = {5, 2, 8, 1, 3};

for (int i = 0; i < numbers.length - 1; i++) {

    for (int j = 0;
         j < numbers.length - 1 - i;
         j++) {

        if (numbers[j] > numbers[j + 1]) {

            int temp = numbers[j];

            numbers[j] = numbers[j + 1];

            numbers[j + 1] = temp;
        }
    }
}

输出:

for (int number : numbers) {

    System.out.print(number + " ");
}

结果:

1 2 3 5 8

四十二、为什么外层是 length - 1

假设有:

5 个元素

最多只需要:

4 轮

就可以完成冒泡排序。

所以:

i < numbers.length - 1

四十三、为什么内层要减 i

代码:

j < numbers.length - 1 - i

因为每一轮都会把当前最大的元素移动到后面。

第一轮后:

最后一个位置已经确定

第二轮后:

倒数第二个位置已经确定

因此已经排好的位置就没必要重复比较。


四十四、优化冒泡排序

如果某一轮一次交换都没有发生:

说明数组已经有序

可以提前结束。

int[] numbers = {1, 2, 3, 4, 5};

for (int i = 0; i < numbers.length - 1; i++) {

    boolean swapped = false;

    for (int j = 0;
         j < numbers.length - 1 - i;
         j++) {

        if (numbers[j] > numbers[j + 1]) {

            int temp = numbers[j];

            numbers[j] = numbers[j + 1];

            numbers[j + 1] = temp;

            swapped = true;
        }
    }

    if (!swapped) {

        break;
    }
}

四十五、Arrays 工具类

Java 提供了一个处理数组的工具类:

java.util.Arrays

使用之前导入:

import java.util.Arrays;

它提供了很多常用功能。

例如:

数组输出
排序
查找
复制
填充
比较

四十六、Arrays.toString()

直接输出数组:

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

System.out.println(numbers);

通常不会直接得到:

[10, 20, 30]

而可能看到类似:

[I@1b6d3586

这不是我们想看的数组内容。


正确打印数组内容

可以使用:

Arrays.toString()

例如:

import java.util.Arrays;

public class Demo {

    public static void main(String[] args) {

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

        System.out.println(
                Arrays.toString(numbers)
        );
    }
}

输出:

[10, 20, 30]

这是调试数组时非常方便的方法。


四十七、Arrays.sort()

Java 已经提供了数组排序方法:

Arrays.sort()

例如:

import java.util.Arrays;

public class SortDemo {

    public static void main(String[] args) {

        int[] numbers = {5, 2, 8, 1, 3};

        Arrays.sort(numbers);

        System.out.println(
                Arrays.toString(numbers)
        );
    }
}

输出:

[1, 2, 3, 5, 8]

实际开发中,如果只是正常排序:

一般直接使用 Arrays.sort(),不需要自己手写冒泡排序。

手写冒泡排序的主要意义是:

练习循环
练习数组
练习交换
理解排序思想

四十八、Arrays.fill()

可以把整个数组填充成同一个值。

例如:

import java.util.Arrays;

int[] numbers = new int[5];

Arrays.fill(numbers, 100);

System.out.println(
        Arrays.toString(numbers)
);

输出:

[100, 100, 100, 100, 100]

四十九、Arrays.copyOf()

可以复制数组。

例如:

import java.util.Arrays;

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

int[] newArray =
        Arrays.copyOf(oldArray, 5);

System.out.println(
        Arrays.toString(newArray)
);

输出:

[10, 20, 30, 0, 0]

新数组长度:

5

原来的三个数据被复制过去,

多出来的位置使用默认值:

0

五十、数组为什么不能直接扩容

数组创建后长度固定。

例如:

int[] numbers = new int[3];

不能直接:

把 numbers 的长度改成 5

所谓数组“扩容”,实际通常是:

1. 创建一个更大的新数组

2. 把旧数组的数据复制过去

3. 让变量引用新数组

例如:

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

numbers = Arrays.copyOf(numbers, 5);

现在:

numbers

指向的是一个新的长度为 5 的数组。

这并不是原数组长度发生了变化。


五十一、System.arraycopy()

Java 还提供:

System.arraycopy()

用于复制数组中的一段数据。

语法:

System.arraycopy(
        源数组,
        源数组起始索引,
        目标数组,
        目标数组起始索引,
        复制长度
);

例如:

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

int[] target = new int[3];

System.arraycopy(
        source,
        1,
        target,
        0,
        3
);

复制的是:

source[1]
source[2]
source[3]

也就是:

20
30
40

输出:

System.out.println(
        Arrays.toString(target)
);

结果:

[20, 30, 40]

五十二、Arrays.equals()

可以比较两个数组中的元素是否相同。

例如:

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

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

System.out.println(
        Arrays.equals(a, b)
);

输出:

true

五十三、不要直接用 == 比较两个数组内容

例如:

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

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

System.out.println(a == b);

结果:

false

虽然内容一样,但:

a

和:

b

通常指向两个不同的数组对象。

如果比较数组元素内容:

Arrays.equals(a, b)

更合适。

这部分背后的原因会在面向对象和引用类型中继续学习。


五十四、Arrays.binarySearch()

Java 还提供二分查找:

Arrays.binarySearch()

例如:

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

int index =
        Arrays.binarySearch(numbers, 30);

System.out.println(index);

输出:

2

注意:

使用二分查找时,数组通常应该先保证有序。

例如:

Arrays.sort(numbers);

int index =
        Arrays.binarySearch(numbers, 30);

五十五、数组属于引用类型

虽然我们现在还没有正式学习引用类型,

但是需要先知道:

int
double
boolean
char

这些是基本数据类型。

而:

int[]
double[]
String[]

这些数组类型属于:

引用类型

例如:

int[] numbers = new int[3];

变量:

numbers

里面并不是直接保存三个整数。

它保存的是:

数组对象的引用

可以先简单理解成:

numbers 指向数组

五十六、数组在内存中的简单理解

例如:

int[] numbers = new int[3];

可以先用这种方式理解:

栈
┌──────────────┐
│ numbers      │
│      │       │
└──────│───────┘
       │
       ▼
堆
┌─────────────────┐
│ int[3]          │
│                 │
│  0   0   0      │
└─────────────────┘

这里先不用死记:

栈
堆
地址

只需要理解:

数组变量保存的是一个引用,通过这个引用找到真正的数组。

后面学习类和对象时会再次遇到这个概念。


五十七、两个变量指向同一个数组

例如:

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

int[] b = a;

这里并没有自动复制一个新数组。

而是:

a 和 b 指向同一个数组

例如:

b[0] = 100;

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

输出:

100

五十八、为什么修改 b 会影响 a

代码:

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

int[] b = a;

可以理解为:

a ─────┐
       ▼
      数组
       ▲
b ─────┘

两个变量:

a
b

都指向同一个数组。

所以通过:

b[0]

修改数据,

使用:

a[0]

看到的也是修改后的结果。


五十九、真正复制数组

如果希望:

修改 b 不影响 a

就需要创建一个新的数组。

例如:

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

int[] b =
        Arrays.copyOf(a, a.length);

现在:

a

和:

b

是两个不同数组。

修改:

b[0] = 100;

输出:

System.out.println(
        Arrays.toString(a)
);

System.out.println(
        Arrays.toString(b)
);

结果:

[10, 20, 30]

[100, 20, 30]

六十、null 数组引用

数组变量可以:

int[] numbers = null;

这里:

numbers

没有指向任何数组。

如果继续:

System.out.println(numbers.length);

运行时会出现:

NullPointerException

也就是:

空指针异常

六十一、数组的两个高频异常

学习数组时最常见的是:

ArrayIndexOutOfBoundsException
NullPointerException

数组越界

int[] numbers = new int[3];

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

错误:

ArrayIndexOutOfBoundsException

原因:

索引超出合法范围

空指针

int[] numbers = null;

System.out.println(numbers.length);

错误:

NullPointerException

原因:

numbers 没有指向真实数组

六十二、数组作为方法参数

虽然下一阶段才会更加系统地学习方法,

但数组和方法经常一起使用,可以先看一个简单例子。

public class Demo {

    public static void main(String[] args) {

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

        printArray(numbers);
    }

    public static void printArray(int[] array) {

        for (int number : array) {

            System.out.println(number);
        }
    }
}

这里方法参数:

int[] array

表示:

接收一个 int 数组

六十三、方法返回数组

方法也可以返回数组。

例如:

public static int[] createArray() {

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

    return numbers;
}

调用:

int[] result = createArray();

后面学习方法时会系统讲。

当前只需要知道:

数组和普通变量一样,可以作为参数,也可以作为返回值。


六十四、二维数组

一维数组可以理解为:

一排数据

例如:

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

二维数组可以简单理解成:

表格

例如:

90 80 70
88 95 76
60 75 82

这种数据就适合使用:

二维数组

六十五、二维数组的声明

int[][] scores;

可以理解:

int[][]

表示二维整数数组。


六十六、二维数组静态初始化

例如:

int[][] scores = {
        {90, 80, 70},
        {88, 95, 76},
        {60, 75, 82}
};

可以理解成:

        第0列 第1列 第2列

第0行     90    80    70

第1行     88    95    76

第2行     60    75    82

六十七、访问二维数组元素

访问:

第 1 行第 1 列

注意索引从 0 开始:

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

输出:

90

访问:

scores[1][2]

输出:

76

可以理解:

scores[行索引][列索引]

六十八、二维数组动态初始化

例如:

int[][] scores = new int[3][4];

表示:

3 行
4 列

可以理解:

0 0 0 0
0 0 0 0
0 0 0 0

六十九、二维数组长度

例如:

int[][] scores = {
        {90, 80, 70},
        {88, 95, 76}
};

外层长度:

scores.length

结果:

2

表示:

2 行

获取第一行长度:

scores[0].length

结果:

3

表示:

第一行有 3 个元素

七十、二维数组遍历

标准写法:

int[][] scores = {
        {90, 80, 70},
        {88, 95, 76},
        {60, 75, 82}
};

for (int i = 0;
     i < scores.length;
     i++) {

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

        System.out.print(
                scores[i][j] + " "
        );
    }

    System.out.println();
}

输出:

90 80 70
88 95 76
60 75 82

七十一、增强 for 遍历二维数组

也可以写:

for (int[] row : scores) {

    for (int value : row) {

        System.out.print(
                value + " "
        );
    }

    System.out.println();
}

这里:

row

本身就是一个:

int[]

也就是二维数组中的某一行。


七十二、二维数组其实是数组里面放数组

例如:

int[][] scores = {
        {90, 80, 70},
        {88, 95, 76}
};

可以简单理解成:

scores
│
├─ scores[0]
│   ├─ 90
│   ├─ 80
│   └─ 70
│
└─ scores[1]
    ├─ 88
    ├─ 95
    └─ 76

所以:

scores[0]

本身就是一个:

int[]

数组。


七十三、不规则二维数组

Java 二维数组每一行长度可以不同。

例如:

int[][] numbers = {
        {1, 2},
        {3, 4, 5},
        {6}
};

结构:

1 2
3 4 5
6

这在 Java 中是允许的。


七十四、动态创建不规则二维数组

例如:

int[][] numbers = new int[3][];

此时只确定:

有 3 行

每一行长度还没确定。

然后:

numbers[0] = new int[2];

numbers[1] = new int[4];

numbers[2] = new int[1];

于是结构变成:

第 0 行:2 个元素

第 1 行:4 个元素

第 2 行:1 个元素

七十五、二维数组练习:统计每个学生总分

例如:

int[][] scores = {
        {90, 80, 70},
        {88, 95, 76},
        {60, 75, 82}
};

可以理解:

每一行代表一个学生

每一列代表一门课程

计算每个学生总分:

for (int i = 0;
     i < scores.length;
     i++) {

    int sum = 0;

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

        sum += scores[i][j];
    }

    System.out.println(
            "第 " + (i + 1)
                    + " 名学生总分:"
                    + sum
    );
}

七十六、二维数组练习:求全班最高分

int[][] scores = {
        {90, 80, 70},
        {88, 95, 76},
        {60, 75, 82}
};

int max = scores[0][0];

for (int[] row : scores) {

    for (int score : row) {

        if (score > max) {

            max = score;
        }
    }
}

System.out.println(
        "最高分:" + max
);

输出:

最高分:95

七十七、字符串数组

数组不只是保存数字。

例如:

String[] names = {
        "张三",
        "李四",
        "王五"
};

遍历:

for (String name : names) {

    System.out.println(name);
}

输出:

张三
李四
王五

七十八、字符数组

例如:

char[] chars = {
        'J',
        'a',
        'v',
        'a'
};

遍历:

for (char c : chars) {

    System.out.print(c);
}

输出:

Java

后面学习字符串时,会发现:

字符串和字符数组

之间有很多联系。


七十九、使用 Scanner 给数组输入数据

例如:

输入 5 名学生成绩

代码:

import java.util.Scanner;

public class ScoreInputDemo {

    public static void main(String[] args) {

        Scanner scanner =
                new Scanner(System.in);

        int[] scores = new int[5];

        for (int i = 0;
             i < scores.length;
             i++) {

            System.out.print(
                    "请输入第 "
                            + (i + 1)
                            + " 名学生成绩:"
            );

            scores[i] =
                    scanner.nextInt();
        }

        System.out.println(
                "成绩录入完成"
        );

        scanner.close();
    }
}

八十、为什么显示学生编号时要 i + 1

数组索引:

0
1
2
3
4

但是用户更习惯看到:

第 1 名
第 2 名
第 3 名
...

所以显示时:

i + 1

例如:

"第 " + (i + 1) + " 名学生"

数组内部仍然使用:

scores[i]

八十一、综合案例:学生成绩统计

需求:

输入 5 名学生成绩,输出:

所有成绩
总分
平均分
最高分
最低分
及格人数
不及格人数

代码:

import java.util.Arrays;
import java.util.Scanner;

public class ScoreStatistics {

    public static void main(String[] args) {

        Scanner scanner =
                new Scanner(System.in);

        int[] scores = new int[5];

        for (int i = 0;
             i < scores.length;
             i++) {

            while (true) {

                System.out.print(
                        "请输入第 "
                                + (i + 1)
                                + " 名学生成绩:"
                );

                int score =
                        scanner.nextInt();

                if (score >= 0
                        && score <= 100) {

                    scores[i] = score;

                    break;
                }

                System.out.println(
                        "成绩必须在 0 ~ 100 之间"
                );
            }
        }

        int sum = 0;

        int max = scores[0];

        int min = scores[0];

        int passCount = 0;

        for (int score : scores) {

            sum += score;

            if (score > max) {

                max = score;
            }

            if (score < min) {

                min = score;
            }

            if (score >= 60) {

                passCount++;
            }
        }

        int failCount =
                scores.length - passCount;

        double average =
                (double) sum
                        / scores.length;

        System.out.println();

        System.out.println(
                "全部成绩:"
                        + Arrays.toString(scores)
        );

        System.out.println(
                "总分:" + sum
        );

        System.out.println(
                "平均分:" + average
        );

        System.out.println(
                "最高分:" + max
        );

        System.out.println(
                "最低分:" + min
        );

        System.out.println(
                "及格人数:" + passCount
        );

        System.out.println(
                "不及格人数:" + failCount
        );

        scanner.close();
    }
}

这个案例已经把前面几章的内容连接起来了:

变量
运算符
if
while
for
break
数组
Scanner

八十二、综合案例:随机点名基础版

准备一个学生姓名数组:

String[] names = {
        "张三",
        "李四",
        "王五",
        "赵六",
        "孙七"
};

随机生成一个合法索引:

int index =
        (int) (
                Math.random()
                * names.length
        );

然后:

System.out.println(
        "本次点名:"
                + names[index]
);

完整代码:

public class RandomNameDemo {

    public static void main(String[] args) {

        String[] names = {
                "张三",
                "李四",
                "王五",
                "赵六",
                "孙七"
        };

        int index =
                (int) (
                        Math.random()
                        * names.length
                );

        System.out.println(
                "本次点名:"
                        + names[index]
        );
    }
}

八十三、为什么 Math.random() 可以生成数组索引

Math.random()

会产生:

0.0 <= 随机数 < 1.0

假设数组长度:

5

那么:

Math.random() * 5

范围:

0.0 <= 数据 < 5.0

强制转成 int 后:

0
1
2
3
4

正好就是合法数组索引。


八十四、综合案例:数组去除指定元素的基础思路

数组长度固定,因此:

不能真正把某个位置删除后让数组自动缩短

例如:

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

如果想“删除”:

30

可以创建一个新数组:

int[] newArray = new int[numbers.length - 1];

然后把其他元素复制过去。

基础代码:

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

int target = 30;

int index = -1;

for (int i = 0;
     i < numbers.length;
     i++) {

    if (numbers[i] == target) {

        index = i;

        break;
    }
}

if (index != -1) {

    int[] newArray =
            new int[numbers.length - 1];

    int newIndex = 0;

    for (int i = 0;
         i < numbers.length;
         i++) {

        if (i == index) {

            continue;
        }

        newArray[newIndex] =
                numbers[i];

        newIndex++;
    }

    numbers = newArray;
}

这个例子可以帮助理解:

数组长度固定,所以很多“增删”操作,本质上都是重新创建数组。

这也是后面学习集合框架的重要铺垫。


八十五、综合案例:向数组末尾添加元素

原数组:

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

希望添加:

40

因为数组不能直接扩容,所以:

numbers =
        Arrays.copyOf(
                numbers,
                numbers.length + 1
        );

新位置:

numbers[numbers.length - 1] = 40;

完整:

import java.util.Arrays;

public class AddDemo {

    public static void main(String[] args) {

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

        numbers =
                Arrays.copyOf(
                        numbers,
                        numbers.length + 1
                );

        numbers[
                numbers.length - 1
        ] = 40;

        System.out.println(
                Arrays.toString(numbers)
        );
    }
}

输出:

[10, 20, 30, 40]

八十六、数组和集合的区别先简单了解

目前只需要了解一个最核心区别。

数组:

长度固定

以后学习:

ArrayList

等集合后,可以更方便地:

添加
删除
修改
查找

数据。

但是数组仍然非常重要。

因为:

数组速度快
结构简单
很多底层实现都会使用数组

所以不能因为以后有集合,就跳过数组。


八十七、数组常见操作模板

1. 遍历

for (int i = 0;
     i < array.length;
     i++) {

    System.out.println(array[i]);
}

2. 增强 for

for (int value : array) {

    System.out.println(value);
}

3. 求和

int sum = 0;

for (int value : array) {

    sum += value;
}

4. 求最大值

int max = array[0];

for (int i = 1;
     i < array.length;
     i++) {

    if (array[i] > max) {

        max = array[i];
    }
}

5. 求最小值

int min = array[0];

for (int i = 1;
     i < array.length;
     i++) {

    if (array[i] < min) {

        min = array[i];
    }
}

6. 查找索引

int index = -1;

for (int i = 0;
     i < array.length;
     i++) {

    if (array[i] == target) {

        index = i;

        break;
    }
}

7. 反转

for (int left = 0,
         right = array.length - 1;
     left < right;
     left++, right--) {

    int temp = array[left];

    array[left] = array[right];

    array[right] = temp;
}

八十八、数组学习中最容易混淆的几个点

1. 长度和最大索引不是一回事

例如:

int[] array = new int[5];

长度:

5

最大索引:

4

即:

array.length - 1

2. length 没有括号

正确:

array.length

错误:

array.length()

3. 数组索引从 0 开始

第一个元素 → 0

不是:

第一个元素 → 1

4. 数组长度固定

new int[5]

创建之后就不能原地改成:

new int[10]

所谓扩容通常是重新创建数组。


5. a = b 不代表复制数组内容

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

int[] b = a;

只是让:

a
b

指向同一个数组。


6. 增强 for 不适合通过变量修改基本类型数组

for (int value : array) {

    value = 100;
}

不会真正修改原数组元素。


八十九、常见错误整理

错误 1:索引越界

错误:

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

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

合法索引只有:

0
1
2

错误 2:遍历条件写成 <=

错误:

for (int i = 0;
     i <= numbers.length;
     i++) {

    System.out.println(
            numbers[i]
    );
}

正确:

for (int i = 0;
     i < numbers.length;
     i++) {

    System.out.println(
            numbers[i]
    );
}

错误 3:数组为 null

int[] numbers = null;

System.out.println(
        numbers.length
);

会出现:

NullPointerException

错误 4:最大值随便初始化成 0

int[] numbers = {
        -10, -20, -5
};

int max = 0;

会导致错误结果。

推荐:

int max = numbers[0];

错误 5:数组为空时直接访问 array[0]

例如:

int[] numbers = new int[0];

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

会越界。

因为:

长度为 0

就没有任何合法索引。


错误 6:直接输出数组变量

System.out.println(numbers);

不一定得到数组内容。

推荐:

System.out.println(
        Arrays.toString(numbers)
);

错误 7:使用 == 比较数组内容

不推荐:

a == b

比较内容可以:

Arrays.equals(a, b)

九十一、本章核心知识总结

数组的定义:

保存一组相同类型的数据

声明:

int[] numbers;

动态初始化:

int[] numbers =
        new int[5];

静态初始化:

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

访问元素:

numbers[0]

修改元素:

numbers[0] = 100;

长度:

numbers.length

合法索引:

0 ~ numbers.length - 1

标准遍历:

for (int i = 0;
     i < numbers.length;
     i++) {

    System.out.println(
            numbers[i]
    );
}

增强 for:

for (int number : numbers) {

    System.out.println(number);
}

二维数组:

int[][] numbers = {
        {1, 2, 3},
        {4, 5, 6}
};

九十二、Arrays 常用方法总结

需要导入:

import java.util.Arrays;

数组转字符串:

Arrays.toString(array);

排序:

Arrays.sort(array);

复制:

Arrays.copyOf(
        array,
        newLength
);

填充:

Arrays.fill(
        array,
        value
);

比较内容:

Arrays.equals(a, b);

二分查找:

Arrays.binarySearch(
        array,
        target
);

九十三、必须重点掌握

学完数组后,下面这些必须能自己写。

1. 创建数组

2. 静态初始化

3. 动态初始化

4. 根据索引访问和修改元素

5. 使用 length

6. 使用 for 遍历数组

7. 使用增强 for 遍历数组

8. 数组求和

9. 数组求平均值

10. 求最大值和最小值

11. 查找某个元素

12. 查找目标索引

13. 数组元素交换

14. 数组反转

15. 二维数组遍历

16. Arrays.toString()

17. Arrays.sort()

18. 理解数组长度固定

19. 理解数组索引越界

20. 理解 null 和空指针

九十四、和下一章面向对象的衔接

目前数组可以保存:

int[]
double[]
String[]

但真实开发中,一条完整的数据往往包含很多字段。

例如一个学生:

姓名
年龄
性别
学号
专业
成绩

如果全部使用多个数组:

String[] names;
int[] ages;
String[] studentNumbers;
double[] scores;

数据关系会越来越难管理。

这时候就需要下一章:

类和对象

例如以后可以创建:

Student[] students;

数组中的每一个元素都可以是一个:

Student 对象

所以从学习顺序上:

变量
↓
条件和循环
↓
数组
↓
类和对象

是非常自然的一条路线。

数组解决的是:

如何批量保存同类型数据