Java —— 数组

目录

[1. 数组的基本概念](#1. 数组的基本概念)

[1.1 什么是数组? 为什么要有数组?](#1.1 什么是数组? 为什么要有数组?)

[1.2 数组的创建及初始化](#1.2 数组的创建及初始化)

[1.3 数组的使用](#1.3 数组的使用)

[1.3.1 数组中元素访问](#1.3.1 数组中元素访问)

[1.3.4 遍历数组](#1.3.4 遍历数组)

[2. 数组是引用类型](#2. 数组是引用类型)

[2.1 初识JVM的内存分布](#2.1 初识JVM的内存分布)

[2.2 基本类型变量与引用类型变量](#2.2 基本类型变量与引用类型变量)

[2.3 认识null](#2.3 认识null)

[3. 数组的应用场景](#3. 数组的应用场景)

[3.1 保存数据](#3.1 保存数据)

[3.2 作为函数的参数](#3.2 作为函数的参数)

[3.3 作为函数的返回值](#3.3 作为函数的返回值)

[4. 数组练习题](#4. 数组练习题)

[4.1 数组转字符串](#4.1 数组转字符串)

[4.2 数组拷贝](#4.2 数组拷贝)

Arrays.copyOf()的源码

[4.3 求数组中元素的平均值](#4.3 求数组中元素的平均值)

[4.4 查找数组中指定元素(顺序查找)](#4.4 查找数组中指定元素(顺序查找))

[4.5 查找数组中指定元素(二分查找)](#4.5 查找数组中指定元素(二分查找))

[4.6 数组逆序](#4.6 数组逆序)

[4.7 数组排序(冒泡排序)](#4.7 数组排序(冒泡排序))

[5. 二维数组](#5. 二维数组)

[5.1 基本语法](#5.1 基本语法)

[5.2 代码示例](#5.2 代码示例)

[5.3 打印二维数组](#5.3 打印二维数组)


1. 数组的基本概念

1.1 什么是数组? 为什么要有数组?

我们举一个例子.

如果要定义三个整型变量, 在本篇文章学习之前一般会这样做:

java 复制代码
int a1 = 1;
int a2 = 2;
int a3 = 3;

如果是要定义更多个整型变量, 我们就会接着往下按照类型 变量名 = 值;去写.

但是会发现不管是几个变量, 他们的类型都是一样的, 属于同一种数据类型, 那么我们把同一种数据类型组织起来, 通过一行代码解决, 这就是我们的数组.

1.2 数组的创建及初始化

在Java中创建(定义)数组就是类型[] 数组名 = {};, 如下所示:

java 复制代码
int[] array = {1, 2, 3};    // 定义了一个数组, 有三个变量.

同样我们可以定义其他类型的数组.

java 复制代码
float[] array2 = {1.0f, 2.5f};

以上面的array数组为例, 它创建的时候是在内存中给它开辟一块连续的空间, 空间大小是3.

给每一个位置编号的时候是从0开始编号.

数组的定义:数组是一块连续的存储空间, 存储的是相同数据类型的元素.

那么如何通过下标访问数据呢? 我们来看.

比如: array[1], 这里的元素是一个整数, 使用ret来接收一下.

java 复制代码
int[] array = {1, 2, 3};
int ret = array[1];
System.out.println(ret);

输出结果:
2

注意, 通过下标访问数据时的下标不可以超出数组的大小, 如果超出会异常.

可以看到, 报错"数组下标超出了范围", 也就是"数组越界异常".

数组的下标范围: [0, len-1] (len为数组长度)

Java中输出数组长度可以通过数组名.length得出数组长度

java 复制代码
int len = array.length;
System.out.println(len);

简单区分几种定义数组的方式:

java 复制代码
int[] array1 = {1, 2, 3};//直接赋值  静态初始化
//             new type[] {dates};
int[] array2 = new int[]{1, 2, 3, 4}; //动态初始化, 不指定数组长度, 根据元素个数确定

以上两种没有本质上的区别, 只有写法上的区别.

再看以下的写法:

java 复制代码
int[] array3 = new int[10];//只是分配了内存 但是没有进行赋值 只有默认值。

int[] array4;
array4 = new int[]{10, 20, 30};//静态和动态初始化也可以分为两步,但是省略格式不可以。
  1. 如果没有对数组进行初始化,数组中元素有其默认值
    • 如果数组中存储元素类型为基类类型,默认值为基类类型对应的默认值,比如:
类型 默认值
byte 0
short 0
int 0
long 0
float 0.0f
double 0.0
char /u0000
boolean false
  • 如果数组中存储元素类型为引用类型,默认值为null
  1. 静态和动态初始化也可以分为两步,但是省略格式不可以。

1.3 数组的使用

1.3.1 数组中元素访问

我们可以通过下标来访问数组中的元素, 也可以通过下标去修改元素.

java 复制代码
int[] array1 = {1, 2, 3};   // 假设这是我们的数组
System.out.println(array1[1]);  // 我们可以通过 sout 来输出它的 1下标
array1[1] = 99; // 也可以通过 array1 把 1下标 的值改成 99.
System.out.println(array1[1]);

以上代码说明我们不仅可以通过 1下标 来访问 1 下标的值, 也可以修改 1下标的值.

【注意事项】

  1. 数组是一段连续的内存空间,因此支持随机访问,即通过下标访问快速访问数组中任意位置的元素

  2. 下标从0开始,介于[0, N)之间不包含N,N为元素个数,不能越界,否则会报出下标越界异常。

java 复制代码
int[] array = {1, 2, 3};
System.out.println(array[3]); // 数组中只有3个元素,下标一次为:0 1 2,array[3]下标越界

执行结果:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 100
at Test.main(Test.java:4)

1.3.4 遍历数组

所谓 "遍历" 是指将数组中的所有元素都访问一遍, 访问是指对数组中的元素进行某种操作.

  1. 使用for循环遍历
java 复制代码
int[] array1 = {1, 2, 3};
for (int i = 0; i < array1.length; i++) {
    System.out.print(array1[i] + " ");
}
System.out.println();
  1. 使用for-each循环遍历
java 复制代码
// for each: 增强for循环
// 数组当中数据的类型定义的变量 : 数组名    
for (int x : array1) {
    System.out.print(x + " ");
}
System.out.println();

两者区别:
for可以拿到数组的下标, 而 for-each拿不到数组的下标.

如果去遍历数组只是为了简单的打印, 就用 for-each即可.
如果有需要使用数组下标, 就使用 for.

  1. 使用工具类Arrays

Java中有一个工具类, 可以专门用来操作数组, 这个工具类叫做Arrays.

那么要使用它, 就需要导包, 也就是在所有代码的前面加上:

java 复制代码
import java.util.Arrays;

于是就可以使用toString()方法.

java 复制代码
//把数组转变为字符串,然后返回
String ret = Arrays.toString(array1);

2. 数组是引用类型

要理解什么是引用, 首先先来了解JVM内存的划分.

2.1 初识JVM的内存分布

为什么要进行内存的划分?
对于我们内存来说, 它是一块连续的空间. 那么我们怎么知道我们的数据放到哪里?
所以我们需要对内存进行划分.

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

  1. 程序运行时代码需要加载到内存

  2. 程序运行产生的中间数据要存放在内存

  3. 程序中的常量也要保存

  4. 有些数据可能需要长时间存储,而有些数据当方法运行结束后就要被销毁

如果对内存中存储的数据不加区分的随意存储,那对内存管理起来将会非常麻烦。


JVM对所使用的内存按照功能的不同进行了划分:

可以看到, 有方法区, 虚拟机栈, 本地方法栈, 堆, 程序计数器. 总共有五块内存, 这五块内存所存储的数据是不同的. 比如我们平时所说的局部变量的内存就存在虚拟机栈中. 再比如本地方法栈, JVM有部分代码底层是C/C++写的, 那么这部分代码就是在本地方法栈中. 再比如堆, 我们所说的对象, 数组就是在堆上, 方法区就是存放静态变量的. 程序计数器就用于保存下一条执行的指令的地址.

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

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

2.2 基本类型变量与引用类型变量

基本数据类型创建的变量,称为基本变量,该变量空间中直接存放的是其所对应的值;

而引用数据类型创建的变量,一般称为对象的引用,其空间中存储的是对象所在空间的地址。


以下代码在内存中应该如何存储?

java 复制代码
int a = 10;
int b = 20;
int[] array = {1, 2, 3, 4};

那么我们如果直接打印array, 会看到一个类似于地址的东西.

java 复制代码
System.out.println(array);

这串字符串可以先简单认为就是地址, 但是它并不是真实的地址, 而是地址经过哈希得到的.
引用变量并不直接存储对象本身,可以简单理解成存储的是对象在堆中空间的起始地址。通过该
地址,引用变量便可以去操作对象.


接下来我们看一些代码来感受一下.
代码示例1

java 复制代码
int[] array = {1, 2, 3, 4}; // 定义一个数组
System.out.println(Arrays.toString(array));// 输出这个数组, 输出为 1234

int[] array2 = array;
array2[1] = 99;
System.out.println(Arrays.toString(array));
System.out.println(Arrays.toString(array2));

执行结果:

代码示例2

java 复制代码
int[] array = {1, 2, 3, 4};
int[] array2 = {4, 5, 6, 7, 8};
array = array2;
System.out.println(Arrays.toString(array));
System.out.println(Arrays.toString(array2));

执行结果:

一个引用能否同时指向多个对象?
不可以. 由上例可知array这个变量存储的引用只能有一个.
再比如: int a = 10 = 20 = 30;显然错误, 一个变量里面所存储的值只能有一个.

2.3 认识null

null 在 Java 中表示 "空引用" , 也就是一个不指向对象的引用.

java 复制代码
// 引用类型的赋值
int[] array = null; // 代表这个引用不指向任何的对象
System.out.println(array);

当引用不指向任何对象的时候, 如果尝试对这个引用做一些事情的时候, 就会发生空指针异常.

java 复制代码
System.out.println(array.length);

null 的作用类似于 C 语言中的 NULL (空指针), 都是表示一个无效的内存位置. 因此不能对这个内存进行任何读写操作. 一旦尝试读写, 就会抛出 NullPointerException.

注意: Java 中并没有约定 null 和 0 号地址的内存有任何关联.

3. 数组的应用场景

3.1 保存数据

java 复制代码
public static void main(String[] args) {
    int[] array = {1, 2, 3};
    for(int i = 0; i < array.length; ++i){
        System.out.println(array[i] + " ");
    }
}

3.2 作为函数的参数

有如下数组:

java 复制代码
int[] array = {1, 2, 3, 4};

接下来我们写两个函数来看一下这两个函数有什么区别:

java 复制代码
public static void func1(int[] array) {
    array = new int[10];
}

public static void func2(int[] array) {
    array[0] = 99;
}

当我们调用其中一个函数的时候, 输出的结果是什么?

java 复制代码
func1(array);
//func2(array);
System.out.println(Arrays.toString(array));

分析调用func1:

所以可以知道: 不是传引用就可以改变实参的值.

分析调用func2:

总结: 所谓的 "引用" 本质上只是存了一个地址. Java 将数组设定成引用类型, 这样的话后续进行数组参数传参, 其实只是将数组的地址传入到函数形参中. 这样可以避免对整个数组的拷贝(数组可能比较长, 那么拷贝开销就会很大).

3.3 作为函数的返回值

java 复制代码
    // 在 Java 中可以返回一整个数组
    public static int[] func3() {
        int[] tmp = {1, 2, 3, 4, 5, 6, 7};
        return tmp;
    }

    public static void main(String[] args) {
        int[] ret = func3();
        System.out.println(Arrays.toString(ret));
    }

代码示例

java 复制代码
    public static void swap(int[] array) {
        int tmp = array[0];
        array[0] = array[1];
        array[1] = tmp;
    }

    public static void main(String[] args) {
        int[] tmp = {1, 2};
        System.out.println("交换前:" + tmp[0] + " " + tmp[1]);
        swap(tmp);
        System.out.println("交换后:" + tmp[0] + " " + tmp[1]);
    }

4. 数组练习题

4.1 数组转字符串

对于数组转字符串来说其实前文已经提过, 就是使用工具类Arrays的toString()方法.
那么我们这里就用自己写代码, 模拟实现一下toString().

首先来看一下我们在使用原来的toString是什么样的一个结果:

java 复制代码
    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4};
        String ret = Arrays.toString(array);
        System.out.println(ret);
    }

可以看到结果字符串由中括号, 逗号, 数字组成.

那么显然我们需要写出字符串拼接的效果.

于是可以自然而然的写出如下代码:

java 复制代码
    public static String myToString(int[] tmp) {
        // 比普通的数据多了中括号和逗号, 所以最后的返回值是要先有"[", 而后进行字符拼接
        String ret = "[";
        for (int i = 0; i < tmp.length; i++) {  // 拿到每个元素数据
            ret = ret + tmp[i] + ",";   // 每次有一个数据就拼接上
        }
        ret += "]";    // 拼接完数字就补"]"在结尾
        return ret;
    }

但是上面的代码是有问题的, 我们对这份我们第一次写出的代码进行测试:

java 复制代码
    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4};
//        String ret = Arrays.toString(array);
        String ret = myToString(array);
        System.out.println(ret);
    }

在运行结果中, 会发现最后一个应该是要不加逗号的, 所以加逗号的逻辑是++++最后一个不加++++.

java 复制代码
    public static String myToString(int[] tmp) {
        // 注意: 增加非空判断, 否则会有空指针异常
        if (tmp == null) {
            return "null";
        }
        String ret = "[";
        for (int i = 0; i < tmp.length; i++) {  // 拿到每个元素数据
//            ret = ret + tmp[i] + ",";   // 每次有一个数据就拼接上 -> 但, 加逗号不能加在这里
            ret += tmp[i];
            if (i != tmp.length - 1) {  // i是最后一个元素的时候就不加逗号, 也就是: 不是最后一个元素的时候才能加逗号
                ret += ",";
            }
        }
        ret += "]";    // 拼接完数字就补"]"在结尾
        return ret;
    }

4.2 数组拷贝

注意拷贝的前提: 拷贝需要有原内容, 并且拷贝后会产生一个一模一样的新内容, 才是拷贝.
我们先来看以下代码.

java 复制代码
int[] array = {1, 3, 5, 7, 9};
int[] array2 = array;

这个代码并不是拷贝, 因为这个代码根本没有产生新的内存空间.


所以:

java 复制代码
int[] array = {1, 2, 3, 4, 5};
int[] array2 = new int[array.length];
// 接下来就可以通过 for循环 把 array 中每个元素拷贝到 array2 中

完整代码:

java 复制代码
    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4, 5};
        int[] array2 = new int[array.length];
        // 把对应的元素拷贝到 array2 中
        for (int i = 0; i < array.length; i++) {
            array2[i] = array[i];
        }
        System.out.println(Arrays.toString(array));
        System.out.println(Arrays.toString(array2));
    }

但是以后我们的拷贝都不用这么麻烦, 直接使用工具类Arrays类的copyOf()方法即可.

java 复制代码
    public static void main(String[] args) {
        int[] array = {1, 3, 5, 7, 91, 11, 22, 44, 88, 18, 29, 17, 14};
        int[] array2 = Arrays.copyOf(array,array.length);
        System.out.println(Arrays.toString(array));
        System.out.println(Arrays.toString(array2));
    }

补充小技巧: 数组扩容, 在数据结构中会有涉及到.

java 复制代码
//扩容 2倍
int[] array2 = Arrays.copyOf(array, 2 * array.length);

Arrays.copyOf()的源码

我们也可以用一下arraycopy()方法.

java 复制代码
    public static void main(String[] args) {
        int[] array = {1, 3, 5, 7, 91, 11, 22, 44, 88, 18, 29, 17, 14};
        int[] copy = new int[array.length];
        // 注: 也支持局部的拷贝
        System.arraycopy(array, 0, copy, 0, array.length);
        System.out.println(Arrays.toString(array));
        System.out.println(Arrays.toString(copy));
    }
java 复制代码
    public static void main(String[] args) {
        int[] array = {1, 3, 5, 7, 91, 11, 22, 44, 88, 18, 29, 17, 14};
        // copyOfRange() 也可进行 局部拷贝, range是范围的意思
        int[] array2 = Arrays.copyOfRange(array, 2, 5);// [2,5) 左闭右开
        System.out.println(Arrays.toString(array));
        System.out.println(Arrays.toString(array2));
    }

注意:数组当中存储的是基本类型数据时,不论怎么拷贝基本都不会出现什么问题,但如果存储的是引用数据类型,拷贝时需要考虑深浅拷贝的问题,关于深浅拷贝在后续详细介绍。

4.3 求数组中元素的平均值

给定一个整型数组, 求平均值.

java 复制代码
    public static double avg(int[] array) {
        int sum = 0;
        for (int x : array) {
            sum += x;
        }
        return sum * 1.0 / array.length;
    }

4.4 查找数组中指定元素(顺序查找)

给定一个数组, 再给定一个元素, 找出该元素在数组中的位置.

java 复制代码
    public static int find(int[] array, int key) {
        for (int i = 0; i < array.length; i++) {
            if (array[i] == key) {
                return i;
            }
        }
        return -1;
    }

4.5 查找数组中指定元素(二分查找)

针对有序数组, 可以使用更高效的二分查找.

啥叫有序数组?
有序分为 "升序" 和 "降序"
如 1 2 3 4 , 依次递增即为升序.
如 4 3 2 1 , 依次递减即为降序.

以升序数组为例, 二分查找的思路是先取中间位置的元素, 然后使用待查找元素与数组中间元素进行比较:

  • 如果相等,即找到了返回该元素在数组中的下标
  • 如果小于,以类似方式到数组左半侧查找
  • 如果大于,以类似方式到数组右半侧查找
java 复制代码
    //建立在有序的情况下
    public static int binarySearch(int[] array, int key) {
        int left = 0;
        int right = array.length - 1;
        while (left <= right) {
            int mid = (left + right) >>> 1;
            if (array[mid] < key) {
                left = mid + 1;
            } else if (array[mid] > key) {
                right = mid - 1;
            } else {
                return mid;
            }
        }
        return -1;
    }

让数组有序(升序):

java 复制代码
Arrays.sort(array);// 不是冒泡排序 底层快排

4.6 数组逆序

给定一个数组, 将里面的元素逆序排列.

思路

设定两个下标, 分别指向第一个元素和最后一个元素. 交换两个位置的元素.

然后让前一个下标自增, 后一个下标自减, 循环继续即可.

java 复制代码
    public static void reverse(int[] array) {
        int left = 0;
        int right = array.length - 1;
        while (left < right) {
            int tmp = array[left];
            array[left] = array[right];
            array[right] = tmp;
            left++;
            right--;
        }
    }

4.7 数组排序(冒泡排序)

给定一个数组, 让数组升序 (降序) 排序.

算法思路

假设排升序:

  1. 将数组中相邻元素从前往后依次进行比较,如果前一个元素比后一个元素大,则交换,一趟下来后最大元素就在数组的末尾

  2. 依次重复上述过程,直到数组中所有的元素都排列好

java 复制代码
    public static void bubbleSort(int[] array) {
        //i控制趟数
        for (int i = 0; i < array.length - 1; i++) {
            //优化:检查某一趟之后,是否有序了?
            boolean flg = false;
            //array.length - 1 - i 中的 -i 优化的是 每一趟比较的次数
            for (int j = 0; j < array.length - 1 - i; j++) {
                if (array[j] > array[j + 1]) {
                    int tmp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = tmp;
                    flg = true;
                }
            }
            if (flg == false) {
                return;
            }
        }
    }

5. 二维数组

二维数组本质上也就是一维数组, 只不过每个元素又是一个一维数组.

5.1 基本语法

java 复制代码
数据类型[][] 数组名称 = new 数据类型 [行数][列数] { 初始化数据 };

5.2 代码示例

java 复制代码
int[][] array1 = new int[2][3];
int[][] array2 = new int[][]{{1, 2, 3}, {4, 5, 6}};
int[][] arrays = {{1, 2, 3}, {4, 5, 6}};

5.3 打印二维数组

java 复制代码
    public static void main(String[] args) {
        //2行3列的
        int[][] array1 = new int[2][3];
        for (int i = 0; i < 2; i++) {
            for (int j = 0; j < 3; j++) {
                System.out.print(array1[i][j] + " ");
            }
            System.out.println();
        }
        System.out.println();
        System.out.println(Arrays.deepToString(array1));
    }
相关推荐
ZSYP-S几秒前
Day 15:Spring 框架基础
java·开发语言·数据结构·后端·spring
yuanbenshidiaos3 分钟前
c++------------------函数
开发语言·c++
yuanbenshidiaos7 分钟前
C++----------函数的调用机制
java·c++·算法
唐叔在学习12 分钟前
【唐叔学算法】第21天:超越比较-计数排序、桶排序与基数排序的Java实践及性能剖析
数据结构·算法·排序算法
程序员_三木15 分钟前
Three.js入门-Raycaster鼠标拾取详解与应用
开发语言·javascript·计算机外设·webgl·three.js
是小崔啊25 分钟前
开源轮子 - EasyExcel01(核心api)
java·开发语言·开源·excel·阿里巴巴
ALISHENGYA31 分钟前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(switch语句)
数据结构·算法
tianmu_sama31 分钟前
[Effective C++]条款38-39 复合和private继承
开发语言·c++
chengooooooo33 分钟前
代码随想录训练营第二十七天| 贪心理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和
算法·leetcode·职场和发展
黄公子学安全34 分钟前
Java的基础概念(一)
java·开发语言·python