Developing, continued, and classes, objects, and methods

Welcome

Announcements

CS Major Application info this Friday and the following Monday. Too soon for you but you might want to go anyway.

Error in Josh’s office hours. Office hours: Wed 1–3pm; Thu 1:15–3:15pm, LGRT 220 (tower, not lowrise!)

Back to Eclipse

We left off last class having started to implement the move() method. Let’s pick up there. We showed in the console that the player’s turn will swap back and forth after each move(). Let’s start by adding a test that captures this behavior:

    @Test
    public void testPlayerChangeAfterMove() {
        TicTacToeBoard board = new TicTacToeBoard();
        assertEquals("X", board.getCurrentPlayer());
        board.move(0);
        assertEquals("0", board.getCurrentPlayer());
    }

And while we’re at it, why not a bunch of moves?

    @Test
    public void testPlayerChangeAfterMultipleMove() {
        TicTacToeBoard board = new TicTacToeBoard();
        assertEquals("X", board.getCurrentPlayer());
        board.move(0);
        assertEquals("O", board.getCurrentPlayer());
        board.move(1);
        assertEquals("X", board.getCurrentPlayer());
        board.move(2);
        assertEquals("O", board.getCurrentPlayer());
        board.move(3);
        assertEquals("X", board.getCurrentPlayer());
        board.move(4);
        assertEquals("O", board.getCurrentPlayer());
        board.move(5);
        assertEquals("X", board.getCurrentPlayer());
    }

OK, and since we’re getting crazy, how about we get move() working now, too? It’s only a one-liner:

        board[move] = Space.X;

And test it in the console. Huh, there’s a problem. Now, I get it, most of you see the problem immediately. But that’s kinda on purpose here; some problems are easier than others to spot (and then fix). Sometimes they’re simple problems you just can’t see yourself; sometimes they’re more subtle. Instead of caveman debugging this one (you know, adding “printlns” everywhere, let’s try the debugger) so we can see what’s going on.

First, we must set a breakpoint! Double-click the “gutter” and create a small blue dot.

Then run under the debugger.

Note that we can escape using the “perspective” buttons in the upper-right.

Values are displayed in the upper-right; everything in scope is here.

We can “step” forward line-by-line. “over” means stay in this method; “into” means follow any method call in that line into another method; “out” means return to the calling method.

The second time through, we see that the wrong value is assigned to the board entry – whoops, we hardcoded it! Let’s fix that:

        board[move] = currentPlayer;

And test it in the console with our driver.

And maybe some tests now too:

In-class exercise

    @Test
    public void testOneMove() {
        TicTacToeBoard board = new TicTacToeBoard();
        board.move(0);
        assertEquals("X . .\n. . .\n. . .", board.toString());
    }

    @Test
    public void testMultipleMoves() {
        TicTacToeBoard board = new TicTacToeBoard();
        board.move(0);
        assertEquals("X . .\n. . .\n. . .", board.toString());
        board.move(1);
        assertEquals("X O .\n. . .\n. . .", board.toString());
        board.move(2);
        assertEquals("X O X\n. . .\n. . .", board.toString());
        // and so on...
    }

Again, (one of) the reason(s) we encode this stuff into tests is so that if we break something later, we’ll spot it immediately and have a working test case to debug with.

How about checking for the end of the game? In English, in isGameOver() we check to see if either player has won or if it’s a draw. If any of those are true, the game is over, otherwise it’s a draw. Similarly, our getResult() method needs to determine which player won, or if it’s a draw.

Stopping and thinking for a second: we can express our draw check in terms of our winner check. If neither player has won, and the board is full, then it’s a draw.

What does this look like?

    public boolean isGameOver() {
        return (isWinner(Space.X) || isWinner(Space.O) || isDraw()); 
    }

How do we check if a player has won? If they have three in a row. We could hardcode this, for example:

    private Space getWinner() {
        if (board[0].equals(Space.X) && board[1].equals(Space.X) && board[2].equals(Space.X)) {
            return Space.X;
        }
        else if //...
    }

but we’d have to do 2 * (3 + 3 + 2) if clauses, mostly copy-pasted, making sure we didn’t screw up the board index each time. That’s OK, I guess, but it fails to capture the patterns we’re looking for: rows, columns, diagonals. Maybe something like this would be better:

    private boolean isWinner(Space player) {
        // check rows
        for (int row = 0; row < 3; row++) {
            if (board[row * 3].equals(player) && 
                    board[row * 3 + 1].equals(player) &&
                    board[row * 3 + 2].equals(player)) {
                return true;
            }
        }

        // check columns
        for (int col = 0; col < 3; col++) {
            if (board[col].equals(player) && 
                    board[col + 3].equals(player) && 
                    board[col + 6].equals(player)) {
                return true;
            }
        }

        // check diagonals
        if (board[0].equals(player) &&
                board[4].equals(player) &&
                board[8].equals(player)) {
            return true;
        }

        if (board[2].equals(player) &&
                board[4].equals(player) &&
                board[6].equals(player)) {
            return true;
        }
        return false;
    }

Two notes: First, we could further condense the row/column checks with another inner for loop (though it’s not much cleaner); second, notice this is a version of the for loop pattern we talked about last week, where we check a bunch of stuff: if it ever becomes true, then the method returns true, and if we make it to the end, the method returns false.

How about a draw? If either player has won, it’s not a draw. Or, if there are still empty spaces, it’s not a draw. Otherwise, it’s a draw:

    private boolean isDraw() {
        if (isWinner(Space.X) || isWinner(Space.O)) {
            return false;
        }

        for (Space s: board) {
            if (s.equals(Space.EMPTY)) {
                return false;
            }
        }
        return true;
    }

Notice I can freely call these “observer” methods (isDraw, isWinner, etc.): they only observe values, but they don’t change anything. Having methods like this means I don’t have to worry about “breaking” an object. A common beginner style error is to have a method that both answers a question about an object and changes its state (Imagine a method that, say, both makes a move and returns the current winner, if any. How do you then check winner without making a move, too? You can’t, unless you add another method. But then there are two methods that check winner, which means you need to keep them in sync, which means it’s easier to get later things wrong…).

Now we can also pretty easily return the game result, too:

    public String getResult() {
        if (isWinner(Space.X)) {
            return "Player X wins!";            
        }
        else if (isWinner(Space.O)) {
            return "Player O wins!";
        }
        else if (isDraw()) {
            return "It's a draw.";
        }
        else {
            return "Game ain't over yet.";
        }
    }

It appears to work (demo in console). One possible test might be:

    @Test
    public void testDiagonalXWinner() {
        TicTacToeBoard board = new TicTacToeBoard();
        board.move(0);
        board.move(1);
        board.move(4);
        board.move(3);
        board.move(8);
        assertEquals("Player X wins!", board.getResult());
    }

OK, enough tic-tac-toe. Let’s go back to our Java review.

Methods

In Java, methods are always attached to a class. A method consists of a declaration and a body, which is just a sequence of statements. Let’s look at a declaration more closely:

public static void main(String[] args) {
  ...
}

WTF is going on here!?!?! is usually the reaction we get in 121 when people first start learning Java. But now, you probably know enough to understand most of it. Let’s tackle it inside-out.

String[] args is the parameter to this method. One or more parameters are passed in, and they look like (and in many respects behave as) variable declarations. The difference is that their values are provided by the calling method (or in the very special case of main, by the JVM). Here, main gets an array of Strings, which are exactly what is passed on the command line to the java interpreter, e.g.,

public class PrintArgs {
  public static void main(String[] args) {
    int i = 0;
    for (String a : args) {
      System.out.println(i + ": " + a);
      i += 1;
    }
  }
}
> javac PrintArgs.java
> java PrintArgs Hello Marc!
0: Hello
1: Marc!

Next is the name of the method, which is by convention “camelCased,” starting with a lowercase letter. Next is the method’s return type, or void if it does not return anything.

Important note: methods that return void typically do something, like print a value, or delete an item from a list, or the like. They affect state, or are stateful. Sometimes but not always, methods that return a value don’t do something (they are more like mathematical functions). The only way to be sure is to read the documentation (or the method code itself)! But a method’s public API should describe what it does.

Next comes one or more method modifiers: either abstract or one or more of static, final, and synchronized. static methods are associated with a class, but not a particular instance of that class (in other words, not with an object). We’ll talk more about the other modifiers later as they come up.

Finally there is at most one of public, protected, or private, which are member access modifiers. Which objects can invoke this method is determined by this modifier. public and private are probably familiar to you (any object and only objects of this class, respectively). protected and no modifier (“default access” or “package access”) have special meanings we’ll skip for now.

Classes and objects

Java is said to be “object-oriented”, which means that the language strongly encourages the use of objects as a paradigm for problem solving.

What’s an object? A collection of related state and behavior. In Java, this means objects are typically variables and associated methods. The “blueprint” for objects is a class. Classes are also the place where we stick stuff that’s not necessarily part of an object. (This falls out of Java’s design: everything is part of a class.)

Classes in the JVM

(Note that like our discussion of the call stack, this is a simplified version to give you a mental model of how things work; the implementation in the JVM differs somewhat.)

