目录
[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 数组拷贝)
[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};//静态和动态初始化也可以分为两步,但是省略格式不可以。
- 如果没有对数组进行初始化,数组中元素有其默认值
- 如果数组中存储元素类型为基类类型,默认值为基类类型对应的默认值,比如:
类型 | 默认值 |
---|---|
byte | 0 |
short | 0 |
int | 0 |
long | 0 |
float | 0.0f |
double | 0.0 |
char | /u0000 |
boolean | false |
- 如果数组中存储元素类型为引用类型,默认值为null
- 静态和动态初始化也可以分为两步,但是省略格式不可以。
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下标的值.
【注意事项】
-
数组是一段连续的内存空间,因此支持随机访问,即通过下标访问快速访问数组中任意位置的元素
-
下标从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 遍历数组
所谓 "遍历" 是指将数组中的所有元素都访问一遍, 访问是指对数组中的元素进行某种操作.
- 使用
for
循环遍历
java
int[] array1 = {1, 2, 3};
for (int i = 0; i < array1.length; i++) {
System.out.print(array1[i] + " ");
}
System.out.println();
- 使用
for-each
循环遍历
java
// for each: 增强for循环
// 数组当中数据的类型定义的变量 : 数组名
for (int x : array1) {
System.out.print(x + " ");
}
System.out.println();
两者区别:
for
可以拿到数组的下标, 而for-each
拿不到数组的下标.如果去遍历数组只是为了简单的打印, 就用
for-each
即可.
如果有需要使用数组下标, 就使用for
.
- 使用工具类
Arrays
Java中有一个工具类, 可以专门用来操作数组, 这个工具类叫做Arrays
.
那么要使用它, 就需要导包, 也就是在所有代码的前面加上:
java
import java.util.Arrays;
于是就可以使用toString()
方法.
java
//把数组转变为字符串,然后返回
String ret = Arrays.toString(array1);
2. 数组是引用类型
要理解什么是引用, 首先先来了解JVM内存的划分.
2.1 初识JVM的内存分布
为什么要进行内存的划分?
对于我们内存来说, 它是一块连续的空间. 那么我们怎么知道我们的数据放到哪里?
所以我们需要对内存进行划分.
内存是一段连续的存储空间,主要用来存储程序运行时数据的。比如:
-
程序运行时代码需要加载到内存
-
程序运行产生的中间数据要存放在内存
-
程序中的常量也要保存
-
有些数据可能需要长时间存储,而有些数据当方法运行结束后就要被销毁
如果对内存中存储的数据不加区分的随意存储,那对内存管理起来将会非常麻烦。
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 数组排序(冒泡排序)
给定一个数组, 让数组升序 (降序) 排序.
算法思路
假设排升序:
-
将数组中相邻元素从前往后依次进行比较,如果前一个元素比后一个元素大,则交换,一趟下来后最大元素就在数组的末尾
-
依次重复上述过程,直到数组中所有的元素都排列好
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));
}