Skip to main content

Section 8.5 Encapsulation

Object-oriented programming languages like Java provide means to enforce encapsulation, that is to hide the internals of a data structure. Our Vec2 example from the previous section gives a reason why we would want to hide the internal workings of a data structure. Let us consider another class, where Vec2 objects are used:

public class Rectangle {
  Vec2 upperLeft, lowerRight;
  ...
  double area() {
    double dx = lowerRight.x - upperLeft.x;
    double dy = lowerRight.y - upperLeft.y;
    return Math.abs(dx * dy);
  }
}

The class Rectangle appears to model a rectangle and has two fields, upperLeft and lowerRight, that store the upper left and the lower right corners of the rectangle. The method area computes the rectangle's enclosed area.

At the moment, Vec2 uses cartesian coordinates. The implementation of area in Rectangle explicitly relies on this choice: it accesses the x and y members of Vec2. But what if the author of the Vec2 class wants to adjust the internal representation of a point, for instance by using polar coordinates instead? Then, all code that directly refers to the member fields of Vec2 would break. In fact, by referring to the fields of Vec2, code of other classes makes itself dependent on the internal implementation details of Vec2.

To prevent such a dependency and to decouple the individual classes of a system, we encapsulate classes by hiding internal information of a class from the rest of the system.

public class Vec2 {
  private double x, y;

  public double getX() { return x; }
  public double getY() { return y; }
  ...
}

In Java, every declaration of a class, a field, or a method can be prepended with one of the keywords private, protected, and public. These keywords restrict the visibility of the declared identifiers. Method and field identifiers that are declared private can only be used in the class that contains the declaration. This would prohibit access to the x and y fields of the Vec2 class in methods of the class Rectangle. public signifies an unrestricted visibility. Table 8.5.1 summarizes all visibility modifiers of Java.

Table 8.5.1. Visibility modifiers in Java
same class same package subclass otherwise
public \(\checkmark\) \(\checkmark\) \(\checkmark\) \(\checkmark\)
protected \(\checkmark\) \(\checkmark\) \(\checkmark\)
none \(\checkmark\) \(\checkmark\)
private \(\checkmark\)

The data represented by an object are then accessed via accessor methods (commonly called “getters” and “setters”). They allow us to change the internal representation of the class without the need to adjust other classes. The accessor methods always provide the same view on an object “to the outside”. If we now modify Vec2 to use polar coordinates, only the accessor methods getX and getY need to be adjusted. Furthermore, there is benefit in using the accessors within the declaring class as well. In our example, we then do not need to adjust other methods whose implementation uses cartesian coordinates:

public class Vec2 {
  private double r, phi;

  public double getX() { return r * Math.cos(phi); }
  ...
  public void setX(double x) {
    phi = Math.atan2(getY(), x);
    r   = x / Math.cos(phi);
  }
  ...

  public void translate(double dx, double dy) {
    setX(getX() + dx);
    setY(getY() + dy);
  }

  public double length() {
    return r;
  }
}

Ensuring Class Invariants.

Encapsulation also helps to ensure that invariants among the fields of a class (so-called class invariants) are preserved. Consider the following class, which models fractions:

public class Fraction {
  private int numerator, denominator;
  ...
  public setDenominator(int d) {
    assert d > 0 : "Denominator must be > 0";
    this.denominator = d;
  }

  public boolean isPositive() {
    return numerator >= 0;
  }
}

The programmer appears to base their implementation of the class Fraction on the fact that the denominator is always greater than zero. Thus, “d \(> 0\)” is an invariant for all Fraction objects. If this invariant was violated, the code of the class would not work correctly. For instance, the method isPositive would yield an incorrect result for denominator \(< 0\) and numerator \(< 0 \text{.}\) With a setter method, we can ensure that the object never reaches an invalid state by raising a suitable exception when the invariant would be violated.

Encapsulating Constructors.

Sometimes, we also want to encapsulate object construction and forbid other classes' code direct access to the constructor(s) of a class. In Java, this can be achieved by setting the constructor private. Object construction has then to be performed by a method. Typically, in a class with private constructors there is a static factory method that creates new objects of the class using the private constructor, like in the following example:

public class Vec2 {
  private double x, y;

  private Vec2(double x, double y) {
    this.x = x;
    this.y = y;
  }

  public static Vec2 cartesian(double x, double y) {
    return new Vec2(x, y);
  }

  public static Vec2 polar(double r, double phi) {
    return new Vec2(r * Math.cos(phi), r * Math.sin(phi));
  }
}

Here, the methods cartesian and polar serve the purpose of constructing a Vec2 in different ways. polar transforms the polar coordinates into cartesian ones and then calls the constructor while cartesian just passes its arguments through. Both methods also carry a sensible name that makes it possible for the programmer to understand how the parameters of the object construction is interpreted (cartesian vs polar).

Other very common application of private constructors are:

Controlling the number of objects of a an immutable class (see Section 8.6)

For example, nothing prevents us to create multiple distinct objects that all represent the Vec2 \((1, 0)\text{.}\) Sometimes one wants to make sure that there is only one object for a specific value. In that case, a factory method could look up in a map if an object was already created for a specific set of parameters and return this object instead of creating a new one.

An extreme variant of this case is the singleton where one wants to permit only one object per class.

Delegating the construction of the object

Sometimes, it turns out that for specific combinations of parameters to the constructor, an object of another (sub-) class is better suited. In this case, a factory method can select which object to create.