Algorithms and Data Structures in an Object-Oriented Framework (“ADSOOF”)

Inheritance in Java: extended drinks machines

Extending Java classes

An important aspect of object-oriented programming languages like Java is inheritance. This has two important effects. Firstly, it enables you to define a class of objects in terms of how it differs from some other class of objects, either by adding new capabilities or changing existing capabilities. Secondly, it enables you to write general code which works for a range of classes so long as those classes share the capabilities required by the code.

In a previous set of notes, we looked at code which implemented objects of a class DrinksMachine. This was intended to simulate a type of machine with buttons for taking money, dispensing two different varieties of cans of drink (“coke” and “fanta”), and returning change. Suppose we want to simulate another type of drinks machine which is like the previous one, but which has a button for dispensing a third variety of drink (“sprite”) as well as the two varieties of the previous machine. We can do this through inheritance. Here is the code for it:

class ExtDrinksMachine1 extends DrinksMachine
{
 private ArrayList<Can> sprites;

 public ExtDrinksMachine1(int p)
 {
  super(p);
  sprites = new ArrayList<Can>();
 }

 public ExtDrinksMachine1(int p, int c, int f, int s)
 {
  super(p,c,f);
  sprites = new ArrayList<Can>();
  for(int i=0; i<s; i++)
     loadSprite(new Can("sprite"));
 }

 public Can pressSprite()
 {
  if(sprites.size()>0&&balance>=price)
     {
      Can can = sprites.get(0);
      sprites.remove(0);
      balance=balance-price;
      cash=cash+price;
      return can;
     }
  else
     return null;
 }

 public void loadSprite(Can can)
 {
  sprites.add(can);
 }

 public boolean spritesEmpty()
 {
  return sprites.size()==0;
 }

}
This defines objects of a class called ExtDrinksMachine1. The words extends DrinksMachine in the header to this class indicate that this class is an extension by inheritance of the class DrinksMachine. Please note that the return of null when there are no sprite cans left is NOT an example you should follow when writing your own code, for the reasons noted here. It would be much better to handle this by throwing an exception, as in the modified versions of DrinksMachine you were given here. Handling it by returning null was done only so that those who had not properly understood the idea of exceptions would be able to get on with the exercise on using objects without getting stuck due to this issue. It is only kept on here in order to maintain uniformity with previous code. The technique shown here should also be used in method pressSprite to give better quality code.

On an object of class ExtDrinksMachine1 you can call all the methods for the class DrinksMachine plus the additional methods listed in the code above. So the methods you can call coming from DrinksMachine are:

insert(int)
getBalance()
collectCash()
getPrice()
pressChange()
pressCoke()
pressFanta()
loadCoke(Can)
loadFanta(Can)
cokesEmpty()
fantasEmpty()
setPrice(int)
Where methods take arguments, the type of the argument is given in this listing. You do not have to give code for these methods in the class ExtDrinksMachine1 because a call on them on an object of this class will automatically use the code from the class DrinksMachine due to the extends DrinksMachine in the header. The methods listed in the code for ExtDrinksMachine1 are only those which are extra to the ones given in DrinksMachine and they are:
pressSprite()
loadSprite(Can)
spritesEmpty()
An object of type ExtDrinksMachine1 has within it all the variables which objects of type DrinksMachine have within them and which are listed in the code for that class, integers price, balance and cash and two arrayLists of Can objects, cokes and fantas. It also has within it an extra variable which is an arrayLists of Can objects, called sprites. This is indicated by the declaration in the code for ExtDrinksMachine1:
private ArrayList<Can> sprites;
Note that in order for the methods given in class ExtDrinksMachine1 to refer to the variables price, balance and cash those variables have to be declared as protected rather than private in the class DrinksMachine. A variable which is declared in a class as private exists in objects of a class which extends it, but cannot be accessed in the code which extends it.

You can see that the extra methods in the class ExtDrinksMachine1 manipulate the arrayList called sprites in the same way that the related methods in the class DrinksMachine manipulate the variables cokes and fantas. The constructors for ExtDrinksMachine1 are

 public ExtDrinksMachine1(int p)
 {
  super(p);
  sprites = new ArrayList<Can>();
 }
and
 public ExtDrinksMachine1(int p,int c, int f,int s)
 {
  super(p,c,f);
  sprites = new ArrayList<Can>();
  for(int i=0; i<s; i++)
     loadSprite(new Can("sprite"));
 }
