3.10 数组
数组保存同一类型的值序列。在下面的部分中,您将看到如何在Java中使用数组。
3.10.1 声明数组
数组是存储同一类型值集合的数据结构。您可以通过一个整数索引访问每个单独的值。例如,如果a是整数数组,则[i]是数组中的第i个整数。
通过指定数组类型来声明数组变量,该数组类型是元素类型后跟[]-和数组变量名。例如,下面是整数数组a的声明:
int[] a;
但是,此语句只声明变量a。它还没有用实际数组初始化。使用新的运算符创建数组。
int[] a = new int[100]; // or var a = new int[100];
此语句声明并初始化一个100个整数的数组。
数组长度不必是常量:new int[n]创建一个长度为n的数组。
创建数组后,就不能更改其长度(当然,可以更改单个数组元素)。如果您经常需要在程序运行时扩展数组的长度,您应该使用第5章中介绍的数组列表。
注意
可以将数组变量定义为
int[] a;
或者
int a[];
大多数Java程序员更喜欢前一种样式,因为它将类型int[](整型数组)与变量名整齐地分隔开来。
Java有一个创建数组对象并提供初始值的快捷方式:
int[] smallPrimes = { 2, 3, 5, 7, 11, 13 };
请注意,此语法不使用new,也不指定长度。
允许在最后一个值后使用逗号,这对于一个随时间不断向其添加值的数组很方便:
String[] authors = {
"James Gosling",
"Bill Joy",
"Guy Steele",
// add more names here and put a comma after each name
};
可以声明匿名数组:
new int[] { 17, 19, 23, 29, 31, 37 }
此表达式分配一个新数组,并用大括号内的值填充它。它计算初始值的数量并相应地设置数组大小。可以使用此语法在不创建新变量的情况下重新初始化数组。例如,
smallPrimes = new int[] { 17, 19, 23, 29, 31, 37 };
等价于
int[] anonymous = { 17, 19, 23, 29, 31, 37 };
smallPrimes = anonymous;
注意
长度为0的数组是合法的。如果编写一个方法来计算数组结果,而结果恰好为空,那么这样的数组可能很有用。将长度为0的数组构造为
new elementType[0]
或者
new elementType[] {}
请注意,长度为0的数组与空数组不同。
3.10.2 访问数组元素
数组元素的编号从0到99(而不是1到100)。创建数组后,可以使用循环填充数组中的元素:
int[] a = new int[100];
for (int i = 0; i < 100; i++)
a[i] = i; // fills the array with numbers 0 to 99
创建数字数组时,所有元素都初始化为零。布尔值数组初始化为false。对象数组初始化为特殊值空,这表示它们还没有保存任何对象。这对初学者来说是令人惊讶的。例如,
String[] names = new String[10];
创建十个字符串的数组,所有字符串均为空。如果希望数组包含空字符串,则必须提供它们:
for (int i = 0; i < 10; i++) names[i] = "";
小心
如果构造一个包含100个元素的数组,然后尝试访问元素a[100](或0到99范围之外的任何其他索引),则会出现“数组索引越界”异常。
要查找数组的元素数,请使用array.length。例如:
for (int i = 0; i < a.length; i++)
System.out.println(a[i]);
3.10.3 for each循环
Java具有强大的循环结构,允许您循环使用数组中的每个元素(或任何其他元素集合),而不必对索引值感到困惑。
for (variable : collection) statement
将给定变量设置为集合的每个元素,然后执行语句(当然,语句可能是块)。集合表达式必须实现Iterable接口(如ArrayList)的数组或类的对象。我们将在第5章讨论数组列表,并在第9章讨论Iterable接口。
例如,
for (int element : a)
System.out.println(element);
每一行打印打印数组a的每个元素。
您应该将此循环理解为“针对a中的每个元素”。Java语言的设计者考虑使用关键字,如foreach和in。但是这个循环是对Java语言的后期添加,最后没有人想打破已经包含了这些名称的方法或变量的旧代码(例如System.in)。
当然,您可以使用传统的for循环实现相同的效果:
for (int i = 0; i < a.length; i++)
System.out.println(a[i]);
但是,“for each”循环更简洁,也不容易出错,因为您不必担心那些烦人的开始和结束索引值。
注意
“for each”循环的循环变量遍历数组的元素,而不是索引值。
如果需要处理集合中的所有元素,“for each”循环比传统循环是一个令人愉快的改进。然而,仍然有很多机会使用传统的for循环。例如,您可能不希望遍历整个集合,或者可能需要循环内的索引值。
提示
使用Arrays类的toString方法,有一种更简单的方法可以打印数组的所有值。调用Arrays.toString(a)返回一个包含数组元素的字符串,用括号括起来,用逗号分隔,如“[2,3,5,7,11,13]”。要打印数组,只需调用
System.out.println(Arrays.toString(a));
3.10.4 数组拷贝
您可以将一个数组变量复制到另一个数组变量中,但随后这两个变量引用同一个数组:
int[] luckyNumbers = smallPrimes;
luckyNumbers[5] = 12; // now smallPrimes[5] is also 12
图3.14显示了结果。如果要将一个数组的所有值复制到新数组中,请使用Arrays类中的copyOf方法:
图3.14 复制数组变量
int[] copiedLuckyNumbers = Arrays.copyOf(luckyNumbers, luckyNumbers.length);
第二个参数是新数组的长度。此方法的常见用法是增大数组的大小:
luckyNumbers = Arrays.copyOf(luckyNumbers, 2 * luckyNumbers.length);
如果数组包含数字,则其他元素将填充为0;如果数组包含布尔值,则填充为false。相反,如果长度小于原始数组的长度,则只复制初始值。
C++注意
Java数组与堆栈上的C++数组有很大的不同。但是,它本质上与指向堆上分配的数组的指针相同。也就是说,
int[] a = new int[100]; // Java
不同于
int a[100]; // C++
而是
int* a = new int[100]; // C++
在Java中,[]操作符被预定义来执行边界检查。此外,没有指针算法,您不能用增量a来指向数组中的下一个元素。
3.10.5 命令行参数
您已经看到一个Java数组的例子重复了好几次。每个Java程序都有一个带有String[] args参数的main方法。此参数指示主方法接收字符串数组,即命令行上指定的参数。
例如,考虑这个程序:
public class Message
{
public static void main(String[] args)
{
if (args.length == 0 || 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 Message -g cruel world
args数组会有下面的内容:
args[0]: "-g"
args[1]: "cruel"
args[2]: "world"
程序打印出消息
Goodbye, cruel world!
C++ 注意
在Java程序的main方法中,程序的名字没有在args数组中。例如,当你启动一个程序
java Message -h world
通过命令行,然后args[0]将会是"-h",并且不是"Message"或者"java"。
3.10.6 数组排序
为了排序一个数组的数字,你能够使用Arrays类中的一个sort方法
int[] a = new int[10000];
...
Arrays.sort(a);
这个方法使用一个优化版本的快排算法,它在大多数数据集中都很高效。Arrays类为数组提供几个其它方便的方法,它们在本节的最后API注释部分被包含。
清单3.7中的程序使数组工作。这个程序为一个彩票游戏随机抽取数字组合。例如,如果您玩“从49中选择6个数字”彩票,程序可能会打印以下内容:
Bet the following combination. It'll make you rich!
4
7
8
19
30
44
为了选择这样一组随机数字,我们首先用值1、2、……,n填充一个数组numbers:
int[] numbers = new int[n];
for (int i = 0; i < numbers.length; i++)
numbers[i] = i + 1;
第二个数组保存要绘制的数字:
int[] result = new int[k];
现在我们抽取k个数字。Math.random方法返回一个介于0(含)和1(不含)之间的随机浮点数。通过将结果与n相乘,我们得到一个介于0和n-1之间的随机数。
int r = (int) (Math.random() * n);
我们将第i个结果设置为该索引处的数字。最初,这只是r+1,但正如您现在看到的,number数组的内容在每次抽取后都会发生更改。
result[i] = numbers[r];
现在我们必须确保再也不要抽那个号码。所有的彩票号码都必须是不同的。因此,我们用数组中的最后一个数字覆盖numbers[r],并将n减少1。
numbers[r] = numbers[n - 1];
n--;
关键是,在每次抽取中,我们都选择一个索引,而不是实际值。索引指向包含尚未抽取的值的数组。
在抽取k个彩票号码后,我们对结果数组进行排序,以获得更令人满意的输出:
Arrays.sort(result);
for (int r : result)
System.out.println(r);
清单3.7 LotteryDrawing/LotteryDrawing.java
import java.util.*;
/**
* This program demonstrates array manipulation.
* @version 1.20 2004-02-10
* @author Cay Horstmann
*/
public class LotteryDrawing
{
public static void main(String[] args)
{
Scanner in = new Scanner(System.in);
System.out.print("How many numbers do you need to draw? ");
int k = in.nextInt();
System.out.print("What is the highest number you can draw? ");
int n = in.nextInt();
// fill an array with numbers 1 2 3 . . . n
int[] numbers = new int[n];
for (int i = 0; i < numbers.length; i++)
numbers[i] = i + 1;
// draw k numbers and put them into a second array
int[] result = new int[k];
for (int i = 0; i < result.length; i++)
{
// make a random index between 0 and n - 1
int r = (int) (Math.random() * n);
// pick the element at the random location
result[i] = numbers[r];
// move the last element into the random location
numbers[r] = numbers[n - 1];
n--;
}
// print the sorted array
Arrays.sort(result);
System.out.println("Bet the following combination. It'll make you rich!");
for (int r : result)
System.out.println(r);
}
}
java.util.Arrays 1.2
- static String toString(xxx[] a) 5
返回一个字符串,其中的元素a用括号括起来,用逗号分隔。在此方法和以下方法中,数组的组件类型xxx可以是int、long、short、char、byte、boolean、float或double。 - static xxx[] copyOf(xxx[] a, int end) 6
- static xxx[] copyOfRange(xxx[] a, int start, int end) 6
返回一个与a类型相同的数组,长度为end或end-start,并用a的值填充。如果end大于a.length,则结果将用0或false值填充。 - static void sort(xxx[] a)
使用优化的快速排序算法对数组排序。 - static int binarySearch(xxx[] a, xxx v)
- static int binarySearch(xxx[] a, int start, int end, xxx v) 6
使用二分查找算法搜索已排序数组a中的值v。如果找到v,则返回其索引。否则,返回负值r;-r-1是插入v以保持排序的点。 - static void fill(xxx[] a, xxx v)
将数组的所有元素设置为v。 - static boolean equals(xxx[] a, xxx[] b)
如果数组的长度相同,并且相应索引处的元素匹配,则返回true。
3.10.7 多维数组
多维数组使用多个索引来访问数组元素。它们用于表和其他更复杂的东西。您可以安全地跳过此部分,直到需要此存储机制为止。
假设你想做一个数字表,显示10000美元的投资在每年支付利息和再投资的不同利率情况下会增长多少(表3.8)。
表3.8 不同利率下的投资增长
10% | 11% | 12% | 13% | 14% | 15% |
---|---|---|---|---|---|
10,000.00 | 10,000.00 | 10,000.00 | 10,000.00 | 10,000.00 | 10,000.00 |
11,000.00 | 11,100.00 | 11,200.00 | 11,300.00 | 11,400.00 | 11,500.00 |
12,100.00 | 12,321.00 | 12,544.00 | 12,769.00 | 12,996.00 | 13,225.00 |
13,310.00 | 13,676.31 | 14,049.28 | 14,428.97 | 14,815.44 | 15,208.75 |
14,641.00 | 15,180.70 | 15,735.19 | 16,304.74 | 16,889.60 | 17,490.06 |
16,105.10 | 16,850.58 | 17,623.42 | 18,424.35 | 19,254.15 | 20,113.57 |
17,715.61 | 18,704.15 | 19,738.23 | 20,819.52 | 21,949.73 | 23,130.61 |
19,487.17 | 20,761.60 | 22,106.81 | 23,526.05 | 25,022.69 | 26,600.20 |
21,435.89 | 23,045.38 | 24,759.63 | 26,584.44 | 28,525.86 | 30,590.23 |
23,579.48 | 25,580.37 | 27,730.79 | 30,040.42 | 32,519.49 | 35,178.76 |
您可以将这些信息存储在二维数组(矩阵)中,我们称之为余额。
在Java中声明二维数组是很简单的。例如:
double[][] balances;
在初始化数组之前,不能使用该数组。在这种情况下,可以按如下方式进行初始化:
balances = new double[NYEARS][NRATES];
在其他情况下,如果知道数组元素,则可以使用一个简短的符号来初始化多维数组,而无需调用new。例如:
int[][] magicSquare =
{
{16, 3, 2, 13},
{5, 10, 11, 8},
{9, 6, 7, 12},
{4, 15, 14, 1}
};
初始化数组后,可以通过提供两对括号(例如balances[i][j]
)来访问单个元素。
示例程序存储一个一维利率数组和一个二维帐户余额数组,每年一个,利率一个。我们用初始余额初始化数组的第一行:
for (int j = 0; j < balances[0].length; j++)
balances[0][j] = 10000;
然后我们计算其他行,如下所示:
for (int i = 1; i < balances.length; i++)
{
for (int j = 0; j < balances[i].length; j++)
{
double oldBalance = balances[i - 1][j];
double interest = . . .;
balances[i][j] = oldBalance + interest;
}
}
清单3.8显示了完整的程序。
注意
“for each”循环不会自动循环二维数组中的所有元素。相反,它通过行进行循环,这些行本身就是一维数组。要访问二维数组a的所有元素,请嵌套两个循环,如下所示:
for (double[] row : a) for (double value : row) do something with value
提示
要打印出二维数组元素的快速脏列表,请调用
System.out.println(Arrays.deepToString(a));
输出结果的格式如下所示:
[[16, 3, 2, 13], [5, 10, 11, 8], [9, 6, 7, 12], [4, 15, 14, 1]]
清单3.8 CompoundInterest/CompoundInterest.java
/**
* This program shows how to store tabular data in a 2D array.
* @version 1.40 2004-02-10
* @author Cay Horstmann
*/
public class CompoundInterest
{
public static void main(String[] args)
{
final double STARTRATE = 10;
final int NRATES = 6;
final int NYEARS = 10;
// set interest rates to 10 . . . 15%
double[] interestRate = new double[NRATES];
for (int j = 0; j < interestRate.length; j++)
interestRate[j] = (STARTRATE + j) / 100.0;
double[][] balances = new double[NYEARS][NRATES];
// set initial balances to 10000
for (int j = 0; j < balances[0].length; j++)
balances[0][j] = 10000;
// compute interest for future years
for (int i = 1; i < balances.length; i++)
{
for (int j = 0; j < balances[i].length; j++)
{
// get last year's balances from previous row
double oldBalance = balances[i - 1][j];
// compute interest
double interest = oldBalance * interestRate[j];
// compute this year's balances
balances[i][j] = oldBalance + interest;
}
}
// print one row of interest rates
for (int j = 0; j < interestRate.length; j++)
System.out.printf("%9.0f%%", 100 * interestRate[j]);
System.out.println();
// print balance table
for (double[] row : balances)
{
// print table row
for (double b : row)
System.out.printf("%10.2f", b);
System.out.println();
}
}
}
3.10.8 不规则数组
到目前为止,您所看到的与其他编程语言没有太大的不同。但是在幕后确实有一些微妙的东西,你有时可以求助于你的优势:Java根本没有多维数组,只有一维数组。多维数组被伪造为“数组的数组”。
例如,前面例子中的balances数组实际上是一个包含十个元素的数组,每个元素都是六个浮点数的数组(图3.15)。
图3.15 一个二维数组
表达式balances[i]
是指第i个子数组,即表的第i行。它本身就是一个数组,balances[i][j]
指的是该数组的第j个元素。
因为可以单独访问数组行,所以实际上可以交换它们!
double[] temp = balances[i];
balances[i] = balances[i + 1];
balances[i + 1] = temp;
制作“不规则”数组也很容易,也就是说,不同行具有不同长度的数组。下面是标准示例。让我们制作一个数组,其中第i行和第j列的元素等于“从i个数字中选择j个数字”彩票可能产生的结果数。
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
因为j不能大于i,所以矩阵是三角形的。第i行有i+1个元素。(我们允许选择0个元素;有一种方法可以做出这样的选择。)要构建这个不规则数组,首先分配包含行的数组:
int[][] odds = new int[NMAX + 1][];
接下来,分配行:
for (int n = 0; n <= NMAX; n++)
odds[n] = new int[n + 1];
既然已经分配了数组,那么只要不超出界限,我们就可以以正常方式访问元素:
for (int n = 0; n < odds.length; n++)
for (int k = 0; k < odds[n].length; k++)
{
// compute lotteryOdds
. . .
odds[n][k] = lotteryOdds;
}
清单3.9给出了完整的程序。
C++注意
在C++中,Java声明
double[][] balances = new double[10][6]; // Java
不同于
double balances[10][6]; // C++
或者甚至
double (*balances)[6] = new double[10][6]; // C++
相反,将分配一个由十个指针组成的数组:
double** balances = new double*[10]; // C++
然后,指针数组中的每个元素都被六个数字组成的数组填充:
for (i = 0; i < 10; i++) balances[i] = new double[6];
谢天谢地,当你要求一个
new double[10][6]
时,这个循环是自动的。当需要不规则数组时,可以单独分配行数组。
清单3.9 LotteryArray/LotteryArray.java
/**
* This program demonstrates a triangular array.
* @version 1.20 2004-02-10
* @author Cay Horstmann
*/
public class LotteryArray
{
public static void main(String[] args)
{
final int NMAX = 10;
// allocate triangular array
int[][] odds = new int[NMAX + 1][];
for (int n = 0; n <= NMAX; n++)
odds[n] = new int[n + 1];
// fill triangular array
for (int n = 0; n < odds.length; n++)
for (int k = 0; k < odds[n].length; k++)
{
/*
* compute binomial coefficient n*(n-1)*(n-2)*...*(n-k+1)/(1*2*3*...*k)
*/
int lotteryOdds = 1;
for (int i = 1; i <= k; i++)
lotteryOdds = lotteryOdds * (n - i + 1) / i;
odds[n][k] = lotteryOdds;
}
// print triangular array
for (int[] row : odds)
{
for (int odd : row)
System.out.printf("%4d", odd);
System.out.println();
}
}
}
现在你已经看到了Java语言的基本编程结构。下一章将介绍Java中的面向对象编程。