【Java】还不会数组?一文万字全搞定

发布于:2024-05-10 ⋅ 阅读:(27) ⋅ 点赞:(0)

前言:前面两章我们详细讲解了Java基本程序设计结构中的基本知识,,包括:一个简单的Java应用,注释,数据类型,变量与常量,运算符,字符串,输入输出,控制流,大数值。 本篇将介绍最后一环——“数组”,带领大家更全面透彻地了解Java中数组的相关知识。

一.数组的基本概念

数组即存储着相同类型值的序列。
从定义中我不可以得到以下结论:

1.数组是一种数据结构,是用来存储同一类型值的集合;
2.数组中存放的值连续的;
3.每个空间有自己的编号,其实位置的编号为0,即数组的下标。

举个例子:在java中,包含6个整形类型元素的数组,就相当于下图中连在一起6个车位,从下图中可以看到:
在这里插入图片描述
那么,我们如何定义,使用一个数组呢?

二.数组的声明和初始化

2.1数组的声明

俗话说:变量先声明后使用。首先我们看看如何声明一个数组。
通过以下方式我们可以声明一个Java数组,在声明数组变量的时候,需要指出数组类型和数组变量的名字:

int[] array1;//array:英文单词表示数组
double[] array2;
boolean[] array3;
//...

这时我们就成功声明了一个array数组。看到这里,一些有C语言基础的小伙伴就会有一种“熟悉的陌生人”的感受,因为C语言是这样声明一个数组的:

int arr[];//C语言实现方式

两者看上去非常相似,仅仅只是“[ ]”的位置不同,实际上,在Java中,这两种形式都可以定义声明一个数组变量。只不过,大多数Java程序员更倾向于第一种方式,因为这样可以清晰地将变量类型和变量名区分开来,显得更加有逻辑性。(相当于我创建了一个名为array的变量,变量的类型是int[ ]).

2.2数组的初始化【重要】

在Java中,数组的初始化方式相对比较灵活,接下来我们一一讲解不同的初始化方式:
方法一:直接法

//对于一中我们声明的数组我们仅仅声明了变量array,并没有将它初始化为一个真正的数组
//方法一:直接初始化法
int[] array={1,2,3,4,5}//这种方式直接指明了数组中元素的值

这种方式对于C小伙伴们是最熟悉不过的,优点是简便直接,缺点是如果数组中元素个数过多,就显得比较麻烦了。
补充:这种初始化语法中不需要使用new(后文讲解),甚至不用指定长度。最后一个值后面允许有逗号,如果你要不断为数组增加值,这很方便:

String[] names={"彭于晏”,
"陈冠希",
"刘德华”,
//...
//可以在这之后添加更多的name,这个逗号是被允许的
}

方法二:新创建法

//方法二:new int[元素个数];
int[] array=new int[100];
//等价于var array=new int[100];

new int[n]语句会创建一个长度为n的数组,一旦创建了数组,就不能再改变它的长度(不过,可以改变单个数组元素)。如果程序运行过程中需要经常扩展数组大小,就应当使用另一种数据结构——“数组列表”(array list),以后会讲解。

方法三:匿名数组法

//方法三:
int[] array=new int[]{1,2,3,4,5};

这种方式会分配一个新数组并填入大括号中提供的值。它会统计初始值个数,所以new int[]中不需要指定数组个数。

补充:在Java中,允许有长度为0的数组。如果在编写一个结果为数组的方法时,这样一个长度为0的数组就很有用。可以如下创建长度为0的数组:
new elementType[0]

new elementType[] {}
注意:长度为0的数组与null并不相同!

三.访问数组元素

3.1数组下标/索引

我们以及成功声明并初始化好了一个数组,即我们准备工作已经完成,那么如何去使用一个数组呢?这时我们就需要学会去访问数组元素。
访问数组元素就需要我们找到数组元素“下标/索引”。

array[index];//通过下标/索引找到了对应数组元素

同其它程序设计语言一样,Java数组元素下标也是从0开始的,例如我们创建一个元素个数为100的数组,那么它的下标就是从0到99(而不是1到100)。一旦创建了数组,我们就可以在数组中填入元素:

int[] arrary=new int[100];
for (int i = 0; i < 100; i++) {
      arrary[i]=i+1;
      //向数组中填入数组1到100
 }

创建一个数字数组的时候,所有元素均初始化为0。boolean数组元素会初始化为false。对象数组元素则初始化为一个特殊值null,表示这些元素还未存放任何对象。初学者可能不解i,例如:

