Classes and Objects

This section illustrates the software development cycle and introduces some concepts from object-oriented programming. As an extended example, we develop a program to play the game of Beetle (Figure 1-4).

Figure 1-4. Beetle, also known as Bug or Cootie, is a children's game of pure luck. Our implementation handles only two players.

(This item is displayed on page 10 in the print version)

Beetle

Players: 2 or more

Object: To be the first player with a complete beetle. A complete beetle has a body, a head, six legs, two eyes, two feelers, and a tail.

Setup: Each player starts with no parts on her beetle.

Play: On your turn, roll a die, and act on the result:

1.

If your beetle already has a body, pass the die to the next player. Otherwise, add a body and roll again.

2.

If your beetle already has a head or has no body, pass the die to the next player. Otherwise, add a head and roll again.

3.

If your beetle already has six legs or has no body, pass the die to the next player. Otherwise, add two legs and roll again.

4.

If your beetle already has two eyes or has no head, pass the die to the next player. Otherwise, add an eye and roll again.

5.

If your beetle already has two feelers or has no head, pass the die to the next player. Otherwise, add a feeler and roll again.

6.

If your beetle already has a tail or has no body, pass the die to the next player. Otherwise, add a tail and roll again.

 

Classes

In Appendix A, we use the word "class" as a rough synonym for "program." While each program needs to have a main() method in some class, many classes are not programs. In object-oriented programming, a class is predominantly a description of a set of similar objects, such as the class of birds, the class of insurance claims, or the class of dice. A class is an encapsulated component of the program.

We can create multiple instances of a class. Each instance is a different object, but they have things in common with the other members of their class. In the Beetle game, we'll eventually have to create two instances of the class of beetlesone for each player. We will also create one instance of the class of dice.

Breaking our program down into classes is the first step in design. For the Beetle game, we will need three classes: the class of beetles, the class of dice, and the class of Beetle games. (We create one instance of the last class each time we play the game.) This organization is illustrated in Figure 1-5. This type of diagram is called a UML class diagram. The UML (Unified Modeling Language) is a widely used set of notations for diagramming many aspects of software development, from user interactions to relationships between methods. Most of the UML is beyond the scope of this book, but we will use these class diagrams as well as (later in this section) instance diagrams. UML notation is introduced gradually over the course of the book and summarized in Appendix B.

Figure 1-5. UML class diagram for the Beetle game program. This diagram says that one instance of BeetleGame is associated with two instances of Beetle and one instance of Die.

In Java, the name of a class traditionally begins with an upper-case letter. The three classes we need to build are therefore called Die, Beetle, and BeetleGame. Each class is defined in a separate file, which must have the same name as the class and a .java extension. The first drafts of these files are shown in Figures 1-6 through 1-8 .

Figure 1-6. The file Beetle.java.

1 /** Beetle with parts for the Beetle game. */ 2 public class Beetle { 3 }

Figure 1-7. The file BeetleGame.java.

1 /** The game of Beetle for two players. */ 2 public class BeetleGame { 3 }

Figure 1-8. The file Die.java.

1 /** A six-sided die for use in games. */ 2 public class Die { 3 }

Objects, Fields, and Methods

We now focus on the simplest class of objects, the Die class.

What exactly is an object? An object has two kinds of components:

In order to make our development cycle for the Die class as short as possible, we start by thinking, "It will have to keep track of which face is on top." We'll design other features, such as the method for rolling, later. In a top-down approach to software development, on the other hand, we would specify all of the methods before thinking about implementation details such as fields.

A first shot at implementing the Die class is shown in Figure 1-9.

Figure 1-9. A first shot at implementing the Die class. It compiles, but it doesn't run.

1 /** A six-sided die for use in games. */ 2 public class Die { 3 4 /** The face of this Die that is showing. */ 5 private int topFace; 6 7 }

This class compiles, but it doesn't run. The problem is that there is no main() method. Before we fix this, notice a couple of things about the field topFace.

First, the field is not declared static. This means that it can have a different value for each instance of Die. A field like this is called an instance field or instance variable. Since this is the most common kind of field, it is often simply called a field.

Second, the field is declared private. Instance fields are normally declared private. This means that they cannot be accessed by methods in other classes. When other classes do things with Die instances, code in those classes can't access private fields directly. This is an example of information hiding.

Let's put in an empty main() method (Figure 1-10).