The first sets up an extended drinks machine which contains no cans. Its only argument is the initial price of drinks for the machine. The second constructor sets up an extended drinks machine which has cans within it, its four arguments are the initial price, and the initial number of cans of coke, fanta and sprite. Note that if the first statement in a constructor consists of the word super followed by some arguments enclosed within rounded brackets the result is to use the code of the constructor of the class which the class extends to fill values of the variables in the new object created by the constructor. So in the first constructor for ExtDrinksMachine1, the statement super(p) calls the code in the constructor for DrinksMachine which has one argument of type int. This is:
 public DrinksMachine(int p)
 {
  price = p;
  balance = 0;
  cash = 0;
  cokes = new ArrayList<Can>();
  fantas = new ArrayList<Can>();
 }
so the result is to set the new object's price variable to the value p, its balance and cash variables to 0 and its cokes and fantas variables to new arrayLists of Cans of initial size 0. The additional code in the first constructor for ExtDrinksMachine1 initialises the variable sprites in the new object being created to a new arrayList of Cans of initial size 0.

In the second constructor for ExtDrinksMachine1, the call super(p,c,f) uses the code from the constructor for DrinksMachine which takes three arguments of type int to set the value of the price, balance and cash variables, and to create c new Can objects to put in the cokes arrayList and f new Can objects to put in the fantas arrayList. After executing this code from the constructor of DrinksMachine, this constructor of ExtDrinksMachine1 sets the variable sprites to a new empty arrayList of Cans, and then adds s new Can objects to it.

Classes and subclasses

Up to this point you might think that every Java object has its own distinct type. We have seen that a class defines objects, it gives the methods that can be called on them, it gives the variables that are inside objects of that class, it gives the code for the methods which manipulates those variables. Every Java class name is also a type name, variables (including method parameters) can be declared as of that type, and a variable of that type can be set to refer to an object of that class. Because a variable has a type, we can write code where a method call is attached to a variable, and the Java compiler makes sure the method is one that is in the class which matches the type of the variable. We cannot attach a method to a variable if the method is not in the class of that variable, it will be noted as a compiler error. In this way, the information given by the types to the Java compiler means the compiler does some checking to see if our code makes sense. Java's type checking prevents us from making a method call which is asking for an operation on an object which is not defined for that object.

Actually the situation is more complex than the situation as described so far, where a type in Java (unless it is one of the primitive types) is the same thing as a class. A Java object has the type of its class, but it may have more than one type, the type given by its class is just one of its types. Part of the reason for this is that Java has the concept of “subclass”. This is what we are dealing with in this section on inheritance. Next, when we look at interface types, we will see another way in which a type in Java is a more general concept than a class.

Every object of a type which extends some other type can be considered an object of both the original type and the extended type. At this point, you can still think of an object type and a class as the same thing. In the example we have been discussing, every object of the class or type ExtDrinksMachine1 is also of type DrinksMachine. The consequence of this is that a variable of type DrinksMachine can be made to refer to an object of type ExtDrinksMachine1. Also a method which has a parameter of type DrinksMachine can take an object of type ExtDrinksMachine1 as its matching argument. We say that ExtDrinksMachine1 is a subclass of DrinksMachine, or that DrinksMachine is a superclass of ExtDrinksMachine1.

As an example, we saw previously a static method called cheaper which took two DrinksMachine objects as its arguments and returned a reference to whichever was the cheapest. Here is its code:

 public static DrinksMachine cheaper(DrinksMachine m1,DrinksMachine m2)
 {
  if(m1.getPrice()<m2.getPrice())
     return m1;
  else
     return m2;
 }
This will still work even if one or both of its arguments are of type ExtDrinksMachine1. For example, if we have the code fragment:
 DrinksMachine mach1 = new DrinksMachine(m);
 ExtDrinksMachine1 mach2 = new ExtDrinksMachine1(n);
 DrinksMachine mach3 = cheaper(mach1,mach2);
then mach3 will be an alias of either mach1 or mach2 depending on which of m or n is the lower.