String[] names=new String[10];

会创建一个包含10个字符串的数组,所有的字符串均为null。如果希望这个数组包含空串,必须为元素指定空串:

 for (int i = 0; i < 10; i++) {
      	names[i]="";
}

3.2 for循环访问数组元素

此外,在访问数组元素的时候如果需要向数组中添加或删除一个值,第一步就是要改变数组内容,第二步在访问新数组元素的时候我们还需要修改数组元素个数。在Java中提供了一种array.length方法可以获取数组中元素个数,再通过for循环访问数组元素,例如:

 int[] a=new int[]{12,3,46,5,5,5,2,879};
 for (int i = 0; i < a.length; i++) {
       System.out.println(a[i]);
}

最后就是一个老生常谈的bug了——数组不能越界访问!
例如:我们创建了一个100个元素的数组,并试图访问array100,就会引发如下异常:
在这里插入图片描述
即表明你访问了一个越界的数组元素,这是不被允许的。

3.3 for each循环

虽然上一篇中我们已经提到了for each循环,但是由于它在数组中是一种较为新颖的循环,我们再次在这里提及它。
for each循环是Java中一种功能很强的循环结构,可以用来依次处理数组(或者其它元素集合)中的每个元素,==而不必考虑指定下标值。==这也是它区别与普通for循环最大的不同。
其格式为下:

for(变量:集合) statement

它定义一个变量用于暂存集合中的每一个元素,并执行相应语句(或语句块)。集合的表达式必须是一个数组或者是一个实现了Iterable接口的类对象(例如ArrayList,以后会讲解)。例如:

int[] a=new int[]{1,2,3,4,5};
for(int element:a)
    System.out.println(element);
    /*1
	  2
	  3
	  4
	  5*/

打印数组a的每一个元素,一个元素占一行。
实际上,这个循环应该读作“循环a中的每一个元素”(for each element in a)这也是它名字由来。
相比于传统for循环for each循环语句显得更加简洁,更不易出错,因为你不必因为下标的起始值和终止而操心。以下是二者对比:

//二维数组 就是一个特殊的一维数组
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();
}/**/

System.out.println("=====");

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

注意:for each循环语句的循环变量会遍历数组中的每个元素,而不是下标值。

此外,如果你仅仅想打印出数组中的所有值,那么Java中有一个更加简单的方式,即利用Array类中的toString方法,调用Array.toString(a),返回一个包含数组元素的字符串,这些元素包含在中括号内,并用逗号分隔开。例如:
在这里插入图片描述
结果如下:
在这里插入图片描述
注意引入包:import java.util.Arrays;

此外,我们还可以自我实现一个myToString方法:

   public static String myToString(int[] array) {
        String ret = "[";
        for (int i = 0; i < array.length; i++) {
            ret = ret + array[i];
            if(i != array.length-1) {
                ret += ", ";
            }
        }
        ret += "]";
        return ret;
    }

四.命令行参数

前面已经看到多个使用Java数组的示例。每一个Java应用程序都有一个带String[] args参数的main方法。这个参数表明main方法将接收一个字符串数组,也就是命令行参数。例如,看一看下面这个程序:

public class Test{
	public static void main(String[] args){
		if (args[0]. equals("-h"))
			System.out. print("Hello, ");
		else if (args[0]. equals("-g"))
			System. out.print("Goodbye, ");
		// print the other command-line arguments
		for (int i = 1; i < args. length; i++)
			System.out. print(" "+ args[i]);
		System.out.println("!");
	}
}

如果使用java Test -g cruel world命令调用这个程序,args数组将会包含一下内容:args[0]=“-g” args[1]=“cruel” args[2]=“world”。这个程序就会显示下面这个信息:
在这里插入图片描述

五.初始JVM的内存分布

