Version without JavaScript, read-only: workshops, quizzes and running code are not available here. Full version · Catalogue · Sign in
Lecture notes · chapter 7
Encapsulation and invariants
Making an attribute private is not enough to protect an object. Encapsulation serves to guarantee invariants, that is, properties that must remain true.
This chapter extends sequence 4.
What encapsulation protects
The purpose of encapsulation is to guarantee properties that must remain true throughout the object's life: a price is never negative, a name is never empty, a position stays within the pond. These properties are called invariants, and encapsulation makes it possible to enforce them.
public class CanardEnPlastique {
private String nom;
private int prix; // in cents
private double x, y; // position in the pond
public CanardEnPlastique(String nom, int prix) {
setNom(nom); // the constructor goes through the same safeguards
setPrix(prix);
}
public String getNom() { return nom; }
public int getPrix() { return prix; }
}
Changes go through methods that check the values:
public void setNom(String nom) {
if (nom == null || nom.trim().isEmpty())
throw new IllegalArgumentException("le nom est obligatoire");
this.nom = nom;
}
public void setPrix(int prix) {
if (prix < 0)
throw new IllegalArgumentException("prix >= 0");
this.prix = prix;
}
Getters and internal collections
public class Troupe {
private final List<Canard> canards = new ArrayList<>();
public List<Canard> getCanards() { return canards; } // ← the leak
}
The attribute is private and final, and yet any code can write troupe.getCanards().clear(). The invariant “a troupe has between three and eight ducks” is then no longer guaranteed.
The same problem arises for any mutable object returned as is. This is the final trap seen in chapter 2: the reference can no longer change, but the referenced object can.
Visibility levels
| Scope | Who sees |
|---|---|
private | the class alone |
| (nothing) | the package |
protected | the package and the subclasses |
public | everyone |
Making a method public is a commitment: once published, it is used by code you do not control, and it can no longer be removed. It is therefore better to start with private and widen the visibility when necessary.
Immutable objects
The simplest form of encapsulation is to make the object immutable: if it never changes, its invariants need no protection after construction.
public final class Accessoire {
private final String type; // "chapeau", "cape", "bouée"
private final String couleur;
public Accessoire(String type, String couleur) {
this.type = type;
this.couleur = couleur;
}
public String description() { return type + " " + couleur; }
}
The final on the class prevents creating a subclass that would add a mutable field. The final on the fields fixes them at construction, and the class has no mutator. Such an object can be shared without precaution, placed in a Set without risk, and read by several threads without a lock.
It is often the right choice for values (a colour, an amount, a coordinate), as opposed to entities, which have an identity and change over time.
Abstract classes
An abstract class states what all its subclasses will have to be able to do, without specifying how.
public abstract class CanardAbstrait {
private final String nom;
public CanardAbstrait(String nom) { this.nom = nom; }
public String getNom() { return nom; }
public abstract void cancaner(); // a promise, not an implementation
public void translater(double dx, double dy) { /* shared code */ }
}
You cannot write new CanardAbstrait("..."), since cancaner() has no implementation. Each subclass provides its own.
Abstract class or interface? An abstract class provides state and shared code, and a class can inherit from only one. An interface only describes a role, and a class can implement as many as it likes. When in doubt, the interface is almost always the right choice (see chapter 5).
Common mistakes
Exam question
A field is
private finaland the class has no setter. Is the object protected?
Not necessarily. final prevents changing the reference, but not modifying the referenced object: if the field contains a mutable object received from outside, or returned by a getter, anyone can modify it. To protect it, you must make a copy on the way in and on the way out, or use an immutable type.