Java数组(万字带你吃透)

在这里插入图片描述

🎉🎉🎉写在前面:
博主主页:🌹🌹🌹戳一戳,欢迎大佬指点!
博主秋秋:QQ:1477649017 欢迎志同道合的朋友一起加油喔💪
目标梦想:进大厂,立志成为一个牛掰的Java程序猿,虽然现在还是一个小菜鸟嘿嘿
-----------------------------谢谢你这么帅气美丽还给我点赞!比个心-----------------------------

在这里插入图片描述



🌈一,初识数组

🌟1.1,数组定义

数组:可以看成是相同类型元素的一个集合。在内存中是一段连续的空间。


🌟1.2,数组创建及初始化

1.2.1,创建

类型名[] 数组名 = new 类型名[元素个数]

可以看到,Java里面的数组与C里面的数组还是有很大的区别的,一是创建的写法,二是创建数组的位置(在堆区)。另外数组是一个引用类型,它指向的是一个数组对象。


1.2.2,初始化

动态初始化:

int[] arr1 = new int[10]

在初始化时直接指定元素的个数,但是没有赋值,需要后期手动的去赋值。

静态初始化:

int[] arr2 = {1,2,3,4,5};
int[] arr3 = new int[]{1,2,3,4,5};//这种赋值的时候,后面的[] 里面不要写元素个数,编译器会自动判断

上面这两种静态初始化就是在创建的时候同时直接赋值,两种方法的本质是一样的,第一种是一种简写(编译的时候编译器会自动还原)。


🚩注意:

🌖1,在Java里面要注意一点,你一旦把长度写定了,就不能改了,如果在运行的时候需要扩展数组的大小,那么就得用到另一种数据结构—数组列表(array list)。

🌖2,数组也可以按照C语言的方法创建,不推荐。(知道它不是错的就行,不要这样用,选择性忘记,而且本身Java的写法才是标准的形式)

int arr[] = {1,2,3};//不推荐,int[] 这样写可以更好的明晰arr是一个数组类型变量

🌖3,静态和动态初始化也可以分为两步,但是省略格式不可以。

int[] arr = new int[10];
//可以分段写为 
int[] arr;
arr = new int[10];

------------------------
int[] arr = new int[]{1,2,3,4};
//可以分段写为
int[] arr;
arr = new int[]{1,2,3,4};
------------------------

//但是对于本身已经是简写模式下的初始化的 int[] arr = {1,2,3,4} 是不能分段写的
int[] arr;
arr = {1,2,3,4};//注意!!!这是错的!

🌖4,在Java里面,支持数组的大小是一个定义初始化好的变量。

int n = 10;
int[] arr4 = new int[n];//这是没有任何问题的

🌖5,采用动态初始化的方式,还没有给数组赋值时,数组元素是有默认值的。

在这里插入图片描述

这是基本的数据类型,如果数组元素也是引用类型,也就是说里面待存放的是地址的话,那么默认值就是null。

🌖6,在Java里面,允许有长度为0的数组。

int[] arr = new int[0] 或者 int[] arr = new int[]
//但是注意,长度为0与null是不一样的

🌈二,数组元素的访问输出

🌟2.1,for循环

public class TestDemo220429 {
    public static void main(String[] args) {
        int[] arr = {1,2,3,4,5};
        for(int i = 0;i < arr.length;i++){
            System.out.print(arr[i] + " ");
        }
    }
}

这是老方法了,只是需要注意下Java里面求取数组长度的方法即可。


🌟2.2,foreach

public class TestDemo220429 {
    public static void main(String[] args) {
        int[] arr = {1,2,3,4,5};
        for(int x:arr){
            System.out.print(x + " ");
        }
    }
} 

foreach是一种功能很强的循环结构,可以理解为加强版的for循环,可以依次拿到每一个元素,然后把其存到类型对应的变量中,然后将这个变量输出即可。这种方法的优点就是我们既可以达到输出数组元素的目的,也不需要考虑下标可能带来的问题,比如下标越界啥的。


🌟2.3,以字符串的形式打印

System.out.println(Arrays.toString(arr));//为了更加方便的去操作数组,有很多的方法,在工具类Arrays中
//将传进来的字符串以字符串的形式打印

🌟2.4,三种形式的输出结果

在这里插入图片描述


🌈三,数组是引用类型的变量

🌟3.1,初步认识JVM内存布局

在这里插入图片描述

🚩各个部分作用简述:

