Topic 4: More on Classes and Objects

Encapsulation

Object-oriented programming makes use of a technique called encapsulation. 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 void display()
    {
        System.out.println("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? So you can see that in this example, the methods can be used outside the class but the attributes can only be used inside the class. The main() called the two methods, walk() and display(), and all references to the attributes are within the methods inside the class.

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.

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... poor cat will be starved!");
        }
        else
        {
            this.weight--;
        }
        
    }

    public void display()
    {
        System.out.println("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 tigger = new Cat ("Tigger", 5 , 7);
        
        tigger.walk();
        tigger.walk();
        
        tigger.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 tigger = new Cat ("Tigger", 5 , 7);
    
        tigger.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 doing unrealistic things.

Exercise 1

Accessor and mutator methods

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 void display()
    {
        System.out.println("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;
    }
    
}

Specifying the return types of methods

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.

Using the accessor methods from the main()

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 tigger = new Cat ("Tigger", 5 , 7);
        Cat tom = new Cat ("Tom", 10, 10);
    
        System.out.println (tigger.getName());
        System.out.println (tigger.getAge());
        System.out.println (tom.getWeight());
    }
}
Note how we use commands such as:
System.out.println (tigger.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.

Mutator methods

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.");
        }
    }
}

Passing Parameters to Methods

You have already seen the concept of passing parameters to a constructor. In Introduction to Programming and Problem Solving, you also passed parameters to functions. Hopefully it should not then be too much of a surprise that you can also pass parameters to regular Java methods. If we take a look at another version of our 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 (int distance)
    {
        this.weight -= distance;
    }

    public void display()
    {
        System.out.println("Name: " + this.name + " Age: " + this.age + " Weight: " + this.weight);
    }
}
Note how the walk method now takes one parameter, representing the distance walked. 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). This could be called in a main() as follows:
public class CatAppWithParameters
{
    public static void main (String[] args)
    {
        Cat tigger = new Cat ("Tigger", 10, 10);
        
        tigger.walk (5);
        tigger.display();
        tigger.walk (3);
        tigger.display();
    }
}
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.

Multiple parameters

A method can have more than one parameter. This version of the Cat class has a modified walk() method which makes the cat lose more weight if they walk faster (the weight is reduced by the distance multiplied by the speed):

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, int speed)
    {
        this.weight -= distance*speed;
    }

    public void display()
    {
        System.out.println("Name: " + this.name + " Age: " + this.age + " Weight: " + this.weight);
    }
}
This could then be called in the main() as follows:
public class CatAppWithMultipleParameters
{
    public static void main (String[] args)
    {
        Cat tigger = new Cat ("Tigger", 10, 15);
        
        tigger.walk (5, 1);
        tigger.display();
        tigger.walk (3, 2);
        tigger.display();
    }
}
The cat initially walks a distance of 5 units with a speed of 1, and then walks a distance of 3 units with a speed of 2. So for the first walk() the cat will lose 5 units of weight and for the second walk the cat will lose 6 units of weight.

Exercise 2

  1. Modify your Cat's eat() method further, so that it now takes a parameter of amount. The weight should increase by the specified amount.
  2. Go back to your Hero class from last week. Make the attributes public and try setting the lives to an unreasonable value, such as -1. Does it work? Now make the attribute private again. Does it still work or does the compiler give an error?
  3. Add accessor methods to the Hero class to read the score and the number of lives.
  4. Modify the loseLife() method inside Hero to prevent the lives ever going below zero.
  5. Add a boolean isAlive() method which returns true or false depending on whether the lives are zero, or more than zero.

More advanced questions

  1. (More advanced) Write a Clock class to represent a clock. It should have:
  2. Return to your Hero class and: