Encapsulation #
Encapsulation is a (vague) principle in object-oriented programming that refers to “bundling” data with the code that operates on it, and restrict access to this code and data from other components of a system.
From Wikipedia: “Essentially, encapsulation prevents external code from being concerned […]”
Each component hides its internal logic by exposing only data and methods that other components may need.
Example. As we saw earlier, in our game, the “view” component (which is in charge of rendering the game on screen) may buffer the game snapshots that it receives from the backend, if these snapshots are received faster than they can be displayed.
As a buffer, this component uses a structure called a queue. This queue is not exposed to other components, because they do not need to see it, and (most importantly) should not modify it. In other words, this queue is an implementation detail, internal to the “view” component.
Encapsulation can have many benefits. Among others:
- Easier debugging. If our queue is internal to the “view” component, then we know that it cannot be responsible for the malfunction of another component.
- Easier collaboration. Carol may refactor the implementation of the “view” component, knowing that this will not affect Alice, who is currently working on the backend.
This is why a common practice in object-oriented programming consists in hiding all attributes and methods of a new class by default. And make accessible only the ones that need to be (in particular, this is likely to be the default behavior of your IDE).
Encapsulation also largely dictates how libraries are structured.
For instance, when you create a String
in Java, you do not have access to the internal representation of the string object.
in Java #
Each attribute or method of a class can have an access modifier, which specifies which other classes can access it.
For instance, the keywords private
and protected
below are access modifiers.
private int myAttribute;
protected int myMethod(){
return 1;
}
Definition. There are four levels of access in Java:
private
restricts access to the current class,- “package-private” relaxes
private
by also allowing access from the folder of the current class (in Java, a folder for source code is called a package), excluding subfolders,protected
relaxes “package-private” by also allowing access from the subclasses of the current class,public
does not restrict access.
Warning. There is no keyword for the “package-private” level. Instead, this is the default level for an attribute or method without access modifier. For instance, in the example below, the attribute
myAttribute
is package-private:
int myAttribute;
Here is a recap table from the Oracle tutorials:
keyword | class | package | subclasses | world |
---|---|---|---|---|
private |
yes | no | no | no |
none | yes | yes | no | no |
protected |
yes | yes | yes | no |
public |
yes | yes | yes | yes |
Warning. A method declared in an interface is (implicitly) public.
Warning. If a method m1 overrides (or implements) a method m2, then m1 must be at least as accessible as m2.
The following program does not compile. Can you see why, and how to fix this?
├── Run.java
└── units
├── Unit.java
└── impl
└── Unicorn.java
public abstract class Unit {
static String configFolder = "path/to/config";
}
public class Unicorn extends Unit {
String name;
public Unicorn (String name){
this.name = name;
}
public static String getConfigFilePath (){
return configFolder + "/unicorn.properties";
}
}
public class Run {
void testUnicorn(){
Unicorn myUnicorn = new Unicorn("Storm");
myUnicorn.name = "Tornado";
}
}
Unicorn.getConfigFilePath
tries to access the package-private attributeUnit.configFolder
(it should be made protected of public),Run.getConfigFilePath
, tries to access the package-private attributename
ofmyUnicorn
(it should be made public).
Hint. Your IDE may suggest how to fix such compilation errors.
To improve encapsulation, it is good practice to restrict access whenever possible (i.e. without compromising compilation).
Hint. As a rule of thumb, in Java:
- use
private
by default for all attributes and methods that you create, and- if the program does not compile, then use your IDE to relax access.
Encapsulation in this program can be improved. Can you see how?
├── Run.java
└── units
├── Unit.java
└── impl
└── Unicorn.java
public abstract class Unit {
public int health;
public Unit(int health) {
this.health = health;
}
public void attack(Unit defender){
health -= defender.health;
defender.health = -health;
}
}
public class Unicorn extends Unit {
public Unicorn (){
super(1);
}
@Override
public void attack(Unit defender){
regen();
super.attack(defender);
}
public void regen(){
health += 1;
}
}
public class Run {
void testUnicorn(){
Unicorn u1 = new Unicorn();
Unicorn u2 = new Unicorn();
u1.attack(u2);
}
}
Unit.health
can be made protected,- the constructor of
Unit
can be made protected, Unit.attack
can be made protected,Unicorn.regen
can be made private.
Note. The constructor of an abstract class can always be made protected (since it can only be called in the constructor of a subclass).
Getters and setters #
For attributes, the notion of “access” can be refined. An attribute may be:
- neither visible nor modifiable, or
- only visible, or
- only modifiable, or
- both visible and modifiable.
This can be achieved with private attributes and so-called “getter” and “setter” methods.
For instance, in the following class,
the attribute health
has public visibility but is not modifiable.
public class Unicorn {
private int health;
public int getHealth(){
return health;
}
}
Conversely, in the following class,
the attribute health
can be modified but is not visible.
public class Butterfly {
private int health;
public void setHealth(int health){
this.health = health;
}
}
Hint. Getter and setter methods can be automatically generated by your IDE.
To go further: inheritance and encapsulation #
Composition #
Example (from Effective Java, Item 18).
Consider a class
MyHashSet
that extends Java’sHashSet
functionalities by keeping track of the number of times something has been added to the set (as opposed to the output ofHashSet.size()
, which returns the number of elements remaining in the set).This class
myHashSet
may have a private attributeint counter
(initialized to0
) that keeps track of the number of elements added to the set so far. And the class may be implemented by overridingadd
andaddAll
in the expected way, i.e.:public class MyHashSet extends HashSet { private int counter; ... @Override public boolean add(E e){ counter++; return super.add(e); } @Override public boolean addAll(Collection<? extends E> c){ counter += c.size(); return super.addAll(c); } }
However, this implementation of
addAll
would count every insertion twice, because the implementation ofHashSet.addAll
calls the methodadd
(which is overridden in this case).
A design pattern called composition can be used to avoid such unintended effects.
Intuitively, instead of extending the original class, use an instance of the original class as a (private) attribute of the new class (e.g.
an instance Hashset
named set
in this example).
However, this requires re-implementing all methods of the original class (albeit in a straightforward way), for instance:
public class MyHashSet {
public boolean isEmpty(){
return set.isEmpty();
}
}
Prevent overriding or inheritance #
As show by the example above, in order to improve encapsulation, one may want in some scenarios to forbid overriding a method or extending a class.
In Java, this can be enforced with the keyword final
, for instance:
public final class NonExtensibleClass {
...
}
public class MyClass{
public final void nonOverridableMethod(){
...
}
}
Warning. In Java, a variable can also be declared
final
. But this has a different meaning.