🌖1,程序计数器 (PC Register): 只是一个很小的空间, 保存下一条执行的指令的地址。
🌖2,虚拟机栈(JVMStack): 保存与方法调用相关的一些信息。
🌖3,本地方法栈(Native Method Stack):本地方法栈与虚拟机栈的作用类似. 只不过保存的内容是Native方法的局部变量
🌖4,堆(Heap):JVM所管理的最大内存区域. 使用 new 创建的对象都是在堆上保存堆是随着程序开始运行时而创建,随着程序的退出销毁,堆中的数据只要还有在使用,就不会被销毁。
🌖5,方法区(Method Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. 方法编译出的的字节码就是保存在这个区域。

我们现阶段可能接触的最多的还是虚拟机栈以及堆区这两个内存分区,那么我们用代码来具体分析一下二者的关系!

在这里插入图片描述


🌟3.2,引用变量使用示例

public class TestDemo220429 {
    public static void main(String[] args) {
        int[] array1 = {1,2,3};
        int[] array2 = {4,5,6};
        array2 = array1;
        for (int x:array2) {
            System.out.print(x + " ");
        }
    }
}

其实对于引用就当成是一个普通变量就好,只不过就是这个变量里面存放的是地址,其它没有什么差别,上面的这段代码的输出结果是 1 2 3,原理如下:

在这里插入图片描述

这里补充一个点,就是当某一个对象不在被指向(引用)的时候,就会被JVM的垃圾回收器给回收掉,并不需要我们手动释放。


🌟3.3,认识null

  int[] arr = null;

可以将一个引用类型的变量赋值为null,表示它不指向任何的对象,也就是空对象。所以你不能对这个引用变量arr做任何的操作,因为它没有实体的对象,而这也就是我们可能常会看到的空指针异常的原因。


🌟3.4,一个题加固对引用的认识

public class TestDemo220429 {
    public static void func1(int[] arr1){
        arr1 = new int[]{1,2,3};
    }
    public static void func2(int[] arr2){
        arr2[0] = 9;
    }
    public static void main(String[] args) {
        int[] arr = {8,8,8};
        func1(arr);
        for (int x:arr) {
            System.out.print(x + " ");
        }
        System.out.println();
        func2(arr);
        for (int x:arr) {
            System.out.print(x + " ");
        }
    }
}

程序运行截图:
在这里插入图片描述

对于这个题,我一开始的想法是,因为传的是引用,也就是对象的地址,所以会改变值,那我预测的结果就是 1 2 3,9 2 3,但是结果往往是不尽如人意,我的预测结果不能说是一模一样吧,只能说是毫不相干,下面就让我们来分析分析到底是哪里出了问题?🤔🤔🤔
在这里插入图片描述

在这里插入图片描述

这里不要弄混淆了,记住,实参与形参永远是两个东西,但是这里因为传的是引用,也就是说对象的地址,所以利用形参是可以改动到实参的值的。也就是说,对于数组而言,我们传引用调用方法,如果是直接在方法内部去改动值的话,是可以直接影响到这个数组对象的值的,就比如func2中,这个题坏就坏在它在func1中是直接给形参自己new了一个对象,可以说从这里开始形参与实参直接是一点关系都没有了。

总结:传引用调用的时候,形参可以改变实参指向的对象的内部元素的值,但是不能改变实参的指向,最多只能改变形参其自身的指向。


🌈四,数组的应用

🌟4.1,传参传数组类型

这里就不给大家举例子了,因为前面很多例子都用到了传参传数组类型。其本质上传的是一个引用类型的变量,也就是数组对象的地址,我们都是通过这个地址访问到数组对象,然后对其进行一些操作。同时因为传的是地址,而不是真正的整个数组,也免去了对于整个数组的临时拷贝的过程。


🌟4.2,作为返回值

//写一段代码将array数组中的元素变为2倍
public class TestDemo220429 {
    public static int[] getDouble(int[] array){
        for(int i = 0;i < array.length;i++){
            array[i] *= 2;
        }
        return array;
    }
    public static void main(String[] args) {
        int[] array = {1,2,3};
        int[] ret = getDouble(array);
        System.out.println(Arrays.toString(ret));
    }
}

程序运行截图:

在这里插入图片描述

在Java里面,我们可以返回一个数组,也就是一个引用类型的变量,它里面存放的是数组对象的地址,数组对象是开辟在堆上的。而在C语言里面,我们是不能返回数组的,因为数组名是首元素的地址,返回就相当于是在返回栈空间的地址了,是不允许的,返回都是只能返回值变量。


🌈五,数组练习

数组里面会用到的很多方法都是放在我们的工具类Arrays里面的。

🌟5.1,数组转字符串

数组转字符串利用 Arrays类里面的toString方法是可以直接完成的,但是下面我们来模拟实现一下这个简单的功能,如下:

public class TestDemo220429 {
    public static String myToString(int[] array){
        String tmp = "[";
        for(int i = 0;i < array.length;i++){
            tmp += array[i];
            if(i != array.length - 1){
                tmp += ",";
            }
            if(i == array.length - 1){
                tmp += "]";
            }
        }
        return tmp;
    }
    public static void main(String[] args) {
        int[] array = {1,2,3};
        String ret = myToString(array);
        System.out.println(ret);
    }
}

原理其实很简单,就是利用了Java里面的 “ +” 号的对于字符串的拼接特性。


🌟5.2,数组拷贝

5.2.1,for循环拷贝

public class TestDemo220429 {
    public static int[] copyArray(int[] array){
        int[] tmp = new int[array.length];
        for(int i = 0;i < array.length;i++){
            tmp[i] = array[i];
        }
        return tmp;
    }
    public static void main(String[] args) {
        int[] array = {1,2,3,4,5};
        int[] ret = copyArray(array);
        System.out.println(Arrays.toString(ret));
    }
}

基本原理就是创建一个中间数组,然后一个元素一个元素的进行拷贝。


5.2.2,利用Arrays类方法

public class TestDemo220429 {
    public static void main(String[] args) {
        int[] array = {1,2,3,4,5};
        int[] ret1 = Arrays.copyOf(array,array.length);
        System.out.println(Arrays.toString(ret));
    }
}

在工具类Arrays中,有专门的用于数组拷贝的方法供我们使用。方法具体的解析我们可以看一下源码,如下:

在这里插入图片描述

这里是以int类型的数组作为展示,其实在Arrays类里面,每种类型的数组都会有一个自己对应的拷贝的方法,是可以直接使用的。


5.2.3,利用System类的方法

import java.lang.System;
import java.util.Arrays;
public class TestDemo220430 {
    public static void main(String[] args) {
        int[] array = {1,2,3,4,5};
        int[] ret = new int[array.length];
        System.arraycopy(array,0,ret,0,array.length);
        System.out.println(Arrays.toString(ret));
    }
}

源码解析:

在这里插入图片描述

arraycopy是一个System类里面的本地方法,因为是用C/C++代码实现的,所以运行起来效率速度肯定是更快的,另外,如果仔细观察会发现,在上面的Arrays.copyof()方法里面的实际拷贝过程调用的就是arraycopy这个native方法。


5.2.3,克隆方法

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

clone()产生的是这个数组对象的一个副本,此方法属于Object类(是所有类的父类)。


5.2.3,深浅拷贝问题

上面介绍完了四种拷贝数组的方法,但是说到拷贝,我们就不得不谈到深浅拷贝的问题,那下面我们就来浅浅的分析一下…

首先,认识认识深浅拷贝的定义:

🌖1,浅拷贝:浅拷贝即只复制对象的引用,所以副本最终也是指向父对象在堆内存中的对象(也就是说改动其中一个,另一个会受到影响)。

🌖2,深拷贝:深拷贝则是直接复制父对象在堆内存中的对象,最终在堆内存中生成一个独立的,与父对象无关的新对象。(也就说拷贝过去内容会生成一个新的对象,与父对象二者相互不影响)。


那么对于上面的四种拷贝方法,我们的结论是,从本质上都是浅拷贝的。

🚩注意:

🌖1,对于基本数据类型的数组:

在这里插入图片描述

可以看到对于基本数据类型的数据,拷贝过去的对象与原来的是毫不相干,互不影响的,那又为什么说本质上是浅拷贝呢?因为还有引用类型的变量没有考虑…
在这里插入图片描述


🌖2,对于引用类型变量的数组

在这里插入图片描述


所以说,因为有引用数据类型的数组的拷贝的这么一个特例,我们还是认为上面的拷贝方式从本质都是浅拷贝的,但如果深究,那就必须得分两种情况来仔细分析了。至于解决方法原理其实也很简单,就是我们在拷贝数组之前,先把他里面引用指向的对象先拷贝一份,比如说上面的对象a,把它拷贝一份后,然后再拷贝数组,让两个数组元素指向的不是同一个对象a即可。(具体的实现后续学到接口会细说)


🌟5.3,找出数组元素的最大值

public class TestDemo220430 {
    public static int getMaxnum(int[] array){
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        for(int i = 0;i < array.length;i++){
            if(array[i] > max){
                max = array[i];
            }
            if(array[i] < min){
                min = array[i];
            }
        }
        return max;//最大值
        //return min // 最小值
    }
    public static void main(String[] args) {
        int[] array = {12,13,4,5,6,88};
        int ret = getMaxnum(array);
        System.out.println("最大值是:" + ret);
    }
}

🌟5.4,查找数组中的指定元素

5.4.1,顺序查找

import java.util.Scanner;
public class TestDemo220430 {
    public static int find(int[] arr,int key){
        for(int i = 0;i < arr.length;i++){
            if(arr[i] == key){
                return i;
            }
        }
        return -1;//找不到就返回-1
    }
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        System.out.println("请输入你要查找的数:");
        int key = scan.nextInt();
        int[] arr = {1,2,3,4,5};
        int ret = find(arr,key);
        if(ret != -1){
            System.out.println("找到了,下标为:" + ret);
        }else{
            System.out.println("未找到!");
        }
    }
}

