1.3-Object-Georienteerd-Programmeren

Week 1: Encapsulation

Competency: I understand the concept of encapsulation and can apply the relevant associated concepts (public / private, getters and setters, constructors).

Introduction

One of the pillars of object-oriented programming is encapsulation (from the verb to encapsulate which means to encase or to wrap). In this lesson we will learn what encapsulation means and what it should mean to you as a programmer. And obviously, how you can ensure that your classes are encapsulated correctly and especially: why do you want to encapsulate classes?

To properly explain encapsulation, it is necessary to know a little more about (the design of) classes and the attributes that are included in the classes. Encapsulation serves to ensure that the state of an object (an instance of a class) remains valid at all times. But what are we talking about when talking about the state of an object? And when is a state “valid”? And is there such a thing as an “invalid” state?

In short: Before we can talk about encapsulation, we must first talk about the state of objects.

State of objects

The state of an object is determined by how the class is defined and more importantly: the values of the attributes in the object. Let’s use the class Person as example:

public class Person {
    String name = "";
    int age = 0;
}

In this class you see two attributes (name and age) and together this information represents a person. Now assume that these two attributes are mandatory, as they are required to perform a certain task (eg. find the youngest person and print their name). Normally you would write the following code:

Person p = new Person();
p.name = "John Smith";
p.age = 25;

With this code, we have given the newly created Person object an initial state. The object now represents a 25 year old person named “John Smith”. It’s not hard to imagine this person could actually exist, since the chosen values for the attributes name and age are semantically correct (25 is a valid age, “John Smith” is a valid name). In object-oriented programming we therefore speak of that this object is in a valid state, all values have been entered with valid values and have an understandable meaning.

Obviously mistakes can be made! Take this piece of code for example:

Person p = new Person();
p.name = "J|_+a%$@neHa42#@@#rrison";
p.age = -15;

In this case, it is easy to spot that something might have gone wrong. Although both the name and age values are valid in terms of data type, semantically (what the attribute represents in this class) they are illogical! The name contains invalid characters (for the most places in the world) and what exactly does a negative age represent? Does that mean this person is not yet born? Or is this a programming error?

Let’s say that within our program, with which we want to find a youngest person from a list of persons, the name is still acceptable (after all, it is a valid String), but the age might cause problems: This person will probably always be the youngest person to be found. Of course, other functions of this application like calculating an average age might have similar problems.

Within object-oriented programming this scenario becomes a problem, because this Person object has ended up in an invalid state. It contains values that are illogical (or just shouldn’t be) and could potentially bug the rest of your application. And we want to prevent invalid states as much as we can.

(Technically there are two reasons to reach an invalid state, e.g. the values don’t make any sense or due to the fact that values that are not filled in. We’ll talk more about this second problem later!)

