目录
一、数组基本用法
什么是数组
数组,能让我们批量地创建相同类型的变量.
例如:
如果需要表示两个数据, 那么直接创建两个变量即可 int a; int b
如果需要表示五个数据, 那么可以创建五个变量 int a1; int a2; int a3; int a4; int a5;
但是如果需要表示一万个数据, 那么就不能创建一万个变量了. 这时候就需要使用数组, 帮我们批量创建.
注意事项:在 Java 中,数组中包含的变量必须是 相同类型.
创建数组
基本语法
// 动态初始化
数据类型[] 数组名称 = new 数据类型 [] { 初始化数据 };
// 静态初始化
数据类型[] 数组名称 = { 初始化数据 };
代码示例
int[] arr1 = new int[]{1, 2, 3};
int[] arr2 = {1, 2, 3};
注意事项:静态初始化的时候,数组元素个数和初始化数据的格式一致.
其实数组也可以写成
int arr[] = {1, 2, 3};
这样就和 C 语言更相似了. 但是我们还是更推荐写成 int[] arr 的形式. int和 [] 是一个整体.
数组的使用
代码示例: 获取长度 & 访问元素
int[] arr = {1,2,3};
//获取数组长度 -> 3
System.out.println("length:" + arr.length);
//访问数组中的元素
System.out.println(arr[1]); //2
System.out.println(arr[0]); //1
arr[1] = 199;
System.out.println(arr[1]); //199
注意事项
- 使用 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 异常. 使用数组一定要下标谨防越界.
代码示例: 遍历数组
所谓 “ 遍历 ” 是指将数组中的所有元素都访问一遍,通常需要搭配循环语句.
public static void main(String[] args) {
int[] arr = {1,2,3,4};
for (int i = 0; i < arr.length; i++){
System.out.println(arr[i]);
}
}
代码示例: 使用 for-each 遍历数组
public static void main(String[] args) {
int[] arr = {1,2,3,4};
//传递数组名,之后操作使用 x 来代替
for (int x : arr) {
//直接输出 x 即可遍历数组
System.out.println(x);
}
}
//执行结果
1
2
3
4
for-each 是 for 循环的另一种使用方式. 能够更加方便的完成对数组的遍历. 可以避免循环条件和更新语句写错.
二、数组作为方法的参数
基本用法
代码示例: 打印数组内容
public static void printArr(int[] arr) {
for (int x : arr) {
System.out.println(x);
}
}
public static void main(String[] args) {
int[] arr = {1,2,3};
printArr(arr);
}
//输出结果
1
2
3
数组可以直接作为方法的参数传递过去使用,传递的性质是传址。
理解引用类型(重点 / 难点)
我们尝试以下代码
代码示例1 参数传内置类型
public static void main(String[] args) {
int num = 0;
func(num);
System.out.println(num);
}
public static void func(int x){
x = 10;
System.out.println(x);
}
//执行结果
x = 10
num = 0
我们发现,修改形参 x 的值,不影响实参的 num 值.
代码示例2 参数传数组类型
public static void main(String[] args) {
int[] arr = {1,2,3};
func2(arr);
System.out.println(arr[0]);
}
public static void func2(int[] a){
a[0] = 10;
System.out.println(a[0]);
}
//执行结果
10
10
我们发现,在函数内部修改数组内容,函数外部也能发生改变.
此时数组名 arr 是一个“引用”. 当传参的时候,是按照引用传参.
所谓的 “引用” 本质上只是存了一个地址.
Java 将数组设定成引用类型,这样的话后续进行数组参数传参,其实只是将数组的地址传入到函数形参中.
这样可以避免对整个数组的拷贝(数组可能比较长,那么拷贝开销就会很大).
认识 null
null 在 Java 中表示“空引用”,也就是一个无效的引用.
int[] arr = null;
System.out.println(arr[0]);
// 执行结果
Exception in thread "main" java.lang.NullPointerException
null 的作用类似于 C 语言中的 NULL,都是表示一个无效的内存位置.
因此不能对这个内存进行任何读写操作. 一旦尝试读写,就会抛出 NullPointerException.
Java 中没有约定 null 和 0 号地址的内存有关系.
初始 JVM 内存区域划分(重点)
在学校,一栋宿舍楼会划分成不同区域:大一、大二…计算机专业,通信专业…
内存也是类似,被划分成不同的部分,每个区域存放不同的数据.
JVM 的内存被划分成了几个区域,如:
- 程序计数器(PC Register):只是一个很小的空间,保存下一条执行的指令地址.
- 虚拟机栈(JVM Stack):重点是存储局部变量表(也有其它信息). 我们刚才创建的数组
int[] arr
这样的存储地址的引用就是在这里保存. - 本地方法栈(Native Method Stack):本地方法栈与虚拟机栈的作用类似. 只不过保存的内容是 Native 方法的局部变量. 在有些版本的 JVM 实现中(HotSpot),本地方法栈和虚拟机栈是在一起的.
- 堆(Heap):JVM 所管理的最大内存区域,使用 new 创建的对象都是在堆上保存(例如前面的
new int[]{1,2,3}
). - 方法区(Method Area):用于存储已经被虚拟机加载的类信息、常量、静态常量、即使编译器编译后的代码等数据. 方法编译后的字节码就保存在这个区域.
- 运行时常量池(Runtime Constant Pool):是方法区的一部分,存放字面量(字符串常量)与符号引用. (从JDK1.7开始,运行时常量池在堆上).
ps.
Native 方法:
JVM 是一个基于 C++ 实现的程序.
在 Java 程序执行过程中,本质也需要调用 C++ 提供的一些函数运行和操作系统底层交互.
因此在 Java 开发中也会调用到一些 C++ 实现的函数.
Native 方法指的是这些 C++ 实现的,再由 Java 来调用的函数.
小结:
总的来说,局部变量和引用保存在栈上,new 出来的对象保存在堆上.
堆的空间非常大,栈的空间比较小.
堆是整个 JVM 共享一个,而栈是每个线程具有一份(一个 Java 程序中可能存在多个栈).
三、数组作为方法的返回值
代码示例 :写一个方法,将数组中的每个元素都 *2.
public static void main(String[] args) {
int[] arr = {1,2,3};
transform(arr);
printArray(arr);
}
public static void transform(int[] arr) {
for (int i = 0; i < arr.length; i++) {
arr[i] *= 2;
}
}
public static void printArray(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
这个代码固然可行, 但是破坏了原有数组. 有时候我们不希望破坏原数组, 就需要在方法内部创建一个新的数组, 并由方法返回出来.
// 返回一个新的数组
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;
}
}
这样的话就不会破坏原有数组了.
另外由于数组是引用类型, 返回的时候只是将这个数组的首地址返回给函数调用者, 没有拷贝数组内容, 从而比较高效.
四、数组练习
数组转字符串
代码示例
int[] arr = {1,2,3,4,5,6};
String str = Arrays.toString(arr);
System.out.println(str);
//执行结果
[1, 2, 3, 4, 5, 6]
使用这个方法后续打印数组就更方便一些.
自己实现一个数组转字符串方法.
//纯纯字符串拼接整型
public static String my_toString(int[] arr) {
String str = "[";
for (int i = 0; i < arr.length - 1; i++) {
str += (arr[i] + ", ");
}
str += (arr[arr.length - 1] + "]");
return str;
}
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6};
String string = my_toString(arr);
System.out.println(string);
}
数组拷贝
代码示例 :字符串拷贝
Arrays 包下的 copyOf 方法.
从 arr1 数组中拷贝,arr1.length 长度的数据到新数组.
int[] arr1 = {1,2,3,4,5,6};
int[] arr2 = Arrays.copyOf(arr1, arr1.length);
System.out.println("arr2:" + Arrays.toString(arr2));
arr1[0] = 199;
System.out.println("arr1:" + Arrays.toString(arr1));
System.out.println("arr2:" + Arrays.toString(arr2));
代码示例 :拷贝某个范围
Arrays.copyOfRange(arr, n1, n2);
从数组 arr 中拷贝数据,数据范围是 n1 到 n2.
int[] arr1 = {1,2,3,4,5,6,7,8,9};
int[] arr2 = Arrays.copyOfRange(arr1, 2,4);
System.out.println(Arrays.toString(arr2));
//运行结果
[3, 4]
注意事项 :
相比于newArr = arr
这样的赋值,copyOf 是将数组进行了深拷贝,即又创建了一个数组对象,拷贝原有数组中的所有元素到新数组中.
因此,修改原数组,不会影响到新数组.
模拟实现数组拷贝 :
public static int[] my_copyOf(int[] arr, int len) {
int[] newArr = new int[len];
for (int i = 0; i < len; i++) {
newArr[i] = arr[i];
}
return newArr;
}
找数组中的最大元素
给定一个整型数组,找到其中最大的元素.
代码示例
public static int getMax(int[] arr){
int max = arr[0];
for (int i = 1; i < arr.length; i++){
if (max < arr[i]){
max = arr[i];
}
}
return max;
}
public static void main(String[] args) {
int[] arr = {1,5,9,3,5,7,4,6,8,2};
int max = getMax(arr);
System.out.println(max);
}
类似于”打擂台“,假设数组的第一个元素是最大,然后让其余元素依次与之对比。
求数组中元素的平均值
给定一个整型数组,求平均值
public static double getAvg(int[] arr){
int sum = 0;
for (int i = 0; i < arr.length; i++){
sum += arr[i];
}
return sum/arr.length;
}
public static void main(String[] args) {
int[] arr = {1,5,9,3,5,7,4,6,8,2};
double avg = getAvg(arr);
System.out.println(avg);
}
平均值有可能是小数,需要使用 double 来表示.
查找数组中指定元素(顺序查找)
给定一个数组,再给定一个元素,找出该元素在数组中的位置.
public static int findPost(int[] arr, int num){
for (int i = 0; i < arr.length; i++){
if (num == arr[i]){
return i;
}
}
return -1;
}
public static void main(String[] args) {
int[] arr = {1,5,9,3,7,4,6,8,2};
int post = findPost(arr,2);
System.out.println(post);
}
查找数组中指定元素(二分查找)
针对有序数组,才能使用二分查找.
二分查找的思路就是:
先取中间位置的元素,与要找的数据对比;
元素大了,表示数据在中间元素的左侧;
元素小了,表示数据在中间元素的右侧。
代码示例
public static int binarySearch(int[] arr, int num) {
int left = 0;
int mid = 0;
int right = arr.length - 1;
while (left <= right) {
mid = (left + right) / 2;
if (arr[mid] > num) {
right = mid - 1;
} else if (arr[mid] < num) {
left = mid + 1;
}else {
return mid;
}
}
return -1;
}
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7,8,9};
int key = 6;
int post = binarySearch(arr, key);
System.out.println(post);
}
感受二分查找的效率
public static int[] makeBigArray() {
int[] arr = new int[10000];
for (int i = 0; i < 10000; i++){
arr[i] = (i + 1);
}
return arr;
}
public static int binarySearch(int[] arr, int num) {
int left = 0;
int mid = 0;
int right = arr.length - 1;
while (left <= right) {
count++;
mid = (left + right) / 2;
if (arr[mid] > num) {
right = mid - 1;
}else if (arr[mid] < num) {
left = mid + 1;
}else {
return mid;
}
}
return -1;
}
public static void main(String[] args) {
int[] arr = makeBigArray();
int post = binarySearch(arr, 9999);
System.out.println(post);
System.out.println(count);
}
//运行结果
9998
14
创建一个静态变量 count 来记录。
我们可以看到,一个长度为 10000 的数组,使用二分查找只需要循环 14 次。
可见数组越大,二分查找的优势越大。
检查数组的有序性
给定一个整型数组,判断是否该数组有序(升序).
public static boolean isSorted(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
if (arr[i] > arr[i + 1]) {
return false;
}
}
return true;
}
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6};
System.out.println(isSorted(arr));
}
数组排序(冒泡排序)
给定一个数组,让数组升序(降序)排序.
算法思路
每次尝试找当前待排序区间中最小(最大)的元素,放在数组最前面(最后面).
代码示例
public static void bubbleSort(int[] arr) {
//外层循环,长度减一,因为最后一个元素不需要排序;
for (int i = 0; i < arr.length-1; i++) {
//内层循环,走完一趟循环需要减少一个元素,因为已经排序好了;
//担心接下来使用数组变量来交换会越界,所以数组长度减一;
for (int j = 0; j < arr.length-1-i; j++) {
//使用内层循环变量来对比
if (arr[j] > arr[j+1]) {
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
public static void main(String[] args) {
int[] arr = {1,5,9,3,7,4,6,8,2};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
冒泡排序是比较简单的排序,性能不高.
Java 中内置了更高效的排序算法,我们可以直接使用.
public static void main(String[] args) {
int[] arr = {1,7,9,3,4,6,8,2};
Arrays.sort(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--;
}
}
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7,8,9};
reverse(arr);
System.out.println(Arrays.toString(arr));
}
数组数字排列
给定一个整型数组,将所有的偶数放在前半部分,将所有奇数放在数组后半部分.
{1, 2, 3, 4}
调整后
{2, 4, 1, 3}
基本思路
设定两个下标,分别是数组最前面和最后面.
最前面找第一个奇数,最后面找第一个偶数,彼此交换,依次循环.
代码示例
public static void transform(int[] arr) {
int left = 0;
int right = arr.length - 1;
while (left < right) {
//找第一个奇数
if (left < right && arr[left] % 2 != 1) {
left++;
}
//找第一个偶数
if (left < right && arr[right] % 2 != 0) {
right--;
}
int tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
}
}
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6};
transform(arr);
System.out.println(Arrays.toString(arr));
}
五、二维数组
二维数组本质上也是一维数组,只不过每个元素都是又一个一维数组.
基本语法
数据类型[][] 数组名称 = new 数据类型 [行数][列数] { 初始化数据 };
代码示例
public static void main(String[] args) {
int[][] arr = {{1,2,3}, {4,5,6}, {7,8,9}};
for (int row = 0; row < arr.length; row++) {
for (int col = 0; col < arr[row].length; col++) {
System.out.print(arr[row][col] + " ");
}
System.out.println();
}
}
//执行结果
1 2 3
4 5 6
7 8 9
二维数组的用法和一维数组并没有明显差别. 和C语言差不多.
同理,还有“三维数组”,“四维数组”等更加复杂的数组,只不过出现的频率都很低。