The Artima Developer Community
Sponsored Link

Designing Object Initialization
Ensure Proper Initialization of Your Objects at All Times
by Bill Venners
First Published in JavaWorld, February 1998

<<  Page 5 of 7  >>

Advertisement

Strategies for object initialization
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.

Four approaches to initializing an instance variable
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:

  1. Always assign it the same proper initial state via an initializer or constructor
  2. Calculate a proper initial state from data passed to a constructor
  3. Do either of the above, depending on which constructor is used to create the object
  4. Don't assign it a proper initial state and declare the object "invalid"

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:

// In Source Packet in ex7/Example2.java
class Example2 {
    public static void main(String[] args) {

        // Create a "tall" coffee cup filled with 50 ml coffee.
        CoffeeCup cup1 = new CoffeeCup(CoffeeCup.TALL, 50);

        // Create an empty "short" coffee cup.
        CoffeeCup cup2 = new CoffeeCup(CoffeeCup.SHORT);
    }
}

This version of class CoffeeCup requires that the client programmer specify a cup size in both constructors. It doesn't have a no-arg constructor.

The fourth approach
One other option you could offer a client programmer is to create a CoffeeCup object with no specific size. Here, you would allow a client programmer to create a CoffeeCup object with a default constructor, but on creation the object would not be "valid" because it wouldn't have a valid size. Later, that programmer would have to invoke a method to give the CoffeeCup object a proper size. Until it was given a proper size, the CoffeeCup object would be "invalid" and could not be used. Here is how you would implement this scheme:

// In Source Packet in ex8/InvalidObjectException.java
class InvalidObjectException extends Exception {
}

// An illustration of approach 4
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 boolean sizeValid;
    private int size;
    private int innerCoffee;

    // Only three constructors defined in this
    // version of CoffeeCup
    public CoffeeCup() {
    }

    public CoffeeCup(int size) {
        this(size, 0);
    }

    public CoffeeCup(int size, int startingAmount) {
        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);
        }
        setSize(size);
        innerCoffee = startingAmount;
    }

    public final void setSize(int size) {
        if ((size != SHORT) && (size != TALL)
            && (size != GRANDE)) {
            String s = "Invalid cup size.";
            throw new IllegalArgumentException(s);
        }
        sizeValid = true;
        this.size = size;
    }

    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;
    }

    public int add(int amount) throws InvalidObjectException {

        if (!sizeValid) {
            throw new InvalidObjectException();
        }
        innerCoffee += amount;
        int max = getMaxAmount(size);
        int spillAmount = 0;
        if (innerCoffee > max) {
            spillAmount = innerCoffee - max;
            innerCoffee = max;
        }
        return spillAmount;
    }
    //...
}

In this example, sizeValid indicates whether or not instance variable size has been set to a proper value. If the object is constructed with either of the two constructors that include a size in their parameter list (and the passed size is valid), sizeValid is set to true. If the object is created with the no-arg constructor, the size is not specified and sizeValid is left at its default initial value, false. Later, the size variable can be set to a proper value via the setSize() method, which also sets sizeValid to true.

This is not a very appropriate design for class CoffeeCup. Although you will probably encounter some situations in which it makes sense to design a class such that its objects can at times be invalid, generally you should avoid this approach. As a guideline, you should attempt to design classes such that their instances are always valid, from the beginning to the end of their lifetimes. To follow this guideline, you either initialize instance variables to default proper values or require that the client programmer provide data in constructor parameters from which you can calculate proper initial values.

Objects that can have invalid states are harder to use and harder to understand than those that are always valid. Usually, when an object is in an invalid state, many of its methods won't work. In the example above, the add() method doesn't work when the CoffeeCup doesn't have a valid size. Here, the add() method uses both the size and innerCoffee variables to determine how much more coffee can legally be added to the cup. If the amount of coffee passed as a parameter to add() plus the amount of coffee already in the cup exceeds the maximum capacity of the cup, the add() method will "spill" the extra coffee back to the caller through its return value. When sizeValid is false, the add() method can't determine the maximum capacity of the cup, because it doesn't know the cup's size. Hence, the add() method won't work when the size variable is invalid.

If you do design a class that has invalid states, you should throw an exception when a method can't perform normally because its object is invalid. In the example above, the add() method throws an InvalidObjectException exception if sizeValid is false. This exception clearly indicates to the caller that the add() method did not work and why.

<<  Page 5 of 7  >>


Sponsored Links



Google
  Web Artima.com   
Copyright © 1996-2017 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use - Advertise with Us