A single copy of the class (note: the class! not an object of class!) lives in memory, annotated with its name, each method, and space for each static variable. There also some other stuff: a pointer to the class’s superclass, and a list of interfaces it implements, and the like.

Objects that are instances of the class will be able to refer to these methods and variables here.

Instantiating objects

Objects are defined by a class. But an object does not exist until you instantiate it, that is, “make a new one” using the class as a template for the new object. For example:

Bus bus44 = new Bus();

There’s a class called Bus, and we’ve instantiated an object of type Bus. The object is named by the variable bus44.

Thinking back to our first class, what’s going on in memory when this happens?

Unlike primitive types, which are stored directly in the variable’s allocated space, the variable “holding” an object isn’t really the object’s value. It’s a pointer or reference to the place where the object really lives.

This is really important to understand, so I’ll say it again: what Java stores in primitive type variables and object variables is different, conceptually: The former stores the value itself, the latter stores as its value the memory address of the object. Weird but true!

Let’s do an example. What happens here?

String message = new String("Hi!");
String wassup = message;

First a new String is allocated. Then it’s initialized on the heap. Then a new variable of type String, named message, is created. Its value is set to the address of the actual String object we created.

Note that this String object implicitly refers to a String class somewhere else. When you call methods on an object, it uses this reference to find the method – only one copy of the code for a method exists at a time – all objects of a class share it.

Next, another new variable, again of type String, named wassup is created. The address of message is looked up, and is then assigned to wassup. Both variables now “point to” or “refer to” the same object, which is a String object containing the data "Hi!".

Having two variables refer to the same object is called “aliasing”; it is a common source of program bugs. Always think carefully about the value stored in a variable, not just the name of the variable.

So this leads to an important thing: == vs .equals(). With primitive types (int and so on), you only have one option: ==. What does it do? It looks up the values stored in the variables, and returns true if and only if they are the same. x = 3; y = 3; x==y;

But what happens when we use == on variables that refer to objects? Exactly the same thing! Which might or might not be what we mean. Following from above, let’s add String hello = new String("Hi!");, and ask, does message == wassup? Yes, because they refer to the same object.

Does message == hello? No. Even though the two String objects represent the same value (“Hi!”), they are stored in separate objects at different addresses. So the variables store the value of two different addresses.

But oftentimes we don’t actually want to know if two variables point to the same objects. Instead, we want to know if the two objects’ values are equivalent. In the case of Strings. this means that they hold the same text; you can imagine that for more complicated objects, we might need a more complicated comparison of the two objects’ instance variables. There is a method .equals() that a class can implement to provide a class-specific test of equality. (If you don’t implement it, the Object class’s equals() method, which defaults to ==, is used.)

So we can write message.equals(hello) to check if the two objects store equivalent Strings, rather than the two variables storing the same address as a value.

In-class exercise

Suppose we define a Bus class. Note we can use Eclipse to write a semantically meaningful equals method for us (for now – later in the semester we’ll see what all this means and how to do it ourselves, then go back to letting Eclipse write the boilerplate).

public class Bus {
    private int number;

    public Bus(int n) {
        number = n;
    }

    public static void main(String[] args) {
        Bus busA = new Bus(44);
        Bus busB = new Bus(44);

        Bus busC = busA;

        System.out.println(busA == busB);
        System.out.println(busA.equals(busB));

        busC.setNumber(13);
        System.out.println(busA == busC);
        System.out.println(busA.equals(busC));

        busC = new Bus(13);
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + number;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Bus other = (Bus) obj;
        if (number != other.number)
            return false;
        return true;
    }
}

What are the four lines of output (true/false)? (On board: F/T/T/T, and why.)

Using objects and methods

If the flow of control is executing within an object, and we access a variable, say x, what happens?

First the local scope is checked, inside out, for example:

class Bar {
  int x = 0;

  private void foo(int j) {
    for (int i = 0; i<x.length; i++) {
      x[i] = j;
    }
  }
}

x is not declared in the for loop, nor in the method body, nor as a parameter, so the next place that’s checked is the object itself, where it’s found. (The superclasses, etc.) This all happens at compile-time; you can’t fail this lookup in a source that correctly compiles.

Relatedly, when methods are called on an object, the type of the object is examined, and the method is looked up in the corresponding class. If it’s not found, the class’s superclass is checked, and so on. Again, this is checked at compile-time. This is how the default “equals” method works; it’s implemented in Object, which is by default the superclass of all classes that don’t otherwise declare a superclass. In other words, it “inherits” the method from its superclass.

We’ll do (slightly) more on inheritance, but best practices over the years have moved toward a relatively flat class hierarchy. Usually you won’t see (or use) deep inheritance, with some exceptions for older codebases and large libraries (like, say, the Java Platform API).