Learn Programming in Java


<<Previous | ToC | Next >>

Lesson #5: Arrays

An important use of iteration is to step through arrays of data, one item at a time. An array is any data type, repeated (different values) some specified number of times. Except for "final" arrays of constants, Java arrays are all dynamic, which is sort of like an object, only different. We will get into objects later, but objects and arrays are both created using the keyword "new":
int[] intary; // declared, but not yet exists
int[] myary = new int[99]; // declared and created but (probably) undefined
final int[] nums = {3,1,4,1,5,9,2,6,5,3,5}; // constant array with 11 predefined elements
intary = new int[1000]; // now allocated memory for 1000 numbers
for (int n=0; n<1000; n++) intary[n] = 0; // now it has known values
for (int i=0; i<99; i++) myary[i] = nums[i%11]; // filled with 9 copies of nums


Underneath the covers a String is also an array, but (as we have already seen) it has special uses, and mostly we do not want to step through the characters one at a time. When we do, there is an accessor method to do that, but it only lets us look; for efficiency, you are not allowed to change the characters in a String, you can only make a new String as changed. But you can make arrays of String, and even arrays of arrays.

Like C (which Java mostly copied), all Java arrays start at element number ("index") zero, and the last element in any array has an index one less than the number (size) you used to create it. It is an error if you try to access the array with an index less than zero or greater than the highest actual element index. Java checks every array access: the array must exist, and the specified index must be within the bounds. This much you need to know, because if there's a problem, your program will get an exception ("crash").

If you plan to write computer program professionally, you might also want to know this (optional) information:

Better languages than C do not allow uncaught array access errors, but one of the problems with C is that the compiler/runtime has no way to know if you are staying within the bounds of your array. The result is a whole bunch of "security errors" that software vendors are constantly patching.

Checking every array access costs something at runtime, but not much more than a well-written C program that does its own checking. It's lots faster than crashing or (as in C) destroying other data or letting Bad Guys steal secrets. My compiler notices if you did your own checking, and omits its own if so, but I don't think standard Java does that. So if you look at my example code, or especially the runtime code for my Game Engine, you will see all these checks for null (array not yet allocated) and index bounds. It's a good habit, and as compilers get smarter, they will remove their own (now superfluous) checks and your code will run faster and safer. Right now, Java is just safer (which is not a bad thing). In the examples above, null checks are not needed, because the allocation obviously precedes the access. Range checks are not needed because the for-loop keeps the index within bounds, except for the access to "nums[i%11]" where the modulus operator ensures the computed index cannot exceed the array bounds. Compilers can recognize this, and mine does.

Most programming languages require you to specify all the dimensions when you declare a multidimensional array. Two-dimensional Java arrays are defined to be arrays of arrays, and Java arrays are given a dimension (the number of elements) when memory is allocated, which could happen several times for the same array in the course of a program, and even for different sizes, so there's no requirement for the second dimension to be uniform, nor even for all the elements to exist. This imposes a small performance penalty, about the same as accessing the first dimension of an array, but larger than in languages where multidimensionality is fixed at declaration time. This is not a problem, but you need to be aware of the differences when you move to another programming language. Me, I'm always pushing at the performance limits of the computer, so I look for opportunities to make my program run faster.


Later on we will be programming a Tic-Tac-Toe game. We can define the game board as TTTboard[3][3], or we can linearize it to TTTboard[9] and separate the rows and columns in software, which is somewhat faster -- but not enough to notice. Programmers get to make these kinds of choices and trade-offs. But Tic-Tac-Toe is pretty challenging. Lets start with an easier game.
 

Hangman

Hangman is a game that uses iteration and arrays and other fun stuff to program it. You might have played Hangman when you were younger. We will write a program that plays scorekeeper for two humans playing hangman. Maybe later on you can improve it to play one side, but that requires access to a dictionary and stuff I don't want to get into at this time.

Vivian Killian provided the basic structure of this program. It's always a good idea to set this out in English (or whatever language you think in) so that you know what you are aiming for:

1. Pick a word
2. Draw dashes (computer displays this)
3. Guess a letter (loop!)

If the letter is wrong, draw a body part
If the letter is correct, replace the dashes with the letter


