- 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
- traditional vs. flexible way of object instantiation
- example of static factory method:
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
- (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
- backed by a
- (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
from()
of()
valueOf()
instance()
orgetInstance()
getType()
newType()
type()
Avoid the reflex to provide a public constructor and consider static methods/factories.
- 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)
- telescoping constructor
- 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
- almost always start with a builder in the first place
- 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)
- with
- conclusion:
- if a singleton is indeed warranted, create it as a single-element enum
- well-defended against reflection
- solves serialization problems
- if a singleton is indeed warranted, create it as a single-element enum
- 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
orjava.util.Arrays
- examples:
- 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
- group related methods on primitive values or arrays
- 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
- 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
- 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()
inMap
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)
- when relying on automatic gc, be wary of leaks
- example: a resizing-array stack that doesn't null out its references on
pop()
- example: a resizing-array stack that doesn't null out its references on
- 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
- can be mitigated with
- 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
- 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
orRuntim.runFinalizerOnExit
- examples:
- 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
- spec makes no guarantee when they'll be executed
- instead of using finalizers or classes simply use
AutoCloseable
- require clients to invoke
close()
whenever instance is not required
- require clients to invoke
- legitimate uses:
- act as a safety net for closeables
- think long and hard before doing so
- reclaim native peer objects
- act as a safety net for closeables
- 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
- Java libraries include many resources that must be closed manually by invoking
close()
- examples:
InputStream
,OutputStream
,java.sql.Connection
, etc.
- examples:
- 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
- nested
- it's tricky to get it right, even in JDK
- nested
finally
block complicate debugging
- while it doesn't look bad for a single resource, it doesn't scale well with the increase of resources required to be closed
try-with-resources
suffers none of this issues- example:
// 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
- 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
- example:
- there is no need for the class to provide 'logical equality' test
- example:
java.util.regex.Pattern
- example:
- superclass is already overriding
equals()
and superclass' behaviour is appropriate for the subclass- example: most root classes from
java.util.collection
and subclasses ofjava.util.Map
inheritequals()
behaviour from their abstract implementations
- example: most root classes from
- 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 risk-averse may throw an exception in the subclass if
- the class is instance controlled (Item 1)
- example: enum types
- each instance of the class is inherently unique
- 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
orjava.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
andjava.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
- examples: subclass adds a new value component;
- (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
- hard to violate unintenionally, unless an exception is thrown instead of returning
- (reflexivity)
- 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
- rely on IDEs to generate the
- 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
- (consistency)
- worst possible hash-code implementation is returning the same number
- degrades performance horrifically
- conclusions:
- it is mandatory to override
hashCode()
each timeequals()
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
- it is mandatory to override
- 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
- conclusions:
- do not use
clone()
- use static factory to copy objects whenever possible
- do not use
- 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
- example:
- general contract of
compareTo()
is very similar toequals
:- (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
inHashSet
andTreeSet
- (reflexivity)
- 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
- use
- 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
- rely on static
- whenever a value class has sensible ordering,
- 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)
- access control mechanism (
- 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 alsofinal
- 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
- exception: it's always wrong to expose a mutable data structure as
- gives up :
- 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
- 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
)
- there are examples in JDK that violate this rule (
- making them more accessible would be dangerous and is not advised
- 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
andBigDecimal
- many reasons to use immutable classes -> easier to design, implement and use than mutable classes
- to make a class mutable, 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
- declare every field
- constructors should create fully initialised objects with all their invariants established
- e.g.
CountDownLatch
- e.g.
- 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
andVector
classes had to had their security holes fixed before being retrofitted to participate in Collections framework
- e.g.
- 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
extendsVector
Properties
extendsHashtable
- 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
- 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
orSerializable
interfaces are especially difficult to design for inheritance- use neither in such classes
- the class must document its self-use of overridable methods
- 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
- 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'
- e.g.
Comparable
- e.g.
- abstract classes cannot be used to define mixins
- a mixin is a type thtat a class implements additionally to its 'primary type'
- 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
- wrapper class idiom