Encapsulation
A field that is declared as public allows developers to write code that reads and writes its value directly, which may result in data corruption and loss of integrity, even though relevant safe operations could be provided by the application. The issue is that a developer is not forced to invoke such operations if direct access to data is allowed due to lack of encapsulation.
public class SyncCounter {
public int value; // (1)
public synchronized void increment() { value++; }
public synchronized int getValue() { return value; }
}
In the preceding code, the variable value at (1) should be private; otherwise, no enforcement of memory integrity would be possible. This principle is applicable to any operation—not just those that enforce synchronization (§22.4, p. 1387), but also those that validate values, or transform values, or perform any additional actions besides just setting or getting values.
Data Integrity
Restricting access to data via methods provides additional opportunities to improve overall program integrity. Operations can be used to validate values, and guard against erroneous values, such as number overflow (§2.8, p. 59). Consider using methods that guard against such overflows. It may be better for a program to throw an ArithmeticException, rather than silently overflow a numeric value.
int x = Integer.MAX_VALUE;
int y = x + 1; // (1)
int z = Math.addExact(x, 1); // (2)
This example attempts to add 1 to the maximum int value. The code at (1) causes a value to overflow and wrap around (§2.8, p. 59), and the code at (2) throws an ArithmeticException when overflow occurs.
Similarly, a floating-point operation may result in positive or negative infinity, or result in Not-a-number (NaN) when attempting floating-point division by zero. Developers are advised to guard against such erroneous cases using simple boolean verification of operation results.
boolean veryLargeNum = Double.isInfinite(1/Double.MIN_VALUE); // (1)
boolean nan = Double.isNaN(0.1/0); // (2)
The code at (1) checks if the operation result is an infinitely large number, and the code at (2) checks if the operation result is NaN.
Robustness
Finally, it is worth considering guarding against references with a null value using an Optional object (§16.6, p. 940).
Optional<String> o = computeOptionalResult(); // (1)
String s = null;
if (o.isPresent()) { // (2)
s = o.get();
}
s = o.orElse(“Default”); // (3)
s = o.orElseThrow(); // (4)
The method at (1) returns an Optional object that can encapsulates an actual value. The caller is forced to check what value is in the Optional object and take appropriate action. Here are some example of such actions:
- Check whether the value is present before attempting to retrieve it from the Optional object.
- Substitute a default value if a value is not present in the Optional object.
- Throw a NoSuchElementException if the value is not present in the Optional object.
The common requirement for all of these cases to work is proper encapsulation to ensure that relevant validations and value corrections are actually applied by appropriate methods.
Extensibility
An example of a loss of semantic cohesion when extending classes is when a developer extends a class and overrides a method in such a way that the overriding method breaks semantic assumptions imposed by the supertype method (§5.1, p. 196). This may lead to erroneous interpretation of the behavior when using polymorphism. Other developers may expect the invoked method to adhere to certain semantics, but instead the invoked method would exhibit unexpected and possibly erroneous behavior.
Immutability
In order to avoid code corruption issues, consider using immutable design, declaring all variables that do not change value as final (§5.5, p. 225), as well as declaring classes and methods as final to prevent them from being overridden in all cases where extensibility is not required, or if certain method semantics are critical for application functionality.
public class Authenticator {
public boolean authenticate() { // (1)
/* Perform authentication logic. */
}
}
The method or even the entire class above could be made final to ensure that no other class can extend this class or override its methods. This would prevent overriding method implementations that can disable or compromise the actual authentication process. This principle is also applicable in many other cases, such as operations that validate values, ensure memory integrity, and so on.