【Java】数组

本文详细介绍了Java中的数组,包括创建、初始化、内存分配以及作为方法参数和返回值的使用。讨论了数组作为引用类型在内存中的存储方式,以及如何通过引用进行操作。此外,还涉及到了Arrays类的常用方法如数组转字符串、拷贝和排序。文章还讲解了冒泡排序和二分查找算法,并对二维数组进行了定义和使用说明。
摘要由CSDN通过智能技术生成

数组的定义和使用

数组的创建和初始化

数组,是相同类型的集合,允许在里面存储同类型的多个元素,那么我们在存储多个同类型元素的时候,就不用创建一堆变量了,而是可以用一个数组将他们存储在一起

那么如何创建一个数组?Java中提供了三种方式来创建数组

数据类型[] 数组名 = new 数据类型[数组的大小];

数据类型[] 数组名 = new 数据类型[]{要存储的数据};

数据类型[] 数组名 = {要存储的数据};

那么是什么意思呢?我们以一个实例来帮助理解

public class Test {
    public static void main(String[] args) {
        int[] arr1 = new int[5];
        
        int[] arr2 = new int[]{1, 2, 3, 4, 5};
        
        int[] arr3 = {1, 2, 3, 4, 5};
    }
}

分别解释一下三个创建的过程:

  • 创建一个大小为5的数组,并且将这五个数据初始化为int的默认值0
  • 创建一个存储了1,2,3,4,5的数组,大小根据数据数量确定,大小为5
  • 相当于第二种写法的省略版本

值得一提的是,第一种写法和第二种写法也允许我们在后面初始化

public class Test {
    public static void main(String[] args) {
        int[] arr1;
        arr1 = new int[5];

        int[] arr2;
        arr2 = new int[]{1, 2, 3, 4, 5};

        int[] arr3;
        //会报错
        arr3 = {1, 2, 3, 4, 5};
    }
}

上面提到了,第一种写法会直接将其初始化为对应类型的默认值,那么下面是一些常见类型的默认值表格

类型默认值
byte0
short0
int0
long0
float0.0f
double0.0
char/u0000
booleanfalse
引用类型null

下面列举一些有关数组的注意点:

  1. 数组初始化后,大小就是固定的,无法改变
  2. 类似于int[] arr;前面的int[]是类型,不能往[]里面放数字

数组的使用

那么假如我们已经将数据存放在了数组里面,我们要如何使用它们呢?

这里就要借用下标来访问数组中的元素,下标就类似于数组中各个元素的编号,下标从0开始自增,依次指向数组中的各个元素。

同时我们还要借助下标访问操作符[]来通过下标访问数组中的各个元素,下面通过例子说明

public class Test {
    public static void main(String[] args) {
        int[] arr = new int[]{1, 2, 3, 4, 5};
        for (int i = 0; i < 5; i++) {
            //通过从零开始的下标依次访问各个元素
            System.out.println(arr[i]);
        }
    }
}

使用下标的时候应该注意,由于下标的起始点是0,因此下标的最大值应该是元素个数-1,在使用下标访问数组中数据的时候,不要越界,否则会报出数组越界异常

那么用for循环遍历数组的时候,怎么样更好的防止我们误操作越界呢?

  1. 用前闭区间后开区间的写法
  2. 使用.length来帮助确定数组长度
public class Test {
    public static void main(String[] args) {
        int[] arr = new int[]{1, 2, 3, 4, 5};
        for (int i = 0; i < arr.length; i++) {
            //通过使用 .length来确认数组长度
            System.out.println(arr[i]);
        }
    }
}

在Java中,提供了一种特殊的for循环,叫做for-each循环,它可以快速的帮助我们遍历数组中的各个元素,语法如下

for(int 变量名 : 数组){
    
}

它相当于在第n次循环帮你把数组中的第n个元素赋值给x

public class Test {
    public static void main(String[] args) {
        int[] arr = new int[]{1, 2, 3, 4, 5};
        //IDEA生成的格式,如果觉得不好看放一行也可以
        for (int x :
                arr) {
            System.out.println(x);
        }
    }
}

由于它是将值赋给x进行操作,所以如果使用这个循环我们并不能通过x改变原数组,并且我们也不能对数组的访问顺序进行操作