Figure 1-10. The class can now be run, although it still doesn't do anything.

1 /** A six-sided die for use in games. */ 2 public class Die { 3 4 /** The face of the die that is showing. */ 5 private int topFace; 6 7 /** Doesn't do anything yet. */ 8 public static void main(String[] args) { 9 } 10 11 }

Was there any point in doing this? Yes: we now have a program that we can run. After each change we make from now on, we can check if the program still runs. If not, the bug is most likely in the new code. By making such small, incremental changes to the code, we can avoid spending a lot of time hunting for bugs.

We have now completed one iteration of the software development cycledesign, implementation, and testingfor the Die class.

Constructors

Returning to design, the next thing our class needs is a constructor. A constructor is a method that initializes all of the fields of an object. It always has the same name as the class. We decide that, whenever a new instance of the Die is created, it will have the 1 as the top face.

Our design so far can be summed up in a more detailed UML class diagram (Figure 1-11). Since the Die class is an encapsulated component, we don't include the boxes for the other two classes in the program in the diagram.

Figure 1-11. UML class diagram for the Die class. This diagram says that there is one field, topFace, which is of type int. There are two methods: the constructor Die() and the method main(). The constructor takes no arguments and, like all constructors, has no return type. The main() method takes an array of Strings as an argument and has a return type of void. This method is underlined because it is staticmore on this later.

The implementation of the class, including the constructor, is shown in Figure 1-12. The constructor, on lines 710, is easily recognized because (a) it has the same name as the class and (b) it has no return type. Its job is to initialize the topFace field of every new instance of Die to 1.

Figure 1-12. The constructor initializes the field topFace to 1.

1 /** A six-sided die for use in games. */ 2 public class Die { 3 4 /** The face of this Die that is showing. */ 5 private int topFace; 6 7 /** Initialize the top face to 1. */ 8 public Die() { 9 this.topFace = 1; 10 } 11 12 /** 13 * Create a Die, print the top face, set the top face to 6, and 14 * print it again. 15 */ 16 public static void main(String[] args) { 17 Die d = new Die(); 18 System.out.println(d.topFace); 19 d.topFace = 6; 20 System.out.println(d.topFace); 21 } 22 23 }

To create a new instance of the Die class, we use the expression:

new Die();

On line 17, the main() method creates such an instance. This line may seem a bit cryptic, but it is no stranger than a line like:

int x = 3;

Line 17 declares and initializes a variable of type Die. New types are defined in Java by creating classes. The name of the variable is d. The initial value of d is the result of the expression new Die().

Once we have an instance, we can access its topFace field. In the main() method, the instance is called d, so the field is accessed as d.topFace. Within a nonstatic method such as our constructor, we can refer to the current object as this.

Information hiding prevents us from referring to d.topFace within the constructor, because d is a variable inside the main() method. The constructor can still do things to the object (such as setting its topFace field), because this and d are references to the same instance.

This situation is illustrated in Figure 1-13, which is called a UML instance diagram. In an instance diagram, we show the values of the fields within an instance. While there is only one instance in this particular diagram, we will later see instance diagrams containing multiple instances of the same class. In a class diagram, on the other hand, each class appears only once. We do not show the methods in an instance diagram, because all instances of the same class have the same methods.

Figure 1-13. In a UML instance diagram, the (nonstatic) fields of each instance are shown, but the methods are not. In this diagram, this and d are both references to the same instance of Die.

(This item is displayed on page 14 in the print version)

If we fail to initialize a field in an object, Java gives it a default value. For number types such as int and double, the default value is 0. For booleans, the default value is false. For chars, the default value is the unprintable character with the ASCII and Unicode value 0. For arrays and all object types, the default value is the special value null. A null reference does not point to anything in particular. We'll discuss null in more detail in Chapter 2.

This automatic initialization of fields is an unusual feature of Java. If we rely on it, we should include a comment to this effect, in case someone some day wants to translate our code into another language, such as C, which does not have this feature.

We now compile and run our program. It produces the output:

1 6

This is exactly what we expected. On to the next iteration of the development cycle!

Accessors, Mutators, and this

Referring to the topFace field as d.topFace is fine while we're working within the Die class, but eventually other classes (like BeetleGame) will have to know which face is showing on a Die. It would violate encapsulation for a method in another class to directly access this field. In fact, since we declared topFace to be private, Java won't let us do this. This is information hiding enforcing encapsulation.

