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.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.