When an object is referred to through a variable, we have the concept of its actual type and its apparent type. This is because a variable which is declared of one type may refer to an object which was created through a constructor of a subclass type. So, after the above code fragment is executed, if n is less than m the variable mach3 refers to the object created by the call new ExtDrinksMachine1(n). The “apparent type” is the type the variable is declared as, but the actual type of the object is the name that was used to construct it. Here the apparent type would be DrinksMachine and the actual type would be ExtDrinksMachine1.

We actually saw a little of this code inheritance previously when we noted that the method equals could be called on every object, even if we did not put the method equals into the class of the object. The reason for this is that in Java every class is a subclass of Java's built-in class called Object and the method equals is defined in class Object. Any class which is not declared as a subclass of another class using extends is automatically made a subclass of Object. So any class which is a subclass of another class also inherits equals (and a few other methods that are in class Object) because it inherits everything that its superclass has. For this reason, Object is described as “the most general class”.

Note that if a method call is attached to a variable, it must be a method which is appropriate for the apparent type of that variable. So, following the execution of the code fragment above, we could have

 mach3.pressCoke();
but not
 mach3.pressSprite();
since the variable mach3 is of type DrinksMachine and the method pressCoke is declared in DrinksMachine but the method pressSprite is only declared in its subclass ExtDrinksMachine1. When the code is compiled, the Java compiler cannot tell whether mach3 would refer to an object of actual type ExtDrinksMachine1. Remember that although every object of type ExtDrinksMachine1 is also of type DrinksMachine, it is not the case that every object of type DrinksMachine is also of type ExtDrinksMachine1.

If you have a reference to an object through a variable of one type, and you know the actual type of that object is some particular subtype, you can treat it as an object of that subtype by using casting. This is done by preceding the reference to the object by the name of the subtype enclosed within rounded brackets. For example, if we have variable mach4 declared of type ExtDrinksMachine1:

ExtDrinksMachine1 mach4;
then we can have:
mach4 = (ExtDrinksMachine1) mach3;
If this were executed when mach3 referred to an object whose actual type was not ExtDrinksMachine1 an exception of type ClassCastException would be thrown. Note that assignment the other way round:
mach3 = mach4;
does not require casting as it is always possible to assign an object of a subclass to a variable of its superclass.

If you want to test whether a variable of one type refers to an object whose actual type is a subclass, Java has the keyword instanceof to do that. The test r instanceof t, where r is a reference to an object and t is a type name, evaluates to true if r has the type t (or a type which is a subclass of t) and to false otherwise. Here is an example of a code fragment which uses that:

  Can c;
  if(mach3 instanceof ExtDrinksMachine1)
     c = ((ExtDrinksMachine1) mach3).pressSprite();
  else
     c = mach3.pressFanta();
which has the effect of setting c to the result of pressing the “sprite” button of the machine referred to by mach3 if it has a “sprite” button, otherwise setting it to the result of pressing the “fanta” button. Note that ((ExtDrinksMachine1) mach3).pressSprite() combines casting the variable mach3 to the type ExtDrinksMachine1 and then calling a method from that subclass without the need for an extra variable of that subclass. It has the effect of viewing the object referred to by mach3 as of type ExtDrinksMachine1 and then calling the method pressSprite() on the result. It is necessary to have both sets of brackets to do this since the method attachment operator has higher precedence than the casting operator. This means that just (ExtDrinksMachine1) mach3.pressSprite() would be interpreted as (ExtDrinksMachine1) (mach3.pressSprite()), that is an attempt to call pressSprite() on mach3 and then view the result as of type ExtDrinksMachine1 (which does not make sense).

Here is some code you can use to test ExtDrinksMachine1 objects:

import java.util.Scanner;

