Lesson 4. Class Methods and Fields

In this lesson you will:

• refactor an instance method to a class method

• learn about class variables and methods

• use the static import facility

• learn how to use compound assignment and increment operators

• understand what simple design is

• create a utility method

• learn how to judiciously use class methods

• work with the boolean primitive type

• understand why it is important that tests act as documentation

• be exposed to exceptions and stack traces

• learn more about initialization

Class Methods

Objects are a combination of behavior (implemented in Java in terms of methods) and attributes (implemented in Java in terms of fields). Attributes for an object stick around as long as the object sticks around. At any given point in time, an object has a certain state, which is the combined snapshot of all its instance variables. For this reason, instance variables are sometimes called state variables.

Action methods in the object may operate on and change attributes of the object. In other words, action methods can alter the object state. Query methods return pieces of the object state.

image

Design your methods to either change object state or return information, not both.

Occasionally you will find the need for a method that can take parameters, operate on only those parameters, and return a value. The method has no need to operate on object state. This is known as a utility method. Utility methods in other languages are sometimes called functions. They are global: any client code can access them.

Sometimes, having to create an object in order to use a utility method makes little sense. For example, the DateUtil method createDate you coded in Lesson 3 is a simple function that takes month, day, and year integer arguments and returns a Date object. The createDate method changes no other data. Removing the need to construct DateUtil objects will also simplify your code a bit. Finally, since createDate is the only method in DateUtil, there is no other need to construct instances of it.

For these reasons, createDate is a good candidate for being a class method. In this exercise, you will refactor createDate to be a class method in this exercise. Start by changing the test to make a class method call:

image

You no longer create an instance of DateUtil with the new operator. Instead, to call a class method, you specify the class on which the class method is defined (DateUtil), followed by the dot operator (.), followed by the method name and any arguments (createDate(2000, 1, 1)).

Changes to the DateUtil class itself are similarly minor:

image

You declare a class method just like a “regular,” or instance, method, except that you prefix its declaration with the keyword static.

In addition to making the createDate method static, it's a good idea to make the constructor of DateUtil private. By declaring the constructor as private, only code in the DateUtil class can construct new DateUtil instances. No other code will be able to do so. While it wouldn't be harmful to allow creation of DateUtil objects, keeping clients from doing something nonsensical and useless is a good idea.

Adding the private constructor will also make it simpler for you to pinpoint the nonstatic references to createDate. When you compile your code, methods that create a new DateUtil object will generate compilation errors. For example, the setUp method in CourseSessionTest will fail compilation:

image

Fix the remainder of the failing compilation problems and rerun your tests. You now have a general-purpose utility that may find frequent use in your system.1

1 The utility is not the best-performing one. It is not necessary to create a GregorianCalendar object with each call to createDate. For sporadic use, this is probably just fine. For heavy use—say, creating 10,000 dates upon reading an input file—you'll want to consider caching the calendar object using a class variable (see the next section).

The class java.lang.Math in the J2SE 5.0 class library supplies many many mathematical functions. For example, Math.sin returns the sine of a double and Math.toRadians converts a double value from degrees to radians. The Math class also provides two standard mathematical constants, Math.PI and Math.E. Since each of the methods in java.lang.Math is a utility class method, it is known as a utility class.

In UML (Figure 4.1), you indicate a utility class using the stereotype <<utility>>. Stereotypes in UML define semantics beyond the limitations of what UML supplies. A utility stereotype specifies that all class behaviors and attributes may be globally accessed.

Figure 4.1. The Math Utility Class

image

Normally you underline class behaviors and class attributes in UML. Since the <<utility>> stereotype declares that all methods and attributes in a class are global, you need not underline them.

Class Variables

