The first few weeks of lectures, and the associated textbook readings, will also help you to understand the code and this guide.
Question: In which class do the cells get drawn?
There are all sorts of nice features built into Java, such as a fairly easy way to create windows, and very nice networking capabilities. Code for these is stored in code libraries. You may have a program that doesn't use these features; it seems wasteful to have that program carry around all the code for them. Java takes the view that you should only carry around what you need, and you specify this using import statements, which belong at the top of each class file. For example, in class ColorUtils, we announce that we want to use classes and methods from the Color package that Java provides.
Question: Where is the import statement that says this?
Life lifeRunner = new Life ();
main() creates an instance of the Life class, and calls it lifeRunner.
This instance now has its own copy of all of the variables declared
at the top of class Life -- theGrid, backBuffer, etc.
Such variables are called "instance variables".
Glance over the main() method and try to get a sense of what it's doing. Don't worry about any weird Java syntax. When main() calls other methods, such as readFileInfo(), look at those methods to see what they do. The comments and the method signature alone should tell you this (if the comments are well written) -- you shouldn't have to look inside the method to understand what it does. When a method does something to theGrid, such as
theGrid.nextGeneration();
Look at the Grid class to find out what that method does,
but don't worry at all about how it accomplishes it, or how in fact
it stores the information in a grid.
Look at the getNumGenerations() method. It's signature says that it sometimes "throws" some Exceptions:
public void getNumGenerations (StreamTokenizer dStream)
throws EOFException, IOException {
(Actually, getNumGenerations() doesn't throw them, but one or more
of the methods that it calls does.)
An exception is sort of like an error.
For example, when reading from a file, an "end of file" exception
(called an EOFException) is thrown when the end of the file
is reached.
Later on, in method readFileInfo(), there is a try { ... } catch { ... } statement. What happens is the following:
The code inside the try { ... } block is executed. If anything in there throws an exception, it may be "caught" by an appropriate catch { ... } blocks, whose code then handles the error.
The same sort of thing can be done by having methods return special values to indicate a problem, and then using if-statements to check for these errors. So why bother using exceptions? First, there may be lots of possible problems occurring inside a try { ... } block, and the code would get ugly if we had to have an if-statement for every one. Second, it forces us to actually deal with the error. We could omit an if-statement and never notice that a problem occurred; but we cannot leave an exception uncaught. When reading from files, for example, Java will not compile our code unless we handle IOExceptions.
Getting back to getNumGenerations(), if something in that method causes an exception (like you delete the file while your program is reading from it), an exception is thrown. By declaring getNumGenerations() the way we did, without any try-catch pair, no exceptions can be caught in the method itself. Any exceptions that do occur are passed up to the method that called getNumGenerations(). In this case, it is readFileInfo().
Question: Find the instance variables that every cell has. What information do they store?
Notice that class Cell's evolve() method is declared to be abstract. This means that it's body is not defined in class Cell, which makes a lot of sense, since a cell can't know how it's supposed to evolve unless it knows which kind of cell it is -- the rules are different for live and empty cells. Class Cell has two subclasses: LiveCell and EmptyCell. They each define what their own evolve method should do.
Class Cell is itself declared abstract. This means that its behaviour is not fully defined (as we just saw), and so it is illegal to create an instance of Cell.
Cell x = new Cell(); // Illegal.
If we want to create an actual instance, we have to either create
a LiveCell or an EmptyCell.
Question: In class Cell, we have the line protected static int size = 20; What does "protected" do?
Question: Why is Cell.size static?
As we said above, LiveCell is a subclass of Cell. This is what the "extends" below means.
public class LiveCell extends Cell {
(And the same for EmptyCell).
Question: What exactly do LiveCell and EmptyCell inherit from Cell?
EmptyCell and LiveCell each define their own evolve() methods. Take a look at these and see how they implement the rules in the game of life.
LiveCells have an instance variable "age". Notice how it is used: it can be accessed by any LiveCell method. Also, each variable of type LiveCell has its own copy of age, so when we do
LiveCell c1 = new LiveCell();
LiveCell c2 = new LiveCell();
c1.evolve(3);
c2's age is still 0.
The evolve() method has a bit of weird syntax in it. First, there's the odd stuff with '?' and ':'.
return numNeighbours == 3
? (Cell) new LiveCell ()
: (Cell) this;
An equivalent if statement would be
if (numNeighbours == 3) {
return (Cell) new LiveCell();
} else {
return (Cell) this;
}
You can use this ?: syntax directly in an assignment statement, e.g.,
String s = new String("Eeyore");
int i = s.length > 4
? 3
: s.length;
This would store in i the value 3 if s had more than 4 characters, and
otherwise would store in i the length of s. You can use this sort of
expression anywhere you can use a value.
The next weird syntax is the
(Cell)
business. That's called a type cast. It says
"pretend the thing that follows me is a Cell." We need to do this
because the method evolve() is supposed to return something of type
Cell, but we really want to return a subclass of Cell. If we didn't
have this, we'd get an error.
The last weird thing in evolve() is the word 'this'. 'this' refers to the object in which the method is running. Here, 'this' is an EmptyCell.
Notice that when an EmptyCell evolves, we don't need to create and return a new one because the current EmptyCell will do perfectly well.
Question: Does the same sort of thing happen in class LiveCell?
In class EmptyCell there is a method called EmptyCell(). This is a particular type of method called a "constructor"; a constructor must have the same name as the class in which it is defined. The constructor is called automatically every time a new EmptyCell is created. So, for example, the line
EmptyCell e = new EmptyCell();
would create a new EmptyCell, store a reference to it in e, and call
the EmptyCell() constructor on that new object.
LiveCell has a constructor too.
LiveCell c = new LiveCell();
c.evolve(2);
We've called c's own evolve(), and so c is the Cell that is going to
evolve.
The comments say things like "if ... then keep myself alive ...".
In this case, the "myself" is cell c.
This style of commenting is unique to object-oriented programming.
This class just provides some methods for darkening and brightening a given colour.
Question: LiveCell.evolve() calls ColorUtils.darker(), but we never instantiate (declare a variable of type) ColorUtils. How is this possible?
Now we finally get to see how a grid is stored.
Question: Take a look at the instance variables. What data structure is used to store a grid?
Question: Class Grid uses the methods LiveCell.showText() and EmptyCell.showText(), but neither of those classes declares such a method. Where does it come from?
Question: Inside the Grid class, how do you tell how big the array theGrid is? That is, how many rows and columns it has?
From outside this class, you should never ask about the dimensions of the array, since you don't care what sort of data structures are secretly used inside. You may, however, want to ask for the dimensions of your Grid.
Question: What's the difference between these questions? Why should we care about this difference?
Your task in this assignment is to fill in the nextGeneration() method in class Grid. The following material will help you with that (although you might not realize it immediately).
A "circular array" is an array in which we act as if the next spot after the last spot is the first spot -- as if it went around in a circle with no edge there.
Question: Let's say you have a circular array of n integers, and an index i into the array. Using % (modulo), write a statement that will find the next location. You are not allowed to use an if-statement. (Remember, arrays start at 0, so the indices are 0 .. n-1.)
Question: Now write a statement that will give you the previous location. Be careful; taking the mod of a negative number gives you a negative number.