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对访问数组边界非常严格
当出现数组越界会抛出异常:ArrayIndexOutOfBoundsException2. 数组名作为方法的参数
在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地址
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倍
显然,这虽然符合题意,但原数组的结构被破坏了.所以可以在方法中另外new一个相同长度的数组,元素增长2倍. ```java public static void Print(int[] array) { for (int i = 0; i < array.length; i++) {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);}
} }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一个新数组,存放着符合条件的元素,接着返回该数组.<br />这样做就避免了元素组被破坏的情况.> 深拷贝:不影响原来的> 浅拷贝:会影响原来的> 深拷贝和浅拷贝是需要人为实现的,是要看具体代码是如何实现的.而不是某个固定的方法或代码就是深拷贝或浅拷贝<a name="zLKcY"></a># 4. 练习<a name="pnsJ1"></a>## 4.1 数组转化为字符串Java中有操作数组的工具类:`Arrays.toString`可以将传入的数组以字符串的形式输出.```javaimport 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) {//每次循环更新midint 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);}
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));}
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开始最好,因为只有理解好底层才能更好地使用它们.
