Designing Object Initialization

Ensure Proper Initialization of Your Objects at All Times

by Bill Venners
February 15, 1998

First published in JavaWorld, February 1998
Summary
This installment of the Design Techniques column begins with a quick look at object-design fundamentals, then goes on to discuss various approaches to designing initializers and constructors so as to facilitate the proper initialization of objects.

With this installment of Design Techniques, I begin a series of articles that will focus on the design of classes and objects. In this series I will discuss the following: designing classes for proper object initialization, finalization, and cleanup; designing fields and methods, in general; and designing class fields and methods, in particular. I won't address the design of class hierarchies in these first few installments -- rather, I will focus only on the design of individual classes.

Java and basic software design principles
In the first few installments of this column series, I plan to cover some basic object-oriented and structured design techniques as they apply to Java. Many of you undoubtedly are already familiar with these techniques, as they apply equally well to other languages. In my experience in the cubicle, however, I have encountered a lot of code written by programmers who, shall we say, could stand to take a refresher course on the basics. So I think it is important to cover the basics in the early articles of this column.

To maximize the usefulness of these first articles, I will be focusing on how the basic software design principles apply to Java in light of Java's architecture. For example, when I write about designing objects for proper cleanup, I'll discuss Java's finalization and garbage collection mechanisms and show how they affect design. When I write about designing with class variables and class methods, I'll describe Java's mechanisms for class loading and unloading and show how they affect design. For this article, which looks at designing objects for proper initialization, I've written an entire companion article that describes how object initialization works in the Java virtual machine (JVM). In all the articles of this column, I hope to show how the respective architectures of the Java language, virtual machine, and API affect how you should think about designing Java programs.

Viewing objects as finite state machines
One way to think of objects is as finite state machines. Thinking of objects in this way as you design classes can help you acquire a mindset that is conducive to good object design.

For example, one finite state machine is a simple traffic light that has three states: red, yellow, and green. A state transition diagram for such a traffic light is shown in Figure 1 (a Java applet).

You need a Java-enabled browser to see this applet. State-transition diagram for a traffic light State-transition diagram for a traffic light

In Figure 1, states are represented by labeled circles, state changes by arrows, and events by labels next to the arrows. When the finite state machine experiences an event, it responds by performing the state change indicated by the arrow.

The finite state machine shown in Figure 1 could be represented by instances of the following Java class. (As a matter of fact, the following class is used to represent the traffic light finite state machine in the Java applet that is Figure 1. Click here to view the full source code of the traffic light applet.)

public class TrafficLight {

    public static final int RED = 0;
    public static final int YELLOW = 1;
    public static final int GREEN = 2;

    private int currentColor = RED;

    public int change() {

        switch (currentColor) {

        case RED:
            currentColor = GREEN;
            break;

        case YELLOW:
            currentColor = RED;
            break;

        case GREEN:
            currentColor = YELLOW;
            break;
        }

        return currentColor;
    }

    public int getCurrentColor() {
        return currentColor;
    }
}

In class TrafficLight, the state of the object is stored in the currentColor private instance variable. A "change" event is sent to the object by invoking its public instance method, change(). State changes are accomplished through the execution of the code that implements the change() method.

As you can see from the TrafficLight example, Java objects map to finite state machines in the following ways:

  • The range of possible values that can be stored in the object's instance variables map to the finite state machine's states
  • An invocation of an object's instance method maps to a finite state machine receiving an event
  • Executing an object's instance method maps to the state change that a finite state machine experiences as the result of an event

Most objects you design will have many more states than a TrafficLight object. Some objects will have instance variables whose ranges are limited only by available resources, such as memory, rendering objects that are practically "infinite state machines." But in general, it is helpful to think of objects as state machines and to design them accordingly.

The canonical object design
The TrafficLight class is an example of the generally accepted form of a basic object design. The object has state, represented by instance variables that are private. The only way that code defined in other classes can affect the object's state is by invoking the object's instance methods.

