Classes and Interfaces - Effective Java

Photo by Jess Bailey on Unsplash

Classes and Interfaces - Effective Java

·

9 min read

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:

  1. decouples the components
  2. faster parallel development, as isolated
  3. easy on maintenance
  4. enable effective performance tuning by finding inefficient parts
  5. increases and promotes reuse
  6. 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

  1. public classes should not expose mutable fields, immutable may be exposed (questionable)
  2. 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:

  1. easier to design, implement and use
  2. less prone to error and more secure

Five rules to make a class immutable:

  1. Don't provide methods that modify an object's state (mutators)
  2. Ensure class can't be extended (final class or private / package-private constructor or static factories)
  3. Make all fields final (also thread-safe), some helper or non-core fields may be non-final like caching
  4. Make all fields private (may have public final for constants of type primitive or immutable)
  5. 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:

  1. simple
  2. inherently thread-safe
  3. no synchronization required
  4. shared freely
  5. no need of defensive copies
  6. no need of clone() or copy constructor (String has copy constructor from early days, can't remove backward compatibility)
  7. great building blocks for other objects as robust
  8. consistent

Immutable objects disadvantages:

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

  1. Reusable forwarding classes -> stores the reference and contains only the forwarding methods.
  2. 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

  1. Since a class can only extend one class, existing classes can easily be retrofitted by implementing a new interface.
  2. Ideal for defining mixins. Mixin classes denotes the additional responsibility a class has to perform apart from primary type.
  3. 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:

  1. Interface : define the type, some default methods
  2. 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:

  1. cluttered boilerplate code
  2. reduced readability
  3. high memory footprint
  4. adding a new flavour is different and highly coupled implementation
  5. constructor must set tag and other fields correctly else program fails at runtime
  6. 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:

  1. static member class
  2. nonstatic member class
  3. anonymous class
  4. local class

All (except 1.) are called inner classes.

image.png Inner class usability diagram

Nested classes and some significances:

  1. static member classes
    • common use case - public helper class
  2. 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
  3. 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 type String that is initialized with a constant expression [JLS, 4.12.4])
    • can't use instanceof
  4. 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.