http://www.developer.com/

Back to article

Putting an Object in a Safe State


January 26, 2005

Introduction

This is the ninth installment in a series of articles about fundamental object-oriented (OO) concepts. The Object-Oriented Thought Process is intended for anyone who needs to understand the basic object-oriented concepts before jumping into the code. Click here to start at the beginning of the series.

In keeping with the code examples used in the previous articles, Java will be the language used to implement the concepts in code. One of the reasons that I like to use Java is because you can download the Java compiler for personal use at the Sun Microsystems Web site http://java.sun.com/. You can download the J2SE 1.4.2 SDK (software development kit) to compile and execute these applications and will provide the code listings for all examples in this article. I have the SDK 1.4.0 loaded on my machine. I will provide figures and the output (when appropriate) for these examples. See the previous articles in this series for detailed descriptions for compiling and running all the code examples in this series.

In the last column we continued our discussion on data hiding, as in data protection. When designed properly, encapsulation provides a built-in way to enforce certain levels of data protection. Yet, protecting the integrity of the data does not ensure the integrity of the object itself.

A goal of every object-oriented design is to put the object into what is called a safe-state. By this we mean that the object is in a state that will not cause the system to be unstable. There are several mechanisms that are provided to assist in the construction of safe objects. In this article we explore some of the topics.

Constructors

Constructors are a new concept for people doing structured programming. Constructors do not normally exist in non-O-O languages such as C and Basic. Earlier we spoke about special methods that are used to construct objects. In Java and C++, as well as other O-O languages, constructors are methods that share the same name as the class and have no return type. For example, a constructor for the Cabbie class would look like this:

public Cabbie(){   /* code to construct the object */}

The compiler will recognize that the method name is identical to the class name and consider the method a constructor.

Return Value

Note again that a constructor does not have a return value. If you provide a return value, the compiler will not treat the method as a constructor.

When Is a Constructor Called?

When a new object is created, one of the first things that happens is that the constructor is called. Check out the following code:

Cabbie myCabbie = new Cabbie();

The new keyword creates a new instance of the Cabbie class, thus allocating the required memory. Then the constructor is called, passing the arguments in the parameter list. The developer must do the appropriate initialization within the constructor.

Thus, the code new Cabbie() will instantiate a Cabbie object and call the Cabbie method, which is the constructor.

What's Inside a Constructor?

The most important function of a constructor is to initialize the memory allocated when the new keyword is encountered. In short, code included inside a constructor should set the newly created object to its initial, stable state.

For example, if you have a counter object with an attribute called count, you need to set count to zero in the constructor:

count = 0;

Initializing Attributes

Initializing attributes is a common function performed within a constructor.

The Default Constructor

If you write a class and do not include a constructor, the class will still compile and you can still use it. If the class provides no explicit constructor, such as in C++ and Java, a default constructor will be provided.

The default constructor calls only the constructor of the superclass. For example, if a constructor is not provided for the Cabbie class, the following default constructor is inserted:

public Cabbie(){   super();}

Perhaps the default constructor may be sufficient in some cases; however, in most cases some sort of memory initialization should be performed. Regardless of the situation, it is good programming practice to always include at least one constructor in a class. In any case, if there are attributes in the class, they should be initialized in a constructor.

Providing a Constructor

The rule of thumb is that you should always provide a constructor, even if you do not plan on doing anything inside it. You can provide a constructor with nothing in it and then add to it later. While there is technically nothing wrong with using the default constructor provided by the compiler, it is always nice to know exactly what your code looks like.

Using Multiple Constructors

In many cases, an object can be constructed in more than one way. To accommodate this situation you need to provide more than one constructor. For example, let's consider the Count class presented below.

public class Count {   int count;   public Count(){      count = 0;   }}

On the one hand we simply want to initialize the attribute count to count to zero: We can easily accomplish this by having a constructor initialize count to zero as follows:

public Count(){   count = 0;}

On the other hand, we might want to pass an initialization parameter that allows count to be set to various numbers:

public Count (int number){   count = number;}

This is called overloading a method (overloading pertains to all methods, not just constructors). Most O-O languages provide functionality for overloading a method.

Overloading Methods

Overloading allows a programmer to use the same method name over and over, as long as the signature of the method is different each time. The signature consists of the method name and a parameter list (see Figure 1).

Thus, the following methods all have different signatures:

public void getCab();// different parameter listpublic void getCab (String cabbieName);// different parameter listpublic void getCab (int numberOfPassengers);

Figure 1: The components of a signature.

By using different signatures, you can construct objects differently depending on the constructor used.

Using UML to Model Classes

As an example, let's consider that we have two ways to construct a database reader class:

  • Pass the name of the database and position the cursor at the beginning of the database.
  • Pass the name of the database and the position within the database where we want the cursor to position itself.

Figure 2 shows a class diagram for the DataBaseReader class. Note that the diagram lists two constructors for the class. While the diagram shows the two constructors, without the parameter list there is no way to know which constructor is which. To distinguish the constructors, you can look at the corresponding code listed below.

Figure 2: The DataBaseReader class diagram.

No Return Type

Notice that in this class diagram the constructors do not have a return type. All other methods besides constructors must have return types.

Here is a code segment of the class that shows its constructors and the attributes that the constructors initialize (see Figure 3):

public class DataBaseReader {   String DBName;   int startPosition;   // initialize just the name   public DataBaseReader (String name){   DBName = name;};   // initialize the name and the position   public DataBaseReader (String name, int pos){      DBName = name;      startPosition = pos;   };   .. // rest of class}



Click here for a larger image.

Figure 3: Creating a new object.

How the Superclass Is Constructed

When using inheritance, you must know how the parent class is constructed. Remember that when you use inheritance, you are inheriting everything about the parent. Thus, you must become intimately aware of all the parent's data and behavior. The inheritance of an attribute is fairly obvious. However, how a constructor is inherited is not as obvious. After the new keyword is encountered and the object is allocated, the following steps occur (see Figure 4):

  1. The first thing that happens inside the constructor is that the constructor of the class's superclass is called.
  2. Then each class attribute of the object is initialized. These are the attributes that are part of the class definition (instance variables), not the attributes inside the constructor or any other method (local variables). In the DataBaseReader code presented earlier, the integer startposition is an instance variable of the class.
  3. Then the rest of the code in the constructor executes.



Click here for a larger image.

Figure 4: Constructing an object.

The Design of Constructors

When designing a class, it is good practice to initialize all the attributes. In some languages, the compiler might attempt to do this initialization. As always, don't count on the compiler to initialize attributes! In Java, you cannot use an attribute until it is initialized. If the attribute is first set in the code, make sure that you initialize the attribute to some valid condition, for example, set an integer to zero.

Constructors are used to ensure that the application is in a stable state. For example, initializing an attribute to zero, when it is intended for use as a denominator in a division operation, may lead to an unstable application. You must take into consideration the fact that a division by zero is an illegal operation.

During the design, it is good practice to identify a stable state for all attributes and then initialize them to this stable state in the constructor.

Error Handling

It is rare for a class to be written perfectly the first time. In most, if not all, situations, things will go wrong. Any designer who does not plan for problems is courting danger.

Assuming that your code has the ability to detect and trap an error condition, you can handle the error in several different ways: In a training class based on their book Java Primer Plus, page 223, by Tyma, Torok and Downing, the authors state that there are three basic solutions to handling problems that are detected in a program: fix it, ignore the problem by squelching it, and exit the runtime in some graceful manner. Gilbert and McCarty, in their book Object-Oriented Design in Java, on page 139, basically state the same thing but add the choice of throwing an exception:

