数组的定义与使用【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创建数组的语法的注意事项:

  1. 必须保持数据类型[] 数组名这种形式.原因:数据类型[]是一个整体,它表示紧跟的数组名的数据类型.这在C中也有体现.所以实际上C创建数组的语法是错误的.
  2. []内部不能有数字.这样做相当于破坏了类型.
  3. 第二种写法是在内存中实例化一个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

注意事项:

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

1.3.2 使用for each

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

结果:

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中,一切皆对象.这与指针的用法十分类似,下面通过例子和图示理解.
调用打印方法:

    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就是引用变量.

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

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

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

引用的好处?

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

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

引用只能指向一个对象

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的内存结构是比较复杂的,在这里我们只需了解其结构模型即可.
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倍

    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开始最好,因为只有理解好底层才能更好地使用它们.

posted @ 2022-12-06 22:31  shawyxy  阅读(106)  评论(0编辑  收藏  举报