顺序查找,最朴素的查找方法,原理就是遍历数组一个个对比即可。


5.4.2,二分查找

import java.util.Scanner;
public class TestDemo220430 {
    public static int find1(int[] arr,int key){
        int left = 0;
        int right = arr.length - 1;
        while(left <= right){
            int mid = (left + right)/2;
            if(arr[mid] > key){
                right = mid - 1;
            }else if(arr[mid] < key){
                left = mid + 1;
            }else{
                return mid;
            }
        }
        return -1;
    }
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        System.out.println("请输入你要查找的数:");
        int key = scan.nextInt();
        int[] arr = {1,2,3,4,5};
        int ret = find1(arr,key);
        if(ret != -1){
            System.out.println("找到了,下标为:" + ret);
        }else{
            System.out.println("未找到!");
        }
    }
}

二分查找相信大家也听过,它的原理就是不断地拆分成一半然后去查找直至最后找到那个数,但是需要注意的是他只能针对于有序的数组,因为我们是每次和中间下标的元素比值,然后决定去哪半边找,如果是无序的,这将没有任何的意义。

当然,上面这段代码还不是最优的,还有一点点小毛病需要优化。

int mid = (left + right)/2;
//这里最好的写法是 int mid = left + (right - left)>>>1 ,无符号右移就是相当于除2(正数负数都一样),另外 (left + right)如果在数组比较长的情况下,有可能会超过整形的表示范围,所以最好改成 left + (right - left)>>>1

