|
|
Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs
Page 4 of 5
For more information on these terms, see the companion article to this month's column, "Object initialization in Java."
When you design a class, you should attempt to ensure that all fields declared in the class are initialized to "proper" values, no matter how that object is created. Although Java's mechanisms for object initialization can help you achieve this goal, in the end, it's up to you to be sure to use the mechanisms correctly. By themselves, these mechanisms do not guarantee that classes you design will always be properly initialized.
Fortunately, as the designer of a class, you get to decide what initial values are deemed proper. If you decide that the default values for each instance variable declared in a class are proper, you needn't have any initializers or constructors at all. The important point is that the object begin its life with an internal state that yields proper behavior from then on. As mentioned previously, this is not only a goal of initialization but also of each state transformation the object can go through during its lifetime. If all your instance variables are private, only the methods of your class can transform the state of the object. By proper design of initializers, constructors, and methods of your class, you can ensure that an object of that class will always have a proper internal state -- from the beginning of its lifetime to the end.
In the case of an instance variable for which the default value is not a proper initial state, you can take one of four approaches to initialization:
The first approach
By example:
// In Source Packet in ex2/CoffeeCup.java
// Approach 1
class CoffeeCup {
private int innerCoffee = 355;
// no constructors defined in this version of CoffeeCup
//...
}
In this example, innerCoffee is always initialized to 355, so CoffeeCup objects will always begin life with 355 milliliters of coffee in them.
The second approach
Alternatively, you could require that client programmers using the CoffeeCup class pass in an initial starting value for innerCoffee:
// In Source Packet in ex3/CoffeeCup.java
// Approach 2
class CoffeeCup {
private int innerCoffee;
// Only one constructor defined in this
// version of CoffeeCup
public CoffeeCup(int startingAmount) {
if (startingAmount < 0) {
String s = "Can't have negative coffee.";
throw new IllegalArgumentException(s);
}
innerCoffee = startingAmount;
}
//...
}
In this example, class CoffeeCup declares only one constructor, which takes an int parameter. Because a constructor is explicitly declared in CoffeeCup, the compiler won't generate a default constructor. Therefore, CoffeeCup does not have a no-arg constructor. As a result, client programmers who want to create a CoffeeCup object are forced to use the constructor that requires an int. They must supply an initial value for innerCoffee.
Checking for invalid data passed to constructors
As soon as you allow client programmers to pass data to constructors -- data which you use to calculate initial starting
values for instance variables -- you have to deal with the possibility that a client programmer will pass an invalid parameter
value. You should generally check for invalid parameter values passed to constructors. If an invalid parameter is passed,
you should most likely throw an exception. In the above example, CoffeeCup's constructor throws an IllegalArgumentException, which is an exception defined in the java.lang package. Another alternative upon detecting invalid parameter data is not to use the invalid parameter data in establishing the initial
state of the object:
// In Source Packet in ex4/CoffeeCup.java
// Also Approach 2, but a less desirable way to
// handle invalid parameters
class CoffeeCup {
private int innerCoffee;
// Only one constructor defined in this
// version of CoffeeCup
public CoffeeCup(int startingAmount) {
if (startingAmount < 0) {
innerCoffee = 0;
}
else {
innerCoffee = startingAmount;
}
}
//...
}
As in the previous example, the startingAmount parameter to this constructor is only used if it is greater than zero; however, in this example the constructor doesn't throw
an exception when it discovers that an invalid startingAmount has been passed to it. Instead, it just sets innerCoffee to zero. In general, this way of dealing with invalid parameter data passed to a constructor is less desirable than throwing
an exception, because the behavior of the constructor is more mysterious to client programmers. A constructor that either
uses passed data or throws an exception is easier to understand than one that only uses passed data some of the time. The
less a client programmer has to know about the internal implementation of a constructor, the easier it is for the client programmer
to understand how to use that constructor.
The third approach
A third approach, and one that probably makes the most sense for class CoffeeCup, is to give client programmers a choice between specifying an initial amount of coffee or using a default:
// In Source Packet in ex5/CoffeeCup.java
// Approach 3
class CoffeeCup {
private int innerCoffee;
// Only two constructors defined in this
// version of CoffeeCup
public CoffeeCup() {
}
public CoffeeCup(int startingAmount) {
if (startingAmount < 0) {
String s = "Can't have negative coffee.";
throw new IllegalArgumentException(s);
}
innerCoffee = startingAmount;
}
//...
}
In this version of CoffeeCup, the no-arg constructor has no statements because the default value of innerCoffee, zero, is a natural default amount of coffee in a cup. With this CoffeeCup class, a client programmer could either create an empty CoffeeCup using the no-arg constructor or a CoffeeCup filled with a specified amount of coffee using the constructor that takes an int parameter:
// In Source Packet in ex5/Example1.java
class Example1 {
public static void main(String[] args) {
// Create an empty coffee cup.
CoffeeCup cup1 = new CoffeeCup();
// Create a coffee cup filled with 355 ml coffee.
CoffeeCup cup2 = new CoffeeCup(355);
}
}
The third approach seems like the best way to design class CoffeeCup, because there is a "natural" value for innerCoffee, zero, which represents an empty cup. For some instance variables, however, there may not be any natural initial value. Given
that kind of instance variable, the best approach is usually the second.
Instance variables with no natural initial value
An example of an instance variable with no natural initial value is one representing the size of a cup. If you decide to
sell distinct sizes of coffee product in your virtual café -- short (8 ounce), tall (12 ounce), and grande (16 ounce) -- you
will need to model this in your solution. Rather than modeling this with three different types, such as ShortCoffeeCup, TallCoffeeCup, and GrandeCoffeeCup, you may decide to model it as an attribute of a generic CoffeeCup class. To do so, you could add a private instance variable, size, to class CoffeeCup and three public constants that define the range of values for the size field:
// In Source Packet in ex6/CoffeeCup.java
class CoffeeCup {
public static final int SHORT = 0;
public static final int TALL = 1;
public static final int GRANDE = 2;
private int size;
//...
}
When someone walks into your virtual café and orders a coffee drink without specifying one of the three sizes, the likely scenario is that you'll ask them what size they want. (You ask customers explicitly what they want because there is no default size for a coffee cup. If you guess, giving a "default-size" cup to those who don't voluntarily reveal a preferred size, you'll probably have many unhappy customers, who will say, "Oh, but I wanted a smaller size. Is it okay if I just pay for the smaller size?")
Analogously, in your solution domain, you might require that client programmers tell you what size CoffeeCup they want each time they create a CoffeeCup object. To do so, you would initialize size using the second approach from the list. Here's how you would enhance the CoffeeCup class to include a size:
// In Source Packet in ex7/CoffeeCup.java
// This class uses approach 2 for size
// and approach 3 for innerCoffee.
class CoffeeCup {
public static final int SHORT = 0;
public static final int TALL = 1;
public static final int GRANDE = 2;
public static final int MAX_SHORT_ML = 237;
public static final int MAX_TALL_ML = 355;
public static final int MAX_GRANDE_ML = 473;
private int size;
private int innerCoffee;
// Only two constructors defined in this
// version of CoffeeCup
public CoffeeCup(int size) {
this(size, 0);
}
public CoffeeCup(int size, int startingAmount) {
if ((size != SHORT) && (size != TALL)
&& (size != GRANDE)) {
String s = "Invalid cup size.";
throw new IllegalArgumentException(s);
}
if (startingAmount < 0) {
String s = "Can't have negative coffee.";
throw new IllegalArgumentException(s);
}
if (startingAmount > getMaxAmount(size)) {
String s = "Too much coffee.";
throw new IllegalArgumentException(s);
}
this.size = size;
innerCoffee = startingAmount;
}
public int add(int amount) {
innerCoffee += amount;
int max = getMaxAmount(size);
int spillAmount = 0;
if (innerCoffee > max) {
spillAmount = innerCoffee - max;
innerCoffee = max;
}
return spillAmount;
}
private static int getMaxAmount(int size) {
int retVal = 0;
switch (size) {
case SHORT:
retVal = MAX_SHORT_ML;
break;
case TALL:
retVal = MAX_TALL_ML;
break;
case GRANDE:
retVal = MAX_GRANDE_ML;
break;
default:
String s = "Invalid cup size.";
throw new IllegalArgumentException();
}
return retVal;
}
//...
}
Given this version of CoffeeCup, client programmers could create a new CoffeeCup object using either of two constructors, but in both cases, they would have to indicate a desired cup size: