数组的定义与使用【JavaSE】
1. 数组的使用
1.1 概念
早在C中我们就知道:数组是一组相同类型元素的集合
但是,C对这句话的限制并不严格,例如:
int arr[2] = {1.2, 2.4};
对于这种写法,C编译器是不会对其报错的,顶多会给警告.
但Java对"相同类型元素"限制非常严格,数组元素的数据类型必须对应.
1.2 创建
数组在Java中和C中的创建方法有不同的地方
//
int[] array = {1, 2, 3};
//
int[] array = new int[] {1, 2, 3};
这两种方法的区别下文会提到.
这里主要强调Java创建数组的语法的注意事项:
- 必须保持
数据类型[] 数组名
这种形式.原因:数据类型[]
是一个整体,它表示紧跟的数组名
的数据类型.这在C中也有体现.所以实际上C创建数组的语法是错误的. []
内部不能有数字.这样做相当于破坏了类型.- 第二种写法是在内存中实例化一个
int[]
类型的对象(不久后会学习).实际上第一种写法中的数组也是对象.
ps:虽然有些Java编译器支持C创建数组的语法,只是抛出警告,但Java本身的语法是使用者需要遵守的
1.3 遍历数组
我们通过遍历数组可以理解并使用数组,以及Java中独特的某些"功能"
int[] array = {1,2,3};
1.3.1 使用for循环遍历
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
结果:
1
2
3
注意事项:
array.lenth
是数组长度..
是成员访问操作符(后续会学习)- 遍历要注意循环的区间
1.3.2 使用for each
int[] array = {1,2,3};
for (int x: array) {
System.out.println(x);
}
结果:
1
2
3
注意事项:
int x : array
的意思是:int一个x变量接收array数组的所有元素.打印每个x.- 使用for each遍历数组,可以方便地遍历数组,不用担心越界问题
- 和for循环遍历的区别:for循环可以得到每个元素的下标,而for each不行
1.3.3 数组越界
Java对访问数组边界非常严格
当出现数组越界会抛出异常:
ArrayIndexOutOfBoundsException
2. 数组名作为方法的参数
在C语言中,数组名是该数组的首元素的地址.
而在Java中,一切皆对象.这与指针的用法十分类似,下面通过例子和图示理解.
调用打印方法:
public static void Print(int[] array2) {
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
}
public static void main(String[] args) {
int[] array1 = {1,2,3};
Print(array1);
}
结果:
1
2
3
可以看到,将数组名作为参数传递给方法,方法会成功打印
2.1 理解引用
通过图示理解"对象":
首先要知道的是:
- "对象"是存放在内存中的堆区
- 变量存放在内存中的栈区
- 引用变量简称引用,故存放在栈区
ps:目前只需要知道对象和变量是存放在不同地方的
何为引用?
类似图中array1,存放的是对象的地址的变量,称为引用变量.区分普通变量和引用变量
普通变量:int a = 1;``a
就是普通变量.
引用变量:int[] a = new int[]{1,2,3};``a
就是引用变量.其最大的不同点就是引用变量实际上是保存着实例化对象的地址,不是对象本身;而普通变量保存的是数据本身.
通过图示可以清晰地看到:
引用保存的是对象的地址,所以将它作为方法的参数传参,实际上传递的是对象的地址.方法接收后也会指向该对象.因而在方法中对其操作也会改变对象本身.
所以引用传递可以形象地认为是C中的指针传递,也就是传址.
引用的好处?
- 避免因数据过大形成的临时拷贝而浪费内存
- 提高效率
引用传递多用于方法,引用(变量)实际上就是对象的一个别名,它存放着该对象的地址.
引用只能指向一个对象
public static void Print(int[] array) {
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
}
public static void main(String[] args) {
int[] arr = new int[]{1,2,3};
arr = new int[]{4,5,6};
arr = new int[]{7,8,9};
Print(arr);
}
结果:
7
8
9
想让一个名字代表三个对象是不符合常理的,所以多次改变引用指向的对象,最终只会保存最后的对象的地址.
2.2 理解null
null在Java中表示"空引用",即这个引用是无指向的,无效的.
int[] arr = null;
System.out.println(arr[0]);
抛出异常:
Exception in thread "main" java.lang.NullPointerException
null在C和Java中都有着"空"的含义,但Java与C的区别是null并不表示0地址
所以一旦遇到以上异常,要首先想到null空指向.
2.3 初识JVM内存区域划分
JVM的内存结构是比较复杂的,在这里我们只需了解其结构模型即可.
(图片来源于网络)
- 程序计数器 (PC Register): 只是一个很小的空间, 保存下一条执行的指令的地址.
- 虚拟机栈(JVM Stack): 存储局部变量表. 比如引用就在这里保存.
- 本地方法栈(Native Method Stack): 本地方法栈与虚拟机栈的作用类似. 只不过保存的内容是Native方法的局部变量. 在有些版本的 JVM 实现中(例如HotSpot), 本地方法栈和虚拟机栈是一起的.
- 堆(Heap): JVM所管理的最大内存区域. 使用 new 创建的对象都是在堆上保存 (例如前面的
new int[]{1, 2,3}
) - 方法区(Method Area): 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. 方法编译出的的字节码就是保存在这个区域.
- 运行时常量池(Runtime Constant Pool): 是方法区的一部分, 存放字面量(字符串常量)与符号引用. (注意 从 JDK1.7 开始, 运行时常量池在堆上).
- Native 方法:
JVM 是一个基于 C++ 实现的程序. 在 Java 程序执行过程中, 本质上也需要调用 C++ 提供的一些函数进行和操作系统底层进行一些交互. 因此在 Java 开发中也会调用到一些 C++ 实现的函数.这里的 Native 方法就是指这些 C++ 实现的, 再由 Java 来调用的函数.
2.3.1 小结
- 局部变量和引用(变量)保存在栈中.
- 对象保存在堆中(就是new出来的).
- 堆的空间远比栈大.
- JVM共享堆,而栈不共享
3. 数组名作为方法的返回值
例:写一个方法, 将数组中的每个元素都是原来的2倍
public static void Tranform(int[] array) {
for (int i = 0; i < array.length; i++) {
array[i] *= 2;
}
}
public static void main(String[] args) {
int[] arr = new int[]{1,2,3};
Tranform(arr);
Print(arr);
}
显然,这虽然符合题意,但原数组的结构被破坏了.所以可以在方法中另外new一个相同长度的数组,元素增长2倍.
public static void Print(int[] array) {
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
}
public static int[] doubleArray(int[] array) {
int[] arr = new int[array.length];
for (int i = 0; i < array.length; i++) {
arr[i] = array[i] * 2;
}
return arr;
}
public static void main(String[] args) {
int[] arr = new int[]{1,2,3};
int[] ret = doubleArray(arr);
Print(ret);
}
将原数组的引用作为参数传递给方法,在方法中new一个新数组,存放着符合条件的元素,接着返回该数组.
这样做就避免了元素组被破坏的情况.
深拷贝:不影响原来的
浅拷贝:会影响原来的
深拷贝和浅拷贝是需要人为实现的,是要看具体代码是如何实现的.而不是某个固定的方法或代码就是深拷贝或浅拷贝
4. 练习
4.1 数组转化为字符串
Java中有操作数组的工具类:Arrays.toString
可以将传入的数组以字符串的形式输出.
import java.util.Arrays;
public static void main(String[] args) {
int[] arr = new int[]{1,2,3};
String arr2str = Arrays.toString(arr);
System.out.println(arr2str);
}
结果:[1, 2, 3]
注意:
使用Arrays.toString
需要导入java.util.Arrays
包,其返回值是Srting型.
包可以认为是C中的函数库,使用
import
导入.它包含了操作数组的各类方法.
模拟实现Arrays.toString
包的功能:
public static String my_ArraytoString(int[] array) {
String arr = "[";
for (int i = 0; i < array.length; i++) {
arr += array[i];
//使用判断语句限制最后一个逗号
if(i != array.length - 1) {
arr += ",";
}
}
arr += "]";
return arr;
}
public static void main(String[] args) {
int[] arr = new int[]{1,2,3};
String arr2str = my_ArraytoString(arr);
System.out.println(arr2str);
}
4.2 拷贝数组
4.2.1 copyOf
public static void main(String[] args) {
int[] arr = new int[]{1,2,3};
int[] newArr = Arrays.copyOf(arr,arr.length);
System.out.println(Arrays.toString(newArr));
}
Arrays.copyOf
的返回值是数组.其第一个参数为原数组,第二个参数为新数组的长度.若新数组长度超出原数组长度,多余的元素默认值为0.
相当于new了一个一样的数组,也就是说新老数组互不影响.
4.2.2 arraycopy
public static void main(String[] args) {
int[] arr = new int[]{1,2,3};
int[] newArr = new int[arr.length];
System.arraycopy(arr,0,newArr,0,arr.length);
System.out.println(Arrays.toString(newArr));
}
arraycopy
的返回值是数组.其第一和第二个参数是原数组,原数组起始位置的下标;第二和第三个则为新数组和新数组要复制的下标;最后一个参数是要复制元素的个数.如果范围大于要复制元素的个数,其余补0.
4.2.3 copyOfRange
public static void main(String[] args) {
int[] arr = new int[]{1,2,3};
int[] newArr = Arrays.copyOfRange(arr,0,3);
System.out.println(Arrays.toString(newArr));
}
copyOfRange
的返回值是数组.第一个参数是要复制的数组,第二个参数是要复制数组的起始下标,第三个参数是要复制数组的结束下标.
注意:Java中与范围有关的方法基本都是左闭右开区间.例如这里的下标范围是[0,3),也就是[0,2].
4.3 找数组中最大元素
public static int Findf(int[] arr) {
if(arr.length == 0) {
return -1;
}
int max = arr[0];
for (int i = 0; i < arr.length; i++) {
if(max < arr[i]) {
max = arr[i];
}
}
return max;
}
public static void main(String[] args) {
int[] arr = new int[]{1,2,3};
int ret = Findf(arr);
System.out.println(ret);
}
结果:3
思路非常简单,即假设第一个元素是最大值max,遍历数组,一旦遇到大于max的元素,更新max.最后返回max.
4.4 求数组中元素的平均值
public static double average(int[] arr) {
double ret = 0;
int sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
ret = (double)sum / (double)arr.length;
return ret;
}
public static void main(String[] args) {
int[] arr = new int[]{1,2,3};
double ret = average(arr);
System.out.println(ret);
}
结果:2.0
思路非常简单,即在方法内遍历求和,返回平均值即可.需要注意的是强制类型转换.
4.5 查找数组中指定元素
4.5.1 顺序查找
这是最简单的思路,即遍历数组.找到返回下标,否则返回-1
public static int find(int[] arr, int k) {
for (int i = 0; i < arr.length; i++) {
if(k == arr[i]) {
return i;
}
}
return -1;
}
public static void main(String[] args) {
int[] arr = new int[]{1,2,3};
int k = 1;
int ret = find(arr, k);
System.out.println(ret);
}
结果:0
顺序查找的思路虽然简单,但一旦数组长度很长,效率不够高.
4.5.2 二分查找
对于有序数组,将数组的中间元素mid与k比较,每次比较会缩短一半的长度,效率很高.二分查找的时间复杂度是O(logN)
public static int binarySearch(int[] arr, int k) {
int left = 0;
int right = arr.length-1;
while(right >= left) {
//每次循环更新mid
int mid = (right + left) / 2;
if(k > arr[mid]) {
//k在左边
left = mid + 1;
} else if (k < arr[mid]) {
//k在右边
right = mid - 1;
}else {
return mid;
}
}
return -1;
}
public static void main(String[] args) {
int[] arr = new int[]{1,2,3};
int k = 1;
int ret = binarySearch(arr, k);
System.out.println(ret);
}
结果:0
4.6 检查数组是否有序
假设要检查的数组是升序的.
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,7,5,6};
System.out.println(isSorted(arr));
}
结果:false
思路非常简单,遍历数组,只要不是升序的就将返回false.也可以设置标志变量,遍历完成后判断标志变量是否发生变化.
4.7 (冒泡)排序
用冒泡排序给数组元素排序(升序).
public static int[] bubbleSort(int[] arr) {
for (int i = 0; i < arr.length; 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;
}
}
}
return arr;
}
public static void main(String[] args) {
int[] arr = {6,5,4,3,2,1};
int[] ret = bubbleSort(arr);
System.out.println(Arrays.toString(ret));
}
结果:[1, 2, 3, 4, 5, 6]
假设一共有k个元素
- 第一层循环表示k个元素要比较k-1次(例如2个元素要比大小只要比一次),即遍历所有元素的下标
- 第二层循环表示从下标为0的元
用Java内置的排序方法Arrays.sort
public static void main(String[] args) {
int[] arr = {6,5,4,3,2,1};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));
}
结果:[1, 2, 3, 4, 5, 6]
4.8 数组逆序
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--;
}
}
public static void main(String[] args) {
int[] arr = new int[]{1,2,3,4,5};
reverse(arr);
System.out.println(Arrays.toString(arr));
}
结果:[5, 4, 3, 2, 1]
思路:用left和right分别代表数组的头和尾下标.以头尾为一对,每对交换位置以后同时往中间走.
4.9 fill填充(补充)
public static void main(String[] args) {
int[] array = new int[4];
Arrays.fill(array, 9);
System.out.println(Arrays.toString(array));
}
结果:[9, 9, 9, 9]
其作用是用数值填充未初始化的数组.
5. 二维数组
二维数组实际上就是特殊的一维数组,其每个元素都是一个一维数组.这与C语言中二维数组的概念是对应的.
但Java中的二维数组有所不同.
5.1 创建二维数组
方法1:
int[][] array = new int[3][3];
注意[]
中只能填入正整数
方法2:
public static void main(String[] args) {
int[][] array = new int[][]{{1,2}, {2},{4,5,6}};
System.out.println(array.length);
}
结果:3
这个结果说明,二维数组确实是特殊的一维数组.
注意这样创建的数组不能在[]
中填入数字,否则相当于改变了其数据类型.
通过下面的例子理解与C的不同之处
public static void main(String[] args) {
int[][] array = new int[][]{{1,2}, {2},{4,5,6}};
System.out.println(array[0].length);
System.out.println(array[1].length);
System.out.println(array[2].length);
}
结果:
2
1
3
Java中的数组是不规则的,也就是说它不会像在C中一样,初始化要规定列的长度,当每列的元素不足以填满则补0,而是每行填多少就是多少.
所以另一个区别就是Java中初始化只用一对{}
表示列,不用像C中创建数组是必须写上列数
总结
在学习Java的数组后,对"对象"有了初步认识,对"引用"有了深刻理解.
其实"对象"就是对某个东西或某类东西的集合的别名(以现在的水平看来).对象在堆中.
而引用更像C中的指针,引用是有指向性的.其实我觉得在前期学习最好不要省略"变量"二字会更容易理解.
Java给我与C最大的不同就是Java中有很多函数可以直接用,很多功能只需要调用接口即可,十分方便.而C在实现某些功能的时候总是要自己造轮子,有些麻烦.虽然是这样,但我还是认为初学者还是以C开始最好,因为只有理解好底层才能更好地使用它们.