在深入了解数组的使用之前,我们现要了解一下JVM的内存分布,可是我们为什么要了解这个东西呢?
我们知道,Java的程序是跑在虚拟机(即JVM)上,我们要想理解数组这一引用类型是如何引用变量的,就必须了解其它JVM上的内存是如何分配的。
首先,JVM对所使用的内存按照功能的不同进行了划分:
在这里插入图片描述
解释一下里面的各个部分:

  • 程序计数器 (PC Register): 只是一个很小的空间, 保存下一条执行的指令的地址
  • 虚拟机栈(JVM Stack): 与方法调用相关的一些信息,每个方法在执行时,都会先创建一个栈帧,栈帧中包含有:局部变量表、操作数栈、动态链接、返回地址以及其他的一些信息,保存的都是与方法执行时相关的一些信息。比如:局部变量。当方法运行结束后,栈帧就被销毁了,即栈帧中保存的数据也被销毁了。
  • 本地方法栈(Native Method Stack): 本地方法栈与虚拟机栈的作用类似. 只不过保存的内容是Native方法的局部变量. 在有些版本的 JVM 实现中(例如HotSpot), 本地方法栈和虚拟机栈是一起的
  • 堆(Heap): JVM所管理的最大内存区域. 使用 new 创建的对象都是在堆上保存 (例如前面的 new int[]{1, 2, 3,4,5} ),堆是随着程序开始运行时而创建,随着程序的退出而销毁,堆中的数据只要还有在使用,就不会被销
    毁。
  • 方法区(Method Area): 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. 方法编译出的的字节码就是保存在这个区域

简单解释一下这里为什么会出现两个栈区:我们知道JVM底层是由C/C++实现的,而本地方法栈中就包含了一些由C/C++程序实现的方法。而虚拟机栈才是我们平时所说的栈区。
现在我们只简单关心堆 和 虚拟机栈这两块空间,后序JVM中还会更详细介绍。

六.数组中常见的各类方法

6.1数组拷贝

在Java中,允许将一个数组变量拷贝到另一个数组变量中去,这时,两个变量将引用同一个数组

        int[] smallPrimes=new int[10]; 
        int[] luckyNumbers=new int[10];
        luckyNumbers[5]=12;
        luckyNumbers=smallPrimes;//smallPrimes[5]==12

在这里插入图片描述

正常来说我们自己是可以很轻松实现一个数组拷贝的方法的:

   public static int[] copyArray(int[] array) {
        int[] copy = new int[array.length];
        for (int i = 0; i < array.length; i++) {
            copy[i] = array[i];
        }
        return copy;
    }

但Java中提供了更便捷的Arrays类的copyOf方法:

luckyNumbers=Arrays.copyOf(luckyNumbers,luckyNumbers.length);
//第一个参数是要拷贝的原始数组
//第二个参数是新数组的长度

这个方法通常用来增加数组大小

luckyNumbers=Arrays.copyOf(luckyNumbers,2*luckyNumbers.length);

如果数组元素是数值型,那么额外的元素将被赋值为0;如果数组元素是布尔型,则将赋值为false。相反,如果长度小于原始数组的长度,则只拷贝前面的值。
此外,还有类似的Arrays类的copyOfRange方法:

public static void main(String[] args) {
    int[] array = new int[] { 1,2,3,4 };
    int[] ret = Arrays.copyOfRange(array,2,4);
    //第一个参数:要拷贝的原始数组
    //第二个参数from:表示要拷贝的起始位置
    //第三个参数to:表示要拷贝的终止位置
    System.out.println(Arrays.toString(ret));//[3,4]
}

Java数组与堆栈上的C++数组有很大的不同,但基本上与在**堆(heap)**上分配的数组指针一样,也就是说:
int[] a=new int[100];//Java
不同于
int[] a[100];//C++
而等同于
int* a=new int[100];//C++
表明Java中数组变量存储的本质上是一个地址,通过这个地址“引用”到堆区上的目标对象。
同时,Java中的[ ]运算符被预定义为会完成 越界检查,而且没有指针运算,即不能通过a+1得到数组中的下一个元素。

6.2数组排序

我们之前对数组常用的排序是冒泡排序,这里我们再次回顾一下:

public static void bubbleSort(int[] array){
        //i表示循环的趟数
        for (int i = 0; i < array.length-1; i++) {
            boolean flag=false;
            for (int j = 0; j < array.length-1-i; j++) {
                //从小到大排序
                if(array[j]>array[j+1]) {
                    int tempArray = array[j];
                    array[j]=array[j+1];
                    array[j+1]=tempArray;
                    flag=true;
                }
            }
            if(flag==false)
                return;
        }
    }
    public static void main(String[] args) {
            int[] a=new int[]{1,3,2,6,0,9};
            bubbleSort(a);
        for (int b:a) {
            System.out.print(b+" ");
        }
        //0 1 2 3 6 9
    }

要想对数值型数组进行排序,我们还可以使用Java中Arrays类的sort方法:

int[] a=new int[10000];
Arrays.sort(a);