class UseDrinksMachines5
{
 public static void main(String[] args)
 {
  int p,c,f,s;
  Scanner input = new Scanner(System.in);
  System.out.println("Machine 1 is a standard machine");
  System.out.print("Enter the price for drinks on machine 1: ");
  p = input.nextInt();
  System.out.print("Enter the number of cokes in machine 1: ");
  c = input.nextInt();
  System.out.print("Enter the number of fantas in machine 1: ");
  f = input.nextInt();
  DrinksMachine mach1 = new DrinksMachine(p,c,f);
  System.out.println("Machine 2 is an extended machine");
  System.out.print("Enter the price for drinks on machine 2: ");
  p = input.nextInt();
  System.out.print("Enter the number of cokes in machine 2: ");
  c = input.nextInt();
  System.out.print("Enter the number of fantas in machine 2: ");
  f = input.nextInt();
  System.out.print("Enter the number of sprites in machine 2: ");
  s = input.nextInt();
  ExtDrinksMachine1 mach2 = new ExtDrinksMachine1(p,c,f,s);
  DrinksMachine cheaper = DrinksMachineOps.cheaper(mach1,mach2);
  System.out.print("Enter the amount of money you wish to spend on cokes "+
                   "on the cheaper machine: ");
  int amount = input.nextInt();
  int cokes = DrinksMachineOps.spendOnCokes(amount,cheaper);
  amount = cheaper.pressChange();
  System.out.println("You have "+cokes+" cokes and "+amount+"p change");
  mach2.insert(amount);
  System.out.println("Put the change in Machine 2 and press the Sprite button");
  Can can = mach2.pressSprite();
  amount = mach2.pressChange();
  if(can==null)
     System.out.println("No sprites available");
  else
     System.out.println("You have a "+can+" and "+amount+"p change");
 }

}
This code assumes the static method cheaper and a static method spendOnCokes are in a class called DrinksMachinesOps. We used the method spendOnCokes previously for illustration. Here is some code for it:
 public static int spendOnCokes(int sum,DrinksMachine mach)
 {
  int count=0;
  mach.insert(sum);
  while(!mach.cokesEmpty()&&mach.getBalance()>=mach.getPrice())
     {
      mach.pressCoke();
      count++;
     }
  return count;
 }
The idea of this method is to simulate putting a certain amount of money in a machine then keep pressing the “coke” button until either the money runs out, or the machine runs out of cokes. The code in the file UseDrinksMachine5.java performs this operation on the cheaper machine, so it is only when the code is run that it is determined whether the method spendOnCokes is executed on an object of type ExtDrinksMachine1 or an object which is just of type DrinksMachine. Then any remaining money is put into the machine with the “sprite” button, that button is pressed and the “change” button on that machine is pressed.

You can access the files this example needs from the code index for this section. The files required are DrinksMachine.java, Can.java, EmptyCanException.java, ExtDrinksMachine1.java, DrinksMachineOps.java and UseDrinksMachines5.java.

Changing operation effects

As well as adding new operations to an existing class, inheritance in Java can also be used to make a subclass where the effect of operations is different from that in the superclass. The way this is done is to write a method which has the same header as a method in the superclass, but different code in its body. The effect then is that if a method is called on an object of the subclass, the code in that subclass will be used to execute that method rather than the code in the superclass. This is known as overriding the method.

To demonstrate this, we will consider another extension of the original DrinksMachine class. We will name this ExtDrinksMachine2. This represents a drinks machine which looks just like the original drinks machine. However, this drinks machine has a link to a supplier company. When it runs out of cokes or fantas, it sends a message to the supplier company informing it of this. Here is the code:

class ExtDrinksMachine2 extends DrinksMachine
{
 private DrinksCompany supplier;
 private String identity;

 public ExtDrinksMachine2(DrinksCompany link,String id,int p)
 {
  super(p);
  identity = id;
  supplier = link;
 }

 public ExtDrinksMachine2(DrinksCompany link,String id,int p,int c, int f)
 {
  super(p,c,f);
  identity = id;
  supplier = link;
 }

 public Can pressCoke()
 {
  if(cokes.size()>0&&balance>=price)
     {
      Can can = cokes.get(0);
      cokes.remove(0);
      if(cokes.size()==0)
         supplier.cokesEmpty(this);
      balance=balance-price;
      cash=cash+price;
      return can;
     }
  else
     return null;
 }

 public Can pressFanta()
 {
  if(fantas.size()>0&&balance>=price)
     {
      Can can = fantas.get(0);
      fantas.remove(0);
      if(fantas.size()==0)
         supplier.fantasEmpty(this);
      balance=balance-price;
      cash=cash+price;
      return can;
     }
  else
     return null;
 }

 public String getIdentity()
 {
  return identity;
 }

}
Objects of type ExtDrinksMachine2 have the variables and methods of objects of type DrinksMachine. This is given by the extends DrinksMachine bit, there is no need for any more. However, alternative code is given for the methods pressCoke and pressFanta. Also there are two extra variables in an object of type ExtDrinksMachine2 on top of the ones inherited from DrinksMachine, these extra variables are identity of type String and supplier of type DrinksCompany. There is one extra method, getIdentity which returns the value of the variable identity. The constructors for ExtDrinksMachine2 have arguments which are used to set the identity and supplier variables.

The code for the method pressCoke in the class ExtDrinksMachine2 is like the code for pressCoke in DrinksMachine with the addition that if after removing an item from the arrayList cokes the size of this arrayList falls to 0, the method cokesEmpty with argument this is called on the DrinksCompany object referenced by the supplier variable. Similar applies to the code for the method pressFantas. Note this use of the word this to mean “the object this method is called on”. For example, if the call mach2.pressCoke() is made, and the object referred to by mach2 is an object of type ExtDrinksMachine2 with the cokes variable referring to an arrayList of length 1, then the call to supplier.cokesEmpty(this) which will result will in effect be a call supplier.cokesEmpty(mach2). The method sends a reference to the whole object to the object referred to by the ExtDrinksMachine2 object's supplier variable.

As we saw previously when discussing the ExtDrinksMachine1 type, a variable of a particular class may reference an object of a subclass of that class. So a variable of type DrinksMachine may reference an object of type ExtDrinksMachine1 or ExtDrinksMachine2. If the parameter to a method is given as type DrinksMachine that method is general for objects of type DrinksMachine or any of its subclasses, such as the two given here ExtDrinksMachine1 and ExtDrinksMachine2. This raises an issue: suppose we have a variable of type DrinksMachine which happens to be storing a reference to an object of actual type ExtDrinksMachine2. Then suppose the method pressCoke is called on this variable. Which code will be used to execute this method, the code from the DrinksMachine class or the code from the ExtDrinksMachine2 class?

The answer is ExtDrinksMachine2, this is because method calling in Java works using a feature known as dynamic binding. So if a method call is attached to a variable, the method must be one defined in the class of that variable or a superclass as this is checked when the code is compiled. However, when the code is run, the actual class of the object the variable refers to (which may be a subclass of the class of the variable) is looked at to see if there is a method in it which matches the call and that method is used.

To illustrate this, let us consider a code fragment similar to that we considered above:

 DrinksMachine mach1 = new DrinksMachine(m);
 ExtDrinksMachine2 mach2 = new ExtDrinksMachine2(n);
 DrinksMachine mach3 = cheaper(mach1,mach2);
 mach3.pressCoke();
When the last statement here is executed, which version of pressCode is used will depend on whether mach3 is an alias for mach1 or for mach2. If it is an alias for mach2, it will have the effect of calling the cokesEmpty method on the supplier variable of the object referred to by mach2 if that object's cokes arrayList drops to length 0. This is the behaviour we would want to simulate real life machines. Two machines may look identical with “coke” and “fanta” buttons. One is of the sort which contacts the supplier when it runs out, the other is of the simpler sort which does not. We would expect, when we press the “coke” button, for a machine to react appropriately according to its own type, even if we do not know which type the machine is.

Here is some code for testing the ExtDrinksMachine2 example:

import java.util.Scanner;

class UseDrinksMachines6
{
 public static void main(String[] args)
 {
  int p,c,f;
  Scanner input = new Scanner(System.in);
  System.out.println("Machine 1 is a standard machine");
  System.out.print("Enter the price for drinks on machine 1: ");
  p = input.nextInt();
  System.out.print("Enter the number of cokes in machine 1: ");
  c = input.nextInt();
  System.out.print("Enter the number of fantas in machine 1: ");
  f = input.nextInt();
  DrinksMachine mach1 = new DrinksMachine(p,c,f);
  System.out.println("Machine 2 is an extended machine");
  System.out.print("Enter the price for drinks on machine 2: ");
  p = input.nextInt();
  System.out.print("Enter the number of cokes in machine 2: ");
  c = input.nextInt();
  System.out.print("Enter the number of fantas in machine 2: ");
  f = input.nextInt();
  DrinksCompany comp = new DrinksCompany();
  ExtDrinksMachine2 mach2 = new ExtDrinksMachine2(comp,"no. 2",p,c,f);
  DrinksMachine cheaper = DrinksMachineOps.cheaper(mach1,mach2);
  System.out.print("Enter the amount of money you wish to spend on cokes "+
                   "on the cheaper machine: ");
  int amount = input.nextInt();
  int cokes = DrinksMachineOps.spendOnCokes(amount,cheaper);
  amount = cheaper.pressChange();
  System.out.println("You have "+cokes+" cokes and "+amount+"p change");
  mach2.insert(amount);
  System.out.println("Put the change in Machine 2 and press the Fanta button");
  Can can = mach2.pressFanta();
  amount = mach2.pressChange();
  if(can==null)
     System.out.println("No fantas given");
  else
     System.out.println("You have a "+can+" and "+amount+"p change");
 }
}
As in the previous example, it creates two machines, one of the standard type and one of the extended type, and runs the method spendOnCokes on the cheaper machine. Which is the cheaper machine is only determined when the code is actually run. For this code to work, you will also need a class which provides DrinksCompany objects. Here is an example:
class DrinksCompany
{
 public DrinksCompany()
 {
 }

