3. Object Orientation in Java
In the preceding chapter, we touched on Java's object-oriented features when we talked about variables and classes. Object-oriented programming (OOP) is such a fundamental part of the Java language that we had to introduce some of the concepts even when writing very simple Java programs. Before we go any further, let's explore object orientation in more detail.
We start off with a general explanation of what object orientation really is and why Java is a better language because of it. We also introduce some terminology that describes the conventions of the classes we touched on in Chapter 2, "Java Programming Basics." With a better understanding of what object orientation is-and, yes, a little more technobabble-we can then review what we covered in Chapter 2 with a new eye. Finally we delve into some advanced object-oriented features of the Java language.
Advantages of Object Orientation
Object orientation is possibly the most popular buzz phrase in all of computing. Like all buzz phrases, there are a ton of different interpretations of what it actually means. In Chapter 1, we described the definition that is generally agreed upon: Object orientation is a methodology that makes our problems easier to solve. But that definition describes the "product" that a thousand stress management books, video tapes, and courses try to sell us. Before we chant the mantra of object orientation, let's use what we learned from Chapter 2 and iron down a programming-level definition.
Classes are the real nuts and bolts of object orientation. As you'll remember, a class describes a type that contains both subroutines (or methods) and data. Since a class describes a type, we can create variables that contain both methods and variables. Such variables are objects. Objects differ from the variables of procedural programming languages because they can define how data can be changed. Subroutines in object-oriented languages differ from their counterparts in procedural languages in that they have a set of data (the data members defined in the same class) which they can change but that other methods can't.
This definition describes the implementation of the methodology of object orientation from the programmer's point of view. Now we can begin to discuss why this is advantageous to the programmer. Before we go on, take a look at Table 3-1 for a quick review of some Java terms that are related to object orientation.
Term | Definition |
---|---|
Package | A unit that contains entities of the Java language, including classes. |
Class | A definition of data type that contains data and subroutines. |
Method | The Java name for subroutine instantiation. |
Construction | The creation of class into a variable; occurs at runtime. |
Instance, object, instantiation | A variable of class type that has been instantiated. |
Access modifier | Describes what set of classes can have access to a member of a class. The access modifier is also used to indicate that a class can be accessed from outside its package. |
Remember the private access modifier we were using in Chapter 2? When we declare a variable private, we are practicing data hiding. We are hiding the data (the variable) from all of the subroutines in our program except those defined in the same class. When does it make sense to hide data? Let's consider a paradox that often arises in procedural programming.
If you've ever taken an introductory programming class, at some point your instructor probably told you not to make variables global (that is, accessible from any subroutine in your program). If a variable is global and a bug develops relating to that variable, it becomes very hard to track down precisely which subroutine is causing the bug. Somebody maintaining your program-buggy or not-will have a tough time figuring out what is happening to that variable.
Fair enough. But what if you write a program that has, say, eight subroutines, and four of them use the same variable. In accordance with the taboo against global variables, you obligingly pass it among the four methods that use it. But this is really just a workaround to avoid declaring a variable global. What you really want is for the variable to be global for those four subroutines that use it.
In Java, we just slap that variable into a class, declare it private, and throw those four methods in along with it. When somebody else looks at our code, or when we look at it long after we have forgotten how it actually works, we know that those four methods are the only ones in the program that are allowed to work with the variable.
In the process of hiding data, we implicitly describe a relationship between the variable and the methods contained in the same class. If a method outside of the class wants to alter the variable, it has to do so by calling one of the methods defined in the class. This relationship between the members of a class is a concept called encapsulation , which is closely related to data hiding.
Let's consider the value of encapsulation when we are dealing with a single variable. Suppose the variable is an integer, and the purpose of one of our subroutines is to print out that number of blank lines. If we were writing this in a procedural language, we would be obligated to check to make sure that the integer isn't a negative number. If we encapsulate the subroutine and the variable in a class, we don't need to check to see if it's negative. Since only the methods within the class are allowed to alter the value we just write all of our methods so that none of them ever sets the integer to a negative value:
public class printLines { private int linesToPrint=0; public void printSomeLines() { for (int i=0;i<=linesToPrint;i++) { System.out.println("");} } public void setLinesToPrint(int j) { if (j>0) { linesToPrint=j;} } }
Since the only way the value can be changed is through the setLinesToPrint method, we only have to check for negativity there. So we save a few lines of code. The advantages are much more apparent when we have several variables contained in our class.
Let's switch back to procedural language mode to see why. Previously we described a scenario in which there was a single variable that many subroutines did business with. Let's expand on this and say the variable is an array, and you need to keep track of a position in the array. Thus, every time you pass the array you must pass an index variable, too. Then, the program gets more complicated, and every time you pass this particular array you also need to pass along an entirely different array and its index.
We now have a set of data that is entwined-each member of the set has a relationship with all of the other members. This means that in a procedural language, each subroutine that deals with any member of the set is responsible for the upkeep of that relationship. Since each subroutine is simply acting on the data, it's hard to discern how the relationship is being maintained. If the relationship is breaking down at some point, determining which particular action is messing it up will be hard just like in human relationships!
Since our personal lives are well beyond the scope of this text, let's concentrate on how object orientation helps us to maintain the relationships within our programs. Let's look at a simple example. Suppose we have an array of characters. Our challenge is to start at some position in the array and swap the character that is already there with some other character. The next time we need to do a replacement, we start at the next position in the array. The following class will do the trick:
public class replaceChars { private char myArray[]; private int curPos=0; public replaceChars(char someArray[]) { myArray=someArray;} public boolean replaceNextChar(char c, char d) { if (newPositionSet(c)) { myArray[curPos]=d; return true;} else {return false;} } private boolean newPositionSet(char c) { int i=curPos; while (i<myArray.length) { if (c==myArray[i]) { curPos=i; return true;} else {i++;} } return false; } public boolean atEnd() { return(curPos==myArray.length-1);} //subtract 1 because positions in an array //start at zero }
We were able to solve our problem completely in a few lines of code. Also, notice that our newPositionSet method is marked private. This goes back to the concept of data hiding. Instead of hiding data, we are hiding a method that changes the data. Presumably, we don't want the position to be changed unless a character has been replaced.
Now, consider the difficulties involved in solving our problem using a procedural language. First, we can't hide our data or any helper subroutines. This means that we must always check and make sure that our curPos variable is in range. Second, we have no straightforward way to keep track of our current position in the array.
The simple challenge we solved above isn't impossible in procedural languages. However, our class has the advantage that it is actually a type definition. When we encapsulate our methods and variables into a class, we are really encapsulating our solution. Since our solution is a type, it's easy to reuse our solution we just instantiate another variable:
replaceChars solutionl=new replaceChars("Java is great!"); replaceChars solution2=new replaceChars("I want to learn more java!"); while (!soulutionl.atEnd()) {solutionl.replaceNextChar('a','x');} while (!solution2.atEnd()) {solution2.replaceNextChar('o','y');}
Once we define methods that interact correctly with a set of data, we needn't worry about the details of our code. If we tried to solve our simple problem using a procedural language, we would always have to maintain the relationship between our position integer and the array. If we wanted to perform our operation on multiple arrays, the complexity would be compounded. Our object-oriented approach allows us to use our code at a higher level of abstraction.
Of course, abstraction isn't new with OOP Procedural languages define sets of action in subroutines, and the subroutines are thus reusable. Also, simple data types like integers are just abstractions of how bits are stored in the computer's memory. Object orientation simply takes this abstraction to a new level. It marries the abstractions of data types and subroutines, so that the relationship between data and action can also be reused.
Once we solve a problem by encapsulating methods and variables, we can easily deploy our solution over and over again in our programs. But what if we find ourselves faced with a new problem that is very much like one we have already solved? Object orientation also has a feature, inheritance, that allows us to reuse already-coded solutions when solving new, similar problems.
Let's see inheritance in action. In the code fragment of the preceding section, we call replaceNextChar for the same two characters again and again. Wouldn't it be nice if we could do this using one of the methods in the replaceChar class? We could just add it to the class and recompile. But let's say somebody else is already using the original replaceChar class. Now we have to maintain two classes of the same name, which will probably cause confusion down the road. Instead, we can create a new class that inherits the characteristics of our replaceNextChar class:
class betterReplaceNextChar extends ReplaceNextChar{ public int replaceAllChars(char c, char d) { int i=0; while(!atEnd()) { replaceNextChar(c,d); i++;} return i;} }
Now we have a new class that has all of the methods of the ReplaceNextChar class, plus the one additional method we have defined here. We have been able to encapsulate the solution to a new problem by extending the class. As we saw when we wrote our first applet in Chapter 2, inheritance is a very important concept in Java programming. We explore it more fully a bit later in this chapter.
Throughout our discussion, we have repeatedly mentioned that object-oriented code is easier to maintain. But what exactly does code maintenance mean? After all, once the code compiles, it will presumably work forever-it's not like we're building bridges that will eventually suffer from metal fatigue. Software, however, is expected to adjust to its environment in ways that physical structures aren't expected to.
For instance, a program that was originally designed to keep track of payroll needs to be updated to take care of health care benefits. Or, a networking system originally designed to just broadcast messages to neighboring machines now needs to listen for updates from a server on Wall Street. We could make a very lengthy list of examples, but our premise is that software lives in a world of infinite and ever-changing complexity. Problems arise that the original programmer didn't foresee, or new demands are placed on the system. Rarely does a production computer program go unchanged for more than a few years. When it is changed, someone new may be making the changes, or the original programmer may have long ago forgotten the intricacies of the program. In either case, whoever is making the changes would prefer not to start from scratch. Modern programming languages must provide features that ensure programs' maintainers that the programs can be easily modified to meet new needs.
This is the underlying goal of object-oriented languages, and all of the features we have mentioned support this goal in some way. For instance, reusability directly implies maintainability. If someone can reuse code we have written to solve new problems, it will be easier to grow the program to deal with new circumstances. Additionally, the code itself is easier to understand. When we employ data hiding by declaring a variable private inside a class, any Java-fluent programmer will know that only the methods in that class are able to alter that variable. Likewise, the encapsulation of methods and variables makes it easy to discern the relationship between data and action in our programs.
Encapsulation also makes it easier to add features to the program. As long as a class works as it is supposed to, someone trying to add features to our programs won't have to figure out the underlying details. All they need to know is how to use the public methods and constructors.
There's another implicit advantage of encapsulation. Since other objects in the program can only interface with a given object through its public methods and constructors, the private parts of the system and the code making up the public methods and constructors can be changed without breaking the entire system.
Why is this advantageous? Let's consider the problem of the year 2000. As the millennium approaches, a lot of specialist programmers are going to make up to $500 per hour making sure that institutional computers don't misinterpret the turn of the century Why? There are tons of mission-critical programs that won't correctly interpret the new millennium because they use only two digits to store the year. This means that your bank may start thinking that you are -73 years old, or a phone call from the East Coast to the West Coast starting at 11:59, December 31, 1999, could be logged as taking 99 years!
The problem is hard to fix because these programs predate object orientation. Written in procedural languages, each has its own individual way of comparing two dates. Those high-flying specialists are going to be sifting through tons and tons of individual subroutines, looking for places where dates were compared incorrectly Let's consider how an object-oriented approach would eliminate this problem. Below is the Year class, in which we deliberately messed up the comparison. (So our example doesn't make us look like complete idiots, let's just say our original intention was to store the Year in as little space as possible.)
public class Year { private byte decadeDigit; private byte yearDigit; public Year(int thisYear) { byte yearsSince1900=(byte)(thisYear-1900); decadeDigit=(byte)(yearsSince1900/10); yearDigit=(byte)(yearsSince1900-(decadeDigit*10));} public int getYear() { return decadeDigit*yearDigit;} //Other methods }
We go on to build dozens of systems that rely on this class to store the date, and other programmers use it, too. Then one day in December of 1999, we realize what boneheads we are. Time to call in the $500-an-hour consultant? Of course not! All we have to do is rewrite the implementation of the class. As long as we don't change the public method declarations, everything in all of those systems will still work correctly:
public class Year { private byte centuryDigit; private byte decadeDigit; private byte yearDigit; public Year(int thisYear) { centuryDigit=(byte)(thisYear/100); int lastTwo=(byte)(thisYear-(centuryDigit*100)); decadeDigit=(byte)(lastTwo/10); yearDigit=(byte)(lastTwo-(decadeDigit*10)); } public int getYear() { return decadeDigit*yearDigit*centuryDigit;} //Other methods }
Now, we're set until the year 12799, and some hack wasn't paid $500 an hour to fix our code!
Java's API, which we discuss at length in Chapter 6, contains a
Date class that won't break at the turn of the century.
We have illuminated the concepts underlying some of the code we were writing in Chapter 2, and, we hope, convinced you that they are sound advantages of the Java languages. Now, we're going to focus on how these features actually work in practice.
When we use the term class hierarchy, we are describing what happens when we use inheritance. Let's say we have three classes; call them Mom, Son, and Daughter. The Son and Daughter classes inherit from Mom. Our code would look as follows:
class Mom { //declarations, definitions } class Son extends Mom ( //declarations, definitions } class Daughter extends Mom { //declarations, definitions }
We just created a class hierarchy! Just like an organizational hierarchy it's very easy to visualize.
Table 3-2 lists some terms that we use to describe our hierarchy. Mom is the base class-the class on which other classes are based. Son and Daughter are subclasses of Mom, and Mom is the superclass of Son and Daughter.
Term | Definition |
---|---|
Class hierarchy | A group of classes that are related by the inheritance |
Superclass | A class that a certain class extends |
Subclass | A subclass that is extended from a certain class |
Base class | The class in a certain hierarchy that is superclass of all other classes in the hierarchy |
Now that we have some vocabulary to work with, we can talk specifically about class hierarchies in Java. First, all classes in Java have exactly one direct superclass. As we discussed in Chapter 1, this characteristic of the Java language is known, in object-oriented parlance, as single inheritance. Of course, a class can have more than one superclass. Mom and Daughter, for example, could both be superclasses of another class, Granddaughter.
"But wait," you may be wondering, "if all classes have exactly one direct superclass, what is Mom's superclass?" The class hierarchy we describe here is actually a subset of a huge class hierarchy that contains every single class ever written in Java. Figure 3-1 illustrates how the small class hierarchy we developed fits in with this much larger hierarchy. At the top of this class sits a special class named the Object class.
Figure 3-1: How our class hierarchy fits in with the entire Java hierarchy.
When we declare a class, if we don't explicitly specify that it extends some other class, the Java compiler assumes we are extending the Object class. Thus, the following definition for our Mom class is exactly equivalent to the definition we gave earlier:
class Mom extends Object { //declarations,definitions }
So why is this global, all-encompassing class hierarchy a good thing? Since all classes inherit from the Object class, we know that we can call the methods of the Object class.
The Object class's methods include methods that determine equality and that allow all Java classes to use Java's multithreaded features. Also, we never have to worry about combining different hierarchies, because all hierarchies are simply subsets of the global Java hierarchy And finally class hierarchy guarantees that every class has a superclass; we'll see the importance of this in Chapter 6, when we look at the object container classes.
Each class in Java has three predefined variables we can use: null, this, and super. The first two are of type Object. In short, null represents a nonexistent object and this points to the same instance. Super is a special variable that allows access to the methods defined by the superclass. Let's examine each of them in turn.
Null Variable
In Chapter 2, "Java Programming Basics," we explained that a class needs to be instantiated before it can be used. Before it is instantiated, it has the value of the null variable, and we say that the object is null. When an object is null, we aren't allowed to access any of its members because there hasn't been an object created for them to be associated with. If we try to access members before they are created, we run the risk of causing a NullPointerException, which will halt our program. The following method runs that risk because it takes a ReplaceNextChar object as a parameter and uses it without checking to see if it is null:
public void someMethod(ReplaceChars A) { A.replaceNextChar('a','b');}
The following code, which calls someMethod, will generate a NullPointerException because ReplaceNextChar hasn't been constructed:
ReplaceChars B; someMethod(B);
To keep our programs from crashing, we must check objects to make sure they aren't null before we attempt to use them. This rewritten someMethod checks to make sure that A isn't null before attempting to access one of its members:
public void someMethod(replaceChars A) { if (A==null) { System.out.println("A is null!!!");} else ( A.replaceNextChar('a','b'); }
This Variable
Sometimes you'll need to pass a reference to the current object to another routine. You can do so by simply passing the variable this. Let's say that our Son and Daughter classes define a constructor that takes a Mom variable in its constructor. The this variable allows the Son and Daughter to keep track of Mom by storing a reference to her in a private variable:
public class Son{ Mom myMommy; public Son(Mom mommy) { myMommy=mommy;} //methods } public class Daughter { Mom myMommy; public Daughter(Mom mommy) { myMommy=mommy;}
When Mom constructs her Sons and Daughters, she needs to pass a reference of herself to their constructors. She does this using the this variable:
public class Mom ( Son firstSon; Son secondSon; Daughter firstDaughter; Daughter secondDaughter; public Mom() ( firstSon=new Son(this); secondSon=new Son(this); firstDaughter=new Daughter(this); secondDaughter=new Daughter(this);} //other methods}
If we construct Mom with:
Mom BigMama=new Mom();
then Figure 3-2 will represent the relationships of our family.
Figure 3-2: BigMama's family.
SuperVariable
You'll often need to access a parent's implementation of a method that you have overridden. Suppose that you implemented a constructor that was defined in your parent class. Maybe you wanted to initialize a few variables private to the new class, and now you want to call your parent's constructor. This is where the super variable is useful. In the following example, we define a class that overrides its parent constructor and then calls it using the super variable.
Think back to our Mom, Son, and Daughter hierarchy Let's say Mom defines a method that cleans up a room, called cleanUpRoom. Son is supposed to clean up the room exactly as Mom has defined, after which he needs to print out, "Cleaned up my room!" Since Mom has defined the method for cleaning up the room, Son can call it using the super variable and then perform the additional action of printing out the message:
public class Mom { //declarations, constructors public void cleanUpRoom() { //code for cleaning up room } //other methods) } public class Son { //variables, constructors public void cleanUpRoom() { super.cleanUpRoom(); System.out.println("Cleaned up my room!!");} //other methods }
Be careful not to think of the super variable as pointing to a
completely separate object. You need not instantiate the superclass
to use the super variable. It's really just a way to run the methods
and constructors that were defined in the superclass.
Constructors, like methods, can also use the super variable, as in this example:
public class SuperClass { private int onlyInt; public SuperClass(int i) { onlyInt=i;} public int getOnlyInt() { return onlyInt;} }
Our subclass can reuse the code we have already written in our constructor by using the super variable:
public class SubClass extends SuperClass { private int anotherInt; public SubClass(int i, int j) { super(i); anotherInt=j;} public int getAnotherInt() { return anotherInt;} }
There are two important restrictions on using the super variable to access the constructors of the superclass. First, you can only use it in this way inside a constructor. Second, it must be the very first statement inside a constructor.
We've been instantiating classes since Chapter 2. When we use the new operator, we are bringing our class to life as an object and assigning it to a variable. Now, let's look at some issues about instantiation we haven't covered yet. When we first started instantiating classes, we used the default constructor that takes no parameters:
someClass A=new someClass();
Then, we showed that we can pass variables to the constructor. What we haven't taken advantage of is constructor overloading, in which a class defines multiple constructors with differing parameter lists. Since constructors are really just a special type of method, constructor overloading works like the method overloading discussed in Chapter 2. The class we define below utilizes constructor overloading:
public class Box { int boxWidth; int boxLength; int boxHeight; public Box(int i) { boxWidth=i; boxLength=i; boxHeight=i;} public Box(int i, int j) { boxWidth=i; boxLength=i; boxHeight=j;} public Box(int i,int j, int k) { boxWidth=i; boxLength=j; boxHeight=k;} //other methods }
In the section of code above, we have a class that describes a box. If we are only passed one parameter for the constructor, we assume that a cube is desired. If we are passed two, we assume that the base is square and that the second integer describes the height of the box. When passed all three, we use each of them in describing the box. Overloading constructors allows us to give the users of our class multiple ways to create the class.
When we declare a variable and don't initialize it to a particular value, Java assigns a value for us. Above, our variables were initialized to zero. Generally you want to make sure that each constructor assigns values for each of the variables in the class. Data encapsulation depends on the data being valid, and doesn't work very well if you don't initialize the value.
But this creates an interesting predicament-we can't expect constructors defined in the superclass to properly initialize variables defined in the subclass. Java resolves this difficulty by having a different set of rules for constructor inheritance than it does for method inheritance and variable inheritance. If you define a constructor, any constructor Java ignores all constructors in the superclass.
When we were discussing the advantages of data hiding, we introduced the private modifier. The private modifier only allows a variable or a method to be accessed from within the class, while the public modifier makes a member accessible from anywhere. There are three other modifiers that affect the object-oriented nature of members of a class: protected, private protected, and final. We've listed them in order of familiarity - protected is closest in behavior to the public and private modifiers that we've been using, and final is furthest from what we are used to. Thus, let's start by looking at protected.
The Protected Modifier
The protected modifier lets us make class members public to only a certain set of classes-the ones that are in the same package. We place a class into a package with a statement at the top of the file:
package somePackage;
If we don't explicitly put a class in a specific package, it's placed into a default package with all classes defined in the current directory.
If you don't explicitly modify a method or variable, the compiler
assumes you want it to be protected. However if you later decide
you want to put it in its own package, members that had previously
been accessible from classes in the same working directory won't be
accessible anymore. It's always a better idea to explicitly modify
class members.
The Private Protected Modifier
The private protected modifier represents a more narrow accessibility than the protected modifier but wider accessibility than private. A private protected member can be accessed only by the subclasses of a class. While the other access modifiers we've used fit into the concept of data hiding, the private protected modifier has the most important implications when we are considering class inheritance.
Let's say we declare a variable or a method as private in a particular class. If we subclass this class, the subclass can't access the private members if the superclass isn't in the subclass. As we explain in the next section, it's often advantageous to develop a base class that is really just a placeholder-you expect it to have several subclasses. In such a case, it's much more convenient to use the private protected modifier instead of the private modifier so that the subclasses don't have to do all of their real work through the public methods of the superclass.
We have looked at much of what is under the hood of Java's object orientation. Hopefully you are now comfortable with two key concepts of object orientation, data hiding and encapsulation, and how to use them in Java. We've introduced inheritance. Now let's look at the mechanism of inheritance in greater depth. In this section, we demonstrate how you can become a more efficient programmer by using inheritance to form class hierarchies. Java provides us with abstract classes and methods to help us out in structuring class hierarchies.
When we discussed reusability earlier in this chapter, we showed you that inheritance allows us to build on classes that have already been written. But our example showed only one part of reusing code. Reusability is only one advantage of inheritance. Using inheritance, we can lay out the key modules of our program in an intelligent manner.
Consider the scenario we used to introduce object orientation in Chapter 1, "The World Wide Web & Java." As you may recall, we looked at the simple problem, "go to the store and get milk," and showed how to think about it in terms of object orientation. Let's look at one of the components of the problem, the carton of milk.
Suppose we are coding up the entire system. We could write a class that describes the milk. But there are several different types of milk, such as lowfat and chocolate. And if all types of milk were grouped together as a unit, that unit would be only one of many dairy products. Thus, we could create a class hierarchy such as the one shown in Figure 3-3.
Figure 3-3: Class hierarchy of dairy products.
Luckily this is more than just an exercise in critical thinking. Java allows us to instantiate a subclass and then cast it so that it acts like a variable of the superclass. This is very valuable if we only care about one general aspect defined at the top of the class hierarchy, such as whether a dairy product is going to go sour this week. Let's say that our dairyProduct class has a method that will tell us this:
public class dairyProduct { //variables, constructors public boolean sourThisWeek() { //appropriate code } //other methods public void putOnSale() { //code for putting a dairy product //on sale }
This is where casting comes in. If we already have a variable of, say, lowfatMilkType, we can cast it to be a variable of type dairyProduct:
lowfatMilk M=new lowfatMilk(); dairyProduct D=M; if (D.sourThisWeek()) ( System.out.println("Don't buy");}
What's the advantage of this? Let's say our store manager wants to check to see which cartons of milk are going to go sour this week. The ones that are will be put on sale. He can just pass all of his lowfatMilk, Milk, Cheese, and Yogurt objects to the following method:
public void dumpSourGoods(dairyGood d) { if (d.sourThisWeek()) { d.putOnSale();} }
If we hadn't created a structured class hierarchy in the first place, we would have to write a different method for each type of dairy product.
In the previous example, we created a class hierarchy to make our code more useful in its system. But our dairyProduct class has methods that have no body. When we wrote it above, we just said they would be overridden in the subclasses. However, somebody looking at our code may not understand that this is our intention. Java provides as with the abstract modifier to help us with this situation.
When we use the abstract modifier with methods, all subclasses have to override the abstract method. This is how we would make abstract methods for our dairyProduct class:
public class dairyProduct { //variables, constructors public abstract boolean sourThisWeek(); //other methods public abstract void putOnSale(); }
The dairyProduct class can still be instantiated-the abstract methods just can't be accessed via a dairyProduct instance. However we can also use the abstract modifier to describe that we don't want a class to be directly instantiated:
public abstract myAbstractClass { //code }
When we define a class as abstract, we can put regular methods and variables in it. When we subclass the abstract class, it inherits all of the members of the abstract class according to the same inheritance rules we have already described.
Polymorphism & Java's Interfaces
When we covered the advantages of object orientation, we stuck with the concepts we could easily explain based on what we learned about Java in Chapter 2, "Java Programming Basics." Now we are going to introduce the concept of polymorphism and how a Java structural mechanism, interfaces, allows you to employ it in your code.
Polymorphism is the process by which we are able to make the same method call on a group of objects, with each object responding to the method call in different ways. We have already dealt with polymorphism in our example about dairyGoods. For instance, the methods putOnSale and sourThisWeek are defined in all of the classes in the hierarchy. We are free to call these methods on any of the objects-as we did when we put all nearly sour dairy goods on sale-and each different class defines how its instantiations will actually will react.
However, polymorphism is somewhat limited. We are only guaranteed that the classes in the same hierarchy will contain the methods defined in the top class. Many times, some of the subclasses may need to have methods that the entire hierarchy doesn't have. For instance, since milk and yogurt are liquid, we may want a method cleanUpSpill in case they tumble. But it would be silly to define a method for the cheese class describing how to clean up a cheese spill. Also, many products in the store that aren't dairy products can spill.
A well structured class hierarchy doesn't solve this predicament. Even if we have a class, storeGood, that sits above all classes ; defining products in our store, it wouldn't make sense to define a ; cleanUpSpill method at the top, because so many goods in the store can't spill. What is needed, and is supplied by Java, is a way to define a set of methods that are implemented by some classes in the hierarchy but not all. This structure is called an interface.
Let's start exploring interfaces by defining one for our liquid products that may spill:
interface spillable { public void cleanUpSpill(); public boolean hasBeenSpilled(); }
As you can see, the methods are defined in the same way we define abstract methods. Indeed, they are abstract-they have to be defined inside a class that implements the interface. Also, notice that we don't have any variables or constructors. These aren't allowed in an interface because an interface is just a set of abstract methods. Here's how we use an interface in our class Milk:
public class Milk extends dairyProduct implements Spillable ( //variables, constructors public boolean hasBeenSpilled { //appropriate code) public void cleanUpSpill { //appropriate code) //other methods }
The key here is the keyword implements. It designates that class Milk defines the methods in the interface Spillable. Of course, if we have an instance of the Milk class, we can call the hasBeenSpilled and cleanUpSpill methods. The advantage of interfaces is that, like classes, they are data types. Although we can't instantiate them directly we can represent them as variables:
class Milk M=new Milk(); Spillable S=(Spillable)M; if (S.hasBeenSpilled()) {S.cleanUpSpill();}
Therefore, we can access all of the methods having to do with spilling through the Spillable data type, without having to define the methods in a base class for all products, spillable or not spillable, in the store.
We're also allowed to implement more than one interface in a class. For instance, we could write a Perishable interface that would describe all products that may spoil. Our Milk class implements both of them with the following class declaration:
public class Milk implements Spillable, Perishable { //class definition }
In actuality it would be better to implement the Perishable interface at the dairyGoods level, since all dairy goods are perishable. Not to worry-subclasses inherit the interfaces their superclasses implement.
We covered a lot of concepts in this chapter. You learned why OOP techniques are helpful in general, and how to declare and use objects and apply fundamental OOP practices, such as inheritance and overloading, in Java. Arrays were introduced to show what objects could do for the language itself. Let's wrap up the chapter with Table 3-3, which summarizes the OOP concepts, and an example that employs them all.
Concept | Description |
---|---|
Class | A type describing some data and a group of functions that act on this data. |
Object, instance, instantiation | A variable of a class type after instantiation of the class has taken place. |
Data hiding | The practice of making variable hidden from other objects. Data hiding typically makes it easier to change underlying data structures |
Encapsulation | Grouping like functions and data into one package |
Access modifiers | Statements that describe what classes can access the variables or methods defined in a class. |
Instantiation | Creating an object from a class. Instantiation creates an instance of the class. |
Constructor | A bit of initialization code called when a class is instantiated. |
Class hierarchy | A multitiered diagram showing the relationships between classes. |
Inheritance | Creating a new class by extending the functionality of another class. |
Superclass | A class that is inherited from a particular class. |
Subclass | A class that inherits from a particular class. |
Method overriding | Redefining methods in the subclass that have already been defined in the superclass. |
In order to summarize these concepts in a program, let's introduce a small classes hierarchy. These groups of objects are going to implement a low-level graphics system. Suppose our client has asked us to create a painting program. She would like to be able to move picture elements around as whole objects. The first demo will include primitive shapes, but the final project could include many complicated shapes and bitmaps. If we can get a demo copy to her by next week, we get the contract; otherwise, we'll be stuck manning the technical support lines for another six months. Dreadful thought, so let's get this demo up and running!
The fact that we don't know all the shapes to be implemented makes our task harder. We will have to incorporate our knowledge of OOP techniques and make our code as extensible as possible. One of our most powerful tools is inheritance. If we design our object hierarchy right, it will be a snap to add any number of new shapes.
Remember our discussion of interfaces? We use interfaces to make a group of objects conform to a standard set of features. We will need this ability to implement our paint program. Each shape must be able to handle a few important routines. Primarily we need each shape to be able to show itself on the screen, hide itself from view and change its location. With this basic set of operations, we can create a simple paint program. Let's call this interface Shape. The definition for Shape is:
interface Shape { public void show(); public void hide(); }
In order for a new shape to be added to the paint program, the paint program will have to implement only these routines. The rest of the routines will be handled by other objects in our hierarchy. This object will be responsible for keeping track of a shape's position. Any code we wish to share between shapes will be stored in this class. We call this class BaseShape; it is defined below. Notice that it is abstract and contains abstract methods:
abstract class BaseShape { protected int x,y; public void setPos(int newX, int newY) { x = newX; y = newY; } }
We now have a common interface for each shape and a base class to inherit from. Any method that needs to be implemented in all shapes will be placed in the interface. All common code between shapes goes in the baseShape class. The final piece of coding is to implement the individual shapes and a small demo paint program.
The following shows the implementations of a few shapes, namely, a rectangle and a circle. Each shape may need extra data elements and methods to implement its particular picture. In order to follow good data hiding practices, we declare these variables and methods as private:
class Rectangle extends BaseShape implements Shape { private int len, width; Rectangle(int x, int y, int Len, int Width) { setPos(x,y); len = Len; width = Width; } public void show() { System.out.println("Rectangle(" + x + "," + y + ")"); System.out.println("Length=" + len + ", Width=" + width); } public void hide() {} } class Circle extends BaseShape implements Shape { private int radius; Circle(int x1, int y1, int Radius) { setPos(x1,y1); radius = Radius; } public void show() { System.out.println("Circle(" + x + " " + y + ")"); System.out.println("Radius=" + radius); } public void hide() {} }
The last piece to be coded is the paint program itself. Imagine how you might go about coding a paint program. Since we want to keep each shape separate, we must have a way to store individual components. The combination of these shapes will form some picture. The advantage to this is that we can easily move or copy elements of a picture to different places. To do this, we need a way to store the elements of the picture.
We have something of a problem here. What type of data structure can we use to hold many different types of objects? The simplest would be an array. Arrays in Java allow you to hold any type of data. The data could be a simple type like an integer a more complex type like an object, or, as in this case, an interface, which is a programmer-defined type. We declare an array that will hold objects that implement the interface shape. This lets us call any of the defined shape methods without knowing exactly what type of object it is. We can keep creating new shapes without having to change our paint program. This is a great improvement on procedural-based languages!
class testShapes { public static void main(String ARGV[]) { Shape shapeList[] = new Shape[2]; int i; shapeList[0] = new Rectangle(0,0,5,5); shapeList[1] = new Circle(7,7,4); for (i=0; i<2; i++) { shapeList[i].show(); } } }
There we have it-a simple program that implements the basics of our paint program. Add to this a little graphics code, and we have ourselves a usable and extensible paint program. When the client comes back and asks for changes to the original program, we'll be ready. This framework provides a base to implement an ever-improving paint program. No more technical support for us-we got the contract!
Hopefully you now have a good grasp of the key concepts of object orientation and how Java uses them. In the next chapter we'll spend some time covering the syntax of the language. Although some of this will be a review other parts will be entirely new to you. With a strong conceptual understanding of the language, we hope that when we get to the nitty-gritty-writing Java applications and applets-you'll be ready to get down to business.