Algorithms and Data Structures in an Object-Oriented Framework (“ADSOOF”)
Defining objects in Java: return to drinks machines
So far we have used objects in Java but not seen how they are
defined. We have used objects whose definitions are provided
as part of the Java code library, and objects where we already
have a .class
file to define them. It is likely you have
seen the use of classes to define objects in previous modules,
but using objects without knowing the code of their classes was
a deliberate part of this module. The aim was to help you move away from
thinking about a program as a whole thing where you need to know every
bit of its code, to thinking about dealing with components of
programs where you work on one bit without knowing the details of how
other bits work.
When using Java we can talk about the application of a class of objects, which is code that uses object of that class, and the implementation of an object, which is the code that is used to make the objects work when methods are called on them. These two aspects are united with the specification which describes how an object of a particular class works in terms of its interaction with the application code. The writer of the application code writes that code under the assumption that objects of the class work according to the specification, he or she does not need to know what happens underneath to make them work that way. The writer of the implementation code has to make sure objects of the class work according to the specification, but he or she does not need to know what else happens in the application code.
Breaking up code into separate well-defined portions like this is a key part of good programming style. It may not seem so apparent on the sort of small-scale programs you write when you first start learning to program, but remember the sort of computer programs which are developed and sold commercially are on a hugely greater scale, written by whole teams of people. It is impossible for any one person to have a full understanding of how all the code works, breaking down the program into well-defined parts is the only way to manage its development.
The object-oriented style of programming also encourages re-use of
code. This means that once a class of objects has been developed, that class
can be used again in other circumstances where that sort of object is required.
This means that it is not necessary to write every part of a program in
terms of the most basic structures of the programming language. We have
already seen software re-use when we have used code from Java's libraries.
If we want something that works like an array, but whose size may change,
for example, we do not start from scratch and think how we could write
code to do that. Instead, we pick the class
ArrayList<E>
from the Java library, and make use of it, it runs using the code already
written by the developers of the Java system.
However, we cannot assume there will always be a suitable class already defined for every need we might have. It is important as Java programmers to be able to develop our own classes when necessary.
So far, we have used methods called on objects, but the only methods where we
have seen the implementation have been declared as static
in Java. If
we have a method with the header(1):
static R meth(T1 p1,T2 p2,...,Tn pn)it means
meth
is the method's name,
R
is the method's return type, and there are
n
parameters where pi
is the
name of the i
th parameter and Ti
its
declared type. Then a call takes the form
meth(a1,a2,...,an)
where each
ai
is an expression which evaluates to a value of type
Ti
. When the method call is evaluated, the code which
forms the body of the method is evaluated in an environment which consist
of variables p1
, p2
up to pn
where each pi
is initialised to the value ai
from the method call. Additional local variables may be added to the
environment as variable declarations inside the code for the method are executed.
The call may be used in any place where a value of type R
is required. If the method call is executed as a statement on its own
then the return value is ignored or there is no return value if
R
is void
, in which case the method
call will be intended to have an effect by changing a mutable object passed
as an argument or by performing some external action.
Static methods are self-contained: all the data they use is passed to them
as arguments (this ignores variables declared as static
otherwise
known as “class variables”, but we will not consider these here). Methods
which are not declared as static
must be “called on” a reference to an
object of the class the method is defined in.
In most cases this means the method call takes the form
v.meth(a1,a2,...,an)
where v
is a variable name of that class, but the call could also be attached
to another method call which returns a value of the type of that class.
The name “instance method” is sometimes used for any method which is not static, as it is called on an “instance” of the class in which it occurs. We may also call them “object methods” or just “non-static methods”.
An instance method is defined in the class with the form
R meth(T1 p1,T2 p2,...,Tn) { ... code ... }and execution of the code in the method works similarly to execution of the code in a static method, with an environment which contains variables
p1
, p2
up to pn
with each pi
initalised to the matching ai
of the method call. However, the environment of the method call also
contains the variables from the object it is called on. These are the
variables which came into existence when the object was created and remain
in existence until the object disappears when no other object references it.
If these variables are changed to refer to something else during the
execution of the code in the method, then that change is permanent, it
remains in effect after the method call is finished. So that is how a
method call on an object can change the state of that object. Like
a static method call, an instance method call where the return type of
the method is R
can be used in any place where a
value of type R
is required.
Note that if a call to a further instance method is made in the code
of an instance method it does not have to be indicated as attached to
any particular object. If it is not, it is taken to be attached to the
same object that the method it is called in is attached to. If a method
is intended to be called only by other methods in the same
class then it can be declared as private which means
any attempt to use it in another class will be indicated as an error
by the Java compiler. This is done by putting the word private
in front of the rest of the method. Using the word public
in the same way means the method can be accessed in all other Java classes.
If a method is neither declared as public
nor private
it can be accessed by other classes, but only in the same package. But as
we haven't discussed defining our own Java packages, you can ignore that
possibility, and in general should declare methods as either public
or private
.
Object variables are declared inside a class but outside any methods.
So if we have a class called MyObject
and inside it
(but not inside a method) declarations Type1 var1
and Type2 var2
it means that every object of
type MyObject
has its own variable called
var1
which is of type Type1
and its own variable
called var2
which is of type Type2
.
If obj
is a variable of type MyObject
,
then the variable var1
of the object referred to by
obj
can be referred to by obj.var1
and so
on for other variables of the object. However, object variables are
very often declared as private
. As with private
methods, this means they can only be referred to in the code of methods inside
the class where they are declared.
It is possible to declare a variable in an object as public
meaning code in any other class can access it directly in the form
obj.var1
where obj
is an object reference and
var1
a variable name. In general, it is considered good
practice not to have any public
variables in a class.
Making all object variables private
means that an object
controls its own state, the values of its variables can only be altered by
its own methods.
A constructor of a class is written like a method in the
class, but its name must be the same as the class name and it
has no return type. So a constructor for objects of the type
MyObject
will have the form
MyObject(T1 p1,T2 p2,...,Tn an) { ... code ... }A call to it takes the form
new MyObject(a1,a2,...,an)
and it can be used wherever a value of type MyObject
is
required. So note the word new
is always used when a
new object is created, but not in any other circumstances.
The code of the constructor is executed like a method, so in
an environment with local variables p1
to pn
where each variable pi
is initialised to ai
,
with the addition of the variables of the object. This is when those
variables come into existence and they remain in existence so long as
the object constructed remains in existence. In most cases, a constructor
will do nothing more than assign values to object variables. Note that
any object variable which is not assigned a value in the constructor
will be initialised to the value 0
if it is numerical,
false
if it is boolean
, and null
for any
object type. Constructors can also be declared as either public
or private
, though at this stage you may wonder what use is
a private constructor.
Good practice is to decide what you want a class for, and then to define the methods you want available for use on objects of that class so that they do what is necessary for those objects to fulfill their purpose. As mentioned above, this is its specification. Once you have done that, you can consider the variables which go inside each object of that class, and the code for the methods you have decided it needs. The variables which every object of a class has are sometimes called the fields of the class. The methods of a class will manipulate the variables of the object of that class it is called on. A method call can change what one of these variables refers to, either by making it refer to something else or by changing the object it refers to destructively if the variable is of a type which refers to a mutable object. As it is useful for objects to be immutable if that is a possibility, you may decide when considering the methods of a class not to provide any which change the values of the variables in the object, then you will make it an immutable class. The overall behaviour of the methods should make the objects of the class appear to behave in the way you want, so each method reflects some behaviour you want from the object.
This approach of deciding first what you want an object to do in terms of its interaction with other objects in a software system, and only after that deciding what goes inside a class to make it do that, is an important part of good quality programming. It means your software breaks down into well defined parts instead of being one big mess. This becomes more and more important as we move from small scale programming of the sort you might do as simple lab exercises to something on a more realistic scale.
The idea that the definition
of a class in terms of how its objects interact with other objects is
a separate thing from its implementation is what leads to it being
considered good practice to make fields within a class private
.
Then no other software can treat the objects in terms of what are in them
rather than in terms of how they interact. You could decide to change
what is inside the class, maybe because you thought of a better way to
get it to do what is wanted, without any code which uses that class having
also to be changed so long as you made all its fields private
.
For the same reason, you should consider carefully whether any methods
you add to a class make sense in terms of how it interacts with other classes.
Sometimes, you need to define a method which is needed as an auxiliary method
to help other methods, but does not make much sense on its own. You should
declare a method like this as private
, because it isn't intended
for outside use, and if outside code could call it, it may mean objects of
that class are used in a way that does not make sense. If you are given the
definition of a class in terms of its public methods to implement, you
should never add an extra public
methods because you feel they
might be useful. Adding unauthorised public
methods could mean
the application code which uses objects of that class stops making sense
because it accesses internal data of those objects in some unauthorised way.
That could lead to programs which are hard to understand, or even security
problems.
All the above may seem rather abstract. It may be easier to grasp through
an example. We have already seen examples of the
use of objects of
a class called DrinksMachine
. At that point you were
not shown the code that is inside the file DrinksMachine.java
,
you were only given the
methods of
the class, and a link
to enable you to download the .class
file. The full code is now given below.
You can also find it in the
code folder
(but note, it is slightly different there, the reason why will be explained later).
import java.util.ArrayList;
class DrinksMachine
{
private ArrayList<Can> cokes, fantas;
private int price,balance,cash;
public DrinksMachine(int p)
{
price = p;
balance = 0;
cash = 0;
cokes = new ArrayList<Can>();
fantas = new ArrayList<Can>();
}
public DrinksMachine(int p,int c, int f)
{
this(p);
for(int i=0; i<c; i++)
loadCoke(new Can("coke"));
for(int i=0; i<f; i++)
loadFanta(new Can("fanta"));
}
public void insert(int n)
{
balance=balance+n;
}
public int getBalance()
{
return balance;
}
public int collectCash()
{
int oldCash = cash;
cash = 0;
return oldCash;
}
public int getPrice()
{
return price;
}
public int pressChange()
{
int change=balance;
balance=0;
return change;
}
public Can pressCoke()
{
if(cokes.size()>0&&balance>=price)
{
Can can = cokes.get(0);
cokes.remove(0);
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);
balance=balance-price;
cash=cash+price;
return can;
}
else
return null;
}
public void loadCoke(Can can)
{
cokes.add(can);
}
public void loadFanta(Can can)
{
fantas.add(can);
}
public boolean cokesEmpty()
{
return cokes.size()==0;
}
public boolean fantasEmpty()
{
return fantas.size()==0;
}
public void setPrice(int p)
{
price = p;
}
}
Here the lines
private ArrayList<Can> cokes, fantas; private int price,balance,cash;indicate that an object of type
DrinksMachine
has
five variables inside it, cokes
and fantas
of type ArrayList<Can>
(that is, an arrayList of
Can
objects) and price
, balance
and cash
of type int
. So when any non-static
method from class DrinksMachine
is executed (and there
aren't any static methods), it will be in an environment which has the
parameters to the method as variables along with the variables
cokes
, fantas
, price
,
balance
and cash
. In a constructor these
will be new variables, in any other method they will be the variables
from the object the method is called on. A variable with the name
this
is also provided in the environment for any non-static
method, it is set to refer to the object the method is called on, or to
the object being created in a constructor.
The lines
public DrinksMachine(int p) { price = p; balance = 0; cash = 0; cokes = new ArrayList<Can>(); fantas = new ArrayList<Can>(); }are a constructor for
DrinksMachine
. They mean that the
call new DrinksMachine(expr)
where
expr
is an expression which evaluates to an
integer value returns a reference to a new DrinksMachine
object, which has internal variables balance
and
cash
set to 0
, internal variables
cokes
and fantas
set to an arrayList of
Can
objects of size 0, and internal variable price
set to the value of expression expr
. Note that the
lines
balance = 0; cash = 0;are not strictly necessary, since if they weren't there these variables would be set to
0
as the default anyway.
The lines
public DrinksMachine(int p,int c, int f) { this(p); for(int i=0; i<c; i++) loadCoke(new Can("coke")); for(int i=0; i<f; i++) loadFanta(new Can("fanta")); }are a second constructor for class
DrinksMachine
. It is
permissible to have more than one constructor so long as they differ in
number and/or type of parameter. The two constructors mean that if
a call new DrinksMachine(...)
is made where
...
is a single integer expression, the first
constructor is used, if ...
is three integer expressions
separated by commas, the second constructor is used.
The first statement, this(p)
in the code of the second
constructor needs some explanation. It is another use of the
word this
than the one mentioned
above. The word this
as a method call as the first statement of a constructor means that the
code from another constructor of the same class is used. In this case it
means that the code
for the first constructor with argument p
is used to set the
values of the internal variables of the new DrinksMachine
object before the rest of the code in the constructor is used to change
them. Here p
is the variable which holds the value of the
first argument to the constructor. Then the two loops cause methods
loadCoke
to be called c
times and method
loadFanta
to be called f
times where c
and f
are the variables in the local environment which hold the
values of the second and third arguments to the constructor. Note these are
examples of an isntance method call being in the class where the instance
method is defined. As they are not specified as being called on any other
object they are called on the object being created by the constructor.
The methods in class DrinksMachine
are short and their effect should
be easily seen. The method written
public void insert(int n) { balance=balance+n; }causes the variable called
balance
of the
DrinksMachine
object it is called on to be increased
by the value of its argument. So, for example, if d
is a variable of type DrinksMachine
and m
is a variable of type int
, the call d.insert(m)
causes the statement balance=balance+n
to be executed
in an environment where m
is a local variable into
which the value of n
has been copied, but balance
means the actual variable called balance
in the object
referred to by d
. So this variable gets increased by
the value that is in m
in the environment where
d.insert(m)
was called.
The method written
public int getBalance() { return balance; }does nothing but return the value of the
balance
variable
of the DrinksMachine
object it is called on. Strictly
it means that when the call d.getBalance()
is executed,
the statement return balance
is executed in the environment
where balance
is the balance
variable of the
object referred to by d
.
The method written
public int pressChange() { int change=balance; balance=0; return change; }is an example of an instance method with a local variable. When the call
d.pressChange()
is made, the local variable
change
comes into existence in the environment where
this method is executed, it is set to the value of balance
in this environment, and then balance
in this environment
is set to 0
. So the result is that change
holds the old value of the balance
variable of the
object referred to be d
, while that variable has its value
changed to 0
, this change persisting after the method has finished.
So this code enables the old value of balance
to be
returned and the value of balance
to be changed in one
method call. It is necessary to do it this way because a return
statement is always
(2)
the last statement to be executed in a
method and causes the method to terminate. You could not, for example,
write the method as:
public int pressChange() // THIS IS DELIBERATELY SILLY CODE { return balance; balance=0; }because executing
return balance
would cause the
method call to terminate, and the following statement
balance=0
would never get executed.
The method written
public void loadCoke(Can can) { cokes.add(can); }shows an example of a method call on an object causing a method to be called on an object referred to by one of its variables. An object of type
DrinksMachine
has its own variable of type
ArrayList<Can>
called cokes
(and
another of the same type called fantas
). If c
is an expression of type Can
and d
is a
variable of type DrinksMachine
then d.loadCoke(c)
causes the call add(c)
to be made on the
ArrayList<Can>
object referred to by the variable
cokes
of the DrinksMachine
object referred
to by d
. The result is to add the object given by
c
to the end of one of the arrayList of Can
s
which forms part of the state of the DrinksMachine
object.
Note that “an expression of type Can
” is not the same
thing as a variable of type Can
. A variable of type
Can
is one example of an expression of type Can
,
but another example is a method call which returns a reference to an object
of type Can
. So new Can("fanta")
is an
expression of type Can
, as is d.pressCoke()
where
d
is of type DrinksMachine
. In the second
constructor for DrinksMachine
, the repeated call of
loadCoke(new Can("coke"))
means new Can
objects
are repeatedly created and added to the end of the arrayList referred
to by the variables cokes
in the object.
The method written
public Can pressCoke()
{
if(cokes.size()>0&&balance>=price)
{
Can can = cokes.get(0);
cokes.remove(0);
balance=balance-price;
cash=cash+price;
return can;
}
else
return null;
}
shows a local variable, methods called on one of the arrayList objects
which form part of the state of a DrinksMachine
object,
and the values of integer variables which form part of the state being
changed. Remember it is meant to represent the operation of pressing
the button labelled “Coke” on a drinks machine after some money has been
inserted into the machine. The variable balance
which
forms part of the state of the object represents money inserted into the
machine, but not yet spent or collected after pressing the “Change”
button. The variable cash
represents the amount of money
spent on drinks in the machine since the cash was last collected (or
since the machine was created if it has never been collected). The
variable cokes
refers to an arrayList which represents
cans waiting to be delivered. In the code, cokes.remove(0)
causes the first can to be removed from the stored cans. Then
the price of a can is taken from the balance held and added to the
cash held. A reference to the first can was taken before it was removed
and held in the local variable can
, this is then returned
as the result of the method call.
Note the lines
Can can = cokes.get(0); cokes.remove(0);above could actually be written as
Can can = cokes.remove(0);This is because the
remove
method with int
argument
in class ArrayList
has a return value, the value of the item
removed when the method is called.
Often we don't need to keep that value, so we call remove
on
an arrayList without making any use of it. In the code given the operation
of getting the Can
object at the start of the arrayList and then
removing it from the arrayList is split into two parts to make it clearer.
As noted
previously
although here null
is returned by the method pressCoke
to
represent the machine being unable to supply a can of Coke, either because it has none
left or because not enough money has been entered, this should not be considered good
practice. It would be better to throw an exception, the code to do that is given
below.
The important thing about the methods in class DrinksMachine
is that
together they ensure a DrinksMachine
object correctly represents a
drinks machine. The only way the variables in a DrinksMachine
object can be changed are in ways which reflect how a real machine
would work. You cannot change the value of the variable cash
in a DrinksMachine
object except in a way which represents
buying a drink or collecting all the accumulated money spent on the machine.
The arrayLists which represent the cans stored in the machine can only
be changed in a way which represents taking a can from one end and adding
a can to the other.
If code which used the class DrinksMachine
could refer
directly to the arrayList objects inside a DrinksMachine
object
it would be harder to understand because there would be aspects of it which
did not look directly as if they were about representing real drinks
machines. It would mean if you decided to change the way you represent
DrinksMachine
objects, maybe having some other collection type
inside instead of arrayLists, all the code which uses them would have to be
checked to see if there was anywhere it referred directly to the internal
arrayLists, and it would have to be changed. It could mean that if you
perform an operation on a DrinksMachine
object, maybe load
it with 10 Coke cans, in one part of the program, you are surprised to see
in another part it doesn't show the proper effect of that, because there
is code somewhere which directly accesses the arrayLists of the
DrinksMachine
object and changes them in some way. Remember
that because Java has aliasing, that might not be obvious because it may
be that some reference to an arrayList<Can>
object you
use for some other purpose was set to alias the internal arrayList object
of your DrinksMachine
object.
In general, attempting to shortcut the clear separation between implementation and application code may sometimes seem an easy way around a tricky programming problem you have encountered. You may feel it is “powerful” to be able to access and change objects in ways which aren't part of their definition. However, once code becomes more than small-scale exercise code, this is likely to lead to software systems becoming confusing, hard to modify, and dangerous due to unexpected behaviour and possible security breaches. It is important that you get to understand and naturally use the idea of having the clear separation between application and implementation early on.
We saw
previously
the use of a version of the DrinksMachine
class which handled
a machine which could not return a drink, either because it was empty or
because not enough money had been inserted, by throwing an exception.
In the code above, a
test was made to see if enough money was inserted and enough cans were
in the machine, but if not, the result null
is returned.
Just returning null
means the programmer who uses the
class DrinksMachine
and its methods may not take this
possibility into account, and that would cause problems later when a variable
which is assumed set to refer to a Can
object instead is set
to null
. Throwing a checked exception means the programmer
who uses DrinksMachine
is made to write code which deals
with the possibility of no can being returned.
All that is required is for the code for the method pressCoke
to be changed so that it throws the appropriate exception when necessary:
public Can pressCoke() throws EmptyMachineException,NotEnoughMoneyException { if(balance<price) throw new NotEnoughMoneyException(""+(price-balance)); else if(cokes.size()>0) { Can can = cokes.get(0); cokes.remove(0); balance=balance-price; cash=cash+price; return can; } else throw new EmptyMachineException("coke"); }The method
pressFanta
is similarly altered, with
"fanta"
in the place of "coke"
in the
statement throw new EmptyMachineException("coke")
:
public Can pressFantas() throws EmptyMachineException,NotEnoughMoneyException { if(balance<price) throw new NotEnoughMoneyException(""+(price-balance)); else if(fantas.size()>0) { Can can = fantas.get(0); fantas.remove(0); balance=balance-price; cash=cash+price; return can; } else throw new EmptyMachineException("fanta"); }No other method has to be changed.
There are two different types of
exceptions which can be thrown by the method,
NotEnoughMoneyException
which is thrown not enough
money has been inserted to pay for a drink, and
EmptyMachineException
when the machine
does not have any of the required drinks left. If there is not enough
money inserted and there are none of the required drinks left, the code
as it is written throws a NotEnoughMoneyException
rather
than an EmptyMachineException
.
Note that both types of exceptions which it can throw have to be named in the
signature to the method following the keyword throws
, this
is necessary because they are checked exceptions. Both types of
exceptions are created with a String
argument which gives
more detail on what went wrong. In the case of a
NotEnoughMoneyException
it gives the amount of money
that still needs to be inserted to pay for a can, in the case of
EmptyMachineException
it gives the name of the drink
which was asked for. This String
value given by its constructor
is what is returned by the getMessage
method that can be called on an
exception object. Note that as these are not exception types which
are already provided by Java, but instead exception types we have defined
in conjunction with our own type, we also have to write classes for them.
This is easily done, it is a matter of extending the class
Exception
which is provided by Java:
class NotEnoughMoneyException extends Exception { public NotEnoughMoneyException() { super(); } public NotEnoughMoneyException(String message) { super(message); } }and
class EmptyMachineException extends Exception { public EmptyMachineException() { super(); } public EmptyMachineException(String message) { super(message); } }This uses the concept of inheritance, which we discuss later. Strictly what we are doing is provided two constructors for each of the the new classes, one with 0 arguments, the other with one
String
argument, and both constructors just use the code
of the equivalent constructor for Exception
. But you can
just think of this as the pattern for making your own exception type,
since any exception class will be exactly the same apart from the
actual name of the exception.
(1) We have already looked at generic methods, so if we take this into account, the standard form a static method header takes is:
static <V1,V2,...,Vm> R meth(T1 p1,T2 p2,...,Tn pn)where
V1
,V2
,...,Vm
are any type variables used in the rest of the method. If there are no type
variables, then this type variable declaration bit can be left out. In all the
examples we saw where type variables were used, there was only one, so
m
was 1, and so there were no commas which are needed
when there are more than one type variable declared. The return type
R
and the types of the parameters
T1
,T2
,...,Tn
can be one of the type variables, or a generic type which takes one of them
as its type argument (or some more complex arrangement such as a generic
type with more than one type argument, or one which takes another generic
type which takes a type variable as its argument). Later we will look at
generic classes
where a type variable is declared with scope over a whole class, so it is
declared in the class header and not the method header. This means that the
header for an instance method takes the form:
<V1,V2,...,Vm> R meth(T1 p1,T2 p2,...,Tn pn)where the return type or parameter types could also include type variables from the class header. For convenience, in this section of notes we ignore the possibility of methods having type variables.
Note also that method headers may also have “access modifiers” coming
before what is given above, we have seen the most common examples
public
and private
, and they may have a
declaration of exception types that can be thrown coming after.
If there are exception types declared, they are given by the keyword
throws
followed by the exception types which are
separated by commas if there are more than one exception type.
(2) Actually, it is not quite true that the return
statement
is always the last thing to be executed in a method. We have
seen Java's try-catch
statement used to catch exceptions
here.
The try-catch
statement covered there has an optional final part,
the finally
block. The code in the finally
block is always executed no matter how the code in the
try
block is exited. This is so even if the code in the
try
block is exited through a return
statement.
It is possible to have a try
block with no
catch
blocks, just a finally
block.
So if you want something to be executed in a method after the
return
statement, you can put the return
statement in a try
block and the code you want
executed afterwards in a finally
block. This would then
enable you to write the pressChange
method as:
public int pressChange() { try { return balance; } finally { balance=0; } }
Last modified: 28 February 2019