 public void cokesEmpty(ExtDrinksMachine2 mach)
 {
  System.out.println("Machine "+mach.getIdentity()+" out of cokes");
  for(int i=0; i<10; i++)
      mach.loadCoke(new Can("coke"));
 }

 public void fantasEmpty(ExtDrinksMachine2 mach)
 {
  System.out.println("Machine "+mach.getIdentity()+" out of fantas"); 
  for(int i=0; i<10; i++)
     mach.loadFanta(new Can("fanta"));
 }
}
All that is required of a DrinksCompany object from the code in the front-end UseDrinksMachines6.java is that it can have the methods cokesEmpty and fantasEmpty called on it, with arguments of type ExtDrinksMachine2. In this example, the identity of the machine is used in a message that is printed on the screen, and the machine is loaded with ten new cans. If the cheaper machine is of type ExtDrinksMachines2 it will never run out of cokes, because as soon as it does it is instantly replenished. This is because when cokesEmpty is called on the DrinksCompany object the code for it is executed, and execution returns to the code for pressCoke and from that to the code for spendOnCokes afterwards. A more sophisticated simulation might have the DrinksCompany object on a separate “thread”, so that its code is executed in its own time and a delay is observed before the machine is replenished. However, that is beyond the scope of this course.

The files this example requires are: DrinksMachine.java, Can.java, EmptyCanException.java ExtDrinksMachine2.java, DrinksCompany.java, DrinksMachineOps.java and UseDrinksMachines6.java.

Overriding the defaults from Object

As every class is a subclass, directly or indirectly, of class Object, every class may override the methods provided for it from class Object. So you could leave a class to have the code for these methods it inherits, or you could write your own alternative. For example, the default for the method equals is that it returns true if and only if the two object references it is comparing are aliases, which means that obj1.equals(obj2) is equivalent to obj1==obj2 where obj1 and obj2 are variables of any object type. However, quite often we want equals to behave in a more general way so that two different objects are considered equal if they have the same content. Java's String class is an example, if str1 and str2 are two variables of type String we would like str1.equals(str2) to return true if these variables refer to different String objects but both objects have exactly the same length, and exactly the same characters in exactly the same order. Java's code for class String has its own method equals which overrides the default inherited from Object for just this reason, that is why when you test two String variables to see the Strings they refer to are equal, you should use the method equals rather than the operator ==. This is because str1==str2 will give false even if the two Strings are exactly equal if instead of being aliases they refer to separate String objects which have the same length and same characters in the same order. There is not much use in using str1==str2 because it never matters whether two String references are aliases or different but equal Strings. It would only matter if a String could be changed destructively, because then if they were aliases changing one would change the other, if they were not aliases changing one would leave the other unchanged. If that was the case, you might need to check if they were aliases in order to avoid such a thing happening when you changed a String through a variable which referred to it. However, Java deliberately implements the class String as immutable, meaning it has no destructive methods, in order that this could never be a problem.

When you write your own class, you might decide that as with class String it would be a good idea to override the default equals with code which gives a more general equality test than just testing for aliasing. This is something we will look at in more detail later. Another class which is inherited from Object but is often overridden is toString. The default does not return a String which is meaningful in terms of your own class, so overriding it gives you one.

The toString method is used automatically every time you try to “print” an object, or “join” it to a String using +. The equals method is used in a lot of Java's library code. For example the method contains in Java's ArrayList<E> uses it, if we have a of some type ArrayList<T> and obj of type T, then arr.contains(obj) returns true if there is some int value i such that a.get(i).equals(obj) returns true, and it returns false otherwise. The dynamic binding principle means if you define your own equals method to override the default, then the code you define will be used when the contains method is called on an arrayList whose element type is your class. So Java's built-in code uses your code, even though Java's code was written with no knowledge of your code.