  • Ignore the problem—not a good idea!
  • Check for potential problems and abort the program when you find a problem.
  • Check for potential problems, catch the mistake, and attempt to fix the problem.
  • Throw an exception. (This is the preferred way to handle the situation)

These strategies are discussed in the following sections.

Ignoring the Problem

Simply ignoring a potential problem is a recipe for disaster. And if you are going to ignore the problem, why bother detecting it in the first place? The bottom line is that you should not ignore the problem. The primary directive for all applications is that the application should never crash. If you do not handle your errors, the application will eventually terminate ungracefully or continue in a mode that can be considered an unstable state. In the latter case, you might not even know it for some period of time.

Checking for Problems and Aborting the Application

If you choose to check for potential problems and abort the application when a problem is detected, whenever a problem is detected, the application displays a message saying that you have a problem. Then the code gracefully exits and the user is left staring at the computer screen, shaking his or her head and wondering what bus just happened. While this is a far superior option to ignoring the problem, it is by no means optimal. However, this does allow the system to clean up things and put itself in a more stable state, such as closing files.

Checking for Problems and Attempting to Recover

Checking for potential problems, catching the mistake, and attempting to recover is a tremendously better solution than simply checking for problems and aborting. In this case, the problem is detected by the code, and the application attempts to fix itself. This works well in certain situations. For example, consider the following code:

if (a == 0)   a=1;c = b/a;

It is obvious that if the if statement is not included in the code, and a zero makes its way to the divide statement, you will get a system exception because you cannot divide by zero. By catching the exception and setting a to 1, at least the system will not crash. However, setting a to 1 may not be a proper solution. You may need to prompt the user for the proper input value.

Although this means of error checking is preferable to the previous solutions, it still has a few potentially limiting problems. It is not always easy to determine where a problem first appears. And it might take a while for the problem to be detected. In any event, it is beyond the scope of this article to explain error handling in great detail. However, it is important to design error handling into the class right from the start.

Throwing an Exception

Most O-O languages provide a feature called exceptions. In the most basic sense, exceptions are errors that occur within a system. Exceptions provide a way to detect problems and then handle them. In Java and C++, exceptions are handled by the keywords catch and throw. This may sound like a baseball game, but the key here is that a specific block of code is written to handle a specific exception. This solves the problem of trying to figure out where the problem started and unwinding the code to the proper point.

Here is how the code for a try/catch block looks:

try {   // possible nasty code} catch(Exception e) {   // code to handle the exception}

If an exception is thrown within the try block, the catch block will handle it. When an exception is thrown while the block is executing, the following occurs:

  • The execution of the block is terminated.
  • The catch clauses are checked to determine whether an appropriate catch block for the offending exception was included (there may be more than one catch clause).
  • If none of the catch clauses handles the offending exception, then it is passed to the next higher-level try block (if the exception is not caught in the code, then the system ultimately catches it and the results are unpredictable).
  • If a catch clause is matched (the first match encountered), the statements in the catch clause are executed.
  • Then execution resumes with the statement following the try block.

Again, it is beyond the scope of this article to explain exception handling in great detail. Suffice it to say that exceptions are an important advantage for O-O programming languages. Here is an example of how an exception is caught in Java:

try {   // possible nasty code   count = 0;   count = 5/count;} catch(ArithmeticException e) {   // code to handle the exception   System.out.println(e.getMessage());   count = 1;}System.out.println("The exception is handled.");

Exception Granularity

You can catch exceptions at various levels of granularity. You can catch all exceptions or just check for specific exceptions, such as arithmetic exceptions. If your code does not catch an exception, the Java runtime willand it won't be happy about it!

In this example, the division by zero (because count is equal to 0) within the try block will cause an arithmetic exception. If the exception was generated (thrown) outside a try block, then the program would most likely have been terminated. However, because the exception was thrown within a try block, the catch block is checked to see if the specific exception (in this case, an arithmetic exception) was planned for. Because the catch block contains a check for the arithmetic exception, the code within the catch block is executed, thus setting count to 1. After the catch block executes, the try/catch block is exited and the message, The exception is handled. appears on the Java console (see Figure 5).



Click here for a larger image.

Figure 5: Catching an exception.

If you had not put ArithmeticException in the catch block, then the program would likely have crashed. You can catch all exceptions by using the following code:

try {   // possible nasty code} catch(Exception e) {   // code to handle the exception}

The Exception parameter in the catch block is used to catch any exception that might be generated within a try block.

Bulletproof Code

It's a good idea to use a combination of the methods described here to make your program as bulletproof as possible to your user.

The Concept of Scope

Multiple objects can be instantiated from a single class. Each of these objects has its own identity and state. This is an important point. Each object is constructed separately and is allocated its own memory. However, some attributes and methods may be shared by all the objects instantiated from the same class, thus sharing the memory allocated for these class attributes and methods.

A Shared Method

A constructor is a good example of a method that is shared by all instances of a class.

Although a method generally represents the behavior of an object, the state of the object is normally represented by attributes. There are three types of attributes:

  • Local attributes
  • Object attributes
  • Class attributes

Local Attributes

Local attributes are local to a specific method. Consider the following code:

public class Number {   public method1() {      int count;   }   public method2() {   }}

The method method1 contains a local variable called count. This integer is accessible only inside method1. The method method2 has no idea that the integer count exists.

At this point we can touch on a very important concept: scope. Attributes exist within a particular scope. In this case, the integer count exists within the scope of method1. In Java and C++, scope is delineated by curly braces ({}). In the Number class, there are several possible scopes—just start matching the curly braces.

The class itself has its own scope. Each instance of the class (that is, each object) has its own scope. Both method1 and method2 have their own scopes as well. Because count lives within method1's curly braces, when method1 is invoked, a copy of count is created. When method1 terminates, the copy of count is deleted.

For some more fun, look at this code:

public class Number {   public method1() {      int count;   }   public method2() {      int count;   }}

How can this be? There are two copies of an integer count in this class. Remember that method1 and method2 each has its own scope. Thus, the compiler can tell which copy of count to access simply by recognizing which method it is in. You can think of it in these terms:

method1->count;method2->count;

As far as the compiler is concerned, the two attributes are easily differentiated, even though they have the same name. It is almost like two people having the same last name, but based on the context of their first names, you know that they are two separate individuals.

Object Attributes

There are many design situations in which an attribute must be shared by several methods within the same object. In Figure 6, for example, three objects have been constructed from a single class. Consider the following code:

public class Number {   int count;   public method1() {      count = 1;   }   public method2() {      count = 2;   }}



Click here for a larger image.

Figure 6: Object attributes.

In this case, the attribute count is declared outside the scope of both method1 and method2. However, it is within the scope of the class. Thus, count is available to both method1 and method2 (basically all methods in the class have access to this attribute). Notice that the code for both methods is setting count to a specific value. There is only one copy of count for the entire object, so both assignments operate on the same copy in memory. However, this copy of count is not shared between different objects.

To illustrate, let's create three copies of the Number class:

Number Number1 = new Number ();Number Number2 = new Number ();Number Number3 = new Number ();

Each of these object, Number1, Number2, and Number3, is constructed separately and is allocated its own resources. There are actually three separate instances of the integer a. When Number1 changes its attribute count, this in no way affects the copy of count in object Number2 or object Number3. In this case, integer count is an object attribute.

You can play some interesting games with scope. Take a look at the following code:

public class Number {   int count;   public method1() {      int count;   }   public method2() {      int count;   }}

In this case there are actually three copies of count. The object has one copy, and method1() and method2() each have a copy of their own.

To access the object variable from within one of the methods, say method1(), you can use the following code:

public method1() {   int count;      this.count = 1;   }

Notice that there is some code that looks a bit weird:

this.count = 1;

The selection of the word this as a keyword is, perhaps unfortunate. However, we must live with it. The use of the this keyword directs the compiler to access the object variable count, and not the local variables within the method bodies.

The this Keyword

In Java, the keyword this is a reference to the current object.

Class Attributes

As mentioned earlier, it is possible for two or more objects to share attributes. In Java and C++ you do this by making the attribute static:

public class Count {   static int count;   public method1() {   }}

By declaring count as static, this attribute is allocated a single piece of memory for the class. Thus, all objects of the class use the same memory location for a. Essentially, each class has a single copy, which is shared by all objects of that class (see Figure 7).

Figure 7: Class attributes.

There are many valid uses for class attributes; however, you must be aware of potential synchronization problems. Let's instantiate two Count objects:

Count Count1 = new Count();Count Count2 = new Count();

For the sake of argument, let's say that the object Count1 is going merrily about its way and is using count as a means to keep track of the pixels on a computer screen. This is not a problem until the object Count2 decides to use attribute count to count sheep. The instant that Count2 records its first sheep, the data that Count1 was saving is lost.

Operator Overloading

Some O-O languages allow you to overload an operator. C++ is an example of one such language. Operator overloading allows you to change the meaning of an operator. For example, most people, when they see a plus sign, assume that it represents addition. If you see this equation

X = 5 + 6;

you expect that X would contain the value 11. And in this case, you would be correct.

However, there are times when a plus sign could represent something else. For example, in the following code:

String firstName = "Joe", lastName = "Smith";String Name = firstName + " " + lastName;

You would expect that Name would contain Joe Smith. The plus sign here has been overloaded to perform string concatenation.

String Concatenation

String concatenation is when two strings are combined to create a single string.

In the context of strings, the plus sign does not mean addition of integers or floats, but concatenation of strings.

What about matrix addition? You could have code like this:

Matrix A, B, C;C = A + B;

Thus, the plus sign now performs matrix addition, not addition of integers or floats.

Overloading is a powerful mechanism. However, it can be downright confusing for people who read and maintain code. In fact, developers can confuse themselves.

Java does not allow the option of overloading operators. The language itself does overload the plus sign for string concatenation, but that is it. The designers of Java must have decided that operator overloading was more of a problem than it was worth. If you must use operator overloading, take care not to confuse the people who will use the class.

Multiple Inheritance

As the name implies, multiple inheritance allows a class to inherit from more than one class. In practice this seems like a great idea. Objects are supposed to model the real world, are they not? And there are many real-world examples of multiple inheritance. Parents are a good example of multiple inheritance. Each child has two parents—that's just the way it is. So it makes sense that you can design classes by using multiple inheritance. And in some O-O languages, such as C++, you can.

However, this situation falls into a category similar to operator overloading. Multiple inheritance is a very powerful technique, and in fact, some problems are quite difficult to do without it. Multiple inheritance can even solve some problems quite elegantly. However, multiple inheritance can significantly increase the complexity of a system.

As with operator overloading, the designers of Java decided that the increased complexity of allowing multiple inheritance far outweighed its advantages, so they eliminated it from the language. In some ways, the Java language construct of interfaces compensates for this; however, the bottom line is that Java does not allow conventional multiple inheritance.

Behavioral and Implementation Inheritance

Java interfaces are a mechanism for behavioral inheritance, whereas abstract classes are used for implementation inheritance. The bottom line is that Java interfaces provide interfaces, but no implementation, whereas abstract classes may provide both interfaces and implementation.

Object Operations

Some of the most basic operations in programming become more complicated when you're dealing with complex data structures and objects. For example, when you want to copy primitive data types, the process is quite straightforward. However, copying objects is not quite as simple. In his book Effective C++, on page 34, Scott Meyers devotes an entire section to copying and assigning objects.

Classes and References

The problem with complex data structures and objects is that they may contain references. Simply making a copy of the pointer does not copy the data structures or object that it references.

The problems arise when comparisons and copies are performed on objects. Specifically, the question boils down to whether or not you follow the pointers or not. For example, there should be a way to copy an object. Again, this is not as simple as it may seem. Since objects may contain references, these reference trees must be followed to do a valid copy (if you truly want to do a deep copy).

Deep Versus Shallow Copies

A deep copy is when all the references are followed and new copies are created for all referenced objects. There may be many levels involved in a deep copy. For objects with references to many objects, which in turn may have references to even more objects, the copy itself can create significant overhead. A shallow copy would simply copy the reference and not follow the levels. Gilbert and McCarty have a good discussion about what shallow and deep hierarchies are on page 265 of Object-Oriented Design in Java in a section called "Prefer a Tree to a Forest."

To illustrate, in Figure 8, if you just do a simple copy of the object (called a bitwise copy), then any object that the primary object references will not be copied—only the references will be copied. Thus, both objects (the original and the copy) will point to the same objects. To perform a complete copy, in which all reference objects are copied, you have to write the code to create all the sub-objects.



Click here for a larger image.

Figure 8: Following object references.

This problem also manifests itself when comparing objects. As in the copy function, this is not as simple as it may seem. Because objects contain references, these reference trees must be followed to do a valid comparison of objects. In most cases, languages provide a default mechanism to compare objects. As is usually the case, do not count on the default mechanism. When designing a class, you should consider providing a comparison function in your class that you know will behave, as you want it to.

Conclusion

This article covers a number of advanced O-O concepts that, although perhaps not vital to a general understanding of O-O concepts, are quite necessary in the higher-level O-O tasks such as designing a class and putting an object in a safe-state.

References

Gilbert, Stephen, and Bill McCarty: Object-Oriented Design in Java. The Waite Group, 1998.

Tyma, Paul, Gabriel Torok and Troy Downing: Java Primer Plus. The Waite Group, 1996.

Meyers, Scott: Effective C++. Addison-Wesley, 1992.

About the Author

Matt Weisfeld is a faculty member at Cuyahoga Community College (Tri-C) in Cleveland, Ohio. Matt is a member of the Information Technology department, teaching programming languages such as C++, Java, and C# .NET as well as various web technologies. Prior to joining Tri-C, Matt spent 20 years in the information technology industry gaining experience in software development, project management, business development, corporate training, and part-time teaching. Matt holds an MS in computer science and an MBA in project management. Besides the first edition of The Object-Oriented Thought Process, Matt has published two other computer books, and more than a dozen articles in magazines and journals such as Dr. Dobb's Journal, The C/C++ Users Journal, Software Development Magazine, Java Report, and the international journal Project Management. Matt has presented at conferences throughout the United States and Canada.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date