Effective Java's third edition reshuffled its items. Number 5 used to cover resource lists; now it holds a new entry that promotes dependency injection over hardwiring resources. The Fragmented podcast covered this shift in a compact episode, but the principle underneath is one every Java and Android developer encounters daily: don't bake your collaborators into your class.
Quick take
- Item 5 is new in the third edition; the previous Item 5 moved to Item 6.
- Hardwiring blocks testing and composition.
- Dependency injection moves the collaborator choice outside the class.
- A DI framework helps but is not required for the pattern.
Why the item moved (and changed)
The third edition reorganised items around modern JVM practice. Whatever occupied slot 5 previously was less central than the principle that hardwiring resources inside a class is a design smell.
The principle predates the book. Promoting it to Item 5 reflects its pervasiveness in Android and JVM codebases: tests that won't run, services that can't be swapped, classes that resist reuse. Each traces back to a hardwiring failure.
What hardwiring looks like
Two shapes appear most often.
Static singleton:** A class reaches into a global registry for its dependency. Testing becomes tricky because the singleton is baked in at compile time.
Constructor new call:** A class instantiates a collaborator inside its constructor. The collaborator can't be swapped, so the class inherits its dependencies' configuration, lifecycle and failure modes.
Both mean the class owns its collaborator's choices—the wrong direction of dependency.
What injection looks like
The fix is simple: accept the collaborator via the constructor. The class no longer decides what to use; it declares what it needs.
That change unlocks three wins. The class becomes testable with a stub. It can swap collaborators at runtime. It can be reused across different contexts.
No framework required. A constructor parameter is sufficient.
When to inject: a decision checklist
Use this checklist when a class needs a collaborator:
- [ ] Is the collaborator an interface or abstraction (not a concrete class)?
- [ ] Does the class get the collaborator via constructor parameter?
- [ ] Can you swap the collaborator for a test double without touching the class?
- [ ] Is the instantiation point moved to a composition root?
If you answered yes to all four, the class is well-injected. If not, consider refactoring.
The framework question
DI frameworks automate wiring for large graphs. They add opinions about lifecycle scopes and provide compile-time safety in some cases.
But the pattern is independent. A class that takes its collaborator via constructor is well-injected even without a framework. Many small apps and libraries thrive without one.
Frameworks become valuable as the dependency graph grows complex, not because the principle requires them.
Worked example: a logging class
A class builds a logger via `LoggerFactory.getLogger(MyClass::class.java)` in its constructor. The class is clean but can't run unit tests without a real logger implementation.
Refactor: take `ILogger` as a constructor parameter. The class becomes logger-agnostic. Tests pass a stub. Production code passes the real logger at the composition root.
That single change—move `getLogger` out of the constructor—converts hardwired to injectable. Nothing else needs to change.
The same pattern applies to HTTP clients, database connections, analytics trackers and feature-flag services. Anywhere a class says `new Foo()`, a constructor parameter is the cleaner shape.
Why this matters beyond Item 5
Hardwiring is the original sin of object-oriented code. Tests become expensive. Composition becomes impossible. Refactors become risky.
Effective Java's choice to promote this from a passing note to a top-level item signals how frequently it appears in code reviews. Most "I can't test this" tickets trace to one collaborator being instantiated inside a constructor.
What to watch
DI tooling continues to evolve. The pattern itself is older than any current framework and will outlast the next one. The principle remains: any class that owns its collaborators' lifecycle is harder to test, reuse and maintain.
Bottom line: pass collaborators in. The rest follows.
Key takeaways
- Item 5 of v3 covers dependency injection; the previous Item 5 moved to Item 6.
- Hardwiring via static singletons or constructor new calls blocks testing.
- A constructor parameter is enough to make a class well-injected.
- Frameworks help with wiring but are not the principle itself.
Frequently asked questions
Is Item 5 in v3 the same as Item 5 in v2?
No. The new Item 5 covers dependency injection. The previous Item 5 moved to Item 6.
Do I need a DI framework for the principle to apply?
No. A constructor parameter is sufficient. Frameworks automate wiring for complex graphs.
What's the simplest refactor for hardwired collaborators?
Move the instantiation from inside the constructor to a constructor parameter.
Does injection work with concrete classes or only interfaces?
Interfaces or abstractions work best because they allow swapping implementations. Concrete classes can be injected too, but interfaces give you more flexibility.
What's a composition root?
The single place in your application where objects are assembled and dependencies are wired together. It's where `LoggerFactory.getLogger(…)` lives in the logging example above.