CO523: Object-orientation in Java |
[ introduction | model | statics | interaction | magic objects | constructors | inheritance ]
This page provides a brief introduction to object-orientation ("OO") in Java, in the context of the CO523 module.
The OO model is pretty clean conceptually. Articles in the problem domain (talking about what we want a program to do) are modelled as classes. A class, or rather an instance of that class (aka 'object' below), has associated with it some data (state) and some code (to manipulate that state). When going from one to the other, some thought is required as to how the problem is decomposed, i.e. what particular set of classes we end up with.
For example, if we wanted to implement a system to manage the library's book loaning process (not a trivial undertaking), we could try and have a single class called "Library" with all the necessary code and data in there, but that would end up being impossibly complex (and a single massive Java file). At the other end of the scale, we could create a set of classes "FredBarnes", "JoeBlogs", "ProfessorSmart", "AHistoryOfUnix", "JavaInTwoSemesters", "LinuxKernelInternals", etc. -- and that's likely to end up being impossibly complex too. Instead some balance must be sought, which requires a bit of programming insight (something gained through the combination of knowledge, understanding, creative thinking and probably motivation). A good choice might be to define classes such as "Student", "Staff", "Book", "LoanRecord", etc.
When deciding on how to decompose a problem, it's important that you understand the roles and relationships of classes and objects. Essentially, a class is nothing significant at run-time -- it is just a description of how to create an object. Objects are significant at run-time, and form the basis of working OO programs. One way of thinking about it is the class being a rubber-stamp, used to stamp out objects. Although objects created from the same class are initially identical (there are a few exceptions, but we'll come back to those), the data (state) contained within an object can change independent of other objects -- e.g. using the rubber-stamp idea, colouring in the stamped images differently. Within a program, we could use a "Student" class (which describes a generic student) to create instances of specific students, e.g. "fred_barnes", "dave_reeve", "phill_camp".
[more formally, a class is a type, and objects are members of that type (like elements of a set); a 32-bit integer type has around 4 billion different values (or members/elements), class types generally have significantly more (theoretically often infinite, but in reality bounded by a machine's memory size)]
The actual execution (running) of a Java program is based on method calls. That is, making calls on object instances to do stuff (where 'stuff' includes inspecting or changing the state of an object, e.g. changing the name of a "Student" object, and more general methods). The program has to start somewhere, and that is handled by the slightly special 'main' method. The code found in 'main' will typically create some objects then start making method calls on these.
Since "Student" has already been mentioned above, we'll use that as an example. A good first question to consider is what attributes a student has (what 'data' or 'state' does it need ?). A few obvious things might be the student's name and student-ID. We could then start out by writing:
/** * Student.java -- defines a student * @author J. Random Hacker */ public class Student { private String name; private String student_id; }
The class "String" has been chosen for the type of 'name' and 'student_id'. This is the type used for handling strings in Java; C and C++ use arrays of characters. We've also declared the 'name' and 'student_id' to be private, i.e. only accessible from code within the "Student" class itself.
The next step would be to define some methods for getting and setting the attributes already defined -- e.g. to find out what a student's name is. Methods that specifically get or set attributes are known as accessors and mutators respectively. The modified code (in a file called "Student.java") would be:
/** * Student.java -- defines a student * @author J. Random Hacker */ public class Student { private String name; private String student_id; public String getName () { return name; } public String getStudentID () { return student_id; } public void setName (String name) { this.name = name; } public void setStudentID (String id) { student_id = id; } }
The first methods ('getName' and 'getStudentID') are fairly straightforward. The method signature for both indicates that the method is publically visible (can be called from anywhere), returns a 'String' and takes no parameters. The method body in each case returns the corresponding attribute ('name' or 'student_id').
The third method ('setName'), used to change the name of a "Student", is more complex. The method signature indicates that the method takes a "String" parameter called 'name' and returns nothing. Essentially, we want the body of this method to change the 'name' attribute (done by assignment), but the parameter from which we want to set it is also called 'name'. There are two ways out of this situation, the first would be to change the name of the parameter and write the method as:
public void setName (String newname) { name = newname; }
This is perfectly sensible, since the name of the parameter only has meaning (scope) within the body of that method -- not anywhere else. Less sensible would be changing the name of the attribute from 'name' to 'student_name', as this would require changing the accessor method 'getName' too (and possibly others).
The second solution is as shown in the large code block -- to prefix the attribute referred to with "this." (note the dot). The dot-notation in Java is used to access attributes or methods within an object (instance of a class), and sometimes within classes themselves. The "this" object is special -- within a method body it represents the current object for the class in which the method is executing. For instance, when executing the body of "setName", "this" will refer to the "Student" object that is having its name set (or for any method called). Hence, "this.name" refers to the 'name' attribute within the particular Student object.
[the 'this' object can be used to refer to all attributes or methods within a class's own methods, e.g. the 'getName' accessor method could 'return this.name', but is generally only used when necessary (differentiating an object/class attribute from a parameter or local variable of the same name).]
The fourth method above ('setStudentID') like the first two is fairly straightforward. The method signature indicates that the method takes a "String" parameter called 'id' and returns nothing. All the body of the method does is assign the new student ID given in the 'id' parameter to the 'student_id' attribute.
The "static" keyword is almost inescapable in Java. The first place you are likely to encounter it is in the 'main' method of a program. For example:
public class MyProgram { public static void main (String args[]) { System.out.println ("Hello world!"); } }
Normally, to access an attribute or method within a class requires an instance of that class -- i.e. an object. The "static" modifier makes an attribute or method exist only once, regardless of how many objects there are. The typical use is to allow methods or attributes to exist without requiring an instance of a class, and is how it works for 'main'. Another use is to allow all instances of a certain class to share some particular piece of data (which may be an instance of another class) -- in this way, a class can behave differently each time a new instance of it is created.
As an example, consider something like the "Sqrt" class, which has a static method by default:
public class Sqrt { public static double sqrt (double val) { ... method body } }
To call this, the "SqrtTest" test-harness does something like:
r = Sqrt.sqrt (42.0);
That is, it uses the "sqrt" method by referring to it as "Sqrt.sqrt", which uses the class name, rather than any instance of it. If the 'static' modifier was omitted from the method header, i.e. if we had:
public class Sqrt { public double sqrt (double val) { ... method body } }
then the previous technique for calling the "sqrt" method would not work anymore -- the compiler would report an error. Instead we need an instance of the "Sqrt" class to be able to use its method. This can be done as follows:
Sqrt my_sq = new Sqrt (); r = my_sq.sqrt (42.0);
The "new" operator is used to create an instance of a class (an object) -- similar to C++. Unlike C++, however, there is no "delete" operator to get rid of an object, the Java Virtual Machine (JVM) cleans up as necessary.
Much of the code within a Java program is spent interacting with objects, as shown in the last bit of code above. Using the "Student" example, we might have a program create a few student objects for use later, and set the names of those students, e.g.:
public static void main (String args[]) { Student x, y, z; // declare 3 students // create new student objects x = new Student (); y = new Student (); z = new Student (); // set student names x.setName ("Fred Barnes"); y.setName ("Dave Reeve"); z.setName ("Phill Camp"); ... do more stuff }
Now we have 3 student objects, which can be inspected by calling the various accessor methods, e.g.:
System.out.println ("The student 'x' is called: " + x.getName ()); System.out.println ("The student 'y' is called: " + y.getName ()); System.out.println ("The student 'z' is called: " + z.getName ());
The 'getName' accessor returns a "String" object, which we "add" to a constant string, before passing the whole lot to "System.out.println" which prints it on the screen. Because we don't need to worry about memory management in Java programs (i.e. telling the system we no longer need objects created by "new"), we can be quite free in expression of program logic. For example:
x.setName (y.getName() + " Junior");
You can write things such as this in C++, but it is not generally a good idea.
Something worth mentioning at this point is the "System" class. Typically you will use this is for outputting data to the screen. The "System" class has within it 3 static attributes:
public static PrintStream err; // standard error output (screen) public static PrintStream out; // standard output (screen) public static InputStream in; // standard input (keyboard)
These are instances of "PrintStream" and "InputStream" objects that provide access to the screen and keyboard. It is the "PrintStream" class that contains the method "println" (amongst others).
Most of Java is straightforward -- programs are built from objects (instances of classes), and there are a vast number of classes in the Java API. Two classes are slightly peculiar, these are arrays and strings.
Strings are peculiar in the sense that they can be added together, though the meaning is obvious -- string concatenation (joining two or more strings together). We can also sometimes write code such as:
public static void reportStudent (Student s) { System.out.println ("The student is called: " + s); }
Where we "add" some "Student" object directly. This works provided that the class "Student" (as for any other class) has the method:
public String toString () { ... method body }
When such objects are encountered where a "String" is expected, Java will automatically put in the call to the 'toString' method. The earlier code to report a student could therefore also be written as:
public static void reportStudent (Student s) { System.out.println ("The student is called: " + s.toString()); }
For the primitive types ('int', 'double', etc.), string concatenation will automatically transform these into string representations. The way this works is not entirely unlike the introduction of ".toString()" for objects -- Java has corresponding classes for each of the primitive types, e.g. "Integer" for an 'int', "Double" for a 'double', etc.; and these can be constructed with a given value. For example, the code:
int x = 42; System.out.println ("The number was: " + x); // concatenate primitive 'int'
could also be written as:
int x = 42; Integer ix = new Integer (x); System.out.println ("The number was: " + ix); // concatenate object type "Integer"
or as:
int x = 42; Integer ix = new Integer (x); System.out.println ("The number was: " + ix.toString());
or as:
int x = 42; System.out.println ("The number was: " + new Integer(x).toString());
The last example creates a very temporary "Integer" object, which is used only to extract its string representation with the 'toString' method. This is a good example of something which you would normally not do in other OO languages (like C++) -- because there's no easy way to free the temporary "Integer" object created. A side effect of string handling in Java, without looking at it deeply, is that turning single integers (or other objects) into strings requires a slight fudge. For example, we cannot write this:
int x = 42; System.out.println (x);
The Java compiler will complain that 'println()' expects a "String" parameter, whereas we're currently passing an 'int' primitive type. To turn it into a string requires concatenating it with an empty string, i.e.:
int x = 42; System.out.println ("" + x);
One last point about strings in Java which is worth mentioning: string constants, such as '"hello world!"', are automatically "String" objects. This makes it possible to write slightly odd things such as:
System.out.println ("The length of the message is: " + "this is the message".length());
Where we call the 'length()' method on a string constant, which returns an 'int', which is automatically converted into a new "String" and concatenated with the message text (creating another new string), before being passed to 'System.out.println'.
Arrays are slightly odd in Java. Like the primitive types they aren't really objects per se, but can be treated as such in some cases. For example, a 'main' method that prints out its arguments (parameters given on the command-line when invoking Java) could be written:
public static void main (String args[]) { int i; for (i=0; i<args.length; i++) { System.out.println ("argument " + i + " was: " + args[i]); } }
As shown in the code, the length of an array can be determined by accessing its 'length' attribute ("args.length"). Like objects, arrays are created using the "new" operator, for example:
int values[]; // declare array of ints values = new int[42]; // allocate array
Creating an initialised array (one already filled with data) is done using a slightly special syntax. For example:
values = new int[] { 2, 4, 6, 8, 10 };
This creates an array of 5 integers, with the values shown. A common use for this is when creating temporary arrays to pass parameters to methods. For example (and slightly more realistic):
CSProcess prod, cons; Channel c = new Channel (); prod = new Producer (c); cons = new Consumer (c); new CSParallel (new CSProcess[] {prod, cons}).run();
In the last line here, a temporary array is created containing the two objects 'prod' and 'cons', which is then passed as a parameter to the constructor of a new "CSParallel" object, on which we call the 'run' method.
One peculiar thing you may notice in the last example is that a new "Producer" object is created, but then assigned to a variable of type "CSProcess". This is related to inheritance, covered below.
Constructors are a slightly special type of method, used when creating objects with the "new" operator. For example, we have already seen things such as:
Integer x = new Integer (42);
The corresponding constructor codes for the creation of an "Integer" with a single 'int' argument. Returning to the "Student" example, we might want to create a student without having to set the name explicity using a mutator method. E.g. instead of writing:
Student x = new Student (); x.setName ("Fred Barnes");
We would prefer to write this:
Student x = new Student ("Fred Barnes");
In order to allow this, we need to add a constructor to the "Student" class, that takes a single 'String' parameter:
public Student (String name) { this.name = name; }
The body of the constructor is essentially the same as the 'setName' method -- it assigns the 'name' parameter to the 'name' attribute. The odd thing about the constructor is that it has no return type, nor does it actually need to create the "Student" object -- Java implicitly creates the object (from the "new" operator) before executing the constructor (so 'this' refers to the newly created object). For more complex objects, the constructor would typically initialise all the various attributes.
As with methods (although this hasn't really been covered yet), constructors can be overloaded. This is a term that refers to the ability to have many methods with the same name, but with differing parameter types. As with C++, this provides for some flexibility in interacting with objects. For example, we can define another constructor for the "Student" that takes another "Student" object as a parameter -- essentially making a copy of a 'Student' object:
public Student (Student s) { name = s.getName (); student_id = s.getStudentID (); }
This simply sets the 'name' and 'student_id' attributes to the values extracted from the student represented by "s". However, there is a small issue of duplicated code here -- we now have two constructors, both of which contain an assignment to set the 'name' attribute. In this example it's not really a problem, but for complex classes, this might represent hundreds of lines of duplicated code. To avoid this problem, we can call the code in one constructor from another constructor. For example, the above constructor, reusing the earlier (single 'String' parameter) constructor, could be written as:
public Student (Student s) { this (s.getName()); student_id = s.getStudentID (); }
A bit of thought needs to be taken when dealing with these to avoid problems with recursion. A good rule of thumb is to only reuse constructors which are defined above the current constructor.
[although this does not stop problems with mutual constructor recursion between classes)]
It might be noticed that one of the constructors (single 'String' parameter) and the 'setName()' method both do essentially the same job -- assigning a parameter to the 'name' attribute. A better way of handling this type of code duplication is to have the constructor call the mutator. For example:
public Student (String name) { setName (name); }
The way objects and classes have been treated so far is as singletons, i.e. alone and not related to other classes. One of the main features in OO is inheritance. This is a mechanism whereby one class can extend another, that is, to use another class as a starting point and extend its functionality in some way. Ultimately this results in relationships between classes which, if drawn on paper, look like upside-down trees.
[unlike C++, Java does not permit multiple inheritance (and the associated C++ headache of 'virtual' classes), so the inheritance graphs are always trees (only one path from the root to each node).]
When one class 'A' extends another 'B', we say that 'A' is a specialisation of 'B' -- i.e. it takes 'B' and changes its behaviour in some specific way. Looking backwards, we would say that 'B' is a generalisation of 'A' -- i.e. it represents something more general than 'A'. When decomposing a problem for implementation, some thought needs to go into what classes might be needed with respect to inheritance. For example, in our library system, we are heading towards having two separate classes for students and staff:
public class Student { private String name; private String student_id; ... methods }
and:
public class Staff { private String name; private String staff_id; ... methods }
At first glance, this may appear satisfactory, but we will run into problems later on (hence needing to think about the design of the program a bit). For example, we may start to define a class to handle book loans:
public class LoanRecord { private Book the_book; private Date due_date; private Student student_borrower; private Staff staff_borrower; ... methods }
The problem here is that we will end up writing similar code to handle both staff and student book loans. For instance, to report the contents of a "LoanRecord" we could add the method:
public String toString () { if (student_borrower != null) { // student borrowing this book return ("book " + the_book.getTitle() + " borrowed by " + student_borrower.getName()); } else if (staff_borrower != null) { // staff borrowing this book return ("book " + the_book.getTitle() + " borrowed by " + staff_borrower.getName()); } // if we get here, nobody is borrowing the book! return ("book " + the_book.getTitle() + " has an invalid LoanRecord!"); }
The only significant difference between these 'if' branches is the use of the 'student_borrower' or 'staff_borrower' attribute. Even though there's not much code here, it's not particularly pleasant.
A better way forward is to have identified earlier that "Staff" and "Student" have something in common -- they are both people. More to the point, we would like to associate a "LoanRecord" with some person, regardless of whether they are "Student" or "Staff".
To this end, we define a new class called "Person":
public class Person { private String name; // constructor public Person () { setName (""); // use mutator to set name } // constructor public Person (String name) { setName (name); // use mutator to set name } // accessor public String getName () { return name; } // mutator public void setName (String newname) { name = newname; } // method for reporting public String toString () { return getName (); // use accessor to get name } }
At the moment, this class captures everything that we want to know about a general 'person' within the system. At this point, we can fix the "LoanRecord" class to deal with a "Person" object, instead of both "Student" and "Staff" objects:
public class LoanRecord { private Book the_book; private Date due_date; private Person borrower; // this line replaces staff/student public String toString () { if (borrower == null) { // nobody borrowing this book! return ("book " + the_book.getTitle() + " has an invalid LoanRecord!"); } return ("book " + the_book.getTitle() + " borrowed by " + borrower); } ... other methods }
Compared with the previous code for the 'toString()' method, this is much nicer. Furthermore, the "LoanRecord" class no longer depends directly on either the "Staff" or "Student" classes (in theory we can change these without affecting 'LoanRecord').
New versions of "Staff" and "Student" need to be defined, such that they extend (or specialise) the functionality of "Person". These are:
public class Student extends Person { private String student_id; // constructor public Student () { student_id = ""; } // accessor public String getStudentID () { return student_id; } // mutator public void setStudentID (String id) { student_id = id; } }
and:
public class Staff extends Person { private String staff_id; // constructor public Student () { staff_id = ""; } // accessor public String getStaffID () { return staff_id; } // mutator public void setStaffID (String id) { staff_id = id; } }
These classes mostly just provide ID handling, but inherit the properties of the "Person" class. This includes all public methods, so although the "Student" class doesn't explicitly have a 'setName()' method, it inherits that from the "Person" class. The other methods inherited in this case are 'getName()' and 'toString()'. Constructors are not inherited. In use:
Student x = new Student (); x.setName ("Joe Bloggs"); System.out.println ("The student is called: " + x);
As before, we might like to be able to construct a new "Student" object passing the name as a parameter. A first attempt could be:
public Student (String name) { this (); // call default constructor to initialise ID setName (name); // use inherited mutator to set name }
The code here that sets the student's name is doing effectively the same job as the "Person" constructor (the one that takes a single 'String' parameter). To reuse this constructor, we can invoke it using the "super" keyword (meaning superclass -- or parent):
public Student (String name) { this (); // call default constructor to initialise ID super (name); // use superclass (Person) constructor to set name }
In addition to inheritance from other classes, a class may implement an interface. An interface is similar to a class definition, except that it contains no attributes or code. Instead it provides a way of specifying what methods a class provides, and can be used as an object type (referencing any object which implements the named interface).
One place where an interface could be useful is as follows: both the "Student" and "Staff" classes contain some kind of ID. However, those IDs are specific enough to the individual "Staff" and "Student" classes that we wouldn't want to create a completely separate "PersonID" style class. Instead we can specify, by means of an interface, that both the "Staff" and "Student" classes have to provide some methods for getting and setting the ID -- without the interface needing to know details about the particular IDs. Such an interface, "UniversityID", could be defined as:
public interface UniversityID { public String getID (); public void setID (String id); }
Whatever classes implement this method must provide methods called 'getID' and 'setID' that match the method signatures given. For example, we could modify the "Student" class as follows:
public class Student extends Person implements UniversityID { ... existing attributes and methods // accessor for UniversityID interface public String getID () { return getStudentID(); } // mutator for UniversityID interface public void setID (String id) { setStudentID (id); } }
Interfaces such as this might be useful for future extensions, e.g. linkage with the central database:
public class UniDatabaseIF { ... attributes to manage database connection public String findEmailAddress (UniversityID id) { ... method body } }
The point here is that the 'findEmailAddress' method takes as a parameter something that implements the "UniversityID" interface, but doesn't need to know whether that ID is associated with a "Student", "Staff" or something else. The code here can extract the ID by calling the 'getID()' method (and change the ID with 'setID()' if it really wanted to).
There are just two pieces of the interitance puzzle left: abstract classes and overriding. In the code that we've seen so far, the "Staff" and "Student" classes implement the "UniversityID" interface, but the "Person" class does not. This means, for instance, that we cannot easily extract a "UniversityID" from a "Person", although we know (in most cases) that everything which extends "Person" does provide this interface.
The obvious solution here would be to have the "Person" class implement the "UniversityID" interface, instead of "Student" and "Staff" implementing them. If we go down this route, we end up having to add 'setID' and 'getID' methods to "Person":
public class Person implements UniversityID { ... existing attributes and methods // accessor for UniversityID interface public String getID () { return "Invalid-ID!"; } // mutator for UniversityID interface public void setID (String id) { System.error.println ("Erm, can't set ID here.. was: " + id); } }
At this point, we have a "Person" class that compiles, but doesn't do anything useful in its 'getID()' and 'setID()' methods. To have a functional 'getID()' or 'setID()' requires that functionality to be overridden in the particular "Student" or "Staff" class. This doesn't require any changes in the "Staff" or "Student" classes themselves, as they already contain perfectly good 'getID()' and 'setID()' methods. To show how this works, we can modify the 'toString()' method in "Person" to include the ID:
// method for reporting public String toString () { return (getName() + " (" + getID() + ")"); }
At first glance, and given the contents of 'Person's own 'getID()' method, it looks like this would return a string containing "Invalid-ID!". However, if the "Person" involved is really a "Student" or "Staff", then the 'getID()' method in those classes will be called -- returning a sensible ID. The 'getID' in the "Person" class has been overridden by 'getID' in the "Student" and "Staff" classes. Additionally, since "Staff" and "Student" automatically implement the "UniversityID" interface (because they extend "Person", which does implement that interface), we no longer need to explicity say that when defining these, e.g.:
public class Student extends Person { ... attributes and methods }
This mechanism, whereby methods defined in one class replace those defined in an ancestor class (one which it inherits from), is known as overriding. In the same way that "super" can be used to access a constructor in the parent class, it can also be used to access overridden methods in the parent class. For example, if an object of type "Staff" has no 'staff_id' set (i.e. that "String" object is 'null'), we might want it to return whatever is provided by the "Person" parent class. This can be done as follows:
// overridden accesor public String getID () { if (staff_id == null) { return super.getID (); } return getStaffID (); }
The above shows an implementation where we have the "Person" class explicitly implement the "UniversityID" interface -- which includes providing fairly silly 'getID' and 'setID' methods. An alternative is to define "Person" as an abstract class. If this route is taken, the "Person" class no longer needs to provide 'getID' or 'setID' methods. The definition is only a slight modification from the original version:
public abstract class Person implements UniversityID { ... attributes and methods, without getID() or setID() }
Because the "Person" class no longer contains an implementation for 'getID' (nor 'setID'), the currently overriding 'setID' method may attempt to call a non-existant method -- from the "super.getID()" call (because the superclass, "Person", no longer has a 'getID'). If this happens, an exception is thrown at the point of the call and the program aborts, e.g.:
raptor$ java Library loan record 1: book Perl in a nutshell, Siever et al., ISBN: 1234 borrowed by Dave Reeve (960002) loan record 2: book On single combat, Kernspecht, ISBN: 5678 borrowed by Fred Barnes (960001) Exception in thread "main" java.lang.AbstractMethodError: Person.getID()Ljava/lang/String; at Staff.getID(Staff.java:58) at Person.toString(Person.java:56) at java.lang.String.valueOf(String.java:2577) at java.lang.StringBuilder.append(StringBuilder.java:116) at LoanRecord.toString(LoanRecord.java:46) at java.lang.String.valueOf(String.java:2577) at java.lang.StringBuilder.append(StringBuilder.java:116) at Library.main(Library.java:33) raptor$
This is easily solved by having the 'getID' method in staff handle the no-ID case differently:
// overridden accesor public String getID () { if (staff_id == null) { return "no-ID!" } return getStaffID (); }
Here are the various 'library' demonstrators, as zip-files containing BlueJ projects:
Last modified 19th October 2006 |