![]() | Monkeys at Keyboards: Java-Fu © Michael James Heron | ||||
| Topic: Java Programming Level: 3 Version: beta | |||||
5 - Programming with Interfaces | |||||
| Previous | Table of Contents | Next |
| Forum |
| Chapter Objectives |
By the end of this chapter, the reader will be able to:
|
In this chapter we're going to take a closer look at the idea of interfaces and how they enforce structure on the many components in Java's class library. We briefly touched on the syntax of interfaces in second year programming, but for various reasons couldn't spend much time looking at why they were valuable development tools. As our discussion of component architectures continues, it will become more apparent why good design is important for maximum effectiveness. The Java class libraries are very good examples of this, as they enforce a coherent structure through the use of interfaces and polymorphism. We'll look at how this relates to the Swing architecture in particular during the course of this chapter.
Interfaces, at their base, are classes that contains a number of method stubs, but no method code. They are implemented by other classes, which are then syntactically obligated to provide the code for each method stub. A class may implement any number of interfaces, but only ever extend one other class. Interfaces themselves can extend other interfaces. A typical interface with which you are undoubtedly familiar is the ActionListener interface. The code for this is as follows: Why extend from an empty interface? We'll answer that when we recap the key principles of polymorphism later in this chapter. Any class that implements ActionListener must provide the code for all method stubs. The interface specifies only one - actionPerformed. If the class does not provide the code for this, it simply will not compile. Interfaces enforce a run-time check on the consistency of code. This strategy is used throughout the Java class libraries, and we'll look at a number of examples of this.
Interfaces really show off their power best when combined with polymorphism. Sure, the syntactical enforcement of a structure is a great benefit, but polymorphism allows for a class to be treated only in terms of its implemented interfaces, ignoring all other considerations. This means that methods can specialise their behaviour by dealing only with the general case and ignoring all specialisations. The idea sounds very complicated, but in practise it is very simple. Consider a real world example - three people are driving down a road. One is in a bus, one is in a motorcycle, and one is in a car. They see the following sign (a sign that means 'no motor vehicles'):
A car is a motor vehicle, a lorry is a motor vehicle, and a motorcycle is a motor vehicle. They all conform to the general case 'motor vehicle' vehicle description. Upon driving down this road, all drivers would be aware that the sign related to the vehicle they were occupying. However, if they saw the following sign (no buses):
Then the situation is different - a car is not a bus and a motorcycle is not a bus. Only the bus driver would be bound by the sign. This is a real world example of what is known in object oriented programming as polymorphism... we can treat an object either as its most specialised form, or as any of its more general forms. So consider the following simple class hierarchy:
If we create an instance of Dog:
And then write a method that expects a Dog parameter:
Then the only parameters we can legitimately pass as parameters are instances of the Dog class. But we could write a method that made use of polymorphism by deciding which of the more general classes will guarantee enough information to implement the functionality:
This method will happily accept parameters of type Dog, type Mammal and type Animal, but it won't accept parameters of type Thing. Within the method, the parameter will be treated as an instance of the most general class - so any methods that are declared as part of the Dog class will not be available to the tmpAnimal object while it is within the walkAnimal method. Any specialised class can be treated as an instance of a more generalised parent class. Likewise, any class that we have that implements an interface can be treated as an instance of that interface. For example, an object that implements the ActionListener interface can be treated as an object of type ActionListener:
Or, because ActionListener extends EventListener, it can be treated like an instance of that interface:
This is a very important and powerful idea, and one that may take a substantial amount of time to digest before its ramifications become apparent.
The two of these combine into very powerful programming structures. Let's take a very simple example of something that should already be familiar with - sorting arrays. In the Olden Days, it was necessary for a developer to learn how to implement sorting algorithms of their own - but nowadays, it's all done for us as a natural part of an object oriented language class library. Java is no exception, and provides two classes for sorting. One is Arrays, which will sort an array, and the other is Collections, which will sort a more complex data structure. We'll look at the latter of these. You may not have spent much time thinking about this, but how would you actually code a sorting routine that can take an object of any kind and put it into a sorted order? It's one of those things that is often taken for granted before you realise how ingeniously it is implemented. Java has no natural way of comparing two objects other than a straight memory address comparison. Most of the classes provided by the Java libraries provide a way of comparing one instance against another. For example, the String class has a method called compareTo that takes an object as a parameter and then returns a number that indicates whether the compared object was before, equal to, or after the string being compared to. What these actually mean in real terms is up to whoever implements the method. The sort method of the Collections class enforces this convention through the use of an interface - the Comperable interface requires that a method called compareTo be implemented. Since this enforces a compile-time check, the developer is then free to refer to an object as an instance of Comperable:
Collections accept (usually) any type of object as a parameter - this is an immensely generic approach that allows for a list to be entirely indifferent as to what type of objects are contained within - all it cares about is that they are Object. If it comes down to sorting a collection, all it cares about is that they are Comperable.
At the base (in a class called JComponent), all Swing components implement the Serializable interface. They offer a range of properties accessible through a series of set and get methods... and, they all offer a default constructor. Swing components are articulations of the Javabean framework. It just goes to show, Javabeans are not a concept that is new to us, even if the terminology is unfamiliar. Swing components also articulate the model-view-controller architecture we discussed in the last chapter - the component itself represents the view and the controller, but the job of implementing the model is left to individual developers. This is a common 'middle of the road' approach to MVC design, and is a consequence of the realities of programming... the original intention was to enforce separate classes for the look, the interface, and the model - however, the necessity of tightly coupling a controller with its view meant that some compromises had to be made. This is an important guiding principle - let your application dictate your design strategy. Swing components represent many of the best things about component based programs. They are conceptually simple to appreciate, they are familiar, and they are well-designed. In short, they are ideal solid examples of the design principles we discussed in the previous chapter.
Principle: A software component needs to have a formally defined interface that determines how data is sent to and returned from it.Swing components adhere to the Javabean specification, and so follow the common standard for sending and receiving information. Partially this is done through the standard property accessors, and partially this is done through the Java event model. Principle: A software component should have a single external 'footprint' - it should be included in a project as an instantiation of a single unit, not as a collection of many classes that must be inter-related by the developer.The internal makeup of a Swing component is very complex - let's look at a subset of how a JButton is internally constructed: Here we are taking an arbitrary cut-off point at JComponent (which extends from a class called Container), ignoring the many other classes that are involved in the construction of a JButton. The point is not to exhaustively document the details of a JButton's class structure, but to demonstrate an important point - you don't need to know any of this. When you want to include a JButton in your code, you just create an instance of it - that's all you need to know. The JButton class is the culmination of a hugely complex setup, but it has only a single external footprint... that of the JButton class. The internal complexity is neatly encapsulated away from the user in a perfect articulation of the 'black box' principle. Principle: A software component needs to make use of a well documented file format for storing persistent data. All relevant elements of data should be accessible so that they can be fed into further components for specialising functionality.This isn't usually an issue with GUI components since they are containers for data that is represented elsewhere, but even so the Swing components implement the Serializable interface, and as such do indeed make use of a well documented file format. Some components even make use of formal tree structures for representing complex data - we'll look at this later on in the module when we talk about XML and the Document Object Model.
The design strategy used within Swing is sometimes known as the separable model architecture - the GUI component is handled by what is known as a UI Delegate, and the model is handled by an external object through the use of events. This is a common strategy for those situations where separating out the view and controller does not give measurable benefits (consider the event-driven applets written during your second year programming modules). However, this hasn't reduced the configurability of Swing components, as is evidenced by the fact that it's possible to programmatically change the look and feel of a Swing application. This is done through a static method called setLookAndFeel on the class UIManager. There are a range of supported values for this - not all are available on all systems, but you should experiment to see one with which you are happy:
For example:
This example shows the interaction between the Swing component UI delegate and a general manager class that allows for commonalities to be enforced throughout an application. The whole MVC model of a swing component is shown in the following diagram:
The result of setting the look and feel is that the way widgets are rendered on the screen is altered according to the requested template, like so:
As we can see, Swing components meet all the design principles of components as outlined in the last chapter, and they do provide very substantial elements of functionality. However, the fact that they divorce the model from the view and controller, and place the emphasis for that implementation onto the developer provides an opportunity for specialising them into even more useful components. Consider for example a rotating image system - to implement that from the standard libraries, we need some additional code. However, we can specialise the JLabel component to provide us with such a component we can simply plug into an application with no extra code:
This takes the basic framework as provided by the Swing component library, and builds on it to provide a more specialised component that can be slotted into an application in the same way as a standard label:
Despite the fact we have created an entirely new component, we can still make use of it exactly the same way as any component in the base Java libraries can be used. Polymorphism ensures that any method that accepts a JLabel as a parameter will also accept an instance of our new RotatingImageLabel - we are required to do nothing extra. The fundamental aspects of polymorphism are woven all the way through Java - its' a feature of how we deal with collections, and how we deal with exceptions, and even how we deal with setting up a user interface. All of it is based on the fact that we can generalise when specifying the type of objects. Part of the power of that polymorphism comes from the flexibility of interfaces. Thoughtful readers may ask themselves 'Ah, but wouldn't that also be possible if Java just allowed for multiple inheritance'? It certainly would, but multiple inheritance comes with its own problems - notably that of the diamond conflict. Consider an example of a class hierarchy incorporating multiple parents:
As you can probably guess from the diagram, the term diamond conflict relates to the shape of the class tree. Consider the following implementation in Java (obviously this is not a syntactically correct example, since there is only single class inheritance in Java. Consider it an example of what the syntax might look like if you could extend several classes):
So, what happens when the develop creates an instance of the class Pegasus and then calls move? Does it call the one in Horse? Or the one in Bird? Or does it call both, and if it does, in what order? The diamond conflict raises its head when a child class inherits a method with an indentical method signature to that of another parent. The conflict can be resolved by over-riding the method within Pegasus to ensure that the correct functionality is executed:
For very simple structures, this is possible - but in the real world, it becomes more and more difficult to ensure multiple-class compatibility as the class library grows. The real world complexities of ensuring a solid class library means that single inheritance is growing in popularity (as is evidenced by much of the .NET framework), with interfaces being used to fill the gap:
However, this introduces its own problem - it's possible for two interfaces to have a naming clash, and if a class implements both interfaces there can be integration difficulties. If they have identical method signatures, then it is not a syntactical problem - but the power of polymorphism means that the subclass can be cast into any of its more general interfaces - our Pegasus above could be either a FlyingThing or a RunningThing, for example. If the method signatures are identical (but the return types are different), then it is not possible for a class to implement two clashing methods. This problem can only really be avoided by making sure there are no naming clashes in the first place - but as developers draw on software from locations other than the Java libraries, it results in less control over any standards. The only place the developer has control is within their own code. Java may have solved the traditional multiple inheritance diamond problem, but it has introduced one of its own in the process. Interfaces are definitely great tools for the thoughtful developer, but thought must go into their design lest they devour you utterly. The point of all of this is that polymorphism drives much of Java's flexibility, and Interfaces are one of the primary tools that can be used to take advantage of polymorphism.
Understanding polymorphism is often confounded by the strange syntax and even stranger behaviour. Consider the following Java program:
If myAnimal is an instance of Bird, then it will print out 'Flying!'. If myAnimal is an instance of Horse it will print out 'Galloping!'. And yet, those methods are implemented in a class that specialises Animal - that functionality is not available in the Animal class itself. It is the mechanism of dynamic or late binding that allows for this. Binding refers to the process where a compiler will a method call with the method body - in some languages, this is done at compile time. Unless a method is declared as final or static in Java, the JVM will perform the binding at run-time. This is a cost in terms of efficiency, but it allows for polymorphism by delaying the final binding operation until all facts are known - such as, what the exact object being used as the basis of the call will be. The consequence of this is that Java will know, when the program is running, what the specialised version of the polymorphic object is, and will call the most specialised version of any method. Polymorphism allows for objects to be treated as general cases for references, but still as their most specific case for functionality.
The makeup of Java's classes is technically interesting, and serves as a good example of what is (mostly) a very coherent design strategy that is worth understanding. Swing itself is an excellent example of this, articulating both a solid design strategy (through the use of model separation) and a flexible class structure. It is Interfaces and Polymorphism that drives most of Java's flexibility, and this is something you should keep in mind when developing your own components, especially those components that have a wide-scope or potential for future expansion. Thinking through your design from the start will be far more valuable than what code you may be able to produce in the short-term. 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