The game play requires nested loops (iteration). For each turn, the program needs to accept a letter guess, and then redraw the new state of the game. Drawing the state of the game usually means putting the gallows up with some part of the body; we can do this with "ASCII graphics" which is typing out on the console letters and/or special characters, so the result looks (more or less) like a picture. We programmers did this for years, long before we had graphics displays. It's a little tricky, so at first well play the game by counting the mistakes (but not drawing anything). And then the program types out (in another loop) dashes for the unguessed letters and substituting the actual letters that have been guessed correctly. A third (inner) loop compares each guessed letter to every letter in the word. And if you want the game to restart after each game ends, that would be another (outer) loop. We can do all of this inside the "StartHere" main() program we have been modifying.

We need to declare the data this game will be using. We could do some of this as String variables, but this is about arrays, so let's use arrays of char (character) for both the word being guessed and the displayed version, and also for the letters that have already been guessed.

Later we can build a constant array of String for the ASCII graphics of the gallows.

For now we need three character arrays, one for the word as given, one for the word as guessed so far, and one for the alphabet, the letters that have already been guessed. Here is a declaration for the alphabet array:

char[] alfa = new char[26];
You are writing this program, you get to declare two more character arrays for the two words. Recall that "declare" is the part of the line above that specifies the type (in that line, "char[]") and name ("alfa") of the variable; the rest of the line "defines" (in this case, allocates memory for) it, which you would do if the array size never changes. I think I would be lazy and pick a fixed size -- say 32 -- and refuse to take any words longer than that. With a little more effort, you can look at the word you are given (which we don't know until the user types it in) then allocate as much space as you need for the whole word at that time.

Decide which you want to do, then declare your two variables. Or do it the easy way now, then upgrade it after everything else works.

We will also need some working variables, an integer index to step through the arrays, a number to remember the length of the actual word, another number to count the number of wrong guesses = the number of body parts of the hanged man to draw as the game progresses, a character variable for the user guess and for other transitory uses, and a String variable to read the initial word into. We may think of other variables we need as we go along, but you might as well declare what you know about.

For starters, we'll just play the game once, one word, then quit. Later you can wrap a while-loop around the game play. This now is the part of the design we are working on:

1. Pick a word
My fake System-like class Zystem has three user-input methods. In previous programs you have used ReadLetter() to get a single user-typed letter, and ReadInt() to get a user-typed number. Today we'll use the third, ReadWord() to get a whole user-typed word. The first thing you need to do is print out a prompt to ask the user to type in a word, which the second player will try to guess. You know how to do that, do it.

Then in a line by itself, assign to the String variable you declared, the result of calling Zystem.ReadWord(). If you are unsure of what this will look like, go back and look at how you called the other two methods (in previous programs, or else in previous pages where I told you how to do it). This is your program, you get to write it. I'm only the coach.

In Java, the String type is an object, not an honest string data type, so when you compare two strings, you don't get a letter-by-letter comparison like you would want and expect, it only compares their object references (for equality, without looking inside at the actual text), which is not useful at all. The Java String class has a letter-by-letter string compare method (compareTo) but because it's an object method, it can blow up ("throw an exception") instead of giving you a reasonable answer. My class Zystem has what I call a "SafeCompare" that takes care of all the edge cases and gives you a credible answer regardless, basically what you'd expect from a true string type.

When you call Zystem.ReadWord(),  it returns a word, that is a string of characters with no spaces or other "whitespace" in it. It might return an empty string if Something Bad Happened, so you should check for that possibility before you use the standard Java length() method to get the string length:

if (Zystem.SafeCompare(UserTyped,"") <= 0) WordLen = 0;
  else WordLen = UserTyped.length();
if ((WordLen <= 1)||(WordLen>32)) {
  // not a useful Hangman word, assume the user wants to quit, say so..
  return;}
You would replace "UserTyped" in these lines with the name of your String variable, and "WordLen" with whatever you named your variable, and then replace my comment with your code to tell the user you are quitting. If you have a fixed size, you can put it where I have 32; otherwise omit that test and add another couple lines allocating your two arrays to be that size.

Next we need to extract the characters from this word and verify that they are all (lower-case, not capital) letters, and insert them into the character array you created for them, and also fill that many characters in the other array with hyphens or underscore characters.

for (nx=0; nx<WordLen; nx++) {
  xch = UserTyped.charAt(nx);
  if ((xch < 'a')||(xch > 'z')) {
    // tell the user and quit..
    return;}
  theWord[nx] = xch;
  theGuess[nx] = '_';
  } // end of this for-loop
My favorite name for a throw-away index variable is nx (the first and last consonants of the word "index"), and for a temporary character variable is xch, you should replace them with whatever names you already declared, and then also substitute your names for theWord and theGuess arrays.

Somewhere along the way, as part of this initialization process, we need to fill the alfa array with blanks (non-letters) and set the number of wrong guesses to zero:

for (nx=0; nx<26; nx++) alfa[nx] = ' ';
nerrs = 0;


OK, now we are ready to do

2. Draw dashes (computer displays this)
This is actually the beginning of the loop that does one play, so we have nested loops:
while (nerrs <= 8) {
  // draw the (initially empty) gallows here, or else..
  System.out.println("So far " + nerrs + " wrong guesses");
  if (nerrs >= 8) break; // there are 8 body parts, the guy is dead
  for (nx=0; nx<WordLen; nx++) System.out.print(" " + theGuess[nx]);


Now we do this part

3. Guess a letter
It's always a good idea to tell the user when they are expected to do something. I would put this inside a loop in case the user types something nonsensical, we can ask for it again:
while (true) {
  System.out.print("Guess a letter: ");
  xch = Zystem.ReadLetter();
  if (xch >= 'a') if (xch <= 'z') break;
  } // end of while-loop, didn't see a letter, so ask again
Now we know we have a letter, has it already been guessed? For this we need to search the alphabet array, but there's a faster way than just stepping through the array. First we convert the character to a number by casting it, then we adjust its range down to the size of the array:
nx = ((int)xch)-((int)'a');
if (nx >= 0) if (nx<26) { // we already know it is
  if (alfa[nx] == xch) { // already been played..
    // tell the user, then
    continue;} // go back for another guess
  alfa[nx] = xch; // now we know it's been played
  } // done with validation
If you were going to do it by searching, it might look like this:
for (nx=0; nx<26; nx++) {
  if (alfa[nx] == ' ') { // didn't see it..
    alfa[nx] = xch; // now we know it's been played
    break;} // exit the loop
  else if (alfa[nx] == xch) { // already played it
    xch = ' '; // so we know it's been played after we exit
    break;} // exit the loop
  } // end of the for loop (still looking)
if (xch == ' ') { // already been played..
  // tell the user, then
  continue;} // go back for another guess
Do you know why we had to wait until after we got out of the for-loop before telling the user that this letter had already been played and continuing back to ask for another letter? Telling the user is OK, but if you said continue inside the for-loop, it would have just gone back to the top of the for-loop, because continue (like break) always applies only to the closest enclosing loop.

Do you think you can do the next part? That would be finding all the places where the user-guessed letter is in theWord and setting the same index in theGuess to the same letter, or if there aren't any, increment the bad guess count. You need another variable, either a boolean initially false, which you set to true each time you find a matching letter in theWord, or else an integer initially zero, which you increment each time you find it. Then after scanning the whole word (there might be more than one of the same letter, you should get them all), if the boolean is true or the integer is non-zero, you know to increment the count. Can you do that?

Your for-index needs to step from 0 up to WordLen-1 (you can use "nx<WordLen" in the for-loop specification) but theGuess is the same size, so everything happens in the same loop.

When you finish that, close off the while-loop, so it can go back for the next guess.

After you fix all your typing errors, the Java compiler may tell you that there are unhandled exceptions possible because of the string operations. Of course you and I know that it can't happen, but the compiler does not. The cure (for now) is to wrap a try/catch around the whole program. Before the first line that Java complains about (hopefully after the variable declarations, but OK before) you insert a try line, then at the bottom, before the final brace of the main(), you insert a default catch, like this:

public static void main() {
  // declarations here
  try {
    // the rest of your program here..
  } catch (Exception ex) {}
} // end of main
This does nothing more than hop out if an exception happens, but it shouldn't happen. If it does, the console log will tell you what went wrong, and you can deal with it.

Next: ASCII Graphics

<<Previous | ToC | Next >>

Revised: 2020 November 21