![]() | Monkeys at Keyboards: The Javanomicon © Michael James Heron | ||||
| Topic: Java Programming Level: 2 Version: delta | |||||
11 - Inheritance | |||||
| Previous | Table of Contents | Next |
| Forum |
| Chapter Objectives |
By the end of this chapter, the reader will be able to:
|
In the previous sections of this chapter we looked at the basics of objects and classes. There is much more to the idea of object oriented programming than just what we have discussed so far - it is perfectly understandable if you feel entirely under-whelmed. In the rest of this chapter we will look at the idea of inheritance, and how it is applied to programming to develop powerful solutions to real world problems. Inheritance allows us to make use of previously existing classes - this idea of reuse is a common thread throughout object oriented programming. We can make use of previously-written classes within our own code, and where it is needed, we can extend the functionality of those classes to meet our own specific needs. We will also take a look at the idea of encapsulation, and how we can use it to protect and nurture our precious data, like mother hens crowding around a nest of eggs.
The idea of inheritance should be a fairly familiar biological system. The study of genetics is not as venerable a science as some branches of biology, but it has been around long enough and the idea penetrated deeply enough into society for us to start from at least a fairly informed perspective as to what it involves. In object oriented programming, we apply this biological principle to software development. To put it simply, we can have classes that inherit the attributes and behaviours of other classes. By itself, this is not useful - if we just wanted the attributes and behaviours of a class, then we'd just create an instance of that class. Inheritance also allows us to extend and specialise these attributes and behaviours. We extend by adding new attributes and behaviours to a class - these are then available in addition to the ones that we gained from the inherited relationship. We specialise by changing the functionality of the behaviours that we inherit. In this way we can ensure that any class that we have inherited will behave properly within the context of our own program. We will look at this idea in more depth with a case study based on the bank account applet that we wrote for the previous chapter. We have already seen this idea in practise with the applets we have been writing. The Java keyword we use to describe an inherited relationship is extends, which we have seen often in the context of our applets. Consider the class definition of our first JApplet code:
With this code, we are saying to Java that the class we are creating (MyFirstApplet) inherits the attributes and behaviours of the JApplet class. This saves us huge amounts of time and effort - the JApplet class defines all the functionality for setting up an applet, placing components on the screen, calling init at the right time, and so on. Actually, this is a considerable over-simplification - but we'll return to that later in this chapter.
Consider our model of the natural world. Classification theory, as a branch of philosophy, is very complex and has numerous different systems for working out to what category particular things belong. For example, John Wilkins in 1668 wrote an essay called 'Towards a Real Character, and a Philosophical Language', within which he discussed which categories (or classes) to which particular real world and abstract objects belonged. However, let's ignore all of this complexity and look at a very simplified example:
Obviously this shows just a tiny general outline of how inheritance works. The important thing to realise is that inheritance can be a recursive system. Consider the box at the very top of the chart: a thing. It doesn't inherit the characteristics of anything else. It's just a thing - we don't have any means of defining it further. It exists purely to define other things in terms of a provided (self-referential) context. In Java, we have a class at the very top of the framework called Object that performs this task. All objects in Java inherit the Object class, even if we don't specifically tell the compiler that this is the case. Once we have our base class, we can expand on that by inheriting. A vegetable inherits all the properties of being a thing, but it also extends by providing behaviours and attributes that are specific to that particular class. For example, not all things will decay, but vegetables will - so the attributes and behaviours for modelling that aspect belong to the vegetable class. Likewise for minerals and animals. They each inherit the characteristics of being things, but they provide their own specialised behaviours and attributes that further specialise them. In our chart, we have further inherited the animal class into mammals and reptiles. Here is where recursion starts to kick in - a mammal and a reptile both inherit the properties of being animals and in doing so they both inherit the properties of being things. Again, they provide their own specialised behaviours and attributes to model their individual complexity. Mammals are then extended into cats and dogs. Both inherit the properties of being a mammal, and in turn an animal, and in turn a thing. Finally, dogs are specialised into poodles and great danes. This relationship can extend indefinitely. Object orientation in Java is a little bit more complicated, because it's not always the case that it is modelled as a simple extension of a class - we'll look at that later in the book. Fundamentally the idea is identical, especially since Java does not provide any facility for more than one class to be inherited (in the proper sense) into another class. We cannot model the following relationship as an inheritance in Java:
We will look at what facility Java provides for simulating this kind of complex class relationship in a later chapter. In Java terminology, a class that is being inherited from is known as a parent. A class that is inheriting is known as a child. So the dog class is the parent of poodle and the child of mammal.
Now that we've looked at an abstract example of inheritance, let's look at a concrete code example. As stated above, we've used the idea of inheritance all the way through our code, but the relationship implied by extending JApplet is complex because of JApplet's own inheritance tree. Discussing this complexity is only going to confuse the issue. Instead, we're going to look at this idea in a little more depth by making use of the Account class that we wrote in the previous chapter. Let's say we've written our bank account applet, and the bank was very pleased. So pleased that they mentioned our hard work to a sister bank, and they wanted in on the action. They sent you a request that you write them a similar applet, except that it needed some extra functionality: It needs to maintain a list of all deposits and withdrawals, and it needes to make sure that the balance of the account could never drop below a specified overdraft limit. In this chapter, we are going to use the technique of inheritance to provide this extra functionality. We won't need to write any of the other methods or attributes that were used to model the system in the previous chapter, since we'll get them all by extending the Account that we've already written. Our new class is going to be called ExtendedAccount. Our class definition will look like this:
Believe it or not, that's all that we need to do to create a fully working ExtendedAccount class. We can now use this in place of our existing Account object without any remodelling of the code. We can change the relevant sections of our applet to reflect this:
becomes
And we need to change our populateDatabase method to the following:
All we are doing is changing each reference to the Account class to a reference to ExtendedAccount. Now, we can compile the program, and execute it - and it won't work. Curses! It comes up with an error saying something about a ClassCastException. We'll worry about what this actually means in a later chapter... for now we'll just solve the problem. Remember how in the last chapter we talked about constructor methods? Well, constructor methods are indeed inherited along with the rest of the methods in a class - but we need to provide an appropriate constructor method for ExtendedAccount too. At the moment we have the default 'empty' constructor, but we're attempting to create new instances of the account with five pieces of data. We must add a suitable constructor to our ExtendedAccount class. Now, it's possible to simply set each of the attributes in the constructor like we did with Account:
However, this isn't a very good idea - we'll talk a little bit about why later on, but one reason why it's not a very good idea is that it's actually already been done for us in Account - the benefit (or one of the benefits) of Object Orientation is reuse, and we're not reusing code if we're rewriting it. Java provides us with a special keyword called super, and super refers to the class that the current class is inheriting. In the case of our ExtendedAccount object, super refers to Account. There is a special syntax of super available to constructor methods - we can use it just like a method call to pass on the configuration details to the constructor method of the parent. Note, this call to super must be made before any other code is executed in the constructor.
Now we can recompile our program, and execute it, and we'll find it works exactly the same as before, even though we're using an entirely different object. That's because our ExtendedAccount has everything that Account does - it just has the capability to be extended further to meet specialised requirements.
So, we have our ExtendedAccount object. We have extra functionality we need to provide. In the first case, we need to provide a list of transactions. In order to do this, we must add some extra code into our new object - in other words, we must extend beyond what we've inherited. We need a variable to hold the transaction history - we'll use an ArrayList and call it transactionHistory. We create the variable in the same way we do with any other class-wide variable declaration - we declare it at the top of the class:
That's reserved for us the memory space we'll need to hold our transaction history. We also need to put something into this memory space - we'll do this in the constructor method we've already written by adding the instantiation code:
We also need to add in some accessor methods for our new variable. We simply write them into our extended class the same way we would into any other class:
Tada - we've now extended our class to provide ourselves with the new methods and attributes we need to meet the expanded functionality. To implement the rest of the extended functionality, we must look at the idea of specialising behaviour, since we're not adding new methods - we're changing the behaviour of existing methods (we need to make a note of a transaction when the balance is changed, and also to make sure the balance doesn't go below a certain value. The method for changing the balance has already been written, so we must specialise it).
Before we can attempt to implement the rest of the expanded functionality, we must first understand what is happening what we call a method on an object. With our ExtendedAccount object, we have no definition for a method called setBalance - and yet we can still call that method on the object. We can do this because setBalance is defined in the parent, and when Java can't find a method in a child, it checks each of the parents in turn until it either finds the right method, or reaches the end of the inheritance chain. However, as soon as it finds a matching method, it will stop looking unless explicitly told to execute the method in any parents through the use of the super keyword. For example, we could write a method called setBalance in ExtendedAccount. Even though the method exists in the parent, Java will be happy for us to include the following code in the child:
When we then call setBalance on ExtendedAccount, it would find this matching definition and execute the code that belongs to that method. Since this method code does nothing, the method call would do nothing - it would never get to the setBalance method in Account. This is called over-riding a method. In this example, we have overridden the setBalance method to do nothing. We can change the code so that instead of doing nothing, we just call the parent method as before, using the super keyword. The syntax for this is super, followed by a dot, followed by the name of the method to call and any parameters:
With this method, we have the same functionality as we did before we started meddling, but we're doing it by overriding our setBalance method. There is a requirement for a constructor method to have the super call on the first line of the method - there is no such requirement for when super is used elsewhere. We can use this to good effect since we can decide if and when the method in the parent is called. If we wanted to change it so that we only let the balance be changed if it's being set to a positive number, we can do this with a simple check:
If newBalance is greater than 0, then the setBalance method on the parent will be called. If it's not, then the setBalance method will never be called. That is something similar to what we want to do, but not quite - we want to make sure we can't adjust the balance below a certain level - the level set in the overdraft variable. We can determine the lowest balance that is permissible by subtracting the overdraft value from 0... if the newBalance is less than that, then we don't call the setBalance parent method. Otherwise we do:
So, that's one part of the extended requirements implement - but we also want to keep a log of the transaction history. We're only going to do this when we are actually adjusting the balance, so we use the same method as above, but add some additional code:
And there we have it, both pieces of additional functionality implemented without us touching the original class. This hopefully shows some of the power of inheritance, but there are still a number of concepts relating to inheritance and object orientation in general that we still need to discuss.
Working out an effective class structure is very tricky - it's really something that only comes with practise. There are two ways objects can interact with each other - one is inheritance, which we have discussed above. The other is association, which we have used in all our code so far. Association refers to when we make use of one object in another by creating an object reference - like when we create Swing components in an applet, or when we made use of an ArrayList in ExtendedAccount. Determining when to use inheritance and when to use association is sometimes very difficult, but there is a simple linguistic trick that can help you work out when to use one over the other. We use inheritance when the relationship between two classes can be described using is a. We use Association when the relationship can be described as has a. For example:
This works for simple problems where the problem domain is well understood - for complex systems it may not be very illuminating. For example: An air traffic control system has a vector management system An air traffic control system is a vector management system. Well... maybe. Who knows? There is also a problem in that the two relationships are not exhaustive, or necessarily exclusive. There are many cases where this clear-cut distinction simply does not exist. Despite its limitations for complex scenarios, this exercise will often help you to clarify exactly what you should be using and when, particularly when the systems being developed are as simple as the ones in this book. Real world problems are usually much more complex and not easily resolved in this way. Only a thorough understanding of object orientation and the real world problem domain will help in these situations, but for us such complicated distractions are years away and can safely be ignored.
Next on the agenda in this chapter is the idea of encapsulation. The definition used in this book is not the only definition - there is sometimes a distinction made between the concepts of encapsulation and data hiding. We will discuss the two as a single unit. The distinction is sometimes useful for more complex object designs, but it doesn't serve us particularly well at this point. Another of the benefits provided by object orientation is that the data and the behaviour for acting on that data are stored in one place - it is encapsulated into a single location. This has a great effect on portability and reusability of programming components. It also has an effect which is primarily of benefit to actual developers, and it is that the developer can make use of this technique to change significant portions of functionality without it impacting on any other classes that make use of the developer's class. This technique is properly called data hiding. Since the data and the behaviour are located in a single place, it is possible to easily to control access. Java provides us with some special keywords that let us do that - you have already seen the public keyword. The principle of encapsulation is that access to an object's functionality should be permitted only through predefined interfaces - essentially, only the methods that another developer would require to use the object for its allotted purpose. All variable access should be restricted to provided accessor methods. This idea is also something that is common to real life. If you consider a car, you don't manually work the engine, or manually turn the wheels. You have interfaces that you interact with, and these interfaces are responsible for manipulating the various elements of your car. It doesn't matter how this happens, just that the interface works as we expect. All that you need to know is that when you turn the steering wheel, it turns the wheels, and when you press the pedals, the car speeds up/slows down. Someone could come along and completely redesign the engine of your car, and provided that the interfaces work in the same way as before, you need never know about it. The details of the engine are hidden away from you. It is only when the interfaces change that the driver must encounter a learning curve. To give an example from my own life, I took my car to the garage for a service, and was given a courtesy car to replace it for the day. My car is a manual drive, whereas the car they gave me was an automatic. A manual car has three pedals - the accelerator, the brake, and the clutch. An automatic has only two - the brake and the accelerator. Mostly the car drives the same, but automatically when driving a manual car you reach for the clutch when coming to roundabouts or junctions, because you are about to change the gear. Alas, in an automatic car the result of this is to press down very hard on the brake pedal. Every time I came to a new junction I almost put my head through the dashboard. Much whiplash and injury was caused by the fact that my interface to the car had changed. Encapsulation is a very powerful tool when developing object oriented software. If you can control the access to the functionality of an object, then you leave yourself maximum flexibility if you later wish to change the way it works. When developing programs as an individual, it doesn't really matter since you know what programs make use of which methods and in which classes. When developing in a multi-developer environment, you don't know who is using your classes and in what way. You must assume that if someone can have accessed a method of your object that they will have. This can have significant repercussions for maintenance. Consider if you have a class called Person that contains attributes that describe the details about a person, and some useful utility methods. Perhaps this is a class used by an insurance agency, and you have a method that you expect people to use called isValidForInsurance that returns true or false dependant on whether someone is a candidate to be insured. In your method, you are making use of another method called queryLifeExpectancy that returns an integer based on how long that person is expected to live, based on their gender. This is a method you intended only for yourself, but you left it open for all to access. Later, you decide that you don't want to use this queryLifeExpectancy method any more - instead, you want to base the calculation on a number of different methods. If you had locked down the access, then you could have simply removed queryLifeExpectancy and changed the isValidForInsurance method accordingly. Provided the return types and parameters remained the same, no-one would know. But if you leave access to queryLifeExpectancy open, then someone else in the company may decide that your method is exactly what they need to provide the functionality for their own class, and so make use of it. Then if you change your method, or remove it, their class will break, as will any other class that makes use of your code. And any class that makes use of any class that was broken, and so on. It's possible that even the slightest change could break significant sections of the whole system. Obviously this is something that we would wish to avoid - and so we restrict access.
We have already seen the use of the public keyword in our code - this keyword means that the variables or methods using that keyword are fully open. Any class can make use of the method or variable. There is also a keyword called private, which means that only objects of the class in which the method or variable are defined have access. Finally, there is protected, which means that objects of the class in which the method or variable are defined in have access, and also any class that extends that class. Nothing else has access. Consider the following three classes:
Class C has attributes of Class A and Class B. Class B is extended from Class A, and Class A is a base class. Class B has access to everything that is public and protected in Class A, which means it has the following attributes and behaviours:
Although it has no access to the variable bang, it does have access to the public method getBang which does have access. It can manipulate the other variables directly. ClassC can manipulate the following variables and methods in each class:
This access model can be quite complex, but it will hopefully come to make more sense as we continue through the book.
Now that we've covered how to restrict access to methods and variables, we should look at solving a problem we have with our extended bank account object. Consider all the nifty code we have for regulating the setting of balances. Unfortunately, all of this can be bypassed by someone directly manipulating the balance:
This is problematic - people shouldn't be able to simply bypass all of our checks by manipulating variables. So to solve this, we need to revisit our Account object and change the access rights to the variables: public class Account {private String name;private String address;private double balance;private String notes;private double overdraft; } Now the only access to our variables is through the accessor methods we provide. Problem solved! In general, you should restrict the access to variables as tightly as possible. There is usually very little reason to have an internal variable as anything other than private, or at the most lenient, protected. You must assume that any access you provide to other developers will be made use of. This is a benefit of object orientation This is a drawback of object orientation Striking the balance between providing reusability and providing for maintenance is a tricky business, and one that is very application specifi There are no hard and fast rules that you can use to always make the right decision, but practise will be very useful in helping you to make that call.
We've learned quite a lot of new information about object orientation in this chapter, and we now need a way of documenting these new relationships of classes, methods and variables. It's time to look at some new notation for our class diagrams. In UML, an inherited relationship is represented using a line that terminates with a large outline arrow. The arrow points towards the parent class... so for our Account and ExtendedAccount, we would represent the relationship like so:
We've also talked a little about access modifiers... these also are represented on a UML class diagram. Methods and attributes can each have their own access modifiers, and we represent them with a range of symbols:
So, tying all of this together into our larger class diagram notation, we'd end up with the following representation of our ExtendedAccount program:
Again, we can see how at a glance we have a clear understanding of what methods are defined in Account, which methods are defined in ExtendedAccount, and we can even see which methods and attributes are accessible in ExtendedAccount via the access modifier symbols. We're still not done talking about new UML notation - we'll be returning to the idea periodically as we progress further through the field of object orientation.
In chapter three, we discussed some of the elements that contribute to 'good' programs. Our discussion of object orientation has opened up an opportunity to discuss another metric we can apply... that of the quality of the object architecture. As was indicated previously, much of the benefit of Object Orientation is based around the idea of re-use... there's no need to keep re-inventing the wheel because we can just take objects that already exist and extend and specialise them until they do what we want them to do. That's a very powerful idea. However, a number of problems are introduced alongside this tool. These problems generally raise their ugly heads most actively in real world situations where many developers may be working together on some larger piece of software. In such circumstances, the decisions we make regarding the way we structure our classes have ramifications that extend beyond ourselves. At this point in our development, we cannot be expected to design 'good' object architectures. However, a discussion about what a 'good' architecture is may be useful for when we start developing our own programs. There are some metrics that can be applied to determine the quality of any given object architecture. Two of the most useful ones are the metrics of coupling and cohesion. Cohesion is the degree to which the methods within a class combine to form a meaningful unit of functionality. A good class is one in which all of the methods combine to meet one firmly defined responsibility. Classes that are responsible for many different tasks demonstrate low cohesion. Classes that are responsible for one firmly defined task demonstrate high cohesion. As developers, we aim for high cohesion with our classes. Cohesion is profitably applied as a metric to classes, but also to the methods within a particular class. Just as a class should have one responsibility, so too should methods. A method should be written to achieve one firmly defined task. If the task if very complex, it should be split up into smaller methods. High cohesion has a number of benefits. The first of these is that it is easier to understand what a class is supposed to do. If the responsibilities are firmly defined, it is easier to see how the methods interact to meet the requirements of the class. If you are coming to a new class for the first time, this is a tremendous benefit. High cohesion leads to easier reuse of classes. If a class has one responsibility and doesn't attempt many unrelated responsibilities then it is a comparatively simple task to extract it from its original context and apply it to a different context. High cohesion also leads to more opportunities for further generalisation. The more we think about cohesion, the more we can recognise opportunities for extracting functionality into other classes. If a class requires functionality beyond the remit of its responsibility then it can be extracted out into one or more separate classes. Coupling on the other hand is the number of interdependancies between units of functionality (meaning methods or classes). The more connections there are between units of functionality, the more things can go wrong (we'll talk about this a bit more when we discuss integration testing in chapter 17). A coupling relationship can be one of the following:
As developers we aim for low coupling. However, it is important to realise that this doesn't have a logical termination of no coupling = best. No coupling within a program is impossible - communication between different units of functionality is neccessary. It is just important that we spent time thinking about how to minimise communication to only that which is neccessary for the functioning of our programs. There are many different kinds of coupling. The worse of these is known as content coupling, and occurs when one class directly alters the state of an attribute in another class:
As we discussed above, such modifications avoid any kind of the consistency checking that can be provided via suitable accessors. A slightly better form of coupling is called common coupling, and occurs when methods share class-wide variables. Sometimes this is the best way to share data between elements of a class. For example, when we set up a GUI we create a class-wide variable for the GUI object and then potentially access that object in many methods. Interfaces are generally not designed with reusability in mind, and so such coupling is not really a problem. For other classes however, it becomes a problem because again, there is no consistency checking. A method can easily set a class-wide variable into an invalid state which then breaks the correct functioning of the rest of the program. Another problem is that methods are tied into their context - they cannot easily be extricated and used in another context because all of their neccessary class-wide variables must go with them. It is also hard to see how methods interact - if any method can potentially change the state of the variable, then all methods must be comprehended before any of them can. The best kind of coupling is called data coupling... information is passed via parameters and return types. This is what we do all the way through this book. Data coupling is best because it allows for clear control over attribute manipulation via a predefined interface. However, even though there are good and bad forms of coupling, it should be a priority to reduce all coupling... the less coupling there is in your object architecture, the easier it is to test, update, modify and maintain. Low coupling has a number of useful benefits. For one thing, classes can be introduced as a single unit... if I want to know what class A does, I don't need to also know what class B and class C do. Much like with high cohesion, this makes comprehending a new class much easier. More importantly, it means changes can be implemented in a program without it having much of an effect on the rest of the program. In technical parlance, low coupling leads to a reduced impact of change. The overall result is much improved maintainability of object oriented programs.
There is a world of difference between maintaining your own code and maintaining code within a multi-developer collaborative environment. In such an environment, you cannot merrily change anything you don't like the look of, because you never know who is using which elements of functionality. You could ask around and make sure a change won't break anything, but that can be tricky. If you are in an organisation with two hundred developers spread across all the international time-zones, it's very difficult to communicate effectively with everyone. What happens if the one person making use of the code you want to change is on holiday, and you only find out the problem when the whole system collapses? Maintaining code in a multi-developer environment means that impact of change is a big issue - it's a straightjacket that limits all developers. There are certain things that you just cannot arbitrarily change, because of the potential for impact on other classes within the project. When maintaining code that other people may be making use of, there are some things you need to consider when altering functionality:
Also:
What a drag! It's possible to circumvent these restrictions by changing every class that is going to be affected by the change - provided that you have full access to all classes being used. That's where the importance of access modifiers comes in.
If a method is public, then it can potentially be used by any class in your project. That can be a lot of code to check. Protected attributes and methods can only be accessed in sub-classes of the main class, so that limits the impact to direct children. That greatly restricts the number of classes that must be changed. Private attributes and methods can only be accessed in the main class, so provided everything within the class is updated it won't have an affect on any other class in the project. Sometimes though you don't have that kind of luxury... consider the developers of Java. They don't have access to every class that uses their classes, because they're spread over thousands of developers in hundreds of countries. They cannot just make a unilateral change and hope for the best... how annoyed would you be if you installed a new version of Java and suddenly nothing you coded works? In such situations, you must simply sigh and accept the limitations of what you are able to do:
Deprecation in Java 1.5 is handled through deprecated attributes (more on these towards the end of the book)... but as a process it simply tells the user that the method or class they are using is not long for this world. It gives the developer a change to gently migrate towards the new improved implementation... and if they don't, then hey... you warned them. On their own pointy little heads be it.
Object orientation is powerful and a versatile tool for developing software solutions. Inheritance and encapsulation are two of the most important techniques for ensuring the effectiveness of object orientation as a concept. Object orientation can be very difficult to get right - it involves thinking at right angles compared to the way most people normally think. With a little perseverance, you will find that it is very useful for ensuring the reusability of your code and the effectiveness of later maintenance. We still haven't covered everything we need to regarding object orientation - we will have cause to return to this idea later in the book.
Memorable MemoriesThe 'memorable memories' publishing company specialises in the production of books of profound, humorous and insightful quotations culled from various profound, humorous and insightful people. They have recently been looking into spreading their influence into the online world, and their CEO (Dr. Quotey McFakeson) has decided that the best way to do this is to license out a quote generator that can be incorporated into various Java applications (for a nominal license fee). You have been hired to create this quote generator. The CEO, who is fairly knowledgeable about Object Oriented Programming, has insisted you make use of the Model-View-Controller architecture throughout. Quotes have a number of attributes associated with then:
The generator must wrap each quote into an object. The quote generator must create a library of quotes, and then provide an interface for the user to request a random quote, or request a random quote in a particular subject area, or a random quote by a particular author. Your generator must make no assumptions about presentation - it must implement only the Model of the MVC architecture. 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