1. 数组的使用

1.1 概念

早在C中我们就知道:数组是一组相同类型元素的集合
但是,C对这句话的限制并不严格,例如:

  1. int arr[2] = {1.2, 2.4};

对于这种写法,C编译器是不会对其报错的,顶多会给警告.
但Java对”相同类型元素”限制非常严格,数组元素的数据类型必须对应.

1.2 创建

数组在Java中和C中的创建方法有不同的地方

  1. //
  2. int[] array = {1, 2, 3};
  3. //
  4. int[] array = new int[] {1, 2, 3};

这两种方法的区别下文会提到.
这里主要强调Java创建数组的语法的注意事项:

  1. 必须保持数据类型[] 数组名这种形式.原因:数据类型[]是一个整体,它表示紧跟的数组名的数据类型.这在C中也有体现.所以实际上C创建数组的语法是错误的.
  2. []内部不能有数字.这样做相当于破坏了类型.
  3. 第二种写法是在内存中实例化一个int[]类型的对象(不久后会学习).实际上第一种写法中的数组也是对象.

    ps:虽然有些Java编译器支持C创建数组的语法,只是抛出警告,但Java本身的语法是使用者需要遵守的

1.3 遍历数组

我们通过遍历数组可以理解并使用数组,以及Java中独特的某些”功能”

  1. int[] array = {1,2,3};

1.3.1 使用for循环遍历

  1. for (int i = 0; i < array.length; i++) {
  2. System.out.println(array[i]);
  3. }

结果:

1 2 3

注意事项:

  1. array.lenth是数组长度..是成员访问操作符(后续会学习)
  2. 遍历要注意循环的区间

    1.3.2 使用for each

    1. int[] array = {1,2,3};
    2. for (int x: array) {
    3. System.out.println(x);
    4. }
    结果:

    1 2 3

注意事项:

  1. int x : array的意思是:int一个x变量接收array数组的所有元素.打印每个x.
  2. 使用for each遍历数组,可以方便地遍历数组,不用担心越界问题
  3. 和for循环遍历的区别:for循环可以得到每个元素的下标,而for each不行

    1.3.3 数组越界

    Java对访问数组边界非常严格
    当出现数组越界会抛出异常:
    ArrayIndexOutOfBoundsException

    2. 数组名作为方法的参数

    在C语言中,数组名是该数组的首元素的地址.
    而在Java中,一切皆对象.这与指针的用法十分类似,下面通过例子和图示理解.
    调用打印方法:

    1. public static void Print(int[] array2) {
    2. for (int i = 0; i < array.length; i++) {
    3. System.out.println(array[i]);
    4. }
    5. }
    6. public static void main(String[] args) {
    7. int[] array1 = {1,2,3};
    8. Print(array1);
    9. }

    结果:

    1 2 3

可以看到,将数组名作为参数传递给方法,方法会成功打印

2.1 理解引用

通过图示理解”对象”:
首先要知道的是:

  • “对象”是存放在内存中的堆区
  • 变量存放在内存中的栈区
  • 引用变量简称引用,故存放在栈区

    ps:目前只需要知道对象和变量是存放在不同地方的 何为引用? 类似图中array1,存放的是对象的地址的变量,称为引用变量.

    区分普通变量和引用变量 普通变量:int a = 1;``a就是普通变量. 引用变量:int[] a = new int[]{1,2,3};``a就是引用变量.

    其最大的不同点就是引用变量实际上是保存着实例化对象的地址,不是对象本身;而普通变量保存的是数据本身.

image.png
通过图示可以清晰地看到:
引用保存的是对象的地址,所以将它作为方法的参数传参,实际上传递的是对象的地址.方法接收后也会指向该对象.因而在方法中对其操作也会改变对象本身.

所以引用传递可以形象地认为是C中的指针传递,也就是传址.

引用的好处?

  1. 避免因数据过大形成的临时拷贝而浪费内存
  2. 提高效率

    引用传递多用于方法,引用(变量)实际上就是对象的一个别名,它存放着该对象的地址.

引用只能指向一个对象

  1. public static void Print(int[] array) {
  2. for (int i = 0; i < array.length; i++) {
  3. System.out.println(array[i]);
  4. }
  5. }
  6. public static void main(String[] args) {
  7. int[] arr = new int[]{1,2,3};
  8. arr = new int[]{4,5,6};
  9. arr = new int[]{7,8,9};
  10. Print(arr);
  11. }

