前言:前面两章我们详细讲解了Java基本程序设计结构中的基本知识,,包括:
一个简单的Java应用,注释,数据类型,变量与常量,运算符,字符串,输入输出,控制流,大数值。
本篇将介绍最后一环——“数组”,带领大家更全面透彻地了解Java中数组的相关知识。
目录
- 一.数组的基本概念
- 二.数组的声明和初始化
- 2.1数组的声明
- 2.2数组的初始化【重要】
- 三.访问数组元素
- 3.1数组下标/索引
- 3.2 for循环访问数组元素
- 3.3 for each循环
- 四.命令行参数
- 五.初始JVM的内存分布
- 六.数组中常见的各类方法
- 6.1数组拷贝
- 6.2数组排序
- 6.3数组查找
- 6.4数组填充
- 6.5数组比较
- 七.多维数组【二维数组】
- 八.不规则数组
一.数组的基本概念
数组:即存储着相同类型值的序列。
从定义中我不可以得到以下结论:
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={{1,2,3},{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中都有相应的封装好的类方法供你使用,一方面我们要去了解怎么实现,但更重要的是要学会去自行检索各类方法然后快速上手使用。本篇到此结束,看到这里实属不易,您的支持与鼓励是我前进最大的动力!也希望屏幕前的你能有所收获。