Object-oriented programming makes use of a technique called encapsulation to control operations on the attributes of a class and prevent unintended errors and data corruption occurring. If we revise last week's Cat class:
public class Cat { private String name; private int age, weight; public Cat (String nameIn, int ageIn, int weightIn) { this.name = nameIn; this.age = ageIn; this.weight = weightIn; } public void walk() { this.weight--; } public String toString() { return "Name: " + this.name + " Age: " + this.age + " Weight: " + this.weight; } }you can see the use of the keywords
private
and public
throughout the code. What do these
mean?
private
can only be used inside the current class.public
can be used both inside and outside the class.main()
- repeated below - calls the two methods, walk()
and display()
, and all references to the attributes are within the methods inside the class.
public class CatApp { public static void main (String[] args) { Cat clyde = new Cat ("Clyde", 2, 5); Cat binnie = new Cat ("Binnie", 5, 4); System.out.println(clyde); System.out.println(binnie); clyde.walk(); binnie.walk(); System.out.println(clyde); System.out.println(binnie); clyde.walk(); System.out.println(clyde); } }
This is common practice and is known as encapsulation. Encapsulation means to keep the inner workings of the class hidden from the outside world, and control access to those inner workings by means of the methods, which act as "gateways" between the outside world and the interior of the class.
Note that the display()
method has been replaced by a toString()
method. What does this do? It returns the details of the cat as a String, rather than printing it. Why is that better? It gives more flexibility to the code using the class: it allows the code using the Cat class (CatApp
in this case) to choose how to display it, which may not be via System.out.println
. For example, a Java web application (which we are looking at later) does not use System.out.println()
to display things.
Note also how we are printing the cats directly:
System.out.println(clyde); System.out.println(binnie);When we do this,
toString()
is used to figure out how to display the cat (or, in general, whatever object we're trying to print).
For the flexibility reason above, you should in general use toString()
rather than trying to write methods which print to the console.. It allows your code to be used in non-console environments, e.g. web or Android.
Why is encapsulation performed? Consider this new version of the Cat class:
public class Cat { private String name; private int age, weight; public Cat (String nameIn, int ageIn, int weightIn) { this.name = nameIn; this.age = ageIn; this.weight = weightIn; } public void walk() { if(this.weight <= 5) { System.out.println("Can't walk any further... the poor cat will be starved!"); } else { this.weight--; } } public String toString() { return "Name: " + this.name + " Age: " + this.age + " Weight: " + this.weight; } }This new version includes an
if
statement inside the walk()
method, which prevents the cat
from walking if the weight is 5 or less. Thus we are controlling how the cat's weight can be altered using the walk() method. So if we tried the following in the main():
public class EncapsulationApp { public static void main (String[] args) { Cat sniffy = new Cat ("Sniffy", 5 , 7); sniffy.walk(); sniffy.walk(); sniffy.walk(); } }The first two calls to
walk()
would succeed, as the weight would be reduced from 7 to 6 and then from
6 to 5. However the third call to walk()
would fail, as the weight would now be 5 and cannot be reduced
any further.
Note how we have used encapsulation to prevent unrealistic things happening. The following code will not compile but only because weight is private:
public class NoEncapsulationApp { public static void main (String[] args) { Cat sniffy = new Cat ("Sniffy", 5 , 7); sniffy.weight = -1; } }If the weight was not
private
, you would legitimately be able to set it to -1 from the main() as in the above example. This illustrates the whole concept of encapsulation: to keep the inner workings of a class private and control access from the outside world, to prevent the outside world corrupting it.
It is fairly common in object-oriented programming that we wish to access a single attribute, such
as, for example, the name of the cat. We wish to keep this attribute private
, so that the outside world
cannot arbitarily change it, but we still wish the outside world to access it. How can we do this? We can use an accessor method, also referred to as a "getter". Here is a version of the Cat class with
accessor methods for the three attributes:
public class Cat { private String name; private int age, weight; public Cat (String nameIn, int ageIn, int weightIn) { this.name = nameIn; this.age = ageIn; this.weight = weightIn; } public void walk() { if(this.weight <= 5) { System.out.println("Can't walk any further... poor cat will be starved!"); } else { this.weight--; } } public String toString() { return "Name: " + this.name + " Age: " + this.age + " Weight: " + this.weight; } public String getName() { return this.name; } public int getAge() { return this.age; } public int getWeight() { return this.weight; } }
Note how these three methods, getName()
, getAge()
and getWeight()
, are
returning the corresponding attribute. Note also how we specify the return type when writing the method:
public String getName()When writing methods, the return type always immediately precedes the method name. So we are saying that the
getName()
method returns a String. Likewise, getAge() and getWeight() both return an int.
The example below shows how we can use the accessor methods in the main() to get individual pieces of information about the cat object:
public class AccessorsApp { public static void main (String[] args) { Cat sniffy = new Cat ("Sniffy", 5 , 7); Cat tom = new Cat ("Old Tom", 10, 10); System.out.println (sniffy.getName()); System.out.println (sniffy.getAge()); System.out.println (tom.getWeight()); } }Note how we use commands such as:
System.out.println (sniffy.getName());What this line is doing is printing the return value of the getName() method, which, as we can see above, is the
name
attribute of the Cat object.
This example illustrates how we can use getter methods to provide "read" access to an attribute but to prevent "write" access. We can obtain the value of the attribute by using the getter method, but we cannot change its attribute. This is a further example of encapsulation: we allow the outside world to access the attributes but we do not allow the outside world to arbitrarily change them.
As well as accessor methods, we can also add mutator methods to update attributes. These are also called setter methods because we use them to set data. However, unlike accessors, they typically have some sort of controls to prevent the data being set to unrealistic values. For example, a setWeight() method for a Cat would probably prevent the weight being set to less than 0. An example of such a method is shown below.
public class Cat { private int weight; // ...other code omitted... public void setWeight (int newWeight) { if(newWeight > 0) { this.weight = newWeight; } else { System.out.println("Cannot set weight to 0 or less."); } } }
You have already seen in COM411 that you can pass parameters to methods. Here is an enhanced version of Cat
showing how you can do this in Java:
public class Cat { private String name; private int age, weight; public Cat (String nameIn, int ageIn, int weightIn) { this.name = nameIn; this.age = ageIn; this.weight = weightIn; } public void walk (int distance) { // Check the weight will be 5 or more after walking that distance if(this.weight - distance < 5) { System.out.println("The poor cat will be starved if they walk that far!"); } // If it is, reduce the weight by the distance. else { this.weight -= distance; } } public String toString() { return "Name: " + this.name + " Age: " + this.age + " Weight: " + this.weight; } }Note how the
walk()
method now takes one parameter, representing the distance walked. Note how, unlike Python, the data type (int
) must be declared. We also reduce the weight by the distance. (Note that this.weight -= distance;
is a shorter way of writing this.weight = this.weight - distance;
; the -=
operator reduces a variable by a given value. There are similar +=
, *=
and /=
operators). This could be called in a main()
as follows:
public class CatAppWithParameters { public static void main (String[] args) { Cat sniffy = new Cat ("Sniffy", 10, 10); sniffy.walk (5); System.out.println(sniffy); sniffy.walk (3); System.out.println(sniffy); } }Note how we are passing the distance to the walk method. Note the difference between arguments and parameters. The value passed into a method is called an argument, whereas the parameter is the variable in the method representing that value. So, here, 5 and 3 are the arguments whereas
distance
is the parameter.
As you saw last year in COM411, inheritance allows us to use an existing class as a basis for a new, related class. Imagine we wanted to write classes representing different types of vehicle, e.g. Car, Bus, Motorbike. If we were to write the three classes entirely separately, we'd be repeating a good deal of code - e.g. the code for starting and stopping the engine is common to all three classes.
So what we could do in this case is create a Vehicle
class,
containing common functionality for all types of vehicle, and then inherit various subclasses of Vehicle (such as Car, Bus, and
Motorbike) which provide additional functionality specific to that type
of Vehicle. We can say that:
In Java, we use the keyword extends
, e.g.
public class Car extends Vehicle { .... Car attributes and methods }This means that the
Car
class inherits from Vehicle. All attributes and methods in Vehicle
will be inherited by Car, so when you create a Car, all the Vehicle methods and attributes
will be available.
Below is a Vehicle
superclass with Bike
and Car
subclasses.
// Generic Vehicle superclass public class Vehicle { // Attributes common to all vehicles protected int topSpeed, nWheels; protected String make; public Vehicle (String makeIn, int topSpeedIn, int nWheelsIn) { this.make=makeIn; this.topSpeed=topSpeedIn; this.nWheels=nWheelsIn; } public void move() { System.out.println("Moving along..."); } public String toString() { return( "Make : " + make + "\nTop speed: " + topSpeed + "\nno. wheels: " + nWheels); } }
public class Car extends Vehicle { // Car-specific data private int engineCapacity; private boolean engineRunning; public Car(String makeIn, int topSpeedIn, int engineCapacityIn) { // Call the superclass (Vehicle) constructor to construct the Vehicle // component of the Car. We will pass in the top speed, make, and // number of wheels (which we know will be 4) super(makeIn, topSpeedIn, 4); // Set up the Car-specific attributes (engine capacity) this.engineCapacity = engineCapacityIn; } // Overridden move() for cars public void move() { if (engineRunning) { System.out.println("Driving along..."); } else { System.out.println ("Can't drive the car if the engine's stopped!!!"); } } // Car-specific methods public void start() { engineRunning=true; } public void stop() { engineRunning = false; } // Return string representing the car. Note the use of super.toString() to // call the superclass (Vehicle) version of toString() to print the // make, top speed and number of wheels. public String toString() { return super.toString() + " Engine running? " + engineRunning + "\nEngine capacity: " + engineCapacity; } }
public class Bike extends Vehicle { // Bike-specific data private boolean isOffRoad; private int nGears; public Bike(String makeIn,int topSpeedIn,boolean isOffRoadIn,int nGearsIn) { //Call the superclass (Vehicle) constructor to construct the Vehicle // component of the Bike. We will pass in the top speed, make, and // number of wheels (which we know will be 2) super(makeIn,topSpeedIn,2); // Set up the Bike-specific attributes (off-road status and gears) this.isOffRoad = isOffRoadIn; this.nGears = nGearsIn; } public String toString() { return super.toString() + " Off road? " + isOffRoad + "\nNo. Gears: " + nGears; } } public class InheritanceTestApp { public static void main (String args[]) { Car c= new Car("Ford",120,2000); Bike b = new Bike("Raleigh",30,true,27); System.out.println(c); System.out.println(b); c.move(); c.start(); c.move(); c.stop(); b.move(); } }
How is this working?
Vehicle
class the attributes are not private,
but protected. protected means that attributes are accessible from
subclasses (e.g. Car, Bike) whereas private
indicates that, while the attributes
are still there in memory, they are hidden from the subclasses and cannot be used from them.super(make, topSpeed, 4)The
super
keyword represents the superclass. When used in this way, it means
"call the superclass constructor". So when the Car constructor is called, it in turn calls the
Vehicle constructor to initialise the Vehicle aspects of the car (make, top speed, and number of
wheels). The Vehicle constructor will initialise the corresponding attributes to the values
passed to super()
: because we always pass 4 for the number of wheels, the
nWheels
attribute will always be initialised to 4 for Cars.super.toString()
In the example, Vehicle and Car both have a method called
move()
. We have
overridden the original version in Vehicle with the version in Car -
in other words, replaced the original Vehicle version of move() with a new
version in Car.
This will mean that:
move()
on a Vehicle, then the Vehicle
move()
will be called;move()
on a Car, then the Car move()
will be called;move()
on a Bike, then the Vehicle move()
will be called, because we did not override it in BikeIn the same way, toString()
is overridden in both Car and Bike. However, unlike for
move()
, the overridden version of toString()
calls the original, Vehicle version of
toString()
using the super keyword: super.toString();
.
You also met abstract classes in COM411. An abstract class is a class that will never be instantiated; it serves only to be the superclass of inherited classes, and to provide common data and methods. Abstract classes typically contain abstract methods, which are methods containing no code; they will be overridden in subclasses. For example we could have an Animal
abstract class with a makeNoise()
abstract method (as there is no default noise made by a generic Animal!):
public abstract class Animal { protected int age, weight; public Animal(int ageIn, int weightIn) { this.age = ageIn; this.weight = weightIn; } public abstract void makeNoise(); }and then inherit
Cat
and Dog
from it, overriding makeNoise()
appropriately. Cat
and Dog
are concrete classes: classes you can create instances of.
public class Cat extends Animal { public Cat(int ageIn, int weightIn) { super(ageIn, weightIn); } public void makeNoise() { System.out.println("Meow!"); } }
public class Dog extends Animal { public Dog(int ageIn, int weightIn) { super(ageIn, weightIn); } public void makeNoise() { System.out.println("Woof!"); } }
walk()
method from the "Passing parameters to methods" example, above, to your Cat
class. Also modify the Cat's eat()
method so that the weight can never go above 20. Print a suitable error
message if an attempt is made to increase the weight beyond that amount.display()
method to a toString()
method, as in the example above.eat()
method further, so that it now takes a parameter of amount
.
The weight should increase by the specified amount.getName()
, getCourse()
and getMark()
method to your Student
class. These should simply return the appropriate attribute.printDetails()
to a toString()
method which returns the details as a string.setMark()
method to your Student
class, to set the student's mark. The method must validate the mark, and check that it is between 0 and 100. The mark should only be updated if it is valid.getGrade()
method to Student
. This should return the student's grade as a String
based on the mark, according to this scheme :
didPass()
method to Student
. This should return a boolean, depending on whether the mark is above or below 40.Student
constructor so that the mark is no longer passed in as a parameter, and instead, set the mark to 0. The idea is that setMark()
will be used instead to set the mark laterprintDetails()
method with a toString()
method.main()
method of your existing program so that:
getName()
and getCourse()
methods.Scanner
. Set the mark using setMark()
. Print the student's status (mark, grade, and whether they passed or not) by calling the getMark()
, getGrade()
, and the didPass()
methods, and printing the return value of each.Undergraduate
and Masters
should extend from Student
. Make Student
an abstract class and change the getGrade()
method of Student
to be abstract, and provide overridden versions of getGrade()
for Undergraduate
and Masters
. The undergraduate version should work exactly the same as the version above. The masters version should instead return the following grades:
main()
, and printing their grades. You do not need to read them in from the keyboard; you can just hard-code them.