Because other classes don't have direct access to the currentColor variable, they can't screw up its value, such as by setting it equal to 5. (In this case, the only valid values for currentColor are 0, 1, and 2.) In addition, there is no way for a TrafficLight object to go directly from state YELLOW to state GREEN, GREEN to RED, or RED to YELLOW.

Given this design of class TrafficLight, a TrafficLight object will always have a valid state and will always experience valid state transitions -- from the beginning of its lifetime to the end.

Designing objects for flexibility and robustness
Why should you care about designing good objects? One reason is that a set of robust objects can help contribute to the overall robustness of the program they constitute. In addition, well designed objects are more flexible (easier to understand and easier to change) than poorly designed objects.

A fundamental way to make your object designs robust and flexible is by ensuring that your objects have a valid state, and experience only valid state transitions, from the beginning of their lifetimes to the end. The rest of this article will discuss ways to ensure that classes begin their lifetimes with a valid state.

Introducing class CoffeeCup and the virtual café
The discussion of object design in this and subsequent articles will make use of a class that models coffee cups. You can imagine using this class in a program that implements a "virtual café": a place in cyberspace where guests can sit at small tables, sipping virtual cups of coffee and chatting with one another. The primary function of the café is that of a chat room, where people separated by (potentially vast) physical distances yet connected to the same network come together to converse. To make your chat room more compelling, you want it to look like a caf&eacute. You want each participant to see graphical representations ("avatars") of the other people in the caf&eacute. And, to make the participants' experience more real, you want the people to be able to interact with certain items in the café, such as tables, chairs, and cups of coffee.

The CoffeeCup
The basic CoffeeCup class has one instance variable, innerCoffee, which keeps track of the number of milliliters of coffee contained in the cup. This variable maintains your virtual coffee cup's state. The following methods allow you to change its state by:

  • Adding coffee to the cup (the add() method)
  • Removing one sip of coffee from the cup (the releaseOneSip() method)
  • Spilling the entire contents of the cup (the spillEntireContents() method)

Here is a class that represents a simple coffee cup in a virtual caf&eacute:

// In Source Packet in ex1/CoffeeCup.java
class CoffeeCup {

    private int innerCoffee;

    public void add(int amount) {
        innerCoffee += amount;
    }

    public int releaseOneSip(int sipSize) {
        int sip = sipSize;
        if (innerCoffee < sipSize) {
            sip = innerCoffee;
        }
        innerCoffee -= sip;
        return sip;
    }

    public int spillEntireContents() {
        int all = innerCoffee;
        innerCoffee = 0;
        return all;
    }
}

Some definitions
Before getting started with a discussion of design guidelines for object initialization, I'd like to clarify a few terms.

Designer vs. client programmers
If you're like most Java programmers, you alternate between two hats, which you wear at different times. Sometimes you wear your "designer" hat and build libraries of classes for others to use; other times you wear your "client" hat and make use of a library of classes created by someone else. Some Java programmers -- completely oblivious to the rules of fashion -- are known to wear both hats at the same time.

One aspect of the flexibility of a body of code is the ease with which a client programmer can understand the code. Whether a client programmer is planning to change code or just use it as is, that programmer often has to figure out how to change or use the code by reading it.

The guidelines discussed in the remainder of this article, and in subsequent articles of this column, will talk about flexibility in terms of client programmers. Designs and implementations that are flexible are those that are easy for client programmers to understand, use, and change.

Java jargon related to initialization
This article uses several terms related to initialization that are defined in precise ways by the Java Language Specification (JLS).

  • Default values are the values given to instance (and class) variables when they are first allocated on the heap -- before any initialization code is executed
  • A no-arg constructor is a constructor that takes no arguments
  • A default constructor is a no-arg constructor generated implicitly by the compiler for classes that don't have any constructors explicitly declared in the source file
  • An instance variable initializer is an equals sign (=) and expression sitting between an instance variable declaration and its terminating semicolon
  • An instance initializer is a block of code executed during object initialization in textual order along with instance variable initializers