这个方法使用了优化的快速排序算法。快速排序算法对于大多数数据集合来说都是效率比较高的。
接下来,我们通过一个经典案例练习一下——“抽彩游戏”:

问题描述:
抽彩是从多个数字中随机抽取几个不重复的值,进行排序输出且判断是否中奖。若中奖则输出中奖次数,若未中奖则给出提示。
在这里插入图片描述

示例代码如下:

import java.util.Arrays;
import java.util.Scanner;

public class LotteryDrawing {
    public static void main(String[] args) {
        int count = 0;
        //定义一个计量数,计算中奖了几次
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入您要抽取数字的最高数:");
        int n = sc.nextInt();
        //设定一个n存放最高位数值
        int n1 = n;
        //定义了一个n1与n的数值相同
        System.out.println("请输入您要抽取几个数字(1~"+n+"):");
        int k = sc.nextInt();
        //设定了抽取数
        if(k>n){
            System.out.println("对不起,您输入的数字不符合规范!");
            //若此时抽取数比最高数要高则报错
        }else{
            int[] numbers1 = new int[n];
            int[] numbers2 = new int[n1];
            //定义了两个数组,其长度分别与n和n1相同
            for (int i = 0; i < numbers1.length; i++) {
                numbers1[i] = i+1;
                //对数组numbers进行从1~n的赋值;
            }
            for (int i = 0; i < numbers2.length; i++) {
                numbers2[i] = i+1;
                //对数组numbers2进行从1~n1的赋值;
            }
            int[] result = new int[k];//抽取结果
            //定义了result数组使其长度等于抽取数k
            for (int i = 0; i < result.length; i++) {
                int r =(int) (Math.random()*n);
                //Math.random()*n会得到一个0到n-1之间的随机数,左闭右开!
                //抽取随机数的本质是抽取随机下标
                result[i] = numbers1[r];
                //选择随机数numbers[r]赋值给result[i]
                numbers1[r] = numbers1[n - 1];
                n--;
                //这里确保了不会再次抽取到同一个值
            }
            int[] winning = new int[k];//中奖结果
            for (int i = 0; i < winning.length; i++) {
                int w = (int) (Math.random()*n);
                winning[i] = numbers2[w];
                numbers2[w] = numbers2[n1 - 1];
                n1--;
            }
            Arrays.sort(result);
            //对数组进行排序
            System.out.println("抽取完成,您抽取的数为:");
            for(int r :result){
                System.out.print(r+" ");
            }
            //使用foreach循环对数组进行遍历
            System.out.println("");
            //换行
            Arrays.sort(winning);
            System.out.println("中奖数为:");
            for (int w:winning){
                System.out.print(w+" ");
            }
            for (int x = 0; x < result.length; x++) {
                for (int y = 0; y < winning.length; y++) {
                    if(result[x] == winning[y]){
                        count++;
                    }
                }
            }
            //使用两个for循环且利用if语句对数组的值进行判断
            //若有相同值则count计数值自增
            System.out.println();
            if(count == 0){
                System.out.println("对不起,您未中奖,请再接再厉!");
            }else{
                System.out.println("恭喜您,您中奖了"+count+"次");
            }
        }

    }
}

这个案例综合了数组排序的相关内容,仔细研读定会有所收获。

6.3数组查找

在数组使用中,有时我们需要在数组中找到目标元素,这时候我们就需要了解一些数组查找的方法:
方法一:直接暴力查找

  public static int findNum(int[] array,int key) {
        for (int i = 0; i < array.length; i++) {
            if(array[i] == key) {
                return i;
            }
        }
        return -1;
        //因为直接法是挨个查找,所以效率很低
    }

这种方式是最直接最简单但效率最为低下的方式,仅限数组大小较小时使用。
方法一:二分查找

 public static int binarySearch(int[] array,int key) {
        int left = 0;
        int right = array.length-1;
        while (left <= right) {
            int mid = (left+right) / 2;
            if(array[mid] == key) {
                return mid;
            }else if(array[mid] > key) {
                right = mid - 1;
            }else {
                left = mid + 1;
            }
        }
        return -1;
    }

这个方法相比于方法一效率还是提升了不少的。
对于数组查找,当然Java也提供了相应的方法Arrays类的binarySearch方法:

Arrays.binarySearch(int[] a,key);

具体使用方法就不必多说了,直接用!
Java当然不止提供了这一种排序方法,借助Java帮助手册我们可以自行查询相应方法和参数,结合实际情况和对应具体功能合理选择。

