1.数组的基本用法
1.1 什么是数组
数组本质上就是让我们能"批量"创建相同类型的变量
例如:
如果需要表示两个数据,那么直接创建两个变量即可 int a,int b
如果需要表示五个数据,那么可以创建五个变量 int a1,int a2,int a3,int a4,int a5
但是如果需要表示一万个数据,那么就不能创建一万个变量了,这时候就需要使用数组,帮我们批量创建
注意:在Java中,数组中包含的变量必须是相同类型
1.2 创建数组
基本语法
// 动态初始化
数据类型[] 数组名称 = new 数据类型 [] { 初始化数据 };
// 静态初始化
数据类型[] 数组名称 = { 初始化数据 };
代码示例
int[] arr = new int[]{1, 2, 3};
int[] arr = {1, 2, 3};
注意: 静态初始化的时候,数组元素个数和初始化数据的格式是一致的
其实数组也可以写成int arr[ ] = {1, 2, 3}
这样就和 C 语言更相似了,但是我们还是更推荐写成 int[ ] arr 的形式,int和 [ ] 是一个整体
1.3 数组的使用
代码示例: 获取长度&访问元素
int[] arr = {1, 2, 3};
// 获取数组长度
System.out.println("length: " + arr.length); // 执行结果: 3
// 访问数组中的元素
System.out.println(arr[1]); // 执行结果: 2
System.out.println(arr[0]); // 执行结果: 1
arr[2] = 100;
System.out.println(arr[2]);// 执行结果: 100
注意
- 使用 arr.length 能够获取到数组的长度, “.” 这个操作为成员访问操作符,后面在面向对象中会经常用到
- 使用 [ ] 按下标取数组元素,需要注意,下标从 0 开始计数
- 使用 [ ] 操作既能读取数据,也能修改数据
- 下标访问操作不能超出有效范围 [0, length - 1],如果超出有效范围,会出现下标越界异常
代码示例: 下标越界
int[] arr = {1, 2, 3};
System.out.println(arr[100]);
// 执行结果
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 100
at Test.main(Test.java:4)
抛出了 java.lang.ArrayIndexOutOfBoundsException 异常,使用数组一定要下标谨防越界
代码示例: 遍历数组
所谓 “遍历” 是指将数组中的所有元素都访问一遍,不重不漏,通常需要搭配循环语句
int[] arr = {1, 2, 3};
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
// 执行结果
1
2
3
代码示例: 使用for-each遍历数组
int[] arr = {1, 2, 3};
for (int x : arr) {
System.out.println(x);
}
// 执行结果
1
2
3
for-each 是 for 循环的另外一种使用方式,能够更方便的完成对数组的遍历,可以避免循环条件和更新语句写错
2.数组作为方法的参数
2.1 基本用法
代码示例: 打印数组内容
public static void main(String[] args) {
int[] arr = {1, 2, 3};
printArray(arr);
}
public static void printArray(int[] a) {
for (int x : a) {
System.out.println(x);
}
}
// 执行结果
1
2
3
在这个代码中
int[] a 是函数的形参,int[] arr 是函数实参
如果需要获取到数组长度,同样可以使用 a.length
2.2 理解引用类型(重点)
我们尝试以下代码
代码示例1: 参数传内置类型
public static void main(String[] args) {
int num = 0; func(num);
System.out.println("num = " + num);
}
public static void func(int x) {
x = 10;
System.out.println("x = " + x);
}
// 执行结果
x = 10
num = 0
我们发现 修改形参x的值,不影响实参num的值
代码示例2: 参数传数组类型
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
func(arr);
System.out.println("arr[0] = " + arr[0]);
}
public static void func(int[] a) {
a[0] = 10; System.out.println("a[0] = " + a[0]);
}
// 执行结果
a[0] = 10
arr[0] = 10
我们发现, 在函数内部修改数组内容,函数外部也发生改变
此时数组名 arr 是一个 “引用” ,当传参的时候,是按照引用传参
这里我们要先从内存说起
如何理解内存?
内存就是指我们熟悉的 “内存”,内存可以直观的理解成一个宿舍楼,有一个长长的大走廊,上面有很多房间
每个房间的大小是 1 Byte (如果计算机有 8G 内存,则相当于有 80亿 个这样的房间)
每个房间上面又有一个门牌号,这个门牌号就称为地址
那什么又是引用?
引用相当于一个 “别名”,也可以理解成一个指针
创建一个引用只是相当于创建了一个很小的变量,这个变量保存了一个整数, 这个整数表示内存中的一个地址
假设我们分别创建上述代码中三个元素一样的数组,如图
- 当我们创建数组array1的时候,就相当于在堆内存中创建了一块内存空间保存了5个int
- 接下来在执行又依次创建了array2,array3,此时的array1,2,3是三个数组的变量名,他们的类型就是引用类型,存放的分别是自己对应数组的内存地址
- 因此当数组里面的元素被修改之后,我们的三个引用类型变量还是保存的对应数组的内存地址,此时通过引用访问修改后的元素,实质上就是获取对应地址上的数据
总结: 所谓的 “引用” 本质上只是存了一个地址,Java 将数组设定成引用类型,这样的话后续进行数组参数传参,其实只是将数组的地址传入到函数形参中,这样可以避免对整个数组的拷贝(数组可能比较长,那么拷贝开销就会很大)
2.3 认识null
null在Java中表示"空引用",也就是一个无效的引用
int[] arr = null;
System.out.println(arr[0]);
// 执行结果
Exception in thread "main" java.lang.NullPointerException at
Test.main(Test.java:6)
null 的作用类似于 C 语言中的 NULL (空指针),都是表示一个无效的内存位置,因此不能对这个内存进行任何读写操作,一旦尝试读写,就会抛出 NullPointerException(空指针异常)
注意: Java 中并没有约定 null 和 0 号地址的内存有任何关联
3.数组作为方法返回值
代码示例: 写一个方法将数组中每个元素*2
// 直接修改原数组
class Test {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
transform(arr);
printArray(arr);
}
public static void printArray(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
public static void transform(int[] arr) {
for (int i = 0; i < arr.length; i++) {
arr[i] = arr[i] * 2;
}
}
}
这个代码固然可行,但是破坏了原有数组,有时候我们不希望破坏原数组,就需要在方法内部创建一个新的数组,并由方法返回出来
// 返回一个新的数组
class Test {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
int[] output = transform(arr);
printArray(output);
}
public static void printArray(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
public static int[] transform(int[] arr) {
int[] ret = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
ret[i] = arr[i] * 2;
}
return ret;
}
}
这样的话就不会破坏原有数组了
另外由于数组是引用类型,返回的时候只是将这个数组的首地址返回给函数调用者,没有拷贝数组内容,从而比较高效
4.数组联系
4.1 数组转字符串
代码示例 我们实现一个自己的方法将数组转成字符串
public static void main(String[] args) {
int[] array = {1,2,3,4,5};
toString(array);
}
//把数组转换成字符串
public static void toString(int[] array) {
if (array == null) {
return;
}
if (array.length == 0) {
System.out.println("[ ]");
return;
}
System.out.print("[");
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
if (i == array.length-1) {
System.out.println("]");
}
}
}
当然这是我们自己写的方法,使用起来也很不方便,因此我们直接使用Java自带的方法
import java.util.Arrays
int[] arr = {1,2,3,4,5,6};
String newArr = Arrays.toString(arr);
System.out.println(newArr);
// 执行结果
[1, 2, 3, 4, 5, 6]
使用这个方法后续打印数组就更方便一些
Java 中提供了 java.util.Arrays 包,其中包含了一些操作数组的常用方法
什么是包?
例如做一碗油泼面,需要先和面,擀面,扯出面条,再烧水,下锅煮熟,放调料,泼油,但是其中的 “和面,擀面, 扯出面条” 环节难度比较大,不是所有人都能很容易做好,于是超市就提供了一些直接已经扯好的面条,可以直接买回来下锅煮,从而降低了做油泼面的难度,也提高了制作效率.
程序开发也不是从零开始,而是要站在巨人的肩膀上
像我们很多程序写的过程中不必把所有的细节都自己实现,已经有大量的标准库(JDK提供好的代码)和海量的第三方库(其他机构组织提供的代码)供我们直接使用,这些代码就放在一个一个的 “包” 之中,所谓的包就相当于卖面条的超市,只不过, 超市的面条只有寥寥几种,而我们可以使用的 “包” 有成千上万
注意: 这里只是简单了解一下,后续学习会专门去学习
4.2 数组拷贝
代码示例
import java.util.Arrays
int[] arr = {1,2,3,4,5,6};
int[] newArr = Arrays.copyOf(arr, arr.length); System.out.println("newArr: " + Arrays.toString(newArr));
arr[0] = 10;
System.out.println("arr: " + Arrays.toString(arr)); System.out.println("newArr: " + Arrays.toString(newArr));
// 拷贝某个范围.
int[] newArr = Arrays.copyOfRange(arr, 2, 4); System.out.println("newArr2: " + Arrays.toString(newArr2));
注意事项: 相比于 newArr = arr 这样的赋值,copyOf 是将数组进行了深拷贝,即又创建了一个数组对象,拷贝原有数组中的所有元素到新数组中,因此,修改原数组,不会影响到新数组
实现自己版本的拷贝数组
public class HomeWork {
public static void main(String[] args) {
int[] arrary = {1,2,3,4,5};
System.out.println(Arrays.toString(copy(arrary)));
}
//拷贝数组,得到新的数组
public static int[] copy(int[] array) {
int[] copy = new int[array.length];
for (int i = 0; i < array.length; i++) {
copy[i] = array[i];
}
return copy;
}
4.3 找数组中的最大元素
给定一个整型数组,找到其中的最大元素(找最小同理)
代码示例
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6};
System.out.println(max(arr));
}
public static int max(int[] arr) {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
// 执行结果
6
类似于 “打擂台” 这样的过程,其中 max 变量作为 擂台,比擂台上的元素大,就替换上去,否则就下一个对手
4.4 求数组中元素的平均值
给定一个整型数组求平均值
代码示例
public class HomeWork {
public static void main(String[] args) {
int[] arrary = {1,2,3,4,5,6};
System.out.println(average(arrary));
}
//求数组中所有元素的平均值
public static double average (int[] arrary) {
int ret = 0;
for (int i = 0; i < arrary.length; i++) {
ret = ret + arrary[i];
}
return (double) ret /(double) arrary.length;
}
}
注意: 结果用double类型来表示
4.5 查找数组中指定元素(顺序查找)
给定一个数组,再给定一个元素,找出该元素在数组中的位置
代码示例
public static void main(String[] args) {
int[] arr = {1,2,3,10,5,6};
System.out.println(find(arr, 10));
}
public static int find(int[] arr, int toFind) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == toFind) {
return i;
}
}
return -1; // 表示没有找到
}
// 执行结果
3
4.6 查找数组中指定元素(二分查找)
针对有序数组,可以采用效率更高的二分查找
以升序数组为例,二分查找的思路是先取中间位置的元素,看要找的值比中间元素大还是小,如果小,就去左边找,否则就去右边找
代码示例
public static void main(String[] args) {
int[] array = {1,2,3,4,5};
System.out.println(binArraySearch(array,3));
}
//二分查找
public static int binArraySearch(int[] array,int n) {
int left = 0;
int right = array.length-1;
while (left <= right) {
int mid = (left+right) / 2;
if (array[mid] < n) {
//去右侧区域找
left = mid + 1;
}else if (array[mid] > n) {
//去左侧区域找
right = mid - 1;
}else {
//找到了
return mid;
}
}
//没找到
return -1;
}
感受二分查找的效率
class Test {
static int count = 0; // 创建一个成员变量, 记录二分查找循环次数
public static void main(String[] args) {
int[] arr = makeBigArray();
int ret = binarySearch(arr, 9999);
System.out.println("ret = " + ret + " count = " + count);
}
public static int[] makeBigArray() {
int[] arr = new int[10000];
for (int i = 0; i < 10000; i++) {
arr[i] = i;
}
return arr;
}
public static int binarySearch(int[] arr, int toFind) {
int left = 0;
int right = arr.length - 1;
while (left <= right) {
count++; // 使用一个变量记录循环执行次数
int mid = (left + right) / 2;
if (toFind < arr[mid]) {
// 去左侧区间找
right = mid - 1;
} else if (toFind > arr[mid]) {
// 去右侧区间找
left = mid + 1;
} else {
// 相等, 说明找到了
return mid;
}
}
// 循环结束, 说明没找到
return -1;
}
}
// 执行结果
ret = 9999 count = 14
可以看到,针对一个长度为 10000 个元素的数组查找,二分查找只需要循环 14 次就能完成查找,随着数组元素个数越多, 二分的优势就越大
4.7 检查数组有序性
给定一个整型数组, 判断是否该数组是有序的(升序)
代码示例
public static void main(String[] args) {
int[] array = {1,2,3,5,4};
System.out.println(judgeSort(array));
}
//判断是否有序
public static boolean judgeSort(int[] array) {
for (int i = 0; i < array.length-1; i++) {
if (array[i] > array[i+1]) {
return false;
}
}
return true;
}
4.8 数组数字排列
给定一个整型数组,将所有的偶数放在前半部分,将所有的奇数放在数组后半部分
例如
{1, 2, 3, 4}调整后得到{4, 2, 3, 1}
基本思路
设定两个下标分别指向第一个元素和最后一个元素
用前一个下标从左往右找到第一个奇数,用后一个下标从右往左找到第一个偶数,然后交换两个位置的元素,依次循环即可
代码示例
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5, 6};
transform(arr);
System.out.println(Arrays.toString(arr));
}
public static void transform(int[] arr) {
int left = 0;
int right = arr.length - 1;
while (left < right) {
// 该循环结束, left 就指向了一个奇数
while (left < right && arr[left] % 2 == 0) {
left++;
}
// 该循环结束, right 就指向了一个偶数
while (left < right && arr[right] % 2 != 0) {
right--;
}
// 交换两个位置的元素
int tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
}
}
4.9 数组排序(冒泡排序)
给定一个数组,让数组升序 (降序) 排序
算法思路
每次尝试找到当前待排序区间中最小(或最大)的元素,放到数组最前面(或最后面)
代码示例
public static void main(String[] args) {
int[] array = {1,3,5,4,2};
bubbleSort(array);
System.out.println(Arrays.toString(array));
}
//冒泡排序
public static void bubbleSort(int[] array) {
boolean flg = false;//默认不交换
for (int i = array.length-1; i >= 0 ; i--) {
flg = false;//每一次都有可能不交换
for (int j = 0; j < i; j++) {
if (array[j+1] < array[j]) {
int tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
flg = true;//交换
}
}
if (flg = false) {//不交换了
break;
}
}
}
冒泡排序性能较低,Java 中内置了更高效的排序算法
public static void main(String[] args) {
int[] arr = {9, 5, 2, 7};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
}
关于 Arrays.sort 的具体实现算法,我们在后面的排序算法上再详细介绍,到时候会介绍很多种常见排序算法
4.10 数组逆序
给定一个数组,将里面的元素逆序排列
思路
设定两个下标,分别指向第一个元素和最后一个元素,交换两个位置的元素,然后让前一个下标自增,后一个下标自减,循环继续即可
代码示例
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4};
reverse(arr);
System.out.println(Arrays.toString(arr));
}
public static void reverse(int[] arr) {
int left = 0;
int right = arr.length - 1;
while (left < right) {
int tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
left++; right--;
}
}
5.二维数组
二维数组本质上也就是一维数组,只不过每个元素又是一个一维数组
一个二维数组的每一行第一个元素又相当于是一个数组
基本语法
数据类型[][] 数组名称 = new 数据类型 [行数][列数] { 初始化数据 };
代码示例
int[][] arr = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
}
for (int row = 0; row < arr.length; row++) {
for (int col = 0; col < arr[row].length; col++) {
System.out.printf("%d\t", arr[row][col]);
}
System.out.println("");
}
// 执行结果
1 2 3 4
5 6 7 8
9 10 11 12
二维数组的用法和一维数组并没有明显差别,因此我们不再赘述
同理,还存在 “三维数组”,“四维数组” 等更复杂的数组,只不过出现频率都很低