1. Introduction to Java
2. Reference Types and Semantics
3. Method Specifications and Testing
4. Loop Invariants
5. Analyzing Complexity
6. Recursion
7. Sorting Algorithms
8. Classes and Encapsulation
8. Classes and Encapsulation

8. Classes and Encapsulation

Types play a central role in Java, as we have seen. They represent the state of our program, and they determine how different components can behave and interact. While Java’s libraries provide us with many built-in (reference) types, we often will want to create our own bespoke types to fit the requirements of our program. In this lecture, we’ll discuss how to write classes for user-defined types and how we can extend our memory diagrams to represent objects from the implementer’s perspective. We’ll also extend our discussion of invariants and specifications to classes and their members. Finally, we’ll introduce other properties of variables (scope, lifetime, and visibility) that are enforced by the compiler and see how to leverage these to adhere to the object-oriented programming design principle of encapsulation.

Defining New Types

As we have discussed, a type associates a set of possible states and behaviors to a name. For example, variables with the primitive int type have state that represents integers within a certain range, and we can perform calculations with ints using built-in Java operations such as addition and multiplication. Objects with the String reference type store as their state sequences of characters, and we can interact with these objects through their instance methods like length(), charAt(), and substring().

If we want to define a new reference type, we can do this by declaring a class with the same name as the type. As an initial example, suppose that we’d like to create a Counter type that will model a handheld counter similar to the one pictured below.

a handheld counter device with an increment button, a 4-digit display, and a reset wheel

We start by creating a new file “Counter.java” in which we will declare the Counter class. We mark the class as public so that it can be accessed from other files (e.g., the client’s code file that will use Counter objects).

Counter.java

1
2
3
public class Counter {

}
1
2
3
public class Counter {

}

Next, we should think about the state and behaviors of a Counter object.

Representing State

An object’s state models its current configuration and is represented through variables called fields.

Definition: Field

A field (also called an instance variable) is a variable that is associated to an object that stores part of its state.

Fields can be either primitive types or reference types. To determine appropriate fields for a class, we should think about what information is necessary to model the state of one of its objects. In the case of our Counter object, we should store the numbers that it currently displays. There are different ways we can choose to represent this information. We could store it as a String of digits, or as a char[] of length 4. We will opt to represent the state using an int field that we will call count.

Remark:

We pause to note a subtle distinction from the preceding paragraph between state and state representation. A type is an abstract notion which is characterized as having a state. In our example, the abstract notion of a counter is as a type that has an intrinsic "count". We model a type by defining a class, which requires us to settle on a particular state representation for its fields. There are likely many different choices of state representation that all model the same state.

We declare fields directly within the class, outside of any of its methods. Fields are declared using the same syntax as all the other variable declarations we’ve seen. However, we typically do not initialize their values as part of their declarations; we leave initialization to the class’ constructor, as we’ll soon see. When we declare fields, we always include a documentation comment that describes their specifications. This includes their interpretation in the class (i.e., the part of the state that they model) as well as any constraints on their values. This documentation helps to give a clear picture of how the class was designed, which will be crucial for future maintainers. In the case of the count field, it models the number displayed on the counter, which is limited to 4 digits. This bounds the range of possible values for the field.

Counter.java

1
2
3
4
public class Counter {
  /** The value displayed on the counter. Must have `0 <= count <= 9999. */
  int count;
}
1
2
3
4
public class Counter {
  /** The value displayed on the counter. Must have `0 <= count <= 9999. */
  int count;
}

Sometimes, the specification will also describe relationships that must be satisfied between multiple fields. Together, all the field specifications make up the class invariant.

Definition: Class Invariant

The class invariant describes a collection of properties of an object's fields that must be true at the start and end of any non-private method invoked on that object.

We will think more about the second half of this definition shortly, once we have introduced the syntax for defining instance methods. First, let’s consider how to model fields within our memory diagrams (now often called object diagrams since they will visualize the state representation of the objects in our program). We do this by adding variable boxes within the rounded rectangle representing the object. You can interpret this as depicting that each instantiation of a class (i.e., each object) has a variable for each of that class’ fields whose values represent that particular object’s state. A diagram for a Counter object (which would live on the memory heap) is shown below.

Defining Behaviors

Now that we have introduced fields as a way to model a type’s state, we need a way to model its behaviors. Behaviors describe ways that a client can interact with a type. There are multiple different types of behaviors. Some behaviors simply access (or look at) part of the object’s state. Other behaviors mutate (or modify) an object’s state in some way. Still other behaviors may be called for some side-effect, an action that does not modify an object’s state or return a value to the client (such as printing). A behavior may even do multiple of these in combination.

Let’s think about what behaviors our Counter object should have. How does a person interact with one of these handheld counters? First, they might want to look at number that it currently displays, so our Counter object should have a way to access the current value of count. Next, they can press the silver button, which increments the count by 1, we’ll need a way to increment the count field. Lastly, they can spin the black dial to reset the display to “0000”. We’ll need a way to reset count to 0.

Behaviors are modeled with instance methods. These are methods defined within a class that do not have a static modifier and are able to access an object’s fields. Let’s look at the instance methods for the Counter class that implement the three behaviors described above.