For more information on these terms, see the companion article to this month's column, "Object initialization in Java."

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.

Keeping initialization focused
One final object initialization guideline involves the actual activities of constructors and initializers.

As a C++ programmer, I encountered a class one day that had no methods and no fields -- only a constructor. In trying to determine what this constructor did, I found that it called a function that called a function that called a function, and so on. After looking through several files, I realized that instantiating an object of this class caused a file to be parsed. After that the class was useless.

Granted, this particular class was designed by someone new to object-oriented programming, but its design illustrates an important point relating to constructors and initializers in Java: constructors and initializers should be about initialization.

In a previous section of this article, I recommended that you strive to design objects that give client programmers no way to instantiate the object in an invalid state. Another way to look at this is that client programmers should not have to do anything besides invoke a constructor to get an object in a proper initial state. An object should be fully initialized by the constructors and initializers of its class.

Likewise, just as objects should not be less than initialized after a constructor invocation, they also shouldn't be more than initialized. As a general principle, constructors and initializers should do no more than bring the new object to a proper initial state. To get the object moving after construction has brought it to a proper initial state, you should require that client programmers invoke a method on the object.

Conclusion
Two important design guidelines this article attempts to promote are the "canonical object design" and "omni-valid state principle."

The canonical object design
An object should have state, represented by instance variables that are private. Invoking an object's instance methods should be the only way code defined in other classes can affect the object's state.

The omni-valid state principle
Objects should have a valid state, and experience only valid state transitions, from the beginning of their lifetimes to the end.

With respect to object initialization, this article suggests a number of handy guidelines:

  • The no-way-to-create-an-object-with-an-invalid-state guideline -- Design an object's initializers and constructors such that the object cannot possibly be created in an invalid state.
  • The not-natural rule of thumb -- If an instance variable doesn't have a natural default value, force client programmers to pass an initial value (or data from which an initial value can be calculated) to every constructor in the class.
  • The exceptional rule concerning bad constructor input -- Check for invalid data passed to constructors. On detecting invalid data passed to a constructor, throw an exception.
  • The valid guideline about invalid objects -- Sometimes it will make sense to design a class whose instances can, at times, be in an invalid, unusable state. For such classes, throw an exception when a method can't perform its normal duties because the object's state was invalid when the method was invoked.
  • The hold-your-horses-during-initialization guideline -- Constructors and initializers should be about initialization, the whole initialization, and nothing but initialization.

One last guideline pertains to the nature of guidelines themselves. The guidelines proposed in this column are not proposed as laws you should blindly follow at all times but as rules of thumb you'll probably want to follow much of the time. They are intended to help you acquire a mindset conducive to good design. Thus, the final guideline is:

  • The guideline guideline -- All guidelines proposed by this column should be disregarded some of the time -- including this one.

Next month
In next month's Design Techniques, I'll continue this mini-series of articles focusing on designing classes and objects. Next month's installment, the second of this series, will offer field and method design guidelines.

A request for reader participation
Software design is subjective. Your idea of a well-designed program may be your colleague's maintenance nightmare. In light of this fact, I hope to make this column as interactive as possible.

I encourage your comments, criticisms, suggestions, flames -- all kinds of feedback -- about the material presented in this column. If you disagree with something, or have something to add, please let me know by e-mailing me at bv@artima.com.

Resources

This article was first published under the name Designing Object Initialization in JavaWorld, a division of Web Publishing, Inc., February 1998.

Talk back!

Have an opinion? Be the first to post a comment about this article.

About the author

Bill Venners has been writing software professionally for 12 years. Based in Silicon Valley, he provides software consulting and training services under the name Artima Software Company. Over the years he has developed software for the consumer electronics, education, semiconductor, and life insurance industries. He has programmed in many languages on many platforms: assembly language on various microprocessors, C on Unix, C++ on Windows, Java on the Web. He is author of the book: Inside the Java Virtual Machine, published by McGraw-Hill.