So far we have implemented two simple classes with few methods and few instance variables, but this doesn't really show the benefits of using object oriented programming. Where OOP really pays off is its ability to structure our code in way which allows us to reuse existing code.
Remember the Counter class from the previous lecture. We will incrementally develop a new kind of counters which in addition to being able to count objects are also uniquely identified with a serial number. Most hardware producers put a stamp on the gadgets that they produce in order to identify them later if for example some defect is found. Our software model will have the same property. By doing this example we will demonstrate some more aspects of OOP.
We don't want to copy the code from Counter class since this leads to duplicated code and at the end this makes the code more difficult to maintain. The solution that OOP provides is to define classes which are extensions of other classes. The new class will have the same functionality as the previous one but in addition it will provide some extra features. This is how we define an extension:
public class SerialCounter extends Counter { }
This will declare a new class SerialCounter which will automatically inherit the definitions of methods increment(), reset() and getValue() from the counter class. We specify it with the keyword extend and the name of the parent class Counter. The standard terminology in OOP is that SerialCounter inherits Counter, or in the other direction we say that Counter is the parent class of SerialCounter.
The new class still does not add anything new but we are free to declare new methods and instance variables. This is our first attempt to implement counters with serial numbers:
public class SerialCounter extends Counter { private String serNo; public SerialCounter() { serNo = ""; } public String getSerialNumber() { return serNo; } public String setSerialNumber(String no) { serNo = no; } }
The serial number is just a string which is stored in a new instance variable. We can verify that the methods from the old Counter class still work as usual but in addition we are allowed to do more, i.e. we can do something like this:
SerialCounter counter = new SerialCounter(); counter.setSerialNumber("CNT123"); counter.increment(); counter.increment(); System.out.println(counter.getSerialNumber()+": "+counter.getValue());
The serial counters are also usable in all contexts where a simple counter is required. For instance we can assign a reference to SerialCounter to a variable of type Counter:
SerialCounter serial_counter = new SerialCounter(); Counter counter = serial_counter; counter.increment();
The code above will be accepted and it will work normally.
We can also do the opposite assignment, i.e. we can assign a reference to Counter to a variable of type SerialCounter but this works only if the reference is actually a SerialCounter. Since the conversion is not always possible the casting is not automatic. Instead we have to write it like this:
SerialCounter serial_counter = (SerialCounter) counter;
If the reference in counter is not actually a serial counter, then the above operation will generate a runtime error.
The new kind of counters share the functionality with the simple counters but this is still not a very faithful model of the real world counters. The problem is that in the real world the producer assigns serial numbers which are unique and they cannot be changed once the gadget has been produced. In our model this doesn't hold since we can call the setSerialNumber method with any value and as many times as we want. This means that the numbers are neither unique nor constant. In order to model this kind of behaviour we need to use another OOP feature. Look at the new implementation:
public class SerialCounter extends Counter { private String serNo; private static int lastSerNo = 0; public SerialCounter() { lastSerNo = lastSerNo + 1; serNo = "CNT" + lastSerNo; } public String getSerialNumber() { return serNo; } }
The setSerialNumber method is now removed. Instead the constructor sets the serial number and after that it is never changed. We can only read by calling getSerialNumber.
How to we guarantee that the serial numbers are unique?
There is one more variable in the class called lastSerNo.
Unlike serNo it is declared with the keyword static.
This means that the variable is shared between all objects of
the same class. The following picture should make it clearer:
There are two different different objects of class SerialCounter and each object has its own copies of instance variables like count and serNo. When we declare some variable as static then instead we store it a single place and it is shared between all objects. Each time when we create an object we increment the variable lastSerNo and then we use its current value to assign a serial number to the new object. In this way the objects will get serial numbers such as CNT1, CNT2, etc.
Our intention is that once we assign a serial number to the counter, it should never change anymore. There is one keyword that let us to make our intention of having immutable instance variables more explicit. We can redefine serNo in this way:
private final String serNo;
The keyword 'final' tells the compiler to reject any code that attempts to change the instance variable after it has been initialized in the constructor. This means that even in principle we are not allowed to add methods like setSerialNumber.
When final is used with instance variables then it just makes our intention more explicit and prevents us from changing the value accidentially. It is a lot more often used in combination with the keyword static since this let us to define constants in Java. For example instead of having the serial number prefix "CNT" burried in the code it is a lot cleaner to define it as a constant inside the SerialCounter class:
public static final String SERIAL_NUMBER_PREFIX = "CNT";
The constants are exactly this: values which are shared among all objects and are immutable. The combination static final gives us the right semantics.
Inside a method we can access both instance variables, static variables and methods defined in the same class by just citing the variable or method name. What happens then is that we manipulate the variables of one particular instance which is implicit in the context. If we want to access a variable or a method from another instance then we must explictly point to it. For instance if we call:
counter.increment()
then the method increment will be executed in the context of the instance referenced from the variable counter. How does the method know which instance it must deal with? Each method can have one or more explict arguments which are the input to the method. However, there is also one implicit argument called this which is a reference to the instance which is the context for all instance variables. We can also choose to make this explicit. For example, we may choose to reimplement the increment method as follows:
public void increment() { this.count = this.count + 1; }
This is an equivalent but just more verbose definition of the same method. In fact this is pretty silly use of the keyword this.
There are, however, two quite frequent uses of this. Imagine that for some reason we again want to make it possible to create a counter with a specific serial number rather than the automatically generated one. We can naively provide a constructor like:
public SerialCounter(String serNo) { serNo = serNo; }
But there is a problem, now the name serNo can refer both to the argument of the constructor and to the instance variable. By default the Java compiler will resolve serNo as a reference to the argument. If we want to access the instance variable as well then we have to either rename the argument, or we can access the instance variable explicitly as this.serNo:
public SerialCounter(String serNo) { this.serNo = serNo; }
It is a convention that when a constructor takes as arguments the initial values of some instance variables, then the arguments should have the same names as the names of the instance variables. After that the initialization itself should be done as above.
The other common use of the keyword this is when a method wants to pass a reference to its object to a method of another object. Consider that we want to define the following slightly contrived method to a class:
public void printMe() { System.out.println(this); }
Here the method println will execute in the context of System.out but as an argument it will get a reference to the object which is the context of printMe.
The concept of context also makes clearer the notion of static variables. While the instance variables are defined in the object referenced with this, the static variables are global. Their only connection with the class in which they are defined is that the class defines the visibility of the variable. For instance a private variable is visible only to the methods defined in the same class. A public variable is visible to other classes as well but then it must be accessed by citing both the name of the class and the name of the variable. For example, it is possible from any class to access the constant SERIAL_NUMBER_PREFIX, but you must write it as:
SerialCounter.SERIAL_NUMBER_PREFIX
The reason is that it completely possible that there are variables with the same names defined in different classes. In order to disambiguate we must use the class name as well.
Just like static variables, we could also have static methods. The different between (instance) method and static methods is that the later does not have the implicit argument this. This means that they cannot access instance variables because they do not execute in the context of any object. They could, however, access static variables and other static methods.
Speaking about object references we must also mention the special reference null. Most of the work in a object oriented program is to manipulate objects and to send messages between them. It is often useful to indicate that a variable is not associated with any object yet. This is exactly the use for the keyword null. Any variable of object type could be initialized with null, for example:
Counter c = null;
Will declare the variable c but it will not associate it with any object, on the contrary by assigning null we indicate that it points to nowhere. Any attempt to call a method or access an instance variable from a variable with null value will lead to a runtime error.
The static methods in Java play the role of what is called functions, procedures or subroutines in other languages. In other words this are pure computations which are not associated with any objects. In the Java standard libraries there are several classes whose purpose is solely to be containers for commonly used functions and constants. A typical example is the class Math. This is a class which provides several standard mathematical functions such as sin, cos, exp, etc. Since this are static methods we must call them from the Math class. For instance we can compute sine of 2π as follows:
double x = Math.sin(2*Math.PI)
In addition to the traditional mathematical functions there are also other small utilities useful for programming. For instance in Lab 2 we are going to use the function random which generates a new random number from 0 to 1 each time when it is called. Try something like this:
double x = Math.random()
If you need a random number in another range, let's say from a to b then you need a simple arithmetic:
double x = a + (b-a)*Math.random();
We said above that a counter can be casted to a serial counter by using the construction:
SerialCounter serial_counter = (SerialCounter) counter;
This is actually an example of the more general concept of type casting. In Java each variable has a type but some types are more general or more specific than others. For example an integer is a special kind of real number. If we write the following two lines:
int x = 3; double y = x;
then they will be perfectly fine for the compiler since the integer in x can be safely converted to a real number without loss of precision. The converse however:
double y = 3.14; int x = y;
will not be permitted since the number 3.14 cannot be made into integer without rounding it to 3, i.e. we lose precision. For that reason the compiler will not do the conversion unless if it is explicitly asked to do so:
double y = 3.14; int x = (int) y;
You see the same pattern as with the case of casting one class to the other. We use both (int) and (SerialCounter) as operators which enforce conversions from one type to another for which the compiler cannot determine automatically that those are safe.
This can be used for instance to generate a random integer in a given range. Lets say that you need an integer from 0 to 10. You can get it by doing:
int x = (int) 10*Math.random();
Although we haven't mentioned it so far, we have seen arrays already. Remember the definition of the 'main' method:
public static void main(String[] args) { }
So far, we have intentionally been silent about the parameter args since this is a variable of type array. When we run an application, the operating system lets us to pass one or more parameters to it. For example, if we have the class Hello, then we can run it like this:
java Hello John Mary Jim
The first part, i.e. java Hello, is something that we have seen already; it just runs the Java interpreter and tells it to execute the class Hello. The new thing is that everything that we write after the class name is parsed as a sequence of words and passed as arguments to the main method in the class. Every word will be passed as a different element in the array args. We can test this straight away:
public class Hello { public static void main(String[] args) { System.out.println("Hello "+args[0]); System.out.println("Hello "+args[1]); System.out.println("Hello "+args[2]); } }
If you run this code with the command line from above, you will see that the program prints the names 'John', 'Mary' and 'Jim' each on a new line.
In general you can define an array by adding square brackets after the type for its elements. In the following, the variables 'a', 'b' and 'c' are arrays with elements of type integer, string and double:
int[] a; String[] b; double[] c;
In order to access an element of the array, we need to use the variable name followed by a number in square brackets. The number tells which element from the array you want to access. Each element is uniquely identified by its position. When you run the Hello class, its main method will get as parameter an array filled in this way:
element | John | Mary | Jim |
index | 0 | 1 | 2 |
Note that the indices start from zero. If we want to access the first element, then we must use index zero. If we want the third element, then we need index two.
The example from above already shows one thing that you cannot do without arrays. When you write your program you cannot know how many arguments the user is going to supply. Even if you know it, the compiler does not know your intentions and the interpreter cannot check it. Instead the interpreter collects all arguments that are available, and packs them in an array which is after that given to you. It is your responsibility to verify whether the user has typed something that you can interpret.
The first thing that you need to know is how many parameters there are. Every array has a public final variable called 'length' which tells you its size. The example above presupposes that there are exactly three arguments and it will crash if there are less. If you supply more, then the rest will be ignorred. Try to call it like this for instance:
java Hello John
it will crash with the error:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 1 at Hello.main(Hello.java:4)
Any attempt to access an element with index which doesn't exist will lead to a similar error.
The arrays are usually used in loops. In our example the user can supply many names, why should we restrict ourself to only three? We can iterate over all names and say hello to all of them:
public class Hello { public static void main(String[] args) { for (int i = 0; i < args.length; i++) { String arg = args[i]; System.out.println("Hello "+arg); } } }
There should not be anything surprising here. There is a loop which iterates over all arguments and prints hello for each of them. The only thing that deserves a comment here is the termination condition for the loop. Note that it iterates until the index 'i' is smaller than the length of the array, i.e. the last element has index equal to the length minus one. If we had the condition i <= args.length, then the program will try to access the element args[args.length] which does not exist since the indices for the array start from zero. A sequential iteration over all elements of an array is a very common pattern. Java has a more convenient syntax for this purpose:
public class Hello { public static void main(String[] args) { for (String arg : args) { System.out.println("Hello "+arg); } } }
This is a special form of the 'for' loop which lets you to iterate over an array without thinking about the indices and the lengths. The variable that we introduce in the loop will be assigned directly to the value of the element and not to its index. This saves you one more line of code. The disadvantage of this kind of loop is that now you don't know the current index for every iteration.
So far we have only used an existing array but of course we must be able to create our own. The arrays are just objects and they can be created with the operator new. For example, if I need an array with 10 integers, then I can create it in the following way:
int[] a = new int[10];
Note that when we declare the type of the variable for the array, we never specify its length. We must specify the length when we create the array, however. In this case the length of the array is a constant but it does not have to be. It could be any expression that is computed at runtime. After we create the array, its elements will be automatically initialized to a default value. If the elements are of integer or floating number type, then the default value is zero. If the elements are booleans, then the default is 'false', and if this are objects then the default is 'null'.
Sometimes we know in advance both the length of the array and its the values that we want to assign. In this case we can initialize it in a simpler way:
String[] alphabet = {"alpha", "beta", "gamma"};
The number of expressions between the curly braces determine the length of the array and the values for the elements of the array will be set accordingly. Of course the other way to set the values in the array is to use assignments:
alphabet[0] = "alpha"; alphabet[1] = "beta"; alphabet[2] = "gamma";
For doing mathematical calculations we sometimes need arrays with two and more dimensions. Unfortunately more than one dimension is not directly supported in Java. Instead you can use an array containing other arrays. You can create a dwo dimensional matrix in this way:
double[][] matrix = new double[3][3];
but this will actually create one dimensional array of length 3, whose elements are other arrays of length three containing the real numbers. We can verify this by trying to compile the code:
double[][] matrix = new double[3][3]; matrix[0] = null;
The expression matrix[0] refers to the nested array representing the first row from the matrix. Since this is just an object reference, the compiler let us to set it to zero. This will essentially destroy the illusion that we have a two dimensional array.
Despite that Java doesn't have real two dimensional arrays, the replacement serves the same purpose. We can access the elements of the matrix by a sequence of two indices, i.e. matrix[0][0] refers to the upper left corner and matrix[0][2] is the upper right corner.