数组的数据类型

JVM的内存分布

由于数组的数据类型涉及到了内存的分布,所以我们这里先对JVM的内存分布进行一些介绍

JVM的内存分布大概分为下面几块:

  • 虚拟机栈:也就是我们在方法章节所说的栈区,我们的方法在执行的时候就会在这里开辟内存。局部变量也是在这个区域存储的
  • 堆:JVM管理的最大内存区域,使用new创建的对象都是在堆上面保存的(例如上面的new int[])。堆区的内存只要还在被使用就不会销毁,程序退出时,整个堆区的数据都会销毁。
  • 程序计数器:用于保存即将执行的下一条指令的地址
  • 本地方法栈:和虚拟机栈类似,但是保存的内容都是Native方法的局部变量
  • 方法区:用于加载类的信息,常量,静态变量等,字节码就是存储在这个区域的

这里我们暂时只关心堆和虚拟机栈,其他的在后续学习JVM的时候介绍

初识引用类型

引用类型是Java中的一种特殊类型,那么它和基本数据类型有什么区别呢?

基本数据类型创建的变量,其中存储的就是该变量的对应值

但是引用类型创建的变量不同,引用变量(引用变量也可简称引用)存储的是指向的对象的地址

什么是对象?什么是地址?

我们暂时可以简单的理解为new后头的就是对象,剩下的会在类和对象章节详解。

地址,即内存的编号。内存为了能够高效的取出和存放数据,将自己划分为了各个单元,而这个单元的编号就被称作地址。


实际上在Java中,数组属于引用类型,当我们创建数组的时候,实际上创建的是一个引用

例如

int[] arr = new int[5];

这个代码中,我们创建了一个引用arr,它存储的地址指向我们创建的一个数组对象int[5]

假如我们尝试用下面的语句直接输出arr,会发现输出了一串怪异的字符串

public class Test {
    public static void main(String[] args) {
        int[] arr = new int[5];
        System.out.println(arr);
    }
}