上面是我们自己的模拟实现,但是Java这么强大方便的语言怎么会让你自己去写一个二分查找的方法呢,直接就给你安排好了类方法,直接用就完事了!

import java.util.Scanner;
public class TestDemo220430 {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        System.out.println("请输入你要查找的数:");
        int key = scan.nextInt();
        int[] arr = {1,2,3,4,5};
        int ret1 = Arrays.binarySearch(arr,key);//工具类Arrays里面有二分查找的方法
        System.out.println("下标为:" + ret1);
    }
}

对于这个方法,比较有意思的就是它的返回值,如果找到了就还是返回相应的下标值,如果是没找到,就是最后的 -(left + 1)。

在这里插入图片描述


既然说到了类方法的二分查找,那我们来具体的看看定义(这里只是以整形的数组的二分查找法为例,每个类型的数组都有相应的二分查找法):

在这里插入图片描述

我们上面的查找是在整个数组范围下进行的,其实也是可以在范围内查找的,也就是[fromIndex , toIndex) 特别要注意一下这里是左闭右开的范围!

    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        System.out.println("请输入你要查找的数:");
        int key = scan.nextInt();
        int[] arr = {1,2,3,4,5};
        int ret1 = Arrays.binarySearch(arr,1,3,key);
        System.out.println("下标为:" + ret1);
    }

🌟5.5,判断一个数组是否是升序的

public class TestDemo220430 {
    public static boolean isUp(int[] array){
        for(int i = 0;i < array.length;i++){
            if(array[i] > array[i + 1]){
                return false;
            }
        }
        return true;
    }
    public static void main(String[] args) {
        int[] array = {2,3,1,4,6,8,7};
        boolean ret = isUp(array);
        System.out.println(ret);
    }
}

🌟5.6,冒泡排序优化版

public class TestDemo220430 {
    public static void bubbleSort(int[] array){
        for(int i = 0;i < array.length - 1;i++){
            int flg = 1;//定义一个标签,默认每一轮是有序的
            for(int j = 0;j < array.length - 1 -i;j++){
                if(array[j] > array[j+1]){
                    int tmp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = tmp;
                    flg = 0;//如果是if满足了,就说明是无序的,将flg = 0
                }
            }
            if(flg == 1){
                break;//如果比了一轮flg没变,就说明是有序的,不需要再比了
            }
        }
    }
    public static void main(String[] args) {
        int[] array = {1,2,3,5,6,4};
        bubbleSort(array);
        System.out.println(Arrays.toString(array));
    }
}

