![]() | Monkeys at Keyboards: The Javanomicon © Michael James Heron | ||||
| Topic: Java Programming Level: 2 Version: delta | |||||
25 - Abstract Classes, Interfaces and Polymorphism | |||||
| Previous | Table of Contents | Next |
| Forum |
| Chapter Objectives |
By the end of this chapter, the reader will be able to:
|
Our knowledge regarding object orientation in Java is not yet complete. There is still quite a bit of territory we need to cover before we can be reasonably confident we have access to all the tools and concepts that are required to make best use of this powerful technique for modelling computer programs. Today we're going to look at some of the other kinds of classes available in Java, as well as a concept called polymorphism which forms one pillar of the Holy Triumvirate of Object Orientation (the other two are Inheritance and Encapsulation). We will also look at a useful technique for generalising listener objects, and finally achieving the goal of truly separating the model from the view from the controller. Chapters discussing these kind of elements are of a necessity rather heavy on theory... it's simply not possible to give snippet examples of why object orientation works the way it does. The programs required to best show off the benefits of a properly developed class hierarchy are so complex that the message is obscured by the medium. Object Orientation is a difficult subject, and one that requires a good deal of assimilation time to be spent before it starts to make logical sense. If you can't see the use of anything discussed in this chapter, give it a while and come back - before too long you'll start to see many problems that become easier to solve through the use of inheritance, encapsulation and polymorphism.
Let's consider a simple Student class hierarchy. We have a base class Student, from which two children are extended - FullTimeStudent and PartTimeStudent:
If we spend some time considering this structure, one comes to the conclusion that there will never really be an occasion where someone is just a Student. This is a binary situation - a student is either full-time or not, and if a student is not full-time then by definition they must be part-time. We may have cause to further specialise the Student class into other classes (perhaps ForeignStudent, for example), but no-one is ever going to be just a Student within this class system. In many cases in this scenario, simply setting an object as a Student is problematic since the class is so general that we don't have enough information to provide any required functionality. Let's say that the specialised classes provide a method that returns what percentage of external funding the student is entitled to, and that this is based on what proportion of the full forty hours a week for which they are registered. A plain, vanilla Student class has no way of implementing such a method, because it doesn't have the required information. In such a case, we would never want to instantiate an object of type Student since it is a class that exists only to give form, structure and generalisability to its children. When writing classes for our own use, it's no problem. We simple make a mental note saying 'Brain, don't ever create an instance of Student, 'kay?', and we're fine. But remember one of our mantras - one of the primary benefits of object orientation lies in reusability. Let's say we're working in a multi-developer environment. Steve the Developer, who is working on a different part of the system, spies our Student class and thanks 'Oh-ho, a student class... just what I need for this part of the system', and so he incorporates our class into his code. This raises huge problems when it comes to integrating parts of one system with another. In situations like this, it would be ideal if we could say to Java 'Look, this is only a class I'm using to provide some general code to children objects. It should never be used by itself'. And indeed there is... we can declare classes as being abstract:
Effectively what we are saying to Java is 'Hey, I've got this class - it's there to provide form and structure to its children, but I never want anyone to make an instance of it. Would you be a dear and make sure that no-one ever creates an object of this class?' When someone attempts to create an instance of an abstract class, Java will throw up a compile-time error. It's not even possible to compile a program that instantiates an abstract class:
Compiling this program will cause this line to prompt the following error message:
So Java will ensure that nobody codes us into a tight spot by making use of classes that shouldn't be instantiated. Okay, that's all well and good I hear you say... but that sounds very much like a chore we need to do rather than something fun. Give us something fun! Okay then, I will. I am nothing if not a slave to your passions. One of the other benefits of abstract classes is that we can also declare that methods within an abstract class are in themselves also abstract, like so:
This enforces another compile-time check on our programs... any class that extends this class (and is not in itself abstract) is required to implement the code that goes with these methods, or the program won't compile. All we provide is the method signature in our abstract class (the return type, the name, and any parameters), and any children classes must do the actual work. There is a very good reason for this - without doing this we can't ensure a consistency of methods throughout a family of objects. Whoever implements PartTimeStudent may write a method called getStudentLoan, whereas the person implementing FullTimeStudent has a method called getLoanForStudent. Both methods might do exactly the same thing, but the lack of a consistent naming scheme means that we have no ability to write general solutions for our classes. We'd need to call one method for instances of FullTimeStudent and another for instances of PartTimeStudent. What if we had ten such classes? It would be a nightmare, a nightmare I say! The idea behind abstract methods is that they force a particular structure on all subclasses, and also put the emphasis for implementation away from the base class and on those classes that actually have enough information to supply the code:
Okay, okay, I know this doesn't sound particularly fun. But wait... there's more, oh so much more.
We've left the discussion of polymorphism until quite late in the book - we have used it in the past, but we never even mentioned it by name. Polymorphism is one of the Holy Triumvirate of object orientation (Inheritance and Encapsulation being the other two), and is one of the reasons that languages like Java have enjoyed the marketplace dominance they have over the past few years. The idea is deceptively simple, but incredibly powerful in certain situations. The name comes from the Greek; poly meaning many and morphism meaning shapes or forms. Essentially it works by treating children of a specialised class as instances of its more general form. For example, we can treat a FullTimeStudent and a PartTimeStudent as the general case Student. We've used this before when dealing with exceptions... we could either catch specific exceptions, or catch the general case Exception. All exceptions inherit the Exception class along the way, and so we could simply choose to deal with them all according to the lowest common denominator. Consider for example a method that returns the loan for a particular student. The Student object itself gets passed into the method as a parameter:
If we tried to pass in a PartTimeStudent to this method, we'd get an error. So we'd need to overload this method to provide the functionality for part time students:
What if there were five kinds of student? Or ten, or a hundred... it would be madness, madness I tell you! Instead, we deal with them according to what they share in common, which is the Student class:
This method will work for both child classes:
The clever part about polymorphism is that it actually calls the method that belongs to the most specialised class - so it doesn't call the getLoan method that belongs to Student (it can't, because it's abstract)... it calls the getLoan that belongs to the appropriate specialisation. The drawback is that we can only access the functionality that is shared. - Java has no way of knowing how closely children classes adhere to a naming structure if it isn't enforced by the compiler. Let's imagine a Student class that doesn't have any abstract methods - so back to our original abstract class for Student. And let's pretend we have class implementations for PartTimeStudent and FullTimeStudent that provide the methods getLoan and getStatus as they should. Now let's consider our polymorphic method:
This won't work... despite the fact that both classes have a getLoan method, Java refuses to compile. Java is only happy if the method is fully defined in the general class, or if it is declared as an abstract method and therefore all children classes are enforced by the compiler into implementing it. Effectively we say to Java 'Look, I know this class doesn't have the code you need, but its children will... they have to, or you won't let the program compile!'. In this way we can provide generic behaviour for families of related objects, even if we don't know in advance what the code is going to be. Still not fun? Hrmph. There's no pleasing some people.
In the Password case study, we looked at an example of object orientation and inheritance through the case study of a password system. Most of what was going on was fairly obvious, except for a little bit of strangeness... remember how we made use of the Password class and extended it to provide an ExtendedPassword class? We then took the LoginLogic class and provided our own extended version of that called ExtendedLoginLogic. We had to change some of the functionality to get our extended password system working - but curiously, the checkPassword method from LoginLogic worked perfectly even though we were using the ExtendedPassword throughout, and the checkPassword method worked on plain old Password objects:
This was an early example of polymorphism... the Password class contains all the structure we need to make use of an ExtendedPassword in this context because the only method we are making use of is checkPassword which is already defined in Password. Sure, our ExtendedPassword provides a more specialised version of the method, but that's fine... as indicated above, Java will call the most specialised version of the method it can find. So, we put objects onto the myPassword ArrayList as ExtendedPassword objects. In checkPassword, we pull them off and cast them into Password objects - that's okay, because ExtendedPassword extends Password and so we can treat the more specialised class (ExtendedPassword) as an instance of its more general parent (Password). We call the checkPassword method on the object we've pulled out of the ArrayList. Java checks to make sure that it can guarantee this method exists - because it's defined in Password, then it knows that all children of Password will have the method. It accepts this quite happily, and then checks down the inheritance tree until it finds the most specialised version of the method and calls that.
One of the major limitations of the Java programming language is that it provides no support for multiple inheritance. While there is an upside in that this tremendously simplifies the process of building object oriented programs, it comes at a major cost in terms of the power and flexibility of the class libraries you can develop. Consider a natural world parallel as to what is possible with multiple inheritance... if we had a bird class, and a horse class, we could have a subclass that inherited both to create a Pegasus. Of course, it's never that simple - there are considerations that need to be taken care of. For example, a bird has a certain kind of bone structure that a horse does not - you can't just bang the two together and hope that some kind of critical reaction occurs. It requires a lot of careful planning to make multiple inheritance work for complex problems. But let's look at a less fanciful example:
This is a fairly simple class system. Students and Lecturers are separate abstract classes, and they each have their own properties as well as those that they share. Now consider a new class, ResearchAssistant. The role of a research assistant is to study towards a PhD (so they are a Student), and also usually to do some teaching (so they are also a Lecturer). Ideally, a ResearchAssistant would inherit the properties of being a PartTimeStudent and the properties of being a PartTimeLecturer. It is not possible to model such relationships in Java... instead, we have a less powerful infrastructure to support these kind of relationships. We've already seen it and used it, even if we didn't discuss it at the time. Remember our old friend ActionListener? In order to make our class act as an ActionListener, we needed to implement ActionListener in our class definition. We glossed over what that meant at the time in favour of a 'It's magic, let's not worry about it' strategy. Java provides another class syntax, which is that of the interface. An interface is simply a class in which all of the methods are abstract. We use an interface to supply a common set of methods amongst a set of classes, but we cannot include any code at all. In order to use an interface in our class, we must implement it - this means that we are telling the compiler that we will be providing all of the code for the abstract methods. This is exactly like an abstract class except that we can extend only one abstract class, but implement any number of interfaces. There is often confusion amongst Java developers, particularly those who haven't had a lot of time to consider object orientation, about the relationship between inheritance and interfaces. Mainly this confusion stems from the fact they are both used to implement a is-a relationship. We use inheritance to provide common code - we need only change a particular method in one place for it to rattle through all specialisations. On the other hand, we use an interface to share a common structure. We provide a list of methods that all classes that implement the interface will share, in the same way we enforce a structure through the use of abstract methods. We define an interface using the interface keyword. Let's examine this in the context of our Student and Lecturer class hierarchy above. Consider a LibraryAccess interface that determines whether or not an instance of a class can access a library. We could sensibly include another class at the base of our hierarchy to enforce an abstract method declaration - in a Person class, for example. Realistically, we can continue this kind of expansion indefinitely. However, in this particular case, it's not only people that can access a library. Other libraries can, for example - mainly for book-sharing initiatives. If the interface is used to restrict access to library internet resources, there may also be a need for other system classes to have library access. In cases where there are numerous diverse classes that require a set architecture, and there is no way to sensibly implement a common base class, interfaces are the only real recourse.
As you can see, the syntax is simple... we don't use the class keyword, instead we use interface. We also don't have to explicitly declare methods as being abstract. We can't include any code at all in an interface, so we don't need to specify that these are abstract methods - by definition, they have to be. Now, we can use the implements keyword to provide this common framework to any class that requires it:
We can even use polymorphism to treat objects as the general case of interfaces they implement, even though there is no way that an instance of an interface could ever exist:
Phenomenal cosmic power is ours for the taking!
There are a number of differences between interfaces and abstract classes that will hopefully help you resolve in your mind when it is appropriate to use one over the other. A class may extend a maximum of one class, abstract or not. On the other hand, a class may implement any number of interfaces. An interface may provide no code at all, and will produce a compile-time error if it tries. An abstract method may provide some, all, or even none of the code for children classes. In order to make use of an abstract class, we must specialise all of our classes from that base. This means it can be exceptionally difficult to simply slot an abstract class into an existing class structure of any complexity. Interfaces are far simpler to drop into an existing development - all we need to do is add the implements keyword and add in the required methods. We don't need to shift around the relationship between existing classes. Abstract methods harness the power of object orientation, meaning that if you need to change the code for a method then it only has to be changed in one place (provided the existing guidelines about encapsulation and access rights are followed). On the other hand, if you change the method signature of a method in an interface, the code has to be changed in every object that has implemented it. Not cool! This would seem to be the biggest drawback of interfaces... not just changing existing method signatures, but even if you want to add in a new method signature, none of your classes will even compile without being modified. This means it is potentially disastrous to have to modify an interface - and maintenance is all about modifications!
Well... it doesn't always have to be a disaster. It all depends on how the code is implemented. Using the standard techniques we've discussed throughout this book, then yes it would be - imagine if someone came along and changed ActionListener to require another method definition, for example. But there exists a technique for limiting this change management issue, and it involves setting up a class as an adapter. There is no special syntax required to make use of an adapter - it isn't a special class, it's just a specific purpose that a class adopts as its role in a program. That purpose is primarily to sit between an interface and our code. So far we've tried as far as possible to break the model away from our view and controller, but we've never managed to break up the latter two. Using adapters, we can completely separate out the view and the controller. Firstly, we write a class that implements some interface. Let's use ActionListener as our example:
Note that it provides absolutely no code except that which is required to implement the interface. In this example, the class is going to do nothing but handle Action events. Whenever we register an ActionListener in our view for a program, we don't point to this... instead we use a new instance of our adapter class:
This is moving back to a very simple example of an application that provides a button that flashes up a message box. But notice there is no hint of implementing ActionListener in the presentation class. Instead, the functionality is handled by an instance of ActionListenerAdapter. In this way, if we had ten classes that all used this same adapter, if ActionListener was changed then we only need to change ActionListenerAdapter for it to be reflected in all our objects. There is of course a drawback, and that's that it is somewhat difficult to apply an adapter to complex problems, particularly those that require substantial communication between different objects. We can pass objects to the adapter constructor method to allow for this, though:
If the communication between two objects is more complex than the state of a single component, it can be considerably more difficult to properly implement an adapter in such a way that it can be reused amongst many different classes. Even so, it is sometimes beneficial to use them even if only for a single class since they allow for further articulation of the model-view-controller architecture. For simple behaviour though, adapters are excellent tools - especially for interfaces that require very little in the way of interaction, such as the WindowListener interface. Having an adapter that simply deals with unloading an application when the close button is pressed is a benefit in terms of reuse and time saved.
We've met two new kinds of class in this chapter - abstract classes and interfaces. Both have their own unique features, and if developing a UML class diagram of our programs we should use the appropriate notation to indicate the presence of both. An abstract class is indicated in the same we we indicate a normal class - the only difference is that the name of the class (and any abstract methods) are indicated by italics.
Interfaces are shown as classes that contain only operations without any attributes. The name of the class is surrounded in double angle brackets.
We indicate that a class has implemented an interface using a new symbol - a dashed line topped by an outline triangle. This is called a realization relationship. Let's look at how the full class diagram for our Student example above would appear:
All of these tools are very useful, even if it isn't immediately apparent at the moment as to why. The chances are that if you haven't yet come across a situation in which the answer is obviously to use one of these, you won't be able to visualise them as possibilities. Like most things in programming, it comes down to pattern recognition. Now that you know about the existence of things like abstract classes, interfaces, polymorphism and adapters, you will be more likely to see situations in which they can be used. Properly implementing class hierarchies is the key to successful Java development. It's also immensely difficult in anything other than the most simple of cases. Don't be discouraged if you find it difficult to separate out your classes without help - it does get easier. One day in the future, it will all just click and you'll think 'Wow, that all makes perfect sense! Why didn't I see it back then?' Further ReadingThe following table details further reading on the topic in this chapter, and also any external resources that you may find useful.
|
| Previous | Table of Contents | Next |
© 2004-2006 Michael James Heron