Other classes should be able to get at the fields of an object only through methods. Two particularly common types of methods are accessors and mutators. An accessor, also known as a getter, returns the value of some field. A mutator, also known as a setter, changes (mutates) the value of some field within the object.

We add an accessor getTopFace() and a mutator setTopFace() to the design of the Die class in Figure 1-14.

Figure 1-14. Adding an accessor and a mutator to the UML class diagram for the Die class.

The code for these two methods, as well as the revised main() method, is shown in Figure 1-15.

Figure 1-15. Accessor, mutator, and revised main() method for the Die class. Since the rest of the class is unchanged, it is not shown in this Figure.

1 /** Return the top face of this Die. */ 2 public int getTopFace() { 3 return this.topFace; 4 } 5 6 /** Set the top face to the specified value. */ 7 public void setTopFace(int topFace) { 8 this.topFace = topFace; 9 } 10 11 /** 12 * Create a Die, print the top face, set the top face to 6, and 13 * print it again. 14 */ 15 public static void main(String[] args) { 16 Die d = new Die(); 17 System.out.println(d.getTopFace()); 18 d.setTopFace(6); 19 System.out.println(d.getTopFace()); 20 }

Notice the statement

this.topFace = topFace;

on line 8. The use of this distinguishes between the field topFace (on the left) and the argument topFace (on the right).

Static vs. Nonstatic

Whenever we refer to a nonstatic field or invoke a nonstatic method, we must indicate a particular instance. For example, we can't just say getTopFace(); we have to say something like d.getTopFace() or this.getTopFace(). (We'll see a way to implicitly indicate this in Section 1.3.) We can use this only within nonstatic methods, because only nonstatic methods are invoked on a particular instance of the class. For example, we cannot say

System.out.println(this.getTopFace());

in main() because main() is a static method. A static method is about the entire class, rather than about an individual instance. Static methods are sometimes called class methods. Nonstatic methods are sometimes called instance methods. In UML class diagrams, static methods are underlined.

The main() method of any class must be static. This is because it can be the first method run. It can't be invoked on a particular instance because there might not yet be any instances of the class.

Static methods are invoked on a class (such as the Math class, which we'll use momentarily) rather than on a specific instance. While it is legal to invoke a static method on an instance, doing so can lead to some surprising results, so it is a bad idea.

Completing the Die Class

We will need one more method to roll the Die. The expanded class diagram is shown in Figure 1-16.

Figure 1-16. The roll() method takes no arguments and has a return type of void.

The implementation involves the static random() method from the built-in Math class. This method returns a random double which is at least 0 and less than 1. If we multiply the result by 6, throw away any fractional part by casting it to an int, and add 1, we get a random int between 1 and 6 inclusive.

The roll() method, along with a main() method to test it, is shown in Figure 1-17. We should run this one a few times, because it does not always produce the same output.

Figure 1-17. The roll() method and a main() method to test it.

1 /** 2 * Set the top face to a random integer between 1 and 6, inclusive. 3 */ 4 public void roll() { 5 this.topFace = ((int)(Math.random() * 6)) + 1; 6 } 7 8 /** Create a Die, print it, roll it, and print it again. */ 9 public static void main(String[] args) { 10 Die d = new Die(); 11 System.out.println("Before rolling: " + d.getTopFace()); 12 d.roll(); 13 System.out.println("After rolling: " + d.getTopFace()); 14 }

We are done with the Die class for now. We can generate automatic documentation with the command:

javadoc -public Die.java

Notice that in the resulting file Die.html, the private field is not shown. This is encapsulation at work again. If someone comes along later to write another game involving the Die class, she doesn't have to look at our code. The documentation tells her everything she needs to use Die objects.

Exercises

1.6

Remove the statement

this.topFace = 1;  

from the Die constructor (line 9 in Figure 1-12). Does the class still compile and run correctly? Explain.

1.7

Explain why accessors and mutators are not needed for constants. (Constants are explained in Appendix A.)

1.8

Consider the statement:

System.out.println("Practice what you preach.");  

What sort of thing is System? (Is it a class, a static method, an instance method, or something else?) What is System.out? What is System.out.println()?

1.9

Add an assertion to the setTopFace() method to prevent anyone from setting a Die's topFace field to a value less than 1 or greater than 6.

Категории