Table of contents
- Item 15: Minimize the accessibility of classes and members
- Item 16: In public classes, use accessor methods, not public fields
- Item 17: Minimize mutability
- Item 18: Favour composition over inheritance
- Item 19: Design and document for inheritance or else prohibit it
- Item 20: Prefer interfaces to abstract classes
- Item 21: Design interfaces for posterity
- Item 22: Use interfaces only to define types
- Item 23: Prefer class hierarchies to tagged classes
- Item 24: Favor static member classes over nonstatic
- Item 25: Limit source files to a single top-level class
Item 15: Minimize the accessibility of classes and members
A well-designed component hides the API and the implementation. It communicates only via the API which is called encapsulation.
Advantages of encapsulation:
- decouples the components
- faster parallel development, as isolated
- easy on maintenance
- enable effective performance tuning by finding inefficient parts
- increases and promotes reuse
- decreases risk in large systems, even if it fails we have independent reusable parts
Java provides access control mechanism for it. One rule of thumb for efficient encapsulation: make each class or member as inaccessible as possible, i.e using the lowest privilege accessor.
For top-level classes (non-nested) there are two modes:
public
: part of implementation, has to be present and maintained for the lifetime (backward compatibility)- package-private : part of API, gives flexibility can be changed or removed
If a top-level class/interface is used by only one client class, it can be a private static
nested class/interface of the client class.
Both private
and package-private members are part of a class’s implementation and do not normally impact its exported API. These fields can, however, “leak” into the exported API if the class implements Serializable
.
For member of public
classes a huge increase in accessibility is when package-private is changed to protected
. It is part of exported API, must be supported forever and shows implementation commitment detail.
Instance fields of public
classes should rarely be public
. public
mutable fields are not generally thread-safe. Even if it is final
we loose the flexibility of modification. Similar arguments hold for static
fields as well except that constants can be exposed via public static final
, but they should be either primitive or reference to immutable objects. Non-zero length field is mutable and should not be public
or returned by an accessor. You can make the array field private
and return an immutable List
or a copy of the array field.
Item 16: In public classes, use accessor methods, not public fields
public
classes should not expose mutable fields, immutable may be exposed (questionable)- If a class is package-private or is a
private
nested class, there is nothing inherently wrong with exposing its data fields to make code cleaner
Item 17: Minimize mutability
Advantages:
- easier to design, implement and use
- less prone to error and more secure
Five rules to make a class immutable:
- Don't provide methods that modify an object's state (mutators)
- Ensure class can't be extended (
final
class orprivate
/ package-private constructor orstatic
factories) - Make all fields
final
(also thread-safe), some helper or non-core fields may be non-final like caching - Make all fields
private
(may havepublic final
for constants of type primitive or immutable) - Ensure exclusive access to any mutable components
Functional approach -> return result after applying the function to the operand, without modifying. Procedural approach -> like functional approach but modifies the operand.
Immutable objects advantages:
- simple
- inherently thread-safe
- no synchronization required
- shared freely
- no need of defensive copies
- no need of
clone()
or copy constructor (String
has copy constructor from early days, can't remove backward compatibility) - great building blocks for other objects as robust
- consistent
Immutable objects disadvantages:
- a separate object for each state / distinct value
In multiple-step operations costs add up. To minimize this we can provide primitive for the common operations. Nested companion classes can also be used. The companion class can also be public
like StringBuilder
.
If a class implements Serializable
and one or more fields refer to mutable then we must provide an explicit readObject()
or readResolve()
method, or use the ObjectOutputStream.writeUnshared()
and ObjectInputStream.readUnshared()
even if default is fine, because an attacker can create a mutable instance. (More on this in Item 90).
Item 18: Favour composition over inheritance
inheritance -> class extending another class
Unlike method invocation, inheritance violates encapsulation. Subclass depends on the implementation detail of the superclass and may break if the superclass is changed like in subsequent releases. The subclass is not isolated.
Use composition instead -> private
field referencing the instance of the required class (superclass from above).
This can be achieved with two classes:
- Reusable forwarding classes -> stores the reference and contains only the forwarding methods.
- The client class (wrapper class) which extends the forwarding class and overrides the required methods and becomes isolated
Wrapper class design is also known as Decorator pattern.
Delegation = composition + wrapper object passes itself to the internal wrapped object
Wrapper classes can't be used for callback frameworks as the wrapped object doesn't know about the wrapper also known as SELF problem. It is also tedious to write forwarding methods.
Item 19: Design and document for inheritance or else prohibit it
The class must document its self-use of overridable methods. For each public
or protected
method, the documentation must show which overridable (non-final
+ public
/protected
) methods it invokes. Use @implSpec
for it. It does show how besides the usual what, but that's the cost of using inheritance. If you need a common functionality that is in the overridable method then extract it in private
helper method and reuse it.
The overridable subclasses must be tested by subclassing multiple times, not by the class implementor.
The constructor must not invoke overridable methods. It is because in subclass the super constructor is called first, and if the overridable method of a subclass is called from the superclass, then at that time the subclass object is not created and may lead to failure. The constructor may invoke private
, static
and final
methods.
Special care has to be taken if a class implements Serializable
or Cloneable
as they also behave in a much way similar to the constructor.
If a class implements an interface capturing its essence like Set
, Map
then the inheritance can be done.
Item 20: Prefer interfaces to abstract classes
- Since a class can only extend one class, existing classes can easily be retrofitted by implementing a new interface.
- Ideal for defining mixins. Mixin classes denotes the additional responsibility a class has to perform apart from primary type.
- Allow for the construction of non-hierarchical type frameworks. Type hierarchies are good for some cases but in other it is a nightmare. If a hierarchical structure is used and multiple conditions has to be supported it might lead to combinatorial explosion of classes.
Note from author/blogger of this post: The fourth point the book suggests Interfaces enable safe, powerful functionality enhancements. I, personally, don't think that this is an advantage of interfaces over the abstract classes. As the forwarding class can work in same way with an abstract class as with an interface on the lines of Item 18. This needs to be decided upon. Readers may leave the valuable feedback on it.
We can use default methods for implementation assistance. But they should be described using @implSpec
for the purpose of overriding while inheritance if required. There is a shortcoming of default methods in interface is that they can't be used for methods of Object
like hashCode()
, equals()
and toString()
. It is because of class wins rule.
Interfaces can not have instance fields, non-public static
fields except private static
.
The class wins rule can be overcome by a mix of interface and abstract classes. We can provide an abstract skeletal implementation class. The responsibilities shared are:
- Interface : define the type, some default methods
- skeleton implementation : implement remaining non-primitive interface method atop primitive interface methods.
Extending the skeletal implementation takes less work for implementing the interface. It is Template Method pattern. The convention of skeletal class name is AbstractInterface, like AbstractSet
, AbstractMap
etc.
Interface with default methods can also suffice without skeletal class. Skeletal class is a great use case for overriding Object methods in which default interface methods fail. Skeletal classes are incidentally an example of Adapter Pattern in many cases. Try to give good documentation in skeletal class for the client.
Item 21: Design interfaces for posterity
Default methods were introduced primarily for lambdas, and injected to implementations without their consent. It is not always possible to write a default method that maintains all invariants of every conceivable implementation.
In the presence of default methods, existing implementations of an interface may compile without error or warning but fail at runtime. Default methods should be avoided unless absolutely necessary. They are however useful in implementing the interface like in Item 20.
Default methods were not designed to support removing methods from interfaces or changing the signatures of existing methods.
Item 22: Use interfaces only to define types
constant interface is used somewhere to combine constants. It is implemented to use constants. It is a poor use case as it leaks into the implementation and commitment for future use ex java.io.ObjectStreamConstants
. It should be avoided.
Instead non-instantiable class should be used. The client may prefer static
import for simplicity of use.
Item 23: Prefer class hierarchies to tagged classes
Some times class instances come in different flavours. They have a field like tag and works according to its value. Its implementations is stuffed with switch cases to determine the action to perform.
They have many shortcomings:
- cluttered boilerplate code
- reduced readability
- high memory footprint
- adding a new flavour is different and highly coupled implementation
- constructor must set tag and other fields correctly else program fails at runtime
- data type has no clue about the flavour
Hierarchical classes can be used to show the natural relationships, final fields etc.
Item 24: Favor static member classes over nonstatic
Nested class : a class defined inside another class. Their main purpose in their lifetime is to serve the enclosing class.
Types of nested class:
- static member class
- nonstatic member class
- anonymous class
- local class
All (except 1.) are called inner classes.
Inner class usability diagram
Nested classes and some significances:
static
member classes- common use case - public helper class
- non-
static
member class- implicitly associated with enclosing instance
- access enclosing class instance inside instance method of non-static member class using qualified this construct
- impossible to create an instance without an enclosing instance
- increases time and storage cost in instance construction
- can function as an adapter
anonymous class:
- not a member of enclosing class
- simultaneously declared and instantiated
- non-
static
context: access to enclosing instance static
context: access to only constant fields (final
primitive or typeString
that is initialized with a constant expression [JLS, 4.12.4])- can't use
instanceof
local classes
- least frequently used
- used anywhere like a local variable
- have access to enclosing instance iff in non-
static
context - can not contain
static
variables
Item 25: Limit source files to a single top-level class
Don't put multiple top-level classes or interfaces in a single source file.