The ability to override Java's default code for methods such as equals is a powerful one, which has many implications, only some of which can be covered in this module. We will see a little more of what it can achieve in later sections.

Caution with using inheritance

In the early days of object-oriented programming, inheritance was considered to be one of its most important and useful aspects. That was because it enabled you to re-use code (remember, this means using actual existing code, not copying it) in two ways:

  1. A subclass can be written with code that only gives how it differs from its superclass (the extra methods and fields and overridden methods).
  2. Methods which takes arguments of the superclass can be used with arguments of the subclass instead of new methods having to be written.
However, as time went on, many programming experts saw problems with inheritance, with a particularly famous article by Allen Holub highlighting it. The particular issue of concern is more to do with the first point above than the second, which is why what Holub actually said is “extends is evil”, where extends is the keyword used in Java to declare a class as a subclass of another class. There is less of a problem with interface types, which we will consider next, because when a class implements an interface type it has to give code for all its methods, so no code is overridden, but the second point above still applies, as a parameter of an interface type can take an argument of any class that implements that interface type.

The main concern is that if we have

class Beta extends Alpha
so Beta is a subclass of Alpha, that creates a link between two different pieces of code that can be quite hard to keep track of and so can cause difficulties. One aspect is that if the code for Alpha is changed to perform differently, that will make Beta perform differently as it inherits that code. Note this is also an advantage of inheritance, and shared code in general. Consider that if an error was found and corrected in the code for Alpha, then Beta (and any other subclass of Alpha) would share in the correction, whereas if the code for Beta had been written by copying and pasting the code for Alpha it would have to be corrected separately. However, balancing when it is helpful to inherit code and when it is not is an issue, and the problem caused when it is not helpful is called the fragile base class problem.

Another issue with inheritance comes from the dynamic binding principle as covered above, so in this case it is to do with the second of the two points regarding inheritance and re-use of code. If code is written to take an object of a particular class, but the object it takes is actually of a subclass of that class, it will use the code in the methods of the subclass if they override the methods of the original class. These methods could have completely different behaviour so long as they have the same header. That could cause serious problems if code is run without realising that the objects referred to by a variable were actually of a subclass which has overriding methods that behave in a different way than the code is written to expect. This issue was first identified by Barbara Liskov, and led to her establishing a rule of practice regarding inheritance, which has become known as the Liskov Substitution Principle, abbreviated to LSP. The essence of this is that if you override a method, you should only do so with code that does not conflict with the expectations of the method.

Composition compared with inheritance

Due to these problems, it is now generally considered that you should favour composition over inheritance. What this means is that if you are constructing a class Beta and you think that a Beta object is an Alpha object with some extra features, you should think carefully about whether that really means implementing it using inheritance:

// INHERITANCE
class Beta extends Alpha
{
...
} 
or whether actually it is better to think of a Beta object as made up of an Alpha object combined with some other things, which is implemented by having a field referring to an Alpha object inside a Beta object:
// COMPOSITION
class Beta 
{
 private Alpha myAlpha;
...
} 
and you should do it this second way unless you are absolutely sure it makes sense to think of and use a Beta object as a specialised form of an Alpha object. Another way of putting this is to consider whether it is best to say that a Beta object is a Alpha object, or to say that a Beta object has a Alpha object linked to it. If composition is used, Beta is not a subclass of Alpha, so to use the methods of Alpha it needs to have its own code which calls them on the Alpha object it refers to, and a Beta object cannot be passed through a parameter of type Alpha.

In our example of a machine that is like the previous DrinksMachine but with an additional range of drinks, it does make sense to think of it as a specialised form of DrinksMachine, so it is appropriate to use inheritance, as we did above. It would be possible to implement an object representing a drinks machine that sells Sprites as well as Cokes and Fantas by an object which contains a reference to a DrinksMachine object, but the code would be much more complex than the version implemented with inheritance, and in addition that object would not be able to be passed as an argument to methods with a DrinksMachine parameter in the way we did previously. The file ExtDrinksMachine1a.java shows an attempt to do this, with UseDrinksMachines7.java some front end code testing it.


Matthew Huntbach

Last modified: 8 March 2019