You may understand that invalid states of objects is an undesirable thing in programming. Therefore, as a programmer you should always be vigilant about the state of your program and you must at all times try to prevent that a state becomes invalid, either by your own, your fellow programmers or your end users fault. It is your job to make sure errors are prevented as much as possible. For example, think about the age problem. A simple age check (if (age <0) {// ERROR!})) is sufficient in most cases to solve these kinds of problems.

Thinking about your own software in this way is the core of encapsulation. You are going to encapsulate certain parts of your code (e.g. your attributes) with a layer of protective code to prevent your program from getting into an invalid state.

Encapsulation as a protective layer

So how do you actually prevent your program from entering an invalid state? Let’s take a look at this with an example:

We will use the Person class from the previous example:

public class Person {
    String name = "";
    int age = 0;
}

Suppose you are now asked to write a piece of code to add a new instance (because a baby is born, for example) to a program, you can do that with:

Person newbornBaby = new Person();
newbornBaby.name = "John";
newbornBaby.age = 0; // No birthdays yet!

SaxionApp.printLine ("Baby " + newbornBaby.name + " is currently " + newbornBaby.age + " years old!");

There is nothing wrong with this code: You create a new instance of the class Person and give it some initial values. But what if this baby gets older? It is fairly easy to understand that once the baby’s birthday has passed, you will need to increase the value of the age attribute. You can do this like:

p.age = p.age + 1 // Or: p.age ++;

But just as easily as you can make someone a year older, you can also make this person a year younger (p.age--;). Or instead of the person becoming just 1 year older on their birthday, someone can suddenly become 10 years older due to simple typo! Or someone can even enter a negative age .. which can get your object into an invalid state!

All these problems have a similar root problem, the uncontrolled access to the age attribute. Whether you are working in the Person class or in a part of your program where you use a variable with type Person: you can always change the value at age without any control. Nothing is stopping you to cause problems or checking if you (perhaps) made a mistake!

And that brings us right to a first solution: protecting attributes. We will prevent direct access to the variable age by including some code as a protective layer.

Private and public attributes

The problem that you can make an age negative originates from the fact that you can adjust the age attribute directly from your software. You can do this from the code within the class itself, but also from any other point where you use the class in your code. And this is exactly the problem: as a developer of the class Person you have no control over what exactly happens with the value of age!*

(* You could make the argument that this is not your problem since “the other programmer” just has to pay more attention, but do understand that this is not actually a solution, but just moving around responsibility. And if this problem can be resolved by you, why wouldn’t you? In this course, we’ll show you how to do this properly!)

The easiest solution to prevent the value of age from being changed to something invalid, is to simply prevent this value from being changed by others at all. So we want to protect the attribute age from the outside world (all code that is not in the class Person) and we can do this by making the attribute private. A private attribute is an attribute that can not be used from outside the class.

You create a private attribute by literally putting the word private in front of the attribute:

public class Person {
    String name = "";
    private int age = 0;
}

(We intentionally left the name attribute public for a while to show the difference. However, you can imagine that this attribute may also needs to be protected!)

By adding the word private you instruct the compiler to make sure that the attribute age cannot be changed directly anymore. You will be able to see the result immediately if you are using IntelliJ and look at the error message:

Access-denied

This way we have solved our previously posed problem and the object will no longer be able to get into an invalid state due to its age, since you now have no access to the attribute age at all (unless you write code yourself in the Person.java file)! Note however, that we also no longer can read the value of age for printing. The choice to be able to access the attribute from outside the class is hard: you can either allow to see the attribute and thus automatically read and change it or you can not allow anything. To make an attribute public or private, just add the words public or private before an attribute in your class. This is also known as a access modifier. (Until now we have never put anything there, you can now compare this with public.) In Java, there is no “read-only” option for an attribute. So it is either “all” or “nothing” in terms of access to attributes.

But how do you make sure that you can do something with the attribute age? How can you read it’s value and use that value in your program, without it being immediately adjustable? The answer is simple: with methods. We are going to extend the class Person with a number of methods to provide (controlled) access to these attributes.

Given our previous example, we need two pieces of functionality to finish what we started, namely being able to increase the age by 1 (if someone’s birthday) and request the value so that we can print it.

This can be implemented as:

public class Person {
    String name;
    private int age;

    public void haveBirthday() {
        age++;
    }

    public int getAge() {
        return age;
    }
}

(Explanation: The haveBirthday() method has no parameters, so every time the method is called it will increment the value of the age attribute of this instance by 1. This behavior always remains the same for each call. The method getAge returns a copy of the value of the variable age meaning that the original value cannot be altered. Code like like newbornBaby.getAge() = 13; does not affect the state of our object!)

We do need to update the previous code to use these new methods!

Person newbornBaby = new Person();
newbornBaby.name = "John";

// We cannot do anything with age directly!
SaxionApp.printLine ("Baby " + newbornBaby.name + " is currently " + newbornBaby.getAge() + " years old!"); // John is by default 0 years old.

newbornBaby.haveBirthday();

SaxionApp.printLine ("Baby " + newbornBaby.name + " is currently " + newbornBaby.getAge() + " years old!"); // John is now 1 year old!

We have now created a class Person that manages the variable age and will ensure that this attribute cannot be given an invalid value (just check: can you still enter a negative age? Or make a person younger?).

However, we are not quite done with the Person class yet. A new person always has an age of 0 when created. This is fine for babies, but it is easy to imagine that we also want to add people who are not just born to our system. And having to make a loop to call the haveBirthDay() method 25 times, for a person of 25 years old, just feels wrong!

We also still have a problem with the name of a person: this attribute can also be adjusted from the outside (which we most likely do not want), but worse: It is also possible that someone forgets to enter a name at all. In that case the person will be called “” (empty String), which is not allowed (in most cases).

It is therefore time to look at the next part of encapsulation: Using constructors.

Constructors

Until now we have taught you to specify default values for all attributes so that if an attribute is not overwritten, something could always be displayed. This is however not the proper way to set default values: Sometimes an attribute is optional and that attribute does not have to have a value in itself (e.g. a phone number) and sometimes you want an attribute always to have a valid value because otherwise the instance cannot be used properly (e.g. the name of a person in our previous example).

You can control and influence this behavior by using constructors. You can best think of a constructor as a special method that is called when a new instance is created. Just like normal methods you can also give constructors parameters that you can then use to provide the attributes with a value.

Suppose we want to ensure that whenever a Person instance is created, a name must be entered, we can create a constructor like this:

public class Person {
    private String name;
    private int age;
    
    public Person(String providedName) {// <--- THIS IS THE CONSTRUCTOR
        name = providedName; // Read as: Overwrite the variable name from this Person instance with the value of providedName
    }
    
   public void haveBirthday() {
       age++;
   }

   public int getAge() {
       return age;
   }

   public String getName() {
        return name;
    }
}

(Note how the constructor differs from a normal method: no return type is specified and the method name is always the name of the class itself.)

For the sake of completeness, we also made the attribute name private and the Person class has been provided with a method to retrieve the value of the persons name attribute. You can say that the attribute has now been made functionally read-only as it is no longer possible to change the value of name after the instance is created.

If we look at our code example you will also see that the way we now have to create a Person instance has changed. The total example now looks like this:

Person newbornBaby = new Person("John"); // Note that the value of "John" is passed to this instance of Person and cannot be changed after setting it like this!
// We cannot do anything with name directly!
// We cannot do anything with age directly!

SaxionApp.printLine("Baby " + newbornBaby.getName() + " is currently " + newbornBaby.getAge() + " years old!"); // John is by default 0 years old.

newbornBaby.haveBirthday();

SaxionApp.printLine("Baby " + newbornBaby.getName() + " is currently " + newbornBaby.getAge() + " years old!"); // John is now 1 year old!

Note in particular that the name (“John”) is now mandatory when creating the instance. If you forget this you will (of course) receive an error message:

Constructor-error

It is now no longer possible to create a Person instance without a name. Whenever you use this constructor, a name must be provided.

Getters and setters

The last part of encapsulation that needs to be covered is the use of “getters” and “setters”. This term is used to indicate a group of methods that just have one function: being able to (controlled) read out attributes (getters) or (controlled) adjust the values of attributes (setters). On this page, you have already seen two getters already, namely getAge() and getName(). Note that the names of “getters” in general always start literally with the word get, while “setters” are prefixed with set. This is not so much required for your code to work, but is very common and increases readability.

A setter is a method that can override a certain value of an attribute. In the previous example we could have included a method setAge(..) in the class Person that allows us to set the age of an individual person. This “setter” can look like:

public void setAge(int newAge) {
   age = newAge;
}

Of course, thanks to this method, we can also perform a check for invalid entries (i.e. a negative age):

public void setAge(int newAge) {
    if (newAge > 0) {
        age = newAge;
    } else {
        // ERROR
    }
}

When using getters and setters you should always ask yourself if you want them at all. In our earlier example, we had stated that an age could only be increased in steps of 1 year. (And if we want an age other than 0 to start, we might as well adjust the constructor!) That’s why we deliberately chose not to include this setter (setAge (int newAge)) in our program.

Intermezzo: using “this” in your own class

If you are looking for code samples (or you have getters / setters generated automatically) you will probably often see examples like:

public void setAge (int age) {
   this.age = age;
}

The word this is added here to distinguish between the argument in the method (named age) and the age attribute from the Person class. After all, the same name is used for both the argument and the attribute. The word this ensures that you refer to the attribute of the class). So you should read this method as: Change the value of the age attribute of this object to the value of the argument age.

Intermezzo: Introduction class diagrams

This week you learned about the added value of adding methods to your own classes and you’ve also seen the necessary class methods, think getAge(), haveBirthday(), etc. At the moment, keeping track is still manageable since most programs we write have only one or two (or a few more) classes.

If your program gets bigger, however, you can probably imagine that this overview will be harder to keep track of. Therefore, in this module we are going to introduce you to class diagrams. A class diagram is a visual representation of how a class is constructed. For example, the class Person from earlier looks like this:

The attributes (name and age) are always listed first, followed by the methods (split with a separator). On each line there is also a + and a - sign that represents for whether an attribute / method is public (+) or private (-).

For attributes, besides the name and access modifier, you only store the data type. For methods, you name both the arguments and the return type (e.g. the method signature). Constructors are often included as methods, but this can still vary from one drawing tool to another.

We want to teach you this quartile to use class diagrams and show you why they are useful. You may come across assignments where a class diagram is given and not much else is explained: in these assignments you have to implement the class diagram yourself. Sometimes we give only the public interface of an assignmen (which is an overview of just the public methods). We will come back to what exactly a public interface is later, but for now you can remember that with such assignments you have to come up with all the private attributes and methods yourself.

Conclusion

It’s a common (and easy) mistake to create too many getters and setters in your class too quickly. Therefore, use the following guidelines when designing getters / setters for your own classes:

Check per (private) attribute. 1) whether the value of this attribute should be readable: If so, you have to create a getter. If not, omit the getter. 2) whether the value of this attribute should be modifiable: If so, you need to create a setter. If not, you omit the setter. 3) If you have decided that a value is editable, check for yourself if there are any checks to be done on the values before editing it.