6.4数组填充

有时我们定义完数组后想要对其初始化一般是用for循环解决。
而Java中提供了Arrays类的fill方法:
功能:对数组内容进行指定填充。

public static void main(String[] args) {
            int[] array = new int[10];
            Arrays.fill(array,5);
            Arrays.fill(array,4,6,10);
            System.out.println(Arrays.toString(array));
            //[5, 5, 5, 5, 10, 10, 5, 5, 5, 5]
}

6.5数组比较

在上一篇中我们提到了数组之间的比较一定不能使用“ ==”比较 ,如果相等仅仅只能表明两个数组存储在相同的位置,而不能表示两个数组存储的内容是完全等同的。
而Java中提供的Arrays类的equals方法实现了数组的比较:

public static void main(String[] args) {
    int[] arr1 = new int[] { 5,4,3,2,1 };
    int[] arr2 = new int[] { 1,2,3,4,5 };
    int[] arr3 = new int[] { 1,2,3,4,5};
    System.out.println(Arrays.equals(arr1, arr2));//false
    System.out.println(Arrays.equals(arr2, arr3));//true
}

七.多维数组【二维数组】

多维数组中我们主要围绕最常见的二维数组展开讨论,这里我们就不详细一一解释了,因为它和一维数组大同小异:

      //二维数组的声明和初始化
       //以Int为例,
       //法一:静态初始化
       int[][] list={{123}{4,5,6},{7,8,9}};
       //法二:动态初始化
       int[][] list=new int[5][10];
       //法三:列数不确定
       int[][] list=new int[5][];
       //此时所有的一维数组都没有开辟内存空间,它们的地址都为 null;

这里举一个案例,观察一下二维数组在JVM中的内存分布:
在这里插入图片描述
实际上,二维数组的本质就是一个存储着一维数组的数组,它的各个一维数组的内存大小可以相同,也可以不相同。(不规则数组)
对于二维数组的访问我们跟一维数组类似,也有两种实现方式:

int[][] list={{1,2,3},{4,5,6},{7,8,9}};
//法一:
 for (int i = 0; i < list.length; i++) {
     for (int j = 0; j < list[0].length; j++) {
         System.out.print(list[i][j]+" ");
     }
         System.out.println();
}
     System.out.println("==================");
//法二:
for (int[] x :list) {
      for (int y:x) {
         System.out.print(y+" ");
      }
         System.out.println();
}
/*
1 2 3 
4 5 6 
7 8 9 
==================
1 2 3 
4 5 6 
7 8 9 */
   }

八.不规则数组

刚才我们提及了二维数组可以省略行初始化,即:

int[][] list=new int[5][];

为什么可以这样呢?我们知道,二维数组本质上是一个存储着一维数组的数组。列数的不确定可以使我们对数组进行更加灵活的处理,即可以方便地构造一个“不规则”数组,即数组的每一行都有不同的长度
下面我们通过一个杨辉三角案例具体了解一下不规则数组的实现:

问题描述:
在这里插入图片描述

示例代码如下:

    public static void main(String[] args) {
            int[][] a = new int[10][];

            for (int i = 0; i < a.length; i++) {
                // 给二维数组中每一个一维数组在堆上开辟内存空间
                a[i] = new int[i + 1];
                // 遍历每一个一维数组,赋值
                for (int j = 0; j < a[i].length; j++) {
                    // 每一行的第一个元素和最后一个元素都是1
                    if (j == 0 || j == a[i].length - 1) {
                        a[i][j] = 1;
                    } else {
                        // 每一行非第一个元素和最后一个元素的值 = 上一行的同一列 + 上一行的上一列
                        a[i][j] = a[i - 1][j] + a[i - 1][j - 1];
                    }
                }
            }

            // 输出杨辉三角
            for (int i = 0; i < a.length; i++) {
                for (int k = 0; k < a[i].length; k++) {
                    System.out.print(a[i][k] + "\t");
                }
                System.out.println();
            }
}

总结:本篇详细讲解了Java中数组的使用,我们不能发现,其实许多功能和方法Java中都有相应的封装好的类方法供你使用,一方面我们要去了解怎么实现,但更重要的是要学会去自行检索各类方法然后快速上手使用。本篇到此结束,看到这里实属不易,您的支持与鼓励是我前进最大的动力!也希望屏幕前的你能有所收获。