当然,Java里面是有Arrays.sort()的排序方法的,比冒泡排序的效率会高很多。

🌟5.7,数组逆序

public class TestDemo220430 {
    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[] array = {1,2,3,4,5};
        reverse(array);
        System.out.println(Arrays.toString(array));
    }
}

🌟5.8,数组元素的排列(偶数在前,奇数在后)

public class TestDemo220430 {
    public static void swap(int[] arr){
        int left = 0;
        int right = arr.length - 1;
        while(left < right){
            while(left < right && arr[left]%2 == 0){
                left++;//偶数放前面,所以只要前面有偶数,left就往后走
            }//跳出就说明遇到奇数了
            while(left < right && arr[right]%2 != 0){
                right--;//奇数放后面,所以只要后面有奇数,right就往前走
            }//跳出就说明遇到偶数了
            if(left < right){//当right与left相遇了就不用交换了
                int tmp = arr[left];
                arr[left] = arr[right];
                arr[right] = tmp;
            }
        }
    }
    public static void main(String[] args) {
        int[] arr = {1,2,3,4,5,6,7,8,9};
        swap(arr);
        System.out.println(Arrays.toString(arr));
    }
}

🌈六,二维数组(重点理解内存结构)

🌟6.1,二维数组的定义

二维数组的定义方式:

int[][] array1 = new int[2][3];
int[][] array2 = {{1,2,3},{4,5,6}};
int[][] array = new int[][]{{1,2,3},{4,5,6}};

和一维数组一样,也有三种定义方式,其实到这里我们可以发现在Java里面定义数组的话,如果是动态初始化,没有立刻赋值的情况下,那个[]里面是可以写你需要的长度大小的值的,但是如果是静态初始化,记住[]里面一定不能写任何具体的数字,因为编译器会自己推断数组的长度大小。


🌟6.2,二维数组的存储方式

那二维数组在内存中是怎么存储的呢?

在这里插入图片描述

所以我们可知,我们利用数组名array这个引用变量,访问到的其实是一个一维数组,只不过这个一维数组的长度是我们二维数组的行数,它的每一个元素又是一个引用变量,指向的又是一维数组,现在这个一维数组里面就是我们二维数组的每一行的具体的元素,这个一维数组的大小也就是我们的列数。


🌟6.3,遍历输出二维数组

一,规则的二维数组

for循环打印:

public class TestDemo220430 {
    public static void main(String[] args) {
        int[][] array = new int[][]{{1,2,3},{4,5,6}};
        for(int i = 0;i < array.length;i++){
            for(int j = 0;j < array[i].length;j++){
                System.out.print(array[i][j] + " ");
            }
            System.out.println();
        }
    }
}

注意是怎么求二维数组的行数与列数的。


利用Arrays类方法:

System.out.println(Arrays.toString(array));//一维数组可以这么将数组转字符串打印,但是这是二维数组,array这个引用指向的也是一个一维数组,里面存的是地址,所以肯定是行不通的

在这里插入图片描述

既然如此,那我们二维数组肯定也是有特定的方法的,如下:

System.out.println(Arrays.deepToString(array));

在这里插入图片描述


foreach打印输出:

for (int[] tmp:array) {
            for (int x:tmp) {
                System.out.print(x + " ");
            }
            System.out.println();
        }

foreach这里用的话就必须要嵌套使用才可以。


二,不规则的二维数组

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

C语言里面。二维数组的列数必须写,行数可以自动推导。但是在Java里面,你的行是必须指定的,并且列不可以自动推导,因为你如果不指定行数,那么你通过数组名这个引用变量访问到的一维数组都不确定,并且因为列数不能自动推导,所以就相当于现在这个一维数组里面的引用变量值是null。

在这里插入图片描述

可以看到,Java里面的二维数组是很神奇的,因为它的二维数组的每一行的列数竟然是可以不一样的,也就是说对于每一行,也就是那个一维数组里面的引用变量所指向的对象我们是可以单独各自赋值的。


今天的博客就到这了,字数有一点点小多,但是它够详细呀。如果看完觉得不错的话还请帮忙点点赞咯,十分感谢呢!🥰🥰🥰
在这里插入图片描述

  • 39
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 32
    评论
评论 32
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力学习.java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值