Counter.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Counter {
  /** The value displayed on the counter. Must have `0 <= count <= 9999. */
  int count;

  /** Returns the current value displayed by this counter. */
  int count() {
    return this.count;
  }

  /** Increases the value displayed by this counter by 1. */
  void increment() {
    this.count++;
  }

  /** Resets the value displayed by this counter to 0. */
  void reset() {
    this.count = 0;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Counter {
  /** The value displayed on the counter. Must have `0 <= count <= 9999. */
  int count;

  /** Returns the current value displayed by this counter. */
  int count() {
    return this.count;
  }

  /** Increases the value displayed by this counter by 1. */
  void increment() {
    this.count++;
  }

  /** Resets the value displayed by this counter to 0. */
  void reset() {
    this.count = 0;
  }
}

Let’s notice a few things about our instance methods. First, as we already noted, we do not include the static keyword at the start of the method declaration. The static keyword tells Java “this method is not invoked on a particular object”, which is not true of instance methods. Instance methods are called on an object, and we refer to this object as the method’s target or implicit parameter.

Definition: Target / Implicit Parameter

The target or implicit parameter of an instance method is the object on which the method is being invoked.

Next, notice that each of the methods includes JavaDoc specification following the same conventions that we introduced previously. Finally, notice that we are using a new Java keyword, this. this is used to reference the object on which the method was invoked, so it provides us a way to access that object’s fields. A helpful mnemonic to remember what this is doing is to ask a question like, “Whose count is being incremented?”: this object’s count is being incremented since we called the increment() method on this object. We’ll see how to visualize this in our memory diagrams shortly.

Remark:

In most cases, this can be omitted, since Java can infer that we are referencing an object's fields or methods. One exception to this is variable shadowing, which we'll discuss soon. It doesn't hurt to include this, though, and we will continue to do so throughout this and upcoming lectures for pedagogical reasons.

The Constructor

As we saw a few lectures ago, every class has a special method called its constructor that is responsible for setting up the object. With our new terminology, the constructor is responsible for initializing an object’s fields in a way that establishes its class invariant. We can add a constructor to a class we are writing by declaring a method with the same name as the class and with no return type. In our Counter class we add the constructor,

Counter.java

1
2
3
4
5
6
7
8
9
public class Counter {
  /** The value displayed on the counter. Must have `0 <= count <= 9999. */
  int count;

  /** Constructs a new counter object that displays the count 0. */
  Counter() {
    this.count = 0;
  }
}
1
2
3
4
5
6
7
8
9
public class Counter {
  /** The value displayed on the counter. Must have `0 <= count <= 9999. */
  int count;

  /** Constructs a new counter object that displays the count 0. */
  Counter() {
    this.count = 0;
  }
}
Remark:

Technically, the Counter() constructor that we just wrote is not necessary. When no other constructors are defined, Java will provide a "default" constructor that takes in no arguments and initializes all the fields to their default values (0 for primitive numerical types, false for boolean, and null for reference types).

Invoking Instance Method

Whenever you call a method, Java checks whether it is a static method or an instance method (including a constructor). Since instance methods are called using the syntax “<object name>.<method name>()”, where the expression before the “.” is an object reference, Java stores this reference, which can be accessed through the keyword this within the method. Although not technically precise, we’ll imagine that this is another parameter that is included in the call frame of an instance method.

Let’s walk through a simple example of calling an instance method. To do this, we’ll need to write some client code that constructs a Counter object. We’ll do this in a main() method in a separate Runner class.

previous

next

Remark:

While it is technically more accurate, writing phrases like "the Counter object referenced by c" is rather cumbersome, so we will often abbreviate this to just "c" where doing so is unambiguous. Remember, it is objects that have fields, methods, invariants, etc., not variables, so saying "invoke increment() on c is shorthand for "invoke increment() on the Counter object referenced by c."

Variable Scope and Lifetime

These enhanced memory diagrams we just looked at illustrate that variables can exist in multiple different places; for example, some variables are stored in the call frames on the runtime stack whereas others are fields in objects stored on the heap. These distinctions are closely related to different scopes of variables.

Definition: Scope

The scope of a variable describes from where in the code it is allowed to be accessed.

Scope is delimited using curly braces in Java, so every code construct that includes curly braces defines a different scope. Some possible scopes are:

Local variables are those with method scope (including parameters) or block scope within a method. They are allocated in call frames on the runtime stack. Fields, as we have seen, are allocated within objects on the heap.

Scopes are nested, just as we can arbitrarily nest control structures such as loops and if-statements. A line of code can access any variables that are in its same scope (i.e., within the same set of curly braces) or any outer scope that encloses it. In particular, within an instance method of a class, we can access the fields in the outer class scope. A related notion to scope is lifetime.

Definition: Lifetime

The lifetime of a variable describes the times during which it is usable during a program.

Local variables have a lifetime that begins when they are declared and ends when returning from the method where they were declared (which deallocates their memory on the runtime stack). Fields have a lifetime that begins when their associated object is constructed and ends when the object is no longer accessible from the runtime stack (perhaps through a chain of references through multiple objects on the memory heap). Scope and lifetime validity (i.e., making sure that no line of code tries to access a variable that is not in its scope) are properties that are checked statically by the compiler.

Shadowing

In most cases, there can be at most one variable with a given name declared in a given scope. Otherwise, Java would have no way to distinguish to which “version” of that variable the name refers. One convenient exception to this rule relates to class and local variables, which may share a name. This can be useful when defining instance methods that take parameters.

For example, suppose that we wanted to add a setCount() method to our Counter class that takes in a value from the client, checks that it is within range, and then sets the count field to this value. The most natural name for this parameter is count, but this would introduce a count variable (the parameter) into the method scope inside a class scope with a field count. When we type count within this method, which of these variables does it refer to?

The answer is that it refers to the parameter count. Java resolves scopes from inside-out, working its way to larger and larger scopes until it finds a variable with the given name. This resolution starts at the method scope and never reaches the class scope. For this reason, we say that the local variable shadows the field.

Definition: Shadowing

A local variable shadows a field with the same name since references to that name within the method will resolve to the local variable rather than the field.

How do we access the shadowed field? We use the keyword this. Unambiguously, this references the Counter object, so this.count is the count field variable associated with this object. This lets us define our setCount() method as follows:

1
2
3
4
5
6
7
8
/**
 * Sets the current value displayed by this counter to the given `count`.
 * Requires that `0 <= count <= 9999`.
 */
void setCount(int count) {
  assert 0 <= count && count <= 9999; // defensive programming
  this.count = count;
}
1
2
3
4
5
6
7
8
/**
 * Sets the current value displayed by this counter to the given `count`.
 * Requires that `0 <= count <= 9999`.
 */
void setCount(int count) {
  assert 0 <= count && count <= 9999; // defensive programming
  this.count = count;
}

The RHS of this assignment statement evaluates to the value of the count parameter. This gets stored in the variable on the LHS, which is the count field.

Maintaining Class Invariants

The implementer of a class is responsible for maintaining its class invariant, since this will ensure that the code conforms to its specifications. In particular, the class invariant must be true at the beginning and end of every (non-private) instance method call, including at the end of the constructor. Because of this, we can view the class invariant as an implicit pre-condition and post-condition of all (non-private) instance methods.

As an alternate perspective, we can think about the execution of code involving an object as passing control between the client and the implementer. Typically, the client is in control when the code that they have written is being executed. However, when they invoke an instance method on an object, control is passed to the implementer of that object’s class. While the instance method is being executed, the client’s code “pauses” (it sits in a lower call frame on the runtime stack), so the client has no way to check the state of the object. The implementer (who is writing the object’s class, so is granted more control) can temporarily violate the class invariant to execute the method. They must, however, restore the class invariant before handing control back to the client at the end of the method. In this way, the object is always in a “clean state” for the client. This is similar to loop invariants, which may be temporarily violated in the middle of a loop iteration, but must be restored by the time we re-evaluate the loop guard.

Since the class invariant expresses properties about an object’s state, the implementer must ensure that it is maintained (or restored) within any method that modifies the state of the object (including the constructor). Let’s check this for our Counter class, which has the class invariant that 0 <= count <= 9999.

Thinking about the class invariant identified a “corner case” that can cause our Counter class to violate its specification. We can correct this by observing the behavior of the physical handheld counters. When the display reads “9999” and the silver button is pressed, the display “rolls over” to “0000”. Let’s adjust the increment() definition and spec to model this behavior.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/** 
 * Increases the value displayed by this counter by 1. If `count` was 
 * 9999, it instead rolls over and resets to 0. 
 */
void increment() {
  if (this.count < 9999) {
    this.count++;
  } else {
    this.reset();
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/** 
 * Increases the value displayed by this counter by 1. If `count` was 
 * 9999, it instead rolls over and resets to 0. 
 */
void increment() {
  if (this.count < 9999) {
    this.count++;
  } else {
    this.reset();
  }
}

In this definition, we have invoked another instance method (reset()) from within an instance method (increment()), using the keyword this as its target.

Encapsulation

With this modification to the increment() method, Counter now appears to satisfy its class invariant. However, there is still another issue. Consider the following nefarious client code.

1
2
3
4
Counter c = new Counter();
c.count = -50;
c.increment();
System.out.print(c.count());   
1
2
3
4
Counter c = new Counter();
c.count = -50;
c.increment();
System.out.print(c.count());   

When this code is executed, it prints “-49”. This is not good. -49 is not included in the range of allowable values of count, so c’s class invariant has been violated. We might be quick to blame this nefarious client for reassigning c.count to -50, but remember, maintaining the class invariant is the responsibility of the implementer. We need a way, as the implementer, to prevent a client of our class from being able to take this sort of nefarious action, and we can do this by modifying the visibility of our class’ fields and methods.

Definition: Visibility

The visibility of a variable, method, or class describes whether it is accessible within a particular unit of code, such a method or another class.

We can control the visibility of a variable, method, or class by supplying a visibility modifier in its declaration. Since it is included in the declaration, visibility is a static property that can be checked and enforced by the compiler. Today, we’ll consider two new visibility modifiers, public and private.

When a declaration is marked as public, that variable/method/class is visible and accessible whenever it is in scope. On the other hand, when a declaration is marked as private, that variable/method/class is only visible within the class in which it was declared.

Remark:

You might ask what the "default" visibility is (when no visibility modifier is included in the declaration). This is a third visibility level called "package-private", which despite its name is much closer to public for our purposes. We used the default visibility where possible until now to avoid getting bogged down with too many new keywords. However, now that we've introduced visibility modifiers, we'll start to incorporate them in most of our declarations.

Remark:

We don't use visibility modifiers for local variables since their accessibility is already limited by their scope; they are only reachable from within the current call frame.

The following are some guidelines for selecting an appropriate visibility modifier:

Let’s put these rules into practice in our Counter code.

Counter.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Counter {
  /** The value displayed on the counter. Must have `0 <= count <= 9999. */
  private int count;

  /** Constructs a new counter object that displays the count 0. */
  public Counter() {
    this.count = 0;
  }

  /** Returns the current value displayed by this counter. */
  public int count() {
    return this.count;
  }

  /** 
    * Increases the value displayed by this counter by 1. If `count` was 
    * 9999, it instead rolls over and resets to 0. 
    */
  public void increment() {
    if (this.count < 9999) {
      this.count++;
    } else {
      this.reset();
    }
  }

  /** Resets the value displayed by this counter to 0. */
  public void reset() {
    this.count = 0;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Counter {
  /** The value displayed on the counter. Must have `0 <= count <= 9999. */
  private int count;

  /** Constructs a new counter object that displays the count 0. */
  public Counter() {
    this.count = 0;
  }

  /** Returns the current value displayed by this counter. */
  public int count() {
    return this.count;
  }

  /** 
    * Increases the value displayed by this counter by 1. If `count` was 
    * 9999, it instead rolls over and resets to 0. 
    */
  public void increment() {
    if (this.count < 9999) {
      this.count++;
    } else {
      this.reset();
    }
  }

  /** Resets the value displayed by this counter to 0. */
  public void reset() {
    this.count = 0;
  }
}

After these modifications, the “nefarious” code will no longer compile. Instead, we receive the error message


'count' has private access in 'Counter'.

The compiler detected a violation of count’s private visibility, preventing the code from executing and protecting the class invariant. Controlling visibility is one example of encapsulation.

Definition: Encapsulation

Encapsulation is a programming practice where we separate the concerns the implementer and client of a piece of code.

The client should only be able to access fields and methods of the code that correspond to its public interface. Private, implementation specific details that could compromise the class invariant are hidden from the client. A common term for this idea of separation is an abstraction barrier. We abstract the view of a class to the client such that they only have the information that they need to use it. As we proceed in the course, we will discuss other programming practices that promote good encapsulation.

Another Example

Let’s develop another class with a more complicated invariant that will give us more practice with the concepts from today’s lecture. The game Tic-tac-toe is played on a 3 x 3 grid, traditionally drawn without the external border. The grid is initially empty, and two players (“X” and “O”) take turns selecting an empty square on the board and writing their symbol in that square, with the “X” player going first. A player wins if they can write three of their symbols in three cells that form a line, either horizontally, vertically, or diagonally. For example, the following game was a win for the “X” player along a diagonal line.

Modeling State

Let’s write a class that models the state of the game. What do we need to keep track of? Most prominently, we need to keep track of the state of the grid, which symbol is written in each of the cells. Again, there are multiple possible representations of this state, but we’ll use a char[] array, grid, with length 9, whose entries are constrained to ‘X’, ‘O’, and ’ ’ and whose indices each represent particular grid locations.

We call this a row-major ordering of the grid because we number all entries in the first row before proceeding to the next row. We’ll also need variables to keep track of whose turn it is, which we can also do with a char variable, currentPlayer, that stores either ‘X’ or ‘O’. We can add a boolean variable, gameOver that stores whether the game has ended or another turn will take place. Finally, when the game ends, we’ll add a variable that keeps track of who has won. This will be a char variable, winner, that also stores either ‘X’, ‘O’, or ‘T’ (if the game ended in a tie).

Remark:

Some of our choices for how to represent the game state appear a bit arbitrary. Why have we chosen these characters to "encode" the particular states as opposed to other ones? A Java feature called an enumerated type (or enum, for short) offers a more principled way to model sets with a small number of options. However, the representation that we chose is fine, provided it can conform to the specifications.

Let’s set up our TicTacToe class with these fields.

TicTacToe.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class TicTacToe {
  /**
   * The contents of the game grid, listed in row-major order. 
   * Includes exactly 9 entries that are either 'X', 'O', or ' ' 
   * (indicating an empty cell).
   */ 
  private char[] grid;

  /** 
   * Whether the game is over, meaning a player has won or the entire 
   * board has been filled without a winner, resulting in a tie.
   */ 
  private boolean gameOver;

  /** 
   * The next player who will move. Must be 'X' or 'O' if `gameOver`
   * is false, otherwise unspecified. 
   */
  private char currentPlayer;

  /** 
   * The winner of the game. Must be 'X', 'O', or 'T' (to signify a tie)
   * if `gameOver` is true, otherwise unspecified. 
   */
  private char winner;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class TicTacToe {
  /**
   * The contents of the game grid, listed in row-major order. 
   * Includes exactly 9 entries that are either 'X', 'O', or ' ' 
   * (indicating an empty cell).
   */ 
  private char[] grid;

  /** 
   * Whether the game is over, meaning a player has won or the entire 
   * board has been filled without a winner, resulting in a tie.
   */ 
  private boolean gameOver;

  /** 
   * The next player who will move. Must be 'X' or 'O' if `gameOver`
   * is false, otherwise unspecified. 
   */
  private char currentPlayer;

  /** 
   * The winner of the game. Must be 'X', 'O', or 'T' (to signify a tie)
   * if `gameOver` is true, otherwise unspecified. 
   */
  private char winner;
}

assertInv() Methods

The class invariant consists of all the constraints outlined in these field specifications. Since the invariant of this class is more involved, it will be helpful to include a private method that we can use to check that the invariant holds at the end of our (non-private) mutator methods. We’ll call this method assertInv() and it will consist of a series of Java assert statements.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/**
 * Asserts that the `TicTacToe` class invariant holds
 */
private void assertInv() {
  // `grid` includes 9 characters, each either 'X', 'O', or ' '
  assert grid != null && grid.length == 9;
  for (int i = 0; i < 9; i++) {
    assert grid[i] == 'X' || grid[i] == 'O' || grid[i] == ' ';
  }

  // `currentPlayer` is 'X' or 'O' when `gameOver` is false
  assert gameOver || currentPlayer == 'X' || currentPlayer == 'O';

  // `winner` is 'X', 'O', or 'T' when `gameOver` is true
  assert !gameOver || winner == 'X' || winner == 'O' || winner == 'T';
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/**
 * Asserts that the `TicTacToe` class invariant holds
 */
private void assertInv() {
  // `grid` includes 9 characters, each either 'X', 'O', or ' '
  assert grid != null && grid.length == 9;
  for (int i = 0; i < 9; i++) {
    assert grid[i] == 'X' || grid[i] == 'O' || grid[i] == ' ';
  }

  // `currentPlayer` is 'X' or 'O' when `gameOver` is false
  assert gameOver || currentPlayer == 'X' || currentPlayer == 'O';

  // `winner` is 'X', 'O', or 'T' when `gameOver` is true
  assert !gameOver || winner == 'X' || winner == 'O' || winner == 'T';
}

This assertInv() method will help us avoid bugs as we are writing the TicTacToe class by enforcing the class invariant. We usually include all the “easy” checks in an assertInv() but leave out the more complicated checks (such as a check verifying the correctness of the value of gameOver) that would essentially redo the calculations in the other methods. These properties are better enforced by unit testing.

Remark:

When using an assertInv() method, remember to run IntelliJ with the -ea flag so that the assert statements are actually evaluated. Forgetting this is a common mistake made by many students that leads to uncaught bugs in their assignments.

In the constructor, we must initialize the fields to make the class invariant true. grid must be initialized to a char[] containing 9 ' 's, since the grid is initially empty. gameOver should be initialized to false, since more moves are needed before a winner can be determined. currentPlayer should be initialized to X, who moves first. We do not need to initialize the value of winner, as this is only specified once gameOver is true. Finally, since our constructor is a mutator method, we should call assertInv() at the end to confirm the validity of our initializations.

TicTacToe.java

1
2
3
4
5
6
7
8
9
/**
 * Constructs a new TicTacToe game instance with an initially empty board and with 'X' as the starting player. 
 */
public TicTacToe() {
  grid = new char[]{' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '};
  gameOver = false;
  currentPlayer = 'X';
  assertInv();
}
1
2
3
4
5
6
7
8
9
/**
 * Constructs a new TicTacToe game instance with an initially empty board and with 'X' as the starting player. 
 */
public TicTacToe() {
  grid = new char[]{' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '};
  gameOver = false;
  currentPlayer = 'X';
  assertInv();
}

Implementing Behaviors

Next, let’s think about the behaviors provided by this class. We can start with the accessor methods, since these are usually easier to implement. What information might the client want to know about the state of the game? First, they might want to know about the symbols in a particular grid cell so they can draw the current board state for the players. Let’s provide an accessor method contentsOf() that takes in (row, column) grid coordinates and returns the character at that position.

1
2
3
4
5
6
7
8
/**
 * Returns the character in position (`row`,`col`) of the grid, where ' ' 
 * indicates an empty cell. Requires that `0 <= row <= 2` and `0 <= col <= 2`.
 */
public char contentsOf(int row, int col) {
  assert 0 <= row && row <= 2 && 0 <= col && col <= 2; // defensive programming
  return grid[3 * row + col];
}
1
2
3
4
5
6
7
8
/**
 * Returns the character in position (`row`,`col`) of the grid, where ' ' 
 * indicates an empty cell. Requires that `0 <= row <= 2` and `0 <= col <= 2`.
 */
public char contentsOf(int row, int col) {
  assert 0 <= row && row <= 2 && 0 <= col && col <= 2; // defensive programming
  return grid[3 * row + col];
}

This approach is preferable to simply returning grid for a few reasons. First, it better separates the client’s concern (the game state) from the implementer’s concern (the state representation of the board). If we decided to change the TicTacToe class later to store grid as a 2D array (for example), we would not need to adjust the specifications of this method, only its implementation, meaning the client’s code would be unaffected. As a separate concern, returning an array can cause a vulnerability in our code called a rep exposure if we are not careful. We’ll discuss these more soon.

Next, the client may want to query whether the game is over, or who the current player or winning player is. The latter two only make sense at certain points of the game, which we can enforce with method pre-conditions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * Returns whether this game is over.
 */
public boolean gameOver() {
  return gameOver;
}

/**
 * Returns the symbol of the next player to make a move. 
 * Requires that the game is not over.
 */
public char currentPlayer() {
  assert !gameOver;
  return currentPlayer;
}

/**
 * Returns the symbol of the winning player, or 'T' if the game
 * ended in a tie. Requires that the game is over.
 */
public char winner() {
  assert gameOver;
  return winner;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * Returns whether this game is over.
 */
public boolean gameOver() {
  return gameOver;
}

/**
 * Returns the symbol of the next player to make a move. 
 * Requires that the game is not over.
 */
public char currentPlayer() {
  assert !gameOver;
  return currentPlayer;
}

/**
 * Returns the symbol of the winning player, or 'T' if the game
 * ended in a tie. Requires that the game is over.
 */
public char winner() {
  assert gameOver;
  return winner;
}

Lastly, let’s write the mutator method of the TicTacToe class which the client will use to add moves to the board. We’ll call this method processMove(). It should take in the position for the move as its parameters and then update the game state accordingly. We must consider the possibility that the parameters do not represent a valid move (either because they are out of range or refer to a square that is already occupied). How should we handle this? We’ll choose to add pre-conditions that delegate the responsibility of checking for legal moves to the client. In a few lectures, we’ll introduce Exceptions, which offer an alternate approach.

Within the method, how do we adjust the board state to reflect the new move? I find it best to go through each of the fields to see which might need to change.

We can define the processMove() method as follows, delegating the work of checking if the game is over to a (private) helper method. Note that we call assertInv() before returning from this mutator method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * Updates the game state to reflect a move by the `currentPlayer` at board
 * position (`row`,`col`). Requires that `0 <= row <= 2`, `0 <= col <= 2`, 
 * `contentsOf(row, col) == ' '`, and the game is not over.
 */
public void processMove(int row, int col) {
  assert 0 <= row && row <= 2 && 0 <= col && col <= 2; // defensive programming
  assert contentsOf(row, col) == ' ';
  assert !gameOver;

  grid[3 * row + col] = currentPlayer;
  currentPlayer = (currentPlayer == 'X') ? 'O' : 'X'; 
  checkForGameOver(row, col);
  assertInv();
}

/**
 * Checks whether the most recent move at position (row, col) ended the game 
 * and updates the values of `gameOver` and `winner` accordingly.
 */
private void checkForGameOver(int row, int col) { ... }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * Updates the game state to reflect a move by the `currentPlayer` at board
 * position (`row`,`col`). Requires that `0 <= row <= 2`, `0 <= col <= 2`, 
 * `contentsOf(row, col) == ' '`, and the game is not over.
 */
public void processMove(int row, int col) {
  assert 0 <= row && row <= 2 && 0 <= col && col <= 2; // defensive programming
  assert contentsOf(row, col) == ' ';
  assert !gameOver;

  grid[3 * row + col] = currentPlayer;
  currentPlayer = (currentPlayer == 'X') ? 'O' : 'X'; 
  checkForGameOver(row, col);
  assertInv();
}

/**
 * Checks whether the most recent move at position (row, col) ended the game 
 * and updates the values of `gameOver` and `winner` accordingly.
 */
private void checkForGameOver(int row, int col) { ... }

As an exercise, try to complete the implementation of checkForGameOver(). A complete implementation is provided with the lecture code. We’ve also provided a suite of comprehensive unit tests for the TicTacToe class. Read through these tests to see how they are structured and what they assert. Then, you can use these tests to verify the correctness of your implementation.

The Client Side

We can write a “client side” application for the TicTacToe class that processes a user’s inputs to play the game. We’ll write a command line application, TicTacToeConsole.

TicTacToeConsole.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class TicTacToeConsole {
  public static void main(String[] args) {
    TicTacToe ttt = new TicTacToe();

    try (Scanner in = new Scanner(System.in)) {
      while (!ttt.gameOver()) {
        printBoard(ttt);
        System.out.println("It's your turn Player " + ttt.currentPlayer() + ".");
        int pos = 0; // next move position
        boolean validMove = false; // whether the entered move is valid
        while (!validMove) {
          System.out.print("Where would you like to go? ");
          pos = in.nextInt();
          if (pos < 0 || pos > 8 || ttt.contentsOf(pos / 3, pos % 3) != ' ') {
            System.out.println("The position you entered is invalid. Try again.");
          } else {
            validMove = true;
          }
        }
        ttt.processMove(pos / 3, pos % 3);
      }
    }

    printBoard(ttt);
    if (ttt.winner() != 'T') {
      System.out.println("Congratulations Player " + ttt.winner() + "!");
    } else {
      System.out.println("The game ended in a tie.");
    }
  }

  /**
   * Prints the current state of the board for the game instance `ttt`
   */
  private static void printBoard(TicTacToe ttt) { ... } 
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class TicTacToeConsole {
  public static void main(String[] args) {
    TicTacToe ttt = new TicTacToe();

    try (Scanner in = new Scanner(System.in)) {
      while (!ttt.gameOver()) {
        printBoard(ttt);
        System.out.println("It's your turn Player " + ttt.currentPlayer() + ".");
        int pos = 0; // next move position
        boolean validMove = false; // whether the entered move is valid
        while (!validMove) {
          System.out.print("Where would you like to go? ");
          pos = in.nextInt();
          if (pos < 0 || pos > 8 || ttt.contentsOf(pos / 3, pos % 3) != ' ') {
            System.out.println("The position you entered is invalid. Try again.");
          } else {
            validMove = true;
          }
        }
        ttt.processMove(pos / 3, pos % 3);
      }
    }

    printBoard(ttt);
    if (ttt.winner() != 'T') {
      System.out.println("Congratulations Player " + ttt.winner() + "!");
    } else {
      System.out.println("The game ended in a tie.");
    }
  }

  /**
   * Prints the current state of the board for the game instance `ttt`
   */
  private static void printBoard(TicTacToe ttt) { ... } 
}

The complete implementation is provided with the lecture code. Notice the nice separation of roles that our encapsulated class design provides. Most of the code that we have written in the TicTacToeConsole class is focused on input and output, which is its responsibility. It delegates all the work managing the game state to the TicTacToe class. Later in the course, we’ll see how to turn this into a graphical application. While the client code will look a lot different, it will use the TicTacToe class in the same way.

Main Takeaways:

  • We can create new types in Java by defining a class. The fields of the class represent the state of its instances (i.e., objects). The instance methods model the behaviors of these objects and can access and modify the fields.
  • The class invariant includes all properties that we enforce on the fields. It is the responsibility of the implementer to ensure that the class invariant holds at the start and end of any non-private instance method call. We often write a private assertInv() method to help check this.
  • The accessibility of a variable at a particular point in the code is determined by its scope (where it was declared), lifetime (when it exists in memory), and visibility (which code is permitted to access it). All these properties are checked statically by the compiler.
  • The keyword this is used to access the target of an instance method within its body. Most of the time, this can be omitted and inferred by the compiler, except in the case of shadowing.
  • Encapsulation is a principle of object-oriented programming in which implementation details of a class are hidden from its client. This helps to separate the concerns of the client and the implementer and maintain class invariants.

Exercises

Exercise 8.1: Check Your Understanding
(a)
Consider a class with a static method foo() and a non-static method bar(). Which of the following will fail to compile?
Check Answer
(b)

Consider the following class definition:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class MealCard {
  private int balance;

  public boolean pay(int cost) {
    int newBalance = balance - cost;
    boolean couldAfford;
    if (newBalance < 0) {
      int shortage = -newBalance;
      couldAfford = false;
    } else {
      balance = newBalance;
      couldAfford = true;
    }
    // HERE
    return couldAfford;
  }

  private void addPoints(int points) {
    balance += points;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class MealCard {
  private int balance;

  public boolean pay(int cost) {
    int newBalance = balance - cost;
    boolean couldAfford;
    if (newBalance < 0) {
      int shortage = -newBalance;
      couldAfford = false;
    } else {
      balance = newBalance;
      couldAfford = true;
    }
    // HERE
    return couldAfford;
  }

  private void addPoints(int points) {
    balance += points;
  }
}
Which variables and methods are in scope at the line of code with the comment “// HERE”?
Check Answer
(c)

Consider the partial definition of a class Incrementer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Incrementer {
  private int count;

  public void bar() {
    count = count + 1;
    int count = 2;
    count = count + 2;
  }

  // ...
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Incrementer {
  private int count;

  public void bar() {
    count = count + 1;
    int count = 2;
    count = count + 2;
  }

  // ...
}
Suppose c is an object of class Incrementer, and c’s count field is initialized to some value \(W\). After calling c.bar(), what will the value of c’s count field be?
Check Answer
(d)
Which of the following statements about encapsulation are true? Select all that apply.
Check Answer
Exercise 8.2: Diagramming Objects
For each of the following programs, draw an object diagram for the state of the program after executing the code. Assume that the code below is the only code in some main() method when ran. Do not include args in your diagram.
(a)

Refer to the Counter class in the above lecture notes.

1
2
3
4
5
6
Counter c1 = new Counter();
Counter c2 = new Counter();
c1.increment();
int a = c2.count();
c2.increment();
int b = c1.count();
1
2
3
4
5
6
Counter c1 = new Counter();
Counter c2 = new Counter();
c1.increment();
int a = c2.count();
c2.increment();
int b = c1.count();
(b)
1
2
3
4
5
6
7
8
Counter c1 = new Counter();
for (int i = 0; i < 10; i++) {
  c1.increment();
}
Counter c2 = c1;
int a = c1.count();
Counter c1 = new Counter();
int b = c1.count();
1
2
3
4
5
6
7
8
Counter c1 = new Counter();
for (int i = 0; i < 10; i++) {
  c1.increment();
}
Counter c2 = c1;
int a = c1.count();
Counter c1 = new Counter();
int b = c1.count();
(c)

Refer to the TicTacToe class in the above lecture notes.

1
2
3
4
5
6
TicTacToe ttt = new TicTacToe();
ttt.processMove(1, 1);
ttt.processMove(2, 0);
int a = ttt.currentPlayer();
char b = ttt.contentsOf(0, 0);
char c = ttt.contentsOf(2, 0);
1
2
3
4
5
6
TicTacToe ttt = new TicTacToe();
ttt.processMove(1, 1);
ttt.processMove(2, 0);
int a = ttt.currentPlayer();
char b = ttt.contentsOf(0, 0);
char c = ttt.contentsOf(2, 0);
(d)

Consider the following Student class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Student {
  /** The netid of a student. */
  private String netid;
  
  /** The array of course codes this student is enrolled in. */
  private String[] courses;

  /** Constructs a new student with `netid` and `courses`. */
  public Student(String netid, String[] courses) {
    this.netid = netid;
    this.courses = courses;
  }

  /** Returns the courses this student is enrolled in. */
  public String[] getCourses() { return courses; }

  /** Returns the student's netid. */
  public String getNetId() { return netid; }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Student {
  /** The netid of a student. */
  private String netid;
  
  /** The array of course codes this student is enrolled in. */
  private String[] courses;

  /** Constructs a new student with `netid` and `courses`. */
  public Student(String netid, String[] courses) {
    this.netid = netid;
    this.courses = courses;
  }

  /** Returns the courses this student is enrolled in. */
  public String[] getCourses() { return courses; }

  /** Returns the student's netid. */
  public String getNetId() { return netid; }
}
1
2
3
4
5
6
7
String netid = "abc123";
String[] courses = {"CS2112", "CS2800"};
Student s1 = new Student(netid, courses);
netid = "def456";
courses[0] = "CS2110";
String newNetid = s1.getNetId();
String[] newCourses = s1.getCourses();
1
2
3
4
5
6
7
String netid = "abc123";
String[] courses = {"CS2112", "CS2800"};
Student s1 = new Student(netid, courses);
netid = "def456";
courses[0] = "CS2110";
String newNetid = s1.getNetId();
String[] newCourses = s1.getCourses();
The code above is an example of rep exposure. By directly returning a private mutable reference type, clients are able to edit the contents of the state without restrictions, possibly violating class invariants.
Exercise 8.3: Brainstorming States and Behaviors
For each of the following class descriptions and behaviors, write a list of fields and methods with specifications on how to represent this class' intended behavior.
(a)
Implement a BankAccount class that represents a user’s bank account. A user should be able to withdraw and deposit money.
(b)
Implement a Point class that represents a point in 2-dimensional space. A user should be able to query its distance from the origin and translate the point around the 2D plane.
(c)
Implement a Stopwatch class that represents a stopwatch. A user should be able to add time, lap, and view all previous lap times.
Exercise 8.4: To See or Not to See
For each of the following code snippets, identify if there are any compile time errors. If so, edit the class to make the code compile.
(a)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/** Represents a possible electric bicycle. */
public class Bicycle {
  /** The weight of the bicycle. */
  private double weight;

  /** Whether the bike is electric. */
  private double isElectric;

  /** The weight of the battery if electric. If `isElectric = false`, then unspecified. */
  private double batteryWeight;

  /** Returns the total weight of the bicycle. */
  public double weight() {
    if (isElectric) {
      double totalWeight = weight;
    } else {
      double totalWeight = weight + batteryWeight;
    }
    return totalWeight;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/** Represents a possible electric bicycle. */
public class Bicycle {
  /** The weight of the bicycle. */
  private double weight;

  /** Whether the bike is electric. */
  private double isElectric;

  /** The weight of the battery if electric. If `isElectric = false`, then unspecified. */
  private double batteryWeight;

  /** Returns the total weight of the bicycle. */
  public double weight() {
    if (isElectric) {
      double totalWeight = weight;
    } else {
      double totalWeight = weight + batteryWeight;
    }
    return totalWeight;
  }
}
(b)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/** Represents a (not necessarily simplified) improper fraction. */
public class Fraction {
  /** The numerator. */
  private int numer;

  /** The denominator. Requires != 0. */
  private int denom;

  /** Creates a fraction with numerator `n` and denominator `d`.
   * Requires `d` != 0.
   */
  public Fraction(int n, int d) {
    numer = n;
    denom = d;
  }

  /** Returns the product of `this` and `other`. */
  public Fraction multiply(Fraction other) {
    return new Fraction(numer * other.numer, denom * other.denom);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/** Represents a (not necessarily simplified) improper fraction. */
public class Fraction {
  /** The numerator. */
  private int numer;

  /** The denominator. Requires != 0. */
  private int denom;

  /** Creates a fraction with numerator `n` and denominator `d`.
   * Requires `d` != 0.
   */
  public Fraction(int n, int d) {
    numer = n;
    denom = d;
  }

  /** Returns the product of `this` and `other`. */
  public Fraction multiply(Fraction other) {
    return new Fraction(numer * other.numer, denom * other.denom);
  }
}
Exercise 8.5: Interval Representations
We want to design a class to represent an (open) interval in 1-dimensional space. Consider one such implementation of an interval where we represent the state with a left and right endpoint.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class IntervalLeftRight {
  /** The left (exclusive) endpoint of the interval. Requires `left <= right`. */
  private double left;

  /** The right (exclusive) endpoint of the interval. Requires `left <= right`. */
  private double right;

  /** Creates a new interval of the range [`left`..`right`). Requires `left <= right`. */ 
  public IntervalLeftRight(int left, int right) { 
      assert left <= right;
      this.left = left;
      this.right = right;
  }

  /** Returns if the interval is empty. */
  public boolean isEmpty() {
    return left == right;
  }

  /** Returns whether `this` intersects with `other`. */
  public boolean intersects(Interval other) {
    return Math.max(this.left, other.left) < Math.min(this.right, other.right);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class IntervalLeftRight {
  /** The left (exclusive) endpoint of the interval. Requires `left <= right`. */
  private double left;

  /** The right (exclusive) endpoint of the interval. Requires `left <= right`. */
  private double right;

  /** Creates a new interval of the range [`left`..`right`). Requires `left <= right`. */ 
  public IntervalLeftRight(int left, int right) { 
      assert left <= right;
      this.left = left;
      this.right = right;
  }

  /** Returns if the interval is empty. */
  public boolean isEmpty() {
    return left == right;
  }

  /** Returns whether `this` intersects with `other`. */
  public boolean intersects(Interval other) {
    return Math.max(this.left, other.left) < Math.min(this.right, other.right);
  }
}
Another possible state representation of an open interval stores its midpoint its and radius.
(a)

Consider the partial class definition of IntervalMidpointRadius.

1
2
3
4
public class IntervalMidpointRadius {
  private double midpoint;
  private double radius;
}
1
2
3
4
public class IntervalMidpointRadius {
  private double midpoint;
  private double radius;
}

Add documentation for these two fields to describe the class invariant of IntervalMidpointRadius.

(b)
Write specifications for and implement a constructor of the class.
(c)
Implement isEmpty() and intersects() in this implementation of an interval.
(d)
Add a method convert() to the IntervalMidpointRadius class that returns an IntervalLeftRight containing the same set of points. Add a method convert() to the IntervalLeftRight class that returns an IntervalMidpointRadius containing the same set of points.
(e)
Add an intersectWith() method to both interval classes that has another interval other (of its same class) as a parameter and returns the set of points common to both this and other (which will be another open interval).
(f)
Write unit tests to verify the correctness of both interval classes.
Exercise 8.6: Design Connect 4
The classic game of Connect 4 is another classic 2 player game. Players alternate taking turns dropping yellow and red discs into a grid with seven columns and six rows. When a disc is dropped, it falls to the bottom most row in the column that is empty. The first person to achieve four of their colored discs in a row (horizontally, vertically, or diagonally) wins. In the following example, the player with the yellow discs wins.
Instead of fixing the board size, this version of Connect 4 will have variable height and width. We have decided to represent the state of this ConnectFour class as follows:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ConnectFour {
  /**
   * The contents of the game grid, where `board[i]` is the (`i`+1)th row from 
   * the top. Requires `board.length = height`, `board[i].length = width`, and
   * `board[i][j]` is either 0 (representing an empty cell), 1, or 2. Each column
   * should be filled from the bottom up, or formally, for each column `0 <= j < 
   * width` there exists `0 <= i <= height` such that `board[i..height)[j] != 0` and 
   * `board[0..i)[j] = 0`.
   */ 
  private int[][] board;

  /** The winner of the game. If no winner, is zero. */
  private int winner;

  /** The next player to move. Either one of 1 or 2. */
  private int player;

  /**
   * The width of the board. Requires `width > 0` and at least one of `width` or
   * `height` is at least 4.
   */
  private int width;

  /** 
   * The width of the board. Requires `height > 0` and at least one of `width` or
   * `height` is at least 4.
   */
  private int height;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ConnectFour {
  /**
   * The contents of the game grid, where `board[i]` is the (`i`+1)th row from 
   * the top. Requires `board.length = height`, `board[i].length = width`, and
   * `board[i][j]` is either 0 (representing an empty cell), 1, or 2. Each column
   * should be filled from the bottom up, or formally, for each column `0 <= j < 
   * width` there exists `0 <= i <= height` such that `board[i..height)[j] != 0` and 
   * `board[0..i)[j] = 0`.
   */ 
  private int[][] board;

  /** The winner of the game. If no winner, is zero. */
  private int winner;

  /** The next player to move. Either one of 1 or 2. */
  private int player;

  /**
   * The width of the board. Requires `width > 0` and at least one of `width` or
   * `height` is at least 4.
   */
  private int width;

  /** 
   * The width of the board. Requires `height > 0` and at least one of `width` or
   * `height` is at least 4.
   */
  private int height;
}
(a)
Write an assertInv() method to enforce the class invariant.
Now, implement each of the following methods according to its specification.
(b)
1
2
3
4
5
6
/**
 * Creates a new ConnectFour game with an empty board. Player 1 starts first. 
 * Requires `height > 0`, `width > 0`, and at least one of `height` or `width` 
 * is at least 4.
 */
public ConnectFour(int height, int width) { ... }
1
2
3
4
5
6
/**
 * Creates a new ConnectFour game with an empty board. Player 1 starts first. 
 * Requires `height > 0`, `width > 0`, and at least one of `height` or `width` 
 * is at least 4.
 */
public ConnectFour(int height, int width) { ... }
(c)
1
2
3
4
/**
 * Returns the winner of the game. If no winner, returns 0.
 */
public int getWinner() { ... }
1
2
3
4
/**
 * Returns the winner of the game. If no winner, returns 0.
 */
public int getWinner() { ... }
(d)
1
2
3
4
5
6
/**
 * Checks whether the most recent move at position `col` ended the game 
 * and updates the values of `winner` accordingly. Requires that `0 <= col 
 * < width`.
 */
private void checkForGameOver(int col) { ... }
1
2
3
4
5
6
/**
 * Checks whether the most recent move at position `col` ended the game 
 * and updates the values of `winner` accordingly. Requires that `0 <= col 
 * < width`.
 */
private void checkForGameOver(int col) { ... }
(e)
1
2
3
4
5
/**
 * Updates the game state to reflect a move by the `player` after dropping a disc 
 * in column `col`. Requires that `0 <= col < width` and `board[0][col] == 0`.
 */
public int processMove(int col) { ... }
1
2
3
4
5
/**
 * Updates the game state to reflect a move by the `player` after dropping a disc 
 * in column `col`. Requires that `0 <= col < width` and `board[0][col] == 0`.
 */
public int processMove(int col) { ... }
(f)
Now that we’ve built the core logic behind Connect Four, write a client-side application for this game, processing the user’s input to set up the game board and to process moves.