7.1 Linear Arrays
Key terms: array, element type, linear array, one-dimensional array, initializer list, for-each loop, enhanced for loop, utility class, shallow copy, deep copy, clone, command-line arguments
7.1.1 Using Arrays
When rolling a pair of ordinary 6-sided dice, there are 11 possible sums (2-12). Listing 7.1.1a calculates the probabilities of these sums. Note the redundancy in the code: there are 11 variable declarations, a switch statement with 11 case labels, and 11 arithmetic calculations of the same general form. Even if this redundancy were acceptable, the approach would not scale well to additional dice or dice with a different number of sides.
Listing 7.1.1a - DiceSums.java
package chap07.sect1;
/**
* Calculates the probability of each possible sum when rolling a pair of dice.
*
* @author Drue Coles
*/
public class DiceSums {
public static void main(String[] args) {
// possible sums
int sum2 = 0;
int sum3 = 0;
int sum4 = 0;
int sum5 = 0;
int sum6 = 0;
int sum7 = 0;
int sum8 = 0;
int sum9 = 0;
int sum10 = 0;
int sum11 = 0;
int sum12 = 0;
// for each possible outcome, increment the corresponding counter
for (int die1 = 1; die1 <= 6; die1++) {
for (int die2 = 1; die2 <= 6; die2++) {
switch (die1 + die2) {
case 2 -> sum2++;
case 3 -> sum3++;
case 4 -> sum4++;
case 5 -> sum5++;
case 6 -> sum6++;
case 7 -> sum7++;
case 8 -> sum8++;
case 9 -> sum9++;
case 10 -> sum10++;
case 11 -> sum11++;
case 12 -> sum12++;
}
}
}
// calculate and format results
System.out.printf("%3s %12s%n", "SUM", "PROBABILITY");
final int possibleRolls = 36;
final String rowFormat = "%3d %6.1f%% %n";
double prob2 = (double) sum2 / possibleRolls * 100;
System.out.printf(rowFormat, 2, prob2);
double prob3 = (double) sum3 / possibleRolls * 100;
System.out.printf(rowFormat, 3, prob3);
double prob4 = (double) sum4 / possibleRolls * 100;
System.out.printf(rowFormat, 4, prob4);
double prob5 = (double) sum5 / possibleRolls * 100;
System.out.printf(rowFormat, 5, prob5);
double prob6 = (double) sum6 / possibleRolls * 100;
System.out.printf(rowFormat, 6, prob6);
double prob7 = (double) sum7 / possibleRolls * 100;
System.out.printf(rowFormat, 7, prob7);
double prob8 = (double) sum8 / possibleRolls * 100;
System.out.printf(rowFormat, 8, prob8);
double prob9 = (double) sum9 / possibleRolls * 100;
System.out.printf(rowFormat, 9, prob9);
double prob10 = (double) sum10 / possibleRolls * 100;
System.out.printf(rowFormat, 10, prob10);
double prob11 = (double) sum11 / possibleRolls * 100;
System.out.printf(rowFormat, 11, prob11);
double prob12 = (double) sum12 / possibleRolls * 100;
System.out.printf(rowFormat, 12, prob12);
}
}
Output 7.1.1a
SUM PROBABILITY
2 2.8%
3 5.6%
4 8.3%
5 11.1%
6 13.9%
7 16.7%
8 13.9%
9 11.1%
10 8.3%
11 5.6%
12 2.8%
The solution to the problem of redundancy is to store the sum counters not as separately declared
and manipulated int variables, but as a single array. An array is a sequence of items of the
same type stored contiguously in memory. Because the elements occupy contiguous memory locations,
the runtime system can immediately determine the address of any element given its index. Listing
7.1.1b is an improved version of the DiceSums program that uses an array for the sum counters. It
is much shorter and simpler, and also displays the results in the form of a bar chart. To generate
results for 12- or 20-sided dice, faces can be initialized accordingly.
Listing 7.1.1b - DiceSums2.java
package chap07.sect1;
/**
* Calculates the probability of each possible sum when rolling a pair of dice.
*
* @author Drue Coles
*/
public class DiceSums2 {
public static void main(String[] args) {
final int faces = 6;
final int maxSum = 2 * faces;
// increment counter for each sum
int[] sums = new int[maxSum + 1];
for (int die1 = 1; die1 <= faces; die1++) {
for (int die2 = 1; die2 <= faces; die2++) {
sums[die1 + die2]++;
}
}
// calculate and format results
final int possibleRolls = faces * faces;
final String rowFormat = "%3d %6.1f%% %s %n";
System.out.printf("%3s %12s%n", "SUM", "PROBABILITY");
for (int i = 2; i <= maxSum; i++) {
double prob = (double) sums[i] / possibleRolls * 100;
// sequence of stars for a bar chart
int numStars = (int) Math.round(prob);
String stars = "★".repeat(numStars);
System.out.printf(rowFormat, i, prob, stars);
}
}
}
Output 7.1.1b
SUM PROBABILITY
2 2.8% ★★★
3 5.6% ★★★★★★
4 8.3% ★★★★★★★★
5 11.1% ★★★★★★★★★★★
6 13.9% ★★★★★★★★★★★★★★
7 16.7% ★★★★★★★★★★★★★★★★★
8 13.9% ★★★★★★★★★★★★★★
9 11.1% ★★★★★★★★★★★
10 8.3% ★★★★★★★★
11 5.6% ★★★★★★
12 2.8% ★★★
The type int[] is pronounced array of ints, and the array referenced by sums is said to have
element type int, just as, for example, the element type of String[] is String. The size
of an array is fixed when the array is created and cannot be changed afterward. Indexing is
zero-based as with strings, and indexing into an array out of bounds will cause an exception to be
thrown. The elements of an array of primitive values are automatically initialized to their default
values (zero for the numeric types, and false for boolean). An array type with a single pair of
brackets, such as int[] or String[], is called a linear array, or one-dimensional array.
Multidimensional arrays are discussed later in this chapter.
7.1.2 Initializer Lists
Listing 7.1.2 shows how an array can be declared and initialized in a single statement using an
initializer list. This is a list of values or object references to be stored in the array,
separated by commas and wrapped in curly braces. The program declares several String arrays in
order to fill a sentence template with randomly chosen words. Arrays are objects in Java, and
each has a length attribute that stores its number of elements. The get method uses length to
generate a random index in the valid range for the array.
Listing 7.1.2 - LoveLetterGenerator.java
package chap07.sect1;
import java.util.Scanner;
import java.util.concurrent.ThreadLocalRandom;
/**
* Outputs a custom line of poetry of the following form:
*
* Dear [name], I will [verb] your [adverb] [adjective] [noun] forever! ♥ ♥ ♥
*
* @author Drue Coles
*/
public class LoveLetterGenerator {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.print("Enter recipient's name: ");
String name = in.next();
String[] verbs = {"cherish", "embrace", "endure", "fear", "follow", "seek", "treasure"};
String[] adverbs = {"deceptively", "intensely", "ominously", "strangely", "unspeakably"};
String[] adjectives = {"ancient", "blinding", "eternal", "overflowing", "shimmering"};
String[] nouns = {"doom", "echo", "flame", "heart", "smile", "reflection", "shadow", "soul"};
String verb = get(verbs);
String adverb = get(adverbs);
String adjective = get(adjectives);
String noun = get(nouns);
final String message = "Dear %s, I will %s your %s %s %s forever! ♥ ♥ ♥%n";
System.out.printf(message, name, verb, adverb, adjective, noun);
}
/**
* Returns a randomly chosen string from a given array.
*/
private static String get(String[] array) {
ThreadLocalRandom rand = ThreadLocalRandom.current();
int index = rand.nextInt(array.length);
return array[index];
}
}
Output 7.1.2
Dear Jane, I will cherish your unspeakably blinding echo forever! ♥ ♥ ♥
7.1.3 For-Each Loops
Many array-processing tasks require visiting every element of an array in turn. Up to this point, a loop control variable has been used as an array index for this purpose. For example:
int[] values = {3, 1, 4, 1, 5, 9};
int sum = 0;
for (int i = 0; i < values.length; i++) {
sum += values[i];
}
When an array is processed by reading each element in turn without modifying it, the same pattern
appears repeatedly: declare and initialize an index variable, test whether the end of the array has
been reached, and increment the index. A for-each loop (officially, an enhanced for loop)
eliminates this bookkeeping:
int[] values = {3, 1, 4, 1, 5, 9};
int sum = 0;
// sum the values in the array
for (int value : values) {
sum += value;
}
The loop header is read as: "For each element value in the array values, do the following."
The variable value is assigned each array element in turn. The programmer no longer needs to
manage an index variable or test whether the end of the array has been reached. This raises the
level of abstraction, allowing the programmer to focus on the application logic rather than the
mechanics of traversing the array.
A for-each loop should be used whenever the elements of an array must be read in order and the index of an element is not required.
7.1.4 Copying Arrays
In Section 5.1.3, we learned that BigInteger has an equals method to test for deep equality.
Arrays also have an equals method, but it tests for shallow equality. It would be straightforward
but tedious to write code for checking if two arrays have the same contents (deep equality).
However, there is no need: the Arrays class in the java.util package has over 200 static methods
for searching, sorting, comparing, copying, and other common operations on arrays. The following
code shows how two arrays of the same primitive element type can be checked for deep equality. The
code will also work for arrays of objects provided that their class has an equals method that
tests for deep equality.
if (Arrays.equals(a1, a2)) {
// do something
}
Arrays is an example of a utility class, a collection of static methods for common tasks in a
specific domain, such as array processing. A utility class cannot be instantiated. The Math
class is another example that we have encountered.
The following code is an erroneous attempt at creating a copy of an existing array. The result is two references to the same array, a shallow copy.
int[] fib = {2, 3, 5, 8, 13, 21, 34, 55, 89};
int[] fib2 = fib;
A deep copy copies the contents of an array, not just a reference to it. If you want to
clone an array (that is, deep copy it), you can declare a new array of the same size and use a
loop to copy each element of the original array into the corresponding position of the new one as
shown below. (Note that the assignment statement in the loop body is valid for primitive type
arrays; if we were dealing with arrays of objects, we would need to create a deep copy of each
fib[i] to be assigned to fib2[i], unless the element type is immutable, in which case shallow
copying of the individual objects would cause no problems.)
int[] fib2 = new int[fib.length];
for (int i = 0; i < fib.length; i++) {
fib2[i] = fib[i];
}
But this is reinventing the wheel because the Java platform provides many useful methods for working with arrays. The following examples illustrate some of them.
The toString method of the Arrays class returns a string representation of an array suitable for
display:
int[] a = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
System.out.println(Arrays.toString(a));
Output
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
The clone method creates a deep copy of an array of primitive values. (For arrays of objects, only
the array itself is copied; the individual objects are not cloned.)
int[] a2 = a.clone();
System.out.println(Arrays.toString(a2));
Output
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
The copyOf method creates a new array containing the first specified number of elements of an
existing array.
int[] a3 = Arrays.copyOf(a, 5);
System.out.println(Arrays.toString(a3));
Output
[0, 1, 2, 3, 4]
The copyOfRange method creates a new array containing a specified range of elements. The second
index is exclusive, as with the substring method of strings.
int[] a4 = Arrays.copyOfRange(a, 2, 7);
System.out.println(Arrays.toString(a4));
Output
[2, 3, 4, 5, 6]
The arraycopy method copies a specified number of elements from one existing array into another.
The parameters are the source array, the starting index in the source, the destination array, the
starting index in the destination, and the number of elements to copy.
int[] a5 = new int[10];
System.arraycopy(a, 3, a5, 3, 4);
System.out.println(Arrays.toString(a5));
Output
[0, 0, 0, 3, 4, 5, 6, 0, 0, 0]
Note that arraycopy does not follow Java's usual camel-case naming convention for method names. A
few methods in the standard library are historical exceptions.
7.1.5 Command-Line Arguments
The main method, the entry point to an application, has a parameter (args):
public static void main(String[] args) {
}
It is now clear that args is an array of strings. But where does this array come from, and how it
is used? You are probably running programs in an IDE, but it is possible to compile and run programs
in a terminal window provided by the operating system. The user could perform these tasks by typing
the following commands.
javac MyClass.java
java MyClass
The first line invokes the Java compiler (javac) and provides the name of the source file to be
compiled. Assuming there are no syntax errors, the compiler produces a class file (MyClass.class
in this example) containing the bytecode instructions to be interpreted by the JVM. The second line
invokes the JVM and specifies the compiled class whose main method is to be executed.
It is possible to supply string inputs to a program from the command line. To illustrate, suppose the user types the following text on the command line.
java MyClass R5-D4 IG-88
The Java runtime system creates an array of strings containing R5-D4 and IG-88, and passes it as
an argument to main. The strings can be accessed as shown below.
public static void main(String[] args) {
for (int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
}
Output
R5-D4
IG-88
There are numerous practical uses for command-line arguments. To give just one example, consider a program that processes a file. Instead of prompting the user for the name of the file, it might be convenient for the user to simply run the program from a terminal window and enter the file name right on the command line.