结果:

7 8 9

想让一个名字代表三个对象是不符合常理的,所以多次改变引用指向的对象,最终只会保存最后的对象的地址.

2.2 理解null

null在Java中表示”空引用”,即这个引用是无指向的,无效的.

  1. int[] arr = null;
  2. System.out.println(arr[0]);

抛出异常:
Exception in thread "main" java.lang.NullPointerException

null在C和Java中都有着”空”的含义,但Java与C的区别是null并不表示0地址

所以一旦遇到以上异常,要首先想到null空指向.

2.3 初识JVM内存区域划分

JVM的内存结构是比较复杂的,在这里我们只需了解其结构模型即可.
image.png
(图片来源于网络)

  • 程序计数器 (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 小结

  1. 局部变量和引用(变量)保存在栈中.
  2. 对象保存在堆中(就是new出来的).
  3. 堆的空间远比栈大.
  4. JVM共享堆,而栈不共享

    3. 数组名作为方法的返回值

    例:写一个方法, 将数组中的每个元素都是原来的2倍
    1. public static void Tranform(int[] array) {
    2. for (int i = 0; i < array.length; i++) {
    3. array[i] *= 2;
    4. }
    5. }
    6. public static void main(String[] args) {
    7. int[] arr = new int[]{1,2,3};
    8. Tranform(arr);
    9. Print(arr);
    10. }
    显然,这虽然符合题意,但原数组的结构被破坏了.所以可以在方法中另外new一个相同长度的数组,元素增长2倍. ```java public static void Print(int[] array) { for (int i = 0; i < array.length; i++) {
    1. 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};

  1. int[] ret = doubleArray(arr);
  2. Print(ret);

}

  1. 将原数组的引用作为参数传递给方法,在方法中new一个新数组,存放着符合条件的元素,接着返回该数组.<br />这样做就避免了元素组被破坏的情况.
  2. > 深拷贝:不影响原来的
  3. > 浅拷贝:会影响原来的
  4. > 深拷贝和浅拷贝是需要人为实现的,是要看具体代码是如何实现的.而不是某个固定的方法或代码就是深拷贝或浅拷贝
  5. <a name="zLKcY"></a>
  6. # 4. 练习
  7. <a name="pnsJ1"></a>
  8. ## 4.1 数组转化为字符串
  9. Java中有操作数组的工具类:`Arrays.toString`可以将传入的数组以字符串的形式输出.
  10. ```java
  11. import java.util.Arrays;
  12. public static void main(String[] args) {
  13. int[] arr = new int[]{1,2,3};
  14. String arr2str = Arrays.toString(arr);
  15. System.out.println(arr2str);
  16. }

结果:[1, 2, 3]
注意:
使用Arrays.toString需要导入java.util.Arrays包,其返回值是Srting型.

包可以认为是C中的函数库,使用import导入.它包含了操作数组的各类方法.

模拟实现Arrays.toString包的功能:

  1. public static String my_ArraytoString(int[] array) {
  2. String arr = "[";
  3. for (int i = 0; i < array.length; i++) {
  4. arr += array[i];
  5. //使用判断语句限制最后一个逗号
  6. if(i != array.length - 1) {
  7. arr += ",";
  8. }
  9. }
  10. arr += "]";
  11. return arr;
  12. }
  13. public static void main(String[] args) {
  14. int[] arr = new int[]{1,2,3};
  15. String arr2str = my_ArraytoString(arr);
  16. System.out.println(arr2str);
  17. }

4.2 拷贝数组

4.2.1 copyOf

  1. public static void main(String[] args) {
  2. int[] arr = new int[]{1,2,3};
  3. int[] newArr = Arrays.copyOf(arr,arr.length);
  4. System.out.println(Arrays.toString(newArr));
  5. }

Arrays.copyOf的返回值是数组.其第一个参数为原数组,第二个参数为新数组的长度.若新数组长度超出原数组长度,多余的元素默认值为0.
相当于new了一个一样的数组,也就是说新老数组互不影响.

4.2.2 arraycopy

  1. public static void main(String[] args) {
  2. int[] arr = new int[]{1,2,3};
  3. int[] newArr = new int[arr.length];
  4. System.arraycopy(arr,0,newArr,0,arr.length);
  5. System.out.println(Arrays.toString(newArr));
  6. }

arraycopy的返回值是数组.其第一和第二个参数是原数组,原数组起始位置的下标;第二和第三个则为新数组和新数组要复制的下标;最后一个参数是要复制元素的个数.如果范围大于要复制元素的个数,其余补0.

4.2.3 copyOfRange

  1. public static void main(String[] args) {
  2. int[] arr = new int[]{1,2,3};
  3. int[] newArr = Arrays.copyOfRange(arr,0,3);
  4. System.out.println(Arrays.toString(newArr));
  5. }

copyOfRange的返回值是数组.第一个参数是要复制的数组,第二个参数是要复制数组的起始下标,第三个参数是要复制数组的结束下标.

注意:Java中与范围有关的方法基本都是左闭右开区间.例如这里的下标范围是[0,3),也就是[0,2].

4.3 找数组中最大元素

  1. public static int Findf(int[] arr) {
  2. if(arr.length == 0) {
  3. return -1;
  4. }
  5. int max = arr[0];
  6. for (int i = 0; i < arr.length; i++) {
  7. if(max < arr[i]) {
  8. max = arr[i];
  9. }
  10. }
  11. return max;
  12. }
  13. public static void main(String[] args) {
  14. int[] arr = new int[]{1,2,3};
  15. int ret = Findf(arr);
  16. System.out.println(ret);
  17. }

结果:3
思路非常简单,即假设第一个元素是最大值max,遍历数组,一旦遇到大于max的元素,更新max.最后返回max.

4.4 求数组中元素的平均值

  1. public static double average(int[] arr) {
  2. double ret = 0;
  3. int sum = 0;
  4. for (int i = 0; i < arr.length; i++) {
  5. sum += arr[i];
  6. }
  7. ret = (double)sum / (double)arr.length;
  8. return ret;
  9. }
  10. public static void main(String[] args) {
  11. int[] arr = new int[]{1,2,3};
  12. double ret = average(arr);
  13. System.out.println(ret);
  14. }

结果:2.0
思路非常简单,即在方法内遍历求和,返回平均值即可.需要注意的是强制类型转换.

4.5 查找数组中指定元素

4.5.1 顺序查找

这是最简单的思路,即遍历数组.找到返回下标,否则返回-1

  1. public static int find(int[] arr, int k) {
  2. for (int i = 0; i < arr.length; i++) {
  3. if(k == arr[i]) {
  4. return i;
  5. }
  6. }
  7. return -1;
  8. }
  9. public static void main(String[] args) {
  10. int[] arr = new int[]{1,2,3};
  11. int k = 1;
  12. int ret = find(arr, k);
  13. System.out.println(ret);
  14. }

结果:0
顺序查找的思路虽然简单,但一旦数组长度很长,效率不够高.

4.5.2 二分查找

对于有序数组,将数组的中间元素mid与k比较,每次比较会缩短一半的长度,效率很高.二分查找的时间复杂度是O(logN)

  1. public static int binarySearch(int[] arr, int k) {
  2. int left = 0;
  3. int right = arr.length-1;
  4. while(right >= left) {
  5. //每次循环更新mid
  6. int mid = (right + left) / 2;
  7. if(k > arr[mid]) {
  8. //k在左边
  9. left = mid + 1;
  10. } else if (k < arr[mid]) {
  11. //k在右边
  12. right = mid - 1;
  13. }else {
  14. return mid;
  15. }
  16. }
  17. return -1;
  18. }
  19. public static void main(String[] args) {
  20. int[] arr = new int[]{1,2,3};
  21. int k = 1;
  22. int ret = binarySearch(arr, k);
  23. System.out.println(ret);
  24. }

结果:0

4.6 检查数组是否有序

假设要检查的数组是升序的.

  1. public static boolean isSorted(int[] arr) {
  2. for (int i = 0; i < arr.length - 1; i++) {
  3. if (arr[i] > arr[i + 1]) {
  4. return false;
  5. }
  6. }
  7. return true;
  8. }
  9. public static void main(String[] args) {
  10. int[] arr = {1,2,3,7,5,6};
  11. System.out.println(isSorted(arr));
  12. }

结果:false
思路非常简单,遍历数组,只要不是升序的就将返回false.也可以设置标志变量,遍历完成后判断标志变量是否发生变化.

4.7 (冒泡)排序

用冒泡排序给数组元素排序(升序).

  1. public static int[] bubbleSort(int[] arr) {
  2. for (int i = 0; i < arr.length; i++) {
  3. for (int j = 0; j < arr.length-1-i; j++) {
  4. if(arr[j] > arr[j+1]) {
  5. int tmp = arr[j];
  6. arr[j] = arr[j+1];
  7. arr[j+1] = tmp;
  8. }
  9. }
  10. }
  11. return arr;
  12. }
  13. public static void main(String[] args) {
  14. int[] arr = {6,5,4,3,2,1};
  15. int[] ret = bubbleSort(arr);
  16. System.out.println(Arrays.toString(ret));
  17. }

结果:[1, 2, 3, 4, 5, 6]
假设一共有k个元素

  • 第一层循环表示k个元素要比较k-1次(例如2个元素要比大小只要比一次),即遍历所有元素的下标
  • 第二层循环表示从下标为0的元

用Java内置的排序方法Arrays.sort

  1. public static void main(String[] args) {
  2. int[] arr = {6,5,4,3,2,1};
  3. Arrays.sort(arr);
  4. System.out.println(Arrays.toString(arr));
  5. }

结果:[1, 2, 3, 4, 5, 6]

4.8 数组逆序

  1. public static void reverse(int[] array) {
  2. int left = 0;
  3. int right = array.length-1;
  4. while(left < right) {
  5. int tmp = array[left];
  6. array[left] = array[right];
  7. array[right] = tmp;
  8. left++;
  9. right--;
  10. }
  11. }
  12. public static void main(String[] args) {
  13. int[] arr = new int[]{1,2,3,4,5};
  14. reverse(arr);
  15. System.out.println(Arrays.toString(arr));
  16. }

结果:[5, 4, 3, 2, 1]
思路:用left和right分别代表数组的头和尾下标.以头尾为一对,每对交换位置以后同时往中间走.

4.9 fill填充(补充)

  1. public static void main(String[] args) {
  2. int[] array = new int[4];
  3. Arrays.fill(array, 9);
  4. System.out.println(Arrays.toString(array));
  5. }

结果:[9, 9, 9, 9]
其作用是用数值填充未初始化的数组.

5. 二维数组

二维数组实际上就是特殊的一维数组,其每个元素都是一个一维数组.这与C语言中二维数组的概念是对应的.
但Java中的二维数组有所不同.

5.1 创建二维数组

方法1:

  1. int[][] array = new int[3][3];

注意[]中只能填入正整数
方法2:

  1. public static void main(String[] args) {
  2. int[][] array = new int[][]{{1,2}, {2},{4,5,6}};
  3. System.out.println(array.length);
  4. }

结果:3
这个结果说明,二维数组确实是特殊的一维数组.
注意这样创建的数组不能在[]中填入数字,否则相当于改变了其数据类型.
通过下面的例子理解与C的不同之处

  1. public static void main(String[] args) {
  2. int[][] array = new int[][]{{1,2}, {2},{4,5,6}};
  3. System.out.println(array[0].length);
  4. System.out.println(array[1].length);
  5. System.out.println(array[2].length);
  6. }

结果:

2 1 3

Java中的数组是不规则的,也就是说它不会像在C中一样,初始化要规定列的长度,当每列的元素不足以填满则补0,而是每行填多少就是多少.
所以另一个区别就是Java中初始化只用一对{}表示列,不用像C中创建数组是必须写上列数

总结

在学习Java的数组后,对”对象”有了初步认识,对”引用”有了深刻理解.
其实”对象”就是对某个东西或某类东西的集合的别名(以现在的水平看来).对象在堆中.
而引用更像C中的指针,引用是有指向性的.其实我觉得在前期学习最好不要省略”变量”二字会更容易理解.
Java给我与C最大的不同就是Java中有很多函数可以直接用,很多功能只需要调用接口即可,十分方便.而C在实现某些功能的时候总是要自己造轮子,有些麻烦.虽然是这样,但我还是认为初学者还是以C开始最好,因为只有理解好底层才能更好地使用它们.