Algorithms and Data Structures in an Object-Oriented Framework (“ADSOOF”)
Inheritance in Java: extended drinks machines
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 Can
s 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 Can
s 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
Can
s, and then adds s
new Can
objects
to it.
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
.
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
.
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 String
s 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 String
s 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 String
s. 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.
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:
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 Alphaso
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.
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.
Last modified: 8 March 2019