You will occasionally want to track information about all instances of a class or perform an operation without first creating an instance of an object. As a simplistic example, you might want to track the total number of course sessions. As each CourseSession object is created, you want to bump up a counter. The question is, where should you put this counter? You could provide an instance variable on CourseSession to track the count, but this is awkward: Would all instances of CourseSession have to track the count? How would one CourseSession instance know when others were created so that it could update the count?

You could provide another class, CourseSessionCounter, whose sole responsibility is to track the CourseSession objects created. But a new class seems like overkill for the simple goal you are trying to accomplish.

In Java, you can use class variables, as opposed to instance variables, for a solution. Client code can access a class variable without first creating an instance of that class. Class variables have what is known as static scope: they exist as long as the class exists, which is pretty much from the time the class is first loaded until your application terminates.

You have already seen class constants in use. Class constants are class variables that you have designated as final.

The following test code (in CourseSessionTest) counts the number of CourseSession instances created:

image

(Don't forget to update the setUp method to use createCourseSession.)

To support the test, create a class variable in the CourseSession class named count. You use the static keyword to designate a variable as static in scope. Also add code to CourseSession to update count when a new Course-Session instance is created.

image

You access the class variable count similar to the way you call a class method: First specify the class name (CourseSession), followed by the dot (.) operator, followed by the variable name (count). The Java VM does not create an instance of CourseSession when code accesses the class variable.

As I mentioned, class variables have a different lifetime than instance variables. Instance variables stick around for the lifetime of the object that contains them. Each new CourseSession object that the Java VM creates manages its own set of the instance variables declared in CourseSession. When the VM creates a CourseSession object, it initializes its instance variables.

A class variable, however, comes into existence when the Java VM first loads the containing class—when code that is currently executing first references the class. There is one copy of the class variable in memory. The first time the Java VM loads a class, it initializes its class variables, and that's it. If you need to reset a class variable to an initial state at a later time, you must explicitly initialize it yourself.

As an experiment, comment out the first line in testCount (the line that reads CourseSession.count = 0). Then run the tests in JUnit. Turn off the checkbox in JUnit that says “Reload classes every run.”2 If you run the tests twice (by clicking the Run button), they will fail, and you should see the actual count go up with each execution of the tests. You may even see the first run of the test fail: Other test methods in CourseSessionTest are creating CourseSession objects, which increments the count variable.

2 This JUnit switch, when turned on, results in your test classes being physically loaded from disk and reinitialized each time the tests are run in JUnit. If you are running in an IDE such as Eclipse, you may not have control over this JUnit feature.

Operating on Class Variables with Class Methods

Just as it is bad form to expose instance variables of your objects directly to prying clients, it is also bad form to expose class variables publicly. The notable exception is the class constant idiom—but there are even good reasons to avoid using class constants that you will learn in Lesson 5.

In addition to being able to use class methods for utility purposes, you can use class methods to operate on static data.

The CourseSession method testCount accesses the count class variable directly. Change the test code to ask for the count by making a class method call.

public void testCount() {
   CourseSession.count = 0;
   createCourseSession();
   assertEquals(1, CourseSession.getCount());
   createCourseSession();
   assertEquals(2, CourseSession.getCount());
}

Then add a class method to CourseSession that returns the class variable count.

static int getCount() {
   return count;
}

Class methods can access class variables directly. You should not specify the class name when accessing a class variable from a class method.

The Java VM creates no instances of CourseSession as a result of calling the class method. This means that class methods on CourseSession may not access any of the instance variables that CourseSession defines, such as department or students.

The test method still refers directly to the count class variable, however, since you need it initialized each time the test is run:

public void testCount() {
   CourseSession.count = 0;
   ...

Change the code in CourseSessionTest to make a static message send to reset the count:

public void testCount() {
   CourseSession.resetCount();
   createCourseSession();
   assertEquals(1, CourseSession.getCount());
   createCourseSession();
   assertEquals(2, CourseSession.getCount());
}

Add the resetCount method to CourseSession and make the count class variable private:

public class CourseSession {
   // ...
   private static int count;
   // ...
   static void resetCount() {
      count = 0;
   }
    static int getCount() {
     return count;
   }
   // ...

Making the count variable private will point out (when you recompile) any other client code that directly accesses it.

The testCount method, which documents how a client should use the Course-Session class, is now complete and clean. But the CourseSession class itself still accesses the class variable directly in its constructor. Instead of accessing static data directly from a member (an instance-side constructor, field, or method), a better approach is to create a class method that you call from the instance side. This is a form of encapsulation that will give you greater control over what happens to the class variable.

Change the CourseSession constructor to send the incrementCount message instead of accessing the class variable directly:

image

Then add a class method to CourseSession that increments the count. Declare this method as private to prevent other classes from incrementing the counter, which could compromise its integrity:

private static void incrementCount() {
   count = count + 1;
}

It is possible to access a class method or variable from the instance side of a class without specifying the class name. As an example, you could change the constructor code to the following:

image

Even though it will work, avoid doing this. Accessing class methods without using the class name introduces unnecessary confusion in your code and is considered bad form. Is incrementCount a class method or an instance method? Since it's not possible to tell from looking at the code in the CourseSession constructor alone, the intent is not clear. The expectation that a method is an instance method when it is in reality a class method can lead to some interesting problems.

image

Scope a class method call with the class name when invoking the class method from anywhere but another class method on the same class.

Static Import

I just told you to not call class methods from the instance side unless you supply the class name. Doing so obscures where the class method is defined. The same applies for accessing class variables (not including class constants).

Java permits you to muddle things even further. Including a static import in a class allows you to use class methods or variables defined in a different class as if they were defined locally. In other words, a static import allows you to omit the class name when referring to static members defined in another class.

There are appropriate uses for static import and inappropriate uses. I'll demonstrate an inappropriate use first. Modify CourseSessionTest:

image

A static import statement looks similar to a regular import statement. However, a regular import statement imports one or all classes from a package, while a static import statement imports one or all class members (variables or methods) from a class. The above example imports all class members from the class DateUtil. Since there is only one class method in DateUtil, you could have explicitly imported just that method:

import static sis.studentinfo.DateUtil.createDate;

If DateUtil were to contain more than one class method with the name createDate (but with different argument lists), or if it also were to contain a class variable named createDate, they would each be statically imported.

Statically importing methods to avoid having to provide a class name in a few spots is lazy and introduces unnecessary confusion. Just where is createDate defined? If you are coding a class that requires quite a few external class method calls (perhaps a couple dozen or more), you might have an excuse to use static import. But a better approach would be to question why you need to make so many static calls in the first place and perhaps revisit the design of the other class.

A similar, potentially legitimate use of static import is to simplify the use of several related class constants that are gathered in a single place. Suppose you've created several report classes. Each report class will need to append new line characters to the output, so each report class will need a NEWLINE constant such as the one currently defined in RosterReporter:

static final String NEWLINE = System.getProperty("line.separator");

You don't want the duplication of defining this constant in each and every report class. You might create a new class solely for the purpose of holding this constant. Later it might hold other constants such as the page width for any report.

image

Since the NEWLINE constant will be used in a lot of places in a typical report class, you can add a static import to clean up your code a little:3

3 You can eliminate the need for the NEWLINE constant entirely in a few other ways. You'll learn about one such technique, using the Java Formatter class, in Lesson 8.

image

You can make similar changes to the RosterReporter class.

Putting a bunch of constants in a class with no behavior (methods) represents questionable OO design. Classes don't exist in a vacuum; the constants in ReportConstants class may be better off as part of another “normal” Java class, such as a class named Report.

Additional notes on static import:

• It is not possible to statically import all members from all classes in a given package in a single statement. That is, you cannot code:

import static java.lang.*;  // this does not compile!

• If a local method has the same signature as a statically imported method, the local method is called.

Use static imports with prudence. They make it more difficult to understand your classes by obscuring where members are defined. The rule of thumb is to limit use of static imports to things that are both universal and pervasive in your application.

Incrementing

In the incrementCount method, you coded:

count = count + 1;

The right-hand side of the statement is an expression whose value is one plus whatever value that count references. On execution, Java stores this new value back into the count variable.

Adding a value to a variable is a common operation, so common that Java supplies a a shortcut. The following two statements are equivalent:

count = count + 1;
count += 1;

The second statement demonstrates compound assignment. The second statement adds the value (1) on the right hand side of the compound assignment operator (+=) to the value referenced by the variable on the left hand side (count); it then assigns this new sum back to the variable on the left hand side (count). Compound assignment works for any of the arithmetic operators. For example,

rate *= 2;

is analogous to:

rate = rate * 2;

Adding the value 1 to an integer variable, or incrementing it, is so common that there is an even shorter cut. The following line uses the increment operator to increase the value of count by one:

++count;

The following line uses the decrement operator to decrease the value of count by one:

- -count;

You could code either of these examples with plus or minus signs after the variable to increment:

count++;

count- -;

The results would be the same. When they are used as part of a larger expression, however, there is an important distinction between the prefix operator (when the plus or minus signs appear before the variable) and the postfix operator (when the plus or minus signs appear after the variable).

When the Java VM encounters a prefix operator, it increments the variable before it is used as part of a larger expression.

int i = 5;
assertEquals(12, ++i * 2);
assertEquals(6, i);

When the Java VM encounters a postfix operator, it increments the variable after it is used as part of a larger expression.

int j = 5;
assertEquals(10, j++ * 2);
assertEquals(6, j);

Modify the code in CourseSession to use an increment operator. Since you're incrementing count all by itself, and not as part of a larger expression, it doesn't matter whether you use a pre-increment operator or a post-increment operator.

private static void incrementCount() {
   ++count;
}

Recompile and retest (something you should have been doing all along).

Factory Methods

You can modify CourseSession so that it supplies a static-side factory method to create CourseSession objects. By doing so, you will have control over what happens when new instances of CourseSession are created.

Modify the CourseSessionTest method createCourseSession to demonstrate how you will use this new factory method.

image

In CourseSession, add a static factory method that creates and returns a new CourseSession object:

image

Find all other code that creates a CourseSession via new CourseSession(). Use the compiler to your advantage by first making the CourseSession constructor private:

image

Replace the CourseSession constructions you find (there should be one in RosterReporterTest and one in CourseSessionTest) with message sends to the static factory method. With the CourseSession constructor declared as private, no client code (which includes test code) will be able to create an instance of CourseSession directly using a constructor. Clients must instead use the static factory method.

Joshua Kerievsky names the above refactoring “Replace [Multiple4] Constructors with Creation Methods.”5 The class methods are the creation methods. The significant benefit is that you can provide descriptive names for creation methods. Since you must name a constructor the same as the class, it cannot always impart enough information for a developer to know its use.

4 Java allows you to code multiple constructors in a class. The descriptive names of creation methods are far more valuable in this circumstance, since they help a client developer determine which to choose.

5 [Kerievsky2004].

Now that you have a factory method to create CourseSession objects, tracking the total count can be done on the static side, where it belongs. Much of good object-oriented design is about putting code where it belongs. This doesn't mean that you must always start with code in the “right place,” but you should move it there as soon as you recognize that there is a better place for it to go. It makes more sense for incrementCount message sends to be made from the static side:

image

Simple Design

Software development purists will tell you that you could have saved a lot of time by thinking through a complete design in the first place. With enough foresight, you might have figured out that static creation methods were a good idea and you would have put them in the code in the first place. Yes, after considerable experience with object-oriented development, you will learn how to start with a better design.

However, more often than not, the impact of design is not felt until you actually begin coding. Designers who don't validate their design in code frequently produce an overblown system by doing things such as adding static creation methods where they aren't warranted. They also often miss important aspects of design.

The best tactic to take is to keep your code as clean as possible at all times. The rules to keep the design clean are, in order of importance:

• Make sure your tests are complete and always running 100 percent green.

• Eliminate duplication.

• Ensure that the code is clean and expressive.

• Minimize the number of classes and methods.

The code should also have no more design in it than is necessary to support the current functionality. These rules are known as simple design.6

6 [Wiki2004b].

Simple design will give you the flexibility you need to update the design as requirements change and as you require design improvements. Creating a static factory method from a constructor wasn't all that difficult, as you saw; it is easily and safely done when you follow simple design.

Static Dangers

Using statics inappropriately can create significant defects that can be difficult to resolve. A classic novice mistake is to declare attributes as class variables instead of instance variables.

The class Student defines an instance variable named name. Each Student object should have its own copy of name. By declaring name as static, every Student object will use the same copy of name:

image

A test can demonstrate the devastating effect this will have on your code:

image

The last assertEquals statement will fail, since both studentA and studentB share the class variable name. In all likelihood, other test methods will fail as well.

A mistake like this can waste a lot of your time, especially if you don't have good unit tests. Developers often figure that something as simple as a variable declaration cannot be broken, so the variable declarations are often the last place they look when there are problems.

Revert the Student class to eliminate the keyword static, remove testBad-Static, recompile, and retest.

Using Statics: Various Notes

• Avoid turning an instance method into a class method for the sole sake of making it static. Ensure that either it semantically makes sense or at least one additional class needs access to the class method before doing so.

• Static-side collections (for example, storing an ArrayList object in a class variable) are usually a bad idea. A collection holds a reference to any object added to it. Any object added to a class collection stays there until it is either removed from the collection or until the application terminates. An instance-side collection doesn't have this problem; see the sidebar for a brief overview of how garbage collection works.

Jeff's Rule of Statics

Finally, there is what I immodestly call Jeff's Rule of Statics:

image

Don't use statics until you know you need to use statics.

The simple rule comes about from observing first Java development efforts. A little knowledge goes a long way. A little knowledge about statics often leads developers to use them rampantly.

My philosophical opposition to overuse of statics is that they are not object-oriented. The more static methods you have in your system, the more procedural it is—it becomes a bunch of essentially global functions operating on global data. My practical opposition is that improper and careless use of statics can cause all sorts of problems, including design limitations, insidious and bizarre defects, and memory leaks.

You will learn when it is appropriate to use statics and when it is not. Make sure that you do not use a static unless you understand why you are doing so.

Booleans

image

The next small portion of the student information system that you need to build is related to billing students for a semester. For now, the amount that students are billed is based upon three things: whether or not they are in-state students, whether or not they are full-time students, and how many credit hours the students are taking. In order to support billing, you will have to update the Student class to accommodate this information.

Students are either full-time or they are part-time. Put another way, students are either full-time or they are not full-time. Any time you need to represent something in Java that can be only in one of two states—on or off—you can use a variable of the type boolean. For a boolean variable, there are two possible boolean values, represented by the literals true (on) and false (off). The type boolean is a primitive type, like int; you cannot send messages to boolean values or variables.

Create a test method in the StudentTest class named testFullTime. It should instantiate a Student, then test that the student is not full time. Full-time students must have at least twelve credit hours; a newly created student has no credit hours.

public void testFullTime() {
   Student student = new Student("a");
   assertFalse(student.isFullTime());
}

The assertFalse method is another method StudentTest inherits from junit.framework.TestCase. It takes a single boolean expression as an argument. If the expression represents the value false, then the test passes; otherwise, the test fails. In testFullTime, the test passes if the student is not full-time; that is, if isFullTime returns false.

Add a method named isFullTime to the Student class:

boolean isFullTime() {
   return true;
}

The return type of the method is boolean. By having this method return true, you should expect that the test fails—the test asserts that isFullTime should return false. Observe the test fail; modify the method to return false; observe the test pass.

image

The full-time/part-time status of a student is determined by how many credits worth of courses that the student takes. To be considered full-time, a student must have at least a dozen credits. How does a student get credits? By enrolling in a course session.

image

The requirement now is that when a student is enrolled in a course session, the student's number of credits must be bumped up. Simplest things first: Students need to be able to track credits, and a newly created student has no credits.

public void testCredits() {
   Student student = new Student("a");
   assertEquals(0, student.getCredits());
   student.addCredits(3);
   assertEquals(3, student.getCredits());
   student.addCredits(4);
   assertEquals(7, student.getCredits());
}

In Student:

image

The Student constructor initializes the value of the credits field to 0, to meet the requirement that newly created students have no credits. As you learned in Lesson 2, you could have chosen to use field initialization, or to have not bothered, since Java initializes int variables to 0 by default.

Up to this point, the student should still be considered part-time. Since the number of credits is directly linked to the student's status, perhaps you should combine the test methods (but this is a debatable choice). Instead of having two test methods, testCredits and testFullTime, combine them into a single method named testStudentStatus.

image

This test should pass. Now modify the test to enroll the student in a back-breaking five-credit course in order to put them at twelve credits. You can use the assertTrue method to test that the student is now full-time. A test passes if the parameter to assertTrue is true, otherwise the test fails.

image

The test fails. To make it pass, you must modify the isFullTime method to return true if the number of credits is 12 or more. This requires you to write a conditional. A conditional in Java is an expression that returns a boolean value. Change the method isFullTime in Student to include an appropriate expression:

boolean isFullTime() {
   return credits >= 12;
}

You can read this code as “return true if the number of credits is greater than or equal to 12, otherwise return false.”

Refactor isFullTime to introduce a Student class constant to explain what the number 12 means.

image

Now that students support adding credits, you can modify CourseSession to ensure that credits are added to Student objects as they are enrolled. Start with the test:

image

A few quick changes to CourseSession make this failing test pass:

image

Tests as Documentation

The test method testStudentStatus ensures that students report the appropriate full-time or part-time status. It also ensures that the Student class correctly adds credits.

What the test does not do is exhaustively test every possibility. The general strategy for testing is to test against 0, 1, many, and any boundary conditions and any exceptional cases. With respect to student credits, the test would ensure that a student with 0, 1, or 11 credits reported as part-time and that a student with 12 or 13 credits reported full-time. It would also test the results of unexpected operations, such as adding negative credits, or adding very high numbers of credits.

Test-driven development takes a slightly different approach. The strategy is similar, but the goals are not quite the same. The tests are not just a means of ensuring that the code is correct. In addition, test-driven design provides a technique for consistently paced development. You learn how to incrementally develop code, getting feedback every few seconds or minutes that you are progressing in a valid direction. Tests are about confidence.

Test-driven development also begins to affect the design of the system you are building. This is deliberate: Test-driven development teaches you how to build systems that can be easily tested. Far too few production systems have testability as a characteristic. With test-driven development, you will learn how to test classes in isolation from other classes in the system. This leads to a system where the objects are not tightly coupled to one another, the leading indicator of a well-design object-oriented system.

Finally, tests document the functionality that your classes provide. When you finish coding, you should be able to review your tests to understand what a class does and how it does it. First, you should be able to view the names of your tests to understand all the functionality that a given class supports. Second, each test should also be readable as documentation on how to use that functionality.

image

Code tests as comprehensive specifications that others can understand.

In the case of testStudentStatus, you as the developer have a high level of confidence that the production code for isFullTime is valid. It's only a single line, and you know exactly what that line of code states:

return credits >= Student.CREDITS_REQUIRED_FOR_FULL_TIME

You might consider the test sufficient and choose to move on, and in doing so you wouldn't be entirely out of line. Again, tests are largely about confidence. The less confident you are and the more complex the code, the more tests you should write.

What about unexpected operations? Won't it destroy the integrity of a Student object if someone sends a negative value for number of credits? Remember, you are the developer building this system. You are the one who will control access to the Student class. You have two choices: you can test for and guard against every possible exceptional condition, or you can use the design of your system to make some assumptions.

In the student information system, the CourseSession class will be the only place where the Student number of credits can be incremented; this is by design. If CourseSession is coded properly, then it will have stored a reasonable number of credits. Since it will have stored a reasonable number of credits, there is theoretically no way for a negative number to be passed to Student. You know that you have coded CourseSession properly because you did it using TDD!

Of course, somewhere some human will have to enter that number of credits for a course session into the system. It is at that point—at code that represents the user interface level—that you must guard against all possibilities. A human might enter nothing, a letter, a negative number, or a $ character. The tests for ensuring that a reasonable positive integer is passed into the system will have to be made at this point.

Once you have this sort of barrier against invalid data, you can consider that the rest of the system is under your control—theoretically, of course. With this assumption, you can remove much of the need to guard the rest of your classes against bad data.

In reality, there are always “holes” that you unwittingly use to drop defects in your code. But a large key to success with test-driven development is to understand its heavy emphasis on feedback. A defect is an indication that your unit tests were not complete enough: You missed a test. Go back and write the missing test. Ensure that it fails, then fix it. Over time you will learn what is important to test and what is not. You will learn where you are investing too much time or too little time on the tests.

I have confidence in testStudentStatus and the corresponding implementation. Where the test is lacking is in its ability to act as documentation. Part of the problem is that you as a developer have too much knowledge about how you have coded the functionality. It helps to find another developer to read the test to see if it describes all of the business rules and boundaries properly. If another developer is unavailable, take a step back and try to look at the test as if you had never seen the underlying code. Does it tell you how to use the class? Does it demonstrate the different scenarios? Does it indicate the constraints or limitations of the code being tested—perhaps by omission?

In this case, perhaps all that is needed is to let the test use the same constant that Student uses. The test becomes a good deal more expressive by virtue of doing so. The boundaries of full-time are now implicit and reasonably well understood.

image

More on Initialization

image

To support the notion of in-state versus out-of-state students, the Student object needs to store a state that represents where the Student resides. The school happens to be located in the state of Colorado (abbreviation: CO). If the student resides in any other state, or if the student has no state specified (either the student is international or didn't complete the forms yet), then the student is an out-of-state student.

Here is the test:

image

The logic for determining whether a student is in-state will have to compare the String representing the student's state to the String "CO". This of course means you will need to create a field named state.

boolean isInState() {
   return state.equals(Student.IN_STATE);
}

To compare two strings, you use the equals method. You send the equals message to a String object, passing another String as an argument. The equals method will return true if both strings have the same length and if each string matches character for character. Thus "CO".equals("CO") would return true; "Aa".equals("AA") would return false.

In order for testInState to pass, it is important that the state field you create has an appropriate initial value. Anything but "CO" will work, but the empty String ("") will do just fine.

image

You might also consider writing a test to demonstrate what should happen if someone passes a lowercase state abbreviation. Right now, if client code passes in "Co", it will not set the student's status to in-state, since "Co" is not the same as "CO". While that may be acceptable behavior, a better solution would be to always translate a state abbreviation to its uppercase equivalent prior to comparing against "CO". You could accomplish this by using the String method toUpperCase.

Exceptions

What if you do not supply an initial value for the state field? In the test, you create a new Student object and immediately send it the message isInState. The isInState message results in the equals message being sent to the state field. Find out what happens if you send a message to an uninitialized object. Change the declaration of the state field to comment out the initialization:

private String state; // = "";

Then rerun the tests. JUnit will report an error, not a test failure. An error occurs when neither your production code nor your test accounts for a problem. In this case, the problem is that you are sending a message to an uninitialized field reference. The second panel in JUnit shows the problem.

image

This is known as a stack walkback, or stack trace. It provides you with the information on what went wrong, but figuring out the stack trace can take a little bit of detective work. The first line in a stack trace tells you what the problem is. In this case, you got something known as a NullPointerException. A NullPointerException is actually an error object, or exception, that is “thrown” by some underlying, problematic code.

The rest of the lines in a stack trace “walk back” through the message sends leading up to the error. Some of the lines refer to classes and methods that you have coded; other lines refer to Java system library code and third-party library code. The easiest way to decipher stack traces is to read down and find the first line of code that you recognize as being “your” code. Then keep reading lines up to the last line that you recognize. This last recognized line is the entry point into your code; you will probably want to dig down from there.

In the above example, the last line to execute was line 37 in Student.java (the line numbers in your code will likely differ). That code was invoked by the message send in StudentTest line 39. So start with StudentTest line 39 in order to follow the trail leading up to the trouble. That should take you to the following line of code:

assertFalse(student.isInState());

Taking a look at line 37 in Student.java reveals the line of your code that generated the NullPointerException:

return state.equals(Student.IN_STATE);

A reference that you do not explicitly initialize has a value of null. The value null represents the unique instance of an object known as the null object. If you send a message to the null object, you receive a NullPointerException. In this line of code, you sent the message equals to the uninitialized state reference, hence the NullPointerException.

In Lesson 6, you will see how to determine if a field is null before attempting to send a message to it. For now, ensure that you initialize String fields to the empty String ("").

Revert your code so that the state field is initialized properly. Rerun your tests.

Revisiting Primitive-Type Field Initialization

Only reference-type fields—fields that can point to objects in memory—can be initialized to null or have a value of null. You cannot initialize a variable of the primitive type to null, nor can such a variable ever have a value of null.

This includes boolean variables and variables of the numeric type (char and int, plus the rest that you'll learn about in Lesson 10: byte, short, long, float, and double). Fields of the boolean type have an initial value of false. Each of the numeric types has an initial value of 0.

Even though 0 is often a useful initial value for a numeric type, you should explicitly initialize the field to 0 if that represents a meaningful value for the field. For example, explicitly initialize a field to 0 if it represents a counter that will track values starting at 0 when the Java VM instantiates the object. If you are going to explicitly assign a more meaningful value to the field in later execution of the code, then you need not explicitly initialize the field to 0.

Provide explicit initializations only when necessary. This will help clarify what you, the developer of the code, intend.

Exercises

  1. Concatenating a new line character to the end of a string has created repetitive code. Extract this functionality to a new utility method on the class util.StringUtil. Mark StringUtil as a utility class by changing its constructor's access privilege to private. You will need to move the class constant NEWLINE to this class. Use your compiler to help you determine impacts on other code. Ensure that you have tests for the utility method.
  2. Transform your Pawn class into a more generic class named Piece. A Piece is a color plus a name (pawn, knight, rook, bishop, queen, or king). A Piece should be a value object: It should have a private constructor and no way to change anything on the object after construction. Create factory methods to return Piece objects based on the color and name. Eliminate the ability to create a default piece.
  3. Change BoardTest to reflect the complete board:

    image

  4. Ensure that a new Board creates sixteen black pieces and sixteen white pieces. Use a class counter on the Piece object to track the counts. Make sure you can run your tests twice without problems (uncheck the “Reload Classes Every Run” box on JUnit and click Run a second time).
  5. Create methods isBlack and isWhite on the Piece class (test first, of course).
  6. Gather the names of each of your test methods. Put the class name in front of each test name. Show the list to a friend and ask what the methods say about each class.
  7. Read over the section on Simple Design again. Does your current chess design follow the patterns of simple design?
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.234.214