假设输出的是[I@1b6d3586,其中根据前面的知识我们可以知道[代表数组,I代表类型

@暂时不用管,而@后面的我们可以暂时理解为地址

实际上@代表后面的值是哈希值,实际上后面那一串是地址的哈希值,但是暂时不用关注


看了上面的内容,可能有人有所疑惑:数组不是相同元素类型的集合吗?怎么又变成引用了?怎么存的又变成地址了?

数组类型和引用类型并不冲突,数组为何被称作引用类型,是因为它的数据存储方式和基本数据类型不同。在上面我们说过:初始化数组是要用new的,而new创建的对象是存储在堆区的。也就是说数组初始化后的数据实际上被存放在堆区,但是我们创建数组的时候实际上又是在方法内创建的,属于局部变量,是存放在栈区的。那么此时就需要引用来帮助操作数组的元素,而使用了引用的数据类型就被称作引用类型,存储引用的变量就叫做引用变量。因此数组被称为引用类型,而我们创建数组时的变量被称为引用变量。

并且,实际上我们的引用指向的空间就是相同元素类型的集合,所以归根结底数组就是相同元素类型的集合,这个定义并不会冲突。

在之后的学习中,我们还会经常接触到引用,可以通过学习的深入慢慢理解。


引用是不能指向引用的,因为引用一定是伴随着引用类型的

在Java中,我们无法自由的创建一个引用用来指向任意的数据类型,这也是为什么要把那些能使用引用的数据类型称作引用数据类型,而不是定义一个东西叫做引用,让它可以随意的指向任意的数据类型的原因。

public class Test {
    public static void main(String[] args) {
        int[] arr = new int[5];
        int[] arr2 = arr;
        System.out.println(arr);
        System.out.println(arr2);
        //会发现两个引用的值都是一样的
        //实际上此时两个引用都指向着刚开始创建的那个数组
    }
}

对象存储在栈区的时候,倘若此时没有任何引用指向这个对象,则它也无法再被指向,也无法被调用,那么此时JVM会把它识别成内存垃圾,直接进行回收,即销毁掉这些数据

public class Test {
    public static void main(String[] args) {
        int[] array1 = new int[3];
        int[] array2 = new int[]{1,2,3,4,5};
        array1 = array2;
    }
}

在引用中,有一个特殊的引用null,它代表这个引用不指向任何的对象

由于这个引用不指向任何的对象,因此我们也不能对它进行操作,一旦尝试操作就会报错

public class Test {
    public static void main(String[] args) {
        int[] arr = null;
        //会报错
        System.out.println(arr[0]);
    }
}

数组在内存中的存储

数组作为引用类型,在内存中的存储也是和一般变量不同的

那么就以下面的代码为例子,来帮助理解数组的内存存储

public class Test {
    public static void main(String[] args) {
        int[] arr = new int[5];
    }
}

这段代码的内存是如何开辟的呢?

arr是在main方法里的变量,是局部变量,那么就是在栈区开辟的空间

new的一个数组对象,是在堆区开辟的

我们arr这个引用中存储的地址,会指向int[5]这个对象在堆区的地址

用一张图来表示就是


接下来再通过几道题目来理解数组内存开辟

    //输出什么?
	public static void func() {
        int[] array1 = new int[3];
        array1[0] = 10;
        array1[1] = 20;
        array1[2] = 30;
        int[] array2 = new int[]{1,2,3,4,5};
        array2[0] = 100;
        array2[1] = 200;
        array1 = array2;
        array1[2] = 300;
        array1[3] = 400;
        array2[4] = 500;
        for (int i = 0; i < array2.length; i++) {
            System.out.print(array2[i] + " ");
        }
    }

首先创建了一个array1指向着0 0 0,并将内部的值改为10 20 30

然后创建了一个array2指向着1 2 3 4 5,并将内部的值改为100 200 3 4 5

随后将array2赋值给array1,那么此时array1array2都指向了100 200 3 4 5

10 20 30没有引用指向时,会被JVM自动回收掉

最后就通过array1改变了数组中的后面三个值,所以最后打印的结果为100 200 300 400 500


//输出什么
public class Test {
    public static void fun1(int[] arr){
        arr = new int[5];
        arr[0] = 10;
    }
    public static void main(String[] args) {
        int[] arr1 = new int[]{1,2,3,4,5};
        fun1(arr1);
        for (int x :
                arr1) {
            System.out.print(x + " ");
        }
    }
}

这里注意,虽然这里把arr1作为引用传出去了,此时arr中存储的也是arr1数组的地址,但是fun1()中赋予了一个新的数组给arr,那么改变的实际上是那个新创建的数组

所以原来的arr1并没有发生任何改变,输出的是1 2 3 4 5


 //输出什么?
public class Test {
    public static void fun2(int[] arr){
        arr[0] = 99;
    }
    public static void main(String[] args) {
        int[] arr1 = new int[]{1, 2, 3, 4};
        fun2(arr1);
        for (int x :
                arr1) {
            System.out.print(x + " ");
        }
    }
}

这里将arr1作为引用传出去了,那么在fun2arr的地址指向的就是1,2,3,4,那么我们对它的第一个元素改为99,所以最后的输出结果就是99 2 3 4

数组的使用

作为方法的参数

由于数组在局部变量中存储的是引用,而引用是指向栈区空间的,那么当我们把数组引用作为参数传出去的时候,那就可以跨方法的操作栈区中的数据

比如我们上一节没写出来的交换变量的方法,利用数组就可以写出来

public class Test {
    public static void swap(int[] arr){
        int tmp = arr[0];
        arr[0] = arr[1];
        arr[1] = tmp;
    }
    public static void main(String[] args) {
        int x = 10;
        int y = 20;
        System.out.println("交换前 x = " + x + " y = " + y);
        int[] arr = {x , y};
        swap(arr);
        x = arr[0];
        y = arr[1];
        System.out.println("交换后 x = " + x + " y = " + y);
    }
}

可能有人说这里多此一举,我的评价是我也这样觉得,但是重点是演示效果

并且这里也能体现引用类型的一个优势,我们传数组的时候传的就是一个地址而不是整个数组的值。因为数组是可以自定义空间大小的,那么倘若当数组很大的时候,也不会因为传参而导致压栈空间过大。

作为方法的返回值

比较简单,直接上代码

下面是一个返回斐波那契数列前N项的方法

public class Test {
    public static int[] fib(int n) {
        if (n <= 0) {
            return null;
        }
        if (n == 1) {
            int[] arr = new int[]{1};
            return arr;
        }
        int[] arr = new int[n];
        arr[0] = 1;
        arr[1] = 1;
        for (int i = 2; i < n; i++) {
            arr[i] = arr[i - 1] + arr[i - 2];
        }
        return arr;
    }

    public static void main(String[] args) {
        int[] fib = fib(5);
        for (int x: fib) {
            System.out.print(x+" ");
        }
    }
}

Arrays类简单介绍

这个类提供了一些方法,能够让我们更加便利的操作数组,下面是一些常用方法的介绍和简易模拟实现

数组转字符串

顾名思义,就是能将数组转换为一个字符串,然后方便我们将其输出

用到的方法是toString(),使用格式如下

Arrays.toString(数组名);

使用例

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

上面的代码会输出[1, 2, 3, 4, 5, 6, 7, 8, 9]


下面模拟实现一下参数为int类型的toString

这里需要提前知道一个字符串的性质,利用+=操作符就可以延长字符串,即使是变量它也会将其中的值变为字符串然后合并,例如

String str = "123";
int a = 45;
str += a;
//此时str里是 12345

下面为模拟实现代码

    public static String myToString(int[] arr) {
        //左括号
        String str = "[";
        for(int i = 0; i < arr.length; i++){
            //添加数字
            str += arr[i];
            //添加格式
            if(i < arr.length - 1){
                str += " ,";
            }
        }
        //右括号
        str += "]";
        return str;
    }

数组拷贝

copyOf()这个方法允许我们将数组中指定个数的元素复制粘贴到另外一个数组里,使用格式如下

Arrays.copyOf(数组来源, 要复制的个数);

使用例

public class Test {
    public static void main(String[] args) {
        int[] arr1 = {1, 2, 3, 4, 5, 6, 7, 8, 9};
        int[] arr2 = Arrays.copyOf(arr1,5);
        System.out.println(Arrays.toString(arr2));
    }
}

上面的代码会输出[1, 2, 3, 4, 5]


下面模拟实现一个int类型的copyOf

    public static int[] myCopyOf(int[] arr, int num){
        int[] ret = new int[num];
        for(int i = 0; i < num; i++){
            ret[i] = arr[i];
        }
        return ret;
    }

排序数组

sort()这个方法可以帮助我们排序数组,排序的顺序为升序,使用格式如下

Arrat.sort(数组名);

使用例

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

上面的代码输出[1, 2, 3, 4, 5, 6, 7, 8, 9]


冒泡排序

由于排序的算法很多,所以我们这里先通过比较简单的冒泡排序来模拟实现一个sort()

那么什么是冒泡排序呢?

冒泡排序的核心就是两两元素进行比较,并通过判断大小决定是否调换位置,最终使得数组元素按照大小顺序排列

以 5 1 3 4为例说明一下冒泡排序的算法是如何执行的

5比1大,交换

1 5 4 3

5比4大,交换

1 4 5 3

5比3大,交换

1 4 3 5

后面的流程类似就不多说了

那么其实我们也可以发现,假设数组有n个元素,则循环最多要执行n-1轮,因为每进行一轮一个数字就会变得有序,当n-1个数字都有序后,n个数就都有序了

那既然我们每进行一轮,一个数字就会变得有序,那么我们在内层循环进行比较的时候就可以少比一次,那么如果是到了n轮,就有n个数字有序,就可以少比较n个数字

代码实现如下

	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;
    }

实际上,这段代码还有优化空间。假如我们的数组已经有序了,那么后面的循环也不用再执行了。那么问题是,如何判断这个数组已经有序了?

实际上,if判断一旦执行,也就代表这个数组还是需要进行再一次的循环确定才能知道是否有序。但是一旦if判断在某一个循环没有执行,则代表该数组已经有序,可以直接跳出循环。

那么我们就可以设定一个标记,来检查这个if语句是否执行

public class Test {
    public static int[] bubbleSort(int[] arr){
        for(int i = 0; i < arr.length; i++){
            boolean flag = true;
            for (int j = 0; j < arr.length - 1 - i; j++){
                if(arr[j] > arr[j + 1]){
                    flag = false;
                    int tmp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = tmp;
                }
            }
            if(flag){
                break;
            }
        }
        return arr;
    }
    public static void main(String[] args) {
        int[] arr = {9,4,5,6,8,1,2,3,7};
        bubbleSort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

查找元素

binarySearch()这个方法可以帮助我们找出有序数组的某个元素,并且返回它的下标。假如没有找到就返回负值,使用格式如下

Arrays.binarySearch(数组名, 要找的元素);

使用例

public class Test {
    public static void main(String[] args) {
        int[] arr =  new int[]{1,2,3,4,5,6,7};
        int i = Arrays.binarySearch(arr, 6);
        System.out.println(i);
    }
}

二分查找

那么实际上,上面的查找运用了二分查找的思想

什么是二分查找,二分查找的核心即和中间值比较,从而减少查找次数

以上面的那个使用例为例,我们要从1 2 3 4 5 6 7中找出6

首先找中间值,中间值是 4,6和 4比 4更小,所以数字肯定在 4的右边

所以我们将 5作为左边界,7作为右边界,此时中间值为 6(按照整型计算方式)

那么就找到6了

那么我们就通过代码实现一下二分查找

public class Test {
    public static int myBinarySearch(int[] arr, int key) {
        int left = 0;
        int right = arr.length - 1;
        while (left <= right) {
            int mid = (left + right) / 2;
            if (key < arr[mid]) {
                //注意这里千万不能让right = mid,下面也同理
                //否则找不到数字时会死循环
                right = mid - 1;
            } else if (key > arr[mid]) {
                left = mid + 1;
            } else {
                return mid;
            }
        }
        return -1;
    }

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

二维数组

二维数组的定义和初始化

二维数组,即数组里面的元素也是数组,创建的时候也没有任何特殊的,就是多了一个[]

public class Test {
    public static void main(String[] args) {
        int[][] arr1 = {{123},{123}};
        int[][] arr2 = new int[][]{{123},{123}};
        int[][] arr3 = new int[1][1];
    }
}

既然都被叫做了二维数组,那么我们也可以从二维的角度上去理解这个数组

第一个括号内的数字为行,第二个括号里的数字为列

public class Test {
    public static void main(String[] args) {
        int[][] arr = {{1,2,3},{4,5,6}};
        //可以以下面的方式理解,也就是一行一个数组
        // 1 2 3
        // 4 5 6
    }
}

由于二维数组的各个元素都是数组,所以分开初始化的时候也有那么一点点特殊,但实际上就是创建一个数组

public class Test {
    public static void main(String[] args) {
        int[][] arr = new int[2][3];
        arr[0] = new int[]{1,2,3};
        arr[1] = new int[]{1,2,3};
    }
}

二维数组的使用

在访问二维数组的时候,两个括号里分别填写行和列的下标即可访问二维数组的元素(下标依旧是从零开始的)

public class Test {
    public static void main(String[] args) {
        int[][] arr = {{1,2,3},{4,5,6}};
        // 1 2 3
        // 4 5 6
        System.out.println(arr[0][1]);
        //打印2
    }
}

那么可能有人会问,那如果我只用一个括号访问二维数组那代表什么呢?

上面我们说过,二维数组的每一行都可以看作是一个数组。那么我们只用一个括号访问,即拿到的也就是一行,也就是拿到了一个数组。

同时,由于数组是通过引用访问的,所以我们只用一个括号的时候,拿到的就是对应行数组的引用

public class Test {
    public static void main(String[] args) {
        int[][] arr = {{1,2,3},{4,5,6}};
        System.out.println(arr[0]);
        System.out.println(arr[1]);
        //打印两个地址
    }
}

我们之前介绍的Arrays类里面的一些方法也可以对行直接使用

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

二维数组的特殊性

二维数组在使用new初始化的时候,行不能省略,但是列是可以省略的

并且当我们省略列的时候,每一行的列数可以不同

public class Test {
    public static void main(String[] args) {
        int[][] arr = new int[2][];
        arr[0] = new int[]{1,2,3};
        arr[1] = new int[]{1,2,3,4,5};
        System.out.println(Arrays.toString(arr[0]));
        System.out.println(Arrays.toString(arr[1]));
		//输出 [1, 2, 3] [1, 2, 3, 4, 5]
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值