Classes and Interfaces
Item15: Minimize the accessibility of classes and members
Always hide internal details, and seperate APIs from implementation. It is called encapsulation.
Emcapsulation does not cause better performance, but can be operated in isolation. In my opinion, currently at the most time, performance, unless it is really influential, does not matter in fact. But the scaling or maintenance of codes does matter.
By using modifiers (private, default, protected and public), we could manage information hiding properly.
in class | in package | in subclass | in external package | |
---|---|---|---|---|
private | O | X | X | X |
default (or omitted) | O | O | X | X |
protected | O | O | O | X |
public | O | O | O | O |
Make each class or member as inaccessible as possible
Private and package-private (default) methods are part of implementation, and they would not influence exported APIs. While protected and public methods must be careful to treat, because they are just the APIs.
Private methods can be a little hard to do unit test, so that we can make it default, or package-private, because the implementation ans unit tests are in the same package, which means test code can visit the default classes or members directly.
Instances fields of public classes should rarely be public. Because by making them public, these objects could be modified from external operations unexpectively, which would lead to a disaster.
Public static fields or accessors to arrays are wrong, because arrays could be modified. Others which served as constants are acceptable.
By making something public, you give up the flexibility to modify it, and you need to support this API forever. So be careful when making things public.
Item16: Use accessor methods in public classes
// Bad design
public class Point {
public double x;
public double y;
}
// Good design
public class Point {
private double x;
private double y;
public getX() { return x; }
public getY() { return y; }
public setX(double x) { this.x = x; }
public setY(double y) { this.y = y; }
}
If a class is accessible outside its package, provide accessor methods.
If a class is private or package-private, we can make the fields public.
But final fields can be public without much harm.
Item17: Minimize mutability
Immutable classes are easier to design, implement and use than mutable classes.
- Don't provide methods that modify the object's state (mutator).
- Ensure the class can't be extended.
- Make all fields final.
- Make all fields private.
- Ensure exclusive access to any mutable components.
Do not modify the instance itself, but generate a new instance and return it instead.
Pros
Immutable objects are simple, thread-safe and can be shared freely.
It can catch frequently requested instances and provide static factory to improve performance.
Immutable objects can share their internals. They are great building blocks for other objects, and provide failure atomicity for free.
Cons
Immutable objects require a seperate object for each distinct value.
The performance problem can be magnificent if we perform a multistep operation that generates a new object at every step, but eventually discarding all other objects except the final result.
- Guess which multistep operations will be commonly required and to provide them as primitives.
- Provide a public mutable companion class to store temperate values.
How could a class guarantee not to be subclassed? Here are some alternatives:
- Make the class final
- make all of its constructors private or package-private and use static factory instead.
We do not need to make all the fields final in a class, which is a little too stronger. We can let it have some nonfatal fields to cache the results of expensive computations to improve performance.
Item18: Favor composition over inheritance
It is unsafe to use inheritance across different packages, except interface inheritance.
Inheritance violates encapsulation. Because the implementation of the superclass may be changed over time, and this would bring unexpected influence on subclasses. Also, the suoerclasses could add new mthods in following releases, which would have conflicts with existing methods in subclasses.
Use compostion instead of inheritance. We can reference an instance of the existing class in the private field of the new class. Each instance method in the new class invokes the corresponding method on the contaned instance of the existing class and returns the result. This is called forwarding.
// Wrapper class - uses composition in place of inheritance
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(Set<E> s) {
super(s);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
// Reusable forwarding class
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) { this.s = s; }
public void clear() { s.clear(); }
public boolean contains(Object o) { return s.contains(o); }
public boolean isEmpty() { return s.isEmpty(); }
public int size() { return s.size(); }
public Iterator<E> iterator() { return s.iterator(); }
public boolean add(E e) { return s.add(e); }
public boolean remove(Object o) { return s.remove(o); }
public boolean containsAll(Collection<?> c)
{ return s.containsAll(c); }
public boolean addAll(Collection<? extends E> c)
{ return s.addAll(c); }
public boolean removeAll(Collection<?> c)
{ return s.removeAll(c); }
public boolean retainAll(Collection<?> c)
{ return s.retainAll(c); }
public Object[] toArray() { return s.toArray(); }
public <T> T[] toArray(T[] a) { return s.toArray(a); }
@Override public boolean equals(Object o)
{ return s.equals(o); }
@Override public int hashCode() { return s.hashCode(); }
@Override public String toString() { return s.toString(); }
}
This design is robust and flexible, and it also supports to instrument any Set
implementation.
Set<Instant> times = new InstrumentedSet<>(new TreeSet<>(cmp));
Set<E> s = new InstrumentedSet<>(new HashSet<>(INIT_CAPACITY));
The InstrumentedSet
class is known as a wrapper class because each InstrumentedSet
instance contains (wrap) another Set
instance. Also known as Decorator pattern.
Cons
Not suitable for callback framework, because the wrapped object fails to pass the wrapper to itself (this
) but the wrapped object itself. The performance of the forwarding method and the memory footprint impact of wrapper objects have little infuence, which should not be worried about.
Be careful to use inheritance. We should use it only when a genunine subtype relationship exists between the subclass and the superclass.
Summary
Ingeritance violates encapsulation. The change of superclasses will affect subclasses. We can use a wrapper class and a forwarding class to replace it, in which the wrapper class is a composition of the forwarding class. Every operation is accepted by the forwarding class and passed to the wrapper class, and the forwarding class hides the implementation details, which involves any possible modifications of wrapper classes.
Item 19: Design and document for inheritance
For classes designed for inheritance, they must document their self-use of overridable methods. Also, they couuld be required to provide hooks into their internal workings for protected methods or fields.
Some rules:
- Write subclasses to test the superclass.
- Never make constructors invoke overridable methods. (because superclass constructors run before subclass constructors, and they will use the overridable methods mplemented in subclasses, which could involve uninitiated fileds and values)
Best solutions: prohibit subclassing in classes not designed d documented to be safely subclasses.
Item 20: Favor interfaces over abstract classes
Because Java only allows single inheritance, abstract classes and their subclasses are severely constrained.
Pros of interfaces:
- Existing classes can easily be retrofitted to implement a new interface
- Interfaces are ideal for defining mixins
- Interfaces allow for the construction of nonhierarchical type frameworks
- Interfaces enable safe, powerful functionality enhancements
Item 21: Design interfaces for posterity
Interfaces provide default methods after Java 8, but it does not mean we could modify interfaces easily and freely. It is not always possible to write a default method that maintains all invariants of every conceivable implementation.
The thing is: In the presence of default methods, existing implementations of an interface may compile without error or warning but fail at runtime.
So we must be very careful when designing interfaces.
Item 22: Use interfaces only to define types
When a class implements an interface, the interface serves as a type that can be used to refer to instances of the class. That a class implements an interface should therefore say something about what a client can do with instances of the class. It is inappropriate to define an interface for any other purpose.
Like constant interfaces, which include some constants in them, are poor examples for this item.
Item 23: Prefer class hierarchies to tagged classes
Sometimes classes have moew than one flavors of instances and contain a tag field to indicate this. This is definitely a poor design. Tagged classes are verbose, error-prone, and inefficient, and a tagged class is just a pallid imitation of a class hierarchy.
When we encounter an existing class with a tag field, we should consider refactoring it into a hierarchy.
Item 24: Favor static member classes over nonstatic
A nested class is a class defined within another class. A nested class should exist only to serve its enclosing class.
There are four kinds of nested classes: static member classes, nonstatic member classes, anonymous classes, and local classes. All but the first kind are known as inner classes.
- Static member classes: an ordinary class that happens to be declared inside another class and has access to all of the enclosing class's members, even those declared private.
- Nonstatic member classes: each instance of a nonstatic member class is implicitly associated with an enclosing instance of its containing class, and it is impossible to create an instance of a nonstatic member class without an enclosing instance.
The rule is: If you declare a member class that does not require access to an enclosing instance, always put the static modifier in its declaration.
- Anonymous classes: an anonymous class has no name, and it is not a member of its enclosing class. We can now use lambda expressions to replace them.
- Local classes: a local class can be declared practically anywhere a local variable can be declared and obeys the same scoping rules.
Item 25: Limit source files to a single top-level class
Defining multiple top-level classes in a source file makes it possible to provide multiple definitions for a class.
In this case, the order the source files passed to the complier matters.
Example here:
// Utensil.java
class Utensil {
static final String NAME = "pan";
}
class Dessert {
static final String NAME = "cake";
}
// Dessert.java
class Utensil {
static final String NAME = "pot";
}
class Dessert {
static final String NAME = "pie";
}
// Main.java
public class Main {
public static void main(String[] args) {
System.out.println(Utensil.NAME + Dessert.NAME);
}
In command line:
javac Main.java
javac Main.java Utensil.java
============================
output: pancake
javac Dessert.java Main.java
============================
output: potpie
This can be confusing. So never put multiple top-level classes or interfaces in a single source file.
Use this instead:
public class Test {
public static void main(String[] args) {
System.out.println(Utensil.NAME + Dessert.NAME);
}
private static class Utensil {
static final String NAME = "pan";
}
private static class Dessert {
static final String NAME = "cake";
}
}