Developing, continued, and classes, objects, and methods
Welcome
Announcements
Are you a new add to the course? You need to get in touch with me immediately.
Informatics info session this coming Thursday from 6-7pm in room 140 in the CS building. The target audience for this is prospective and recent applicants to the major. We will cover the basic structure of the requirements and answer questions. It is fine if students arrive late or have to leave early.
CS Major Application info this Friday and the following Monday. Too soon for you but you might want to go anyway.
Friday, September 14
4:00 PM
Location: ELABII 119
Monday, September 24
5:30 PM
Location: ELABII 119
Back to TicTacToe
Note: corrected code (including the corrections described below) is on the schedule page. I am not going to do the corrections in class, to save time.
When we left off last class, we had the following code:
package tictactoe;
public class TicTacToeBoard {
public static final String X = "X";
public static final String O = "O";
public static final String EMPTY = " ";
public static final int BOARDSIZE = 3;
private String[][] board; // "X", "O", "EMPTY"
public TicTacToeBoard() {
board = new String[BOARDSIZE][BOARDSIZE];
for (String[] s : board) {
for (int i = 0; i < s.length; i++) {
s[i] = EMPTY;
}
}
}
public String toString() {
String output = "";
for (String[] rows: board) {
for (String space: rows) {
output = output + space + " | ";
}
output = output + "\n";
}
return output.substring(0, output.length() - 1);
}
public static void main(String[] args) {
}
}
and test:
package tictactoe;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class TicTacToeBoardTest {
@Test
void testNewBoard() {
TicTacToeBoard t = new TicTacToeBoard();
// assertEquals(expected, actual)
assertEquals(" | | | \n" +
" | | | \n" +
" | | | \n", t.toString())
}
}
And the problem here is (of course) I was rushing at the end of class. We want to use JUnit 4, not JUnit 5 – I got the wrong version by accepting Eclipse’s suggestions, something you should only do if you read and understand them. I skipped the “read” step. Oops.
Anyway, fix it by right-clicking on the project name and choosing to Configure Build Path. Remove JUnit 5 from the build path, add JUnit 4 (under Library -> Add Library), and add src/test to your build path if you need to.
You should also add public
modifiers to the class and test method (oops, again, rushed), and verify that the expected string is correct (remember, we trimmed the newline):
Once you do all this, the test will look like:
package tictactoe;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class TicTacToeBoardTest {
@Test
public void testNewBoard() {
TicTacToeBoard t = new TicTacToeBoard();
// assertEquals(expected, actual)
assertEquals(" | | | \n" +
" | | | \n" +
" | | | ", t.toString());
}
}
Ahh, much better. Run the test. One green checkbox!
This is a pretty common pattern, actually, where you write and test code in a driver class or method, then encode your test as a test case. This way, if you break something by accident later, you’ll discover it immediately before you move on.
Output legibility
It’s kinda hard to read the above. I’m going to change EMPTY
to be a single .
(and update our test appropriately). Usually this sort of thing is a good idea – anything to make your life easier when reading your code (and its output) will pay dividends in the future, either to you or the programmer who’s reading your code in six months.
Schools, especially intro classes, are very misleading about the code lifecycle. You spend most of your time in school writing your own code. But in the real world, you spend far more time reading, modifying, and fixing others’ code. And once in a while, when you’re reading and debugging some particularly gnarly code, you’ll come to an awful realization: The programmer who wrote this mess was you, six months ago. Don’t let this happen to you!
More on tests
Good tests are generally small – testing just one thing.
They usually use assertEquals()
, where the first argument is what’s expected and the second is what’s being tested. (They might use assertTrue
or assertFalse
when checking boolean results, and there are a few others, but we’ll come back to them later). It’s important to write tests this way, so that tools (like, for example Eclipse) show the right things to you in their UI.
In-class exercise
Is this a well-written test (x4)
Back to the driver
Let’s sketch out the driver so we know what we’ll need to do. How is a typical game played?
- The board is empty
- Players take turns until
- one player wins
- or it’s a draw (tie game)
- The game ends, the result is announced
What might this look like in code? Here’s a first cut.
public static void main(String[] args) {
TicTacToeBoard board = new TicTacToeBoard();
try (Scanner scanner = new Scanner(System.in)) {
while (!board.isGameOver()) {
System.out.println(board);
System.out.println("Player " + board.getCurrentPlayer() + ", choose a move (0-8):");
int move = scanner.nextInt();
board.move(move);
}
System.out.println("Game result: " + board.getResult());
}
}
Notice that we don’t worry about various error conditions (like if the player chooses an invalid move); we’ll get to that later. Also notice we haven’t yet defined any of these methods. Eclipse will sketch them out for us if we want (demo in class) though notice that we of course need to implement them.
Also notice that we’ve divided the methods into observers, which return a fact about the underlying object but don’t change the object’s state, and mutators, which change something about the object. It’s good practice to put methods into one or the other of these groups, to help us reason about our code.
Let’s leave isGameOver() alone for now, and work on getCurrentPlayer() and move().
Ways to implement an observer
getCurrentPlayer() returns the current player as a string. We can either explicitly keep track of whose turn it is (in an instance variable), or compute it by looking at the board (if there are an equal number of Xs and Os, it’s X’s turn; if there are more Xs, it’s O’s turn). Let’s keep track explicitly for now by adding an instance variable and initializing it in the constructor.
private String currentPlayer;
//...later, in the constructor
currentPlayer = X;
Another observer
Let’s make sure we return it in our getter:
public String getCurrentPlayer() {
return currentPlayer;
}
A mutator
And, at the end of every move, it should be the other player’s turn, so let’s encode that, too:
public void move(int move) {
if (currentPlayer == X) {
currentPlayer = O;
} else { // currentPlayer == O
currentPlayer = X;
}
}
Note that this method, which changes the object’s state, does not return a value (it’s void
). Often mutators are void, since you generally don’t want to mix mutation and observation.
Checking in the driver, it works, inasmuch as the players alternate.
Testing our mutator
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("O", 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 / 3][move % 3] = 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 String getWinner() {
if (board[0][0].equals(X) && board[0][1].equals(X) && board[0][2].equals(X)) {
return 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(String player) {
// check rows
for (int row = 0; row < 3; row++) {
if (board[row][0].equals(player) &&
board[row][1].equals(player) &&
board[row][2].equals(player)) {
return true;
}
}
// check columns
for (int col = 0; col < 3; col++) {
if (board[0][col].equals(player) &&
board[1][col].equals(player) &&
board[2][col].equals(player) ) {
return true;
}
}
// check diagonals
if (board[0][0].equals(player) &&
board[1][1].equals(player) &&
board[2][2].equals(player)) {
return true;
}
if (board[0][2].equals(player) &&
board[1][1].equals(player) &&
board[2][0].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 in a previous class, 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(X) || isWinner(O)) {
return false;
}
for (String[] row : board) {
for (String s : row) {
if (s.equals(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(X)) {
return "Player X wins!";
}
else if (isWinner(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. There’s more you could do (like make sure moves are in the right range, and not over an existing player’s move) but I’m sure you could figure it out (and test it!) yourself. 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 String
s. 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 String
s, 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).