Effective Java - 3rd Edition Notes
- 02 - Creating and Destroying Objects
- 03 - Methods Common to All Objects
- 04 - Classes and Interfaces
- 05 - Generics
- 06 - Enums and Annotations
- 07 - Lambdas and Streams
- 08 - Methods
- 09 - General Programming
- 10 - Exceptions
- 11 - Concurrency
- 12 - Serialization
Chapter 02 - Creating and Destroying Objects
Item 1 - Consider static factory methods instead of constructors
- traditional vs. flexible way of object instantiation
- example of static factory method:
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
Advantages and disadvantages:
- (PRO) static factories have names, unlike constructors
- (PRO) static factories are not required to create a new object on each invocation
- such classes are called instance controlled
- enable singleton (Item 2) and non-instantiability (Item 3) guarantees
- allows immutable value class to guarantee no two instances exist
- forms the basis of Flyweight Pattern
- Enum types provide this guarantee
- (PRO) static methods can return an object of any subtype of their return type, unlike constructors
- this can lead to compact APIs
- lends itself to interface-based frameworks (Item 20)
- companion classes mostly obviated in Java 8
- default methods
- still some limitations which are dealt with in Java 9+
- (PRO) static factories allow the class of the returned object to vary from call to call as function of input params
- example: EnumSet
- backed by a long - RegularEnumSet
- backed by a long[] - JumboEnumSet
- (PRO) static factories do not require the class of returned object to exist when the class containing the method is written
- form the basis of service provider frameworks
- example: JDBC
- variants:
- dependency-injection frameworks (i.e. Spring, Guice)
- Bridge Pattern
- (CON) classes without public/protected constructors cannot be subclassed
- a blessing in disguise
- encourages composition over inheritance
- (CON) hard to find in documentation
- current JavaDoc limitations
Some common names for static factory methods
- from()
- of()
- valueOf()
- instance() or getInstance()
- getType()
- newType()
- type()
Avoid the reflex to provide a public constructor and consider static methods/factories.
Item 2 - Consider a builder when faced with many constructor params
- static factories and constructors share a limitation: they scale poorly with the increase of (optional) params
- traditional ways of dealing with this:
- telescoping constructor
- also scales poorly: hard to write with many params and even harder to read
- JavaBeans pattern
- allows inconsistency
- precludes immutability (Item 17)
- a better way: Builder pattern
- typically a static member class (Item 24)
- the client code is easy to write and, more importantly, easy to read
- easy to incorporate validity checks
- check on object fields after copying params from the builder (Item 50)
- failing check will throw an IllegalArgumentException (Item 72) with exception details (Item 75)
- well suited to class hierarchies
- generic builder with recursive type parameters (Item 30) can construct any subclass
- abstract self() simulates self-type which, combined with covariant return typing, obviates need for casting
- flexible
- disadvantages:
- cost of creating a builder
- more verbose than a telescoping constructor
- conclusion:
- almost always start with a builder in the first place
- especially so if we have more than a handful params
- client code much easier to read and write than telescoping constructors
- builder are much safer than JavaBeans
Item 3 - Enforce the singleton property when a private constructor or enum type
- singleton is a class that is instantiated exactly once
- making a class a singleton can make it difficult to test
- three common ways of implementing it:
- with public final field
- with static factory
- with a single-element enum (preferred)
- conclusion:
- if a singleton is indeed warranted, create it as a single-element enum
- well-defended against reflection
- solves serialization problems
Item 4 - Enforce non-instantiability with a private constructor
- occasionally, we want to write a class that is simply a grouping of static methods and static fields
- such utility classes have acquired a bad reputation due to their abuse in avoiding thinking in terms of objects but they have valid uses:
- group related methods on primitive values or arrays
- examples: java.util.Math or java.util.Arrays
- group static methods, including factories (Item 1)
- default methods are also available (providing we own the interface)
- group methods on a final class, since we can't put them in a subclass
- make such classes non-instantiable
- provide an explanatory comment
- throw an AssertionError from constructor instead of empty one, to guard against accidental constructions from within the class
- as a side-effect, this class is now effectively final
- the class cannot be subclassed as there are no available constructors
- still, it is good to document this a make the class itself final
Item 5 - Prefer dependency injection to hard-wiring resources
- do not use static utility methods or singletons to handle creations of class' resources
- these yield inflexible and untestable classes
- favour dependency injection by supplying the required resource parametrically
- we inject (pass) the dependency (resource) into the class that requires it
- testable
- flexible (esp. with Factory Method pattern)
- Supplier<T> is perfectly suited for representing factories
- methods that take this interface should typically constrain the factory's type parameter with a bounded wildcard type (Item 31)
- the client should be able to pass in a factory that requires any subtype of a specified type
- the manual dependency injection can be automatised with frameworks
Item 6 - Avoid creating unnecessary objects
- creating unnecessary objects can be avoided by using static factory methods (Item 1)
- some object creations are much more expensive than others
- it may be advisable to cache such objects
- example: regex matching
- immutable objects can trivially be reused
- other examples are Adapters (a.k.a. views)
- adapter is an object delegating to a backing object, providing an alternative interface
- has not state beyond the backing object thus provides a view of it
- example: keySet() in Map interface
- autoboxing can often subtly create unnecessary objects
- almost never create own object pools
- JVM gc will almost always outperform such pools
- exception to this: very expensive objects
- example: database connection objects
- counterpoint to this is defencive copying (Item 50)
Item 7 - Eliminate obsolete object references
- when relying on automatic gc, be wary of leaks
- example: a resizing-array stack that doesn't null out its references on pop()
- common sources of leaks
- classes that manage their own memory
- caches
- can be mitigated with WeakHashMap
- sophisticated caches might need to use java.lang.ref directly
- listeners and other callbacks
- APIs that register callbacks but don't deregister them explicitly
- can be mitigated with WeakHashMap
- conclusion: it very desirable to learn to anticipate such problems before they occur as they can be very costly to fix
Item 8 - Avoid finalizers and cleaners
- finalizers and cleaners are used to reclaim non-memory resources
- example: input/output streams, files, etc.
- they are not analogues of C++ destructors
- finalizers are unpredictable, dangerous and generally unnecessary
- cleaners are less dangerous than finalizers but still slow, unpredicatble and generally unnecessary
- disadvantages:
- spec makes no guarantee when they'll be executed
- their execution is a function of GC algorithm, thus JVM implementation
- never do anything time-critical in a finalizer or cleaner
- providing a finalizer may arbitrarily delay reclamation of class' instances
- cleaners are a bit better but still run under the control of GC so still the same applies
- any existing methods that claim to trigger finalization are decade-old traps
- examples: System.runFinalizerOnExit or Runtim.runFinalizerOnExit
- uncaught exception thrown during finalization is ignored and finalization of that object terminates
- object is potentially left in corrupted state
- normally, uncaught exception terminates the executing thread and dumps stacktrace but not if it occurs in finalizer
- cleaners do not suffer from this problem
- there is a severe performance GC penalty for using finalizers and cleaners
- finalizers: ~50x slower reclamation
- cleaners: ~5x slower reclamation, equal to finalizers if they're used to clean all instance of the class
- finalizers open up classes to finalizer attacks
- if an exception is thrown during finalization, the finalizer leaves the class unreclaimed
- attackers can exploit this and run code that shouldnt've existed
- to protect non-final classes against it - write a final finalize() that does nothing
- instead of using finalizers or classes simply use AutoCloseable
- require clients to invoke close() whenever instance is not required
- legitimate uses:
- act as a safety net for closeables
- think long and hard before doing so
- reclaim native peer objects
- conclusion: don't use cleaners, or in releases prior to Java 9, finalizers - except as a safety net or to terminate non-critical native resources. Even then, beware the indeterminacy and performance consequences
Item 9 - Prefer try-with-resources to try-finally
- Java libraries include many resources that must be closed manually by invoking close()
- examples: InputStream, OutputStream, java.sql.Connection, etc.
- closing objects is often overlooked by clients
- many use finalizers as safety net, with dire performance consequences (Item 8)
- historically, try-finally was the best way to guarantee a resource would be closed properly, even when facing exception or return
- while it doesn't look bad for a single resource, it doesn't scale well with the increase of resources required to be closed
- nested try-finally blocks stick out like a sore thumb
- it's tricky to get it right, even in JDK
- nested finally block complicate debugging
- try-with-resources suffers none of this issues
// try-with-resources on multiple resources - short and sweet
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
}
} catch (IOException e) {
// do something here
}
- shorter, more readable than try-finally
- provides far better diagnostics
- no exceptions are suppressed
- conclusion: always use try-with-resources when working with resources that must be closed
Chapter 03 - Methods Common to All Objects
Item 10 - Obey the general contract when overriding equals()
- overriding the equals() seems simple but there are many pitfalls and consequences can be dire
- easiest way to avoid problems is not override the method at all
- then, we fall back to object identity only - each object is equal only to itself
- do not override the method if:
- each instance of the class is inherently unique
- example: java.util.thread.Thread
- there is no need for the class to provide 'logical equality' test
- example: java.util.regex.Pattern
- superclass is already overriding equals() and superclass' behaviour is appropriate for the subclass
- example: most root classes from java.util.collection and subclasses of java.util.Map inherit equals() behaviour from their abstract implementations
- the class is (package-)private and we're certain equals() will never be invoked
- the risk-averse may throw an exception in the subclass if equals() is called
- the class is instance controlled (Item 1)
- do override the method if a class has a notion of logical equality that differs from mere object identity
- this is generally the case for value classes
- examples: java.util.Integer or java.util.String
- overriding the equals() implies adhering to its general contract of equivalence relation:
- (reflexivity) x.equals(x) == true, for x != null
- hard to violate unintentionally
- (symmetry) x.equals(y) == y.equals(x), for x, y != null
- example: compare ordinary and case insensitive strings
- (transitivity) x.equals(y) == y.equals(z) == x.equals(z), for x, y, z != null
- examples: subclass adds a new value component; java.util.Date and java.util.Timestamp
- easier to violate when using inheritance
- once this property is violated, subsequent fixes are likely to violate other properties
- fundamental problem of equivalence relations in OO languages => there is no way to extend an instantiable class and add a value component whilst preserving the relation!
- workaround: favour composition over inheritance
- (consistency) x.equals(y) == x.equals(y), for x, y != null
- example: 'java.util.URL'
- do not write equals() that depend on unreliable resources
- (non-nullity) x.equals(null) == false, for x != null
- hard to violate unintenionally, unless an exception is thrown instead of returning false
- violation of this contract means it is uncertain how other objects will behave when confronted with our object
- conclusions:
- rely on IDEs to generate the equals()
- if manual tuning is really necessary, pay attention to equivalence relation violation (write tests!) and performance
Item 11 - Always override hashCode() when overriding equals()
- violating this rule will violate the general contract for hashCode()
- it prevents proper functioning of hash-based collections
- general hashCode() contract:
- (consistency) x.hashCode() == x.hashCode(), for x != null
- (equality) x.equals(y) => (x.hashCode() == y.hashCode()), for x, y != null
- worst possible hash-code implementation is returning the same number
- degrades performance horrifically
- conclusions:
- it is mandatory to override hashCode() each time equals() is overridden
- failure to do so precludes correct functioning of the program (fallback on Object)
- obey the general hashCode() contract
- rely on the Object.hashCode(...) to compute the hash-code unless performance is very important
Item 12 - Always override toString()
- makes systems using the class easier to debug
- disadvantage: once specified, it's for-life
- clearly document intentions
- don't override toString() on
- static utility class (Item 4)
- enum types (Item 34)
- rely on IDE to create a good toString() implementation
Item 13 - Override clone() judiciously
- conclusions:
- do not use clone()
- use static factory to copy objects whenever possible
Item 14 - Consider implementing Comparable
- isn't inherited from Object type
- located in functional interface Comparable
- similar to equals() except it permits order comparisons in addition to simple equality comparisons
- implementing Comparable, class indicates its instances follow natural ordering
- it also makes it easy to apply various searching, sorting and extreme values computations on such a class
- example: java.util.String
- class automatically interoperates with a variety of generic algorithms and collection implementations
- small effort and code footprint to leverage existing (vast) capabilites
- general contract of compareTo() is very similar to equals:
- (reflexivity) (x.compareTo(y) == 0) => (sgn(x.compareTo(z) == sgn(y.compareTo(z)), for x, y, z != null
- (symmetry) sgn(x.compareTo(y)) == -sgn(y.compareTo(x)), for x, y != null
- helps imposing total order
- (transitivity) (x.compareTo(y) && y.compareTo(z)) > 0 => (x.compareTo(z)) > 0, for x, y, z != null
- helps imposing total order
- (optional consistency with equals) (x.compareTo(y) == 0) <=> (x.equals(y)), for x, y != null
- if violated, still will yield valid comparisons but will generally not guarantee obeying the general contract for collections and maps
- examples: java.util.BigDecimal in HashSet and TreeSet
- to compare object ref fields, invoke compareTo() recursively
- do not use relational operators in compareTo()
- use compare() on boxed primitive types
- alternately, use comparator fluent construction methods in Comparator interface
- do not use hashCode arithmetics-based comparisons
- though somewhat more performant, they are fraught with danger from integer overflow and FP arithmetic artifacts
- use the same techniques as in the bullet point above
- generally, same limitations and workarounds as for the equals() apply here
- exceptions may be thrown, however, without violating the contract
- conclusions:
- whenever a value class has sensible ordering, Comparable should be implemented
- easy sorting, searching and usage in comparison-based collections
- do not use relational comparison operators to determine the result of comparing two elements
- rely on static compare() in the boxed primitive types
- alternately, use comparator construction methods in Comparator interface
Chapter 04 - Classes and Interfaces
Item 15 - Minimise the accesibility of classes and members
- well-designed components:
- hide their internal data and other implementation details from other components
- thus, they cleanly separate their API from its implementation
- components are then oblivios to each other's workings
- this is encapsulation (information hiding) -> a fundamental tenet of software design
- importance of encapsulation:
- decouples the components comprising a system
- this allows for development/optimisation/usage/reasoning/modification in isolation
- speeds up development (parallelisation)
- reduces maintenance costs (easier reasoning/debugging/replacement)
- makes for effective performance tuning (isolation -> does not influence correctness of others)
- increases reuse (decoupled components usually provide use in contexts beyond original design)
- de-risks development (individual components may prove successful even if the system does not)
- Java encapsulation facilities:
- access control mechanism (private, protected, public keywords + default access)
- encapsulation rule of thumb: make each class/member as inaccessible as possible
- after carefully designing a class' public API, the reflex should be to make all other members private
- change design if you find yourself opening up the API too frequently
- there may be a better decomposition with higher decoupling
- some fields may leak into the exported API if the class implements Serializable interface
- huge increase in visibility when going from default to protected
- becomes a part of exported API (has to be supported forever)
- should be relatively rare
- overridden methods cannot have restrictive access level in the subclass than it is in the superclass
- fields a compile-time error
- a consequence of Liskov substition principle
- special rule -> if a class implements an interface, all implemented methods must be public
- opening up a class to facilitate testing is acceptable (to a degree)
- the less restrictive access must not be any higher than default access
- it is fine to make the code "worse" in order to cover it with tests (M. Feathers, "Working Effectively With Legacy Code")
- instance fields of public classes should rarely be public
- gives up :
- the ability to enforce invariants on the field
- the flexibility to change data structure
- not thread-safe
- same applies for static fields, except if they are also final
- exception: it's always wrong to expose a mutable data structure as public static final (e.g. Arrays, Lists...)
- for such instances make an accessor which defencively copies the data structure and make the field private
- Java 9 modules:
- module is a group of packages, much like a pakcage is group of classes
- it may explicitly export some of its packages (via export declarations in its module declaration module-info.java)
- public/protected members of unexported packages in a module are inaccessible outside the module
- within, they work as before
- using the module system allows us to share classes among packages within a module without making them visible to the entire world
- the need for this kind of sharing is relatively rar and can often be eliminated by rearranging classes within a package
- it is unclear if it will achieve widespread use outside of the JDK itself (where it's strictly enforced)
- summary:
- reduce accebility of program elements as much as possible
- do not expose mutable types as public static final fields
Item 16 - Favour accessors over public fields in public classes
- occasionaly, degenerate classes are rolled out such as this one:
class Point {
public double x;
public double y
}
- if confined to package-private or private access modifier, they can be very useful in reducing visual clutter and overhead
- making them more accessible would be dangerous and is not advised
- there are examples in JDK that violate this rule (java.awt.Point, java.awt.Dimension)
Item 17 - Minimise mutability
- immutable classes are classes whose instances cannot be modified
- all of the data in the object is fixed for the lifetime of the object
- e.g. java.lang.String, the boxed primitive classes, BigInteger and BigDecimal
- many reasons to use immutable classes -> easier to design, implement and use than mutable classes
- to make a class immutable, follow these 5 rules:
- don't provide mutators
- ensure that the class cannot be extended
- make all fields final
- make all fields private
- ensure exclusive access to any mutable components
- immutable classes are easier to realise using functional, rather than imperative approach
- immutable objects are simple
- has always exactly one state - the state in which it was created
- they are easier to use reliably
- immutable object are inherently thread-safe (they require no synchronisation)
- thus can be shared freely, promoting reuse
- no defencive copies necessary
- their internals can be shared freely
- immutable objects make great building blocks for other objects
- they also make great map keys or set elements
- immutable object provide atomicity for free
- the one disadvantage -> they require a separate object for each distinct value
- the problem is exacerbated if the object is a part of multistep transformation
- one way to solve this issue is by providing mutable 'companion classes'
- no method may produce an externally visible change in the object's state
- consider using lazy initialisation technique
- summary:
- classes should be immutable unless there is a very good reason to make them mutable
- if a class cannot be made immutable, limit its mutability as much as possible
- declare every field private final unless there is a good reason to do so otherwise
- constructors should create fully initialised objects with all their invariants established
Item 18 - Favour composition over inheritance
- this chapter applies to implementation inheritance, not interface inheritance
- inheritance is a powerful way to achieve code reuse
- can lead to fragile software if employed inappropriately
- safe to use:
- within a package (where sub- and super-class are under control of the same programmers)
- if a class specifically is designed and documented for inheritance
- inheritance violates encapsulation
- subclass depends on implementation details of the superclass
- both must evolve in tandem
- e.g. HashSet extension
- depending on superclass' method to implement your own can lead to fragility - as the superclass evolves, your code may break without any changes
- related cause of fragility is that their superclass can acquire new methods in subsequent releases
- e.g. Hashtable and Vector classes had to had their security holes fixed before being retrofitted to participate in Collections framework
- solution to these issues is the application of composition
- existing class becomes a component of the new one
- new class is free to forward calls to the old class where appropriate (forwarding methods)
- resulting classes are then rock solid:
- they don't depend on the implementation details of the existing class
- the forwarder-wrapper pattern is also known as the Decorator pattern
- this combination of composition and forwarding is also informally defined as delegation
- disadvantages of wrapper classes are few:
- not suited for use in callback frameworks (callbacks elude the wrapper - SELF problem)
- theoretical performance and memory impacts
- tedious to write forwarding classes
- inheritance is only appropriate for classes that pass the "is-a" test
- answer truthfully to question "If B extended A, is B really an A?"
- a number of obvious violations in JDK
- Stack extends Vector
- Properties extends Hashtable
- does the class contemplated to extends has any flaws in its API?
- summary:
- inheritance is powerful but problematic as it violates encapsulation
- appropriate only when a genuine subtype relationship exists between the sub- and super-class
- inheritance may lead to fragility
- use composition and forwarding instead to avoid this fragility
- wrapper classes are more robust and powerful than subclasses
Item 19 - Design and document for inheritance or else prohibit it
- what does it mean to design/document for inheritance?
- the class must document its self-use of overridable methods
- this is one special case where it's ok to document implementation detail (unfortunate side-effect of inheritance violating encapsulation)
- the class may have to provide hooks into its internal workings
- careful choosing of protected methods
- enables programmers to write efficient subclasses without undue pain
- the only way to test a class designed for inheritance is to write subclasses
- test before releasing the class
- constructors must not invoke overridable methods (directly or indirectly)
- safe to invoke private/final/static methods, none of which are overridable
- Cloneable or Serializable interfaces are especially difficult to design for inheritance
- use neither in such classes
- designing a class for inheritance requires great effort and places substantial limitations on the class
- occasionaly, it is clearly the right thing to do (e.g. abstract classes, interfaces or skeletal implementations)
- the opposite also holds (e.g. immutable classes)
- best approach is to prohibit subclassing in classes that are not designed and documented for safe subclassing
- prohibit inheritance on classes implementing an interface that captures its essence
- for other classes, ensure at least that the class never invokes any of its overridable methods and document this
- summary:
- designing a class for inheritance is hard
- document all self-use patterns
- export one or more protected methods
- strongly consider prohibiting inheritance altogether
Item 20 - Prefer interfaces to abstract classes
- two mechanisms to define a type that permits multiple implementations: interfaces and abstract classes
- existing classes can easily be retrofitted to implement a new interface
- they cannot, in general, be retrofitted with a new abstract class without doing collateral damage to the type hierarchy
- interfaces are ideal for defining mixins
- a mixin is a type thtat a class implements additionally to its 'primary type'
- abstract classes cannot be used to define mixins
- interfaces allow for the construction of non-hierarchical type frameworks
- they can prevent combinatorial explosion produced by abstract classes
- interfaces enable safe, powerful functionality enhancements