An Object Orientation
Overview
Java is an object-oriented language. You have seen the object part by now—classes are instantiated into one or more objects, each with its own unique memory yet with common variable definitions and methods. This alone is a great thing, but there is more to being object-oriented than just this. There are more capabilities in OO and more syntax in Java to support those capabilities. You will find the syntax easy, but you will probably struggle with the question of why, and the question of how best to design code to exploit the power of OO. Not to worry: we all do this when we go from procedural to OO design. The most important thing for you to know now is the syntax, so that you can read and use other people's code effectively, and so that you can start writing code today. With time, experience, and more reading, the questions of why, when, and how will simply fall into place.
Let's start right here, by listing the additional syntax in Java, followed by some examples and some discussion to help you understand how to use it. (We will also take a shot at helping you understand why it is all goodness.) Java's additional object-oriented syntactical capabilities are these:
- You can create a new class by extending an existing class, inheriting all non-private methods and variables in the existing class. You can then define new methods and variables and override inherited methods by redefining them. You can only extend one class, but that class can, in turn, extend another. The extending class is the child class, while the extended class is the parent class.
- Methods in a child class that redefine methods in the parent class can call the overridden method in the parent class by using the super keyword.
- All classes implicitly extend the Java-supplied class Object, hence they inherit its methods.
- Constructors are not inherited, but there is a Java-supplied special method named super that can be used to call the extended class's constructor.
- You can specify two additional class modifiers that affect extending classes: abstract, meaning this class must be extended, and final, meaning this class can't be extended.
- You can also specify additional method modifiers that affect extending classes: abstract, meaning this method must be overridden; final, meaning this method can't be overridden; and protected, restricting access to only this class and child classes.
- Classes that are abstract cannot be instantiated.
- Methods that are abstract have no code in their body. They have only a signature, and must be overridden by child classes.
- You can specify an additional non-local variable modifier, protected, that restricts access to the variable to only this class and classes that extend this class.
- You can declare an object reference variable to be of one class type, and assign it to an object of that class or any child of that class.
- You can define interfaces, which are really classes that have only constant variables and abstract methods.
- You can define a class to implement an interface, which requires you to override all of the methods declared in the interface. You can implement more than one interface in any class.
- You can define an object reference variable to be of an interface type, and you can assign such a variable to an instance of any class that implements the interface.
- You can test at runtime what class type an object reference variable actually refers to.
- You can cast an object reference variable to a particular class type, as long as the object really is of that type at runtime.
That's the entire OO story of Java in a nutshell. Here, in a nutshell, is what OO and these Java syntactical capabilities are designed for:
- Writing highly reusable, highly adaptable code
The Three Tenets of OO
It is generally agreed that for a computer language to be considered object-oriented, it must support the following concepts:
- Encapsulation
- Inheritance
- Polymorphism
The Java OO syntax listed supports these, in Java's own way. That is, all the items listed in the previous section are simply Java constructs to enable these three tenets. These tenets, in turn, are what enable OO languages to support highly reusable, highly adaptable code. The following sections describe these tenets and expand on the Java constructs that enable them.
Encapsulation
You get encapsulation in Java just by having classes. These allow you to put all the variables and methods that define a particular concept in one place, such as Employee, EmployeeId, Customer, Item, ItemOrder, Account, Department, Loan, Attendee, String, Vector, and Date. These are like record formats or data structures, but the difference is they contain not only data, but also all the necessary code to access and manipulate that data. You can then reuse this anywhere you want. Subsequent changes are isolated to one class, which instantly affects all code using this class.
The ability to restrict access and hide implementation details is done using modifiers such as private and protected.
There are objects on the AS/400, such as *FILE and *PGM, but they do not encapsulate supporting code inside them. Rather, you must call that code externally and tell it the object to work on, as in RNMOBJ, DLTOBJ, GRTOBJAUT, and CRTDUPOBJ.
In RPG, you can declare data structures, but not the related code inside the data structure. Rather, you end up creating subroutines and procedures that act on a given data structure. And that code often ends up scattered and duplicated throughout many applications. With RPG IV service programs, you now have the capability to encapsulate in one place at least all the data and procedures related to a particular concept. Indeed, you can think of a module with exported procedures as being very similar to a single object. The only difference is you can only have one instance of that module, so that data active at once, unless you get into the complex use of memory management via the ALLOC op-code.
Inheritance
The ability to create one class that extends another, thus inheriting all the capability of the first class, is where you get inheritance in Java. Having inherited those capabilities, you are able to refine and extend them via method overrides and new methods. This is how to get highly adaptable code. Write a solid, reusable base, and then extend it to accommodate additional situations.
When was the last time you wrote a new RPG program completely from scratch? Chances are, not in years. Rather, you use the poor man's version of inheritance: copy and change. You have a number of RPG programs that do specific functions, such as order entry, subfile processing, and customer maintenance. For each new application, you copy the appropriate common program and then change it as much as needed to deal with this particular application and database. This works, but what happens as you improve the common base program? Nothing. All programs written using it as a base do not benefit from that change. You might have discovered this when you did your Y2K work!
With inheritance, those common base programs would become classes, and each new application you write reuses those classes as is, or else extends them and refines their behavior for this particular application's need.
Let's get into the specific syntax for creating new classes by extending existing classes. As you'll see, this syntax is very easy. For a base class, start with the ItemOrder class from Chapter 2, shown in Listing 9.1.
Listing 9.1: A Base Class, ItemOrder
import java.math.*; // for BigDecimal class public class ItemOrder { protected static int nextId = 1; protected int id; protected int quantity; protected BigDecimal unitCost; public ItemOrder(int quantity, double unitCost) { this.id = nextId; nextId = nextId + 1; this.quantity = quantity; this.unitCost = new BigDecimal(unitCost); this.unitCost = this.unitCost.setScale(2,BigDecimal.ROUND_HALF_UP); } public BigDecimal getTotalCost() { BigDecimal quantityBD = new BigDecimal(quantity); BigDecimal totalCost = unitCost.multiply(quantityBD); totalCost = totalCost.setScale(2,BigDecimal.ROUND_HALF_UP); return totalCost; } public void displayOrder() { System.out.println(); System.out.println("Order Information"); System.out.println("————————-"); System.out.println(" id........: " + id); System.out.println(" quantity..: " + quantity); System.out.println(" unit cost.: " + unitCost); System.out.println(" total cost: " + getTotalCost()); } }
The modifier on the instance variables was changed from private to protected in this example. This allows code in classes that extend this class to access these variables directly, but still restricts any other code from direct access. Now, finally, you can see the reason for having four different access options. In Java, you can divide the users of your method or variable into:
- Other methods in this class (private)
- Other methods in other classes in this package (the default)
- Other methods in any subclass in any package (protected)
- Other methods in any class in any package (public)
To restrict the use of your method or variable to one of these four categories, choose the indicated modifier keyword. This restricts any method in a lower category (that is, any category below the chosen one on the list above) from your method or variable. However, it allows access from any method in the chosen or higher category.
The code from the version in Chapter 2 was also changed to use BigDecimal instead of double, which nicely solves the problem of huge decimal places.
Listing 9.2 shows an updated version of the PlaceOrders code from Chapter 2, which tests this ItemOrder class. This class declares a Vector instance variable named orders to hold all ItemOrder objects. The main method creates an instance named orderList, and then calls the method placeAnOrder twice, to create two orders stored in Vector. It then calls the method processOrders to process that Vector of orders, which in this example simply calls displayOrder on each ItemOrder element in the Vector.
Listing 9.2: A PlaceOrders Class to Test the Base Class
import java.util.*; // for Vector class public class PlaceOrders { private Vector orders = new Vector(); public void placeAnOrder(int quantity, double unitCost) { ItemOrder order = new ItemOrder(quantity, unitCost); orders.addElement(order); } public void processOrders() { ItemOrder currentOrder; for (int idx=0; idx < orders.size(); idx++) { currentOrder = (ItemOrder)orders.elementAt(idx); currentOrder.displayOrder(); } } public static void main(String args[]) { PlaceOrders orderList = new PlaceOrders(); orderList.placeAnOrder(10, 35.99); orderList.placeAnOrder(10, 26.15); orderList.processOrders(); } }
Running PlaceOrders from the command lines results in this output:
Order Information ———————————————— id........: 1 quantity..: 10 unit cost.: 35.99 total cost: 359.90 Order Information ———————————————— id........: 2 quantity..: 10 unit cost.: 26.15 total cost: 261.50
Notice in Listing 9.2, the processOrders method has to cast the output of or ders.elementAt(idx) to a type of ItemOrder, which is the class type of the objects in the orders Vector object. The reason for this is that the method elementAt in class Vector is defined to return a type named Object. This is a class supplied by Java, which all classes implicitly extend. Whenever you wish to write generic code that will work with objects of any class, such as the Vector class does, you use the class type Object. However, in order to call methods defined in the class, you must cast the object to an object reference variable defined with the class type first, as Listing 9.2 does. (You'll see more about the casting of objects later in this chapter.)
Now, let's have some fun by creating a new class that extends ItemOrder, inheriting all of its instance variables and methods, and then changes the behavior of one of the methods and adds a new method. Assume you have a special kind of order that is like the common ItemOrder except that it supports discounts. The new class is named DiscountedItemOrder, and it extends ItemOrder. Figure 9.1 shows what this class hierarchy will look like, with each box representing a class and the up-arrows indicating an "extends" relationship.
Figure 9.1: The class hierarchy diagram
The constructor for DiscountedItemOrder will have to take a third value, for the discount amount as a percentage. The getTotalCost method will have to be overridden in the new class to accommodate the discount amount in the total cost. A new method will also be added for returning the total amount of the discount, which the revised getTotalCost method will have to call. The displayOrder method can be left alone, inheriting it as-is. Listing 9.3 is the new class.
Listing 9.3: The DiscountedItemOrder Class Extending the ItemOrder Class
import java.math.*; // for BigDecimal public class DiscountedItemOrder extends ItemOrder { protected BigDecimal discountPerCent; /** * Constructor */ public DiscountedItemOrder(int quantity, double unitCost, double discount) { super(quantity, unitCost); discountPerCent = new BigDecimal(discount); discountPerCent = discountPerCent.setScale(2,BigDecimal.ROUND_HALF_UP); } /** * Override of method getTotalCost from parent class */ public BigDecimal getTotalCost() { BigDecimal totalCost = super.getTotalCost(); BigDecimal discountAmount = getDiscountAmount(); totalCost = totalCost.subtract(discountAmount); totalCost = totalCost.setScale(2, BigDecimal.ROUND_HALF_UP); return totalCost; } /** * New method unique to this class */ public BigDecimal getDiscountAmount() { BigDecimal totalCostWithoutDiscount=super.getTotalCost(); BigDecimal discountAmount = totalCostWithoutDiscount.multiply(discountPerCent); discountAmount = discountAmount.setScale(2,BigDecimal.ROUND_HALF_UP); return discountAmount; } }
There are some syntactic details to discuss about this new class. First, notice that you create a class that extends another class by simply using the extends keyword on the class definition, identifying after it the class you wish to extend:
public class DiscountedItemOrder extends ItemOrder
You can only extend a single class in Java (unlike C++), but another class could subsequently extend this one, and another extend that, and so on. As mentioned, any class that does not explicitly extend another always implicitly extends class Object in package java.lang.
The new child class automatically starts with all the class variables, instance variables, and methods of the parent class it extends (and those any classes the parent extends). However, you are only allowed to directly access those variables and methods that are not private. The same applies to users of the new class: they cannot access any of the inherited variables or methods that are private. However, they also cannot access any of the variables inherited or explicitly defined that are protected. These are reserved only for classes in this family tree (that is, classes that extend this one).
The DiscountedItemOrder class defines a new instance variable named discountPerCent, which is a BigDecimal object reference variable that will hold the discount amount as a percentage. Next, it defines the constructor for the class, which takes three parameters now, versus the two that the parent class constructor takes. The third parameter is the new discount value, taken as a double so that callers can specify literal numbers.
You must define a constructor in your child class because constructors are not inherited. However, there is an easy way to reuse the code in the constructors of a parent class. You simply call the special method named super, which always represents a call to the constructor in the parent class. Which constructor gets called depends on the usual rules of parameter numbers and type matching. If you call super, it must be the first line of code in your child class constructor. You should always call super in your constructors, because there is usually important initialization work done in constructors. Yes, call your super parents more often!
After calling super, Listing 9.3 records the given discount value, but converts it to a BigDecimal object and sets the scale to two, so math can be kept to two fixed decimal places. After the constructor, it redefines or overrides the method getTotalCost from the parent class. This is an override because the method has the same name, same number of parameters, and same type of parameters (signature) as a method in the parent class. In this case, getTotalCost has to take into account the discount amount before returning the total cost. However, you do not want to duplicate the code from the parent class to calculate the initial total cost prior to the discount. Instead, simply call that method in the parent class and record the returned value, which will subsequently have the discount applied.
To call a method in a parent class that has been overridden in this child class, you have to tell Java you want the parent's version of the method, not the child's version. You do this by using the super keyword again, but this time as a qualifier for the name of the method to call, as in:
BigDecimal totalCost = super.getTotalCost();
The rest of the method calls a new method, getDiscountAmount, which returns how much the discount is off the total cost, and subtracts that amount from the original total cost. After taking care to force the decimal places (scale) to two, the code then returns the final BigDecimal object to its callers.
The getDiscountAmount method is a new method that is not an override of a method from the parent class. It is straightforward, but notice that it qualifies the call to getTotalCost with super to ensure it calls the parent's version. Otherwise, it would call the local override version, which in turn calls it back, and you'd have an infinite loop.
The displayOrder method is not overridden from the parent class, so it is simply inherited as-is. Notice, though, that the code in that method calls getTotalCost. It will be interesting to see which version of getTotalCost gets called when you use an instance of this child class. Let's do that now! Listing 9.4 shows the PlaceOrders class updated to test the child class as well as the parent class.
Listing 9.4: The Updated PlaceOrders Class Using the New DiscountedItemOrder Class
import java.util.*; // for Vector class public class PlaceOrders { private Vector orders = new Vector(); public void placeAnOrder(int quantity, double unitCost) { ItemOrder order = new ItemOrder(quantity, unitCost); orders.addElement(order); } public void placeAnOrder(int quantity, double unitCost, double discount) { DiscountedItemOrder order = new DiscountedItemOrder(quantity,unitCost,discount); orders.addElement(order); } public void processOrders() { ItemOrder currentOrder; for (int idx=0; idx < orders.size(); idx++) { currentOrder = (ItemOrder)orders.elementAt(idx); currentOrder.displayOrder(); } } public static void main(String args[]) { PlaceOrders orderList = new PlaceOrders(); orderList.placeAnOrder(10, 35.99); orderList.placeAnOrder(10, 26.15); orderList.placeAnOrder(10, 26.15, .10); orderList.processOrders(); } }
Well, not much changed here, only what is shown in bold. A new overloaded version of placeAnOder is created, which takes three parameters versus two, and it instantiates the new child class DiscountItemOrder instead of the parent class ItemOrder. The main method is then changed to simply add a single call to the new method, passing a discount value of ten percent (.10). Remember, when you have two methods with the same name in the same class, the runtime determines which one you want to call by matching up the number and type of the parameters.
Before dissecting the interesting aspects of this, let's run it and see the output:
Order Information ————————-———————— id........: 1 quantity..: 10 unit cost.: 35.99 total cost: 359.90 Order Information ————————-———————— id........: 2 quantity..: 10 unit cost.: 26.15 total cost: 261.50 Order Information ————————-———————— id........: 3 quantity..: 10 unit cost.: 26.15 total cost: 235.35
As you can see, the third order is in fact discounted by 10 percent. This is all very exciting isn't it? (Okay, so maybe you have a life outside of computers.) The new class appears to have inherited and leveraged all the code of its parent class, yet extended it only as necessary to refine its behavior. The calling code simply uses the new class interchangeably with the old class. Notice that the vector of orders named orders holds two objects of type ItemOrder and one object of type DiscountedItemOrder. Also, notice that the method processOrders did not need to be changed at all. You might have expected the casting to type (ItemOrder) to fail on that third object, since the rule is supposed to be that you can only cast an object to its actual runtime type, and for the third object the runtime type is DiscountedItemOrder, not ItemOrder. Well, the rule has a caveat: you can cast to the actual runtime type or any parent class of that type.
Let's look closer at the method processOrders from Listing 9.4:
public void processOrders() { ItemOrder currentOrder; for (int idx=0; idx < orders.size(); idx++) { currentOrder = (ItemOrder)orders.elementAt(idx); currentOrder.displayOrder(); } }
This method iterates through the elements of the orders vector. These elements are objects, and for each, it calls the method displayOrder in that object. What class is that method in? Well, the Java compiler has to know that answer, too, so that it can verify the method actually exists. This is why the code had to declare a local variable of type ItemOrder and assign each element of the vector to it before being allowed to call method displayOrder. The compiler accepts this because it looks inside the ItemOrder class and discovers it does, indeed, have a method in there with the same signature as the displayOrder call.
This is all fine, except in the example, the third object is not of type ItemOrder at all! It is of type DiscountedItemOrder. Wow, you fooled the compiler! So what happens at runtime, then, when you actually make that call to displayOrder? It should fail, since you do not have a method named displayOrder in class DiscountedItemOrder. Or do you? Well, you do, in fact! You inherited it from the parent class ItemOrder, remember. So, luckily, this works.
In fact, there was no luck whatsoever here, and you did not fool the compiler. This is a legal loophole that you are encouraged to exploit. The compiler and runtime let you call any method on an object, as long as that method exists either explicitly or by inheritance.
Did you follow this example of inheritance? As usual, the syntax is very easy. Appreciating its true power, though, comes with time.
Inheritance is what separates "object-based" from "object-oriented."
One crude AS/400 example of inheritance is reference fields in DDS. Maintaining a field reference file of field definitions, is somewhat like having a base class. Each database file that you define by using or referencing your field reference file (FRF) fields is somewhat similar to a child class that extends its parent class (the FRF). You define new fields (simple reference), override existing fields (by redefining a reference field's length, for example), and remove (by omission) other fields. The difference is we are only talking data, not methods or code.
Note that there is no way in Java to remove methods you inherit. The best you can do is to overload them with empty methods, as in:
public void aMethodWithSameSignatureAsParentsVersion() { }
Polymorphism
No, polymorphism is not denture cream! In fact, you have already seen this concept in action. When you define a class that extends another class and then override one of the inherited methods with a new implementation of that method, you are practicing polymorphism. (Aren't you naughty?!) Polymorphism refers to the fact that calling the same method on two objects can have different results if the objects come from different classes in the same hierarchy. (There's a mouthful!)
Let's look again at the code in method processOrders in Listing 9.4:
ItemOrder currentOrder; for (int idx=0; idx < orders.size(); idx++) { currentOrder = (ItemOrder)orders.elementAt(idx); currentOrder.displayOrder(); }
If you just looked at this code without the rest of the class as context, you would have to conclude, as the compiler has to conclude, that all the objects in the orders vector are of type ItemOrder, and you are always calling the method displayOrder in that class. The reality is different at runtime, where the third object is of type DiscountedItemOrder and you are in fact calling the inherited version of displayOrder in that class. So, the method that the compiler thinks is being called is one thing (displayOrder in class ItemOrder), while the method that gets called at runtime is another (either displayOrder in class ItemOrder or displayOrder in class DiscountedItemOrder). This is polymorphism, which is from the Greek for "many shapes."
This example is not very interesting because both versions of displayOrder are the same. Let's look at a more interesting example, one you've seen already. Consider the code in the method displayOrder itself:
public void displayOrder() { System.out.println(); System.out.println("Order Information"); System.out.println("————————-"); System.out.println(" id........: " + id); System.out.println(" quantity..: " + quantity); System.out.println(" unit cost.: " + unitCost); System.out.println(" total cost: " + getTotalCost()); }
This code seems straightforward enough, but look at that last line, where you call getTotalCost. This method exists in ItemOrder and is overridden with a new version in DiscountedItemOrder as shown in Listing 9.3. So which version gets called by the line of code in displayOrder? It depends on the object type at runtime.
As you saw when Listing 9.4 was run, when the target object is an ItemOrder instance, it calls the version in ItemOrder, and when the target object is a DiscountedItemOrder, it calls the version in that class. In other words, you can write totally reusable code both here and in the processOrders method that calls one of two possible methods depending on the runtime type of the object. The result you get is correct for that type. Notice, also, that there is no code anywhere that says "if order is of type ItemOrder call this method, else if it is of type DiscountedItemOrder call this method." The decision about which version of the overridden method getTotalCost to call is made for you. It is polymorphic magic.
The syntax rule that makes polymorphism possible is this:
You can declare an object reference variable to be one class type, and yet assign to it at runtime an instance of a different class type, as long as the runtime class type extends the declared compile-time class type.
For example, this is legal:
ItemOrder anOrder = new DiscountedItemOrder();
This rule also applies to parameters, so you might have a method defined to take an object of type ItemOrder, and at runtime pass it an object of type DiscountedItemOrder:
public static void verifyOrder(ItemOrder anOrder) { ... } ... verifyOrder(new DiscountedOrder);
The reverse is not true, however. You cannot assign an object that is of a class type higher in the hierarchy tree. That is, you cannot do this:
DiscountedItemOrder anOrder = new ItemOrder();
More examples? Here is a fun one. Let's declare a parent class named Animal, and give it one method named talk. Then, let's define a child class of Animal named Cat, which overrides the method talk to write out "meow" to the console. Let's also create other child classes named Dog and Cow, which similarly override the talk method. Then, let's can create a little Zoo class that instantiates an array of Animal objects, populating it with instances of randomly chosen classes. Finally, let's walk that array and call the method talk on each object to see what we get.
Listing 9.5 shows all four animal classes together, one after another, in the interest of space. In reality, of course, they must all be in their own files.
Listing 9.5: The Animal Class and Its Children
public class Animal { public void talk() { System.out.println("I'm an animal"); } } public class Cat extends Animal { public void talk() { System.out.println("meow"); } } public class Dog extends Animal { public void talk() { System.out.println("woof"); } } public class Cow extends Animal { public void talk() { System.out.println("moo"); } }
Listing 9.6 shows the Zoo class.
Listing 9.6: A Zoo Class to Test the Animal Classes
import java.util.*; // for Random class public class Zoo { protected Animal animals[] = new Animal[10]; private Random randomizer = new Random(System.currentTimeMillis()); public void createAnimals() { for (int idx=0; idx < animals.length; idx++) { int randomNbr = randomizer.nextInt(3); //between 0 and 2 if (randomNbr == 0) animals[idx] = new Cat(); else if (randomNbr == 1) animals[idx] = new Dog(); else if (randomNbr == 2) animals[idx] = new Cow(); } } public void allTalk() { for (int idx=0; idx < animals.length; idx++) animals[idx].talk(); } public static void main(String args[]) { Zoo theZoo = new Zoo(); theZoo.createAnimals(); theZoo.allTalk(); } }
What we have done is define an array of Animal type as an instance variable, then defined two methods: createAnimals to populate the animals array with objects of randomly chosen Animal child classes, and allTalk to walk the array and call the talk method on each object element. Note that arrays, unlike vectors, allow you to explicitly set the class type every element has, so you do not need to do casting. The main method instantiates things and calls the two methods. Here is the output of running this class:
moo woof meow meow moo woof meow moo woof moo
This is why you got into programming, isn't it? It's fun! Notice because the Random object is seeded with the current time, you get a different result every time you run this.
What you should take out of this example is the power of polymorphism to replace if/else coding with elegant, simple logic that knows how to do the right thing for each object, because the objects themselves know how to do the right thing. For example, look at the simplicity of the allTalk method in Listing 9.6. It simply calls the method talk on each element of the array, which is defined to hold objects of type Animal. Yet, when you run the code, you end up calling the method talk in one of three different classes, "automagically." There's no need to test what type the array element is and hard-code the decision about which method to call. That is done for you, by the Java runtime.
Depending on your occupation and the nature of your co-workers, this example might not apply in your next business application. However, the design pattern will. That is, imagine that instead of Animal, Cat, Dog, and Cow, you have Employee, SalariedEmployee, Hourly Employee, and ContractorEmployee, as shown in the class hierarchy in Figure 9.2.
Figure 9.2: The Employee class hierarchy diagram
Instead of the overloaded method talk, you have an overloaded method printPayCheck, which has a unique pay-calculation algorithm for each type of employee. Now, imagine instead of Zoo you have a class named Payroll, which has an array of type Employee and is populated by reading the database and creating the appropriate object type for each employee. Now, the talk method in Zoo becomes a doPayroll method in Payroll:
public void doPayroll() { for (int idx=0; idx < employees.length; idx++) employees[idx].printPayCheck(); }
Do you see the elegance of polymorphism now? It allows you to write very concise code that can deal magically with many different object types at runtime. Further, the code is very adaptable to change, without change! If in the future a new employee type is identified, such as MaternityLeaveEmployee, you simply create a new class that overrides the appropriate methods in Employee. Then you only have to change the code that decides which object type to create, and all the rest of the code remains untouched. The entire HumanResources and Payroll systems will work perfectly with the new employee type. As far as they know, they are calling methods in the Employee class. The reality that they are calling methods in child classes of Employee is polymorphic magic.
Now you can see why no formal language construct exists for a child class to remove methods from its parent class. The concept of polymorphism requires the compiler to assume that a method defined in one class is always available in its lower, or child, classes. That is why you are allowed to declare an object reference variable using a parent class type, and assign to it an object of a child class, and subsequently call methods in that child class. Via inheritance rules, you are guaranteed the child classes have at least all the methods the parent class has. You also cannot restrict a method's access rights beyond what is defined at the parent level. For example, you cannot override a method in a child class and change it from a public method to a private method, as that effectively would make it available in parent objects, but unavailable in child objects. The reverse, however, is permitted.
There are many examples of polymorphism in the Java-supplied classes in the Java Development Kit. For example, in Chapter 6, you saw the list utility classes Vector and Hashtable. These classes are designed to hold objects of any class. Generally, they do not know or care about the class type of these objects. However, there are some operations in Vector and Hashtable that cannot be done without calling methods in each of the elements. For example, the contains method allows you to search the list for a given object. To determine if the given object "matches" any particular object in the list, the code walks the list and calls the equals method of each element object, passing the given object as a parameter. If any of the objects return true from this call, then the contains method returns true to the caller.
In order for this code to compile, the equals method has to exist in every object in the list. This is, in fact, the case, because the engineers put an equals method in the Object class that all classes implicitly extend, therefore guaranteeing it exists in every object in Java. However, this method, by default, only compares the memory addresses of the two objects, so you almost always have to override it to get the correct behavior. The String class overrides it to compare the string content lengths and each of the characters. The Attendee class in Chapter 6 overrode it to compare the attendee names. An Employee class might override it to compare employee numbers.
You can find examples of polymorphism in OS/400, such as PDM (Programming Development Manager). In PDM's list screens, you are presented with a list of libraries, objects, or members; you can enter options beside them to invoke actions on the items in the list. These options (like option 5 to display) are polymorphic in that the resulting action depends on the type of object you use it against. You get a different result for each object type, and yet you, as a user, always use the same simple option; PDM takes care of determining the appropriate action for the selected object's type. PDM is acting like the Java runtime in this case, deciding which command (method) to call based on the object's type.
Yet another example is the WRITE op-code in RPG. You use it the same way whether you are writing to a database, display, or printer file. The RPG runtime keeps track of which one is actually being written to, and handles each uniquely, under the covers.
One input, one of many possible outputs.
Inheriting more or Less
There are a couple of new method modifiers to introduce to you now, which relate to method overriding. One (final) prevents child classes from overriding this method, while the other (abstract) forces child classes to override this method.
Preventing inheritance
There are times when you will want to completely restrict the ability for another programmer to extend your class and override or replace certain methods. From a security point of view, you might have very sensitive methods that you want to ensure cannot be maliciously or unwittingly replaced. The getPay()method for an employee object might be an example of a sensitive method.
Notice the difference here between using and overriding. You do not mind child classes using this method—in fact, you want this, because it hides all the sensitive calculations. What you do not want child classes doing is overriding the implementation of this method. For integrity and security, it is important that all code that invokes this method get to the one and only implementation of it, and not some other implementation farther down the inheritance chain (thus preventing a Trojan horse kind of attack).
You can prevent child classes from overriding a specific method in Java by using the final modifier keyword on the method:
protected final int getPay() { ... }
This modifier tells the compiler that no child subclass (as they are sometimes called) is allowed to override this method with its own implementation. The compiler will enforce this, as will the runtime. In addition to security benefits, final also offers potential performance benefits. It is a clue to the compiler that this method will not be participating in polymorphism, so the compiler has the option of inlining the method. Inlining involves turning calls to the method into static instead of dynamic ones (that is, determining and recording the address of the method at compile time, rather than waiting for runtime).
You can also specify final at the class level, in which case all methods inside the class are automatically designated as final by the compiler.
Enforcing inheritance
The previous section discussed how to prevent child classes from overriding a particular method or prevent child classes altogether for a particular class. There are times, however, when precisely the opposite is required. Sometimes, methods are coded in a parent class just so you can ensure they exist via inheritance in your child classes. Consider the method talk in class Animal in Listing 9.5. A dummy block of code was created for it, but the reality is that this code will probably never be exercised. Rather, every child class should override this method with a more meaningful implementation for that particular animal type. If you forget to override this method in a child class, then at runtime, you will end up executing the dummy version of the method, getting unexpected results.
To turn this loose contract into a more binding contract, code the modifier abstract on the method in the parent class. When you do this, every child class must override the method, or the compilation of that class will fail. This is just what you want. When you specify abstract on a method, you do not have to bother with dummy placeholder code in its body. Indeed, you are not even allowed to code a method body! You must place a semicolon after the signature, like this:
public abstract void talk();
If you specify this modifier on any method in a class, then you must also specify it on the class, as in:
public abstract class Animal { public abstract void talk(); }
Why is this, and what does it mean? A class with even one abstract method is not instantiable directly. That is, you cannot use new to instantiate it. The reason, of course, is that this would allow you to call that abstract method, which would be a problem because it has no code in it! Thus, you must identify the whole class as abstract so that the compiler can prevent attempts to use new on it. The assumption is that this is a class that is only used as a parent for one or more child classes that extend it. These classes will have to override and implement that method, else their compile will fail (unless you flag them as abstract, too).
Why would you want abstract classes? For abstraction. Use them as the base class as you saw with Animal. You would never ever expect someone to instantiate an Animal object directly, since clearly every animal is an instance of some concrete child class, such as Dog.
Are you clear on why we even created that Animal class, and that we could not have written the elegant Zoo code without it? We needed a base class to use as the declared type for the generic "work with any type of animal" code. If we instead used Dog for the type of the animals array, we would have been prevented from putting Cat objects in there, as these do not have a parent/child relationship. We could, alternatively, have used the root parent of all classes, Object, as the type, but that would have made the compiler fail on the call to the talk method in the Zoo code, as the Object class is not defined with this method.
Remember, the compiler is trying to help by ensuring the methods called really do exist in the declared class type. Even if you declare an object reference variable as a parent class type and subsequently assign an object of a child class to it at runtime, you are ensured that the method exists in the children because all non-private methods are inherited.
You will see lots of examples of abstract classes in the Java Development Kit. One such example is an abstract class named JComponent in the GUI packages (java.awt and javax.swing). This class represents a generic GUI widget, and all concrete widget child classes such as JButton and JCheckBox extend it. The JComponent has abstract methods that all widgets are required to support, such as setBackground for setting the background color and repaint for painting the widget after it becomes visible. Certainly, there is no way to write generic code in the parent class that will work for all possible widgets, so that job is left to the child classes, who are forced to override these methods. By abstracting out the idea that all widgets must have these methods, Java was able to write elegant and generic code for frame windows that contain one or more GUI widgets. When a frame window is restored from minimized state, for example, its logic simply walks its array of JComponent objects and calls the repaint method in each object. What that does depends on what type of object it is, but the important thing is every GUI widget object is guaranteed to have this method. The repaint method in JComponent is declared abstract, and hence so is the class, because there is simply no reasonable default code that can be done to paint a pretend component.
This GUI class hierarchy, the Animal class hierarchy in Listing 9.5, and the ItemOrder class hierarchy from earlier in this chapter are all highly adaptable. To support new GUI widgets, new animals, and new types of orders, you simply create new classes that extend the respective parent classes and override the required methods (repaint, talk, and getTotalCost, for example). Suddenly, then, objects of these new classes will work with all the existing infrastructure code that only uses object references variables defined with the parent class type. That is, the frame window code, the Zoo code, and the PlaceOrders code will all work with the new classes, without any change or even recompile needed.
Finally, you can think of field reference files on the AS/400 as being similar to abstract classes. They have no "implementation" (in this case, they never hold data), but are designed explicitly to abstract-out common field attributes for use in child or populated databases.
The Factory Method Design Pattern
At this point, you have seen the core tools in the Java language for writing highly reusable and highly adaptable code. The syntax is easy: the extends keyword and some optional modifiers like protected, final, and abstract simply help you get the compiler to enforce some rules. The tricky part for RPG programmers moving to an object-oriented language is never the "how," it is the "why" and subsequently the "how best to do this."
The latter two questions are part of a larger domain of study called object-oriented analysis and design (OOA and D). The idea is to offer some insights to help you to design an effective solution for a given problem, one that maximizes reuse and adaptability. There is no single right answer for most decisions relating to designing classes and class hierarchies, but by following some proven rules or methodologies, you can usually get some guidance. You'll learn more about OOA and D later, although in-depth discussion of that domain is beyond this book. Further, not all of your team need to be geniuses at deciding which classes to create and which classes should extend other classes. Only one person needs this skill, and he or she can subsequently assign the task of writing each class to other team members.
We do want to give you one big tip, though:
Whatever it is you are trying to design the class hierarchy for, someone somewhere has probably done it before!
Order entry, payroll, human resources, inventory management, accounts receivable, and even wine-list management have all been around for a long time. Your details are no doubt unique, but many of the overall design approaches won't be. You can do yourself a favor by looking at existing solutions to similar problems. Indeed, often the pattern associated with a solution applies to many problem domains. For example, you saw that the Animal class hierarchy and the Zoo class had similar "design patterns" that you could use for employee payroll, at a very high level.
The idea of scouring many successful solutions to common programming problems and documenting what emerges as common patterns was first made immensely popular by Eric Gamma, Richard Helm, Ralph Johnson, and John Vlissides in their watershed 1995 book Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley). The book is based on C++, but it can also be applied to Java. The only difference to keep in mind is that C++ supports extending multiple classes simultaneously, while Java does not.
One pattern from this book that is important for you to know is called the object factory. Taking a great deal of liberty on the explicit details of this pattern, it is roughly this: whenever you have a class hierarchy involving an abstract parent or base class, and a number of child classes that extend it, somehow and somewhere you have to eventually decide which child class to instantiate. The more you can encapsulate this decision process and hide the details of the decision, the less code you will have to change when you subsequently invent new child classes.
Consider the Zoo class in Listing 9.6 and the createAnimals method in it, which populates the animals array with instances of various Animal child classes. If you add a new child class that extends Animal, you must come back to this method and add code to instantiate it. Normally, this is based on some reasonable criteria, not just a random decision. Indeed, it is very often based on one of the fields read from a database, or one of the fields in a user interface. So, the pattern is this: you create a class that has the same name as the parent class, but with "Factory" appended to it, and in it you supply a static method named createXXX that returns an instantiated object. The decision about which child class to instantiate is isolated to that method, using whatever criteria you need. Any pieces of code that want an object from this class hierarchy are encouraged to call this createXXX method, versus coding new themselves.
Applying this design pattern to the Animal class hierarchy gives the class AnimalFactory with its createAnimal static method, as shown in Listing 9.7.
Listing 9.7: The AnimalFactory Class
import java.util.*; // for Random class public class AnimalFactory { private static Random randomizer = new Random(System.currentTimeMillis()); /** Factory method to create appropriate child object */ public static Animal createAnimal() { Animal animalObj = null; int randomNbr = randomizer.nextInt(3); //Between 0 and 2 if (randomNbr == 0) animalObj = new Cat(); else if (randomNbr == 1) animalObj = new Dog(); else if (randomNbr == 2) animalObj = new Cow(); return animalObj; } }
Notice the declared return type for the createAnimal method. It is defined to be just Animal, the parent class. However, we always return an object of a child class. The rules of polymorphism allow us to do this. (An object reference variable declared of class type X can be assigned to an object of class X or any child class of X.) Listing 9.8 revises the Zoo code that uses the AnimalFactory method.
Listing 9.8: The Revised Zoo Class, Using the AnimalFactory Class
public class Zoo { protected Animal animals[] = new Animal[10]; public void createAnimals() { for (int idx=0; idx < animals.length; idx++) animals[idx] = AnimalFactory.createAnimal(); } public void allTalk() { for (int idx=0; idx < animals.length; idx++) animals[idx].talk(); } public static void main(String args[]) { Zoo theZoo = new Zoo(); theZoo.createAnimals(); theZoo.allTalk(); } }
The Zoo code is smaller because the details of object instantiation are delegated to the factory method. The Zoo class is now completely class-agnostic! It only refers to the parent class Animal, not any child class. So, if you define a new animal type by creating a new child class of Animal, you need only change the AnimalFactory class, and the Zoo code will work magically with it! This is nice.
Hopefully, you work at a zoo! Actually, even if you don't, you will probably use this design pattern. Consider the Employee class hierarchy, described earlier. Its factory class might look like Listing 9.9.
Listing 9.9: Another Factory Example: The EmployeeFactory Class
import java.io.*; import java.util.*; public class EmployeeFactory { public static BufferedReader fileConnection = null; public static final String EMPTYPE_SALARY = "S"; public static final String EMPTYPE_HOURLY = "H"; public static final String EMPTYPE_CONTRACTOR = "C"; public static final String EMP_FILENAME = "empdata.dat"; public static Employee createNextEmployee() { StringTokenizer empInfo = getNextEmpFromDB(); Employee empObj = null; if (empInfo != null) // not end of file? { // parse record into constituent fields String recType = empInfo.nextToken(); String empId = empInfo.nextToken(); String empPayType = empInfo.nextToken(); String empPay = empInfo.nextToken(); String empDept = empInfo.nextToken(); String empHours = empInfo.nextToken(); String empName = empInfo.nextToken(); while (empInfo.hasMoreTokens()) empName = empName + " " + empInfo.nextToken(); // Convert String info to appropriate types int employeeId=0, employeeDept=0; double employeePay=0, employeeHours=0; try { employeeId = Integer.parseInt(empId); employeeDept = Integer.parseInt(empDept); employeePay = Double.parseDouble(empPay); employeeHours = Double.parseDouble(empHours); } catch (Exception exc) {} // Create appropriate object if (empPayType.equals(EMPTYPE_SALARY)) empObj=new SalariedEmployee(employeeId, employeePay, employeeDept, empName); else if (empPayType.equals(EMPTYPE_HOURLY)) empObj=new HourlyEmployee(employeeId, employeePay, employeeDept, empName, employeeHours); } return empObj; } } // end EmployeeFactory
Although the getNextEmpFromDB method is not shown here, it is available on the CD-ROM included with this book. It simply reads from a flat file, since you have not yet seen relational database access. An example Payroll class that uses this, as well as the parent Employee class and its two child classes SalariedEmployee and HourlyEmployee, can also be found on the CD-ROM. You'll see they are very similar in design to the Animal classes.
Your factory code can do whatever is required to get the information to make the decision about which class to instantiate. If the information is supplied by a user, then your factory method will probably take the input as one or more parameters.
While the term object factory hails from the referenced book and this explicit design pattern, these days you will find it used generically to refer to any static method that instantiates and returns an object.
Interfaces
You have seen that the syntax for creating a class that extends another class is very simple, but appreciating and realizing the full power and potential of that simple construct comes with time. That is also the case for the next construct you will see: the interface.
Recall that RPG IV procedures require you to write prototypes. For example, consider the simple VECTOR service program from Chapter 6, and the copy member supplied to users, which is shown again in Listing 9.10.
Listing 9.10: RPG IV Procedure Prototypes for the VECTOR Service Program
/* Procedure prototype for VectorAllocate DVectorAllocate PR * /* Procedure prototype for VectorAddElem DVectorAddElem PR * D vectorHeader@ * VALUE D dataSize 5U 0 VALUE /* Procedure prototype for VectorElemAt DVectorElemAt PR * D vectorHeader@ * VALUE D position 5U 0 VALUE /* Procedure prototype for VectorIndexOf DVectorIndexOf PR 5U 0 D vectorHeader@ * VALUE D userData@ * VALUE /* Procedure prototype for VectorGetSize DVectorGetSize PR 5U 0 D vectorHeader@ * VALUE /* Procedure prototype for VectorRmvElem DVectorRmvElem PR D vectorHeader@ * VALUE D userData@ * VALUE
These particular prototypes are for procedures that allow you to:
- Allocate a growable list or vector (VectorAllocate).
- Given a length, allocate that much memory and store the pointer to that memory in the vector (VectorAddElem). The pointer is also returned to the caller.
- Given a one-based element position, return the memory pointer in that position in the vector (VectorElemAt).
- Given a memory pointer, return the one-based position of that element in the vector (VectorIndexOf).
- Return the number of elements in the vector so far (VectorGetSize).
- Remove an element from the vector and free up the memory associated with it (VectorRmvElem).
The prototype for a procedure is a complete duplicate of the procedure interface D-specs, identifying the name of the procedure, the attributes of the return type (length, type, decimals, and any keywords), and the name (optional) and attributes of the parameters. The reason for prototypes is so that the compiler can verify that callers of the procedure get the name right, pass the correct number of parameters, and pass the correct type of the parameters. That is the purpose of a strongly typed language: to catch as many errors at compile time as possible. If any of this information is wrong and the compiler lets it pass, it would lead to a runtime error or function check, and you have probably learned from experience that your users don't seem to like these!
Java however does not require you to prototype your methods, even though the compiler still does verify that the method name, the number of parameters, and the type of the parameters are correct for any calls. It can do this because of the compiler's unique ability to find on disk the file containing the class containing the method, and read (introspect) the signature information for that method. That information is compared to the information on the call, and an error is issued if they are not compatible. All this is good, as you enjoy the benefits of prototyping without the productivity hit.
Nevertheless, there still is something in Java vaguely similar to prototypes:
You can separate your class from its interface, which includes the signatures of all the public methods you want to expose to other programmers.
Let's get right to the syntax. Listing 9.11 shows an interface named Displayable that defines a single method, display.
Listing 9.11: A Java Interface: Displayable
public interface Displayable { public void display(); }
You declare an interface the same way as you declare a class, substituting the keyword interface for the keyword class. Inside the interface however, you have only method signatures, but no method bodies. These signatures look identical to the signatures you saw for defining abstract methods. In fact, they are identical! The keywords public and abstract are implicit for all method signatures in an interface, although they can also be redundantly coded. Indeed, an interface is just special syntax for an abstract class that contains only abstract methods. Well, abstract methods or constants, since Java also allows you to define constant fields in an interface, but these are the only field definitions permitted.
Now to the syntax of using an interface. (We'll get to the "why would I use them?" question shortly.) As mentioned, an interface is similar to an RPG IV copy member that contains prototypes of procedures. In RPG IV, you ultimately have a module somewhere that actually implements those procedure prototypes. For example, the prototypes from Listing 9.10 are implemented in the VECTOR module, which was discussed in Chapter 6 in Listing 6.21.
Java is similar. The methods inside an interface are implemented by some class. If the interface really were defined as an abstract class with abstract methods, you know from the earlier discussion that those methods would be implemented by one or more child classes that extend the abstract class. With interfaces, the abstract methods are implemented by one or more classes that "implement" that interface. This is any class that specifies the "implements XX" phrase and contains actual implements for the methods defined in interface XX. For example, the class in Listing 9.12 implements the Displayable interface.
Listing 9.12: A Java Class that Implements an Interface: ItemOrder
import java.math.*; // for BigDecimal class public class ItemOrder implements Displayable { protected static int nextId = 1; protected int id; protected int quantity; protected BigDecimal unitCost; public ItemOrder(int quantity, double unitCost) { this.id = nextId; nextId = nextId + 1; this.quantity = quantity; this.unitCost = new BigDecimal(unitCost); this.unitCost = this.unitCost.setScale(2,BigDecimal.ROUND_HALF_UP); } public BigDecimal getTotalCost() { BigDecimal quantityBD = new BigDecimal(quantity); BigDecimal totalCost = unitCost.multiply(quantityBD); totalCost = totalCost.setScale(2, BigDecimal.ROUND_HALF_UP); return totalCost; } public void display() { System.out.println(); System.out.println("Order Information"); System.out.println("————————-"); System.out.println(" id........: " + id); System.out.println(" quantity..: " + quantity); System.out.println(" unit cost.: " + unitCost); System.out.println(" total cost: " + getTotalCost()); } }
Does this class look familiar? It comes from Listing 9.1. The only difference is that the name of the method displayOrder is changed to display, which is more generic and is also the name of the method inside the interface. The phrase "implements Displayable" is also added to the class definition at the top. This is all there is to it.
For any class, you can pull one or more of the method signatures out and put them into an interface, and then add the phrase "implements xxx" to the class definition. Why do this? Stay tuned. When you code this phrase on a class, you must implement every one of the methods in that interface, using the same signatures as specified in the interface. Otherwise, the compile of the class will fail. Of course, you are free to code additional methods as well, beyond what the interface dictates.
Back in RPG IV, you have a copy member and a module that implements the prototypes in the copy member. This compares in Java to an interface and a class that implements that interface. You have something else in RPG IV, however: other modules that /COPY the copy member and call one or more of the procedures identified by the prototypes in the copy member.
Java has this, too! You can write code that uses methods prototyped by an interface by simply declaring object reference variables whose type is the name of the interface. However, you are not allowed to assign these variables to instances of interfaces because you are not allowed to instantiate interfaces. Remember, interfaces do not have constructors or non-abstract methods. You can, however, assign them to instances of classes that implement that interface. For example, the following line of code will compile only if the class ItemOrder implements the Displayable interface.
Displayable myVariable = new ItemOrder();
Listing 9.13 is an example of a few static methods that use the Displayable interface. This class has two overloaded versions of a displayAll method. The first takes as input an array of objects whose elements are defined to be of type Displayable, which is the interface. It then walks through the array, and for each element, it calls the display method. The second method does the same thing, but is designed for cases when you have a vector instead of an array. It takes a vector of objects as input, but because the Vector class is defined to only hold items of type Object, you have to cast each element to an object reference variable defined to be of type Displayable before you can attempt to call the display method on that element. Remember from Chapter 6 that vectors offer great flexibility, but do require you to cast the objects as you pull them out of the vector, if you want to call any of the methods in that object.
Listing 9.13: Java Code that Uses the Displayable Interface: The Helpers Class
import java.util.*; public class Helpers { public static void displayAll(Displayable objects[]) { for (int idx=0; idx < objects.length; idx++) objects[idx].display(); } public static void displayAll(Vector objects) { Displayable currentObject = null; for (int idx=0; idx < objects.size(); idx++) { currentObject = (Displayable)objects.elementAt(idx); currentObject.display(); } } }
Listing 9.13 is designed to be a helper class, so let's put it to use to help us. The class in Listing 9.14 uses one of the methods from the Helpers class.
Listing 9.14: A Revised Version of PlaceOrders that Uses the Helpers Class
import java.util.*; // for Vector class public class PlaceOrders { private Vector orders = new Vector(); public void placeAnOrder(int quantity, double unitCost) { ItemOrder order = new ItemOrder(quantity, unitCost); orders.addElement(order); } public void placeAnOrder(int quantity, double unitCost, double discount) { DiscountedItemOrder order = new DiscountedItemOrder(quantity, unitCost, discount); orders.addElement(order); } public void processOrders() { Helpers.displayAll(orders); } public static void main(String args[]) { PlaceOrders orderList = new PlaceOrders(); orderList.placeAnOrder(10, 35.99); orderList.placeAnOrder(10, 26.15); orderList.placeAnOrder(10, 26.15, .10); orderList.processOrders(); } }
This class should look familiar, too. You last saw it in Listing 9.4, and all that's changed is the body of the processOrders method. Rather than do your own loop through the or ders vector, you now simply call the displayAll method in the Helpers class to do this for you, passing your vector into it as a parameter. If you compile and run this, you get the same output as in Listing 9.4.
Here are some details on interface declarations:
- Interfaces can only contain constants and method signatures.
- Interfaces can extend one or more other interfaces, comma-separating the names.
- More than one class can implement the same interface.
- Classes can implement one or more interfaces, comma-separating the names.
The syntax is almost trivial: interfaces are just like Java classes with method signatures (and optionally constants) only; classes that implement the method signatures in an interface do so by declaring the phrase "implements xxx"; you can use an interface name as the type for an object reference variable; and you can assign objects of any class that implement the interface to that object reference variable.
Where does all this leave you? It might seem that you're just back where you started, with code that runs the same as it did before the interface. However, you aren't just back where you started. You have pulled out some of the functionality that was hard-coded in PlaceOrders class and put it into another class (Helpers) that is now reusable in many different situations. You don't get that reuse, however, until you have more than one class that implements the same interface.
Remember, the goal of OO is highly reusable and highly adaptable code. Interfaces enable finer-grained code reuse than inheritance does. With inheritance, you reuse an entire class. With interfaces, you typically reuse just one or two methods. Further, the reuse comes not from the interface code, and not from the one or more classes that implement the interface. Rather, the reuse comes from the code that works with classes that implement the interface, such as the displayAll method in Listing 9.13. This happens when more than one class implements the same interface, enabling code written to use that interface to work with more than one class.
Let's look at another example to see how the displayAll method can be reused in another, entirely different, situation. Chapter 6 had an example of a registration program that tracked attendees at an event by recording their phone numbers and names. One of the functions of that program was to display the current list of attendees. Do you think you can reuse one of the displayAll methods above, as is, to do this? You bet!
Consider the Attendee class in Listing 9.15, which is identical to Listing 6.20 except for the implements Displayable phrase. This class implements the Displayable interface and subsequently supplies a display method (if it didn't, you would get a compile error). As it turns out, this class already has the display method, so no additional work is needed.
Listing 9.15: The Attendee Class Implementing a Displayable Interface
public class Attendee implements Displayable { private String name; public Attendee(String name) { this.name = name; } public String getName() { return name; } public boolean equals(Object other) { Attendee otherAttendee = (Attendee)other; return name.equals(otherAttendee.getName()); } public void display() { System.out.println("—————————————"); System.out.println("Name........: " + name); System.out.println(); } }
Next, there is the class AttendeeList from Chapter 6, which records Attendee objects in a vector. It already has a display method to walk the list and call the display on each element, so you simply change the method to call the displayAll method in the Helpers class, and add a main method so you can quickly test this. The resulting class is shown in Listing 9.16.
Listing 9.16: The AttendeeList Class Reusing the displayAll Method in the Helpers Class
import java.util.*; public class AttendeeList { private Vector attendees = new Vector(); public void register(String name) { Attendee newAttendee = new Attendee(name); if (!checkForAttendee(newAttendee)) attendees.addElement(newAttendee); else System.out.println("An attendee named " + name + " is already registered"); } public void deRegister(String name) { Attendee theAttendee = new Attendee(name); if (!checkForAttendee(theAttendee)) System.out.println("Sorry, name " + name + " is not registered"); else attendees.removeElement(theAttendee); } public boolean checkForAttendee(Attendee attendee) { return attendees.contains(attendee); } public void display() { Helpers.displayAll(attendees); } public static void main(String args[]) { // test attendee code AttendeeList attendeeList = new AttendeeList(); System.out.println(); attendeeList.register("Phil Coulthard"); attendeeList.register("George Farr"); attendeeList.register("George Farr"); attendeeList.display(); attendeeList.deRegister("Phil Coulthard"); attendeeList.deRegister("Phil Coulthard"); attendeeList.display(); } }
If you run this class, you get the following:
An attendee named George Farr is already registered —————————————————————————— Name........: Phil Coulthard —————————————————————————— Name........: George Farr Sorry, name Phil Coulthard is not registered —————————————————————————— Name........: George Farr
The important thing to take out of this is that it you successfully reuses the displayAll method in an entirely new situation. By leveraging interfaces, you can write generic code like displayAll in Listing 9.13, which works with objects of any class. By contrast, by leveraging inheritance and polymorphism, you are only able to write generic code, which works with objects of classes that extend a particular parent class.
These examples don't save much code due to the small size of the displayAll method. However, there are cases where the code saved by doing this abstraction would be significant. For example, imagine an interface like this:
public interface Billable { public boolean prepareBill() public Object getBill(); }
This has two methods that classes like Customer, Client, PurchaseOrder, PhoneBill, and RentalTransaction might implement quite differently. The more classes that implement the same interface, the more reuse you get. The common code in this case might be a helper method printBills that calls these methods on a given list of Billable objects, and then does the necessary code to send the bill to the customer via fax or email. Keep in mind that the list of objects can consist of different objects. That is, the same list might contain some PhoneBill objects, some RentalTransaction objects, and some PurchaseOrder objects. The generic printBill code will handle all of them correctly because it defers the hard part to the objects themselves, by simply calling the commonly named methods prepareBill and getBill in each.
So, interfaces allow you to write generic infrastructure code such as displayAll, printBills, or doMonthEnd that works with any classes from any hierarchy, as long as those classes implement the appropriate interfaces. That lets you reuse the infrastructure code and easily adapt to new situations by simply creating new classes that implement the common interfaces. It also has a great side effect of forcing everyone on the team to use exactly the same method names for doing the same operation uniquely in different classes. Otherwise, you end up with many variations like displayOrder, displayAttendee, and printInformation. A consistent naming convention that is enforced by interfaces makes it much easier to read, understand, maintain, and adapt your code. The enforcement comes from the fact that any class that implements an interface must code all the methods defined in the interface or face a compiler error:
TestClass.java:1: class TestClass must be declared abstract. It does not define void display() from interface Displayable.
Because of this enforcement, an interface is often called a contract. It is a contract between the designer of the interface and the coder of the class. The contract states that the coder agrees to implement methods in his or her class that match the name and parameters of all the methods in the interface.
Once you have some interfaces designed and implement them in all appropriate classes, you will start to discover more and more infrastructure and helper code that can be written to exploit it. For example, you might state that all GUI widgets that accept text from the user support a verify method, which is enforced by writing an interface named Verifiable and insisting all coders implement it in such classes. You might have various classes that extend the entry-field GUI widget in Java (JTextField in javax.swing, as you'll see in Chapter 12), and support specific inputs, such as ZipCodeEntryField, PhoneNumberEntryField, CountryEntryField, and EMailEntryField. They would all code their own unique versions of verify to test that the current input is a valid zip code, phone number, country name, or email address.
Now your validation code when the OK button is pressed on the window containing all these entry field objects is easy:
Verifiable allEntryFields[] = getAllEntryFields(); String error = null; for (int idx=0; (idx
This just iterate through all of the objects, calling the verify method until one returns a non-null error-message string. If none do, all the input is valid. Otherwise, it displays the error-message text in an error-message window. Nice, no? Think of all the if/else code this saves! Interfaces, then, enable generic helper code that works on all objects that implement the interface.
Interfaces versus inheritance
You might have noticed that implementing interfaces is similar to extending classes, except that the methods in an interface are all abstract, and hence must be overridden. Also, code that uses objects whose type is declared to be an interface name is similar to code that uses objects whose type is declared to be a parent class name. If you can force all the classes that implement an interface to instead extend a common parent class, you can simply live without interfaces.
Consider the example of the verify method in the Verifiable interface. (Interface names usually end in "able.") If you instead declared a parent class named BaseEntryField and placed in it an abstract method named verify, and coded the xxxxEntryField classes to each extend BaseEntryField, you would achieve the same results. But you can't.
Why not? Because all the xxxEntryFields already extend the JTextField class, which is Java's entry-field widget class. You have to do that because you want these to actually be just entry fields with some extra functionality added. To "be an entry field" means, in OO terms, to "extend the entry-field class." And remember, you can only extend one class in Java, so you are not able to create a new parent class.
So, interfaces really are a way to give the polymorphic behavior of inheritance, even in cases where you simply can't extend a common parent class. They do this by refining the rule of polymorphism to this:
You can declare an object reference variable to be one class type or interface type, and yet assign to it at runtime an instance of a different class type, as long as the runtime class type extends the declared compile-time class type or implements the declared compile time interface type.
You use interfaces only when you can't use inheritance. For example, you saw how Java uses inheritance to support its utility classes Vector and Hashtable, by calling the equals method that all classes inherit and optionally override from Object. This allows any generic code to search any list of objects for a match. However, there is other generic code in Java that supports the notion of sorting a list, such as the sort methods in the java.util.Arrays class. These sort any given array.
In order to do a sort, or even a binary search, there must be a way to compare two elements and determine if one is greater than the other. What "greater" means for an object is subjective, as is the notion of equality, so it is left up to the objects themselves to decide. The sort methods work by comparing all the elements to each other by calling the compare method in each of them. This method is defined to take two objects, and the expected result should be -1 if the first is less than the second, zero if they are equal, and +1 if the first is greater. If you have a class such as Attendee and you want to support the ability to sort an array of these, you must implement the interface Comparator and implement this method that it dictates. The engineers decided not to put this method in the Object class, cluttering it up, and so instead had to go with the interface. If you try to sort an array of objects whose class does not implement Comparator, you will get an error.
Interfaces are callbacks
The interface examples in this chapter are all examples of callbacks, the most popular use of interfaces. (We mention this briefly so you understand the term when reading other Java books.) Callbacks involve generic helper code or infrastructure code that has no predefined knowledge of the objects it is working on, which are all defined to be an interface type. All it knows or cares about is that those objects support a method with a particular signature (as defined in the interface). The infrastructure code is simply trying to do its job, such as display information or print bills or sort objects, but it can't do it without help from those objects it is working on. So it "calls them back" by invoking the methods it knows exist in them (because they exist in the interface). It defers to the objects to do object-specific tasks.
Callbacks exist in any language that supports function pointers or inheritance of some kind. RPG IV has procedure pointers, so you could write callbacks. Consider the VECTOR service program from Chapter 6, for example. It contains memory allocation code that maintains a growable list of fields. The fields could be scalar fields or data structure fields. The key the service program allocates their memory, and a pointer to it is maintained in a linked list by the service program. Procedures make it easy to add, iterate, and remove from the list. However, this VECTOR code has no idea about the contents of each element in the list. It doesn't know if the element contains a single packed-decimal number or a structure with 12 sub-fields of different types. This is good, as it keeps the VECTOR code generic enough that any program can use it for anything.
This is bad, though, when it comes to supporting the ability to search for a given element. Suppose you'd like to be able to search by some key field for an element. For example, if you are storing structures of event attendees, you would like to easily search for an element in the vector that has a given phone number. You can't easily write generic code to do this, because you don't know what the memory map is for each element, so you don't know that it contains a phone number in the first 10 bytes. Indeed, another program might use this to store structures of information about orders, and might have an item-number key in the first four bytes.
You simply can't write generic code to do this search. However, you could write a generic procedure in VECTOR, named Search, say, that took a pointer to the item to search for (you don't want to hard-code the type of the key), and a procedure pointer to a procedure that the caller would supply. This Search procedure would walk the list of elements, and for each one, it would call the procedure pointer to compare that element's contents to the given key field, and would expect the called procedure to tell it if this is a match or not. The procedure pointed to would be supplied by each program that uses this, and so each program would know how to map the given structure, find the given subfield that is the key value, and compare it to the given key value. It would return *ON for a match and *OFF for no match. The generic search code would end the iteration at the first match, returning the element pointer.
You could similarly support a procedure Sort in VECTOR that sorted the list. To do this, it would need the callers to supply a pointer to a procedure to compare any two elements from the list, and return a number indicating if the first one is less than, equal to, or greater than the second one. Again, each caller would code this procedure differently depending on the contents being stored in the list. Comparing two data structures containing order information, for example, is different from comparing two data structures containing customer information.
Interfaces and constants
Interfaces can contain constants, so one other great use of interfaces is as a place to put constants needed throughout an application. By putting constants in an interface, any code that needs to reference them can simply implement the interface and access the constants directly, without qualifying their names with a class name. This can save much tedious typing.
Java supplied interface examples
Java itself makes significant use of interfaces, for example:
- Comparator in java.util. Implement this interface when your object supports sorting. A number of Java-supplied helper methods use this interface, including the sort and search static methods in the java.util.Arrays class.
- Cloneable in java.lang. Implement this interface when your object supports cloning or duplicating. This allows you to write code to make copies of an object.
- Enumeration in java.util. Implement this interface when your object contains a list, and you want to support iterating through that list. There is much Java code that uses this.
- Runnable in java.lang. Implement this interface and supply the run method it dictates to support running code in a thread (discussed in Chapter 11).
- Graphical user interface (GUI) event callbacks. These are a series of interfaces in java.awt.event and javax.swing.event to implement if you want to be informed of certain user-initiated events. For example, your GUI class would implement ActionListener and supply its actionPerformed method to be called back by the Java runtime when the user clicks a button. (This is covered in Chapter 12.)
- Java Database Connectivity (JDBC) framework. This is an interrelated set of interfaces that database vendors implement to supply JDBC drivers for Java-access to their database. As a Java programmer, you simply code to the methods in the interface, and your code magically works with any vendor's JDBC driver. (This is covered in Chapter 13.)
The Cloneable and Enumeration interfaces are particularly important to know about, due to their pervasiveness in most Java applications.
The Cloneable interface
The Cloneable interface is defined in the java.lang package. It is an interesting case—a completely empty interface! It highlights the fact that interfaces are really tags for the compiler. The java.lang package class named Object, from which all classes inherit, has a method in it named clone. The purpose of this base method is to create a new instance of a given object that is identical to the current instance:
ClassA objA = new ClassA(); ClassA objB = objA.clone();
It is important to have this method, because otherwise you might be tempted to use this:
ClassA objB = objA;
This does not copy objA at all! Rather, it copies the memory address in objA to objB, thus creating a second reference to the same object. To create a second copy of an object, it is up to the object itself to supply a method. This is what clone does. It essentially instantiates a new instance of this class (using the default constructor only) and copies bit-by-bit all the instance variables in the current class to the new class.
Sounds straightforward? Well, it is not! The problem is that you may have objects as instance variables, and as just discussed, copying an object variable only produces a second reference to the same object—not a second copy of that object, as you would expect in this case. That is, nested objects need to be cloned, too. This job is not something that the base clone method inside Object can take on by itself. It needs to be carefully implemented by each class—by you. So, Java makes the clone method in Object protected; it can only be called by you in your class. Thus, objB = objA.clone() will always fail with a "method not accessible" error message unless you do the following:
- Override and implement the clone method in your class
- In that override, change the access to public
And that's not all. The language designers were so anxious that clone only be supported by classes that really think they have implemented it properly, that they force you to also tell the compiler explicitly that you support this method. How? By defining your class to implement the Cloneable interface. Only if all this has been done can a user invoke the clone method on your class.
By the way, if you do decide to support this useful function, your clone method would do the following:
- Invoke your parent's clone method: super.clone();
- Invoke the clone method on all of your object instance variables (if they are not null) and do a simple assignment copy of all primitive instance variables
This is a case where an interface is used in a unique way, as a tag to identify this class's support for a particular function. Can you do this, too? Yes, you can. There is a way to determine programmatically if the class type of a given object implements a particular interface—by first using the Object method getClass to return the object's Class object, and then calling getInterfaces on the Class object. This returns an array of Class objects, one for each interface implemented by this class. You can then loop through this array, invoking getName on each entry looking for the interface name you are interested in. Better yet, you can simply use the instanceof operator, which you will see soon.
The Enumeration interface
The Java utility package java.util includes an interface named Enumeration. This is a convention that classes should use when they want to support the idea of enumerating or walking through a list of items. Suppose you have a class that is obviously a collection of something, such as the AttendeeList class from Listing 9.16. If you want to support the ability for users to iterate through each item in the list, do so by implementing the Enumeration interface. This will make it easier for users who are used to this convention from other Java-supplied classes, such as Hashtable. The Enumeration interface contains two method signatures, both taking no parameters:
- The hasMoreElements signature returns true if there are more elements in the collection.
- The nextElement signature returns the next element in the collection.
The intended usage for these is this:
while (obj.hasMoreElements()) nextObj = (MyClass)obj.nextElement();
Because the nextElement method is defined to return Object, you have to cast the result to the actual object type in the enumeration.
Let's update the AttendeeList class to support this interface. You could do this directly in your AttendeeList class, but by convention, the preferred way to do this is to write a second class that implements this. We will describe why this is preferred, and what you need to do in the first class to use the second class, shortly. First, the new class named AttendeeEnumeration is shown in Listing 9.17.
Listing 9.17: The AttendeeEnumeration Class
import java.util.*; /** * Helper class to enumerate the attendees at an event */ class AttendeeEnumeration implements Enumeration { private AttendeeList list; private int pos = 0; AttendeeEnumeration(AttendeeList list) { this.list = list; } public boolean hasMoreElements() { return (pos < list.getSize()); } public Object nextElement() { Object retobj = null; if (hasMoreElements()) retobj = list.getAttendeeAt(pos++); return retobj; } }
This class supports the two methods from the Enumeration interface. To do this, it requires an instance of AttendeeList, which is passed into the constructor, and it requires an instance variable pos to remember the current position. To support hasMoreElements, it simply compares the pos variable to the total number of elements, which it gets by calling a new method getSize in AttendeeList. To support nextElement, it returns the element at the current position and increments the current position. To get the element, it calls a new method getAttendeeAt in the AttendeeList object.
This class is a helper class, in that the intention is for users to never see it. That is why it is not defined as public. Rather, you create a new method in the AttendeeList class, named elements by convention, which returns an instance of it. However, the return type is defined to be Enumeration instead of the class name, to hide the details (class name). Remember, when you define a return type to be an interface name, you can return any object that implements that interface. The updated AttendeeList class, then, is shown in Listing 9.18.
Listing 9.18: The Updated AttendeeList Class
public class AttendeeList { public int getSize() { return attendees.size(); } public Attendee getAttendeeAt(int pos) { return (Attendee)attendees.elementAt(pos); } public Enumeration elements() { return new AttendeeEnumeration(this); } public static void main(String args[]) { // test attendee code AttendeeList attendeeList = new AttendeeList(); System.out.println(); attendeeList.register("Phil Coulthard"); attendeeList.register("George Farr"); attendeeList.register("George Farr"); attendeeList.display(); attendeeList.deRegister("Phil Coulthard"); attendeeList.deRegister("Phil Coulthard"); attendeeList.display(); // test the Enumeration support... System.out.println("Testing Enumeration Support"); Enumeration elements = attendeeList.elements(); while (elements.hasMoreElements()) { Attendee nextAttendee = (Attendee)elements.nextElement(); nextAttendee.display(); } } }
For brevity, this listing shows only the three new methods and the updated main method that tests the new Enumeration support. Note that the getSize and getElementAt methods were only added to support the code in AttendeeEnumeration, but they were made public anyway, in case others find them of value. The important new method is elements, which returns an instance of the new class, passing this into its constructor. Finally, the main method shows code to demonstrate how users will use this Enumeration support.
A separate class is used to support Enumeration so that the users can support multiple enumerations simultaneously. For example, because each call to elements returns a new object with its own pos instance variable, this is possible:
Enumeration elements1 = attendeeList.elements(); Enumeration elements2 = attendeeList.elements(); while (elements1.hasMoreElements()) ... while (elements2.hasMoreElements()) …
If you implemented the Enumeration interface directly in your AttendeeList class, this would not be possible. Whether you do it this way or not is up to you, of course.
The Enumeration interface is widely used in Java. (There is a replacement for it in JDK 1.2.0 and higher, named Iterator, but most people still use Enumeration.) Indeed, all the work you just saw to support Enumeration in the AttendeeList class is actually a waste of time! Why? Because AttendeeList uses a Vector object for the list, and the Vector class already supports an elements method that returns an Enumeration object. So, you can get the same functionality by simply coding the elements methods in AttendeeList as this:
public Enumeration elements() { return attendees.elements(); // return Enumeration obj }
This certainly makes it easier! You simply delegate the call to the Vector object, which has done the work for you already. You really only have to code your own Enumeration class if you are using arrays. Even Hashtable, as mentioned, already has an elements method.
This gives an opportunity to both expand the usefulness and simplify the Helpers class from earlier in this chapter. Currently, it only supports arrays and Vector objects in its overloaded versions of displayAll. Let's change it now to support only arrays and Enumeration objects. This will be sufficient, because users wanting to call it with a Vector or a Hashtable can simply call elements on their Vector or Hashtable object and pass in the result. We hate to write redundant code however, so even for arrays, let's internally convert them to an Enumeration object first, and then call the Enumeration version of displayAll under the covers.
To do this, you first need a very handy class that implements Enumeration for any array. It is shown in Listing 9.19. This is a handy class. We suggest you keep it around, since it will work with any array.
Listing 9.19: The ArrayEnumeration Class
import java.util.*; public class ArrayEnumeration implements Enumeration { private Object list[]; private int pos = 0; public ArrayEnumeration(Object list[]) { this.list = list; } public boolean hasMoreElements() { return (pos < list.length); } public Object nextElement() { Object retobj = null; if (hasMoreElements()) retobj = list[pos++]; return retobj; } }
The new and improved (how can something be both new and improved?) Helpers class is shown in Listing 9.20.
Listing 9.20: The Updated Helpers Class
import java.util.*; public class Helpers { public static void displayAll(Displayable objects[]) { displayAll(new ArrayEnumeration(objects)); } public static void displayAll(Enumeration objects) { Displayable currentObject = null; while (objects.hasMoreElements()) { currentObject = (Displayable)objects.nextElement(); currentObject.display(); } } }
With this change, it is very easy to change the Hashtable version of the AttendeeList class from Chapter 6 to also use this Helpers class, if you wanted. You'd simply have to add "implements Displayable" to the Attendee class, and change the display method in AttendeeList to call your new Helpers method:
public void display() { Helpers.displayAll(attendees.elements()); }
Chapter 11 revisits this Helpers class and the updated Hashtable version of AttendeeList that uses it.
O2O Object to Object Casting Objects
You have seen already that you can cast objects using syntax similar to that used for casting primitive types (discussed in Chapter 5). You do this to tell the compiler that an object reference variable declared as one type is in fact of another type. For example, many Java-supplied classes are generic utility classes that are defined to return an object of type Object, the root parent of all objects. You have to cast the returned object to your class type to call methods defined in your class. You saw this for the elementAt method in Vector, the get method in Hashtable, and the nextElement method in Enumeration.
By now, you can appreciate why this is necessary. On the one hand, since you want to write utility code that is generic and reusable with objects of any type (such as the Vector and Hashtable classes), you use the base Object as the defined type of parameters and return types, so as not to restrict use to objects of specific classes. On the other hand, Java's strong typing means you are prevented by the compiler from calling methods in an object that are not declared in that object's type. So, after getting an object out of a Vector via elementAt, for example, you can't call any methods except those defined in Object. Casting tells the compiler that this object's type is not actually Object, but really XXX. This allows you to subsequently call any methods defined in class XXX.
Here are the explicit syntactical details of this casting:
- Casting is done using the cast syntax: Type1 result = (Type1)object;
- Casting can only be to a target class type that extends the source class type (lower in the hierarchy)
- Casting can only be to the actual type of the object at runtime or a parent of that type
The last rule is the most important one. When you cast primitive values, you actually morph the data type from one to another. However, when you cast objects, the object type is not changed! In reality, the object must already be of that target type at runtime, and you are simply telling the compiler that this is the case. If you do try to cast an object to a type that is different than the object's actual runtime type, you will get a cast exception error at runtime. Well, the object must not be different from the actual runtime type or a parent of that runtime type, although as you know, you do not legally need to cast in order to assign an object of class C1 to an object reference variable of class C2, if C2 is a parent of C1. In other words:
C2 myVariable = new C1(); // legal if C1 extends C2 C2 myOtherVariable = (C2)new C1(); // legal but not required
You can also cast to an interface if that actual runtime type implements that interface, but once again, this is not necessary because you can legally assign an object to an interface reference variable if the declared type of that object implements that interface:
I1 myVariable = new C3; // legal if C3 implements I1 I1 myOtherVariable = (I1)new C3; // legal but not required
Because of the third rule, it is often good to test whether the object type is what you think it is before attempting the cast. This can be done using the instanceof operator, which is a binary operator that takes an object and a class name or interface name as operators, and returns true if the object is of that class type or implements that interface. Here is an example:
Object obj = myVector.elementAt(0); Employee emp = null; if (obj instanceof Employee) emp = (Employee)obj;
Note the class name to the right of instanceof is not in quotes. The object must be of the given class type or child of that class to get true as a result. For interfaces, the object must implement that interface. The instanceof operator is only used in expressions, since it has no side effects.
The typical need for this operator is when you have a list (such as an array or vector) of different objects. For example, you might define an array of Employee objects and then populate it with objects of various child classes of Employee. If every method you want to call is in the Employee parent class, you do not need to cast. However, if you want to call a method that only exists in one of the child classes, you would need to test the type and subsequently do the cast, like this:
Employee emps[] = getAllEmployees(); for (int idx = 0; idx < emps.length; idx++) if (emps[idx] instanceof SalesEmployee) ((SalesEmployee)emps[idx]).printSalesCommission();
With the proper design of class hierarchies, you will find the need to use instanceof is rare. For example, you could have placed that method in the Employee class and only uniquely overridden it in SalesEmployee. If the method is in the parent class, you need not cast.
Common Methods to Override
You know now that all classes you create in Java implicitly extend the Object class. This gives users of your class access to all the non-final, non-abstract, non-private methods in this universal class. However, there are a few methods, shown in Table 9.1, that you should consider overriding in your new class every time. This is not to say that every class you create needs to support all of these methods, but you really should make an explicit decision in every class whether to implement these methods or not.
Method |
Description |
---|---|
finalize |
This method is called by Java when an object of your class is "swept up" by the garbage collector. If you have any finalization or cleanup code to be done when the object is destroyed, such as closing any possibly still open files, put it here. |
clone |
You have seen this already; it is a convention to supply this method if you want to enable object duplication. |
equals |
By convention, users will invoke this method to compare two objects of your class. If you want to support the idea of equality testing, you must supply this method, as the default implementation returns false unless you are comparing an object to itself. |
hashCode |
This returns a hash code, or unique key, derived from this object. If you implement the equals method, you probably need to override this one too, so as to return a unique hash value for different objects, usually based on instance variable values. |
toString |
This method is called whenever someone tries to convert an object of your class to a string, such as by specifying an object name in the System. out.println method. This should return a string with something useful in it. |
The OO Way of Life
By this time, you have seen all the language tools for object-orientation, as well as all the terminology. So, are you ready to design your first object-oriented project? Probably not. Using these tools efficiently is a skill that comes with time. The process of designing a good object-oriented system is different from the procedural systems that you are used to. It involves a somewhat new way of thinking—thinking in terms of objects.
Consider how you design or develop now with RPG. Everyone is different, but chances are your up-front design centers first around the functionality (subroutines, procedures, and calculation specifications) that you will need to solve the problem. The data (data structures, arrays, and so on) that you will need to support this functionality come in at a secondary stage. We are not talking about the database design here, although, for many, that design emerges from this function-centered thought process. This design process leads to functions throughout the application that access common data, whether data structures or external database files.
With OO, the design process is reversed. Rather than first thinking about the functions required to get the job done, you are required to first consider the objects needed. And what are objects, again? Instances of classes. And what are classes, again? Data (variables) and supporting functions (methods). You need to start thinking of data and functions as a single unit in order to grasp OO. Then, you start thinking about how to partition your "problem domain" into such units (objects). This design process leads to encapsulation, minimizing the number of places that code accesses particular data. Instead of accessing data directly, most code instantiates common objects and calls methods on that object to read, change, or insert data.
You will be surprised at some of the things that become objects in an OO world—for example, who would expect a color like red to be an object? Who would expect a two-dimensional point (with x and y coordinates) to be an object? In Java, they are. Indeed, because in Java executable code can only exist inside of objects, you'll find that everything is an object—well, to be precise, everything is either an object or a primitive variable inside an object.
In search of objects
There is no magic formula to help you begin thinking in terms of OO, but there are many books that describe the up-front part of an OO project—the analysis and design phases (OOA and OOD). You will want to read one or two. All of these books at least discuss the following:
- How to clearly articulate the "problem domain" by writing use cases.
- How to "find" objects in your problem domain.
- How to find and define the "roles" of your objects (that is, the methods).
- How to find and define the "relationships" between your various objects (that is, who uses what, and who extends what).
- How to consistently document your design. That is, most books introduce a language for writing down your object-oriented design, with conventions for depicting classes, objects, relationships, and so on.
The collective approach that a book uses to describe all this is known as a methodology. Such methodologies are usually named after the person or persons who defined them. Is there a "best" methodology? Certainly there is: the one that you decide to use. Pick one, roll up your sleeves, and dig in. Or, ultimately, you can hire or contract-in experienced OOA/D programmers and go with their recommendation.
One methodology of which you should be aware, however, is UML (Unified Modeling Language). It is a convergence of a number of leading methodologies, and it appears poised to become the industry standard. It was predominantly defined by highly regarded methodologists (Grady Booch, Ivar Jacobson, and Jim Rumbaugh) at Rational Software, the makers of the immensely popular OOA/D tool Rational Rose. For more information on UML, see Rational's Internet Web site: www.rational.com/uml.
In an OO project, you spend much more time at the beginning, thinking about the problem to be solved, than you do in the later stages. This is a good thing, because as you know from experience, the more time you spend in the design stage of the project, the less time you have to spend coding and testing the project. The objectives of OO are to reduce maintenance efforts and increase code reuse. By spending time analyzing the problem domain in the search for objects, you typically improve your final time-to-market and ongoing quality, because you:
- Drive out a better problem statement (a crisp list of requirements)
- Derive a system that better models the real-world problem, leading to end-user satisfaction and higher quality
How do you find problem-domain objects? The usual trick is to start with your requirements statement and look for nouns. These are the "low-hanging fruit"—they will almost always become objects. Examples are car, employee, manager, bank account, customer, and service. Your system will also require many implementation objects, in the end, to do the job. These are helper objects with commonly needed functions, like tax calculations, date manipulations, string manipulations, and monetary calculations. After some experience, you will get better at finding these up-front. Initially, though, you might have to stumble into them as you go.
How do you find roles? These affect what methods and variables your objects will have. The usual trick is, again, to start with your requirements statement and this time look for verbs. For example, you might expect to have methods to do printing, sorting, calculating, ordering, and billing. Establishing which roles are played by which objects is also important at this early stage.
How do you find relationships? This requires you to address design issues like class hierarchies, object parameters, object instance variables, and collections of objects. How objects will use each other, including quantitative metrics ("how many"), is important to consider at this early stage. This is where you typically have to make some tough decisions, which will be affected by such real-world things as performance.
In our experience, some of the more interesting decisions that programmers have to face center around inheritance. The trick is to know when to inherit from another class rather than do one of the following:
- Declare an instance of the parent class as an instance variable in your class. This is known as containment—your class "contains" an instance of another class.
- Pass in an instance of the parent class as a parameter to the methods that need it. This is known as usage—your class "uses" an instance of another class.
Again, there are general rules that cover inheritance. The idea in OO is to define objects that model, as closely as possible, the real world you are interested in. This means you rely on the real world to make decisions for you wherever you can. Therefore, to decide if a class should inherit, contain, or use another class, ask yourself some questions about the real-world relationships between the two classes:
- Is the new class an example of the first class? This is the classic "is a" relationship, and if you answer yes, your new class should extend the first class. Is a dog an animal? Yes, so the Dog class should extend the Animal class. Is a purchase order an order? Yes, so PurchaseOrder should extend Order.
- Does the new class have objects of the first class? This is the classic "has a" relationship, and if you answer yes, your new class should contain an instance variable of the first class. Does an employee have a salary? Yes, so the Employee class should contain an instance of the Salary class. Does an order have a purchase item? Yes, so the Order class should have a PurchaseItem object as an instance. Indeed, it probably should have a list of PurchaseItem objects.
- Does the new class use an object of the first class? This is the classic "uses" relationship, and if you answer yes, your new class should accept as a method parameter an instance of the first class. This is typically a case where you are defining a helper or utility class that performs some commonly needed function on any given object. Does a payroll system use employee objects? Yes, so Payroll needs a list of Employee objects as input. Does a reporting system use reportable objects? Yes, so Report needs a list of Reportable objects as input (where Reportable is probably an interface).
OO design tips
Here are some simple tips about OO programming:
Tip 1: Separate, separate, separate
Keep each class small, and separate each functional aspect of your application into its own class or classes. For example, notice how the registration application earlier in this chapter uses a class for each attendee, a class for the list of attendees, and a class for the user interface. This design pattern will apply to many situations; many of your applications will have a class representing an entity key, a class representing an entity that contains an entity key, a class containing a list of entities searchable by key or sequentially, and one or more user interface classes.
Tip 2: Leverage your database skills
As a good starting point in designing your new application, consider making a class for every field in the database you are using, such as EmployeeId, Salary, OrderAmount, ShipDate, ZipCode, PhoneNumber, and Country. You will find it handy to have constructors for these that accept primitive values, and also to supply methods to verify that a given primitive value is syntactically valid. At a minimum, the constructor and verify methods should take a String object, as that is what you get from the console and from GUI fields. All these classes should supply an equals method.
Look for grouping opportunities that reduce complexity. For example, you might want to have an Address class that groups the various objects that make up an address. This will make it easier to prompt for and print an address.
Look for common basic types that can become parent classes. For example, you might find it saves many headaches to have a Money class that knows how to do math on a monetary value, such as applyDiscount. Then, Salary, UnitCost, and OrderAmount will extend this class.
For each record format, especially in logical files, define a class that contains one or more of the field classes, for example, Department, Employee, Order. For keyed record formats, create a class with the same name but with Key appended, as in DepartmentKey, EmployeeKey, and OrderKey. This class should contain appropriate objects from the field classes defined above. This class should also supply equals and hashcode methods and should implement the Comparator interface.
If the record format has fields that contain "type" information that affects processing, such as employee type, salary type, order type, or customer type, make separate child classes for each possible type. For example, you might have SalariedEmployee and HourlyEmployee child classes, or PurchaseOrder and ServiceOrder child classes. Put the unique processing required in overridden methods in the child classes. Supply an object factory class to read the next record of the database, create an instance of the appropriate child class, and return that to the caller.
For logical files, especially join logical files, the corresponding class is often referred to as a business object. This represents an entity that is a composite of information from other files (objects). The cool thing about business objects is that they hide inside them all the complexity involved in reading all the database files necessary to populate them. Indeed, with enough coding, you could support update operations by writing to the physical files yourself. Once you have business objects like Order, Policy, and Contract, all remaining code in the application becomes quick and easy to write. There's no database access to be concerned with, as it is all nicely encapsulated behind a wall.
Tip 3: Use the "is a" rule with a grain of salt
Despite the "is a" rule, do not inherit from an existing class unless you intend to override one or more methods in the parent class. This saves you from using up your single allowance for extending. Also, sometimes you want to extend, but can't. For example, you probably want to consider writing a Money class that is a refinement of BigDecimal that forces the scale (decimal place) to two after every operation. You can't do this by extending BigDecimal, though, because it is an immutable class. You have the same problem if you want to extend String and add more methods. In both cases, your new class will have to contain an instance of the original class as an instance variable, and the methods will operate on it. Be sure to include a method to return the contained BigDecimal or String object, though, so callers can get to the methods in those classes.
Tip 4: Create child classes that add functionality
Despite the terminology, subclasses should offer a superset of functions compared to their parent class. If you find yourself wanting to remove methods from your parent class because they do not apply to your child class, you probably should not be inheriting from that parent. Even if in real life there is an "is a" relationship, there might not be one in the actual code.
For example, a manager might not be an employee, if class Employee has methods dependent on hourly wages versus salary. You might have to abstract-out a third common class named HR (for "Human Resource") from which Employee and Manager both inherit. (Of course, many would argue that the "is a" relationship does not apply to managers and humans!) Another option here is to have a class named JobDescription that ManagerJob extends, and then to contain an instance of ManagerJob in the appropriate Employee objects. This gives you the flexibility of defining additional employee job descriptions and supporting multiple job descriptions per employee.
Tip 5: Strive for easy and elegant user code
When designing your class hierarchies and classes, keep in mind the users of your classes. You want to minimize their efforts. The main part of your application should be as simple and elegant as possible. This means letting your classes do as much as work as possible. For example, it might be as simple as this:
TheApp app = new TheApp(); app.run();
Tip 6: Strive for small methods
If you find yourself with a very large and complicated class that has some really outrageous methods, look hard at it. You might need to abstract-out some code into a base or helper class, and use more and smaller methods. This makes the code easier to maintain and understand.
Tip 7: Balance anticipated function versus bloat
The trick for long-lasting classes is to embellish them with as many methods as you predict might ever be needed. But don't go overboard! Do all private variables really need setXXX methods? Will someone really want to change these variables? Do you need a particular new method, or can the result be achieved by using a combination of existing methods?
Tip 8: Be wary of large numbers of instance variables
If your class has dozens of instance variables, consider grouping those variables into classes of their own and instantiating instances of them. Remember, classes do not have to have methods. Moving related variables into their own class will make your code more elegant, and you might then discover those helper classes have other uses, too. You might even discover that some of the manipulations you were doing directly on those variables can be moved into methods of the new class.
Tip 9: Watch for static candidates
Do you have methods that do not act on any of your class instance variables? Maybe some utility method you wrote takes all its information as parameters? If so, either forgo the parameters and use instance variables instead, or make the method static so it can be used by others without requiring an instantiation of your class.
Summary
This has been a whirlwind "introductory" chapter to object-orientation. This chapter briefly introduced you to the following OO terminology:
- Encapsulation, inheritance, and polymorphism
- "Has a," "is a," and "uses" relationships
- Methodologies for specifying and documenting object-oriented systems.
It also introduced some new specific Java constructs:
- The extends keyword
- The protected, abstract, and final modifiers
- Interfaces and the implements keyword
- The instanceof operator
You also learned about:
- The java.lang.Object and java.lang.Class superclasses
- The java.lang.Cloneable interface and the java.lang.clone()method
- The java.lang.Enumeration interface
- Design patterns, object factories, and business objects.
You should feel more "acclimatized" to OO now and be ready to pursue it further with other books, like those in "References," below. Subsequent chapters will continue to help you get familiar with OO.
References
Gamma, Helm, Johnson, and Vlissides. 1995. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, ISBN 0201633612.Gilbert, Stephen, and Bill McCarty. 1998. Object-Oriented Design in Java. Waite Group Press, ISBN 1571691340.
Larmon, Craig. 1997. Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design. Prentice Hall, ISBN 0137488807.