(Of course you also have the question: “If an attribute has to be readable and editable and you don’t want to check it, why make the attribute private?”)

Making the right choice remains tricky. It is important to take a moment and think about each attribute usages, and what other programmers in your team might want to use (or abuse) this attribute for. As an example we show a complete example of the Person class, showing a more expanded solution.

Relevant videos

Encapsulation

Primitive vs Reference types

Constructors

Classes and objects

Note: The video below has been borrowed from an earlier iteration of the course. The assignment mentioned at the end has been replaced by other assignments. (So you don’t have to look for it!)

An overcomplete example: A fully developed Person class.

The techniques you learned above will enable you to write a full-fledged class that can be used by multiple developers in a “safe” way. Because we made some choices in the previous examples that you may (or may not) agree with, we’ll show you a different person class we could well imagine being used in a system that the municipality can use to keep track of which residents live in the municipality.

The system must therefore be able to take into account new people being born, but also that people die. Furthermore, people must be able to marry another person and possibly choose to adopt a different surname. All these things together give the class Person a lot of responsibilities! We will now show you a possible elaboration to study.

public class Person {
    private String firstName;
    private String lastName;

    private LocalDate birthDate;
    private LocalDate dateOfDeath;

    private Person registeredPartner;

    public Person(String firstName, String lastName, LocalDate birthDate) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.birthDate = birthDate;

