Previous |
Next |
So far this book has concentrated on using Java with a minimal coverage of the underlying theory. This is based on the assumption that most people learn best when doing rather than thinking. I have no time for the attitude that writing code is a trivial activity done only by “code monkeys”, programming is a creative and absorbing pastime that is inherently intellectually demanding. However every programmer should be able to see the bigger picture needed by software architects and system designers. From an organisational perspective creating software is very expensive. Some projects have cost tens of millions of pounds and yet been abandoned without success. In the early days of programming languages the main objective was simply to allow programs to be written. Once that task was achieved the focus moved on to allowing the management of complexity.
When i first started programming I thought that the people who created commercial software must have been geniuses, people of an entirely different level of intellect to myself. Over time I began to appreciate that much of what they achieved was not due simply to raw intellect or ability but due largely to the management of complexity. A great engineer such as those behind new aeroplanes or civil engineering projects will generally not have an intimate knowledge of every nut, bolt and rivet of the system, but will understand the systems in generalised overall terms. Other designers and engineers will have a more intimate knowledge of each subcomponent.
It is very important to understand the underlying theories behind the language, as it can have a big effect on how you design and create your own programs, and it can help you understand the thinking behind programs by other people, and the reasoning behind the decisions of the overall architects of the system.
The designers of programming languages have to prioritise the most important features. Thus the BASIC and Pascal languages were designed primarily for teaching. With BASIC the priority was that it should be very easy to use and with Pascal it was both fairly easy to use and fitted in with the ideas of what a good language should be. With Java one of the overriding requirements was that that it would be used for substantial software engineering projects involving multiple programmers and designers. As a result it supports features that are easy to overlook if you are new to the language and writing your first programs in isolation. If this chapter seems overly theoretical and abstract, bear with me as the ideas it contains are very important to the wider world of software engineering.
Object Oriented programming is the latest in a series of efforts to make programming easier by dealing with its inherent complexity. The first to major steps in language developments was first the move from assembler to high level languages. Assembler required an understanding of the CPU architecture of every machine that was being programmed. If you think that the speed of a modern CPU is measured in Mghz or millions of cycles per second, whereas the speed of the first IBM PC was measured in terms of thousands of cycles per second, you can appreciate how important raw performance used to be.
The limitations of assembler This made programming a complex task, but it did get the maximum performance out of the system. The move to high level languages meant languages could be portable across different hardware systems and they were designed to be more like human languages rather than computer languages.
The next major step was the idea of structured languages where complexity could be managed by breaking the program into discreet chunks of functionality by the use of functions, named pieces of code that could be regarded almost as discreet separate programs.
Object orientation is the latest concept to manage complexity in programming, and it does this by attempting to tie code and data together so that objects (instances of classes) “send messages” to each other. I find the use of the term “sending messages” to be rather distracting and I regard methods as the OO version of functions in structured programming. This may simply reflect my background in structured languages such as C and Pascal rather than any reflection on the nature of Object Oriented concepts.
Java is one of the more recent in a long line of Object Oriented programming languages. One of the earliest and most influential was SmallTalk which was developed at Xerox PARC (Palo Alto Research Centre) in the late 1970's. Yes that is the same Xerox that does the photo copiers, they were good at inventing things but not so good at exploiting them. Smalltalk never reached commercial critical mass, but the C++ language developed at AT&T Bell Laboratories by Bjarne Strousjoup became a huge success. This was partly because it built on the syntax of the C language which was the de-facto standard for low level programming. C++ is a hybrid language which can be used to write in the older procedural way, or in the Object Oriented style. In a similar way that C++ was an extension of C, Java was modelled on C++. Unlike C++ Java was designed to be truer to the ideal of Object Orientation. The use of OO in languages generally as a performance overhead, but by a combination of optimisation and the increase in hardware performance this has been largely offset.
The C# language created by Microsoft borrowed many of the ideas in Java and attempted to improve on it. People who have programmed in C# say it is similar enough to Java to make it easy to switch between the languages.
The terms object class are sometimes used interchangeably, but they are quite separate, if related concepts. A class is the blueprint for an object. If you think of the real world you could consider the plan for a house as being a class. Using the plan for a house you can build many houses. Each one of those houses would be considered an object of the type house.
The three central principles of Object Orientation are
Inheritance
Polymorphism
Encapsulation
Inheritance is at the heart of Object Orientation, it means basing a new class on an existing class. The use term inheritance comes from the way it is used in nature, as in “I inherited my fathers large nose”, or “I inherited my grandfathers huge estate and massive 90 bedroom stately home”. The idea is that rather than creating functionality “from scratch” each time you can get it from an existing example. The practical benefit is that a programmer has less work to do and the code that is used will have been developed and tested over a longer period of time, which should result in more reliable code. Another benefit of Inheritance is that you can inherit most of the functionality of a parent class but modify some of that behaviour, again all without necessarily having access to the source code of the parent class.
The relationship between classes that inherit from each other can be described in several different ways, some of which are not obvious. Thus a class is said to be a child of another or that it “inherits from another” . A class can also be described as “a base class”, a slightly confusing term as base generally means at the bottom of, and if you look at an inheritance tree, you might think that the most “recent” class would be at the bottom and would thus be the base class. In OO terms the base class is the one further up the inheritance tree, i.e. The grandparent or great grandparent class. Where one class is a child of another it is sometimes described as being a subtype of the other.
The keyword for inheritance is “extends”, thus one class extends another. Here is an example
abstract class Animal{
private String sName="animal";
public String getName(){
return sName;
}
}
public class Cat extends Animal{
public static void main(String argv[]){
Cat c = new Cat();
System.out.println(c.getName());
}
}
Note how the class Cat has no method called getName but because it extends the class Animal it has access to that functionality. A class can only extend one other class, as Java implements only single inheritance (i.e. One parent class, unlike humans who have a default of two parents).
Going back to the example of the Animal class as a base type or child type, note that there is no concrete example in the real world of a type of Animal that is just an Animal. All animals are some other type, be that a Cat, Dog, bird or Cow. In the same way a base class in Java will often be of a type that you may never create an instance of, but is only used as the base class, or foundation of another. You can specifically mark a class for this purpose using the keyword abstract. An abstract class can have data and methods but you cannot create and instance of it, only an instance of a sub class.
The hierarchy of inheritance can be as deep as you like, i.e. you can have a child with a parent, grandparent, great grandparent great, great, grandparent etc. etc. (you get the picture). However in a similar way that hierarchies of disk directories usually only go to a few levels it is more common to only have relatively shallow hierarchies. You can see demonstrations of typical inheritance trees if you look at the API (Application Programmer Interface) documentation of the Java class libraries. For example if you take a look at any of the Collection classes you will see what the parents and grandparents of a class are, and also what other classes inherit from each class.
The linguistic roots of the term polymorphism can be translated into “many meanings”. In programming terms polymorphism is where multiple methods of the same names have different meanings. Selecting method names is an important skill for a programmer, choosing good names can make a big difference to how easy it is to understand code. For this reason programmers have developed naming conventions so that methods have similar names for similar purposes. Thus in Java there is the JavaBeans naming convention that cover frequently recurring tasks that methods are expected to perform. This takes the form of what are called get and set methods e.g.
getFirstName setFirstName
An experienced programmer will immediately recognise these methods as “accessor” and “mutator” methods. Thus getFirstName will return information on a field called firstname and setFirstName will set the value of the firstname field. A programmer who was not familiar with the JavaBeans naming convention might reasonably have created methods called findFirstName and updateFirstName, but adhering to the established naming convention gives other programmers a slight head start in understanding the code.
As most programming involves working with existing code, ease of understanding is an essential attribute of good code. One of the ways polymorphism is implemented in Java is via method overloading. This is where two methods have the same name but different argument types.
If you have two methods with the same name, there is a need to distinguish between the different versions. In java this is done via the argument list to the method. Thus for example you could have
public update(String sFirstName) public update(double dSalary)
Note how this shows two methods with the same name but different parameter types. Thus any call to the method update will have a different meaning, depending on the type of parameter passed to it. Thus a String parameter will call the first version, whereas the second will call the double version. This is mainly of use to programmers as it eases the burden of memorising method names. From a computer/compiler perspective it would as efficient to have two different methods, perhaps one called updateName and the other called updateSalary. In a small program having two methods would not be a problem, but in an industrial sized program with multiple programmers it places a slight additional burden on the attention of programmers and the need for documentation.
public class PolyUpdate{
private String sFirstName;
private double dSalary;
public static void main(String argv[]){
new PolyUpdate();
}
/** Constructor for the class */
PolyUpdate(){
/*Note how the same method name is used to
call two versions of update method */
update("james");
update(10.1);
}
public void update(String sFirstName){
this.sFirstName = sFirstName;
}
public void update(double dSalary){
this.dSalary=dSalary;
}
}
In this example an anonymous instance of the program is created with the call
new PolyUpdate();
Within the constructor there are two calls to update, one receives a string and one a double value. Java knows which version to call based on the data type passed to the method. Note that the names of the parameter do not make any difference only the type and order. Thus it would be considered overloading if you had two methods as follows.
|
|
Overloaded methods are distinguished by the argument type and order |
public void update(double dSalary, String sFirstName){
/* method body goes here */
}
public void update(String sFirstName, double dSalary){
/* method body goes here */
}Runtime polymorphism is more subtle than the compile time overloading of method names. To understand runtime polymorphism you have to be clear about the purpose of the reference and the actual object in memory a reference points to. Usually they have the same type, e.g.
MyClass mc = new MyClass();
The reference mc has the type MyClass which is the same as the type of actual object that is created in memory. A reference is effectively a pointer to the actual object in memory. You can think of this like an entry in an index of a book. You can use the index entry to find out about the part of the book it points to, but it is only a pointer it is not the text itself. If you tear out the index, the text still exists, you just don't have a map of how to get to it. Another analogy used by some authors is to view a reference as if it were a remote control device for a piece of electronic equipment.
You can use a remote control to manipulate the TV but it is not the device itself. Not only that but you can use more than one remote control device with a single piece of equipment. That doesn't mean you have more than one piece of equipment, you just have more than one device to “point” to it. When you press a button on a remote control device the functionality invoked is based on the equipment being controlled not the type of remote control. Thus if you have stereo sound on your TV, turning on the stereo sound via the remote control will give you stereo on the device. Changing anything on the remote control will not do anything to give you stereo if the actual TV does not support it.
To leave that analogy alone and go back to Java objects and references, Java decides what method will be invoked based on the type of the object not the type of reference to it. The type of method invoked depends on the type of the object not the type of the reference that points to it.
It is possible to create an object but have the reference be of another type such as a base class or interface implemented by the object. Take the following code.
abstract class Animal{
}
public class Cat extends Animal{
public static void main(String argv[]){
Animal c = new Cat();
}
}
|
|
The version of method executed depends on the object type, not the reference type
|
Because the actual method invoked is based on the Object type and not the reference type it cannot be certain until runtime what method runs. This is illustrated by the following code.
/**
*Demonstration of late binding
*by Marcus Green 2005
*/
class Animal{
public String getSound(){
return "AnimalNoise";
}
}
class Duck extends Animal{
public String getSound(){
return "Quack";
}
}
class Dog extends Animal{
public String getSound(){
return "Bark";
}
}
public class LateBind{
public static void main(String argv[])throws Exception{
/**The next line creates a class based
* on the command line parameter.
**/
Animal a = (Animal) Class.forName(argv[0]).newInstance();
/** The method invoked depends on the type of
* the object not the type of the reference
**/
System.out.print(a.getSound());
}
}
The first line of the main method creates an object based on the first parameter passed from the command line. Thus if you type
Java LateBind Dog
The reference will point to an instance of the class Dog. Note, you do not need to know the syntax in that line of code for the purpose of the Certification exam, and it is not integral to most examples of late binding, just accept that it works for this example.
The final line with the call to the getSound method will output either Quack or Bark depending on the type of the object. Note that either way the type of the reference is type Animal. This depends on inheritance in that both Duck and Dog are sub classes of the Animal class and thus they are both of type Animal as well as their actual class. If you were using the objects as parameters to a method you could pass either Dog or Duck to a method that expects a parameter of type Animal.
You can write an awful lot of Java without worrying too much about late binding or runtime polymorphism, but it is a central concept that is worth studying to get a grasp on a fundamental language concept. It is the type of topic that people like to ask about at Job interviews to check that you are not a language lightweight.
Constructors are a common feature of many Object Oriented languages. Constructors are special methods that have the same name as the class itself, and are executed automatically when an object is created, and are generally used to initialize the object. They were mentioned briefly in chapter 4, but they are worth looking at in detail as almost any non trivial Java program will use them. In addition to having the same name as the class a constructor must have no return type.
Thus in the following class the method MyCon is not a constructor.
public class MyCon{
public static void main(String argv[]){
MyCon mc = new MyCon();
}
public void MyCon(){
System.out.println("MyCon");
}
}
If you compile and run this code you will find that there is no output, because method MyCon has a return type of void, which means it is not a constructor. Constructors are not inherited. It can sometimes seem like they are inherited because they are invoked “down the stack”. Thus if you take the following class the output will be
Base constructor
ConIn constructor
class Base{
Base(){
System.out.println("Base constructor");
}
}
public class ConIn extends Base{
public static void main(String argv[]){
ConIn ci = new ConIn();
}
ConIn(){
System.out.println("ConIn constructor");
}
}
Note there is no explicit call to the Base constructor, but it is invoked automatically when the ConIn constructor is called. Let me re-state, constructors are not inherited. If there were further generations of grandparent constructors these would also be called.
Constructors can be overloaded in a similar way to methods. Constructors are used to initialize classes, and it is common to want more than one way of initialize an instance of a class. Thus you might have a quiz class you might have one version that included a userid and one that had no associated user. Thus you could have classes as follows.
public class Quiz{
Quiz(int iSubjectid, int iUserid){
/* constructor body */
}
Quiz(int iSubjectId){
/* no userid in this version */
}
}Both of the following lines of code will instantiate Quiz objects.
Quiz q = new Quiz(1,1); Quiz q2 = new Quiz(1);
Once you get used to overloaded constructors it is hard to imagine writing code without them. I have written code using PHP4 and found the lack of overloaded constructors to be quite frustrating.
If you don't provide your class with a constructor the Java system provides it with a default zero parameter constructor. This is effectively an invisible, behind the scenes constructor that you generally would not be aware of. However it has a slightly peculiar side effect. If you provide your class with any other constructor, i.e. one that takes a parameter the Java system doesn't provide you with the default zero parameter constructor. The effect of this is that if you have an existing class without a constructor you can happily create it without any parameter.
MyClass mc = new MyClass();
And everything is fine. But if you then modify the class so it has a constructor that takes a parameter, e.g.
MyClass(int i){
//do some initializing
}Suddenly the previous code wont work. If you try to create an instance of your class with the code
MyClass mc = new MyClass();
You will get an error.
The other situation where the default constructor come into play is due to inheritance. Take the following code
//Warning: will not compile.
class Base{
Base(int i){
System.out.println("single int constructor");
}
}
public class Cons extends Base{
public static void main(String argv[]){
Cons con = new Cons();
}
}
Although constructors are not inherited, they are called “down the stack”, thus the constructor for Base will be called before any constructor in Cons. Because Base has no zero parameter constructor the code will not compile. The code can be fixed simply by giving the Base class a zero parameter constructor, thus.
//fixed version of Cons
class Base{
Base(){}
Base(int i){
System.out.println("single int constructor");
}
}
public class Cons extends Base{
public static void main(String argv[]){
Cons con = new Cons();
}
}Actually Java doesn't support the direct equivalent of what are destructor's in other languages. languages such as C++ support destructor's as a way of ensuring that resources, memory in particular are freed up when objects cease to exist. Java does not need destructor's for this purpose as it has automatic memory management. However Java supports the idea of a finalizer, a method with the magic name of finalize and return type of void that is guaranteed to run, and run once only before an object is garbage collected and ceases to exist.
You may read in some texts that the finalize method is a useful place to release system resources such as database connections and file handles. The problem with this is that you can absolutely never be certain when the finalizer method will run because you can never be certain when garbage collection will run. Note that you can suggest that Garbage collection runs, but this is purely a suggestion and may not actually cause it to happen. So if you create a large number of objects that acquire a large amount of resources, you may well exhaust those resources before garbage collection releases those that are not used. My view of the finalize method can be summed up by the fact that when I just searched Google on the phrase “avoid finalizers” it returned 1,130 references, on balance you should avoid relying on finalize methods.
Other Sources
The API docs for the finalize method
http://java.sun.com/j2se/1.5.0/docs/api/java/lang/Object.html#finalize()
Sun tutorial on constructors
http://java.sun.com/docs/books/tutorial/java/javaOO/constructors.html
Foundations of OO Languages
http://www.cs.pomona.edu/~kim/FOOLbook.html
Some notes on
constructors
http://leepoint.net/notes-java/oop/constructors/constructor.html
Bruce Eckel on polymorphism
http://www.faqs.org/docs/think_java/TIJ309.htm
Dan McCraken on Constructors
http://www.ccnyddm.com/
Bruce Eckel on constructors
http://codeguru.earthweb.com/java/tij/tij0049.shtml
Previous |
Next |