        dateOfDeath = null; // We set this explicitly to null for your understanding. This is however redundant.
        registeredPartner = null; // Same..
    }

    public String getFirstName() {
        return firstName;
    }

    // Note: no setters for first name!

    public String getLastName() {
        return lastName;
    }

    // Suppose someone wants to change their last name in case of marriage (or something else). - Setter allowed!
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    
    public void marry(Person otherPerson) {
        if(registeredPartner == null) {
            registeredPartner = otherPerson;
        } else {
            // Uhm.. this is awkward and unhandled for now!
        }
    }
    
    public void divorce() {
        registeredPartner = null; // Explicitly remove the link to the other person.
    }

    public void reportDead() {
        dateOfDeath = LocalDate.now();
    }

    /**
     * Note that this is not actually a getter, but a computed value based on the person's date of birth.
     * If the person has died, the age of the person at the moment of death is returned.
     * @return the number of years the person is / was alive (for)
     */
    public int getAge() {
        if(dateOfDeath == null) {
            return Period.between(birthDate, LocalDate.now()).getYears();
        } else {
            return Period.between(birthDate, dateOfDeath).getYears();
        }
    }

    @Override
    public String toString() {
        return firstName + " " +  lastName;
    }
}

Videos from our archives

The following videos are from our archive and don’t quite match what you need to do in terms of code examples, but are still very informative!

Classes and objects

Example classes and objects